fix(poster_v2): 确保 Fabric JSON 和 PNG 位置一致
- 在所有布局类中添加 _add_object() 调用 - 渲染时同时记录元素位置到 _fabric_objects - V2 引擎直接从布局类获取 Fabric 对象 - 移除硬编码的 fallback 位置
This commit is contained in:
parent
5cc31fc733
commit
2d21647f10
Binary file not shown.
@ -110,11 +110,21 @@ class PosterSmartEngineV2(BaseEngine):
|
|||||||
|
|
||||||
theme = THEMES[theme_name]
|
theme = THEMES[theme_name]
|
||||||
|
|
||||||
# 4. 生成预览 PNG (无底图)
|
# 4. 生成预览 PNG 和 Fabric 对象
|
||||||
preview_base64 = self._generate_preview(content, layout, theme)
|
preview_base64, fabric_objects = self._generate_preview(content, layout, theme, image_url)
|
||||||
|
|
||||||
# 5. 生成 Fabric JSON
|
# 5. 构建 Fabric JSON
|
||||||
fabric_json = self._generate_fabric_json(content, layout, theme, image_url)
|
fabric_json = {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"canvas": {
|
||||||
|
"width": self.CANVAS_WIDTH,
|
||||||
|
"height": self.CANVAS_HEIGHT,
|
||||||
|
"backgroundColor": theme.secondary,
|
||||||
|
},
|
||||||
|
"layout": layout,
|
||||||
|
"theme": theme.name,
|
||||||
|
"objects": fabric_objects if fabric_objects else self._generate_fallback_objects(content, layout, theme, image_url),
|
||||||
|
}
|
||||||
|
|
||||||
return EngineResult(
|
return EngineResult(
|
||||||
success=True,
|
success=True,
|
||||||
@ -229,10 +239,12 @@ class PosterSmartEngineV2(BaseEngine):
|
|||||||
"suggested_theme": self.CATEGORY_THEME_MAP.get(category, "sunset"),
|
"suggested_theme": self.CATEGORY_THEME_MAP.get(category, "sunset"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _generate_preview(self, content: dict, layout: str, theme: Theme) -> str:
|
def _generate_preview(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> tuple:
|
||||||
"""生成预览 PNG (无底图)"""
|
"""生成预览 PNG (无底图) 并返回 Fabric 对象
|
||||||
factory = self._get_poster_factory()
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(preview_base64, fabric_objects)
|
||||||
|
"""
|
||||||
# 构建 PosterContent
|
# 构建 PosterContent
|
||||||
poster_content = PosterContent(
|
poster_content = PosterContent(
|
||||||
title=content.get("title", ""),
|
title=content.get("title", ""),
|
||||||
@ -247,38 +259,56 @@ class PosterSmartEngineV2(BaseEngine):
|
|||||||
image=None, # 无底图
|
image=None, # 无底图
|
||||||
)
|
)
|
||||||
|
|
||||||
# 生成海报
|
# 获取布局实例
|
||||||
poster_image = factory.generate_from_content(poster_content, layout=layout, theme=theme.name)
|
from poster_v2.layouts import (
|
||||||
|
HeroBottomLayout, OverlayCenterLayout, OverlayBottomLayout,
|
||||||
|
SplitVerticalLayout, CardFloatLayout
|
||||||
|
)
|
||||||
|
|
||||||
|
layout_map = {
|
||||||
|
"hero_bottom": HeroBottomLayout,
|
||||||
|
"overlay_center": OverlayCenterLayout,
|
||||||
|
"overlay_bottom": OverlayBottomLayout,
|
||||||
|
"split_vertical": SplitVerticalLayout,
|
||||||
|
"card_float": CardFloatLayout,
|
||||||
|
}
|
||||||
|
|
||||||
|
layout_class = layout_map.get(layout, HeroBottomLayout)
|
||||||
|
layout_instance = layout_class()
|
||||||
|
|
||||||
|
# 生成海报 (同时记录 Fabric 对象)
|
||||||
|
# 所有布局都支持 image_url 参数
|
||||||
|
try:
|
||||||
|
poster_image = layout_instance.generate(poster_content, theme, image_url=image_url)
|
||||||
|
except TypeError:
|
||||||
|
# 兼容旧版布局
|
||||||
|
poster_image = layout_instance.generate(poster_content, theme)
|
||||||
|
|
||||||
|
# 获取 Fabric 对象
|
||||||
|
fabric_objects = layout_instance.get_fabric_objects()
|
||||||
|
|
||||||
# 转 Base64
|
# 转 Base64
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
poster_image.convert("RGB").save(buffer, format="PNG")
|
poster_image.convert("RGB").save(buffer, format="PNG")
|
||||||
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
preview_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
def _generate_fabric_json(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> dict:
|
return preview_base64, fabric_objects
|
||||||
"""生成 Fabric.js JSON"""
|
|
||||||
objects = []
|
|
||||||
|
|
||||||
# 通用配置
|
def _generate_fallback_objects(self, content: dict, layout: str, theme: Theme, image_url: str = "") -> list:
|
||||||
|
"""生成后备 Fabric.js 对象 (当布局类没有提供时)"""
|
||||||
margin = 48
|
margin = 48
|
||||||
content_width = self.CANVAS_WIDTH - margin * 2
|
content_width = self.CANVAS_WIDTH - margin * 2
|
||||||
|
|
||||||
# 1. 背景图片占位
|
objects = [{
|
||||||
objects.append({
|
|
||||||
"id": "background_image",
|
"id": "background_image",
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"src": image_url or "",
|
"src": image_url or "",
|
||||||
"left": 0,
|
"left": 0, "top": 0,
|
||||||
"top": 0,
|
"width": self.CANVAS_WIDTH, "height": self.CANVAS_HEIGHT,
|
||||||
"width": self.CANVAS_WIDTH,
|
|
||||||
"height": self.CANVAS_HEIGHT,
|
|
||||||
"scaleX": 1,
|
|
||||||
"scaleY": 1,
|
|
||||||
"selectable": True,
|
"selectable": True,
|
||||||
"evented": True,
|
}]
|
||||||
})
|
|
||||||
|
|
||||||
# 2. 根据布局生成不同的结构
|
# 简化的后备对象
|
||||||
if layout == "hero_bottom":
|
if layout == "hero_bottom":
|
||||||
objects.extend(self._fabric_hero_bottom(content, theme, margin, content_width))
|
objects.extend(self._fabric_hero_bottom(content, theme, margin, content_width))
|
||||||
elif layout == "overlay_center":
|
elif layout == "overlay_center":
|
||||||
@ -290,17 +320,7 @@ class PosterSmartEngineV2(BaseEngine):
|
|||||||
elif layout == "card_float":
|
elif layout == "card_float":
|
||||||
objects.extend(self._fabric_card_float(content, theme, margin, content_width))
|
objects.extend(self._fabric_card_float(content, theme, margin, content_width))
|
||||||
|
|
||||||
return {
|
return objects
|
||||||
"version": "5.3.0",
|
|
||||||
"canvas": {
|
|
||||||
"width": self.CANVAS_WIDTH,
|
|
||||||
"height": self.CANVAS_HEIGHT,
|
|
||||||
"backgroundColor": theme.secondary,
|
|
||||||
},
|
|
||||||
"layout": layout,
|
|
||||||
"theme": theme.name,
|
|
||||||
"objects": objects,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _fabric_hero_bottom(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
|
def _fabric_hero_bottom(self, content: dict, theme: Theme, margin: int, content_width: int) -> List[dict]:
|
||||||
"""hero_bottom 布局的 Fabric 对象"""
|
"""hero_bottom 布局的 Fabric 对象"""
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -23,6 +23,7 @@ class BaseLayout(ABC):
|
|||||||
1. 计算内容区域位置和尺寸
|
1. 计算内容区域位置和尺寸
|
||||||
2. 协调各渲染器绘制内容
|
2. 协调各渲染器绘制内容
|
||||||
3. 生成最终海报图像
|
3. 生成最终海报图像
|
||||||
|
4. 记录元素位置 (用于 Fabric.js)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 默认尺寸 (小红书 3:4)
|
# 默认尺寸 (小红书 3:4)
|
||||||
@ -41,6 +42,21 @@ class BaseLayout(ABC):
|
|||||||
self.shape = ShapeRenderer()
|
self.shape = ShapeRenderer()
|
||||||
self.effect = EffectRenderer()
|
self.effect = EffectRenderer()
|
||||||
|
|
||||||
|
# 元素记录 (用于 Fabric.js)
|
||||||
|
self._fabric_objects = []
|
||||||
|
|
||||||
|
def _reset_objects(self):
|
||||||
|
"""重置元素记录"""
|
||||||
|
self._fabric_objects = []
|
||||||
|
|
||||||
|
def _add_object(self, obj: dict):
|
||||||
|
"""添加元素"""
|
||||||
|
self._fabric_objects.append(obj)
|
||||||
|
|
||||||
|
def get_fabric_objects(self) -> list:
|
||||||
|
"""获取 Fabric.js 对象列表"""
|
||||||
|
return self._fabric_objects.copy()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -64,8 +64,10 @@ class CardFloatLayout(BaseLayout):
|
|||||||
# 限制范围
|
# 限制范围
|
||||||
return max(500, min(height, 800))
|
return max(500, min(height, 800))
|
||||||
|
|
||||||
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image:
|
||||||
"""生成海报"""
|
"""生成海报"""
|
||||||
|
self._reset_objects()
|
||||||
|
|
||||||
# 计算卡片高度
|
# 计算卡片高度
|
||||||
card_height = self.calculate_content_height(content, theme)
|
card_height = self.calculate_content_height(content, theme)
|
||||||
card_y = self.height - card_height - 100
|
card_y = self.height - card_height - 100
|
||||||
@ -79,6 +81,16 @@ class CardFloatLayout(BaseLayout):
|
|||||||
img = content.image.copy().resize(self.size, Image.LANCZOS)
|
img = content.image.copy().resize(self.size, Image.LANCZOS)
|
||||||
canvas = img.convert("RGBA")
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
# 卡片阴影
|
# 卡片阴影
|
||||||
shadow = self.effect.create_shadow(
|
shadow = self.effect.create_shadow(
|
||||||
(card_width, card_height),
|
(card_width, card_height),
|
||||||
@ -95,6 +107,18 @@ class CardFloatLayout(BaseLayout):
|
|||||||
(self.CARD_MARGIN, card_y, self.width - self.CARD_MARGIN, card_y + card_height),
|
(self.CARD_MARGIN, card_y, self.width - self.CARD_MARGIN, card_y + card_height),
|
||||||
radius=self.CARD_RADIUS, fill=(255, 255, 255, 250))
|
radius=self.CARD_RADIUS, fill=(255, 255, 255, 250))
|
||||||
|
|
||||||
|
# 卡片 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "card_bg",
|
||||||
|
"type": "rect",
|
||||||
|
"left": self.CARD_MARGIN, "top": card_y,
|
||||||
|
"width": card_width, "height": card_height,
|
||||||
|
"rx": self.CARD_RADIUS, "ry": self.CARD_RADIUS,
|
||||||
|
"fill": "rgba(255,255,255,0.98)",
|
||||||
|
"shadow": "rgba(0,0,0,0.1) 0 8px 30px",
|
||||||
|
"selectable": False,
|
||||||
|
})
|
||||||
|
|
||||||
# 颜色
|
# 颜色
|
||||||
text_dark = theme.text_dark_rgb
|
text_dark = theme.text_dark_rgb
|
||||||
accent = theme.accent_rgb
|
accent = theme.accent_rgb
|
||||||
@ -128,6 +152,20 @@ class CardFloatLayout(BaseLayout):
|
|||||||
draw, (content_x, cur_y), content.title, title_font,
|
draw, (content_x, cur_y), content.title, title_font,
|
||||||
text_dark, content_width, line_spacing=4
|
text_dark, content_width, line_spacing=4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "title",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.title,
|
||||||
|
"left": content_x, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": self.TITLE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.text_dark,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += title_h + 12
|
cur_y += title_h + 12
|
||||||
|
|
||||||
# === 副标题 (支持换行) ===
|
# === 副标题 (支持换行) ===
|
||||||
@ -178,13 +216,26 @@ class CardFloatLayout(BaseLayout):
|
|||||||
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
||||||
draw.text((content_x, cur_y), content.price, font=price_font, fill=primary)
|
draw.text((content_x, cur_y), content.price, font=price_font, fill=primary)
|
||||||
|
|
||||||
|
# 价格 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "price",
|
||||||
|
"type": "text",
|
||||||
|
"text": content.price,
|
||||||
|
"left": content_x, "top": cur_y,
|
||||||
|
"fontSize": self.PRICE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.primary,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
# 后缀 (如果有)
|
# 后缀 (如果有)
|
||||||
suffix = content.price_suffix or ""
|
suffix = content.price_suffix or ""
|
||||||
if suffix:
|
if suffix:
|
||||||
draw.text((content_x + price_w + 8, cur_y + price_h - 28), suffix,
|
draw.text((content_x + price_w + 8, cur_y + price_h - 28), suffix,
|
||||||
font=small_font, fill=(*text_dark, 120))
|
font=small_font, fill=(*text_dark, 120))
|
||||||
|
|
||||||
# 查看详情按钮 (更明显的颜色)
|
# 查看详情按钮
|
||||||
link_text = "查看详情"
|
link_text = "查看详情"
|
||||||
link_w, _ = TextRenderer.measure_text(link_text, subtitle_font)
|
link_w, _ = TextRenderer.measure_text(link_text, subtitle_font)
|
||||||
link_x = content_right - link_w - 28
|
link_x = content_right - link_w - 28
|
||||||
|
|||||||
@ -50,8 +50,11 @@ class HeroBottomLayout(BaseLayout):
|
|||||||
|
|
||||||
return height
|
return height
|
||||||
|
|
||||||
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image:
|
||||||
"""生成海报 (与 design_samples 保持一致)"""
|
"""生成海报 (与 design_samples 保持一致)"""
|
||||||
|
# 重置 Fabric 对象列表
|
||||||
|
self._reset_objects()
|
||||||
|
|
||||||
# 计算内容高度
|
# 计算内容高度
|
||||||
content_height = self.calculate_content_height(content, theme)
|
content_height = self.calculate_content_height(content, theme)
|
||||||
content_y = self.height - content_height - 40
|
content_y = self.height - content_height - 40
|
||||||
@ -73,11 +76,9 @@ class HeroBottomLayout(BaseLayout):
|
|||||||
bottom_region = img.crop((0, int(self.height * 0.7), self.width, self.height))
|
bottom_region = img.crop((0, int(self.height * 0.7), self.width, self.height))
|
||||||
bottom_small = bottom_region.resize((50, 50), Image.LANCZOS)
|
bottom_small = bottom_region.resize((50, 50), Image.LANCZOS)
|
||||||
pixels = list(bottom_small.getdata())
|
pixels = list(bottom_small.getdata())
|
||||||
# 计算平均颜色
|
|
||||||
r = sum(p[0] for p in pixels) // len(pixels)
|
r = sum(p[0] for p in pixels) // len(pixels)
|
||||||
g = sum(p[1] for p in pixels) // len(pixels)
|
g = sum(p[1] for p in pixels) // len(pixels)
|
||||||
b = sum(p[2] for p in pixels) // len(pixels)
|
b = sum(p[2] for p in pixels) // len(pixels)
|
||||||
# 稍微加深以便文字可读
|
|
||||||
overlay_color = (max(0, r - 30), max(0, g - 30), max(0, b - 30))
|
overlay_color = (max(0, r - 30), max(0, g - 30), max(0, b - 30))
|
||||||
else:
|
else:
|
||||||
# 无图片: 渐变色
|
# 无图片: 渐变色
|
||||||
@ -88,23 +89,48 @@ class HeroBottomLayout(BaseLayout):
|
|||||||
)
|
)
|
||||||
canvas.paste(gradient, (0, 0))
|
canvas.paste(gradient, (0, 0))
|
||||||
|
|
||||||
# === 底部渐变遮罩 (根据图片颜色自适应) ===
|
# 记录背景图片 Fabric 对象
|
||||||
# 清晰范围更大,只遮盖底部文字区域
|
self._add_object({
|
||||||
|
"id": "background_image",
|
||||||
|
"type": "image",
|
||||||
|
"src": image_url,
|
||||||
|
"left": 0, "top": 0,
|
||||||
|
"width": self.width, "height": self.height,
|
||||||
|
"scaleX": 1, "scaleY": 1,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
# 实色底(仅覆盖文字区域)
|
# === 底部渐变遮罩 ===
|
||||||
solid_start = content_y + 50 # 更低的起点,让图片更清晰
|
solid_start = content_y + 50
|
||||||
solid_bg = Image.new("RGBA", (self.width, self.height - solid_start), (*overlay_color, 200))
|
solid_bg = Image.new("RGBA", (self.width, self.height - solid_start), (*overlay_color, 200))
|
||||||
canvas.paste(solid_bg, (0, solid_start))
|
canvas.paste(solid_bg, (0, solid_start))
|
||||||
|
|
||||||
# 渐变过渡(更大范围,更柔和)
|
fade_height = 350
|
||||||
fade_height = 350 # 更大的渐变范围
|
|
||||||
fade = self.effect.create_gradient(
|
fade = self.effect.create_gradient(
|
||||||
(self.width, fade_height),
|
(self.width, fade_height),
|
||||||
(*overlay_color, 0), # 顶部:完全透明
|
(*overlay_color, 0),
|
||||||
(*overlay_color, 200) # 底部:与实色底衔接
|
(*overlay_color, 200)
|
||||||
)
|
)
|
||||||
canvas.paste(fade, (0, solid_start - fade_height), fade)
|
canvas.paste(fade, (0, solid_start - fade_height), fade)
|
||||||
|
|
||||||
|
# 记录渐变遮罩 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "gradient_overlay",
|
||||||
|
"type": "rect",
|
||||||
|
"left": 0, "top": solid_start - fade_height,
|
||||||
|
"width": self.width, "height": self.height - solid_start + fade_height,
|
||||||
|
"fill": {
|
||||||
|
"type": "linear",
|
||||||
|
"coords": {"x1": 0, "y1": 0, "x2": 0, "y2": self.height - solid_start + fade_height},
|
||||||
|
"colorStops": [
|
||||||
|
{"offset": 0, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0)"},
|
||||||
|
{"offset": 0.5, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0.6)"},
|
||||||
|
{"offset": 1, "color": f"rgba({overlay_color[0]},{overlay_color[1]},{overlay_color[2]},0.85)"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"selectable": False,
|
||||||
|
})
|
||||||
|
|
||||||
# 重新获取 draw (因为 paste 后需要)
|
# 重新获取 draw (因为 paste 后需要)
|
||||||
draw = ImageDraw.Draw(canvas)
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
@ -127,6 +153,7 @@ class HeroBottomLayout(BaseLayout):
|
|||||||
content.title, content_width, base_size=self.TITLE_SIZE, min_size=56
|
content.title, content_width, base_size=self.TITLE_SIZE, min_size=56
|
||||||
)
|
)
|
||||||
title_w, title_h = TextRenderer.measure_text(content.title, adaptive_title_font)
|
title_w, title_h = TextRenderer.measure_text(content.title, adaptive_title_font)
|
||||||
|
title_font_size = adaptive_title_font.size
|
||||||
|
|
||||||
# 居中显示
|
# 居中显示
|
||||||
title_x = self.MARGIN + (content_width - title_w) // 2
|
title_x = self.MARGIN + (content_width - title_w) // 2
|
||||||
@ -134,14 +161,43 @@ class HeroBottomLayout(BaseLayout):
|
|||||||
draw, (title_x, cur_y), content.title, adaptive_title_font,
|
draw, (title_x, cur_y), content.title, adaptive_title_font,
|
||||||
theme.text, shadow_color=(0, 0, 0, 60), offset=(2, 2)
|
theme.text, shadow_color=(0, 0, 0, 60), offset=(2, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 记录标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "title",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.title,
|
||||||
|
"left": title_x, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": title_font_size,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.text,
|
||||||
|
"shadow": "rgba(0,0,0,0.3) 2px 2px 4px",
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += title_h + 14
|
cur_y += title_h + 14
|
||||||
|
|
||||||
# === 副标题 (支持换行) ===
|
# === 副标题 (支持换行) ===
|
||||||
|
subtitle_top = cur_y
|
||||||
if content.subtitle:
|
if content.subtitle:
|
||||||
_, sub_h = TextRenderer.draw_wrapped_text(
|
_, sub_h = TextRenderer.draw_wrapped_text(
|
||||||
draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font,
|
draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font,
|
||||||
(*text_white, 200), content_width, line_spacing=4
|
(*text_white, 200), content_width, line_spacing=4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 记录副标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "subtitle",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.subtitle,
|
||||||
|
"left": self.MARGIN, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": self.SUBTITLE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": f"rgba({text_white[0]},{text_white[1]},{text_white[2]},0.85)",
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += max(sub_h, 36) + 8
|
cur_y += max(sub_h, 36) + 8
|
||||||
|
|
||||||
# === 装饰线 ===
|
# === 装饰线 ===
|
||||||
@ -160,42 +216,92 @@ class HeroBottomLayout(BaseLayout):
|
|||||||
cur_y += 20
|
cur_y += 20
|
||||||
|
|
||||||
# === 价格 ===
|
# === 价格 ===
|
||||||
|
price_top = cur_y
|
||||||
if content.price:
|
if content.price:
|
||||||
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
||||||
|
|
||||||
# 计算后缀宽度
|
|
||||||
suffix = content.price_suffix or ""
|
suffix = content.price_suffix or ""
|
||||||
suffix_w = 0
|
suffix_w = 0
|
||||||
if suffix:
|
if suffix:
|
||||||
suffix_w, _ = TextRenderer.measure_text(suffix, suffix_font)
|
suffix_w, _ = TextRenderer.measure_text(suffix, suffix_font)
|
||||||
|
|
||||||
# 价格背景 (半透明主题色)
|
# 价格背景
|
||||||
bg_width = price_w + suffix_w + 40 if suffix else price_w + 24
|
bg_width = price_w + suffix_w + 40 if suffix else price_w + 24
|
||||||
self.shape.draw_rounded_rect(draw,
|
self.shape.draw_rounded_rect(draw,
|
||||||
(self.MARGIN - 12, cur_y - 6, self.MARGIN + bg_width, cur_y + price_h + 8),
|
(self.MARGIN - 12, cur_y - 6, self.MARGIN + bg_width, cur_y + price_h + 8),
|
||||||
radius=12, fill=(*accent, 40))
|
radius=12, fill=(*accent, 40))
|
||||||
|
|
||||||
# 价格文字 (白色,与背景形成对比)
|
# 记录价格背景 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "price_bg",
|
||||||
|
"type": "rect",
|
||||||
|
"left": self.MARGIN - 12, "top": cur_y - 6,
|
||||||
|
"width": bg_width + 12, "height": price_h + 14,
|
||||||
|
"rx": 12, "ry": 12,
|
||||||
|
"fill": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)",
|
||||||
|
"selectable": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 价格文字
|
||||||
draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_white)
|
draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_white)
|
||||||
|
|
||||||
# 后缀 (如果有)
|
# 记录价格 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "price",
|
||||||
|
"type": "text",
|
||||||
|
"text": content.price,
|
||||||
|
"left": self.MARGIN, "top": cur_y,
|
||||||
|
"fontSize": self.PRICE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.text,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 后缀
|
||||||
if suffix:
|
if suffix:
|
||||||
draw.text((self.MARGIN + price_w + 8, cur_y + price_h - 28), suffix,
|
draw.text((self.MARGIN + price_w + 8, cur_y + price_h - 28), suffix,
|
||||||
font=suffix_font, fill=(*text_white, 200))
|
font=suffix_font, fill=(*text_white, 200))
|
||||||
|
|
||||||
|
self._add_object({
|
||||||
|
"id": "price_suffix",
|
||||||
|
"type": "text",
|
||||||
|
"text": suffix,
|
||||||
|
"left": self.MARGIN + price_w + 8, "top": cur_y + price_h - 28,
|
||||||
|
"fontSize": 28,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": f"rgba({text_white[0]},{text_white[1]},{text_white[2]},0.85)",
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
# === 标签 ===
|
# === 标签 ===
|
||||||
if content.tags:
|
if content.tags:
|
||||||
tag_x = self.width - self.MARGIN
|
tag_x = self.width - self.MARGIN
|
||||||
for tag in reversed(content.tags):
|
tag_top = cur_y + 20
|
||||||
|
for i, tag in enumerate(reversed(content.tags)):
|
||||||
tag_text = f"#{tag}"
|
tag_text = f"#{tag}"
|
||||||
tag_w, _ = TextRenderer.measure_text(tag_text, tag_font)
|
tag_w, _ = TextRenderer.measure_text(tag_text, tag_font)
|
||||||
tag_x -= tag_w + 22
|
tag_x -= tag_w + 22
|
||||||
|
|
||||||
self.shape.draw_rounded_rect(draw,
|
self.shape.draw_rounded_rect(draw,
|
||||||
(tag_x, cur_y + 20, tag_x + tag_w + 16, cur_y + 54),
|
(tag_x, tag_top, tag_x + tag_w + 16, tag_top + 34),
|
||||||
radius=17, fill=(*accent, 35))
|
radius=17, fill=(*accent, 35))
|
||||||
draw.text((tag_x + 8, cur_y + 24), tag_text,
|
draw.text((tag_x + 8, tag_top + 4), tag_text,
|
||||||
font=tag_font, fill=text_white)
|
font=tag_font, fill=text_white)
|
||||||
|
|
||||||
|
# 记录标签 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": f"tag_{i}",
|
||||||
|
"type": "text",
|
||||||
|
"text": tag_text,
|
||||||
|
"left": tag_x + 8, "top": tag_top + 4,
|
||||||
|
"fontSize": self.TAG_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": theme.text,
|
||||||
|
"backgroundColor": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)",
|
||||||
|
"padding": 8,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
tag_x -= 8
|
tag_x -= 8
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
|
|||||||
@ -59,8 +59,10 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
|
|
||||||
return height
|
return height
|
||||||
|
|
||||||
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image:
|
||||||
"""生成海报"""
|
"""生成海报"""
|
||||||
|
self._reset_objects()
|
||||||
|
|
||||||
# 计算内容高度
|
# 计算内容高度
|
||||||
content_height = self.calculate_content_height(content, theme)
|
content_height = self.calculate_content_height(content, theme)
|
||||||
glass_y = self.height - content_height - 60
|
glass_y = self.height - content_height - 60
|
||||||
@ -74,6 +76,16 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
img = content.image.copy().resize(self.size, Image.LANCZOS)
|
img = content.image.copy().resize(self.size, Image.LANCZOS)
|
||||||
canvas = img.convert("RGBA")
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
# 毛玻璃效果
|
# 毛玻璃效果
|
||||||
glass = self.effect.create_frosted_glass(
|
glass = self.effect.create_frosted_glass(
|
||||||
canvas, (0, glass_y, self.width, self.height),
|
canvas, (0, glass_y, self.width, self.height),
|
||||||
@ -81,6 +93,16 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
)
|
)
|
||||||
canvas.paste(glass, (0, glass_y))
|
canvas.paste(glass, (0, glass_y))
|
||||||
|
|
||||||
|
# 毛玻璃区域 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "glass_bg",
|
||||||
|
"type": "rect",
|
||||||
|
"left": 0, "top": glass_y,
|
||||||
|
"width": self.width, "height": glass_height,
|
||||||
|
"fill": "rgba(255,255,255,0.92)",
|
||||||
|
"selectable": False,
|
||||||
|
})
|
||||||
|
|
||||||
draw = ImageDraw.Draw(canvas)
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
# 颜色
|
# 颜色
|
||||||
@ -108,6 +130,20 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
draw, (self.MARGIN, cur_y), content.title, title_font,
|
draw, (self.MARGIN, cur_y), content.title, title_font,
|
||||||
text_dark, content_width, line_spacing=4
|
text_dark, content_width, line_spacing=4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "title",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.title,
|
||||||
|
"left": self.MARGIN, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": self.TITLE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.text_dark,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += title_h + 12
|
cur_y += title_h + 12
|
||||||
|
|
||||||
# === 副标题 (支持换行) ===
|
# === 副标题 (支持换行) ===
|
||||||
@ -116,13 +152,27 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font,
|
draw, (self.MARGIN, cur_y), content.subtitle, subtitle_font,
|
||||||
(*text_dark, 150), content_width, line_spacing=4
|
(*text_dark, 150), content_width, line_spacing=4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 副标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "subtitle",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.subtitle,
|
||||||
|
"left": self.MARGIN, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": self.SUBTITLE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.7)",
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += max(sub_h, 34) + 16
|
cur_y += max(sub_h, 34) + 16
|
||||||
|
|
||||||
# === 亮点标签 (胶囊,自动换行) ===
|
# === 亮点标签 (胶囊,自动换行) ===
|
||||||
max_y = self.height - 150 # 预留底部价格区域
|
max_y = self.height - 150 # 预留底部价格区域
|
||||||
if content.highlights:
|
if content.highlights:
|
||||||
hl_x = self.MARGIN
|
hl_x = self.MARGIN
|
||||||
for hl in content.highlights[:4]: # 最多4个
|
hl_start_y = cur_y
|
||||||
|
for i, hl in enumerate(content.highlights[:4]): # 最多4个
|
||||||
hl_w, _ = TextRenderer.measure_text(hl, hl_font)
|
hl_w, _ = TextRenderer.measure_text(hl, hl_font)
|
||||||
# 换行检测
|
# 换行检测
|
||||||
if hl_x + hl_w + 36 > self.width - self.MARGIN:
|
if hl_x + hl_w + 36 > self.width - self.MARGIN:
|
||||||
@ -131,6 +181,20 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
if cur_y < max_y:
|
if cur_y < max_y:
|
||||||
self.shape.draw_capsule(draw, (hl_x, cur_y), hl_w, 40, (*accent, 50))
|
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)
|
draw.text((hl_x + 14, cur_y + 6), hl, font=hl_font, fill=text_dark)
|
||||||
|
|
||||||
|
# 亮点 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": f"highlight_{i}",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": hl,
|
||||||
|
"left": hl_x + 14, "top": cur_y + 6,
|
||||||
|
"fontSize": self.HL_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": theme.text_dark,
|
||||||
|
"backgroundColor": f"rgba({accent[0]},{accent[1]},{accent[2]},0.25)",
|
||||||
|
"padding": 10,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
hl_x += hl_w + 28
|
hl_x += hl_w + 28
|
||||||
cur_y += 44 + 16
|
cur_y += 44 + 16
|
||||||
|
|
||||||
@ -154,13 +218,26 @@ class OverlayBottomLayout(BaseLayout):
|
|||||||
if content.price:
|
if content.price:
|
||||||
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
||||||
|
|
||||||
# 价格背景 (更明显的颜色)
|
# 价格背景
|
||||||
self.shape.draw_rounded_rect(draw,
|
self.shape.draw_rounded_rect(draw,
|
||||||
(self.MARGIN - 10, cur_y - 6, self.MARGIN + price_w + 20, cur_y + price_h + 10),
|
(self.MARGIN - 10, cur_y - 6, self.MARGIN + price_w + 20, cur_y + price_h + 10),
|
||||||
radius=12, fill=(*accent, 60))
|
radius=12, fill=(*accent, 60))
|
||||||
|
|
||||||
draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_dark)
|
draw.text((self.MARGIN, cur_y), content.price, font=price_font, fill=text_dark)
|
||||||
|
|
||||||
|
# 价格 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "price",
|
||||||
|
"type": "text",
|
||||||
|
"text": content.price,
|
||||||
|
"left": self.MARGIN, "top": cur_y,
|
||||||
|
"fontSize": self.PRICE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.text_dark,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
# === 标签 ===
|
# === 标签 ===
|
||||||
if content.tags:
|
if content.tags:
|
||||||
tag_x = self.width - self.MARGIN
|
tag_x = self.width - self.MARGIN
|
||||||
|
|||||||
@ -41,8 +41,10 @@ class OverlayCenterLayout(BaseLayout):
|
|||||||
|
|
||||||
return height
|
return height
|
||||||
|
|
||||||
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image:
|
||||||
"""生成海报"""
|
"""生成海报"""
|
||||||
|
self._reset_objects()
|
||||||
|
|
||||||
# 创建渐变背景
|
# 创建渐变背景
|
||||||
canvas = self.create_gradient_background(theme)
|
canvas = self.create_gradient_background(theme)
|
||||||
|
|
||||||
@ -51,10 +53,30 @@ class OverlayCenterLayout(BaseLayout):
|
|||||||
img = content.image.copy().resize(self.size, Image.LANCZOS)
|
img = content.image.copy().resize(self.size, Image.LANCZOS)
|
||||||
canvas = img.convert("RGBA")
|
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)
|
canvas = self.effect.darken_overlay(canvas, alpha=60)
|
||||||
draw = ImageDraw.Draw(canvas)
|
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
|
text_white = theme.text_rgb
|
||||||
accent = theme.accent_rgb
|
accent = theme.accent_rgb
|
||||||
|
|
||||||
@ -85,6 +107,22 @@ class OverlayCenterLayout(BaseLayout):
|
|||||||
draw, (title_x, center_y), content.title, adaptive_font,
|
draw, (title_x, center_y), content.title, adaptive_font,
|
||||||
theme.text, shadow_color=(0, 0, 0, 80), offset=(3, 3)
|
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
|
center_y += title_h + 24
|
||||||
|
|
||||||
# === 副标题 (居中) ===
|
# === 副标题 (居中) ===
|
||||||
@ -93,6 +131,20 @@ class OverlayCenterLayout(BaseLayout):
|
|||||||
sub_x = (self.width - sub_w) // 2
|
sub_x = (self.width - sub_w) // 2
|
||||||
draw.text((sub_x, center_y), content.subtitle,
|
draw.text((sub_x, center_y), content.subtitle,
|
||||||
font=subtitle_font, fill=(*text_white, 200))
|
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
|
center_y += sub_h + 40
|
||||||
|
|
||||||
# === 装饰线 (副标题下方) ===
|
# === 装饰线 (副标题下方) ===
|
||||||
|
|||||||
@ -29,8 +29,10 @@ class SplitVerticalLayout(BaseLayout):
|
|||||||
"""计算内容区域高度 (此布局不需要动态高度)"""
|
"""计算内容区域高度 (此布局不需要动态高度)"""
|
||||||
return self.height
|
return self.height
|
||||||
|
|
||||||
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
def generate(self, content: PosterContent, theme: Theme, image_url: str = "") -> Image.Image:
|
||||||
"""生成海报"""
|
"""生成海报"""
|
||||||
|
self._reset_objects()
|
||||||
|
|
||||||
split = self.width // 2 # 左右各50%
|
split = self.width // 2 # 左右各50%
|
||||||
|
|
||||||
# 创建画布
|
# 创建画布
|
||||||
@ -51,6 +53,16 @@ class SplitVerticalLayout(BaseLayout):
|
|||||||
img = img.resize((split, self.height), Image.LANCZOS)
|
img = img.resize((split, self.height), Image.LANCZOS)
|
||||||
canvas.paste(img, (0, 0))
|
canvas.paste(img, (0, 0))
|
||||||
|
|
||||||
|
# 左侧图片 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "background_image",
|
||||||
|
"type": "image",
|
||||||
|
"src": image_url,
|
||||||
|
"left": 0, "top": 0,
|
||||||
|
"width": split, "height": self.height,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
draw = ImageDraw.Draw(canvas)
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
# 颜色
|
# 颜色
|
||||||
@ -84,10 +96,25 @@ class SplitVerticalLayout(BaseLayout):
|
|||||||
cur_y += 44
|
cur_y += 44
|
||||||
|
|
||||||
# === 标题 (支持换行) ===
|
# === 标题 (支持换行) ===
|
||||||
|
title_top = cur_y
|
||||||
_, title_h = TextRenderer.draw_wrapped_text(
|
_, title_h = TextRenderer.draw_wrapped_text(
|
||||||
draw, (content_x, cur_y), content.title, title_font,
|
draw, (content_x, cur_y), content.title, title_font,
|
||||||
text_dark, content_width, line_spacing=6
|
text_dark, content_width, line_spacing=6
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "title",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.title,
|
||||||
|
"left": content_x, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": self.TITLE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.text_dark,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += title_h + 16
|
cur_y += title_h + 16
|
||||||
|
|
||||||
# === 装饰线 (标题下方) ===
|
# === 装饰线 (标题下方) ===
|
||||||
@ -100,6 +127,19 @@ class SplitVerticalLayout(BaseLayout):
|
|||||||
draw, (content_x, cur_y), content.subtitle, subtitle_font,
|
draw, (content_x, cur_y), content.subtitle, subtitle_font,
|
||||||
(*text_dark, 130), content_width, line_spacing=8
|
(*text_dark, 130), content_width, line_spacing=8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 副标题 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "subtitle",
|
||||||
|
"type": "textbox",
|
||||||
|
"text": content.subtitle,
|
||||||
|
"left": content_x, "top": cur_y,
|
||||||
|
"width": content_width,
|
||||||
|
"fontSize": self.SUBTITLE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.6)",
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
cur_y += sub_h + 24
|
cur_y += sub_h + 24
|
||||||
|
|
||||||
# === 特色亮点 (横排标签) ===
|
# === 特色亮点 (横排标签) ===
|
||||||
@ -152,10 +192,34 @@ class SplitVerticalLayout(BaseLayout):
|
|||||||
|
|
||||||
draw.text((content_x, price_y), content.price, font=price_font, fill=primary)
|
draw.text((content_x, price_y), content.price, font=price_font, fill=primary)
|
||||||
|
|
||||||
|
# 价格 Fabric 对象
|
||||||
|
self._add_object({
|
||||||
|
"id": "price",
|
||||||
|
"type": "text",
|
||||||
|
"text": content.price,
|
||||||
|
"left": content_x, "top": price_y,
|
||||||
|
"fontSize": self.PRICE_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fill": theme.primary,
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
# 后缀 (如果有)
|
# 后缀 (如果有)
|
||||||
suffix = content.price_suffix or ""
|
suffix = content.price_suffix or ""
|
||||||
if suffix:
|
if suffix:
|
||||||
draw.text((content_x + price_w + 8, price_y + price_h - 28), suffix,
|
draw.text((content_x + price_w + 8, price_y + price_h - 28), suffix,
|
||||||
font=small_font, fill=(*text_dark, 120))
|
font=small_font, fill=(*text_dark, 120))
|
||||||
|
|
||||||
|
self._add_object({
|
||||||
|
"id": "price_suffix",
|
||||||
|
"type": "text",
|
||||||
|
"text": suffix,
|
||||||
|
"left": content_x + price_w + 8, "top": price_y + price_h - 28,
|
||||||
|
"fontSize": self.SMALL_SIZE,
|
||||||
|
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
||||||
|
"fill": f"rgba({text_dark[0]},{text_dark[1]},{text_dark[2]},0.6)",
|
||||||
|
"selectable": True,
|
||||||
|
})
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user