diff --git a/scripts/__pycache__/compose_poster.cpython-38.pyc b/scripts/__pycache__/compose_poster.cpython-38.pyc new file mode 100644 index 0000000..2326f4d Binary files /dev/null and b/scripts/__pycache__/compose_poster.cpython-38.pyc differ diff --git a/scripts/__pycache__/export_psd.cpython-38.pyc b/scripts/__pycache__/export_psd.cpython-38.pyc new file mode 100644 index 0000000..eddd593 Binary files /dev/null and b/scripts/__pycache__/export_psd.cpython-38.pyc differ diff --git a/scripts/compose_poster.py b/scripts/compose_poster.py new file mode 100644 index 0000000..47434c1 --- /dev/null +++ b/scripts/compose_poster.py @@ -0,0 +1,84 @@ +# compose_poster.py - 合成图层 +import os +from PIL import Image +import logging + +# 设置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# 设置默认图片目录 +DEFAULT_IMAGE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'images') + +def compose_layers(layers, output_path, canvas_size=(1080, 1920)): + """ + 合成多个图层为一个图像 + + Args: + layers: 图层文件名列表,从底到顶排序 + output_path: 输出文件路径 + canvas_size: 画布大小,默认为(1080, 1920) + + Returns: + str: 输出文件的路径 + """ + logger.info(f"开始合成{len(layers)}个图层...") + + # 创建空白画布 + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + + # 逐个添加图层 + for i, layer_name in enumerate(layers): + try: + # 构建完整的图层路径 + layer_path = os.path.join(DEFAULT_IMAGE_DIR, layer_name) + + if os.path.exists(layer_path): + # 打开图层图像 + layer = Image.open(layer_path).convert('RGBA') + + # 调整图层大小以适应画布 + # 如果图层尺寸与画布不同,将其居中 + if layer.size != canvas_size: + new_layer = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + paste_x = (canvas_size[0] - layer.width) // 2 + paste_y = (canvas_size[1] - layer.height) // 2 + new_layer.paste(layer, (paste_x, paste_y), layer) + layer = new_layer + + # 合成图层 + canvas = Image.alpha_composite(canvas, layer) + logger.info(f"已添加第{i+1}个图层: {layer_path}") + else: + logger.error(f"图层文件不存在: {layer_path}") + except Exception as e: + logger.error(f"处理图层{layer_path}时出错: {str(e)}") + + # 设置默认输出目录 + default_output_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'outputs') + + # 确保输出目录存在 + if not os.path.exists(default_output_dir): + os.makedirs(default_output_dir) + + # 构建输出文件的完整路径 + output_path = os.path.join(default_output_dir, os.path.basename(output_path)) + + # 保存合成图像 + canvas.save(output_path) + logger.info(f"图层合成完成,已保存到: {output_path}") + + return output_path + +if __name__ == "__main__": + # 简单测试 + from sys import argv + + if len(argv) >= 3: + # 从命令行接收参数 + layers = argv[1:-1] # 现在只需要提供文件名,不需要完整路径 + output = argv[-1] + compose_layers(layers, output) + else: + print("用法: python compose_poster.py layer1.png layer2.png ... output.png") + print(f"图片将从默认目录加载: {DEFAULT_IMAGE_DIR}") \ No newline at end of file diff --git a/scripts/compose_poster_1.py b/scripts/compose_poster_1.py new file mode 100644 index 0000000..60778e9 --- /dev/null +++ b/scripts/compose_poster_1.py @@ -0,0 +1,402 @@ +""" +图层合成模块 - 将生成的各个图层组合成完整海报 (修改版) + +提供增强的图层信息导出,便于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'])}个图层") \ No newline at end of file diff --git a/scripts/export_psd.py b/scripts/export_psd.py new file mode 100644 index 0000000..2800a48 --- /dev/null +++ b/scripts/export_psd.py @@ -0,0 +1,91 @@ +# export_psd.py - PSD导出 (适配psd-tools 1.10.0 API) +import os +from PIL import Image +import logging + +# 设置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# 设置默认目录路径 +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEFAULT_INPUT_DIR = os.path.join(project_root, 'images') +DEFAULT_OUTPUT_DIR = os.path.join(project_root, 'outputs') + +def export_to_psd(layers, output_path, layer_names=None, canvas_size=(1080, 1920)): + # 先导入compose_poster中的函数 + from compose_poster import compose_layers + + try: + # 导入psd-tools相关模块 + from psd_tools import PSDImage + from psd_tools.api.layers import PixelLayer + from psd_tools.constants import Compression + except ImportError: + logger.error("未安装psd-tools库,无法导出PSD。请安装:pip install psd-tools>=1.10.0") + return "" + + logger.info(f"开始创建PSD文件,包含{len(layers)}个图层...") + + # 1. 先使用compose_layers合成图层 + temp_output = os.path.join(DEFAULT_OUTPUT_DIR, "temp_composed.png") + composed_path = compose_layers(layers, temp_output, canvas_size) + + if not composed_path: + logger.error("图层合成失败") + return "" + + # 2. 处理输出路径 + if not os.path.isabs(output_path): + output_path = os.path.join(DEFAULT_OUTPUT_DIR, output_path) + + if not output_path.lower().endswith('.psd'): + output_path += '.psd' + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir) + + try: + # 3. 创建PSD文件 + psd = PSDImage.new('RGB', canvas_size) + + # 4. 添加合成后的图层 + composed_image = Image.open(composed_path).convert('RGBA') + pixel_layer = PixelLayer.frompil(composed_image, psd, "Composed_Layer") + psd.append(pixel_layer) + + # 5. 保存PSD文件 + psd.save(output_path) + logger.info(f"PSD文件已成功创建并保存到: {output_path}") + + # 6. 清理临时文件 + if os.path.exists(temp_output): + os.remove(temp_output) + + return output_path + + except Exception as e: + logger.error(f"创建PSD文件时出错: {str(e)}") + logger.exception(e) + return "" + +if __name__ == "__main__": + # 简单测试 + from sys import argv + + if len(argv) >= 3: + # 从命令行接收参数 + layers = argv[1:-1] + output = argv[-1] + result = export_to_psd(layers, output) + if result: + print(f"PSD文件已成功导出到: {result}") + else: + print("PSD文件导出失败") + else: + print("用法: python export_psd.py layer1.png layer2.png ... output.psd") + print(f"默认输入目录: {DEFAULT_INPUT_DIR}") + print(f"默认输出目录: {DEFAULT_OUTPUT_DIR}") + print("注意:如果只提供文件名,将从默认输入目录查找图片文件") \ No newline at end of file diff --git a/scripts/test1.py b/scripts/test1.py new file mode 100644 index 0000000..3ff1933 --- /dev/null +++ b/scripts/test1.py @@ -0,0 +1,7 @@ +from compose_poster import compose_layers + +# 将多个图层合成为一个图像 +compose_layers( + ['background.jpg','aaai.png', 'nankai.jpg'], + +) \ No newline at end of file diff --git a/scripts/test2.py b/scripts/test2.py new file mode 100644 index 0000000..78bd2d8 --- /dev/null +++ b/scripts/test2.py @@ -0,0 +1,8 @@ +from export_psd import export_to_psd + +# 将多个图层导出为PSD文件 +export_to_psd( + ['background.jpg','aaai.png', 'nankai.jpg'], + 'output.psd', + ['background', 'middle', 'front'] # 可选的图层名称 +) \ No newline at end of file