jinye_huang f96f0257ab feat(poster_v2): 优化布局渲染效果
- split_vertical: 字号调大 (标题84/副标题34/正文32)
- hero_bottom: 从图片提取主色调,渐变范围350px更柔和
- 提示词优化: 小红书爆款公式 (悬念/数字/对比/情感/身份式)
- card_float: 卡片高度限制500-800px
- overlay_bottom: 亮点标签支持换行,间距优化
2025-12-10 15:33:53 +08:00

197 lines
6.8 KiB
Python

#!/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