#!/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()