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'