TravelContentCreator/poster/templates/vibrant_template.py

1123 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Vibrant风格活力风格海报模板
"""
from ast import List
import logging
import math
from typing import Dict, Any, Optional, Tuple
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter
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
},
}
# 设置中文字体路径
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
def generate(self,
images: List,
content: Optional[Dict[str, Any]] = None,
theme_color: Optional[str] = None,
glass_intensity: float = 1.5,
num_variations: int = 1,
**kwargs) -> Image.Image:
"""
生成Vibrant风格海报
Args:
images (List): 主图
content (Optional[Dict[str, Any]]): 包含所有文本信息的字典
theme_color (Optional[str]): 预设颜色主题的名称
glass_intensity (float): 毛玻璃效果强度
num_variations (int): 生成海报数量
Returns:
Image.Image: 生成的海报图像
"""
if content is None:
content = self._get_default_content()
self.config['glass_effect']['intensity_multiplier'] = glass_intensity
main_image = images
logger.info(f"main_image的类型: {np.shape(main_image)}")
if not main_image:
logger.error(f"无法加载图片: ")
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)
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))
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
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一致"""
# 计算标题位置
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
)
title_x = center_x - title_width // 2
# 计算副标题位置
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
)
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
def _render_footer(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, left: int, right: int):
"""渲染页脚文本"""
font = self.text_renderer._load_default_font(18)
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一致"""
# 标题
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
)
title_font = self.text_renderer._load_default_font(title_size)
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
)
subtitle_font = self.text_renderer._load_default_font(subtitle_size)
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):
"""渲染左栏内容:按钮和项目列表"""
button_font = self.text_renderer._load_default_font(30)
button_text = content.get("content_button", "套餐内容")
button_width, _ = self.text_renderer.get_text_size(button_text, button_font)
button_width += 40
button_height = 50
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)
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
font = self.text_renderer._load_default_font(28)
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)
draw.text((x, item_y), " " + item, font=font, fill=(255, 255, 255))
def _render_right_column(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, x: int, right_margin: int):
"""渲染右栏内容价格、票种和备注增强版本与demo一致"""
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
)
price_font = self.text_renderer._load_default_font(price_size)
suffix_font = self.text_renderer._load_default_font(int(price_size * 0.3))
_, 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
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))
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
)
ticket_font = self.text_renderer._load_default_font(ticket_size)
ticket_x = right_margin - ticket_actual_width
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:
remarks_font = self.text_renderer._load_default_font(16)
remarks_y = ticket_y + ticket_height + 30
for i, remark in enumerate(remarks):
remark_width, _ = self.text_renderer.get_text_size(remark, remarks_font)
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
# 参考原始合成模式的PSD版本实现
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)
# === 第一步创建与常规模式完全相同的最终结果作为Reference ===
canvas = self._create_composite_image(main_image, gradient_start, theme_color)
canvas = self._render_texts(canvas, content, gradient_start)
final_canvas = canvas.resize((1350, 1800), Image.LANCZOS)
# === 第二步为PSD创建可编辑的图层版本 ===
final_size = (1350, 1800)
psd = PSDImage.new("RGBA", final_size, color=(0, 0, 0, 0))
logger.info(f"创建PSD文档尺寸: {final_size}")
# 图层1完美一致的参考图层
composite_layer = PixelLayer.frompil(final_canvas, psd, "Perfect Reference")
psd.append(composite_layer)
logger.info("✓ 添加完美参考图层")
# === 第三步:提供丰富的可编辑图层 ===
# 图层2纯背景图层无任何效果
background_only = main_image.resize(final_size, Image.LANCZOS)
bg_layer = PixelLayer.frompil(background_only, psd, "Background Image")
psd.append(bg_layer)
logger.info("✓ 添加纯背景图层")
# 图层3毛玻璃效果图层
glass_overlay = self._create_glass_overlay_layer(main_image, gradient_start, theme_color)
if glass_overlay:
glass_scaled = glass_overlay.resize(final_size, Image.LANCZOS)
glass_layer = PixelLayer.frompil(glass_scaled, psd, "Glass Overlay")
psd.append(glass_layer)
logger.info("✓ 添加毛玻璃效果图层")
# 图层4-9分离的文字图层
detailed_text_layers = self._create_detailed_text_layers(content, gradient_start, self.size)
for layer_name, layer_image in detailed_text_layers.items():
if layer_image:
text_scaled = layer_image.resize(final_size, Image.LANCZOS)
text_layer = PixelLayer.frompil(text_scaled, psd, layer_name)
psd.append(text_layer)
logger.info(f"✓ 添加文字图层: {layer_name}")
# 图层10整体文字图层备用
text_only_canvas = self._create_text_only_layer(content, gradient_start, self.size)
if text_only_canvas:
text_only_scaled = text_only_canvas.resize(final_size, Image.LANCZOS)
all_text_layer = PixelLayer.frompil(text_only_scaled, psd, "All Text (Backup)")
psd.append(all_text_layer)
logger.info("✓ 添加整体文字备用图层")
logger.info(f"PSD包含{len(list(psd))}个图层1个参考层+{len(list(psd))-1}个编辑层")
# 保存PSD文件
psd.save(output_path)
logger.info(f"✓ PSD文件已保存: {output_path} (尺寸: {psd.size})")
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 _create_text_only_layer(self, content: Dict[str, Any], gradient_start: int, canvas_size: tuple) -> Optional[Image.Image]:
"""创建纯文字图层透明背景模拟_render_texts的渲染逻辑"""
try:
# 创建透明画布
text_canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(text_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 text_canvas
except Exception as e:
logger.error(f"创建纯文字图层失败: {e}")
return None
def _create_text_only_layer(self, content: Dict[str, Any], gradient_start: int, canvas_size: tuple) -> Optional[Image.Image]:
"""创建纯文字图层透明背景模拟_render_texts的渲染逻辑"""
try:
# 创建透明画布
text_canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(text_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 text_canvas
except Exception as e:
logger.error(f"创建纯文字图层失败: {e}")
return None
def _create_detailed_text_layers(self, content: Dict[str, Any], gradient_start: int, canvas_size: tuple) -> Dict[str, Optional[Image.Image]]:
"""创建详细的分离文字图层,每个文字元素独立可编辑"""
layers = {}
try:
width, height = canvas_size
center_x = width // 2
left_margin, right_margin = self._calculate_content_margins(content, width, center_x)
# 1. 标题图层
title_canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
title_draw = ImageDraw.Draw(title_canvas)
title_y = gradient_start + 40
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)
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(title_draw, (title_x, title_y), title_text, title_font,
text_color=(255, 255, 255, 255), outline_color=(0, 30, 80, 200), outline_width=4)
layers["Title Text"] = title_canvas
# 2. 副标题图层
subtitle_canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
subtitle_draw = ImageDraw.Draw(subtitle_canvas)
subtitle_text = content.get("slogan", "")
if subtitle_text:
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)
sub_text_w, sub_text_h = self.text_renderer.get_text_size(subtitle_text, subtitle_font)
subtitle_x = center_x - sub_text_w // 2
subtitle_y = title_y + text_h + 30
self.text_renderer.draw_text_with_shadow(subtitle_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))
layers["Subtitle Text"] = subtitle_canvas
# 3. 页脚图层
footer_canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0))
footer_draw = ImageDraw.Draw(footer_canvas)
footer_y = height - 30
try:
footer_font = ImageFont.truetype(self.chinese_font_path, 18)
except:
footer_font = self.text_renderer._load_default_font(18)
if tag := content.get("tag"):
footer_draw.text((left_margin, footer_y), tag, font=footer_font, fill=(255, 255, 255))
if pagination := content.get("pagination"):
width_pg, _ = self.text_renderer.get_text_size(pagination, footer_font)
footer_draw.text((right_margin - width_pg, footer_y), pagination, font=footer_font, fill=(255, 255, 255))
layers["Footer"] = footer_canvas
except Exception as e:
logger.error(f"创建详细文字图层失败: {e}")
return {k: v for k, v in layers.items() if v is not None}
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