jinye_huang 2d21647f10 fix(poster_v2): 确保 Fabric JSON 和 PNG 位置一致
- 在所有布局类中添加 _add_object() 调用
- 渲染时同时记录元素位置到 _fabric_objects
- V2 引擎直接从布局类获取 Fabric 对象
- 移除硬编码的 fallback 位置
2025-12-10 16:28:25 +08:00

177 lines
6.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
布局B: 文字居中叠加在图片上
适用场景: 攻略、活动、大标题海报
"""
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 OverlayCenterLayout(BaseLayout):
"""居中叠加布局"""
# 字号配置
TITLE_SIZE = 100
SUBTITLE_SIZE = 36
TAG_SIZE = 26
def calculate_content_height(self, content: PosterContent, theme: Theme) -> int:
"""计算内容区域高度"""
height = 0
# 标题
title_font = TextRenderer.get_title_font(self.TITLE_SIZE)
_, title_h = TextRenderer.measure_text(content.title, title_font)
height += title_h + 32 # 含装饰线
# 副标题
if content.subtitle:
subtitle_font = TextRenderer.get_body_font(self.SUBTITLE_SIZE)
_, sub_h = TextRenderer.measure_text(content.subtitle, subtitle_font)
height += sub_h + 40
height += 80 # padding
return height
def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image:
"""生成海报"""
self._reset_objects()
# 创建渐变背景
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,
})
# 暗化叠加
canvas = self.effect.darken_overlay(canvas, alpha=60)
draw = ImageDraw.Draw(canvas)
# 暗化遮罩 Fabric 对象
self._add_object({
"id": "dark_overlay",
"type": "rect",
"left": 0, "top": 0,
"width": self.width, "height": self.height,
"fill": "rgba(0,0,0,0.35)",
"selectable": False,
})
text_white = theme.text_rgb
accent = theme.accent_rgb
# 字体
title_font = TextRenderer.get_title_font(self.TITLE_SIZE)
subtitle_font = TextRenderer.get_body_font(self.SUBTITLE_SIZE)
tag_font = TextRenderer.get_body_font(self.TAG_SIZE)
# 计算内容高度和位置
content_height = self.calculate_content_height(content, theme)
center_y = (self.height - content_height) // 2
# === 装饰线 (标题上方) ===
line_w = 80
self.shape.draw_decorator_line(
draw, ((self.width - line_w) // 2, center_y), line_w, (*accent, 200)
)
center_y += 32
# === 标题 (居中,自适应大小) ===
content_width = self.width - self.MARGIN * 2
adaptive_font = TextRenderer.get_adaptive_title_font(
content.title, content_width, base_size=self.TITLE_SIZE, min_size=64
)
title_w, title_h = TextRenderer.measure_text(content.title, adaptive_font)
title_x = (self.width - title_w) // 2
TextRenderer.draw_text_with_shadow(
draw, (title_x, center_y), content.title, adaptive_font,
theme.text, shadow_color=(0, 0, 0, 80), offset=(3, 3)
)
# 标题 Fabric 对象
self._add_object({
"id": "title",
"type": "textbox",
"text": content.title,
"left": title_x, "top": center_y,
"width": content_width,
"fontSize": adaptive_font.size,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fontWeight": "bold",
"fill": theme.text,
"textAlign": "center",
"shadow": "rgba(0,0,0,0.5) 3px 3px 6px",
"selectable": True,
})
center_y += title_h + 24
# === 副标题 (居中) ===
if content.subtitle:
sub_w, sub_h = TextRenderer.measure_text(content.subtitle, subtitle_font)
sub_x = (self.width - sub_w) // 2
draw.text((sub_x, center_y), content.subtitle,
font=subtitle_font, fill=(*text_white, 200))
# 副标题 Fabric 对象
self._add_object({
"id": "subtitle",
"type": "textbox",
"text": content.subtitle,
"left": sub_x, "top": center_y,
"width": content_width,
"fontSize": self.SUBTITLE_SIZE,
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
"fill": "rgba(255,255,255,0.85)",
"textAlign": "center",
"selectable": True,
})
center_y += sub_h + 40
# === 装饰线 (副标题下方) ===
self.shape.draw_decorator_line(
draw, ((self.width - line_w) // 2, center_y), line_w, (*accent, 200)
)
# === 底部标签 (只在有价格时显示) ===
if content.tags and content.price:
tag_y = self.height - 120
# 计算总宽度
total_w = sum(
TextRenderer.measure_text(f"#{t}", tag_font)[0] + 30
for t in content.tags
) - 30
tag_x = (self.width - total_w) // 2
for tag in content.tags:
tag_text = f"#{tag}"
tag_w, _ = TextRenderer.measure_text(tag_text, tag_font)
self.shape.draw_rounded_rect(draw,
(tag_x - 10, tag_y, tag_x + tag_w + 10, tag_y + 42),
radius=21, fill=(*accent, 50))
draw.text((tag_x, tag_y + 8), tag_text,
font=tag_font, fill=text_white)
tag_x += tag_w + 30
return canvas