psd method init

This commit is contained in:
cyborvirtue 2025-05-20 17:01:40 +08:00
parent 574650f3f0
commit fcb02de2cb
7 changed files with 592 additions and 0 deletions

Binary file not shown.

Binary file not shown.

84
scripts/compose_poster.py Normal file
View File

@ -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}")

402
scripts/compose_poster_1.py Normal file
View File

@ -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'])}个图层")

91
scripts/export_psd.py Normal file
View File

@ -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("注意:如果只提供文件名,将从默认输入目录查找图片文件")

7
scripts/test1.py Normal file
View File

@ -0,0 +1,7 @@
from compose_poster import compose_layers
# 将多个图层合成为一个图像
compose_layers(
['background.jpg','aaai.png', 'nankai.jpg'],
)

8
scripts/test2.py Normal file
View File

@ -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'] # 可选的图层名称
)