- 新增 PosterSmartEngine,AI 生成文案 + 海报渲染 - 5 种布局支持文本换行和自适应字体 - 修复按钮/标签颜色显示问题 - 优化渐变遮罩和内容区域计算 - Prompt 优化:标题格式为产品名+描述
918 lines
31 KiB
Python
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()
|