2025-09-02 16:49:39 +08:00
|
|
|
|
import os
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
import json
|
|
|
|
|
|
import math
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ForegroundImage:
|
|
|
|
|
|
"""前景图对象,存储图片信息和变换参数"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, file_path):
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
# 四个顶点坐标 (左上, 右上, 左下, 右下)
|
|
|
|
|
|
self.vertices = [
|
|
|
|
|
|
(0.0, 0.0), # top-left
|
|
|
|
|
|
(100.0, 0.0), # top-right
|
|
|
|
|
|
(0.0, 100.0), # bottom-left
|
|
|
|
|
|
(100.0, 100.0) # bottom-right
|
|
|
|
|
|
]
|
|
|
|
|
|
self.angle = 0.0 # 旋转角度
|
|
|
|
|
|
self.mask_shape = "rect" # 遗罩形状
|
|
|
|
|
|
self.locked = False # 是否锁定
|
|
|
|
|
|
self.visible = True # 是否可见
|
|
|
|
|
|
self.z_index = 0 # 图层顺序
|
|
|
|
|
|
self.hidden = False # 是否隐藏(用于批量处理模式)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加属性x, y, w, h的getter和setter方法,使其与vertices保持同步
|
|
|
|
|
|
@property
|
|
|
|
|
|
def x(self):
|
|
|
|
|
|
return self.vertices[0][0] # 左上角x坐标作为x属性
|
|
|
|
|
|
|
|
|
|
|
|
@x.setter
|
|
|
|
|
|
def x(self, value):
|
|
|
|
|
|
# 更新所有顶点的x坐标以保持宽度不变
|
|
|
|
|
|
dx = value - self.vertices[0][0]
|
|
|
|
|
|
for i in range(len(self.vertices)):
|
|
|
|
|
|
x, y = self.vertices[i]
|
|
|
|
|
|
self.vertices[i] = (x + dx, y)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def y(self):
|
|
|
|
|
|
return self.vertices[0][1] # 左上角y坐标作为y属性
|
|
|
|
|
|
|
|
|
|
|
|
@y.setter
|
|
|
|
|
|
def y(self, value):
|
|
|
|
|
|
# 更新所有顶点的y坐标以保持高度不变
|
|
|
|
|
|
dy = value - self.vertices[0][1]
|
|
|
|
|
|
for i in range(len(self.vertices)):
|
|
|
|
|
|
x, y = self.vertices[i]
|
|
|
|
|
|
self.vertices[i] = (x, y + dy)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def w(self):
|
|
|
|
|
|
# 宽度为右上角和左上角x坐标的差值
|
|
|
|
|
|
return self.vertices[1][0] - self.vertices[0][0]
|
|
|
|
|
|
|
|
|
|
|
|
@w.setter
|
|
|
|
|
|
def w(self, value):
|
|
|
|
|
|
# 保持左上角不变,调整右上角和右下角的x坐标
|
|
|
|
|
|
if value > 0:
|
|
|
|
|
|
old_w = self.vertices[1][0] - self.vertices[0][0]
|
|
|
|
|
|
if old_w != 0:
|
|
|
|
|
|
x0, y0 = self.vertices[0] # 左上角
|
|
|
|
|
|
x1, y1 = self.vertices[1] # 右上角
|
|
|
|
|
|
x2, y2 = self.vertices[2] # 左下角
|
|
|
|
|
|
x3, y3 = self.vertices[3] # 右下角
|
|
|
|
|
|
|
|
|
|
|
|
# 调整右上角和右下角的x坐标
|
|
|
|
|
|
self.vertices[1] = (x0 + value, y1) # 右上角
|
|
|
|
|
|
self.vertices[3] = (x0 + value, y3) # 右下角
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def h(self):
|
|
|
|
|
|
# 高度为左下角和左上角y坐标的差值
|
|
|
|
|
|
return self.vertices[2][1] - self.vertices[0][1]
|
|
|
|
|
|
|
|
|
|
|
|
@h.setter
|
|
|
|
|
|
def h(self, value):
|
|
|
|
|
|
# 保持左上角不变,调整左下角和右下角的y坐标
|
|
|
|
|
|
if value > 0:
|
|
|
|
|
|
old_h = self.vertices[2][1] - self.vertices[0][1]
|
|
|
|
|
|
if old_h != 0:
|
|
|
|
|
|
x0, y0 = self.vertices[0] # 左上角
|
|
|
|
|
|
x1, y1 = self.vertices[1] # 右上角
|
|
|
|
|
|
x2, y2 = self.vertices[2] # 左下角
|
|
|
|
|
|
x3, y3 = self.vertices[3] # 右下角
|
|
|
|
|
|
|
|
|
|
|
|
# 调整左下角和右下角的y坐标
|
|
|
|
|
|
self.vertices[2] = (x2, y0 + value) # 左下角
|
|
|
|
|
|
self.vertices[3] = (x3, y0 + value) # 右下角
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
"""转换为字典,用于序列化"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"file_path": self.file_path,
|
|
|
|
|
|
"vertices": self.vertices,
|
|
|
|
|
|
"angle": self.angle,
|
|
|
|
|
|
"mask_shape": self.mask_shape,
|
|
|
|
|
|
"locked": self.locked,
|
|
|
|
|
|
"visible": self.visible,
|
|
|
|
|
|
"z_index": self.z_index,
|
|
|
|
|
|
"hidden": self.hidden
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def from_dict(cls, data):
|
|
|
|
|
|
"""从字典创建对象"""
|
|
|
|
|
|
fg = cls(data["file_path"])
|
|
|
|
|
|
fg.vertices = data["vertices"]
|
|
|
|
|
|
fg.angle = data["angle"]
|
|
|
|
|
|
fg.mask_shape = data.get("mask_shape", "rect")
|
|
|
|
|
|
fg.locked = data.get("locked", False)
|
|
|
|
|
|
fg.visible = data.get("visible", True)
|
|
|
|
|
|
fg.z_index = data.get("z_index", 0)
|
|
|
|
|
|
fg.hidden = data.get("hidden", False)
|
|
|
|
|
|
return fg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Template:
|
|
|
|
|
|
"""模板对象,存储尺寸和背景信息"""
|
|
|
|
|
|
|
2025-09-02 18:14:12 +08:00
|
|
|
|
def __init__(self, ratio=(16, 9), width_px=1200, height_px=None):
|
2025-09-02 16:49:39 +08:00
|
|
|
|
self.ratio = ratio
|
|
|
|
|
|
self.width_px = width_px
|
2025-09-02 18:14:12 +08:00
|
|
|
|
# 如果提供了高度,使用提供的高度,否则根据比例计算
|
|
|
|
|
|
if height_px is not None:
|
|
|
|
|
|
self.height_px = height_px
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.height_px = int(width_px * ratio[1] / ratio[0])
|
2025-09-02 16:49:39 +08:00
|
|
|
|
self.bg_image = None
|
|
|
|
|
|
self.bg_color = "#FFFFFF" # 默认白色背景
|
|
|
|
|
|
|
|
|
|
|
|
def set_custom_size(self, width, height):
|
|
|
|
|
|
"""设置自定义尺寸"""
|
|
|
|
|
|
self.width_px = width
|
|
|
|
|
|
self.height_px = height
|
|
|
|
|
|
# 根据实际尺寸设置比例
|
|
|
|
|
|
gcd = math.gcd(width, height)
|
|
|
|
|
|
self.ratio = (width // gcd, height // gcd)
|
|
|
|
|
|
|
|
|
|
|
|
def load_background(self, file_path):
|
|
|
|
|
|
"""加载背景图"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.bg_image = Image.open(file_path).convert("RGBA")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载背景图失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def clear_background(self):
|
|
|
|
|
|
"""清除背景图"""
|
|
|
|
|
|
self.bg_image = None
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
"""转换为字典,用于序列化"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ratio": self.ratio,
|
|
|
|
|
|
"width_px": self.width_px,
|
|
|
|
|
|
"height_px": self.height_px,
|
|
|
|
|
|
"bg_color": self.bg_color
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def from_dict(cls, data):
|
|
|
|
|
|
"""从字典创建对象"""
|
2025-09-02 18:14:12 +08:00
|
|
|
|
template = cls(data["ratio"], data["width_px"], data["height_px"])
|
2025-09-02 16:49:39 +08:00
|
|
|
|
template.bg_color = data.get("bg_color", "#FFFFFF")
|
|
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Project:
|
|
|
|
|
|
"""项目对象,管理模板和前景图"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.template = Template()
|
|
|
|
|
|
self.foregrounds = []
|
|
|
|
|
|
self.canvas_zoom = 1.0
|
|
|
|
|
|
self.file_path = None # 项目文件路径
|
|
|
|
|
|
|
|
|
|
|
|
def add_foreground(self, file_path, reference_fg=None):
|
|
|
|
|
|
"""添加前景图"""
|
|
|
|
|
|
fg = ForegroundImage(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果提供了参考图片,则复制其属性
|
|
|
|
|
|
if reference_fg is not None:
|
|
|
|
|
|
fg.vertices = [tuple(v) for v in reference_fg.vertices] # 复制顶点
|
|
|
|
|
|
fg.angle = reference_fg.angle
|
|
|
|
|
|
fg.mask_shape = reference_fg.mask_shape
|
|
|
|
|
|
fg.locked = reference_fg.locked
|
|
|
|
|
|
fg.visible = reference_fg.visible
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 尝试获取图片原始尺寸并设置初始大小
|
|
|
|
|
|
try:
|
|
|
|
|
|
with Image.open(file_path) as img:
|
|
|
|
|
|
w, h = img.size
|
|
|
|
|
|
# 按比例缩小到模板的2/3大小
|
|
|
|
|
|
scale = min(self.template.width_px * 2/3 / w, self.template.height_px * 2/3 / h)
|
|
|
|
|
|
# 计算初始位置(居中)
|
|
|
|
|
|
initial_w = w * scale
|
|
|
|
|
|
initial_h = h * scale
|
|
|
|
|
|
initial_x = (self.template.width_px - initial_w) / 2
|
|
|
|
|
|
initial_y = (self.template.height_px - initial_h) / 2
|
|
|
|
|
|
|
|
|
|
|
|
# 设置顶点坐标
|
|
|
|
|
|
fg.vertices = [
|
|
|
|
|
|
(initial_x, initial_y), # top-left
|
|
|
|
|
|
(initial_x + initial_w, initial_y), # top-right
|
|
|
|
|
|
(initial_x, initial_y + initial_h), # bottom-left
|
|
|
|
|
|
(initial_x + initial_w, initial_y + initial_h) # bottom-right
|
|
|
|
|
|
]
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取图片尺寸失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 设置图层顺序
|
|
|
|
|
|
fg.z_index = len(self.foregrounds)
|
|
|
|
|
|
self.foregrounds.append(fg)
|
|
|
|
|
|
return fg
|
|
|
|
|
|
|
|
|
|
|
|
def remove_foreground(self, fg):
|
|
|
|
|
|
"""移除前景图"""
|
|
|
|
|
|
if fg in self.foregrounds:
|
|
|
|
|
|
self.foregrounds.remove(fg)
|
|
|
|
|
|
# 更新图层顺序
|
|
|
|
|
|
self.update_z_indices()
|
|
|
|
|
|
|
|
|
|
|
|
def update_z_indices(self):
|
|
|
|
|
|
"""更新所有前景图的图层顺序"""
|
|
|
|
|
|
for i, fg in enumerate(sorted(self.foregrounds, key=lambda x: x.z_index)):
|
|
|
|
|
|
fg.z_index = i
|
|
|
|
|
|
|
|
|
|
|
|
def save(self, file_path=None):
|
|
|
|
|
|
"""保存项目到文件"""
|
|
|
|
|
|
if file_path is None:
|
|
|
|
|
|
file_path = self.file_path
|
|
|
|
|
|
|
|
|
|
|
|
if file_path is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
|
"template": self.template.to_dict(),
|
|
|
|
|
|
"foregrounds": [fg.to_dict() for fg in self.foregrounds],
|
|
|
|
|
|
"canvas_zoom": self.canvas_zoom
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"保存项目失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def load(self, file_path):
|
|
|
|
|
|
"""从文件加载项目"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
self.template = Template.from_dict(data["template"])
|
|
|
|
|
|
self.foregrounds = [ForegroundImage.from_dict(fg_data) for fg_data in data["foregrounds"]]
|
|
|
|
|
|
self.canvas_zoom = data.get("canvas_zoom", 1.0)
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载项目失败: {e}")
|
|
|
|
|
|
return False
|