#!/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 = 84 SUBTITLE_SIZE = 34 BODY_SIZE = 32 # 正文描述 LIST_SIZE = 32 PRICE_SIZE = 80 SMALL_SIZE = 26 def calculate_content_height(self, content: PosterContent, theme: Theme) -> int: """计算内容区域高度 (此布局不需要动态高度)""" return self.height def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报""" self._reset_objects() 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)) # 左侧图片 Fabric 对象 self._add_object({ "id": "background_image", "type": "image", "src": image_url, "left": 0, "top": 0, "width": split, "height": self.height, "selectable": True, }) 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) body_font = TextRenderer.get_body_font(self.BODY_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 = 70 max_content_y = self.height - 170 # 预留价格区域 # === 标签 === 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 + 32), radius=16, fill=(*accent, 180)) draw.text((content_x + 10, cur_y + 6), content.label, font=small_font, fill=(255, 255, 255)) cur_y += 44 # === 标题 (支持换行) === title_top = cur_y _, title_h = TextRenderer.draw_wrapped_text( draw, (content_x, cur_y), content.title, title_font, text_dark, content_width, line_spacing=6 ) # 标题 Fabric 对象 self._add_object({ "id": "title", "type": "textbox", "text": content.title, "left": content_x, "top": cur_y, "width": content_width, "fontSize": self.TITLE_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fontWeight": "bold", "fill": theme.text_dark, "selectable": True, }) cur_y += title_h + 16 # === 装饰线 (标题下方) === self.shape.draw_decorator_line(draw, (content_x, cur_y), 50, (*accent, 180)) cur_y += 24 # === 副标题/描述 (文艺叙述) === 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, 130), content_width, line_spacing=8 ) # 副标题 Fabric 对象 self._add_object({ "id": "subtitle", "type": "textbox", "text": content.subtitle, "left": content_x, "top": cur_y, "width": content_width, "fontSize": self.SUBTITLE_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.6)", "selectable": True, }) cur_y += sub_h + 24 # === 特色亮点 (横排标签) === if content.highlights and cur_y < max_content_y - 100: hl_x = content_x for hl in content.highlights[:3]: # 最多3个 hl_w, _ = TextRenderer.measure_text(hl, small_font) if hl_x + hl_w + 24 > content_right: break self.shape.draw_rounded_rect(draw, (hl_x, cur_y, hl_x + hl_w + 20, cur_y + 36), radius=18, fill=(*accent, 50)) draw.text((hl_x + 10, cur_y + 6), hl, font=small_font, fill=(*text_dark, 170)) hl_x += hl_w + 28 cur_y += 52 # === 特色列表 (竖排) === if content.features and cur_y < max_content_y - 60: for feature in content.features[:4]: # 最多4个 if cur_y >= max_content_y - 60: break self.shape.draw_ellipse(draw, (content_x, cur_y + 10, content_x + 8, cur_y + 18), fill=(*accent, 160)) draw.text((content_x + 18, cur_y), feature, font=list_font, fill=(*text_dark, 160)) cur_y += 40 cur_y += 16 # === 详情描述 (文艺叙述,填充空白) === if content.details and cur_y < max_content_y - 40: for detail in content.details[:3]: # 最多3条 if cur_y >= max_content_y - 30: break _, detail_h = TextRenderer.draw_wrapped_text( draw, (content_x, cur_y), f"「{detail}」", body_font, (*text_dark, 110), content_width, line_spacing=6 ) cur_y += detail_h + 16 # === 价格 (底部) === 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) # 价格 Fabric 对象 self._add_object({ "id": "price", "type": "text", "text": content.price, "left": content_x, "top": price_y, "fontSize": self.PRICE_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fontWeight": "bold", "fill": theme.primary, "selectable": True, }) # 后缀 (如果有) 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)) self._add_object({ "id": "price_suffix", "type": "text", "text": suffix, "left": content_x + price_w + 8, "top": price_y + price_h - 28, "fontSize": self.SMALL_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.6)", "selectable": True, }) return canvas