#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 布局A: 大图在上,文字在下 适用场景: 景点、美食、通用 """ 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 HeroBottomLayout(BaseLayout): """大图底部文字布局""" # 字号配置 TITLE_SIZE = 96 SUBTITLE_SIZE = 36 PRICE_SIZE = 88 DETAIL_SIZE = 32 TAG_SIZE = 24 def calculate_content_height(self, content: PosterContent, theme: Theme) -> int: """计算内容区域高度 (与 design_samples 保持一致)""" height = self.PADDING # 标题 title_font = TextRenderer.get_title_font(self.TITLE_SIZE) _, title_h = TextRenderer.measure_text(content.title, title_font) height += title_h + 14 # 副标题 (始终计算,即使为空也保留空间) height += 32 + 44 # 装饰线 height += 28 # 详情 if content.details: height += len(content.details) * 44 + 20 # 价格 price_font = TextRenderer.get_title_font(self.PRICE_SIZE) _, price_h = TextRenderer.measure_text("¥999", price_font) height += price_h + self.PADDING return height def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报 (与 design_samples 保持一致)""" # 重置 Fabric 对象列表 self._reset_objects() # 计算内容高度 content_height = self.calculate_content_height(content, theme) content_y = self.height - content_height - 40 # 创建画布 (淡色底) canvas = self.create_canvas(theme.secondary_rgb) draw = ImageDraw.Draw(canvas) # === 上半部分: 渐变或图片 === overlay_color = theme.primary_rgb # 默认使用主题色 if content.image: # 有图片: 图片覆盖整个画布 img = content.image.copy().convert("RGBA") img = img.resize(self.size, Image.LANCZOS) canvas.paste(img, (0, 0)) # 从图片底部区域提取主色调 bottom_region = img.crop((0, int(self.height * 0.7), self.width, self.height)) bottom_small = bottom_region.resize((50, 50), Image.LANCZOS) pixels = list(bottom_small.getdata()) r = sum(p[0] for p in pixels) // len(pixels) g = sum(p[1] for p in pixels) // len(pixels) b = sum(p[2] for p in pixels) // len(pixels) overlay_color = (max(0, r - 30), max(0, g - 30), max(0, b - 30)) else: # 无图片: 渐变色 gradient = self.effect.create_gradient( self.size, theme.gradient_rgb[0], theme.gradient_rgb[1] ) canvas.paste(gradient, (0, 0)) # 记录背景图片 Fabric 对象 self._add_object({ "id": "background_image", "type": "image", "src": image_url, "left": 0, "top": 0, "width": self.width, "height": self.height, "scaleX": 1, "scaleY": 1, "selectable": True, }) # === 底部渐变遮罩 === solid_start = content_y + 50 solid_bg = Image.new("RGBA", (self.width, self.height - solid_start), (*overlay_color, 200)) canvas.paste(solid_bg, (0, solid_start)) fade_height = 350 fade = self.effect.create_gradient( (self.width, fade_height), (*overlay_color, 0), (*overlay_color, 200) ) canvas.paste(fade, (0, solid_start - fade_height), fade) # 记录渐变遮罩 Fabric 对象 self._add_object({ "id": "gradient_overlay", "type": "rect", "left": 0, "top": solid_start - fade_height, "width": self.width, "height": self.height - solid_start + fade_height, "fill": { "type": "linear", "coords": {"x1": 0, "y1": 0, "x2": 0, "y2": self.height - solid_start + fade_height}, "colorStops": [ {"offset": 0, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0)"}, {"offset": 0.5, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0.6)"}, {"offset": 1, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0.85)"}, ] }, "selectable": False, }) # 重新获取 draw (因为 paste 后需要) draw = ImageDraw.Draw(canvas) # 字体 title_font = TextRenderer.get_title_font(self.TITLE_SIZE) subtitle_font = TextRenderer.get_body_font(self.SUBTITLE_SIZE) price_font = TextRenderer.get_title_font(self.PRICE_SIZE) detail_font = TextRenderer.get_body_font(self.DETAIL_SIZE) tag_font = TextRenderer.get_body_font(self.TAG_SIZE) suffix_font = TextRenderer.get_body_font(28) text_white = theme.text_rgb accent = theme.accent_rgb cur_y = content_y + self.PADDING content_width = self.width - self.MARGIN * 2 # === 标题 (自适应大小,尽量单行显示) === adaptive_title_font = TextRenderer.get_adaptive_title_font( content.title, content_width, base_size=self.TITLE_SIZE, min_size=56 ) title_w, title_h = TextRenderer.measure_text(content.title, adaptive_title_font) title_font_size = adaptive_title_font.size # 居中显示 title_x = self.MARGIN + (content_width - title_w) // 2 TextRenderer.draw_text_with_shadow( draw, (title_x, cur_y), content.title, adaptive_title_font, theme.text, shadow_color=(0, 0, 0, 60), offset=(2, 2) ) # 记录标题 Fabric 对象 self._add_object({ "id": "title", "type": "textbox", "text": content.title, "left": title_x, "top": cur_y, "width": content_width, "fontSize": title_font_size, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fontWeight": "bold", "fill": theme.text, "shadow": "rgba(0,0,0,0.3) 2px 2px 4px", "selectable": True, }) cur_y += title_h + 14 # === 副标题 (支持换行) === subtitle_top = cur_y if content.subtitle: _, sub_h = TextRenderer.draw_wrapped_text( draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font, (*text_white, 200), content_width, line_spacing=4 ) # 记录副标题 Fabric 对象 self._add_object({ "id": "subtitle", "type": "textbox", "text": content.subtitle, "left": self.MARGIN, "top": cur_y, "width": content_width, "fontSize": self.SUBTITLE_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fill": f"rgba({text_white[0]},{text_white[1]},{text_white[2]},0.85)", "selectable": True, }) cur_y += max(sub_h, 36) + 8 # === 装饰线 === self.shape.draw_decorator_line(draw, (self.MARGIN, cur_y), 60, (*accent, 200)) cur_y += 28 # === 详情 === if content.details: for detail in content.details: self.shape.draw_ellipse(draw, (self.MARGIN, cur_y + 12, self.MARGIN + 8, cur_y + 20), fill=(*accent, 180)) draw.text((self.MARGIN + 18, cur_y), detail, font=detail_font, fill=(*text_white, 180)) cur_y += 44 cur_y += 20 # === 价格 === price_top = cur_y if content.price: price_w, price_h = TextRenderer.measure_text(content.price, price_font) suffix = content.price_suffix or "" suffix_w = 0 if suffix: suffix_w, _ = TextRenderer.measure_text(suffix, suffix_font) # 价格背景 bg_width = price_w + suffix_w + 40 if suffix else price_w + 24 self.shape.draw_rounded_rect(draw, (self.MARGIN - 12, cur_y - 6, self.MARGIN + bg_width, cur_y + price_h + 8), radius=12, fill=(*accent, 40)) # 记录价格背景 Fabric 对象 self._add_object({ "id": "price_bg", "type": "rect", "left": self.MARGIN - 12, "top": cur_y - 6, "width": bg_width + 12, "height": price_h + 14, "rx": 12, "ry": 12, "fill": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)", "selectable": False, }) # 价格文字 draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_white) # 记录价格 Fabric 对象 self._add_object({ "id": "price", "type": "text", "text": content.price, "left": self.MARGIN, "top": cur_y, "fontSize": self.PRICE_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fontWeight": "bold", "fill": theme.text, "selectable": True, }) # 后缀 if suffix: draw.text((self.MARGIN + price_w + 8, cur_y + price_h - 28), suffix, font=suffix_font, fill=(*text_white, 200)) self._add_object({ "id": "price_suffix", "type": "text", "text": suffix, "left": self.MARGIN + price_w + 8, "top": cur_y + price_h - 28, "fontSize": 28, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fill": f"rgba({text_white[0]},{text_white[1]},{text_white[2]},0.85)", "selectable": True, }) # === 标签 === if content.tags: tag_x = self.width - self.MARGIN tag_top = cur_y + 20 for i, tag in enumerate(reversed(content.tags)): tag_text = f"#{tag}" tag_w, _ = TextRenderer.measure_text(tag_text, tag_font) tag_x -= tag_w + 22 self.shape.draw_rounded_rect(draw, (tag_x, tag_top, tag_x + tag_w + 16, tag_top + 34), radius=17, fill=(*accent, 35)) draw.text((tag_x + 8, tag_top + 4), tag_text, font=tag_font, fill=text_white) # 记录标签 Fabric 对象 self._add_object({ "id": f"tag_{i}", "type": "text", "text": tag_text, "left": tag_x + 8, "top": tag_top + 4, "fontSize": self.TAG_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fill": theme.text, "backgroundColor": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)", "padding": 8, "selectable": True, }) tag_x -= 8 return canvas