- split_vertical: 字号调大 (标题84/副标题34/正文32) - hero_bottom: 从图片提取主色调,渐变范围350px更柔和 - 提示词优化: 小红书爆款公式 (悬念/数字/对比/情感/身份式) - card_float: 卡片高度限制500-800px - overlay_bottom: 亮点标签支持换行,间距优化
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
布局D: 左图右文
|
|
适用场景: 酒店、民宿、信息量大
|
|
"""
|
|
|
|
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 SplitVerticalLayout(BaseLayout):
|
|
"""左图右文布局 - 文艺气质"""
|
|
|
|
# 字号配置 (大字号,填满区域)
|
|
TITLE_SIZE = 84
|
|
SUBTITLE_SIZE = 34
|
|
BODY_SIZE = 32 # 正文描述
|
|
LIST_SIZE = 32
|
|
PRICE_SIZE = 80
|
|
SMALL_SIZE = 26
|
|
|
|
def calculate_content_height(self, content: PosterContent, theme: Theme) -> int:
|
|
"""计算内容区域高度 (此布局不需要动态高度)"""
|
|
return self.height
|
|
|
|
def generate(self, content: PosterContent, theme: Theme) -> Image.Image:
|
|
"""生成海报"""
|
|
split = self.width // 2 # 左右各50%
|
|
|
|
# 创建画布
|
|
canvas = self.create_canvas(theme.secondary_rgb)
|
|
draw = ImageDraw.Draw(canvas)
|
|
|
|
# 左侧渐变
|
|
left_gradient = self.effect.create_gradient(
|
|
(split, self.height),
|
|
theme.gradient_rgb[0],
|
|
theme.gradient_rgb[1]
|
|
)
|
|
canvas.paste(left_gradient, (0, 0))
|
|
|
|
# 如果有真实图片
|
|
if content.image:
|
|
img = content.image.copy()
|
|
img = img.resize((split, self.height), Image.LANCZOS)
|
|
canvas.paste(img, (0, 0))
|
|
|
|
draw = ImageDraw.Draw(canvas)
|
|
|
|
# 颜色
|
|
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)
|
|
body_font = TextRenderer.get_body_font(self.BODY_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)
|
|
|
|
# 右侧内容区
|
|
content_x = split + self.MARGIN
|
|
content_right = self.width - self.MARGIN
|
|
content_width = content_right - content_x - 10 # 可用宽度
|
|
cur_y = 70
|
|
max_content_y = self.height - 170 # 预留价格区域
|
|
|
|
# === 标签 ===
|
|
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 + 20, cur_y + 32),
|
|
radius=16, fill=(*accent, 180))
|
|
draw.text((content_x + 10, cur_y + 6), content.label,
|
|
font=small_font, fill=(255, 255, 255))
|
|
cur_y += 44
|
|
|
|
# === 标题 (支持换行) ===
|
|
_, title_h = TextRenderer.draw_wrapped_text(
|
|
draw, (content_x, cur_y), content.title, title_font,
|
|
text_dark, content_width, line_spacing=6
|
|
)
|
|
cur_y += title_h + 16
|
|
|
|
# === 装饰线 (标题下方) ===
|
|
self.shape.draw_decorator_line(draw, (content_x, cur_y), 50, (*accent, 180))
|
|
cur_y += 24
|
|
|
|
# === 副标题/描述 (文艺叙述) ===
|
|
if content.subtitle and cur_y < max_content_y:
|
|
_, sub_h = TextRenderer.draw_wrapped_text(
|
|
draw, (content_x, cur_y), content.subtitle, subtitle_font,
|
|
(*text_dark, 130), content_width, line_spacing=8
|
|
)
|
|
cur_y += sub_h + 24
|
|
|
|
# === 特色亮点 (横排标签) ===
|
|
if content.highlights and cur_y < max_content_y - 100:
|
|
hl_x = content_x
|
|
for hl in content.highlights[:3]: # 最多3个
|
|
hl_w, _ = TextRenderer.measure_text(hl, small_font)
|
|
if hl_x + hl_w + 24 > content_right:
|
|
break
|
|
self.shape.draw_rounded_rect(draw,
|
|
(hl_x, cur_y, hl_x + hl_w + 20, cur_y + 36),
|
|
radius=18, fill=(*accent, 50))
|
|
draw.text((hl_x + 10, cur_y + 6), hl,
|
|
font=small_font, fill=(*text_dark, 170))
|
|
hl_x += hl_w + 28
|
|
cur_y += 52
|
|
|
|
# === 特色列表 (竖排) ===
|
|
if content.features and cur_y < max_content_y - 60:
|
|
for feature in content.features[:4]: # 最多4个
|
|
if cur_y >= max_content_y - 60:
|
|
break
|
|
self.shape.draw_ellipse(draw,
|
|
(content_x, cur_y + 10, content_x + 8, cur_y + 18),
|
|
fill=(*accent, 160))
|
|
draw.text((content_x + 18, cur_y), feature,
|
|
font=list_font, fill=(*text_dark, 160))
|
|
cur_y += 40
|
|
cur_y += 16
|
|
|
|
# === 详情描述 (文艺叙述,填充空白) ===
|
|
if content.details and cur_y < max_content_y - 40:
|
|
for detail in content.details[:3]: # 最多3条
|
|
if cur_y >= max_content_y - 30:
|
|
break
|
|
_, detail_h = TextRenderer.draw_wrapped_text(
|
|
draw, (content_x, cur_y), f"「{detail}」",
|
|
body_font, (*text_dark, 110), content_width, line_spacing=6
|
|
)
|
|
cur_y += detail_h + 16
|
|
|
|
# === 价格 (底部) ===
|
|
if content.price:
|
|
price_y = self.height - 160
|
|
price_w, price_h = TextRenderer.measure_text(content.price, price_font)
|
|
|
|
# 分隔线
|
|
self.shape.draw_line(draw, (content_x, price_y - 24),
|
|
(content_right, price_y - 24), (*text_dark, 20), 1)
|
|
|
|
draw.text((content_x, price_y), content.price, font=price_font, fill=primary)
|
|
|
|
# 后缀 (如果有)
|
|
suffix = content.price_suffix or ""
|
|
if suffix:
|
|
draw.text((content_x + price_w + 8, price_y + price_h - 28), suffix,
|
|
font=small_font, fill=(*text_dark, 120))
|
|
|
|
return canvas
|