2025-04-17 11:05:46 +08:00
|
|
|
|
import cv2
|
|
|
|
|
|
import os
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
import json
|
|
|
|
|
|
import random
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
class PosterInfo:
|
|
|
|
|
|
def __init__(self, index, main_title, texts):
|
|
|
|
|
|
self.index = index
|
|
|
|
|
|
self.main_title = main_title
|
|
|
|
|
|
self.texts = texts
|
|
|
|
|
|
|
|
|
|
|
|
class PosterConfig:
|
|
|
|
|
|
def __init__(self, config_path):
|
|
|
|
|
|
self.config_path = config_path
|
2025-04-21 09:33:25 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if isinstance(config_path, str) and os.path.exists(config_path):
|
|
|
|
|
|
# 如果是文件路径,从文件读取
|
|
|
|
|
|
print(f"从文件加载配置: {config_path}")
|
|
|
|
|
|
self.config = json.load(open(config_path, "r", encoding="utf-8"))
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果是字符串但不是文件路径,尝试直接解析
|
|
|
|
|
|
print("尝试直接解析配置字符串")
|
|
|
|
|
|
self.config = json.loads(config_path)
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-21 09:33:25 +08:00
|
|
|
|
self.img_list = []
|
|
|
|
|
|
for item in self.config:
|
|
|
|
|
|
print(item)
|
|
|
|
|
|
self.img_list.append([item['index'], item["main_title"], item["texts"]])
|
|
|
|
|
|
# self.img_list.append(PosterInfo(item['img_url'], item['main_title'], item['texts']))
|
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
|
|
print(f"JSON解析错误: {e}")
|
|
|
|
|
|
print(f"尝试解析的内容: {config_path[:100]}...") # 只打印前100个字符
|
|
|
|
|
|
# 创建一个默认配置
|
2025-04-27 09:29:11 +08:00
|
|
|
|
self.config = [{"index": 0, "main_title": "", "texts": [""]}]
|
|
|
|
|
|
self.img_list = [[0, "", [""]]]
|
|
|
|
|
|
print("使用默认的空配置")
|
2025-04-21 09:33:25 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载配置时出错: {e}")
|
|
|
|
|
|
# 创建一个默认配置
|
2025-04-27 09:29:11 +08:00
|
|
|
|
self.config = [{"index": 0, "main_title": " ", "texts": [""]}]
|
|
|
|
|
|
self.img_list = [[0, " ", [""]]]
|
|
|
|
|
|
print("使用默认的空配置")
|
2025-04-21 09:33:25 +08:00
|
|
|
|
|
2025-04-17 11:05:46 +08:00
|
|
|
|
def get_config(self):
|
|
|
|
|
|
return self.config
|
|
|
|
|
|
|
|
|
|
|
|
def get_config_by_index(self, index):
|
2025-04-21 09:33:25 +08:00
|
|
|
|
if index >= len(self.config):
|
|
|
|
|
|
print(f"警告: 索引 {index} 超出配置范围,使用默认配置")
|
|
|
|
|
|
return self.config[0]
|
2025-04-17 11:05:46 +08:00
|
|
|
|
return self.config[index]
|
|
|
|
|
|
|
|
|
|
|
|
class PosterGenerator:
|
2025-04-22 17:25:13 +08:00
|
|
|
|
def __init__(self, base_dir, output_dir=None):
|
2025-04-17 11:05:46 +08:00
|
|
|
|
# 基础路径设置
|
2025-04-22 17:25:13 +08:00
|
|
|
|
if not base_dir or not os.path.isdir(base_dir):
|
|
|
|
|
|
# Handle error: Base directory is essential
|
|
|
|
|
|
raise ValueError(f"Poster assets base directory '{base_dir}' is invalid or not provided.")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
self.base_dir = base_dir
|
2025-04-22 17:25:13 +08:00
|
|
|
|
print(f"Initializing PosterGenerator with asset base: {self.base_dir}") # Log the used base_dir
|
2025-04-17 11:05:46 +08:00
|
|
|
|
self.font_dir = os.path.join(self.base_dir, "font")
|
|
|
|
|
|
self.frame_dir = os.path.join(self.base_dir, "frames") # 边框素材目录
|
|
|
|
|
|
self.sticker_dir = os.path.join(self.base_dir, "stickers") # 贴纸素材目录
|
|
|
|
|
|
self.text_bg_dir = os.path.join(self.base_dir, "text_backgrounds") # 文本框底图目录
|
2025-04-18 15:52:31 +08:00
|
|
|
|
|
|
|
|
|
|
self.img_frame_posbility = 0.7
|
2025-04-24 18:31:45 +08:00
|
|
|
|
self.text_bg_posbility = 0
|
2025-04-17 11:05:46 +08:00
|
|
|
|
# 设置输出目录
|
|
|
|
|
|
if output_dir:
|
|
|
|
|
|
self.output_dir = output_dir
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.output_dir = os.path.join(self.base_dir, "output")
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化资源
|
|
|
|
|
|
self.fonts = self._initialize_fonts()
|
|
|
|
|
|
self.frames = self._initialize_frames()
|
|
|
|
|
|
self.stickers = self._initialize_stickers()
|
|
|
|
|
|
self.text_bgs = self._initialize_text_backgrounds()
|
|
|
|
|
|
|
|
|
|
|
|
# 创建输出目录
|
|
|
|
|
|
os.makedirs(self.output_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 固定使用白色字体蓝色描边效果
|
|
|
|
|
|
self.selected_effect = "白色字体蓝色描边"
|
|
|
|
|
|
|
|
|
|
|
|
# 海报计数器,用于控制添加边框的频率
|
|
|
|
|
|
self.poster_count = 0
|
2025-04-24 19:17:19 +08:00
|
|
|
|
self.default_font_path = os.path.join(self.font_dir, "华康海报体简.ttc")
|
|
|
|
|
|
|
2025-04-17 11:05:46 +08:00
|
|
|
|
def _initialize_text_backgrounds(self):
|
|
|
|
|
|
"""初始化文本框底图"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
text_bg_files = [f for f in os.listdir(self.text_bg_dir) if f.endswith(('.png'))]
|
|
|
|
|
|
print(f"找到文本框底图: {text_bg_files}")
|
|
|
|
|
|
return text_bg_files
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"初始化文本框底图失败: {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def _initialize_fonts(self):
|
|
|
|
|
|
"""初始化字体"""
|
|
|
|
|
|
fonts = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
font_files = [f for f in os.listdir(self.font_dir) if f.endswith(('.ttf', '.otf'))]
|
|
|
|
|
|
print(f"找到字体文件: {font_files}")
|
|
|
|
|
|
return font_files
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"初始化字体失败: {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def _initialize_frames(self):
|
|
|
|
|
|
"""初始化边框素材"""
|
|
|
|
|
|
frames = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
frame_files = [f for f in os.listdir(self.frame_dir) if f.endswith(('.png', '.jpg'))]
|
|
|
|
|
|
print(f"找到边框素材: {len(frame_files)}个")
|
|
|
|
|
|
return frame_files
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"初始化边框失败: {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def _initialize_stickers(self):
|
|
|
|
|
|
"""初始化贴纸素材"""
|
|
|
|
|
|
stickers = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
sticker_files = [f for f in os.listdir(self.sticker_dir) if f.endswith(('.png'))]
|
|
|
|
|
|
print(f"找到贴纸素材: {sticker_files}")
|
|
|
|
|
|
return sticker_files
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"初始化贴纸失败: {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def get_random_font(self):
|
|
|
|
|
|
"""获取随机字体"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 使用正确的字体目录
|
2025-04-24 19:17:19 +08:00
|
|
|
|
font_dir = self.font_dir
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取所有支持的字体文件
|
|
|
|
|
|
font_files = [f for f in os.listdir(font_dir)
|
|
|
|
|
|
if f.lower().endswith(('.ttf', '.otf', '.ttc', '.TTC', '.TTF', '.OTF'))]
|
|
|
|
|
|
|
|
|
|
|
|
if not font_files:
|
|
|
|
|
|
print("警告: 字体目录为空,使用默认字体")
|
|
|
|
|
|
return os.path.join(font_dir, "华康海报体简.ttc")
|
|
|
|
|
|
|
|
|
|
|
|
# 随机选择一个字体文件
|
|
|
|
|
|
font_file = random.choice(font_files)
|
|
|
|
|
|
font_path = os.path.join(font_dir, font_file)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证字体文件是否存在且可读
|
|
|
|
|
|
if not os.path.isfile(font_path):
|
|
|
|
|
|
print(f"警告: 字体文件 {font_file} 不存在,使用默认字体")
|
|
|
|
|
|
return os.path.join(font_dir, "华康海报体简.ttc")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"使用字体: {font_file}")
|
|
|
|
|
|
return font_path
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取字体时出错: {str(e)}")
|
|
|
|
|
|
return os.path.join(font_dir, "华康海报体简.ttc")
|
|
|
|
|
|
|
|
|
|
|
|
def create_base_layer(self, image_path, target_size):
|
2025-04-22 21:26:56 +08:00
|
|
|
|
"""创建底层(图片层)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
image_path: 可以是图片文件路径字符串,也可以是已加载的 PIL Image 对象
|
|
|
|
|
|
target_size: 目标图片尺寸 (width, height)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
调整大小后的 PIL Image 对象
|
|
|
|
|
|
"""
|
2025-04-17 11:05:46 +08:00
|
|
|
|
try:
|
2025-04-22 21:26:56 +08:00
|
|
|
|
# 检查输入类型
|
|
|
|
|
|
if isinstance(image_path, Image.Image):
|
|
|
|
|
|
# 如果已经是 PIL Image 对象
|
|
|
|
|
|
print("输入已是 PIL Image 对象,无需加载")
|
|
|
|
|
|
base_image = image_path.convert('RGBA')
|
|
|
|
|
|
print(f"图像原始尺寸: {base_image.size}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 否则,作为路径处理
|
|
|
|
|
|
# 先验证图片路径
|
|
|
|
|
|
if not image_path:
|
|
|
|
|
|
raise ValueError("图片路径为空")
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(image_path):
|
|
|
|
|
|
raise FileNotFoundError(f"图片文件不存在: {image_path}")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"尝试加载底图: {image_path}")
|
|
|
|
|
|
base_image = Image.open(image_path).convert('RGBA')
|
|
|
|
|
|
print(f"底图加载成功,原始尺寸: {base_image.size}")
|
|
|
|
|
|
|
|
|
|
|
|
# 调整尺寸
|
2025-04-17 11:05:46 +08:00
|
|
|
|
base_image = base_image.resize(target_size, Image.Resampling.LANCZOS)
|
2025-04-22 21:26:56 +08:00
|
|
|
|
print(f"底图调整尺寸完成: {target_size}")
|
|
|
|
|
|
|
2025-04-17 11:05:46 +08:00
|
|
|
|
return base_image
|
2025-04-22 21:26:56 +08:00
|
|
|
|
except FileNotFoundError as e:
|
|
|
|
|
|
print(f"创建底层失败: {e}")
|
|
|
|
|
|
print(f"当前工作目录: {os.getcwd()}")
|
|
|
|
|
|
return Image.new('RGBA', target_size, (255, 255, 255, 255))
|
2025-04-17 11:05:46 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"创建底层失败: {e}")
|
2025-04-22 21:26:56 +08:00
|
|
|
|
traceback.print_exc()
|
2025-04-17 11:05:46 +08:00
|
|
|
|
return Image.new('RGBA', target_size, (255, 255, 255, 255))
|
|
|
|
|
|
|
|
|
|
|
|
def add_frame(self, image, target_size):
|
|
|
|
|
|
"""添加边框"""
|
|
|
|
|
|
if not self.frames:
|
|
|
|
|
|
print("没有可用的边框素材")
|
|
|
|
|
|
return image
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 随机选择一个边框
|
|
|
|
|
|
frame_file = random.choice(self.frames)
|
|
|
|
|
|
frame_path = os.path.join(self.frame_dir, frame_file)
|
|
|
|
|
|
|
|
|
|
|
|
# 加载边框图像
|
|
|
|
|
|
frame_image = Image.open(frame_path).convert('RGBA')
|
|
|
|
|
|
|
|
|
|
|
|
# 调整边框大小以匹配目标尺寸
|
|
|
|
|
|
frame_image = frame_image.resize(target_size, Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建一个新图层用于合成
|
|
|
|
|
|
framed_image = Image.new('RGBA', target_size, (0, 0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# 先放置基础图像
|
|
|
|
|
|
framed_image.paste(image, (0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# 再合成边框图像
|
|
|
|
|
|
framed_image.alpha_composite(frame_image)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"添加边框: {frame_file}")
|
|
|
|
|
|
return framed_image
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"添加边框失败: {e}")
|
|
|
|
|
|
return image
|
|
|
|
|
|
|
|
|
|
|
|
def create_middle_layer(self, target_size):
|
|
|
|
|
|
"""创建中间层(文本框底图)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
middle_layer = Image.new('RGBA', target_size, (0, 0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 首先确定固定的文本区域
|
|
|
|
|
|
width, height = target_size
|
|
|
|
|
|
|
|
|
|
|
|
# 定义文本区域大小
|
|
|
|
|
|
header_height = int(height * 0.2) # 占顶部20%
|
|
|
|
|
|
header_width = int(width * 0.9) # 占宽度的90%
|
|
|
|
|
|
|
|
|
|
|
|
# 标题区域的位置(居中)
|
|
|
|
|
|
title_x = (width - header_width) // 2
|
|
|
|
|
|
title_y = int(height * 0.16) # 从原来的0.05(5%)改为0.12(12%),向下移动
|
|
|
|
|
|
|
|
|
|
|
|
# 保存标题区域信息
|
|
|
|
|
|
self.title_area = {
|
|
|
|
|
|
'x': title_x,
|
|
|
|
|
|
'y': title_y,
|
|
|
|
|
|
'width': header_width,
|
|
|
|
|
|
'height': header_height
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算额外文本的区域(位于底部区域)
|
|
|
|
|
|
additional_text_y = int(height * 0.65) # 位于底部35%的位置
|
|
|
|
|
|
additional_text_height = int(height * 0.1) # 占高度的10%
|
|
|
|
|
|
|
|
|
|
|
|
# 保存额外文本区域信息
|
|
|
|
|
|
self.additional_text_area = {
|
|
|
|
|
|
'x': int(width * 0.1), # 左边距10%
|
|
|
|
|
|
'y': additional_text_y,
|
|
|
|
|
|
'width': int(width * 0.8), # 占宽度的80%
|
|
|
|
|
|
'height': additional_text_height
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 输出文本区域信息
|
|
|
|
|
|
print("文本区域信息:")
|
|
|
|
|
|
print(f"主标题区域: x={self.title_area['x']}, y={self.title_area['y']}, "
|
|
|
|
|
|
f"宽={self.title_area['width']}, 高={self.title_area['height']}")
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 检查是否使用文本框底图
|
2025-04-18 15:52:31 +08:00
|
|
|
|
use_text_bg = random.random() < self.text_bg_posbility
|
2025-04-17 11:05:46 +08:00
|
|
|
|
print(f"环境变量USE_TEXT_BG设置为: {os.environ.get('USE_TEXT_BG', 'True')}, 是否使用文本框底图: {use_text_bg}")
|
2025-04-18 15:52:31 +08:00
|
|
|
|
|
2025-04-17 11:05:46 +08:00
|
|
|
|
if use_text_bg and self.text_bgs:
|
|
|
|
|
|
print("根据环境变量设置,使用文本框底图")
|
|
|
|
|
|
# 随机选择文本背景
|
|
|
|
|
|
text_bg_path = os.path.join(self.text_bg_dir, random.choice(self.text_bgs))
|
|
|
|
|
|
text_bg = Image.open(text_bg_path).convert('RGBA')
|
|
|
|
|
|
|
|
|
|
|
|
# 计算文本框底图的尺寸,适度放大
|
|
|
|
|
|
bg_width = int(width * 1.9) # 宽度为画布宽度的130%
|
|
|
|
|
|
bg_height = int(height * 0.85) # 高度为画布高度的50%
|
|
|
|
|
|
|
|
|
|
|
|
# 计算主标题的中心点
|
|
|
|
|
|
title_center_x = self.title_area['x'] + self.title_area['width'] // 2
|
|
|
|
|
|
title_center_y = self.title_area['y'] + self.title_area['height'] // 2
|
|
|
|
|
|
|
|
|
|
|
|
# 根据主标题中心点计算底图位置,确保完全对齐
|
|
|
|
|
|
bg_x = title_center_x - (bg_width // 2) # 从标题中心点向两侧扩展
|
|
|
|
|
|
bg_y = title_center_y - (bg_height // 2) # 从标题中心点向上下扩展
|
|
|
|
|
|
|
|
|
|
|
|
# 调整文本背景大小
|
|
|
|
|
|
text_bg = text_bg.resize((bg_width, bg_height), Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建临时图层并合并
|
|
|
|
|
|
temp = Image.new('RGBA', target_size, (0, 0, 0, 0))
|
|
|
|
|
|
temp.paste(text_bg, (bg_x, bg_y), text_bg)
|
|
|
|
|
|
middle_layer.alpha_composite(temp)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"文本背景: 宽={bg_width}px (画布宽度的{bg_width/width:.1f}倍), "
|
|
|
|
|
|
f"高={bg_height}px (画布高度的{bg_height/height:.1f}倍)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("根据环境变量设置,不使用文本框底图,仅使用固定文本区域")
|
|
|
|
|
|
|
|
|
|
|
|
return middle_layer
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"创建中间层时出错: {str(e)}")
|
|
|
|
|
|
traceback.print_exc() # 打印详细错误信息
|
|
|
|
|
|
return Image.new('RGBA', target_size, (0, 0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
def create_text_layer(self, target_size, text_data=None):
|
|
|
|
|
|
"""创建文字层"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
text_layer = Image.new('RGBA', target_size, (0, 0, 0, 0))
|
|
|
|
|
|
draw = ImageDraw.Draw(text_layer)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用固定效果:字体蓝色立体效果
|
|
|
|
|
|
self.selected_effect = "文字蓝色立体效果"
|
|
|
|
|
|
print(f"使用文字效果: {self.selected_effect}")
|
|
|
|
|
|
|
2025-04-22 21:26:56 +08:00
|
|
|
|
# 检查文字数据
|
2025-04-17 11:05:46 +08:00
|
|
|
|
if text_data is None:
|
2025-04-25 17:35:43 +08:00
|
|
|
|
return text_layer
|
2025-04-22 21:26:56 +08:00
|
|
|
|
|
|
|
|
|
|
print(f"处理文本数据: {text_data}")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 1. 处理主标题
|
2025-04-22 21:26:56 +08:00
|
|
|
|
if hasattr(self, 'title_area') and 'title' in text_data and text_data['title']:
|
2025-04-17 11:05:46 +08:00
|
|
|
|
font_path = self._get_font_path()
|
|
|
|
|
|
title = text_data['title']
|
|
|
|
|
|
|
|
|
|
|
|
# 使用柠檬黄色作为主标题颜色
|
|
|
|
|
|
lemon_yellow = (255, 250, 55, 255) # 柠檬黄色
|
|
|
|
|
|
|
|
|
|
|
|
# 使用统一定义的标题区域
|
|
|
|
|
|
font, text_width, text_height, x, y = self._calculate_text_layout(
|
|
|
|
|
|
draw, font_path, title, self.title_area, size_factor=0.75)
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制主标题文字
|
|
|
|
|
|
self._draw_text_with_effects(draw, title, font, x, y,
|
|
|
|
|
|
shadow_offset=8,
|
|
|
|
|
|
color=lemon_yellow)
|
|
|
|
|
|
|
|
|
|
|
|
# 打印调试信息
|
|
|
|
|
|
self._print_text_debug_info("主标题", font, text_width, x, y, font_path)
|
|
|
|
|
|
print(f"- 主标题颜色: 柠檬黄色 RGB(255, 250, 55)")
|
2025-04-22 21:26:56 +08:00
|
|
|
|
else:
|
|
|
|
|
|
print("警告: 无法处理主标题,可能缺少标题数据或title_area未定义")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 2. 处理副标题(如果有)
|
|
|
|
|
|
if hasattr(self, 'title_area') and 'subtitle' in text_data and text_data['subtitle']:
|
|
|
|
|
|
subtitle = text_data['subtitle']
|
|
|
|
|
|
# 计算副标题区域(在主标题下方)
|
|
|
|
|
|
subtitle_area = {
|
|
|
|
|
|
'x': self.title_area['x'],
|
|
|
|
|
|
'y': self.title_area['y'] + self.title_area['height'],
|
|
|
|
|
|
'width': self.title_area['width'],
|
|
|
|
|
|
'height': int(self.title_area['height'] * 0.5) # 副标题高度为主标题的一半
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 获取副标题字体和布局
|
|
|
|
|
|
font, text_width, text_height, x, y = self._calculate_text_layout(
|
|
|
|
|
|
draw, font_path, subtitle, subtitle_area, size_factor=0.6)
|
|
|
|
|
|
|
|
|
|
|
|
# 副标题保持白色
|
|
|
|
|
|
white_color = (255, 255, 255, 255)
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制副标题
|
|
|
|
|
|
self._draw_text_with_effects(draw, subtitle, font, x, y,
|
|
|
|
|
|
shadow_offset=8,
|
|
|
|
|
|
color=white_color)
|
|
|
|
|
|
|
|
|
|
|
|
# 打印调试信息
|
|
|
|
|
|
self._print_text_debug_info("副标题", font, text_width, x, y, font_path)
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 处理额外文本(如果有)
|
|
|
|
|
|
if 'additional_texts' in text_data and text_data['additional_texts']:
|
2025-04-22 21:26:56 +08:00
|
|
|
|
# 打印接收到的额外文本
|
|
|
|
|
|
print(f"接收到额外文本数据: {text_data['additional_texts']}")
|
|
|
|
|
|
|
2025-04-17 11:05:46 +08:00
|
|
|
|
# 过滤掉空文本项
|
2025-04-22 21:26:56 +08:00
|
|
|
|
valid_additional_texts = []
|
|
|
|
|
|
for item in text_data['additional_texts']:
|
|
|
|
|
|
if isinstance(item, dict) and 'text' in item and item['text']:
|
|
|
|
|
|
valid_additional_texts.append(item)
|
|
|
|
|
|
elif isinstance(item, str) and item:
|
|
|
|
|
|
# 如果是字符串,转换为字典格式
|
2025-04-25 15:29:50 +08:00
|
|
|
|
valid_additional_texts.append({"text": item, "position": "middle", "size_factor": 0.5})
|
2025-04-22 21:26:56 +08:00
|
|
|
|
|
|
|
|
|
|
print(f"有效额外文本项: {len(valid_additional_texts)}")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 21:26:56 +08:00
|
|
|
|
if valid_additional_texts and hasattr(self, 'title_area'):
|
2025-04-17 11:05:46 +08:00
|
|
|
|
# 获取主标题的字体大小
|
2025-04-22 21:26:56 +08:00
|
|
|
|
main_title_font_size = font.size if 'font' in locals() else 48 # 默认字体大小
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 使用固定字体
|
2025-04-24 19:17:19 +08:00
|
|
|
|
specific_font_path = self.default_font_path
|
|
|
|
|
|
# specific_font_path = os.path.join(self.font_dir, "华康海报体简.ttc")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
if not os.path.isfile(specific_font_path):
|
2025-04-22 21:26:56 +08:00
|
|
|
|
specific_font_path = font_path if 'font_path' in locals() else self._get_font_path()
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 计算额外文本在屏幕上的位置
|
|
|
|
|
|
height = target_size[1]
|
|
|
|
|
|
width = target_size[0]
|
|
|
|
|
|
|
|
|
|
|
|
# 将垂直位置调整到主标题下方
|
|
|
|
|
|
title_bottom = self.title_area['y'] + self.title_area['height']
|
|
|
|
|
|
extra_text_y_start = title_bottom + int(height * 0.01) # 从原来的0.05(5%)减小到0.02(2%)
|
|
|
|
|
|
extra_text_height = int(height * 0.2)
|
|
|
|
|
|
|
|
|
|
|
|
# 安全边距
|
|
|
|
|
|
safe_margin_x = int(width * 0.05)
|
|
|
|
|
|
max_text_width = width - (safe_margin_x * 2)
|
|
|
|
|
|
|
|
|
|
|
|
# 总文本行数
|
2025-04-22 21:26:56 +08:00
|
|
|
|
total_lines = len(valid_additional_texts)
|
2025-04-17 11:05:46 +08:00
|
|
|
|
line_height = extra_text_height // total_lines if total_lines > 0 else 0
|
|
|
|
|
|
|
|
|
|
|
|
print(f"额外文本区域: y={extra_text_y_start}, 高度={extra_text_height}, 每行高度={line_height}")
|
|
|
|
|
|
print(f"文本安全宽度: {max_text_width}px (留出两侧各{safe_margin_x}px安全边距)")
|
|
|
|
|
|
print(f"文本颜色: 统一白色")
|
|
|
|
|
|
|
|
|
|
|
|
# 渲染每一行文本
|
2025-04-22 21:26:56 +08:00
|
|
|
|
for i, text_item in enumerate(valid_additional_texts):
|
2025-04-17 11:05:46 +08:00
|
|
|
|
item_text = text_item['text']
|
|
|
|
|
|
|
2025-04-22 21:26:56 +08:00
|
|
|
|
# 检查文本内容
|
|
|
|
|
|
if not item_text:
|
|
|
|
|
|
print(f"警告: 额外文本项 {i+1} 文本为空,跳过")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 设置字体大小为主标题的0.8倍或使用指定的size_factor
|
|
|
|
|
|
size_factor = text_item.get('size_factor', 0.8)
|
2025-04-17 11:05:46 +08:00
|
|
|
|
font_size = int(main_title_font_size * size_factor)
|
|
|
|
|
|
text_font = ImageFont.truetype(specific_font_path, font_size)
|
|
|
|
|
|
|
|
|
|
|
|
# 测量文本宽度并调整
|
|
|
|
|
|
text_width = draw.textlength(item_text, font=text_font)
|
|
|
|
|
|
|
|
|
|
|
|
# 保护措施:如果文本宽度超出安全区域,逐步缩小字体直到适合
|
|
|
|
|
|
while text_width > max_text_width and font_size > int(main_title_font_size * 0.4):
|
|
|
|
|
|
size_factor *= 0.95
|
|
|
|
|
|
font_size = int(main_title_font_size * size_factor)
|
|
|
|
|
|
text_font = ImageFont.truetype(specific_font_path, font_size)
|
|
|
|
|
|
text_width = draw.textlength(item_text, font=text_font)
|
|
|
|
|
|
|
|
|
|
|
|
# 再次测量调整后的文本尺寸
|
|
|
|
|
|
text_bbox = draw.textbbox((0, 0), item_text, font=text_font)
|
|
|
|
|
|
text_height = text_bbox[3] - text_bbox[1]
|
|
|
|
|
|
|
2025-04-22 21:26:56 +08:00
|
|
|
|
# 获取位置参数
|
|
|
|
|
|
position = text_item.get('position', 'bottom')
|
|
|
|
|
|
|
|
|
|
|
|
# 根据位置设置垂直位置
|
|
|
|
|
|
if position == 'top':
|
|
|
|
|
|
line_y = int(height * 0.05) + (i * line_height)
|
|
|
|
|
|
elif position == 'middle':
|
2025-04-25 15:29:50 +08:00
|
|
|
|
line_y = int(height * 0.4) + (i * line_height)
|
2025-04-22 21:26:56 +08:00
|
|
|
|
else: # position == 'bottom' 或其他
|
|
|
|
|
|
# 在底部区域,使用更大的垂直间距,比如整个海报高度的65%到85%
|
|
|
|
|
|
bottom_start = int(height * 0.65)
|
|
|
|
|
|
bottom_height = int(height * 0.2)
|
|
|
|
|
|
bottom_line_height = bottom_height // total_lines if total_lines > 0 else 0
|
|
|
|
|
|
line_y = bottom_start + (i * bottom_line_height)
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 水平居中位置
|
|
|
|
|
|
line_x = (width - text_width) // 2
|
|
|
|
|
|
|
|
|
|
|
|
# 统一使用白色文本
|
|
|
|
|
|
text_color = (255, 255, 255, 255) # 白色
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制文本
|
|
|
|
|
|
self._draw_text_with_effects(draw, item_text, text_font, line_x, line_y,
|
|
|
|
|
|
shadow_offset=3,
|
|
|
|
|
|
color=text_color)
|
|
|
|
|
|
|
|
|
|
|
|
# 打印调试信息
|
|
|
|
|
|
print(f"额外文本{i+1}: '{item_text}'")
|
|
|
|
|
|
print(f"- 文本颜色: 白色")
|
|
|
|
|
|
print(f"- 字体大小: {font_size}px (主标题的{size_factor:.2f}倍)")
|
|
|
|
|
|
print(f"- 位置: x={line_x}, y={line_y}")
|
2025-04-22 21:26:56 +08:00
|
|
|
|
else:
|
|
|
|
|
|
print("无法处理额外文本:没有有效的额外文本项或title_area未定义")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
return text_layer
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"创建文字层时出错: {str(e)}")
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return Image.new('RGBA', target_size, (0, 0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
def _get_font_path(self):
|
|
|
|
|
|
"""获取并验证字体路径"""
|
|
|
|
|
|
# 获取随机字体
|
|
|
|
|
|
font_path = self.get_random_font()
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 尝试加载字体
|
|
|
|
|
|
ImageFont.truetype(font_path, size=1) # 先用小尺寸测试字体是否可用
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载字体失败: {str(e)},使用默认字体")
|
2025-04-24 19:17:19 +08:00
|
|
|
|
font_path = self.default_font_path
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
return font_path
|
|
|
|
|
|
|
|
|
|
|
|
def _get_text_area(self, text_item, target_size, index):
|
|
|
|
|
|
"""获取文本区域"""
|
|
|
|
|
|
width, height = target_size
|
|
|
|
|
|
|
|
|
|
|
|
# 根据指定位置创建文本区域
|
|
|
|
|
|
position = text_item.get('position', 'bottom')
|
|
|
|
|
|
|
|
|
|
|
|
if position == 'custom' and 'x' in text_item and 'y' in text_item:
|
|
|
|
|
|
# 自定义位置
|
|
|
|
|
|
text_area = {
|
|
|
|
|
|
'x': text_item['x'],
|
|
|
|
|
|
'y': text_item['y'],
|
|
|
|
|
|
'width': text_item.get('width', width // 2),
|
|
|
|
|
|
'height': text_item.get('height', height // 10)
|
|
|
|
|
|
}
|
|
|
|
|
|
elif position == 'top':
|
|
|
|
|
|
# 顶部区域
|
|
|
|
|
|
text_area = {
|
|
|
|
|
|
'x': int(width * 0.1),
|
|
|
|
|
|
'y': int(height * 0.05),
|
|
|
|
|
|
'width': int(width * 0.8),
|
|
|
|
|
|
'height': int(height * 0.1)
|
|
|
|
|
|
}
|
|
|
|
|
|
elif position == 'middle':
|
|
|
|
|
|
# 中部区域
|
|
|
|
|
|
text_area = {
|
|
|
|
|
|
'x': int(width * 0.1),
|
|
|
|
|
|
'y': int(height * 0.45),
|
|
|
|
|
|
'width': int(width * 0.8),
|
|
|
|
|
|
'height': int(height * 0.1)
|
|
|
|
|
|
}
|
|
|
|
|
|
else: # 默认为底部
|
|
|
|
|
|
# 底部区域,增加随机性
|
|
|
|
|
|
# 从图像的一半高开始(height * 0.5)到底部的0.9位置之间随机选择
|
|
|
|
|
|
random_y_position = random.uniform(0.5, 0.85)
|
|
|
|
|
|
# 水平位置也增加随机性,在10%到30%之间
|
|
|
|
|
|
random_x_position = random.uniform(0.05, 0.1)
|
|
|
|
|
|
|
|
|
|
|
|
# 多个底部文本时的偏移
|
|
|
|
|
|
offset = index * 0.08
|
|
|
|
|
|
|
|
|
|
|
|
text_area = {
|
|
|
|
|
|
'x': int(width * random_x_position),
|
|
|
|
|
|
'y': int(height * (random_y_position + offset)),
|
|
|
|
|
|
'width': int(width * 0.8),
|
|
|
|
|
|
'height': int(height * 0.1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return text_area
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_text_layout(self, draw, font_path, text, text_area, size_factor=0.85, align_left=False):
|
|
|
|
|
|
"""计算文本布局、字体大小和位置
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
draw: ImageDraw对象
|
|
|
|
|
|
font_path: 字体路径
|
|
|
|
|
|
text: 文本内容
|
|
|
|
|
|
text_area: 文本区域字典
|
|
|
|
|
|
size_factor: 字体大小因子(相对于区域高度)
|
|
|
|
|
|
align_left: 是否左对齐
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 设置初始字体大小
|
|
|
|
|
|
initial_font_size = int(text_area['height'] * size_factor)
|
|
|
|
|
|
font = ImageFont.truetype(font_path, initial_font_size)
|
|
|
|
|
|
|
|
|
|
|
|
# 调整字体大小以适应宽度(目标是占据95%的宽度)
|
|
|
|
|
|
target_width = text_area['width'] * 0.98 # 增加宽度利用率到98%
|
|
|
|
|
|
text_width = draw.textlength(text, font=font)
|
|
|
|
|
|
|
|
|
|
|
|
# 字体大小调整逻辑优化:先尝试增大字体以更好地填充宽度
|
|
|
|
|
|
if text_width < target_width * 0.9 and initial_font_size < int(text_area['height'] * 0.98):
|
|
|
|
|
|
# 如果文本宽度小于目标宽度的90%,尝试增大字体
|
|
|
|
|
|
while text_width < target_width * 0.9 and initial_font_size < int(text_area['height'] * 0.98):
|
|
|
|
|
|
initial_font_size += 2
|
|
|
|
|
|
font = ImageFont.truetype(font_path, initial_font_size)
|
|
|
|
|
|
text_width = draw.textlength(text, font=font)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果字体太大,再缩小字体以适应宽度
|
|
|
|
|
|
while text_width > target_width and initial_font_size > 10:
|
|
|
|
|
|
initial_font_size -= 2
|
|
|
|
|
|
font = ImageFont.truetype(font_path, initial_font_size)
|
|
|
|
|
|
text_width = draw.textlength(text, font=font)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取文字高度
|
|
|
|
|
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
|
|
|
|
|
text_height = text_bbox[3] - text_bbox[1]
|
|
|
|
|
|
|
|
|
|
|
|
# 计算位置
|
|
|
|
|
|
if align_left:
|
|
|
|
|
|
# 左对齐
|
|
|
|
|
|
x = text_area['x'] + int(text_area['width'] * 0.05) # 5% 的左边距
|
|
|
|
|
|
y = text_area['y'] + (text_area['height'] - text_height) // 2
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 居中对齐
|
|
|
|
|
|
x = text_area['x'] + (text_area['width'] - text_width) // 2
|
|
|
|
|
|
y = text_area['y'] + (text_area['height'] - text_height) // 2
|
|
|
|
|
|
|
|
|
|
|
|
return font, text_width, text_height, x, y
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_text_with_effects(self, draw, text, font, x, y, shadow_offset=8, color=(255, 255, 255, 255)):
|
|
|
|
|
|
"""绘制带立体效果的文字
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
draw: ImageDraw对象
|
|
|
|
|
|
text: 文本内容
|
|
|
|
|
|
font: 字体对象
|
|
|
|
|
|
x, y: 文本坐标
|
|
|
|
|
|
shadow_offset: 立体深度
|
|
|
|
|
|
color: 文本表面颜色
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 文字颜色使用传入的color参数
|
|
|
|
|
|
text_color = color
|
|
|
|
|
|
|
|
|
|
|
|
# 蓝色立体效果 - 只用一种深蓝色
|
|
|
|
|
|
blue_color = (29, 60, 171, 255) # 深蓝色
|
|
|
|
|
|
|
|
|
|
|
|
# 立体深度 - 使用更大的深度
|
|
|
|
|
|
depth = 8 # 8像素的立体深度
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 首先绘制深蓝色立体部分
|
|
|
|
|
|
for i in range(1, depth + 1):
|
|
|
|
|
|
# 下方
|
|
|
|
|
|
draw.text((x, y + i), text, font=font, fill=blue_color)
|
|
|
|
|
|
# 右侧
|
|
|
|
|
|
draw.text((x + i, y), text, font=font, fill=blue_color)
|
|
|
|
|
|
# 右下角
|
|
|
|
|
|
draw.text((x + i, y + i), text, font=font, fill=blue_color)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 绘制蓝色描边 - 增强文字的边缘清晰度
|
|
|
|
|
|
stroke_thickness = 2
|
|
|
|
|
|
|
|
|
|
|
|
for dx in range(-stroke_thickness, stroke_thickness+1):
|
|
|
|
|
|
for dy in range(-stroke_thickness, stroke_thickness+1):
|
|
|
|
|
|
if dx == 0 and dy == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
# 计算当前点到中心的距离
|
|
|
|
|
|
distance = (dx**2 + dy**2)**0.5
|
|
|
|
|
|
# 如果距离在描边范围内,绘制蓝色描边
|
|
|
|
|
|
if distance <= stroke_thickness:
|
|
|
|
|
|
draw.text((x + dx, y + dy), text, font=font, fill=blue_color)
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 最后绘制主文字在最上层,使用传入的颜色
|
|
|
|
|
|
draw.text((x, y), text, font=font, fill=text_color)
|
|
|
|
|
|
|
|
|
|
|
|
def _print_text_debug_info(self, text_type, font, text_width, x, y, font_path):
|
2025-04-22 21:26:56 +08:00
|
|
|
|
"""打印文本调试信息"""
|
|
|
|
|
|
print(f"- {text_type} 字体大小: {font.size}px")
|
|
|
|
|
|
print(f"- {text_type} 文本宽度: {text_width}px")
|
|
|
|
|
|
print(f"- {text_type} 位置: x={x}, y={y}")
|
|
|
|
|
|
print(f"- {text_type} 使用字体: {os.path.basename(font_path)}")
|
2025-04-22 18:14:31 +08:00
|
|
|
|
|
2025-04-22 21:26:56 +08:00
|
|
|
|
def add_stickers(self, poster_image):
|
|
|
|
|
|
"""
|
|
|
|
|
|
在海报上添加装饰性贴纸
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
poster_image: 要添加贴纸的海报图像对象
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
添加了贴纸的海报图像对象
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not hasattr(self, 'sticker_files') or not self.sticker_files:
|
|
|
|
|
|
print("没有可用的贴纸素材,跳过贴纸添加")
|
|
|
|
|
|
return poster_image
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取海报尺寸
|
|
|
|
|
|
width, height = poster_image.size
|
|
|
|
|
|
|
|
|
|
|
|
# 决定是否添加贴纸(50%概率)
|
|
|
|
|
|
if random.random() < 0.5:
|
|
|
|
|
|
print("随机决定不添加贴纸")
|
|
|
|
|
|
return poster_image
|
|
|
|
|
|
|
|
|
|
|
|
# 随机决定添加1-3个贴纸
|
|
|
|
|
|
sticker_count = random.randint(1, 3)
|
|
|
|
|
|
print(f"准备添加 {sticker_count} 个贴纸")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建一个新图层用于合成
|
|
|
|
|
|
result_image = poster_image.copy()
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(sticker_count):
|
|
|
|
|
|
# 随机选择一个贴纸
|
|
|
|
|
|
sticker_file = random.choice(self.sticker_files)
|
|
|
|
|
|
sticker_path = os.path.join(self.sticker_dir, sticker_file)
|
|
|
|
|
|
|
|
|
|
|
|
# 加载贴纸图像
|
|
|
|
|
|
sticker = Image.open(sticker_path).convert('RGBA')
|
|
|
|
|
|
|
|
|
|
|
|
# 调整贴纸大小(原始尺寸的10%-30%)
|
|
|
|
|
|
sticker_size_factor = random.uniform(0.1, 0.3)
|
|
|
|
|
|
new_width = int(width * sticker_size_factor)
|
|
|
|
|
|
new_height = int(new_width * sticker.height / sticker.width) # 保持纵横比
|
|
|
|
|
|
sticker = sticker.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
|
|
|
# 随机选择贴纸位置(避开中央区域)
|
|
|
|
|
|
margin = int(width * 0.1) # 边缘区域的10%
|
|
|
|
|
|
|
|
|
|
|
|
# 生成随机位置(避开中间区域)
|
|
|
|
|
|
if random.random() < 0.5: # 左/右边缘
|
|
|
|
|
|
x = random.randint(margin, int(width * 0.25)) if random.random() < 0.5 else random.randint(int(width * 0.75), width - new_width - margin)
|
|
|
|
|
|
y = random.randint(margin, height - new_height - margin)
|
|
|
|
|
|
else: # 上/下边缘
|
|
|
|
|
|
x = random.randint(margin, width - new_width - margin)
|
|
|
|
|
|
y = random.randint(margin, int(height * 0.25)) if random.random() < 0.5 else random.randint(int(height * 0.75), height - new_height - margin)
|
|
|
|
|
|
|
|
|
|
|
|
# 将贴纸粘贴到结果图像上
|
|
|
|
|
|
result_image.paste(sticker, (x, y), sticker)
|
|
|
|
|
|
print(f"添加贴纸 {i+1}: {sticker_file}, 大小: {new_width}x{new_height}, 位置: ({x}, {y})")
|
|
|
|
|
|
|
|
|
|
|
|
return result_image
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"添加贴纸失败: {e}")
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return poster_image
|
|
|
|
|
|
|
|
|
|
|
|
def create_poster(self, image_input, text_data):
|
2025-04-22 18:14:31 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Creates a poster by combining the base image, frame (optional), stickers (optional),
|
|
|
|
|
|
and text layers.
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
Args:
|
2025-04-22 21:26:56 +08:00
|
|
|
|
image_input: 底图输入,可以是图片路径字符串或 PIL Image 对象
|
2025-04-22 18:14:31 +08:00
|
|
|
|
text_data: Dictionary containing text information (
|
|
|
|
|
|
{
|
|
|
|
|
|
'title': 'Main Title Text',
|
|
|
|
|
|
'subtitle': 'Subtitle Text (optional)',
|
|
|
|
|
|
'additional_texts': [{'text': '...', 'position': 'top/bottom/center', 'size_factor': 0.8}, ...]
|
|
|
|
|
|
}
|
|
|
|
|
|
).
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
A PIL Image object representing the final poster, or None if creation failed.
|
|
|
|
|
|
"""
|
|
|
|
|
|
target_size = (900, 1200) # TODO: Make target_size a parameter?
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n--- Creating Poster --- ")
|
2025-04-22 21:26:56 +08:00
|
|
|
|
if isinstance(image_input, Image.Image):
|
|
|
|
|
|
print(f"Input: PIL Image 对象,尺寸 {image_input.size}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"Input Image: {image_input}")
|
2025-04-22 18:14:31 +08:00
|
|
|
|
print(f"Text Data: {text_data}")
|
|
|
|
|
|
|
2025-04-17 11:05:46 +08:00
|
|
|
|
try:
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# 1. 创建底层(图片)
|
2025-04-22 21:26:56 +08:00
|
|
|
|
base_layer = self.create_base_layer(image_input, target_size)
|
2025-04-22 18:14:31 +08:00
|
|
|
|
if not base_layer:
|
|
|
|
|
|
raise ValueError("Failed to create base layer.")
|
|
|
|
|
|
print("Base layer created.")
|
|
|
|
|
|
|
|
|
|
|
|
# 2. (可选) 添加边框 - 根据计数器决定
|
|
|
|
|
|
# if self.poster_count % 5 == 0: # 每5张海报添加一次边框
|
|
|
|
|
|
if random.random() < self.img_frame_posbility:
|
|
|
|
|
|
print("Attempting to add frame...")
|
|
|
|
|
|
base_layer = self.add_frame(base_layer, target_size)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("Skipping frame addition.")
|
|
|
|
|
|
self.poster_count += 1 # 增加计数器
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# 3. 创建中间层(文本框底图)
|
|
|
|
|
|
# middle_layer 包含文本区域定义 (self.title_area, self.additional_text_area)
|
|
|
|
|
|
middle_layer = self.create_middle_layer(target_size)
|
|
|
|
|
|
print("Middle layer (text areas defined) created.")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# 4. 创建文本层
|
2025-04-17 11:05:46 +08:00
|
|
|
|
text_layer = self.create_text_layer(target_size, text_data)
|
2025-04-22 18:14:31 +08:00
|
|
|
|
print("Text layer created.")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# 5. 合成图层
|
|
|
|
|
|
# Start with the base layer (which might already have a frame)
|
|
|
|
|
|
final_poster = base_layer
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# Add middle layer (text backgrounds)
|
|
|
|
|
|
final_poster.alpha_composite(middle_layer)
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# Add text layer
|
|
|
|
|
|
final_poster.alpha_composite(text_layer)
|
|
|
|
|
|
|
|
|
|
|
|
# (可选) 添加贴纸
|
|
|
|
|
|
final_poster = self.add_stickers(final_poster)
|
|
|
|
|
|
print("Layers composed.")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
2025-04-22 18:14:31 +08:00
|
|
|
|
# 转换回 RGB 以保存为 JPG/PNG (移除 alpha通道)
|
|
|
|
|
|
final_poster_rgb = final_poster.convert("RGB")
|
|
|
|
|
|
|
|
|
|
|
|
# 移除保存逻辑,直接返回 Image 对象
|
|
|
|
|
|
# final_save_path = os.path.join(self.output_dir, output_name)
|
|
|
|
|
|
# os.makedirs(os.path.dirname(final_save_path), exist_ok=True)
|
|
|
|
|
|
# final_poster_rgb.save(final_save_path)
|
|
|
|
|
|
# print(f"Final poster saved to: {final_save_path}")
|
|
|
|
|
|
# return final_save_path
|
|
|
|
|
|
return final_poster_rgb
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-04-22 18:14:31 +08:00
|
|
|
|
print(f"Error creating poster: {e}")
|
|
|
|
|
|
traceback.print_exc()
|
2025-04-17 11:05:46 +08:00
|
|
|
|
return None
|
|
|
|
|
|
|
2025-04-24 19:09:50 +08:00
|
|
|
|
def set_img_frame_possibility(self, img_frame_possibility):
|
|
|
|
|
|
self.img_frame_posbility = img_frame_possibility
|
2025-04-22 18:14:31 +08:00
|
|
|
|
|
2025-04-24 19:09:50 +08:00
|
|
|
|
def set_text_bg_possibility(self, text_bg_possibility):
|
|
|
|
|
|
self.text_bg_posbility = text_bg_possibility
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
# 设置是否使用文本框底图(True为使用,False为不使用)
|
|
|
|
|
|
os.environ['USE_TEXT_BG'] = 'False' # 或 'True'
|
|
|
|
|
|
print(f"设置环境变量USE_TEXT_BG为: {os.environ['USE_TEXT_BG']}")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建生成器实例
|
2025-04-22 17:25:13 +08:00
|
|
|
|
generator = PosterGenerator("/root/autodl-tmp/poster_baseboard_0403")
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
poster_config_path = "/root/autodl-tmp/poster_generate_result/2025-04-16_20-49-32.json"
|
|
|
|
|
|
poster_config = PosterConfig(poster_config_path)
|
|
|
|
|
|
for item in poster_config.get_config():
|
|
|
|
|
|
text_data = {
|
|
|
|
|
|
"title": f"{item['main_title']}",
|
|
|
|
|
|
"subtitle": "",
|
|
|
|
|
|
"additional_texts": [
|
|
|
|
|
|
{"text": f"{item['texts'][0]}", "position": "bottom", "size_factor": 0.5},
|
|
|
|
|
|
{"text": f"{item['texts'][1]}", "position": "bottom", "size_factor": 0.5}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
# 处理目录中的所有图片
|
|
|
|
|
|
img_path = "/root/autodl-tmp/poster_baseboard_0403/output_collage/random_collage_1_collage.png"
|
2025-04-22 21:26:56 +08:00
|
|
|
|
|
|
|
|
|
|
# 先加载图片,然后传递 PIL Image 对象
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 先加载图片
|
|
|
|
|
|
collage_img = Image.open(img_path).convert('RGBA')
|
|
|
|
|
|
print(f"已加载拼贴图: {img_path}, 尺寸: {collage_img.size}")
|
|
|
|
|
|
|
|
|
|
|
|
# 传递图片对象而不是路径
|
|
|
|
|
|
generator.create_poster(collage_img, text_data)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载或处理图片时出错: {e}")
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
# 失败时回退到使用路径
|
|
|
|
|
|
generator.create_poster(img_path, text_data)
|
2025-04-17 11:05:46 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|