From 50acade06b8bdfc680cf1886f767710a4f68f848 Mon Sep 17 00:00:00 2001 From: cyborvirtue <2088953655@qq.com> Date: Fri, 4 Jul 2025 23:30:34 +0800 Subject: [PATCH] =?UTF-8?q?PSD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/export_psd_from_json.py | 896 +++++++++++++++++++++----------- 1 file changed, 582 insertions(+), 314 deletions(-) diff --git a/scripts/export_psd_from_json.py b/scripts/export_psd_from_json.py index 3c01a64..c54eee0 100644 --- a/scripts/export_psd_from_json.py +++ b/scripts/export_psd_from_json.py @@ -1,326 +1,594 @@ -""" -测试文件:从JSON配置文件创建PSD文件 -支持通过配置文件精确控制图层位置和属性 +""" +PSD导出模块:基于Vue模板配置生成PSD文件 +支持从Vue模板解析图层信息、文本样式和位置信息 +适配海报生成流程 """ -import json +import yaml +import re from psd_tools import PSDImage -from PIL import Image -import os -from typing import List, Tuple -import numpy as np - -# 新增:导入pytoshop库用于创建真正的PSD文件 -try: - import pytoshop - PYTOSHOP_AVAILABLE = True - print("✅ pytoshop库可用,将创建真正的分层PSD文件") -except ImportError: - PYTOSHOP_AVAILABLE = False - print("⚠️ pytoshop库不可用,将使用备用方案") - -# 修复:在新版本psd-tools中,使用正确的图层创建方式 +from PIL import Image, ImageDraw, ImageFont +from psd_tools.constants import Compression +import os +from typing import List, Tuple, Optional, Dict, Any + +# 导入PixelLayer类 +from psd_tools.api.layers import PixelLayer -def create_psd_from_images( - image_paths: List[str], - output_path: str, - canvas_size: Tuple[int, int] = (1080, 1920), - mode: str = 'RGB' -) -> None: - """ - 从图片列表创建PSD文件,将图片从底到顶堆叠 - - 参数: - image_paths: 图片路径列表 - output_path: 保存PSD文件的路径 - canvas_size: PSD画布大小,格式为(宽度, 高度) - mode: PSD文件的颜色模式 - """ - # 确保输出目录存在 - os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) - - try: - print(f"开始创建PSD文件,画布大小: {canvas_size}") - - # 1. 创建背景图像 - background = Image.new(mode, canvas_size, (255, 255, 255)) - - # 2. 处理每个图片并合成到背景上 - final_image = background.copy() - - for i, img_path in enumerate(image_paths): - print(f"正在处理图片 {i+1}/{len(image_paths)}: {img_path}") - - # 检查文件是否存在 - if not os.path.exists(img_path): - print(f"警告: 图片文件不存在: {img_path}") - continue - - # 打开图片 - image = Image.open(img_path) - - # 确保图片是RGB模式 - if image.mode != 'RGB': - if image.mode == 'RGBA': - # 处理透明图片 - alpha = image.split()[-1] - rgb_image = Image.new('RGB', image.size, (255, 255, 255)) - rgb_image.paste(image, mask=alpha) - image = rgb_image - else: - image = image.convert('RGB') - - # 如果图片太大,按比例缩放以适应画布 - if image.width > canvas_size[0] or image.height > canvas_size[1]: - # 计算缩放比例,保持宽高比 - scale_w = canvas_size[0] / image.width - scale_h = canvas_size[1] / image.height - scale = min(scale_w, scale_h) * 0.8 # 留一些边距 - - new_width = int(image.width * scale) - new_height = int(image.height * scale) - image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) - print(f"图片已缩放到: {new_width}x{new_height}") - - # 计算居中位置 - left = (canvas_size[0] - image.width) // 2 - top = (canvas_size[1] - image.height) // 2 - - print(f"图片位置: ({left}, {top}), 大小: {image.width}x{image.height}") - - # 将图片粘贴到背景上 - final_image.paste(image, (left, top)) - print(f"已合成图片: {os.path.basename(img_path)}") - - # 3. 创建真正的分层PSD文件 - print(f"正在创建PSD文件到: {output_path}") - - # 优先使用pytoshop创建真正的分层PSD - if PYTOSHOP_AVAILABLE: - try: - print(f"🎨 使用pytoshop创建真正的分层PSD文件...") - - # 使用pytoshop的正确API - import pytoshop - - # 创建图层列表 - layers = [] - for i, img_path in enumerate(image_paths): - if not os.path.exists(img_path): - continue - - print(f"📋 正在创建图层 {i+1}: {os.path.basename(img_path)}") - - # 加载图片 - layer_image = Image.open(img_path) - - # 确保是RGB模式 - if layer_image.mode != 'RGB': - if layer_image.mode == 'RGBA': - background_img = Image.new('RGB', layer_image.size, (255, 255, 255)) - background_img.paste(layer_image, mask=layer_image.split()[-1]) - layer_image = background_img - else: - layer_image = layer_image.convert('RGB') - - # 调整图片大小 - if layer_image.width > canvas_size[0] or layer_image.height > canvas_size[1]: - scale_w = canvas_size[0] / layer_image.width - scale_h = canvas_size[1] / layer_image.height - scale = min(scale_w, scale_h) * 0.8 - - new_width = int(layer_image.width * scale) - new_height = int(layer_image.height * scale) - layer_image = layer_image.resize((new_width, new_height), Image.Resampling.LANCZOS) - print(f" ↘️ 图片已缩放到: {new_width}x{new_height}") - - # 计算居中位置 - left = (canvas_size[0] - layer_image.width) // 2 - top = (canvas_size[1] - layer_image.height) // 2 - - # 创建画布大小的图层数据 - layer_canvas = Image.new('RGB', canvas_size, (255, 255, 255)) - layer_canvas.paste(layer_image, (left, top)) - - # 转换为numpy数组 - layer_array = np.array(layer_canvas) - - # 创建图层名称 - layer_name = f"Layer_{i+1}_{os.path.splitext(os.path.basename(img_path))[0]}" - - # 使用pytoshop创建图层(尝试正确的API) - try: - # 分离RGB通道 - channels = [layer_array[:, :, i] for i in range(3)] - - # 创建pytoshop图层(根据实际API调整) - layer_data = { - 'name': layer_name, - 'channels': channels, - 'size': canvas_size, - 'opacity': 255, - 'visible': True - } - layers.append(layer_data) - print(f" ✅ 图层 {layer_name} 数据准备完成") - - except Exception as layer_error: - print(f" ❌ 图层数据准备失败: {layer_error}") - continue - - # 尝试使用pytoshop保存PSD - if layers: - print(f"🔧 正在组装PSD文件...") - # 由于pytoshop API复杂,我们使用更简单的方法 - raise Exception("使用备用方案") - else: - raise Exception("没有可用的图层数据") - - except Exception as pytoshop_error: - print(f"❌ pytoshop创建失败: {pytoshop_error}") - print(f"🔄 使用改进的备用方案...") - - # 改进的备用方案:创建更好的PSD文件 - try: - print(f"🎨 使用改进方法创建PSD文件...") - - # 方法1:尝试使用psd-tools创建基本结构 - try: - # 创建空PSD - psd = PSDImage.new(mode, canvas_size) - - # 将合成图像设置为背景 - psd_array = np.array(final_image) - - # 保存PSD - psd.save(output_path) - print(f"✅ 使用psd-tools创建PSD成功: {output_path}") - - # 验证文件 - if os.path.exists(output_path): - file_size = os.path.getsize(output_path) / (1024 * 1024) - print(f"📁 PSD文件大小: {file_size:.2f} MB") - - # 尝试验证 - try: - test_psd = PSDImage.open(output_path) - print(f"✅ PSD文件验证成功") - except Exception as verify_error: - print(f"⚠️ PSD验证警告: {verify_error}") - - except Exception as psd_tools_error: - print(f"❌ psd-tools方法失败: {psd_tools_error}") - raise psd_tools_error - - except Exception as backup_error: - print(f"❌ 改进备用方案失败: {backup_error}") - print(f"🔄 使用最终备用方案...") - - # 最终备用方案:强制创建PSD - try: - # 将合成图像直接保存为PSD - temp_img = final_image.copy() - - # 创建临时PNG然后转换 - temp_path = output_path.replace('.psd', '_temp.png') - temp_img.save(temp_path, 'PNG') - - # 重新加载并强制保存为PSD - reload_img = Image.open(temp_path) - - # 使用二进制方式创建PSD头部 - psd_data = b'8BPS\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03' - psd_data += canvas_size[1].to_bytes(4, 'big') # 高度 - psd_data += canvas_size[0].to_bytes(4, 'big') # 宽度 - psd_data += b'\x00\x08\x00\x03' # 深度和颜色模式 - - # 添加图像数据 - img_bytes = reload_img.tobytes() - psd_data += len(img_bytes).to_bytes(4, 'big') - psd_data += img_bytes - - # 写入PSD文件 - with open(output_path, 'wb') as f: - f.write(psd_data) - - # 清理临时文件 - if os.path.exists(temp_path): - os.remove(temp_path) - - print(f"✅ 最终方案创建PSD成功: {output_path}") - - except Exception as final_error: - print(f"❌ 最终方案也失败: {final_error}") - # 保存为PNG作为最后的选择 - png_path = output_path.replace('.psd', '.png') - final_image.save(png_path, format='PNG') - print(f"📄 已保存为PNG格式: {png_path}") - else: - # 如果pytoshop不可用,直接使用改进的方案 - print(f"🔄 pytoshop不可用,使用改进的PSD创建方法...") - try: - # 创建空PSD并保存合成图像 - psd = PSDImage.new(mode, canvas_size) - psd.save(output_path) - print(f"✅ 创建基本PSD成功: {output_path}") - except Exception as backup_error: - print(f"❌ 基本PSD创建失败: {backup_error}") - png_path = output_path.replace('.psd', '.png') - final_image.save(png_path, format='PNG') - print(f"📄 已保存为PNG格式: {png_path}") - - # 4. 生成预览图 - preview_path = os.path.splitext(output_path)[0] + "_预览.png" - final_image.save(preview_path, format='PNG') - print(f"🖼️ 预览已保存: {preview_path}") - - # 5. 输出处理结果 - print(f"处理完成!") - print(f"- 输入图片数量: {len(image_paths)}") - print(f"- 画布大小: {canvas_size}") - print(f"- 输出文件: {output_path}") - if os.path.exists(output_path): - file_size = os.path.getsize(output_path) / (1024 * 1024) - print(f"- 文件大小: {file_size:.2f} MB") - - except Exception as e: - print(f"创建PSD文件时出错: {e}") - import traceback - traceback.print_exc() - - -def create_psd_from_config(config_file: str) -> None: +def parse_vue_template_config(vue_templates_path: str, font_config_path: str = None) -> Dict[str, Any]: """ - 从JSON配置文件创建PSD文件 - 简化版本 - + 解析Vue模板配置 + 参数: - config_file: JSON配置文件路径 + vue_templates_path: Vue模板配置文件路径 + font_config_path: 字体配置文件路径(可选) + + 返回: + 解析后的配置字典 """ - # 确保配置文件存在 - if not os.path.exists(config_file): - raise FileNotFoundError(f"配置文件不存在: {config_file}") - - # 读取JSON配置文件 - with open(config_file, 'r', encoding='utf-8') as f: - config = json.load(f) - try: - # 从配置中提取信息 - canvas = config['canvas'] - canvas_size = (canvas['width'], canvas['height']) - mode = canvas['mode'] - - # 提取图片路径列表 - image_paths = [] - for layer_config in config['layers']: - image_paths.append(layer_config['image_path']) - - # 使用简化的图片合成方法 - output_path = config['output']['path'] - create_psd_from_images(image_paths, output_path, canvas_size, mode) - + # 读取Vue模板配置 + with open(vue_templates_path, 'r', encoding='utf-8') as f: + vue_config = yaml.safe_load(f) + + config = {'vue_templates': vue_config.get('vue_templates', {})} + + # 读取字体配置(如果提供) + if font_config_path and os.path.exists(font_config_path): + with open(font_config_path, 'r', encoding='utf-8') as f: + font_config = yaml.safe_load(f) + config['font_config'] = font_config + + print(f"成功加载Vue模板配置: {len(config['vue_templates'])} 个模板") + return config + except Exception as e: - print(f"从配置创建PSD文件时出错: {e}") + print(f"解析配置文件失败: {e}") + return {} + + +def extract_layer_info_from_vue(template_content: str) -> List[Dict[str, Any]]: + """ + 从Vue模板内容中提取图层信息 + + 参数: + template_content: Vue模板内容字符串 + + 返回: + 图层信息列表 + """ + layers = [] + + try: + # 解析所有图片图层(包括背景) + img_pattern = r']*src="([^"]+)"[^>]*(?:class="([^"]*)"|alt="([^"]*)")[^>]*/?>' + img_matches = re.findall(img_pattern, template_content, re.IGNORECASE) + + for i, (img_src, css_class, alt_text) in enumerate(img_matches): + img_filename = os.path.basename(img_src) + layer_name = alt_text or css_class or f'image_{i+1}' + + # 从CSS中提取样式(包括透明度) + styles = extract_css_styles(template_content, css_class) if css_class else {} + + # 根据图片类型和CSS类设置位置和大小 + if 'background' in css_class.lower() or 'background' in img_filename.lower(): + position = {'left': 0, 'top': 0} # 背景图片从左上角开始 + size = {'canvas_fit': True} # 背景图片填满整个画布 + opacity = styles.get('opacity', 100) # 从CSS获取透明度,默认100 + z_index = 1 + elif 'nku' in img_filename.lower(): + position = {'align': 'center-top', 'offset': {'y': 150}} + size = {'width': 120, 'height': 120} + opacity = styles.get('opacity', 100) + z_index = 10 + i + elif 'stamp' in img_filename.lower(): + position = {'align': 'center', 'offset': {'y': 200}} + size = {'width': 200, 'height': 200} + opacity = styles.get('opacity', 100) + z_index = 10 + i + elif 'lotus' in img_filename.lower(): + position = {'align': 'center'} + size = {'scale': 0.8} + opacity = styles.get('opacity', 100) + z_index = 10 + i + else: + position = {'align': 'center'} + size = {'scale': 1.0} + opacity = styles.get('opacity', 100) + z_index = 10 + i + + layers.append({ + 'name': layer_name, + 'type': 'image', + 'image_path': img_filename, + 'position': position, + 'size': size, + 'opacity': opacity, + 'z_index': z_index + }) + + # 解析文本图层 + text_patterns = [ + (r']*class="[^"]*title[^"]*"[^>]*>\{\{\s*(\w+)\s*\}\}', 'title', 60, '#000000'), + (r']*class="[^"]*subtitle[^"]*"[^>]*>\{\{\s*(\w+)\s*\}\}', 'subtitle', 30, '#333333') + ] + + for pattern, text_type, default_size, default_color in text_patterns: + matches = re.findall(pattern, template_content) + for i, var_name in enumerate(matches): + # 从CSS中提取样式 + styles = extract_css_styles(template_content, text_type) + + layers.append({ + 'name': f'{text_type}_{i+1}', + 'type': 'text', + 'text': { + 'text': f'{{{{ {var_name}_content }}}}', + 'font_size': styles.get('font_size', default_size), + 'color': styles.get('color', default_color), + 'font_family': styles.get('font_family', 'Microsoft YaHei') + }, + 'position': styles.get('position', {'align': 'center-top', 'offset': {'y': 300 + i * 100}}), + 'opacity': styles.get('opacity', 100), # 从CSS获取透明度,默认100 + 'z_index': 20 + i + }) + + print(f"从Vue模板中提取了 {len(layers)} 个图层") + return layers + + except Exception as e: + print(f"解析Vue模板失败: {e}") + return [] + + +def extract_css_styles(template_content: str, element_type: str) -> Dict[str, Any]: + """ + 从Vue模板的CSS中提取样式信息 + """ + styles = {} + + try: + # 查找对应的CSS类 + css_pattern = rf'\.{element_type}[^{{]*\{{([^}}]+)\}}' + css_matches = re.findall(css_pattern, template_content, re.DOTALL) + + for css_block in css_matches: + # 提取字体大小 + font_size_match = re.search(r'font-size:\s*(\d+)px', css_block) + if font_size_match: + styles['font_size'] = int(font_size_match.group(1)) + + # 提取颜色 + color_match = re.search(r'color:\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|\w+)', css_block) + if color_match: + styles['color'] = color_match.group(1) + + # 提取字体家族 + font_family_match = re.search(r'font-family:\s*[\'"]*([\'";}]+)[\'"]', css_block) + if font_family_match: + styles['font_family'] = font_family_match.group(1).strip() + + # 提取透明度 + opacity_match = re.search(r'opacity:\s*([0-9.]+)', css_block) + if opacity_match: + opacity_value = float(opacity_match.group(1)) + # 将0-1的透明度值转换为0-100的百分比 + styles['opacity'] = int(opacity_value * 100) + + except Exception as e: + print(f"提取CSS样式失败: {e}") + + return styles + + +def create_text_image(text_config: dict, canvas_size: tuple) -> Image.Image: + """ + 使用PIL创建文本图像 + """ + text = text_config['text'] + font_size = text_config.get('font_size', 24) + color = text_config.get('color', '#000000') + font_family = text_config.get('font_family', 'arial.ttf') + + # 处理颜色格式 + if isinstance(color, str) and color.startswith('#'): + color = tuple(int(color[i:i+2], 16) for i in (1, 3, 5)) + elif isinstance(color, list): + color = tuple(color) + + try: + font = ImageFont.truetype(font_family, font_size) + except (OSError, IOError): + try: + font = ImageFont.load_default() + print(f"警告: 无法加载字体 {font_family},使用默认字体") + except: + font = None + + # 创建临时图像来测量文本尺寸 + temp_img = Image.new('RGBA', (1, 1), (0, 0, 0, 0)) + temp_draw = ImageDraw.Draw(temp_img) + + if font: + bbox = temp_draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + else: + text_width = len(text) * font_size * 0.6 + text_height = font_size * 1.2 + text_width, text_height = int(text_width), int(text_height) + + # 添加边距 + margin = 20 + img_width = text_width + margin * 2 + img_height = text_height + margin * 2 + + # 创建文本图像 + text_img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(text_img) + + if font: + draw.text((margin, margin), text, fill=color, font=font) + else: + draw.text((margin, margin), text, fill=color) + + print(f"创建文本图像: '{text}', 尺寸: {img_width}x{img_height}") + return text_img + + +def calculate_position(position_config: dict, canvas_size: tuple, image_size: tuple) -> tuple: + """ + 计算图层的最终位置 + """ + canvas_width, canvas_height = canvas_size + image_width, image_height = image_size + + left = position_config.get('left', 0) + top = position_config.get('top', 0) + + # 处理对齐方式 + if 'align' in position_config: + align = position_config['align'] + + if align in ['center', 'center-center', 'center-top', 'center-bottom']: + left = (canvas_width - image_width) // 2 + elif align in ['right', 'right-center', 'right-top', 'right-bottom']: + left = canvas_width - image_width + + if align in ['center', 'center-center', 'left-center', 'right-center']: + top = (canvas_height - image_height) // 2 + elif align in ['bottom', 'left-bottom', 'center-bottom', 'right-bottom']: + top = canvas_height - image_height + + # 处理偏移量 + if 'offset' in position_config: + offset = position_config['offset'] + left += offset.get('x', 0) + top += offset.get('y', 0) + + return left, top + + +def resize_image(image: Image.Image, size_config: dict, canvas_size: tuple = None) -> Image.Image: + """ + 根据配置调整图像大小 + """ + if not size_config: + return image + + current_width, current_height = image.size + + # 处理canvas_fit选项:让图片填满整个画布 + if size_config.get('canvas_fit') and canvas_size: + canvas_width, canvas_height = canvas_size + return image.resize((canvas_width, canvas_height), Image.Resampling.LANCZOS) + + if 'width' in size_config and 'height' in size_config: + new_width = size_config['width'] + new_height = size_config['height'] + elif 'scale' in size_config: + scale = size_config['scale'] + new_width = int(current_width * scale) + new_height = int(current_height * scale) + elif 'width' in size_config: + new_width = size_config['width'] + aspect_ratio = current_height / current_width + new_height = int(new_width * aspect_ratio) + elif 'height' in size_config: + new_height = size_config['height'] + aspect_ratio = current_width / current_height + new_width = int(new_height * aspect_ratio) + else: + return image + + return image.resize((new_width, new_height), Image.Resampling.LANCZOS) + + +def find_image_path(image_filename: str) -> Optional[str]: + """ + 查找图片文件的完整路径 + """ + search_paths = [ + '../outputs', + '../images', + './outputs', + './images', + os.path.dirname(os.path.abspath(__file__)) + '/../outputs', + os.path.dirname(os.path.abspath(__file__)) + '/../images' + ] + + for search_path in search_paths: + full_path = os.path.join(search_path, image_filename) + if os.path.exists(full_path): + return full_path + + return None + + +def create_psd_from_vue_config(vue_templates_path: str, + output_path: str, + canvas_size: Tuple[int, int] = (1080, 1920), + template_name: str = None, + content_data: Dict[str, str] = None, + font_config_path: str = None) -> Optional[str]: + """ + 从Vue模板配置创建PSD文件 + + 参数: + vue_templates_path: Vue模板配置文件路径 + output_path: 输出PSD文件路径 + canvas_size: 画布大小 + template_name: 指定使用的模板名称 + content_data: 文本内容数据 + font_config_path: 字体配置文件路径 + + 返回: + 成功时返回输出路径,失败时返回None + """ + try: + print(f"开始从Vue配置创建PSD文件: {output_path}") + + # 解析配置 + config = parse_vue_template_config(vue_templates_path, font_config_path) + if not config or not config.get('vue_templates'): + print("配置解析失败或没有找到Vue模板") + return None + + vue_templates = config['vue_templates'] + + # 选择模板 + if template_name and template_name in vue_templates: + selected_template = vue_templates[template_name] + print(f"使用指定模板: {template_name}") + else: + template_name = list(vue_templates.keys())[0] + selected_template = vue_templates[template_name] + print(f"使用默认模板: {template_name}") + + # 提取图层信息 + template_content = selected_template.get('template', '') + layers_info = extract_layer_info_from_vue(template_content) + + if not layers_info: + print("没有提取到图层信息") + return None + + # 处理文本内容替换 + if content_data: + for layer in layers_info: + if layer.get('type') == 'text' and 'text' in layer: + text_content = layer['text']['text'] + for key, value in content_data.items(): + text_content = text_content.replace(f'{{{{ {key} }}}}', str(value)) + layer['text']['text'] = text_content + + # 创建PSD文件 + return create_psd_from_layers(layers_info, output_path, canvas_size) + + except Exception as e: + print(f"从Vue配置创建PSD失败: {e}") import traceback traceback.print_exc() + return None + + +def create_psd_from_layers(layers_info: List[Dict[str, Any]], + output_path: str, + canvas_size: Tuple[int, int]) -> Optional[str]: + """ + 从图层信息创建PSD文件 + """ + try: + # 确保输出目录存在 + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + + # 创建PSD文件 + psd = PSDImage.new('RGB', canvas_size) + print(f"创建画布: {canvas_size[0]}x{canvas_size[1]}, 模式: RGB") + + # 按z_index排序图层 + sorted_layers = sorted(layers_info, key=lambda x: x.get('z_index', 0)) + + for i, layer_info in enumerate(sorted_layers): + layer_name = layer_info.get('name', f'layer_{i+1}') + print(f"\n处理图层 {i+1}: {layer_name}") + + if layer_info['type'] == 'image': + # 处理图片图层 + image_path = find_image_path(layer_info['image_path']) + if not image_path: + print(f"警告: 找不到图片文件 {layer_info['image_path']},跳过") + continue + + image = Image.open(image_path) + print(f"原始图像尺寸: {image.size}") + + # 调整图像大小 + if 'size' in layer_info: + image = resize_image(image, layer_info['size'], canvas_size) + print(f"图像尺寸调整: {image.size}") + + # 计算位置 + left, top = calculate_position(layer_info.get('position', {}), canvas_size, image.size) + print(f"计算位置: left={left}, top={top}") + + # 创建图层 + layer = PixelLayer.frompil(image, psd, layer_name, top, left, Compression.RLE) + + elif layer_info['type'] == 'text': + # 处理文本图层 + print(f"创建文本图层: '{layer_info['text']['text']}'") + text_image = create_text_image(layer_info['text'], canvas_size) + + # 计算位置 + left, top = calculate_position(layer_info.get('position', {}), canvas_size, text_image.size) + print(f"计算位置: left={left}, top={top}") + + # 创建图层 + layer = PixelLayer.frompil(text_image, psd, layer_name, top, left, Compression.RLE) + + else: + print(f"未知图层类型: {layer_info['type']},跳过") + continue + + # 设置透明度 + opacity = layer_info.get('opacity', 100) + if hasattr(layer, 'opacity'): + layer.opacity = opacity + print(f"设置图层透明度: {opacity}%") + + # 确保图层可见 + layer.visible = True + psd.append(layer) + print(f"成功添加图层: {layer_name}, 位置: ({left}, {top})") + + print("\n生成合成图像...") + + # 生成合成图像 + composite_image = psd.composite(force=True) + + # 更新PSD文件的图像数据 + psd._record.image_data.set_data([channel.tobytes() for channel in composite_image.split()], psd._record.header) + + # 保存PSD文件 + psd.save(output_path) + print(f"\nPSD文件已成功创建,保存在: {output_path}") + + # 生成预览 + preview_path = os.path.splitext(output_path)[0] + "_预览.png" + composite_image.save(preview_path) + print(f"预览已保存在: {preview_path}") + + # 生成缩略图 + thumbnail_path = os.path.splitext(output_path)[0] + "_缩略图.png" + thumbnail = composite_image.copy() + thumbnail.thumbnail((400, 300), Image.Resampling.LANCZOS) + thumbnail.save(thumbnail_path) + print(f"缩略图已保存在: {thumbnail_path}") + + # 验证PSD文件 + print("\n=== PSD文件结构验证 ===") + saved_psd = PSDImage.open(output_path) + print(f"PSD文件信息: {saved_psd}") + print(f"图层数量: {len(saved_psd)}") + for i, layer in enumerate(saved_psd): + print(f"图层 {i}: {layer.name}, 位置: ({layer.left}, {layer.top}), 大小: {layer.width}x{layer.height}, 可见: {layer.visible}") + + return output_path + + except Exception as e: + print(f"创建PSD文件失败: {e}") + import traceback + traceback.print_exc() + return None + + +# 为了兼容run_pipeline.py中的调用,保留这个函数 +def create_psd_from_images(image_paths: List[str], + output_path: str, + canvas_size: Tuple[int, int] = (1080, 1920), + mode: str = 'RGB') -> None: + """ + 从图片列表创建PSD文件(简化版,保持兼容性) + """ + try: + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + + psd = PSDImage.new(mode, canvas_size) + print(f"创建画布: {canvas_size[0]}x{canvas_size[1]}, 模式: {mode}") + + for i, img_path in enumerate(image_paths): + if not os.path.exists(img_path): + print(f"警告: 图像文件不存在,跳过: {img_path}") + continue + + image = Image.open(img_path) + print(f"处理图片 {i+1}: {os.path.basename(img_path)}, 尺寸: {image.size}") + + # 居中布局 + left = (canvas_size[0] - image.width) // 2 + top = (canvas_size[1] - image.height) // 2 + + layer_name = f"layer {i+1} - {os.path.basename(img_path)}" + layer = PixelLayer.frompil(image, psd, layer_name, top, left, Compression.RLE) + layer.visible = True + psd.append(layer) + print(f"添加图层: {layer_name}, 位置: ({left}, {top})") + + # 生成合成图像 + composite_image = psd.composite(force=True) + psd._record.image_data.set_data([channel.tobytes() for channel in composite_image.split()], psd._record.header) + + # 保存文件 + psd.save(output_path) + print(f"PSD文件已成功创建,保存在: {output_path}") + + # 生成预览 + preview_path = os.path.splitext(output_path)[0] + "_预览.png" + composite_image.save(preview_path) + print(f"预览已保存在: {preview_path}") + + except Exception as e: + print(f"创建PSD文件时出错: {e}") + + +if __name__ == "__main__": + import sys + + # 示例: 使用Vue模板配置创建PSD + print("=== 从Vue模板配置创建PSD ===") + + vue_config_path = "../configs/vue_templates.yaml" + font_config_path = "../configs/font.yaml" + + if os.path.exists(vue_config_path): + try: + content_data = { + "title_content": "AI海报生成系统", + "subtitle_content": "智能设计,一键生成" + } + + result_path = create_psd_from_vue_config( + vue_templates_path=vue_config_path, + output_path="../outputs/vue_generated.psd", + template_name="nku.png", + content_data=content_data, + font_config_path=font_config_path if os.path.exists(font_config_path) else None + ) + + if result_path: + print(f"Vue模板PSD创建成功: {result_path}") + else: + print("Vue模板PSD创建失败") + + except Exception as e: + print(f"Vue模板PSD创建失败: {e}") + import traceback + traceback.print_exc() + else: + print(f"配置文件不存在: {vue_config_path}") + + print("\n使用方法:") + print(" python export_psd_from_json.py # 运行Vue模板示例") \ No newline at end of file