#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 布局E: 悬浮卡片 适用场景: 酒店、精品推荐 """ 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 CardFloatLayout(BaseLayout): """悬浮卡片布局""" # 字号配置 TITLE_SIZE = 80 SUBTITLE_SIZE = 32 LIST_SIZE = 30 PRICE_SIZE = 80 SMALL_SIZE = 24 DETAIL_SIZE = 28 # 卡片配置 CARD_MARGIN = 40 CARD_PADDING = 32 CARD_RADIUS = 28 def calculate_content_height(self, content: PosterContent, theme: Theme) -> int: """计算卡片内容高度 (使用固定范围)""" # 基础高度 height = self.CARD_PADDING * 2 # 上下 padding # 标签 if content.label: height += 44 # 标题 (预估 1-2 行) height += 90 # 副标题 if content.subtitle: height += 60 # 装饰线 height += 28 # 特色 (最多2行) if content.features: rows = min((len(content.features) + 1) // 2, 2) height += rows * 48 + 16 # 详情 (最多2条) if content.details: height += min(len(content.details), 2) * 40 + 16 # 价格区域 height += 80 # 限制范围 return max(500, min(height, 800)) def generate(self, content: PosterContent, theme: Theme) -> Image.Image: """生成海报""" # 计算卡片高度 card_height = self.calculate_content_height(content, theme) card_y = self.height - card_height - 100 card_width = self.width - self.CARD_MARGIN * 2 # 创建渐变背景 canvas = self.create_gradient_background(theme) # 如果有真实图片 if content.image: img = content.image.copy().resize(self.size, Image.LANCZOS) canvas = img.convert("RGBA") # 卡片阴影 shadow = self.effect.create_shadow( (card_width, card_height), radius=self.CARD_RADIUS, blur=15, alpha=20, offset=(0, 8) ) shadow_x = self.CARD_MARGIN - 30 shadow_y = card_y - 22 canvas.paste(shadow, (shadow_x, shadow_y), shadow) draw = ImageDraw.Draw(canvas) # 卡片背景 self.shape.draw_rounded_rect(draw, (self.CARD_MARGIN, card_y, self.width - self.CARD_MARGIN, card_y + card_height), radius=self.CARD_RADIUS, fill=(255, 255, 255, 250)) # 颜色 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) detail_font = TextRenderer.get_body_font(self.DETAIL_SIZE) content_x = self.CARD_MARGIN + self.CARD_PADDING content_right = self.width - self.CARD_MARGIN - self.CARD_PADDING content_width = content_right - content_x cur_y = card_y + self.CARD_PADDING # === 标签 === 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 + 24, cur_y + 38), radius=19, fill=(*accent, 30)) draw.text((content_x + 12, cur_y + 8), content.label, font=small_font, fill=accent) cur_y += 46 # === 标题 (支持换行) === _, 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 + 12 # === 副标题 (支持换行) === if content.subtitle: _, sub_h = TextRenderer.draw_wrapped_text( draw, (content_x, cur_y), content.subtitle, subtitle_font, (*text_dark, 140), content_width, line_spacing=4 ) cur_y += max(sub_h, 32) + 20 # === 装饰线 === self.shape.draw_decorator_line(draw, (content_x, cur_y), 50, (*accent, 180)) cur_y += 28 # === 特色 (两列) === if content.features: col_width = (content_right - content_x) // 2 for i, feature in enumerate(content.features): col = i % 2 row = i // 2 fx = content_x + col * col_width fy = cur_y + row * 48 self.shape.draw_ellipse(draw, (fx, fy + 12, fx + 8, fy + 20), fill=(*accent, 180)) draw.text((fx + 16, fy), feature, font=list_font, fill=(*text_dark, 150)) rows = (len(content.features) + 1) // 2 cur_y += rows * 48 + 16 # === 详情 === if content.details: for detail in content.details: draw.text((content_x, cur_y), f"· {detail}", font=detail_font, fill=(*text_dark, 120)) cur_y += 40 cur_y += 16 # === 价格区 === if content.price: # 分隔线 self.shape.draw_line(draw, (content_x, cur_y), (content_right, cur_y), (*text_dark, 15), 1) cur_y += 20 price_w, price_h = TextRenderer.measure_text(content.price, price_font) draw.text((content_x, cur_y), content.price, font=price_font, fill=primary) # 后缀 (如果有) suffix = content.price_suffix or "" if suffix: draw.text((content_x + price_w + 8, cur_y + price_h - 28), suffix, font=small_font, fill=(*text_dark, 120)) # 查看详情按钮 (更明显的颜色) link_text = "查看详情" link_w, _ = TextRenderer.measure_text(link_text, subtitle_font) link_x = content_right - link_w - 28 self.shape.draw_rounded_rect(draw, (link_x - 18, cur_y + 8, content_right, cur_y + 54), radius=23, fill=(*accent, 180)) draw.text((link_x, cur_y + 17), link_text, font=subtitle_font, fill=(255, 255, 255)) return canvas