PictureEdit/models.py

268 lines
9.2 KiB
Python
Raw Normal View History

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