PSD实现

This commit is contained in:
cyborvirtue 2025-07-04 23:30:34 +08:00
parent 20802db28a
commit 50acade06b

View File

@ -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
from PIL import Image, ImageDraw, ImageFont
from psd_tools.constants import Compression
import os
from typing import List, Tuple
import numpy as np
from typing import List, Tuple, Optional, Dict, Any
# 新增导入pytoshop库用于创建真正的PSD文件
try:
import pytoshop
PYTOSHOP_AVAILABLE = True
print("✅ pytoshop库可用将创建真正的分层PSD文件")
except ImportError:
PYTOSHOP_AVAILABLE = False
print("⚠️ pytoshop库不可用将使用备用方案")
# 修复在新版本psd-tools中使用正确的图层创建方式
# 导入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:
def parse_vue_template_config(vue_templates_path: str, font_config_path: str = None) -> Dict[str, Any]:
"""
从图片列表创建PSD文件将图片从底到顶堆叠
解析Vue模板配置
参数:
image_paths: 图片路径列表
output_path: 保存PSD文件的路径
canvas_size: PSD画布大小格式为(宽度, 高度)
mode: PSD文件的颜色模式
vue_templates_path: Vue模板配置文件路径
font_config_path: 字体配置文件路径可选
返回:
解析后的配置字典
"""
try:
# 读取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"解析配置文件失败: {e}")
return {}
def extract_layer_info_from_vue(template_content: str) -> List[Dict[str, Any]]:
"""
从Vue模板内容中提取图层信息
参数:
template_content: Vue模板内容字符串
返回:
图层信息列表
"""
layers = []
try:
# 解析所有图片图层(包括背景)
img_pattern = r'<img[^>]*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'<h1[^>]*class="[^"]*title[^"]*"[^>]*>\{\{\s*(\w+)\s*\}\}</h1>', 'title', 60, '#000000'),
(r'<h2[^>]*class="[^"]*subtitle[^"]*"[^>]*>\{\{\s*(\w+)\s*\}\}</h2>', '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)
try:
print(f"开始创建PSD文件画布大小: {canvas_size}")
# 创建PSD文件
psd = PSDImage.new('RGB', canvas_size)
print(f"创建画布: {canvas_size[0]}x{canvas_size[1]}, 模式: RGB")
# 1. 创建背景图像
background = Image.new(mode, canvas_size, (255, 255, 255))
# 按z_index排序图层
sorted_layers = sorted(layers_info, key=lambda x: x.get('z_index', 0))
# 2. 处理每个图片并合成到背景上
final_image = background.copy()
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}")
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}")
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(img_path)
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)
# 确保图片是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')
print(f"未知图层类型: {layer_info['type']},跳过")
continue
# 如果图片太大,按比例缩放以适应画布
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 # 留一些边距
# 设置透明度
opacity = layer_info.get('opacity', 100)
if hasattr(layer, 'opacity'):
layer.opacity = opacity
print(f"设置图层透明度: {opacity}%")
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}")
# 确保图层可见
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
print(f"图片位置: ({left}, {top}), 大小: {image.width}x{image.height}")
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})")
# 将图片粘贴到背景上
final_image.paste(image, (left, top))
print(f"已合成图片: {os.path.basename(img_path)}")
# 生成合成图像
composite_image = psd.composite(force=True)
psd._record.image_data.set_data([channel.tobytes() for channel in composite_image.split()], psd._record.header)
# 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}")
print(f"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")
composite_image.save(preview_path)
print(f"预览已保存在: {preview_path}")
except Exception as e:
print(f"创建PSD文件时出错: {e}")
import traceback
traceback.print_exc()
def create_psd_from_config(config_file: str) -> None:
"""
从JSON配置文件创建PSD文件 - 简化版本
if __name__ == "__main__":
import sys
参数:
config_file: JSON配置文件路径
"""
# 确保配置文件存在
if not os.path.exists(config_file):
raise FileNotFoundError(f"配置文件不存在: {config_file}")
# 示例: 使用Vue模板配置创建PSD
print("=== 从Vue模板配置创建PSD ===")
# 读取JSON配置文件
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
vue_config_path = "../configs/vue_templates.yaml"
font_config_path = "../configs/font.yaml"
if os.path.exists(vue_config_path):
try:
# 从配置中提取信息
canvas = config['canvas']
canvas_size = (canvas['width'], canvas['height'])
mode = canvas['mode']
content_data = {
"title_content": "AI海报生成系统",
"subtitle_content": "智能设计,一键生成"
}
# 提取图片路径列表
image_paths = []
for layer_config in config['layers']:
image_paths.append(layer_config['image_path'])
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
)
# 使用简化的图片合成方法
output_path = config['output']['path']
create_psd_from_images(image_paths, output_path, canvas_size, mode)
if result_path:
print(f"Vue模板PSD创建成功: {result_path}")
else:
print("Vue模板PSD创建失败")
except Exception as e:
print(f"从配置创建PSD文件时出错: {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模板示例")