TravelContentCreator/poster/templates/vibrant_template.py

951 lines
43 KiB
Python
Raw Normal View History

2025-07-10 10:08:03 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Vibrant风格活力风格海报模板
"""
2025-07-25 17:13:37 +08:00
from ast import List
2025-07-10 10:08:03 +08:00
import logging
import math
from typing import Dict, Any, Optional, Tuple
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter
2025-07-10 10:08:03 +08:00
from .base_template import BaseTemplate
from ..utils import ColorExtractor
logger = logging.getLogger(__name__)
class VibrantTemplate(BaseTemplate):
"""
活力风格模板适用于色彩鲜艳内容丰富的场景如旅游美食分享
特点是底部有毛玻璃效果的文案区域
"""
def __init__(self, size: Tuple[int, int] = (900, 1200)):
super().__init__(size)
self.config = {
'colors': {
'ocean_deep': [(0, 30, 80), (20, 120, 220)],
'sunset_warm': [(255, 94, 77), (255, 154, 0)],
'cool_mint': [(64, 224, 208), (127, 255, 212)],
'royal_purple': [(75, 0, 130), (138, 43, 226)],
'forest_green': [(34, 139, 34), (144, 238, 144)],
'fire_red': [(220, 20, 60), (255, 69, 0)],
"gray_gradient": [(128, 128, 128), (211, 211, 211)],
"dark_gray": [(15, 15, 15), (30, 30, 30)]
},
'glass_effect': {
'max_opacity': 240,
'blur_radius': 22,
'transition_height': 80,
'intensity_multiplier': 1.5
},
}
2025-07-27 16:43:31 +08:00
# 设置中文字体路径
self.chinese_font_path = "/root/TravelContentCreator/assets/font/兰亭粗黑简.TTF"
# 重写text_renderer的字体加载方法以支持中文
self._patch_text_renderer_for_chinese()
def _patch_text_renderer_for_chinese(self):
"""重写text_renderer的字体加载方法以支持中文"""
original_load_font = self.text_renderer._load_default_font
def load_chinese_font(size: int):
try:
return ImageFont.truetype(self.chinese_font_path, size)
except:
logger.warning(f"无法加载中文字体,使用默认字体")
return original_load_font(size)
# 替换字体加载方法
self.text_renderer._load_default_font = load_chinese_font
2025-07-10 10:08:03 +08:00
def generate(self,
2025-07-25 17:13:37 +08:00
images: List,
2025-07-10 10:08:03 +08:00
content: Optional[Dict[str, Any]] = None,
theme_color: Optional[str] = None,
glass_intensity: float = 1.5,
2025-07-25 17:13:37 +08:00
num_variations: int = 1,
2025-07-10 10:08:03 +08:00
**kwargs) -> Image.Image:
"""
生成Vibrant风格海报
Args:
2025-07-25 17:13:37 +08:00
images (List): 主图
2025-07-10 10:08:03 +08:00
content (Optional[Dict[str, Any]]): 包含所有文本信息的字典
theme_color (Optional[str]): 预设颜色主题的名称
glass_intensity (float): 毛玻璃效果强度
2025-07-25 17:13:37 +08:00
num_variations (int): 生成海报数量
2025-07-10 10:08:03 +08:00
Returns:
Image.Image: 生成的海报图像
"""
if content is None:
content = self._get_default_content()
self.config['glass_effect']['intensity_multiplier'] = glass_intensity
2025-07-25 17:13:37 +08:00
main_image = images
logger.info(f"main_image的类型: {np.shape(main_image)}")
2025-07-10 10:08:03 +08:00
if not main_image:
2025-07-25 17:13:37 +08:00
logger.error(f"无法加载图片: ")
2025-07-10 10:08:03 +08:00
return None
2025-07-25 17:13:37 +08:00
main_image = self.image_processor.resize_image(image=main_image, target_size=self.size)
2025-07-10 10:08:03 +08:00
estimated_height = self._estimate_content_height(content)
gradient_start = self._detect_gradient_start_position(main_image, estimated_height)
canvas = self._create_composite_image(main_image, gradient_start, theme_color)
canvas = self._render_texts(canvas, content, gradient_start)
final_image = canvas.resize((1350, 1800), Image.LANCZOS)
return final_image
def _get_default_content(self) -> Dict[str, Any]:
"""获取默认的测试内容"""
return {
"title": "正佳极地海洋世界",
"slogan": "都说海洋馆是约会圣地!那锦峰夜场将是绝杀!",
"price": "199",
"ticket_type": "夜场票",
"content_button": "套餐内容",
"content_items": [
"正佳极地海洋世界夜场票1张",
"有效期至2025.06.02",
"多种动物表演全部免费"
],
"remarks": [
"工作日可直接入园",
"周末请提前1天预约"
],
"tag": "#520特惠",
"pagination": ""
}
def _estimate_content_height(self, content: Dict[str, Any]) -> int:
"""预估内容高度"""
standard_margin = 25
title_height = 100
subtitle_height = 80
button_height = 40
content_items = content.get("content_items", [])
content_line_height = 32
content_list_height = len(content_items) * content_line_height
price_height = 90
ticket_height = 60
remarks = content.get("remarks", [])
if isinstance(remarks, str):
remarks = [remarks]
remarks_height = len(remarks) * 25 + 10
footer_height = 40
total_height = (
20 + title_height + standard_margin + subtitle_height + standard_margin +
button_height + 15 + content_list_height + price_height + ticket_height +
remarks_height + footer_height + 30
)
return total_height
def _detect_gradient_start_position(self, image: Image.Image, estimated_height: int) -> int:
"""动态检测渐变起始位置"""
width, height = image.size
center_x = width // 2
gradient_start = None
for y in range(height // 2, height):
try:
pixel = image.getpixel((center_x, y))
if isinstance(pixel, (tuple, list)) and len(pixel) >= 3:
brightness = sum(pixel[:3]) / 3
if brightness > 50:
gradient_start = max(y - 20, height // 2)
break
except:
continue
if gradient_start is None:
bottom_margin = 60
gradient_start = max(height - estimated_height - bottom_margin, height // 2)
return gradient_start
def _create_composite_image(self, main_image: Image.Image,
gradient_start: int,
theme_color: Optional[str]) -> Image.Image:
"""创建毛玻璃背景和复合图像"""
if theme_color and theme_color in self.config['colors']:
top_color, bottom_color = self.config['colors'][theme_color]
else:
top_color, bottom_color = self._extract_glass_colors_from_image(main_image, gradient_start)
logger.info(f"使用毛玻璃颜色: 顶部={top_color}, 底部={bottom_color}")
gradient_overlay = self._create_frosted_glass_overlay(top_color, bottom_color, gradient_start)
composite_img = Image.new('RGBA', self.size, (0, 0, 0, 0))
composite_img.paste(main_image, (0, 0))
composite_img = Image.alpha_composite(composite_img, gradient_overlay)
return composite_img
def _create_frosted_glass_overlay(self, top_color: Tuple[int, int, int],
bottom_color: Tuple[int, int, int],
gradient_start: int) -> Image.Image:
"""创建高级毛玻璃效果覆盖层"""
overlay = Image.new('RGBA', self.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
gradient_height = self.height - gradient_start
glass_config = self.config['glass_effect']
intensity = glass_config['intensity_multiplier']
enhanced_opacity = min(255, int(glass_config['max_opacity'] * intensity))
enhanced_blur = int(glass_config['blur_radius'] * intensity)
def enhance_color(color, multiplier):
factor = min(1.5, 1.0 + (multiplier - 1.0) * 0.3)
return tuple(min(255, max(0, int(c * factor))) for c in color)
top_c = enhance_color(top_color, intensity)
bottom_c = enhance_color(bottom_color, intensity)
top_c_arr = np.array(top_c)
bottom_c_arr = np.array(bottom_c)
for y in range(gradient_start, self.height):
ratio = (y - gradient_start) / gradient_height if gradient_height > 0 else 0
smooth_ratio = 0.5 - 0.5 * math.cos(ratio * math.pi)
color = (1 - smooth_ratio) * top_c_arr + smooth_ratio * bottom_c_arr
alpha_smooth = ratio ** (1.1 / intensity)
alpha = int(enhanced_opacity * (0.02 + 0.98 * alpha_smooth))
if (y - gradient_start) < glass_config['transition_height']:
transition_ratio = (y - gradient_start) / glass_config['transition_height']
alpha = int(alpha * (0.5 - 0.5 * math.cos(transition_ratio * math.pi)))
color_tuple = tuple(int(c) for c in color) + (alpha,)
draw.line([(0, y), (self.width, y)], fill=color_tuple)
return overlay.filter(ImageFilter.GaussianBlur(radius=enhanced_blur))
2025-07-10 10:08:03 +08:00
def _extract_glass_colors_from_image(self, image: Image.Image, gradient_start: int) -> tuple:
"""从图片中提取用于毛玻璃背景的颜色"""
if image.mode != 'RGB':
image = image.convert('RGB')
width, height = image.size
top_samples, bottom_samples = [], []
top_y = min(gradient_start + 20, height - 1)
for x in range(0, width, 20):
if sum(pixel := image.getpixel((x, top_y))) > 30:
top_samples.append(pixel)
bottom_y = min(height - 50, height - 1)
for x in range(0, width, 20):
if sum(pixel := image.getpixel((x, bottom_y))) > 30:
bottom_samples.append(pixel)
top_color = tuple(max(0, int(c * 0.1)) for c in np.mean(top_samples, axis=0)) if top_samples else (0, 5, 15)
bottom_color = tuple(max(0, int(c * 0.2)) for c in np.mean(bottom_samples, axis=0)) if bottom_samples else (0, 25, 50)
return top_color, bottom_color
def _calculate_optimal_font_size_enhanced(self, text: str, target_width: int,
max_size: int = 120, min_size: int = 10) -> Tuple[int, int]:
"""
计算文本的最佳字体大小使其宽度接近目标宽度增强版本与demo一致
返回:
(字体大小, 实际文本宽度)
"""
# 二分查找最佳字体大小
low = min_size
high = max_size
best_size = min_size
best_width = 0
tolerance = 0.08 # 容差值,使文本宽度更接近目标值
# 首先尝试最大字体大小
try:
font = self.text_renderer._load_default_font(max_size)
max_width, _ = self.text_renderer.get_text_size(text, font)
except:
max_width = target_width * 2 # 如果出错,设置一个大值
# 如果最大字体大小下的宽度仍小于目标宽度的108%,直接使用最大字体
if max_width < target_width * (1 + tolerance):
best_size = max_size
best_width = max_width
else:
# 记录最接近目标宽度的字体大小
closest_size = min_size
closest_diff = target_width
while low <= high:
mid = (low + high) // 2
try:
font = self.text_renderer._load_default_font(mid)
width, _ = self.text_renderer.get_text_size(text, font)
except:
width = target_width * 2 # 如果出错,设置一个大值
# 计算与目标宽度的差距
diff = abs(width - target_width)
# 更新最接近的字体大小
if diff < closest_diff:
closest_diff = diff
closest_size = mid
# 如果宽度在目标宽度的允许范围内,认为找到了最佳匹配
if target_width * (1 - tolerance) <= width <= target_width * (1 + tolerance):
best_size = mid
best_width = width
break
# 如果当前宽度小于目标宽度,尝试更大的字体
if width < target_width:
if width > best_width:
best_width = width
best_size = mid
low = mid + 1
else:
# 如果当前宽度大于目标宽度,尝试更小的字体
high = mid - 1
# 如果没有找到在容差范围内的字体大小,使用最接近的字体大小
if best_width == 0:
best_size = closest_size
# 确保返回的宽度是使用最终字体计算的实际宽度
try:
best_font = self.text_renderer._load_default_font(best_size)
final_width, _ = self.text_renderer.get_text_size(text, best_font)
except:
final_width = best_width
logger.info(f"文本'{text[:min(10, len(text))]}...'的最佳字体大小: {best_size},目标宽度: {target_width},实际宽度: {final_width},差距: {abs(final_width-target_width)}")
return best_size, final_width
2025-07-10 10:08:03 +08:00
def _render_texts(self, canvas: Image.Image, content: Dict[str, Any], gradient_start: int) -> Image.Image:
"""渲染所有文本元素,采用双栏布局"""
draw = ImageDraw.Draw(canvas)
width, height = canvas.size
center_x = width // 2
left_margin, right_margin = self._calculate_content_margins(content, width, center_x)
footer_y = height - 30
self._render_footer(draw, content, footer_y, left_margin, right_margin)
title_y = gradient_start + 40
current_y = self._render_title_subtitle(draw, content, title_y, center_x, left_margin, right_margin)
content_area_width = right_margin - left_margin
left_column_width = int(content_area_width * 0.5)
right_column_x = left_margin + left_column_width
content_start_y = current_y + 30
self._render_left_column(draw, content, content_start_y, left_margin, left_column_width, height)
self._render_right_column(draw, content, content_start_y, right_column_x, right_margin)
return canvas
def _calculate_content_margins(self, content: Dict[str, Any], width: int, center_x: int) -> Tuple[int, int]:
"""计算内容区域的左右边距增强版本与demo一致"""
# 计算标题位置
2025-07-10 10:08:03 +08:00
title_text = content.get("title", "")
title_target_width = int(width * 0.95)
title_size, title_width = self._calculate_optimal_font_size_enhanced(
title_text, title_target_width, min_size=40, max_size=130
)
2025-07-10 10:08:03 +08:00
title_x = center_x - title_width // 2
# 计算副标题位置
2025-07-10 10:08:03 +08:00
slogan_text = content.get("slogan", "")
subtitle_target_width = int(width * 0.9)
subtitle_size, subtitle_width = self._calculate_optimal_font_size_enhanced(
slogan_text, subtitle_target_width, max_size=50, min_size=20
)
2025-07-10 10:08:03 +08:00
subtitle_x = center_x - subtitle_width // 2
# 计算内容区域边距 - 减小额外的边距,让内容区域更宽
padding = 20 # 从30减小到20
content_left_margin = min(title_x, subtitle_x) - padding
content_right_margin = max(title_x + title_width, subtitle_x + subtitle_width) + padding
# 确保边距不超出合理范围,但允许更宽的内容区域
content_left_margin = max(40, content_left_margin)
content_right_margin = min(width - 40, content_right_margin)
# 如果内容区域太窄,强制使用更宽的区域
min_content_width = int(width * 0.75) # 至少使用75%的宽度
current_width = content_right_margin - content_left_margin
if current_width < min_content_width:
extra_width = min_content_width - current_width
content_left_margin = max(30, content_left_margin - extra_width // 2)
content_right_margin = min(width - 30, content_right_margin + extra_width // 2)
return content_left_margin, content_right_margin
2025-07-10 10:08:03 +08:00
def _render_footer(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, left: int, right: int):
"""渲染页脚文本"""
2025-07-25 17:13:37 +08:00
font = self.text_renderer._load_default_font(18)
2025-07-10 10:08:03 +08:00
if tag := content.get("tag"):
draw.text((left, y), tag, font=font, fill=(255, 255, 255))
if pagination := content.get("pagination"):
width, _ = self.text_renderer.get_text_size(pagination, font)
draw.text((right - width, y), pagination, font=font, fill=(255, 255, 255))
def _render_title_subtitle(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, center_x: int, left: int, right: int) -> int:
"""渲染标题和副标题增强版本与demo一致"""
2025-07-10 10:08:03 +08:00
# 标题
title_text = content.get("title", "默认标题")
title_target_width = int((right - left) * 0.98)
title_size, title_actual_width = self._calculate_optimal_font_size_enhanced(
title_text, title_target_width, max_size=140, min_size=40
)
2025-07-25 17:13:37 +08:00
title_font = self.text_renderer._load_default_font(title_size)
2025-07-10 10:08:03 +08:00
text_w, text_h = self.text_renderer.get_text_size(title_text, title_font)
title_x = center_x - text_w // 2
self.text_renderer.draw_text_with_outline(draw, (title_x, y), title_text, title_font, text_color=(255, 255, 255, 255), outline_color=(0, 30, 80, 200), outline_width=4)
# 副标题 (slogan)
subtitle_text = content.get("slogan", "")
subtitle_target_width = int((right - left) * 0.95)
subtitle_size, subtitle_actual_width = self._calculate_optimal_font_size_enhanced(
subtitle_text, subtitle_target_width, max_size=75, min_size=20
)
2025-07-25 17:13:37 +08:00
subtitle_font = self.text_renderer._load_default_font(subtitle_size)
2025-07-10 10:08:03 +08:00
sub_text_w, sub_text_h = self.text_renderer.get_text_size(subtitle_text, subtitle_font)
subtitle_x = center_x - sub_text_w // 2
title_spacing = 30
# 在标题下方添加装饰线
line_y = y + text_h + 5
line_start_x = title_x - text_w * 0.025
line_end_x = title_x + text_w * 1.025
draw.line([(line_start_x, line_y), (line_end_x, line_y)], fill=(215, 215, 215, 80), width=3)
subtitle_y = y + text_h + title_spacing
self.text_renderer.draw_text_with_shadow(draw, (subtitle_x, subtitle_y), subtitle_text, subtitle_font, text_color=(255, 255, 255, 255), shadow_color=(0, 0, 0, 180), shadow_offset=(2, 2))
return subtitle_y + sub_text_h
def _render_left_column(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, x: int, width: int, canvas_height: int):
"""渲染左栏内容:按钮和项目列表"""
2025-07-25 17:13:37 +08:00
button_font = self.text_renderer._load_default_font(30)
2025-07-10 10:08:03 +08:00
button_text = content.get("content_button", "套餐内容")
button_width, _ = self.text_renderer.get_text_size(button_text, button_font)
button_width += 40
button_height = 50
2025-07-25 17:13:37 +08:00
self.text_renderer.draw_rounded_rectangle(draw=draw, position=(x, y), size=(button_width, button_height), radius=20, fill_color=(0, 140, 210, 180), outline_color=(255, 255, 255, 255), outline_width=1)
2025-07-10 10:08:03 +08:00
draw.text((x + 20, y + (button_height - 30) // 2), button_text, font=button_font, fill=(255, 255, 255))
items = content.get("content_items", [])
if not items: return
2025-07-25 17:13:37 +08:00
font = self.text_renderer._load_default_font(28)
2025-07-10 10:08:03 +08:00
list_y = y + button_height + 20
available_h = canvas_height - 30 - (len(content.get("remarks", [])) * 25 + 10) - list_y - 20
total_items_h = len(items) * 36
extra_space_per_item = max(0, min(17, (available_h - total_items_h) / max(1, len(items) - 1)))
line_spacing = 8 + extra_space_per_item
for i, item in enumerate(items):
item_y = list_y + i * (28 + line_spacing)
2025-07-10 10:09:16 +08:00
draw.text((x, item_y), " " + item, font=font, fill=(255, 255, 255))
2025-07-10 10:08:03 +08:00
def _render_right_column(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, x: int, right_margin: int):
"""渲染右栏内容价格、票种和备注增强版本与demo一致"""
2025-07-10 10:08:03 +08:00
price_text = content.get('price', '')
price_target_width = int((right_margin - x) * 0.7)
price_size, price_actual_width = self._calculate_optimal_font_size_enhanced(
price_text, price_target_width, max_size=120, min_size=40
)
2025-07-25 17:13:37 +08:00
price_font = self.text_renderer._load_default_font(price_size)
suffix_font = self.text_renderer._load_default_font(int(price_size * 0.3))
2025-07-10 10:08:03 +08:00
_, price_height = self.text_renderer.get_text_size(price_text, price_font)
suffix_width, suffix_height = self.text_renderer.get_text_size("CNY起", suffix_font)
price_x = right_margin - price_actual_width - suffix_width
2025-07-10 10:08:03 +08:00
self.text_renderer.draw_text_with_shadow(draw, (price_x, y), price_text, price_font)
suffix_y = y + price_height - suffix_height
draw.text((price_x + price_actual_width, suffix_y), "CNY起", font=suffix_font, fill=(255, 255, 255))
2025-07-10 10:08:03 +08:00
underline_y = y + price_height + 18
draw.line([(price_x - 10, underline_y), (right_margin, underline_y)], fill=(255, 255, 255, 80), width=2)
ticket_text = content.get("ticket_type", "")
ticket_target_width = int((right_margin - x) * 0.7)
ticket_size, ticket_actual_width = self._calculate_optimal_font_size_enhanced(
ticket_text, ticket_target_width, max_size=60, min_size=30
)
2025-07-25 17:13:37 +08:00
ticket_font = self.text_renderer._load_default_font(ticket_size)
ticket_x = right_margin - ticket_actual_width
2025-07-10 10:08:03 +08:00
ticket_y = y + price_height + 35
self.text_renderer.draw_text_with_shadow(draw, (ticket_x, ticket_y), ticket_text, ticket_font)
_, ticket_height = self.text_renderer.get_text_size(ticket_text, ticket_font)
remarks = content.get("remarks", [])
if remarks:
2025-07-25 17:13:37 +08:00
remarks_font = self.text_renderer._load_default_font(16)
2025-07-10 10:08:03 +08:00
remarks_y = ticket_y + ticket_height + 30
for i, remark in enumerate(remarks):
remark_width, _ = self.text_renderer.get_text_size(remark, remarks_font)
2025-07-27 16:43:31 +08:00
draw.text((right_margin - remark_width, remarks_y + i * 21), remark, font=remarks_font, fill=(255, 255, 255, 200))
def generate_layered_psd(self,
images,
content: Optional[Dict[str, Any]] = None,
theme_color: Optional[str] = None,
glass_intensity: float = 1.5,
output_path: str = "layered_poster.psd",
**kwargs) -> str:
"""
生成分层的PSD文件方便后续修改
Args:
images: 主图
content: 包含所有文本信息的字典
theme_color: 预设颜色主题的名称
glass_intensity: 毛玻璃效果强度
output_path: PSD文件输出路径
Returns:
str: 生成的PSD文件路径
"""
try:
from psd_tools import PSDImage
from psd_tools.api.layers import PixelLayer
except ImportError:
logger.error("需要安装psd-tools库: pip install psd-tools")
return None
logger.info("开始生成分层PSD文件...")
if content is None:
content = self._get_default_content()
self.config['glass_effect']['intensity_multiplier'] = glass_intensity
main_image = images
if not main_image:
logger.error("无法加载图片")
return None
main_image = self.image_processor.resize_image(image=main_image, target_size=self.size)
estimated_height = self._estimate_content_height(content)
gradient_start = self._detect_gradient_start_position(main_image, estimated_height)
# 创建新的PSD文档
psd = PSDImage.new("RGB", self.size, color=(255, 255, 255))
logger.info(f"创建PSD文档尺寸: {self.size}")
# 1. 添加背景图层
background_layer = PixelLayer.frompil(main_image, psd, "Background")
psd.append(background_layer)
logger.info("✓ 添加背景图层")
# 2. 添加毛玻璃效果层
glass_overlay = self._create_glass_overlay_layer(main_image, gradient_start, theme_color)
if glass_overlay:
glass_layer = PixelLayer.frompil(glass_overlay, psd, "Glass Effect")
psd.append(glass_layer)
logger.info("✓ 添加毛玻璃效果层")
# 3-7. 添加文字图层
text_layers = self._create_text_layers(content, gradient_start)
for layer_name, layer_image in text_layers.items():
if layer_image:
text_layer = PixelLayer.frompil(layer_image, psd, layer_name)
psd.append(text_layer)
logger.info(f"✓ Added {layer_name} layer")
# 注意PSD文件保持原始尺寸最终调整在导出时进行
# 保存PSD文件
psd.save(output_path)
logger.info(f"✓ PSD文件已保存: {output_path}")
return output_path
def _create_glass_overlay_layer(self, main_image: Image.Image, gradient_start: int, theme_color: Optional[str]) -> Optional[Image.Image]:
"""创建毛玻璃效果的独立图层"""
try:
if theme_color and theme_color in self.config['colors']:
top_color, bottom_color = self.config['colors'][theme_color]
else:
top_color, bottom_color = self._extract_glass_colors_from_image(main_image, gradient_start)
# 创建透明背景的毛玻璃层
overlay = self._create_frosted_glass_overlay(top_color, bottom_color, gradient_start)
return overlay
except Exception as e:
logger.error(f"创建毛玻璃层失败: {e}")
return None
def _create_text_layers(self, content: Dict[str, Any], gradient_start: int) -> Dict[str, Optional[Image.Image]]:
"""创建各个文字图层"""
layers = {}
try:
# 创建透明画布
canvas_size = self.size
# 计算布局参数
width, height = canvas_size
center_x = width // 2
left_margin, right_margin = self._calculate_content_margins(content, width, center_x)
# 1. 标题层
layers["Title Text"] = self._create_title_layer(content, gradient_start, center_x, left_margin, right_margin, canvas_size)
# 2. 副标题层
layers["Subtitle Text"] = self._create_subtitle_layer(content, gradient_start, center_x, left_margin, right_margin, canvas_size)
# 3. 装饰线层
layers["Decorations"] = self._create_decoration_layer(content, gradient_start, center_x, left_margin, right_margin, canvas_size)
# 4. 左栏内容层
title_y = gradient_start + 40
subtitle_height = 80 + 30 # 预估副标题高度
content_start_y = title_y + subtitle_height + 30
content_area_width = right_margin - left_margin
left_column_width = int(content_area_width * 0.5)
layers["Left Content"] = self._create_left_column_layer(content, content_start_y, left_margin, left_column_width, canvas_size)
# 5. 右栏内容层
right_column_x = left_margin + left_column_width
layers["Right Content"] = self._create_right_column_layer(content, content_start_y, right_column_x, right_margin, canvas_size)
# 6. 页脚层
footer_y = height - 30
layers["Footer Info"] = self._create_footer_layer(content, footer_y, left_margin, right_margin, canvas_size)
except Exception as e:
logger.error(f"创建文字图层失败: {e}")
return layers
def _draw_text_with_outline_simple(self, draw: ImageDraw.Draw, position: Tuple[int, int],
text: str, font: ImageFont.FreeTypeFont,
text_color: Tuple[int, int, int, int] = (255, 255, 255, 255),
outline_color: Tuple[int, int, int, int] = (0, 0, 0, 255),
outline_width: int = 2):
"""简单的文本描边绘制方法"""
x, y = position
# 绘制描边
for dx in range(-outline_width, outline_width + 1):
for dy in range(-outline_width, outline_width + 1):
if dx == 0 and dy == 0:
continue
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
# 绘制主文本
draw.text((x, y), text, font=font, fill=text_color)
def _draw_text_with_shadow_simple(self, draw: ImageDraw.Draw, position: Tuple[int, int],
text: str, font: ImageFont.FreeTypeFont,
text_color: Tuple[int, int, int, int] = (255, 255, 255, 255),
shadow_color: Tuple[int, int, int, int] = (0, 0, 0, 128),
shadow_offset: Tuple[int, int] = (2, 2)):
"""简单的文本阴影绘制方法"""
x, y = position
shadow_x, shadow_y = shadow_offset
# 绘制阴影
draw.text((x + shadow_x, y + shadow_y), text, font=font, fill=shadow_color)
# 绘制主文本
draw.text((x, y), text, font=font, fill=text_color)
def _create_title_layer(self, content: Dict[str, Any], gradient_start: int, center_x: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]:
"""创建标题图层"""
try:
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
title_text = content.get("title", "默认标题")
title_target_width = int((right_margin - left_margin) * 0.98)
# 使用指定的中文字体
title_size, title_actual_width = self._calculate_optimal_font_size_enhanced(
title_text, title_target_width, max_size=140, min_size=40
)
try:
title_font = ImageFont.truetype(self.chinese_font_path, title_size)
except:
title_font = self.text_renderer._load_default_font(title_size)
# 重新计算实际尺寸
bbox = title_font.getbbox(title_text)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
title_x = center_x - text_w // 2
title_y = gradient_start + 40
# 绘制带描边的标题
self._draw_text_with_outline_simple(
draw, (title_x, title_y), title_text, title_font,
text_color=(255, 255, 255, 255),
outline_color=(0, 30, 80, 200),
outline_width=4
)
return canvas
except Exception as e:
logger.error(f"创建标题层失败: {e}")
import traceback
traceback.print_exc()
return None
def _create_subtitle_layer(self, content: Dict[str, Any], gradient_start: int, center_x: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]:
"""创建副标题图层"""
try:
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
subtitle_text = content.get("slogan", "")
if not subtitle_text:
return None
subtitle_target_width = int((right_margin - left_margin) * 0.95)
subtitle_size, subtitle_actual_width = self._calculate_optimal_font_size_enhanced(
subtitle_text, subtitle_target_width, max_size=75, min_size=20
)
# 使用指定的中文字体
try:
subtitle_font = ImageFont.truetype(self.chinese_font_path, subtitle_size)
except:
subtitle_font = self.text_renderer._load_default_font(subtitle_size)
bbox = subtitle_font.getbbox(subtitle_text)
sub_text_w = bbox[2] - bbox[0]
sub_text_h = bbox[3] - bbox[1]
subtitle_x = center_x - sub_text_w // 2
subtitle_y = gradient_start + 40 + 100 + 30 # title_y + title_height + spacing
# 绘制带阴影的副标题
self._draw_text_with_shadow_simple(
draw, (subtitle_x, subtitle_y), subtitle_text, subtitle_font,
text_color=(255, 255, 255, 255),
shadow_color=(0, 0, 0, 180),
shadow_offset=(2, 2)
)
return canvas
except Exception as e:
logger.error(f"创建副标题层失败: {e}")
import traceback
traceback.print_exc()
return None
def _create_decoration_layer(self, content: Dict[str, Any], gradient_start: int, center_x: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]:
"""创建装饰元素图层"""
try:
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
# 获取标题信息用于装饰线定位
title_text = content.get("title", "默认标题")
title_target_width = int((right_margin - left_margin) * 0.98)
title_size, title_actual_width = self._calculate_optimal_font_size_enhanced(
title_text, title_target_width, max_size=140, min_size=40
)
# 使用中文字体计算标题尺寸
try:
title_font = ImageFont.truetype(self.chinese_font_path, title_size)
except:
title_font = self.text_renderer._load_default_font(title_size)
bbox = title_font.getbbox(title_text)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
title_x = center_x - text_w // 2
title_y = gradient_start + 40
# 在标题下方添加装饰线
line_y = title_y + text_h + 5
line_start_x = title_x - text_w * 0.025
line_end_x = title_x + text_w * 1.025
draw.line([(line_start_x, line_y), (line_end_x, line_y)], fill=(215, 215, 215, 80), width=3)
return canvas
except Exception as e:
logger.error(f"创建装饰层失败: {e}")
import traceback
traceback.print_exc()
return None
def _create_left_column_layer(self, content: Dict[str, Any], y: int, x: int, width: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]:
"""创建左栏内容图层"""
try:
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
# 使用中文字体
# 按钮
try:
button_font = ImageFont.truetype(self.chinese_font_path, 30)
except:
button_font = self.text_renderer._load_default_font(30)
button_text = content.get("content_button", "套餐内容")
bbox = button_font.getbbox(button_text)
button_text_width = bbox[2] - bbox[0]
button_width = button_text_width + 40
button_height = 50
# 绘制简单的矩形按钮
draw.rounded_rectangle([x, y, x + button_width, y + button_height],
radius=20, fill=(0, 140, 210, 180),
outline=(255, 255, 255, 255), width=1)
draw.text((x + 20, y + (button_height - 30) // 2), button_text, font=button_font, fill=(255, 255, 255))
# 项目列表
items = content.get("content_items", [])
if items:
try:
list_font = ImageFont.truetype(self.chinese_font_path, 28)
except:
list_font = self.text_renderer._load_default_font(28)
list_y = y + button_height + 20
line_spacing = 36
for i, item in enumerate(items):
item_y = list_y + i * line_spacing
draw.text((x, item_y), "" + item, font=list_font, fill=(255, 255, 255))
return canvas
except Exception as e:
logger.error(f"创建左栏内容层失败: {e}")
import traceback
traceback.print_exc()
return None
def _create_right_column_layer(self, content: Dict[str, Any], y: int, x: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]:
"""创建右栏内容图层"""
try:
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
# 使用中文字体
# 价格
price_text = content.get('price', '')
if price_text:
price_target_width = int((right_margin - x) * 0.7)
price_size, price_actual_width = self._calculate_optimal_font_size_enhanced(
price_text, price_target_width, max_size=120, min_size=40
)
try:
price_font = ImageFont.truetype(self.chinese_font_path, price_size)
suffix_font = ImageFont.truetype(self.chinese_font_path, int(price_size * 0.3))
except:
price_font = self.text_renderer._load_default_font(price_size)
suffix_font = self.text_renderer._load_default_font(int(price_size * 0.3))
price_bbox = price_font.getbbox(price_text)
price_height = price_bbox[3] - price_bbox[1]
suffix_bbox = suffix_font.getbbox("CNY起")
suffix_width = suffix_bbox[2] - suffix_bbox[0]
suffix_height = suffix_bbox[3] - suffix_bbox[1]
price_x = right_margin - price_actual_width - suffix_width
self._draw_text_with_shadow_simple(draw, (price_x, y), price_text, price_font)
suffix_y = y + price_height - suffix_height
draw.text((price_x + price_actual_width, suffix_y), "CNY起", font=suffix_font, fill=(255, 255, 255))
# 下划线
underline_y = y + price_height + 18
draw.line([(price_x - 10, underline_y), (right_margin, underline_y)], fill=(255, 255, 255, 80), width=2)
# 票种
ticket_text = content.get("ticket_type", "")
if ticket_text:
ticket_target_width = int((right_margin - x) * 0.7)
ticket_size, ticket_actual_width = self._calculate_optimal_font_size_enhanced(
ticket_text, ticket_target_width, max_size=60, min_size=30
)
try:
ticket_font = ImageFont.truetype(self.chinese_font_path, ticket_size)
except:
ticket_font = self.text_renderer._load_default_font(ticket_size)
ticket_x = right_margin - ticket_actual_width
ticket_y = y + price_height + 35
self._draw_text_with_shadow_simple(draw, (ticket_x, ticket_y), ticket_text, ticket_font)
ticket_bbox = ticket_font.getbbox(ticket_text)
ticket_height = ticket_bbox[3] - ticket_bbox[1]
# 备注
remarks = content.get("remarks", [])
if remarks:
try:
remarks_font = ImageFont.truetype(self.chinese_font_path, 16)
except:
remarks_font = self.text_renderer._load_default_font(16)
remarks_y = ticket_y + ticket_height + 30
for i, remark in enumerate(remarks):
remark_bbox = remarks_font.getbbox(remark)
remark_width = remark_bbox[2] - remark_bbox[0]
draw.text((right_margin - remark_width, remarks_y + i * 21), remark, font=remarks_font, fill=(255, 255, 255, 200))
return canvas
except Exception as e:
logger.error(f"创建右栏内容层失败: {e}")
import traceback
traceback.print_exc()
return None
def _create_footer_layer(self, content: Dict[str, Any], footer_y: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]:
"""创建页脚图层"""
try:
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
# 使用中文字体
try:
font = ImageFont.truetype(self.chinese_font_path, 18)
except:
font = self.text_renderer._load_default_font(18)
# 标签(左下角)
tag_text = content.get("tag", "")
if tag_text:
draw.text((left_margin, footer_y), tag_text, font=font, fill=(255, 255, 255))
# 分页信息(右下角)
pagination_text = content.get("pagination", "")
if pagination_text:
pagination_bbox = font.getbbox(pagination_text)
pagination_width = pagination_bbox[2] - pagination_bbox[0]
draw.text((right_margin - pagination_width, footer_y), pagination_text, font=font, fill=(255, 255, 255))
return canvas
except Exception as e:
logger.error(f"创建页脚层失败: {e}")
import traceback
traceback.print_exc()
return None