PSD实现
This commit is contained in:
parent
20802db28a
commit
50acade06b
@ -1,326 +1,594 @@
|
|||||||
"""
|
"""
|
||||||
测试文件:从JSON配置文件创建PSD文件
|
PSD导出模块:基于Vue模板配置生成PSD文件
|
||||||
支持通过配置文件精确控制图层位置和属性
|
支持从Vue模板解析图层信息、文本样式和位置信息
|
||||||
|
适配海报生成流程
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import yaml
|
||||||
|
import re
|
||||||
from psd_tools import PSDImage
|
from psd_tools import PSDImage
|
||||||
from PIL import Image
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import os
|
from psd_tools.constants import Compression
|
||||||
from typing import List, Tuple
|
import os
|
||||||
import numpy as np
|
from typing import List, Tuple, Optional, Dict, Any
|
||||||
|
|
||||||
# 新增:导入pytoshop库用于创建真正的PSD文件
|
# 导入PixelLayer类
|
||||||
try:
|
from psd_tools.api.layers import PixelLayer
|
||||||
import pytoshop
|
|
||||||
PYTOSHOP_AVAILABLE = True
|
|
||||||
print("✅ pytoshop库可用,将创建真正的分层PSD文件")
|
|
||||||
except ImportError:
|
|
||||||
PYTOSHOP_AVAILABLE = False
|
|
||||||
print("⚠️ pytoshop库不可用,将使用备用方案")
|
|
||||||
|
|
||||||
# 修复:在新版本psd-tools中,使用正确的图层创建方式
|
|
||||||
|
|
||||||
|
|
||||||
def create_psd_from_images(
|
def parse_vue_template_config(vue_templates_path: str, font_config_path: str = None) -> Dict[str, Any]:
|
||||||
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:
|
|
||||||
"""
|
"""
|
||||||
从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:
|
try:
|
||||||
# 从配置中提取信息
|
# 读取Vue模板配置
|
||||||
canvas = config['canvas']
|
with open(vue_templates_path, 'r', encoding='utf-8') as f:
|
||||||
canvas_size = (canvas['width'], canvas['height'])
|
vue_config = yaml.safe_load(f)
|
||||||
mode = canvas['mode']
|
|
||||||
|
config = {'vue_templates': vue_config.get('vue_templates', {})}
|
||||||
# 提取图片路径列表
|
|
||||||
image_paths = []
|
# 读取字体配置(如果提供)
|
||||||
for layer_config in config['layers']:
|
if font_config_path and os.path.exists(font_config_path):
|
||||||
image_paths.append(layer_config['image_path'])
|
with open(font_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
font_config = yaml.safe_load(f)
|
||||||
# 使用简化的图片合成方法
|
config['font_config'] = font_config
|
||||||
output_path = config['output']['path']
|
|
||||||
create_psd_from_images(image_paths, output_path, canvas_size, mode)
|
print(f"成功加载Vue模板配置: {len(config['vue_templates'])} 个模板")
|
||||||
|
return config
|
||||||
|
|
||||||
except Exception as e:
|
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'<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
|
import traceback
|
||||||
traceback.print_exc()
|
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模板示例")
|
Loading…
Reference in New Issue
Block a user