TravelContentCreator/scripts/generate_design_samples.py
jinye_huang dcfd820ca4 feat(poster_v2): 智能海报生成引擎 v1.0
- 新增 PosterSmartEngine,AI 生成文案 + 海报渲染
- 5 种布局支持文本换行和自适应字体
- 修复按钮/标签颜色显示问题
- 优化渐变遮罩和内容区域计算
- Prompt 优化:标题格式为产品名+描述
2025-12-10 15:04:59 +08:00

918 lines
31 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成设计样例海报
验证设计资产库中的布局、配色、字体组合效果
"""
import os
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import colorsys
# 添加项目根目录到路径
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 输出目录
OUTPUT_DIR = PROJECT_ROOT / "result" / "design_samples"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 字体路径
FONT_DIR = PROJECT_ROOT / "assets" / "font"
# 可用字体映射
FONTS = {
"title_bold": FONT_DIR / "兰亭粗黑简.TTF",
"title_heavy": FONT_DIR / "兰亭特黑简 GBK.TTF",
"title_poster": FONT_DIR / "华康海报体简.ttc",
"title_cute": FONT_DIR / "字体管家棉花糖.TTF",
"body_regular": FONT_DIR / "腾祥麦黑简.TTF",
"handwrite": FONT_DIR / "邓玉二笔体.ttf",
"emoji": Path("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"),
}
# 配色方案 - 协调优化版
COLOR_SCHEMES = {
"ocean_soft": {
"primary": "#4A8B8B", # 深青
"secondary": "#E8F4F4", # 浅青白
"accent": "#E8956C", # 珊瑚橙
"text": "#FFFFFF",
"text_dark": "#2D5555",
"gradient": ["#8BC4C4", "#4A8B8B"],
},
"sunset_soft": {
"primary": "#D66853", # 日落橙
"secondary": "#FFF5E6", # 暖白
"accent": "#6BA08A", # 草绿
"text": "#FFFFFF",
"text_dark": "#4A3328",
"gradient": ["#F5D5A8", "#D66853"],
},
"peach_soft": {
"primary": "#D4918A", # 蜜桃
"secondary": "#FFF8F5", # 粉白
"accent": "#B85A54", # 深粉
"text": "#FFFFFF",
"text_dark": "#5A3D3D",
"gradient": ["#F8D8D4", "#D4918A"],
},
"mint_soft": {
"primary": "#6A9B88", # 薄荷
"secondary": "#F0F8F4", # 薄荷白
"accent": "#D4A05A", # 金黄
"text": "#FFFFFF",
"text_dark": "#3A5548",
"gradient": ["#B8D8C8", "#6A9B88"],
},
"latte": {
"primary": "#8B7355", # 咖啡
"secondary": "#FAF6F0", # 奶白
"accent": "#B8956A", # 焦糖
"text": "#FFFFFF",
"text_dark": "#4A3D30",
"gradient": ["#D8C8B0", "#8B7355"],
},
}
def hex_to_rgb(hex_color):
"""十六进制转 RGB"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def create_gradient(size, color1, color2, direction="vertical"):
"""创建渐变背景"""
width, height = size
img = Image.new("RGBA", size)
draw = ImageDraw.Draw(img)
r1, g1, b1 = hex_to_rgb(color1)
r2, g2, b2 = hex_to_rgb(color2)
if direction == "vertical":
for y in range(height):
ratio = y / height
r = int(r1 + (r2 - r1) * ratio)
g = int(g1 + (g2 - g1) * ratio)
b = int(b1 + (b2 - b1) * ratio)
draw.line([(0, y), (width, y)], fill=(r, g, b, 255))
return img
def create_frosted_glass(image, region, blur_radius=20, tint_color=None, opacity=0.85):
"""创建毛玻璃效果"""
x, y, w, h = region
# 裁剪区域
crop_box = (x, y, x + w, y + h)
cropped = image.crop(crop_box)
# 模糊
blurred = cropped.filter(ImageFilter.GaussianBlur(blur_radius))
# 添加颜色蒙版
if tint_color:
tint = Image.new("RGBA", (w, h), (*hex_to_rgb(tint_color), int(255 * opacity)))
blurred = Image.alpha_composite(blurred.convert("RGBA"), tint)
return blurred
def load_font(font_key, size):
"""加载字体"""
font_path = FONTS.get(font_key)
if font_path and font_path.exists():
try:
return ImageFont.truetype(str(font_path), size)
except:
pass
# 降级到默认字体
return ImageFont.load_default()
def draw_text_with_shadow(draw, pos, text, font, fill, shadow_color=(0, 0, 0, 128), offset=(2, 2)):
"""绘制带阴影的文字"""
x, y = pos
# 阴影
draw.text((x + offset[0], y + offset[1]), text, font=font, fill=shadow_color)
# 主文字
draw.text((x, y), text, font=font, fill=fill)
def generate_hero_bottom(output_name, color_scheme, content):
"""
布局A: 大图在上,文字在下 (动态内容区域)
"""
width, height = 1080, 1440
colors = COLOR_SCHEMES[color_scheme]
MARGIN = 48
PADDING = 32
# 字体 - 更大 (不换行)
title_font = load_font("title_bold", 96)
subtitle_font = load_font("body_regular", 36)
price_font = load_font("title_bold", 88)
tag_font = load_font("body_regular", 24)
detail_font = load_font("body_regular", 32)
suffix_font = load_font("body_regular", 28)
# === 先计算内容高度 ===
temp_img = Image.new("RGBA", (1, 1))
temp_draw = ImageDraw.Draw(temp_img)
content_height = PADDING # 顶部padding
# 标题高度
title = content.get("title", "正佳极地海洋世界")
title_bbox = temp_draw.textbbox((0, 0), title, font=title_font)
content_height += (title_bbox[3] - title_bbox[1]) + 14
# 副标题高度
content_height += 32 + 44 # 副标题 + 间距
# 装饰线
content_height += 28
# 详情高度
details = content.get("details", [])
if details:
content_height += len(details) * 44 + 20
# 价格高度
price_bbox = temp_draw.textbbox((0, 0), "¥999", font=price_font)
content_height += (price_bbox[3] - price_bbox[1]) + PADDING
# === 画布 ===
canvas = Image.new("RGBA", (width, height), hex_to_rgb(colors["secondary"]))
draw = ImageDraw.Draw(canvas)
# 动态计算内容区域起点
content_y = height - content_height - 40
# 上半部分渐变
gradient = create_gradient((width, content_y + 60), colors["gradient"][0], colors["gradient"][1])
canvas.paste(gradient, (0, 0))
# 底部过渡 (贴合内容区域)
overlay = create_gradient(
(width, content_height + 100),
colors["primary"] + "00",
colors["primary"] + "EE",
)
canvas.paste(overlay, (0, content_y - 60), overlay)
text_white = hex_to_rgb(colors["text"])
accent_color = hex_to_rgb(colors["accent"])
cur_y = content_y + PADDING
# === 标题 ===
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_h = title_bbox[3] - title_bbox[1]
draw_text_with_shadow(draw, (MARGIN, cur_y), title, title_font,
colors["text"], shadow_color=(0, 0, 0, 60), offset=(2, 2))
cur_y += title_h + 14
# === 副标题 ===
subtitle = content.get("subtitle", "周末带娃去的,真的很棒")
draw.text((MARGIN, cur_y), subtitle, font=subtitle_font, fill=(*text_white, 200))
cur_y += 44
# === 分隔装饰线 ===
draw.rounded_rectangle([MARGIN, cur_y, MARGIN + 60, cur_y + 4], radius=2, fill=(*accent_color, 200))
cur_y += 28
# === 产品详情 ===
if details:
for detail in details:
draw.ellipse([MARGIN, cur_y + 12, MARGIN + 8, cur_y + 20], fill=(*accent_color, 180))
draw.text((MARGIN + 18, cur_y), detail, font=detail_font, fill=(*text_white, 180))
cur_y += 44
cur_y += 20
# === 价格区域 ===
price = content.get("price", "¥199")
price_bbox = draw.textbbox((0, 0), price, font=price_font)
price_w = price_bbox[2] - price_bbox[0]
price_h = price_bbox[3] - price_bbox[1]
draw.rounded_rectangle(
[MARGIN - 12, cur_y - 8, MARGIN + price_w + 90, cur_y + price_h + 12],
radius=16, fill=(*text_white, 15)
)
draw.text((MARGIN, cur_y), price, font=price_font, fill=colors["text"])
draw.text((MARGIN + price_w + 10, cur_y + price_h - 32), "/人", font=suffix_font, fill=(*text_white, 160))
# === 标签 ===
tags = content.get("tags", ["周末好去处", "亲子游"])
tag_x = width - MARGIN
for tag in reversed(tags):
tag_bbox = draw.textbbox((0, 0), f"#{tag}", font=tag_font)
tag_w = tag_bbox[2] - tag_bbox[0]
tag_x -= tag_w + 22
draw.rounded_rectangle([tag_x, cur_y + 20, tag_x + tag_w + 16, cur_y + 54], radius=17, fill=(*text_white, 30))
draw.text((tag_x + 8, cur_y + 24), f"#{tag}", font=tag_font, fill=(*text_white, 200))
tag_x -= 8
# 保存
output_path = OUTPUT_DIR / f"{output_name}.png"
canvas.save(output_path)
print(f"✓ 生成: {output_path}")
return output_path
def generate_overlay_bottom(output_name, color_scheme, content):
"""
布局C: 文字叠加在底部 (动态内容区域)
"""
width, height = 1080, 1440
colors = COLOR_SCHEMES[color_scheme]
MARGIN = 48
PADDING = 36
# 字体 - 更大 (不换行)
title_font = load_font("title_bold", 88)
subtitle_font = load_font("body_regular", 34)
price_font = load_font("title_bold", 84)
tag_font = load_font("body_regular", 24)
hl_font = load_font("body_regular", 28)
detail_font = load_font("body_regular", 30)
# === 先计算内容高度 ===
temp_img = Image.new("RGBA", (1, 1))
temp_draw = ImageDraw.Draw(temp_img)
content_height = PADDING
# Emoji
emoji = content.get("emoji")
if emoji:
content_height += 92
# 标题
title = content.get("title", "发现一家超棒的店")
title_bbox = temp_draw.textbbox((0, 0), title, font=title_font)
content_height += (title_bbox[3] - title_bbox[1]) + 14
# 副标题
content_height += 30 + 48
# 亮点标签
highlights = content.get("highlights", [])
if highlights:
content_height += 40 + 20 # 标签行 + 分隔线
# 分隔线
content_height += 24
# 详情
details = content.get("details", [])
if details:
content_height += len(details) * 42 + 16
# 价格
price_bbox = temp_draw.textbbox((0, 0), "¥999", font=price_font)
content_height += (price_bbox[3] - price_bbox[1]) + PADDING
# === 渐变背景 ===
canvas = create_gradient((width, height), colors["gradient"][0], colors["gradient"][1])
draw = ImageDraw.Draw(canvas)
# 动态毛玻璃区域
glass_y = height - content_height - 32
glass_height = height - glass_y
glass_region = canvas.crop((0, glass_y, width, height))
glass_blurred = glass_region.filter(ImageFilter.GaussianBlur(25))
white_overlay = Image.new("RGBA", (width, glass_height), (255, 255, 255, 230))
glass_final = Image.alpha_composite(glass_blurred.convert("RGBA"), white_overlay)
canvas.paste(glass_final, (0, glass_y))
draw = ImageDraw.Draw(canvas)
text_color = hex_to_rgb(colors.get("text_dark", colors["primary"]))
accent_color = hex_to_rgb(colors["accent"])
cur_y = glass_y + PADDING
# === Emoji ===
if emoji:
emoji_font = ImageFont.truetype('/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf', 109)
draw.text((MARGIN, cur_y - 12), emoji, font=emoji_font, embedded_color=True)
cur_y += 92
# === 标题 ===
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_h = title_bbox[3] - title_bbox[1]
draw.text((MARGIN, cur_y), title, font=title_font, fill=text_color)
cur_y += title_h + 14
# === 副标题 ===
subtitle = content.get("subtitle", "朋友推荐的,果然没让我失望")
draw.text((MARGIN, cur_y), subtitle, font=subtitle_font, fill=(*text_color, 150))
cur_y += 48
# === 亮点标签 ===
if highlights:
hl_x = MARGIN
for hl in highlights:
hl_bbox = draw.textbbox((0, 0), hl, font=hl_font)
hl_w = hl_bbox[2] - hl_bbox[0]
draw.rounded_rectangle([hl_x, cur_y, hl_x + hl_w + 22, cur_y + 38], radius=19, fill=(*accent_color, 25))
draw.text((hl_x + 11, cur_y + 7), hl, font=hl_font, fill=(*text_color, 175))
hl_x += hl_w + 34
cur_y += 58
# === 分隔线 ===
draw.line([(MARGIN, cur_y), (width - MARGIN, cur_y)], fill=(*text_color, 20), width=1)
cur_y += 24
# === 产品详情 ===
if details:
for detail in details:
draw.ellipse([MARGIN, cur_y + 10, MARGIN + 8, cur_y + 18], fill=(*accent_color, 160))
draw.text((MARGIN + 18, cur_y), detail, font=detail_font, fill=(*text_color, 150))
cur_y += 42
cur_y += 16
# === 价格区域 ===
price = content.get("price", "¥88")
price_bbox = draw.textbbox((0, 0), price, font=price_font)
price_w = price_bbox[2] - price_bbox[0]
price_h = price_bbox[3] - price_bbox[1]
draw.rounded_rectangle(
[MARGIN - 10, cur_y - 8, MARGIN + price_w + 100, cur_y + price_h + 16],
radius=14, fill=(*hex_to_rgb(colors["primary"]), 15)
)
draw.text((MARGIN, cur_y), price, font=price_font, fill=hex_to_rgb(colors["primary"]))
draw.text((MARGIN + price_w + 10, cur_y + price_h - 28), "/人均", font=tag_font, fill=(*text_color, 110))
# 标签
tags = content.get("tags", ["探店", "周末约会"])
tag_x = width - MARGIN
for tag in reversed(tags):
tag_text = f"#{tag}"
tag_bbox = draw.textbbox((0, 0), tag_text, font=tag_font)
tag_w = tag_bbox[2] - tag_bbox[0]
tag_x -= tag_w + 26
draw.rounded_rectangle([tag_x - 4, cur_y + 16, tag_x + tag_w + 10, cur_y + 50], radius=17, fill=(*text_color, 15))
draw.text((tag_x, cur_y + 20), tag_text, font=tag_font, fill=(*text_color, 130))
tag_x -= 8
# 保存
output_path = OUTPUT_DIR / f"{output_name}.png"
canvas.save(output_path)
print(f"✓ 生成: {output_path}")
return output_path
def generate_overlay_center(output_name, color_scheme, content):
"""
布局B: 文字居中叠加在图片上 (视觉冲击强)
"""
width, height = 1080, 1440
colors = COLOR_SCHEMES[color_scheme]
MARGIN = 60
# 字体
title_font = load_font("title_bold", 100)
subtitle_font = load_font("body_regular", 36)
tag_font = load_font("body_regular", 26)
# === 计算内容高度 ===
temp_img = Image.new("RGBA", (1, 1))
temp_draw = ImageDraw.Draw(temp_img)
title = content.get("title", "探索未知")
title_bbox = temp_draw.textbbox((0, 0), title, font=title_font)
title_h = title_bbox[3] - title_bbox[1]
title_w = title_bbox[2] - title_bbox[0]
subtitle = content.get("subtitle", "")
sub_h = 0
if subtitle:
sub_bbox = temp_draw.textbbox((0, 0), subtitle, font=subtitle_font)
sub_h = sub_bbox[3] - sub_bbox[1]
content_height = title_h + (sub_h + 20 if sub_h else 0) + 80
# === 渐变背景 ===
canvas = create_gradient((width, height), colors["gradient"][0], colors["gradient"][1])
draw = ImageDraw.Draw(canvas)
# 暗化叠加层 (增强文字可读性)
overlay = Image.new("RGBA", (width, height), (*hex_to_rgb(colors["primary"]), 60))
canvas = Image.alpha_composite(canvas, overlay)
draw = ImageDraw.Draw(canvas)
text_white = hex_to_rgb(colors["text"])
accent_color = hex_to_rgb(colors["accent"])
# === 内容居中 ===
center_y = (height - content_height) // 2
# === 装饰线 (标题上方) ===
line_w = 80
draw.rounded_rectangle(
[(width - line_w) // 2, center_y, (width + line_w) // 2, center_y + 4],
radius=2, fill=(*accent_color, 200)
)
center_y += 32
# === 标题 (居中) ===
title_x = (width - title_w) // 2
draw_text_with_shadow(draw, (title_x, center_y), title, title_font,
colors["text"], shadow_color=(0, 0, 0, 80), offset=(3, 3))
center_y += title_h + 20
# === 副标题 (居中) ===
if subtitle:
sub_bbox = draw.textbbox((0, 0), subtitle, font=subtitle_font)
sub_w = sub_bbox[2] - sub_bbox[0]
sub_x = (width - sub_w) // 2
draw.text((sub_x, center_y), subtitle, font=subtitle_font, fill=(*text_white, 200))
center_y += sub_h + 40
# === 装饰线 (副标题下方) ===
draw.rounded_rectangle(
[(width - line_w) // 2, center_y, (width + line_w) // 2, center_y + 4],
radius=2, fill=(*accent_color, 200)
)
# === 底部标签 ===
tags = content.get("tags", [])
if tags:
tag_y = height - 120
total_w = sum(draw.textbbox((0, 0), f"#{t}", font=tag_font)[2] for t in tags) + 30 * (len(tags) - 1)
tag_x = (width - total_w) // 2
for tag in tags:
tag_text = f"#{tag}"
tag_bbox = draw.textbbox((0, 0), tag_text, font=tag_font)
tag_w = tag_bbox[2] - tag_bbox[0]
draw.rounded_rectangle(
[tag_x - 10, tag_y, tag_x + tag_w + 10, tag_y + 42],
radius=21, fill=(*text_white, 30)
)
draw.text((tag_x, tag_y + 8), tag_text, font=tag_font, fill=(*text_white, 220))
tag_x += tag_w + 30
# 保存
output_path = OUTPUT_DIR / f"{output_name}.png"
canvas.save(output_path)
print(f"✓ 生成: {output_path}")
return output_path
def generate_split_vertical(output_name, color_scheme, content):
"""
布局D: 左图右文 (信息量大)
"""
width, height = 1080, 1440
colors = COLOR_SCHEMES[color_scheme]
MARGIN = 40
SPLIT = width // 2 # 左右各50%
# 字体
title_font = load_font("title_bold", 72)
subtitle_font = load_font("body_regular", 28)
list_font = load_font("body_regular", 26)
price_font = load_font("title_bold", 76)
small_font = load_font("body_regular", 22)
# === 画布 ===
canvas = Image.new("RGBA", (width, height), hex_to_rgb(colors["secondary"]))
draw = ImageDraw.Draw(canvas)
# === 左侧渐变 (模拟图片区域) ===
left_gradient = create_gradient((SPLIT, height), colors["gradient"][0], colors["gradient"][1])
canvas.paste(left_gradient, (0, 0))
# === 右侧内容区 ===
text_color = hex_to_rgb(colors.get("text_dark", colors["primary"]))
accent_color = hex_to_rgb(colors["accent"])
content_x = SPLIT + MARGIN
content_right = width - MARGIN
cur_y = 120
# === 标签 ===
label = content.get("label", "")
if label:
label_bbox = draw.textbbox((0, 0), label, font=small_font)
label_w = label_bbox[2] - label_bbox[0]
draw.rounded_rectangle(
[content_x, cur_y, content_x + label_w + 20, cur_y + 36],
radius=18, fill=(*accent_color, 35)
)
draw.text((content_x + 10, cur_y + 7), label, font=small_font, fill=accent_color)
cur_y += 56
# === 标题 ===
title = content.get("title", "精品酒店")
# 处理标题换行 (右侧空间有限)
max_title_w = content_right - content_x - 10
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_h = title_bbox[3] - title_bbox[1]
draw.text((content_x, cur_y), title, font=title_font, fill=text_color)
cur_y += title_h + 16
# === 副标题 ===
subtitle = content.get("subtitle", "")
if subtitle:
draw.text((content_x, cur_y), subtitle, font=subtitle_font, fill=(*text_color, 150))
cur_y += 40
# === 装饰线 ===
draw.rounded_rectangle(
[content_x, cur_y, content_x + 50, cur_y + 4],
radius=2, fill=(*accent_color, 180)
)
cur_y += 36
# === 特色列表 (单列) ===
features = content.get("features", [])
if features:
for feature in features:
draw.ellipse([content_x, cur_y + 10, content_x + 8, cur_y + 18], fill=(*accent_color, 180))
draw.text((content_x + 18, cur_y), feature, font=list_font, fill=(*text_color, 160))
cur_y += 48
cur_y += 20
# === 详情 ===
details = content.get("details", [])
if details:
for detail in details:
draw.text((content_x, cur_y), f"· {detail}", font=small_font, fill=(*text_color, 130))
cur_y += 38
cur_y += 20
# === 价格 (底部) ===
price = content.get("price", "")
if price:
price_y = height - 160
price_bbox = draw.textbbox((0, 0), price, font=price_font)
price_w = price_bbox[2] - price_bbox[0]
price_h = price_bbox[3] - price_bbox[1]
# 分隔线
draw.line([(content_x, price_y - 24), (content_right, price_y - 24)], fill=(*text_color, 20), width=1)
draw.text((content_x, price_y), price, font=price_font, fill=hex_to_rgb(colors["primary"]))
draw.text((content_x + price_w + 8, price_y + price_h - 28), "/晚", font=small_font, fill=(*text_color, 110))
# 保存
output_path = OUTPUT_DIR / f"{output_name}.png"
canvas.save(output_path)
print(f"✓ 生成: {output_path}")
return output_path
def generate_card_float(output_name, color_scheme, content):
"""
布局E: 悬浮卡片 (动态内容区域)
"""
width, height = 1080, 1440
colors = COLOR_SCHEMES[color_scheme]
MARGIN = 40
PADDING = 32
# 字体 - 更大 (不换行)
title_font = load_font("title_bold", 80)
subtitle_font = load_font("body_regular", 32)
list_font = load_font("body_regular", 30)
price_font = load_font("title_bold", 80)
small_font = load_font("body_regular", 24)
detail_font = load_font("body_regular", 28)
# === 先计算卡片内容高度 ===
temp_img = Image.new("RGBA", (1, 1))
temp_draw = ImageDraw.Draw(temp_img)
card_content_height = PADDING
# 标签
card_content_height += 38 + 16
# 标题
title = content.get("title", "海边精品民宿")
title_bbox = temp_draw.textbbox((0, 0), title, font=title_font)
card_content_height += (title_bbox[3] - title_bbox[1]) + 12
# 副标题
card_content_height += 28 + 48
# 装饰线
card_content_height += 28
# 特色列表
features = content.get("features", [])
if features:
rows = (len(features) + 1) // 2
card_content_height += rows * 48 + 24
# 详情
details = content.get("details", [])
if details:
card_content_height += len(details) * 40 + 36
# 价格区域
price_bbox = temp_draw.textbbox((0, 0), "¥999", font=price_font)
card_content_height += (price_bbox[3] - price_bbox[1]) + PADDING + 24
# === 背景 ===
canvas = create_gradient((width, height), colors["gradient"][0], colors["gradient"][1])
draw = ImageDraw.Draw(canvas)
# 动态卡片位置
card_margin = MARGIN + 8
card_height = card_content_height
card_y = height - card_height - card_margin - 32
# 卡片阴影
shadow = Image.new("RGBA", (width - card_margin * 2 + 30, card_height + 30), (0, 0, 0, 0))
shadow_draw = ImageDraw.Draw(shadow)
shadow_draw.rounded_rectangle([15, 15, shadow.width - 15, shadow.height - 15], radius=28, fill=(0, 0, 0, 20))
shadow = shadow.filter(ImageFilter.GaussianBlur(15))
canvas.paste(shadow, (card_margin - 15, card_y - 8), shadow)
# 卡片本体
draw.rounded_rectangle([card_margin, card_y, width - card_margin, card_y + card_height], radius=28, fill=(255, 255, 255, 252))
text_color = hex_to_rgb(colors.get("text_dark", colors["primary"]))
accent_color = hex_to_rgb(colors["accent"])
content_x = card_margin + 36
cur_y = card_y + PADDING
content_right = width - card_margin - 36
# === 标签 ===
label = content.get("label", "精选推荐")
label_bbox = draw.textbbox((0, 0), label, font=small_font)
label_w = label_bbox[2] - label_bbox[0]
draw.rounded_rectangle([content_x, cur_y, content_x + label_w + 22, cur_y + 38], radius=19, fill=(*accent_color, 30))
draw.text((content_x + 11, cur_y + 8), label, font=small_font, fill=accent_color)
cur_y += 54
# === 标题 ===
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_h = title_bbox[3] - title_bbox[1]
draw.text((content_x, cur_y), title, font=title_font, fill=text_color)
cur_y += title_h + 12
# === 副标题 ===
subtitle = content.get("subtitle", "躺在床上就能看海的日子")
draw.text((content_x, cur_y), subtitle, font=subtitle_font, fill=(*text_color, 145))
cur_y += 48
# === 装饰线 ===
draw.rounded_rectangle([content_x, cur_y, content_x + 56, cur_y + 4], radius=2, fill=(*accent_color, 160))
cur_y += 28
# === 特色列表 ===
if features:
col_width = (content_right - content_x) // 2
for i, feature in enumerate(features):
col = i % 2
row = i // 2
item_x = content_x + col * col_width
item_y = cur_y + row * 48
draw.ellipse([item_x, item_y + 11, item_x + 8, item_y + 19], fill=(*accent_color, 180))
draw.text((item_x + 18, item_y), feature, font=list_font, fill=(*text_color, 165))
cur_y += ((len(features) + 1) // 2) * 48 + 24
# === 产品详情 ===
if details:
detail_height = len(details) * 40 + 20
draw.rounded_rectangle([content_x - 10, cur_y, content_right + 10, cur_y + detail_height], radius=14, fill=(*text_color, 8))
cur_y += 12
for detail in details:
draw.ellipse([content_x + 4, cur_y + 10, content_x + 12, cur_y + 18], fill=(*accent_color, 130))
draw.text((content_x + 22, cur_y), detail, font=detail_font, fill=(*text_color, 140))
cur_y += 40
cur_y += 24
# === 价格区域 ===
price = content.get("price", "¥688")
price_bbox = draw.textbbox((0, 0), price, font=price_font)
price_w = price_bbox[2] - price_bbox[0]
price_h = price_bbox[3] - price_bbox[1]
# 分隔线
draw.line([(content_x, cur_y), (content_right, cur_y)], fill=(*text_color, 15), width=1)
cur_y += 20
draw.text((content_x, cur_y), price, font=price_font, fill=hex_to_rgb(colors["primary"]))
draw.text((content_x + price_w + 10, cur_y + price_h - 30), "/晚", font=small_font, fill=(*text_color, 110))
# 查看详情按钮
link_text = "查看详情"
link_bbox = draw.textbbox((0, 0), link_text, font=subtitle_font)
link_w = link_bbox[2] - link_bbox[0]
link_x = content_right - link_w - 28
draw.rounded_rectangle([link_x - 18, cur_y + 8, content_right, cur_y + 54], radius=23, fill=(*accent_color, 28))
draw.text((link_x, cur_y + 17), link_text, font=subtitle_font, fill=accent_color)
# 保存
output_path = OUTPUT_DIR / f"{output_name}.png"
canvas.save(output_path)
print(f"✓ 生成: {output_path}")
return output_path
def main():
print("=" * 50)
print("生成设计样例海报 (大字号 + 填充内容)")
print("=" * 50)
# 示例1: 景点 (带详情)
generate_hero_bottom(
"sample_01_景点_ocean",
"ocean_soft",
{
"title": "正佳极地海洋世界",
"subtitle": "周末带娃去的,企鹅太可爱了",
"details": ["含企鹅馆+海豚表演", "儿童免费入场", "停车方便"],
"price": "¥199",
"tags": ["周末遛娃", "亲子游"]
}
)
# 示例2: 景点 (带详情)
generate_hero_bottom(
"sample_02_景点_sunset",
"sunset_soft",
{
"title": "长隆欢乐世界",
"subtitle": "玩了一整天,真的太开心了",
"details": ["含30+游乐项目", "夜场票更划算", "周末人较多"],
"price": "¥299",
"tags": ["主题乐园", "约会"]
}
)
# 示例3: 美食 (带emoji+详情)
generate_overlay_bottom(
"sample_03_美食_peach",
"peach_soft",
{
"emoji": "🍰",
"title": "发现一家神仙甜品店",
"subtitle": "闺蜜推荐的,颜值和味道都绝了",
"highlights": ["颜值高", "不踩雷", "出片"],
"details": ["招牌千层蛋糕必点", "下午茶套餐更划算", "需要提前预约"],
"price": "¥68",
"tags": ["下午茶", "约会"]
}
)
# 示例4: 美食 (带emoji+详情)
generate_overlay_bottom(
"sample_04_美食_sunset",
"sunset_soft",
{
"emoji": "🍜",
"title": "这家面太绝了",
"subtitle": "本地人私藏的小店,排队也要吃",
"highlights": ["量大", "实惠", "味道正"],
"details": ["招牌红烧牛肉面", "加蛋只要2元", "11点前人少"],
"price": "¥28",
"tags": ["探店", "本地推荐"]
}
)
# 示例5: 酒店 (带详情)
generate_card_float(
"sample_05_酒店_mint",
"mint_soft",
{
"label": "住过都说好",
"title": "海边精品民宿",
"subtitle": "躺在床上就能看日出",
"features": ["独立海景阳台", "私人泳池", "免费早餐", "管家服务"],
"details": ["距离沙滩50米", "可带宠物入住", "提供接送服务"],
"price": "¥688",
}
)
# 示例6: 酒店 (带详情)
generate_card_float(
"sample_06_酒店_latte",
"latte",
{
"label": "小众宝藏",
"title": "山间咖啡民宿",
"subtitle": "远离喧嚣,享受慢生活",
"features": ["独立庭院", "手冲咖啡", "山景露台", "有机早餐"],
"details": ["自驾更方便", "适合2-4人", "周末需提前订"],
"price": "¥458",
}
)
# 示例7: 居中叠加 (视觉冲击)
generate_overlay_center(
"sample_07_攻略_ocean",
"ocean_soft",
{
"title": "广州三日游",
"subtitle": "打卡必去的10个景点",
"tags": ["旅行攻略", "广州"]
}
)
# 示例8: 居中叠加 (活动)
generate_overlay_center(
"sample_08_活动_sunset",
"sunset_soft",
{
"title": "周末露营派对",
"subtitle": "一起看星星烤肉吧",
"tags": ["露营", "周末活动"]
}
)
# 示例9: 左右分栏 (酒店详情)
generate_split_vertical(
"sample_09_酒店_mint",
"mint_soft",
{
"label": "品质优选",
"title": "山海度假",
"subtitle": "面朝大海春暖花开",
"features": ["海景房", "无边泳池", "私人沙滩", "自助早餐"],
"details": ["距机场30分钟", "免费停车"],
"price": "¥888",
}
)
# 示例10: 左右分栏 (民宿)
generate_split_vertical(
"sample_10_民宿_peach",
"peach_soft",
{
"label": "ins风",
"title": "粉色小屋",
"subtitle": "少女心爆棚",
"features": ["拍照超美", "浴缸泡澡", "投影看电影", "免费下午茶"],
"details": ["近地铁站", "适合情侣"],
"price": "¥399",
}
)
print("=" * 50)
print(f"✓ 全部完成! 输出目录: {OUTPUT_DIR}")
print("=" * 50)
if __name__ == "__main__":
main()