#!/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.Image: """生成海报""" # 计算内容高度 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") # 毛玻璃效果 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)) 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 ) 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 ) cur_y += max(sub_h, 34) + 16 # === 亮点标签 (胶囊,自动换行) === max_y = self.height - 150 # 预留底部价格区域 if content.highlights: hl_x = self.MARGIN for hl in 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) 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) # === 标签 === 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