#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 布局C: 底部毛玻璃叠加 适用场景: 美食探店、带emoji """ 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 OverlayBottomLayout(BaseLayout): """底部毛玻璃布局""" # 字号配置 EMOJI_SIZE = 109 TITLE_SIZE = 88 SUBTITLE_SIZE = 34 PRICE_SIZE = 84 TAG_SIZE = 24 HL_SIZE = 28 DETAIL_SIZE = 30 def calculate_content_height(self, content: PosterContent, theme: Theme) -> int: """计算内容区域高度""" height = self.PADDING # emoji if content.emoji: height += self.EMOJI_SIZE + 16 # 标题 title_font = TextRenderer.get_title_font(self.TITLE_SIZE) _, title_h = TextRenderer.measure_text(content.title, title_font) height += title_h + 12 # 副标题 if content.subtitle: height += 34 + 24 # 亮点标签 if content.highlights: height += 44 + 20 # 装饰线 height += 24 # 详情 if content.details: height += len(content.details) * 44 + 16 # 价格 height += 84 + self.PADDING return height def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image: """生成海报""" self._reset_objects() # 计算内容高度 content_height = self.calculate_content_height(content, theme) glass_y = self.height - content_height - 60 glass_height = content_height + 60 # 创建渐变背景 canvas = self.create_gradient_background(theme) # 如果有真实图片 if content.image: img = content.image.copy().resize(self.size, Image.LANCZOS) canvas = img.convert("RGBA") # 背景图片 Fabric 对象 self._add_object({ "id": "background_image", "type": "image", "src": image_url, "left": 0, "top": 0, "width": self.width, "height": self.height, "selectable": True, }) # 毛玻璃效果 glass = self.effect.create_frosted_glass( canvas, (0, glass_y, self.width, self.height), blur_radius=25, overlay_alpha=230 ) canvas.paste(glass, (0, glass_y)) # 毛玻璃区域 Fabric 对象 self._add_object({ "id": "glass_bg", "type": "rect", "left": 0, "top": glass_y, "width": self.width, "height": glass_height, "fill": "rgba(255,255,255,0.92)", "selectable": False, }) draw = ImageDraw.Draw(canvas) # 颜色 text_dark = theme.text_dark_rgb accent = theme.accent_rgb # 字体 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) hl_font = TextRenderer.get_body_font(self.HL_SIZE) detail_font = TextRenderer.get_body_font(self.DETAIL_SIZE) tag_font = TextRenderer.get_body_font(self.TAG_SIZE) cur_y = glass_y + self.PADDING # === Emoji === if content.emoji: TextRenderer.draw_emoji(draw, (self.MARGIN, cur_y), content.emoji, self.EMOJI_SIZE) cur_y += self.EMOJI_SIZE + 16 # === 标题 (支持换行) === content_width = self.width - self.MARGIN * 2 _, title_h = TextRenderer.draw_wrapped_text( draw, (self.MARGIN, cur_y), content.title, title_font, text_dark, content_width, line_spacing=4 ) # 标题 Fabric 对象 self._add_object({ "id": "title", "type": "textbox", "text": content.title, "left": self.MARGIN, "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 + 12 # === 副标题 (支持换行) === if content.subtitle: _, sub_h = TextRenderer.draw_wrapped_text( draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font, (*text_dark, 150), 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_dark[0]},{text_dark[1]},{text_dark[2]},0.7)", "selectable": True, }) cur_y += max(sub_h, 34) + 16 # === 亮点标签 (胶囊,自动换行) === max_y = self.height - 150 # 预留底部价格区域 if content.highlights: hl_x = self.MARGIN hl_start_y = cur_y for i, hl in enumerate(content.highlights[:4]): # 最多4个 hl_w, _ = TextRenderer.measure_text(hl, hl_font) # 换行检测 if hl_x + hl_w + 36 > self.width - self.MARGIN: hl_x = self.MARGIN cur_y += 44 if cur_y < max_y: self.shape.draw_capsule(draw, (hl_x, cur_y), hl_w, 40, (*accent, 50)) draw.text((hl_x + 14, cur_y + 6), hl, font=hl_font, fill=text_dark) # 亮点 Fabric 对象 self._add_object({ "id": f"highlight_{i}", "type": "textbox", "text": hl, "left": hl_x + 14, "top": cur_y + 6, "fontSize": self.HL_SIZE, "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif", "fill": theme.text_dark, "backgroundColor": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)", "padding": 10, "selectable": True, }) hl_x += hl_w + 28 cur_y += 44 + 16 # === 装饰线 === if cur_y < max_y: self.shape.draw_line(draw, (self.MARGIN, cur_y), (self.width - self.MARGIN, cur_y), (*text_dark, 30), 1) cur_y += 20 # === 详情 === if content.details and cur_y < max_y: for detail in content.details[:2]: # 最多2条 if cur_y >= max_y: break draw.text((self.MARGIN, cur_y), f"· {detail}", font=detail_font, fill=(*text_dark, 160)) cur_y += 40 cur_y += 16 # 增加间距 # === 价格区 === if content.price: price_w, price_h = TextRenderer.measure_text(content.price, price_font) # 价格背景 self.shape.draw_rounded_rect(draw, (self.MARGIN - 10, cur_y - 6, self.MARGIN + price_w + 20, cur_y + price_h + 10), radius=12, fill=(*accent, 60)) draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_dark) # 价格 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_dark, "selectable": True, }) # === 标签 === if content.tags: tag_x = self.width - self.MARGIN for tag in reversed(content.tags): tag_text = f"#{tag}" tag_w, _ = TextRenderer.measure_text(tag_text, tag_font) tag_x -= tag_w + 16 # 添加标签背景 self.shape.draw_rounded_rect(draw, (tag_x - 6, cur_y + 16, tag_x + tag_w + 6, cur_y + 52), radius=18, fill=(*accent, 40)) draw.text((tag_x, cur_y + 20), tag_text, font=tag_font, fill=text_dark) tag_x -= 12 return canvas