""" 图层合成模块 - 将生成的各个图层组合成完整海报 (修改版) 提供增强的图层信息导出,便于PSD模块使用 """ import os import json import logging from typing import List, Dict, Any, Optional, Tuple import numpy as np from PIL import Image, ImageFont, ImageDraw # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class PosterComposer: """海报合成类,负责将各个图层组合成完整海报""" def __init__(self, images_dir: str = "./images", output_dir: str = "./outputs"): """ 初始化海报合成器 参数: images_dir: 源图像目录 output_dir: 合成图像输出目录 """ self.images_dir = images_dir self.output_dir = output_dir os.makedirs(output_dir, exist_ok=True) logger.info(f"海报合成器初始化完成,图像目录: {images_dir}, 输出目录: {output_dir}") def apply_golden_ratio_positioning(self, canvas_size: Tuple[int, int], element_size: Tuple[int, int], position: str = "right") -> Tuple[int, int]: """ 基于黄金分割比例(约1.618)计算位置 参数: canvas_size: 画布宽度和高度 element_size: 要放置元素的宽度和高度 position: 黄金分割位置 ("left", "right", "top", "bottom") 返回: 元素的(x, y)坐标 """ width, height = canvas_size elem_width, elem_height = element_size # 黄金比例约为1.618 golden_ratio = 1.618 if position == "right": # 水平方向右侧黄金分割点 x = int((width / golden_ratio) - (elem_width / 2)) y = int((height - elem_height) / 2) # 垂直居中 elif position == "left": # 水平方向左侧黄金分割点 x = int(width - (width / golden_ratio) - (elem_width / 2)) y = int((height - elem_height) / 2) # 垂直居中 elif position == "top": # 垂直方向上侧黄金分割点 x = int((width - elem_width) / 2) # 水平居中 y = int((height / golden_ratio) - (elem_height / 2)) elif position == "bottom": # 垂直方向下侧黄金分割点 x = int((width - elem_width) / 2) # 水平居中 y = int(height - (height / golden_ratio) - (elem_height / 2)) else: # 默认居中 x = int((width - elem_width) / 2) y = int((height - elem_height) / 2) return (x, y) def calculate_position(self, position_type: str, canvas_size: Tuple[int, int], element_size: Tuple[int, int], offset: Tuple[int, int] = (0, 0), reference: str = None) -> Tuple[int, int]: """ 根据指定的位置类型计算位置 参数: position_type: 位置类型 ("center", "golden_ratio", "corner", "custom") canvas_size: 画布宽度和高度 element_size: 要放置元素的宽度和高度 offset: 从计算位置的(x, y)偏移量 reference: 额外位置参考 (如 "top_left", "golden_right") 返回: 元素的(x, y)坐标 """ width, height = canvas_size elem_width, elem_height = element_size offset_x, offset_y = offset if position_type == "center": # 居中定位 x = int((width - elem_width) / 2) + offset_x y = int((height - elem_height) / 2) + offset_y elif position_type == "golden_ratio": # 黄金分割定位 golden_pos = reference.split("_")[1] if reference else "right" x, y = self.apply_golden_ratio_positioning(canvas_size, element_size, golden_pos) x += offset_x y += offset_y elif position_type == "corner": # 角落定位 corner = reference if reference else "top_left" if corner == "top_left": x, y = 0, 0 elif corner == "top_right": x, y = width - elem_width, 0 elif corner == "bottom_left": x, y = 0, height - elem_height elif corner == "bottom_right": x, y = width - elem_width, height - elem_height x += offset_x y += offset_y elif position_type == "custom": # 自定义位置,偏移量为绝对位置 x, y = offset_x, offset_y else: # 默认居中 x = int((width - elem_width) / 2) + offset_x y = int((height - elem_height) / 2) + offset_y return (x, y) def fit_image_to_canvas(self, image: Image.Image, canvas_size: Tuple[int, int], fit_method: str = "contain") -> Image.Image: """ 调整图像大小以适应画布尺寸 参数: image: 要调整大小的PIL图像 canvas_size: 目标画布尺寸 fit_method: 适应方法 ("contain", "cover", "stretch") 返回: 调整大小后的PIL图像 """ canvas_width, canvas_height = canvas_size img_width, img_height = image.size if fit_method == "contain": # 缩放图像以适应画布同时保持纵横比 ratio = min(canvas_width / img_width, canvas_height / img_height) new_width = int(img_width * ratio) new_height = int(img_height * ratio) elif fit_method == "cover": # 缩放图像以覆盖画布同时保持纵横比 ratio = max(canvas_width / img_width, canvas_height / img_height) new_width = int(img_width * ratio) new_height = int(img_height * ratio) elif fit_method == "stretch": # 拉伸图像以匹配画布尺寸 new_width = canvas_width new_height = canvas_height else: # 默认为"contain" ratio = min(canvas_width / img_width, canvas_height / img_height) new_width = int(img_width * ratio) new_height = int(img_height * ratio) # 调整图像大小 return image.resize((new_width, new_height), Image.LANCZOS) def apply_overlay(self, base_image: Image.Image, overlay_image: Image.Image, position: Tuple[int, int] = (0, 0), opacity: float = 1.0) -> Image.Image: """ 将叠加图像应用到基础图像上 参数: base_image: 基础PIL图像 overlay_image: 叠加PIL图像 position: 放置叠加图的位置(x, y) opacity: 叠加图像的不透明度(0.0到1.0) 返回: 合并后的PIL图像 """ # 创建基础图像的副本 result = base_image.copy() # 确保叠加图像有alpha通道 if overlay_image.mode != 'RGBA': overlay_image = overlay_image.convert('RGBA') # 如果不透明度小于1,调整alpha通道 if opacity < 1.0: overlay_data = list(overlay_image.getdata()) new_data = [] for item in overlay_data: # 通过不透明度调整alpha通道 new_data.append((item[0], item[1], item[2], int(item[3] * opacity))) overlay_image.putdata(new_data) # 将叠加图粘贴到结果上 result.paste(overlay_image, position, overlay_image) return result def load_image(self, path: str) -> Optional[Image.Image]: """ 从文件路径加载图像 参数: path: 图像文件的路径 返回: PIL图像对象,加载失败则返回None """ try: if os.path.exists(path): return Image.open(path).convert('RGBA') else: logger.warning(f"未找到图像文件: {path}") return None except Exception as e: logger.error(f"从{path}加载图像时出错: {e}") return None def compose_from_layout(self, layout_config: Dict[str, Any], output_filename: str = "composed_poster.png", canvas_size: Tuple[int, int] = (1200, 1600)) -> Dict[str, Any]: """ 根据布局配置合成海报 参数: layout_config: 指定图层及其属性的配置 output_filename: 输出图像文件的名称 canvas_size: 输出画布的大小(宽度,高度) 返回: 包含合成海报信息的字典 """ # 创建带alpha通道的空白画布 canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) # 获取从底到顶的图层顺序 layer_order = layout_config.get("layer_order", [ "background", "main_subject", "secondary_subject", "decorative_elements", "logo", "title", "subtitle" ]) # 跟踪已处理的图层以供输出信息 processed_layers = {} # 根据布局配置处理每个图层 for layer_name in layer_order: layer_config = layout_config.get(layer_name, {}) if not layer_config: logger.warning(f"未找到图层'{layer_name}'的配置") continue # 获取图层图像路径 image_path = layer_config.get("image_path", "") full_path = os.path.join(self.images_dir, image_path) if image_path else None if not full_path or not os.path.exists(full_path): logger.warning(f"未找到图层'{layer_name}'的图像文件: {full_path}") continue # 加载图层图像 layer_img = self.load_image(full_path) if layer_img is None: continue # 获取拟合方法 fit_method = layer_config.get("fit_method", "contain") # 如果指定,调整图层大小以适应画布 if fit_method != "none": layer_img = self.fit_image_to_canvas(layer_img, canvas_size, fit_method) # 获取位置信息 position_type = layer_config.get("position_type", "center") position_reference = layer_config.get("position_reference", None) position_offset = layer_config.get("position_offset", (0, 0)) # 计算位置 position = self.calculate_position( position_type, canvas_size, layer_img.size, position_offset, position_reference ) # 获取不透明度 opacity = layer_config.get("opacity", 1.0) # 将图层应用到画布 canvas = self.apply_overlay(canvas, layer_img, position, opacity) # 记录处理信息 processed_layers[layer_name] = { "image_path": full_path, "size": layer_img.size, "position": position, "opacity": opacity } logger.info(f"已添加图层'{layer_name}',位置{position},大小{layer_img.size}") # 保存合成图像 output_path = os.path.join(self.output_dir, output_filename) canvas.save(output_path, format="PNG") logger.info(f"已保存合成图像到{output_path}") # 保存图层顺序,用于PSD导出 composition_result = { "output_path": output_path, "layers": processed_layers, "canvas_size": canvas_size, "layer_order": [layer for layer in layer_order if layer in processed_layers] # 只包含实际处理的图层 } return composition_result def generate_layer_info_for_psd(self, composition_result: Dict[str, Any]) -> Dict[str, Any]: """ 生成PSD导出所需的完整图层信息 参数: composition_result: compose_from_layout的结果 返回: 包含图层位置、路径、透明度等信息的字典,供PSD导出使用 """ # 这个方法直接返回composition_result中已经包含的完整图层信息 # 为了保持与原代码的API兼容性,我们保留这个方法 psd_info = { "layer_order": composition_result.get("layer_order", []), "layers": composition_result.get("layers", {}), "canvas_size": composition_result.get("canvas_size", (1200, 1600)) } logger.info(f"已生成PSD导出信息,包含{len(psd_info['layer_order'])}个图层") return psd_info def load_layout_config(config_path: str) -> Dict[str, Any]: """ 从JSON文件加载布局配置 参数: config_path: 布局配置JSON文件的路径 返回: 包含布局配置的字典 """ try: with open(config_path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: logger.error(f"从{config_path}加载布局配置时出错: {e}") return {} if __name__ == "__main__": # 示例用法 layout_config_path = "../configs/layout_config.json" # 加载布局配置 layout_config = load_layout_config(layout_config_path) # 创建合成器并合成海报 composer = PosterComposer() composition_result = composer.compose_from_layout(layout_config) print(f"海报成功合成到: {composition_result['output_path']}") # 为PSD导出生成图层信息 psd_info = composer.generate_layer_info_for_psd(composition_result) print(f"PSD导出信息准备完成,包含{len(psd_info['layer_order'])}个图层")