- 在所有布局类中添加 _add_object() 调用 - 渲染时同时记录元素位置到 _fabric_objects - V2 引擎直接从布局类获取 Fabric 对象 - 移除硬编码的 fallback 位置
177 lines
6.1 KiB
Python
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
|