134 lines
4.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
布局D: 左图右文
适用场景: 酒店民宿信息量大
"""
from typing import Tuple
from PIL import Image, ImageDraw
from .base import BaseLayout
from ..schemas.content import PosterContent
from ..schemas.theme import Theme
from ..renderers.text import TextRenderer
class SplitVerticalLayout(BaseLayout):
"""左图右文布局"""
# 字号配置
TITLE_SIZE = 72
SUBTITLE_SIZE = 28
LIST_SIZE = 26
PRICE_SIZE = 76
SMALL_SIZE = 22
def calculate_content_height(self, content: PosterContent, theme: Theme) -> int:
"""计算内容区域高度 (此布局不需要动态高度)"""
return self.height
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
"""生成海报"""
split = self.width // 2 # 左右各50%
# 创建画布
canvas = self.create_canvas(theme.secondary_rgb)
draw = ImageDraw.Draw(canvas)
# 左侧渐变
left_gradient = self.effect.create_gradient(
(split, self.height),
theme.gradient_rgb[0],
theme.gradient_rgb[1]
)
canvas.paste(left_gradient, (0, 0))
# 如果有真实图片
if content.image:
img = content.image.copy()
img = img.resize((split, self.height), Image.LANCZOS)
canvas.paste(img, (0, 0))
draw = ImageDraw.Draw(canvas)
# 颜色
text_dark = theme.text_dark_rgb
accent = theme.accent_rgb
primary = theme.primary_rgb
# 字体
title_font = TextRenderer.get_title_font(self.TITLE_SIZE)
subtitle_font = TextRenderer.get_body_font(self.SUBTITLE_SIZE)
list_font = TextRenderer.get_body_font(self.LIST_SIZE)
price_font = TextRenderer.get_title_font(self.PRICE_SIZE)
small_font = TextRenderer.get_body_font(self.SMALL_SIZE)
# 右侧内容区
content_x = split + self.MARGIN
content_right = self.width - self.MARGIN
content_width = content_right - content_x - 10 # 可用宽度
cur_y = 100
max_content_y = self.height - 200 # 预留价格区域
# === 标签 ===
if content.label:
label_w, _ = TextRenderer.measure_text(content.label, small_font)
self.shape.draw_rounded_rect(draw,
(content_x, cur_y, content_x + label_w + 20, cur_y + 36),
radius=18, fill=(*accent, 180))
draw.text((content_x + 10, cur_y + 7), content.label,
font=small_font, fill=(255, 255, 255))
cur_y += 50
# === 标题 (支持换行) ===
_, title_h = TextRenderer.draw_wrapped_text(
draw, (content_x, cur_y), content.title, title_font,
text_dark, content_width, line_spacing=4
)
cur_y += title_h + 16
# === 副标题 (支持换行) ===
if content.subtitle and cur_y < max_content_y:
_, sub_h = TextRenderer.draw_wrapped_text(
draw, (content_x, cur_y), content.subtitle, subtitle_font,
(*text_dark, 150), content_width, line_spacing=4
)
cur_y += sub_h + 12
# === 装饰线 ===
if cur_y < max_content_y - 100:
self.shape.draw_decorator_line(draw, (content_x, cur_y), 50, (*accent, 180))
cur_y += 28
# === 特色列表 ===
if content.features and cur_y < max_content_y - 60:
for feature in content.features[:3]: # 最多3个
if cur_y >= max_content_y - 60:
break
self.shape.draw_ellipse(draw,
(content_x, cur_y + 8, content_x + 8, cur_y + 16),
fill=(*accent, 180))
draw.text((content_x + 16, cur_y), feature,
font=list_font, fill=(*text_dark, 160))
cur_y += 38
# === 价格 (底部) ===
if content.price:
price_y = self.height - 160
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
# 分隔线
self.shape.draw_line(draw, (content_x, price_y - 24),
(content_right, price_y - 24), (*text_dark, 20), 1)
draw.text((content_x, price_y), content.price, font=price_font, fill=primary)
# 后缀 (如果有)
suffix = content.price_suffix or ""
if suffix:
draw.text((content_x + price_w + 8, price_y + price_h - 28), suffix,
font=small_font, fill=(*text_dark, 120))
return canvas