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 self.config = json.load(open(config_path, "r", encoding="utf-8")) 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'])) def get_config(self): return self.config def get_config_by_index(self, index): return self.config[index] class PosterGenerator: def __init__(self,base_dir="/root/autodl-tmp/poster_baseboard_0403", output_dir=None): # 基础路径设置 self.base_dir = base_dir 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") # 文本框底图目录 # 设置输出目录 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 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: # 使用正确的字体目录 font_dir = "/root/autodl-tmp/poster_baseboard_0403/font" # 获取所有支持的字体文件 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): """创建底层(图片层)""" try: base_image = Image.open(image_path).convert('RGBA') base_image = base_image.resize(target_size, Image.Resampling.LANCZOS) return base_image except Exception as e: print(f"创建底层失败: {e}") 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. 检查是否使用文本框底图 use_text_bg = os.environ.get('USE_TEXT_BG', 'True').lower() in ('true', '1', 't', 'yes') print(f"环境变量USE_TEXT_BG设置为: {os.environ.get('USE_TEXT_BG', 'True')}, 是否使用文本框底图: {use_text_bg}") 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}") # 如果没有文字数据,使用默认值 if text_data is None: text_data = {'title': '泰宁县 甘露岩寺'} # 1. 处理主标题 if hasattr(self, 'title_area') and 'title' in text_data: 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)") # 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']: # 过滤掉空文本项 additional_texts = [item for item in text_data['additional_texts'] if item.get('text')] if additional_texts and hasattr(self, 'title_area'): # 获取主标题的字体大小 main_title_font_size = font.size # 使用固定字体 specific_font_path = os.path.join("/root/autodl-tmp/poster_baseboard_0403/font", "华康海报体简.ttc") if not os.path.isfile(specific_font_path): specific_font_path = font_path # 计算额外文本在屏幕上的位置 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) # 总文本行数 total_lines = len(additional_texts) 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"文本颜色: 统一白色") # 渲染每一行文本 for i, text_item in enumerate(additional_texts): item_text = text_item['text'] # 设置字体大小为主标题的0.8倍 size_factor = 0.8 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] # 计算垂直位置 - 在分配的空间内居中 line_y = extra_text_y_start + (i * line_height) + ((line_height - text_height) // 2) # 水平居中位置 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}") 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)},使用默认字体") font_path = os.path.join("/root/autodl-tmp/poster_baseboard_0403/font", "华康海报体简.ttc") 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): """打印文字调试信息""" print(f"{text_type}信息:") print(f"- 字体大小: {font.size}") print(f"- 文字宽度: {text_width}") print(f"- 文字位置: x={x}, y={y}") print(f"- 使用字体: {os.path.basename(font_path)}") def create_poster(self, image_path, text_data, output_name): """生成海报""" try: # 增加计数器 self.poster_count += 1 # 设置目标尺寸为3:4比例 target_size = (900, 1200) # 3:4比例 # 创建三个图层 base_layer = self.create_base_layer(image_path, target_size) middle_layer = self.create_middle_layer(target_size) # 先合成底层和中间层 final_image = Image.new('RGBA', target_size, (0, 0, 0, 0)) final_image.paste(base_layer, (0, 0)) final_image.alpha_composite(middle_layer) # 创建并添加文字层 text_layer = self.create_text_layer(target_size, text_data) final_image.alpha_composite(text_layer) # 确保文字层在最上面 # 使用模10的余数决定是否添加边框 # 每十张中的第1、5、9张添加边框(余数为1,5,9) add_frame_flag = (self.poster_count % 10) in (1, 5, 9) if add_frame_flag: final_image = self.add_frame(final_image, target_size) # 保存结果 # 检查output_name是否已经是完整路径 if os.path.dirname(output_name): output_path = output_name # 确保目录存在 os.makedirs(os.path.dirname(output_path), exist_ok=True) else: # 如果只是文件名,拼接输出目录 output_path = os.path.join(self.output_dir, output_name) # 如果没有扩展名,添加.jpg if not output_path.lower().endswith(('.jpg', '.jpeg', '.png')): output_path += '.jpg' final_image.convert('RGB').save(output_path) print(f"海报已保存: {output_path}") print(f"图片尺寸: {target_size[0]}x{target_size[1]} (3:4比例)") print(f"使用的文字特效: {self.selected_effect}") return output_path except Exception as e: print(f"生成海报失败: {e}") return None def process_directory(self, input_dir, text_data=None): pass """遍历处理目录中的所有图片""" # 支持的图片格式 image_extensions = ('.jpg', '.jpeg', '.png', '.bmp') try: # 获取目录中的所有文件 files = os.listdir(input_dir) image_files = [f for f in files if f.lower().endswith(image_extensions)] if not image_files: print(f"在目录 {input_dir} 中未找到图片文件") return print(f"找到 {len(image_files)} 个图片文件") # 处理每个图片文件 for i, image_file in enumerate(image_files, 1): image_path = os.path.join(input_dir, image_file) print(f"\n处理第 {i}/{len(image_files)} 个图片: {image_file}") try: # 构建输出文件名 output_name = os.path.splitext(image_file)[0] # 生成海报 self.create_poster(image_path, text_data, output_name) print(f"完成处理: {image_file}") except Exception as e: print(f"处理图片 {image_file} 时出错: {e}") continue except Exception as e: print(f"处理目录时出错: {e}") def main(): # 设置是否使用文本框底图(True为使用,False为不使用) os.environ['USE_TEXT_BG'] = 'False' # 或 'True' print(f"设置环境变量USE_TEXT_BG为: {os.environ['USE_TEXT_BG']}") # 创建生成器实例 generator = PosterGenerator() 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" generator.create_poster(img_path, text_data, f"{item['index']}.jpg") if __name__ == "__main__": main()