成品内容

This commit is contained in:
wsb1224 2025-09-04 12:55:33 +08:00
parent fc83bb0bfc
commit 91a353b0f9
9 changed files with 230 additions and 44 deletions

View File

@ -1,3 +1,9 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from PIL import Image, ImageTk from PIL import Image, ImageTk
@ -36,6 +42,9 @@ class CanvasWidget(tk.Canvas):
# 初始绘制 # 初始绘制
self.draw_preview() self.draw_preview()
def on_template_changed(self):
"""当模板变化时重绘画布"""
self.draw_preview()
def canvas_to_template(self, x, y): def canvas_to_template(self, x, y):
"""将画布坐标转换为模板坐标""" """将画布坐标转换为模板坐标"""
scale = self.zoom scale = self.zoom
@ -204,16 +213,16 @@ class CanvasWidget(tk.Canvas):
if img: if img:
# 应用遮罩 # 应用遮罩
masked_img = ImageManager.apply_mask(img, fg.mask_shape) masked_img = ImageManager.apply_mask(img, fg.mask_shape)
# 使用基于顶点的图像变形方法 # 使用基于顶点的图像变形方法
transformed_img, (x, y) = ImageManager.transform_with_vertices( transformed_img, (x, y) = ImageManager.transform_with_vertices(
masked_img, fg.vertices masked_img, fg.vertices
) )
# 确保图像具有透明度 # 确保图像具有透明度
if transformed_img.mode != 'RGBA': if transformed_img.mode != 'RGBA':
transformed_img = transformed_img.convert('RGBA') transformed_img = transformed_img.convert('RGBA')
# 缩放到预览尺寸 # 缩放到预览尺寸
scaled_img = transformed_img.resize( scaled_img = transformed_img.resize(
(max(1, int(transformed_img.width * self.zoom)), (max(1, int(transformed_img.width * self.zoom)),

View File

@ -1,6 +1,19 @@
"""导出设置对话框""" """导出设置对话框"""
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import tkinter as tk import tkinter as tk
from tkinter import ttk, filedialog, messagebox from tkinter import ttk, filedialog, messagebox
import os
from pathlib import Path from pathlib import Path
from exporter import Exporter from exporter import Exporter

View File

@ -1,3 +1,9 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed

View File

@ -1,3 +1,9 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import cv2 import cv2
import numpy as np import numpy as np
from PIL import Image, ImageDraw, ImageOps from PIL import Image, ImageDraw, ImageOps

11
main.py
View File

@ -1,3 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import sys
import os
import tkinter as tk import tkinter as tk
from main_window import MainWindow from main_window import MainWindow

View File

@ -1,3 +1,9 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog from tkinter import ttk, messagebox, filedialog, simpledialog
from pathlib import Path from pathlib import Path
@ -173,7 +179,7 @@ class MainWindow(tk.Tk):
if file_paths: if file_paths:
# 检查是否是第一次添加图片 # 检查是否是第一次添加图片
is_first_batch = len(self.project.foregrounds) == 0 is_first_batch = len(self.project.foregrounds) == 0
# 添加所有选择的图片 # 添加所有选择的图片
added_foregrounds = [] added_foregrounds = []
for file_path in file_paths: for file_path in file_paths:
@ -181,19 +187,19 @@ class MainWindow(tk.Tk):
if not any(fg.file_path == file_path for fg in self.project.foregrounds): if not any(fg.file_path == file_path for fg in self.project.foregrounds):
fg = self.project.add_foreground(file_path) fg = self.project.add_foreground(file_path)
added_foregrounds.append(fg) added_foregrounds.append(fg)
# 如果是第一次批量添加,则设置批量处理模式 # 如果是第一次批量添加,则设置批量处理模式
if is_first_batch and len(added_foregrounds) > 1: if is_first_batch and len(added_foregrounds) > 1:
# 第一张图片保持可见和可编辑 # 第一张图片保持可见和可编辑
first_fg = added_foregrounds[0] first_fg = added_foregrounds[0]
first_fg.hidden = False first_fg.hidden = False
first_fg.visible = True first_fg.visible = True
# 其他图片设置为隐藏 # 其他图片设置为隐藏
for fg in added_foregrounds[1:]: for fg in added_foregrounds[1:]:
fg.hidden = True fg.hidden = True
fg.visible = False fg.visible = False
# 选中第一张图片 # 选中第一张图片
self.canvas.selected_item = first_fg self.canvas.selected_item = first_fg
self.update_property_panel(first_fg) self.update_property_panel(first_fg)
@ -264,19 +270,43 @@ class MainWindow(tk.Tk):
"""设置模板""" """设置模板"""
dialog = TemplateDialog(self, self.project.template) dialog = TemplateDialog(self, self.project.template)
if dialog.result: if dialog.result:
if dialog.result["type"] == "preset": # 处理所有类型的模板设置结果
self.project.template = Template( if dialog.result["type"] == "preset_loaded":
dialog.result["ratio"], # 直接更新现有模板的属性
dialog.result["width"] self.project.template.ratio = dialog.result["ratio"]
) self.project.template.width_px = dialog.result["width"]
else: self.project.template.height_px = dialog.result["height"]
self.project.template = Template() self.project.template.bg_color = dialog.result["bg_color"]
if "bg_image_path" in dialog.result and dialog.result["bg_image_path"]:
self.project.template.load_background(dialog.result["bg_image_path"])
elif dialog.result["type"] == "preset":
# 更新现有模板的属性
self.project.template.ratio = tuple(dialog.result["ratio"])
self.project.template.width_px = dialog.result["width"]
self.project.template.height_px = dialog.result["height"]
self.project.template.bg_color = dialog.result["bg_color"]
# 设置背景图
if "bg_image_path" in dialog.result and dialog.result["bg_image_path"]:
self.project.template.load_background(dialog.result["bg_image_path"])
elif "bg_image" in dialog.result:
self.project.template.bg_image = dialog.result["bg_image"]
elif dialog.result["type"] == "custom":
# 更新现有模板的属性
self.project.template.set_custom_size( self.project.template.set_custom_size(
dialog.result["width"], dialog.result["width"],
dialog.result["height"] dialog.result["height"]
) )
# 设置ratio属性
if "ratio" in dialog.result:
self.project.template.ratio = dialog.result["ratio"]
self.project.template.bg_color = dialog.result["bg_color"]
# 设置背景图
if "bg_image_path" in dialog.result and dialog.result["bg_image_path"]:
self.project.template.load_background(dialog.result["bg_image_path"])
elif "bg_image" in dialog.result:
self.project.template.bg_image = dialog.result["bg_image"]
self.project.template.bg_color = dialog.result["bg_color"] # 确保更新画布显示
self.canvas.draw_preview() self.canvas.draw_preview()
def batch_export(self): def batch_export(self):
@ -300,7 +330,7 @@ class MainWindow(tk.Tk):
if self.project.foregrounds: if self.project.foregrounds:
first_image_dir = Path(self.project.foregrounds[0].file_path).parent first_image_dir = Path(self.project.foregrounds[0].file_path).parent
default_output_dir = first_image_dir / "output" default_output_dir = first_image_dir / "output"
dialog = ExportDialog(self, len(self.project.foregrounds)) dialog = ExportDialog(self, len(self.project.foregrounds))
if dialog.result: if dialog.result:
# 禁用主窗口防止重复操作 # 禁用主窗口防止重复操作

View File

@ -1,3 +1,9 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import os import os
from pathlib import Path from pathlib import Path
import json import json
@ -124,13 +130,13 @@ class Template:
self.width_px = width_px self.width_px = width_px
self.height_px = int(width_px * ratio[1] / ratio[0]) self.height_px = int(width_px * ratio[1] / ratio[0])
self.bg_image = None self.bg_image = None
self.bg_image_path = None # 背景图片路径
self.bg_color = "#FFFFFF" # 默认白色背景 self.bg_color = "#FFFFFF" # 默认白色背景
# models.py 中 set_custom_size 方法添加通知机制
def set_custom_size(self, width, height): def set_custom_size(self, width, height):
"""设置自定义尺寸"""
self.width_px = width self.width_px = width
self.height_px = height self.height_px = height
# 根据实际尺寸设置比例
gcd = math.gcd(width, height) gcd = math.gcd(width, height)
self.ratio = (width // gcd, height // gcd) self.ratio = (width // gcd, height // gcd)
@ -138,6 +144,7 @@ class Template:
"""加载背景图""" """加载背景图"""
try: try:
self.bg_image = Image.open(file_path).convert("RGBA") self.bg_image = Image.open(file_path).convert("RGBA")
self.bg_image_path = file_path # 保存背景图片路径
return True return True
except Exception as e: except Exception as e:
print(f"加载背景图失败: {e}") print(f"加载背景图失败: {e}")
@ -146,6 +153,7 @@ class Template:
def clear_background(self): def clear_background(self):
"""清除背景图""" """清除背景图"""
self.bg_image = None self.bg_image = None
self.bg_image_path = None
def to_dict(self): def to_dict(self):
"""转换为字典,用于序列化""" """转换为字典,用于序列化"""
@ -153,7 +161,8 @@ class Template:
"ratio": self.ratio, "ratio": self.ratio,
"width_px": self.width_px, "width_px": self.width_px,
"height_px": self.height_px, "height_px": self.height_px,
"bg_color": self.bg_color "bg_color": self.bg_color,
"bg_image_path": self.bg_image_path
} }
@classmethod @classmethod
@ -162,6 +171,12 @@ class Template:
template = cls(data["ratio"], data["width_px"]) template = cls(data["ratio"], data["width_px"])
template.height_px = data["height_px"] template.height_px = data["height_px"]
template.bg_color = data.get("bg_color", "#FFFFFF") template.bg_color = data.get("bg_color", "#FFFFFF")
template.bg_image_path = data.get("bg_image_path")
# 如果存在背景图片路径,加载背景图片
if template.bg_image_path:
template.load_background(template.bg_image_path)
return template return template

View File

@ -1,5 +1,13 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog
from tkinter import TclError
import math
from template_presets import TemplatePresetManager from template_presets import TemplatePresetManager
@ -29,7 +37,7 @@ class TemplateDialog(tk.Toplevel):
preset_frame.grid(row=0, column=0, columnspan=3, sticky=tk.EW, pady=(0, 10)) preset_frame.grid(row=0, column=0, columnspan=3, sticky=tk.EW, pady=(0, 10))
ttk.Label(preset_frame, text="选择预设:").grid(row=0, column=0, sticky=tk.W) ttk.Label(preset_frame, text="选择预设:").grid(row=0, column=0, sticky=tk.W)
self.preset_var = tk.StringVar() self.preset_var = tk.StringVar() # 默认空字符串,不选中任何预设
self.preset_combo = ttk.Combobox(preset_frame, textvariable=self.preset_var, width=30) self.preset_combo = ttk.Combobox(preset_frame, textvariable=self.preset_var, width=30)
self.preset_combo.grid(row=0, column=1, padx=5) self.preset_combo.grid(row=0, column=1, padx=5)
self.update_preset_list() self.update_preset_list()
@ -58,11 +66,15 @@ class TemplateDialog(tk.Toplevel):
self.custom_frame = ttk.Frame(frame) self.custom_frame = ttk.Frame(frame)
ttk.Label(self.custom_frame, text="宽度:").grid(row=0, column=0, sticky=tk.W) ttk.Label(self.custom_frame, text="宽度:").grid(row=0, column=0, sticky=tk.W)
self.width_var = tk.IntVar(value=current_template.width_px) self.width_var = tk.IntVar(value=current_template.width_px)
ttk.Entry(self.custom_frame, textvariable=self.width_var, width=8).grid(row=0, column=1, padx=5) width_entry = ttk.Entry(self.custom_frame, textvariable=self.width_var, width=8)
width_entry.grid(row=0, column=1, padx=5)
width_entry.bind("<FocusOut>", self.on_custom_size_change)
ttk.Label(self.custom_frame, text="高度:").grid(row=0, column=2, sticky=tk.W, padx=5) ttk.Label(self.custom_frame, text="高度:").grid(row=0, column=2, sticky=tk.W, padx=5)
self.height_var = tk.IntVar(value=current_template.height_px) self.height_var = tk.IntVar(value=current_template.height_px)
ttk.Entry(self.custom_frame, textvariable=self.height_var, width=8).grid(row=0, column=3, padx=5) height_entry = ttk.Entry(self.custom_frame, textvariable=self.height_var, width=8)
height_entry.grid(row=0, column=3, padx=5)
height_entry.bind("<FocusOut>", self.on_custom_size_change)
# 背景设置 # 背景设置
ttk.Label(frame, text="背景颜色:").grid(row=3, column=0, sticky=tk.W, pady=5) ttk.Label(frame, text="背景颜色:").grid(row=3, column=0, sticky=tk.W, pady=5)
@ -100,8 +112,7 @@ class TemplateDialog(tk.Toplevel):
presets = self.preset_manager.get_presets() presets = self.preset_manager.get_presets()
preset_names = [preset["name"] for preset in presets["presets"]] preset_names = [preset["name"] for preset in presets["presets"]]
self.preset_combo['values'] = preset_names self.preset_combo['values'] = preset_names
if preset_names: # 不自动选中任何预设,让用户手动选择
self.preset_combo.current(0)
def load_selected_preset(self): def load_selected_preset(self):
"""加载选中的预设""" """加载选中的预设"""
@ -110,6 +121,8 @@ class TemplateDialog(tk.Toplevel):
messagebox.showwarning("警告", "请先选择一个预设") messagebox.showwarning("警告", "请先选择一个预设")
return return
# 设置标志以避免在刷新界面时更改尺寸
self._loading_preset = True
if self.preset_manager.load_preset_to_template(selected_preset, self.current_template): if self.preset_manager.load_preset_to_template(selected_preset, self.current_template):
# 更新界面显示 # 更新界面显示
self.refresh_from_template() self.refresh_from_template()
@ -118,6 +131,8 @@ class TemplateDialog(tk.Toplevel):
messagebox.showinfo("成功", f"已加载预设: {selected_preset}") messagebox.showinfo("成功", f"已加载预设: {selected_preset}")
else: else:
messagebox.showerror("错误", "加载预设失败") messagebox.showerror("错误", "加载预设失败")
# 清除标志
self._loading_preset = False
def save_current_as_preset(self): def save_current_as_preset(self):
"""将当前设置保存为预设""" """将当前设置保存为预设"""
@ -172,15 +187,28 @@ class TemplateDialog(tk.Toplevel):
self.show_custom_frame() self.show_custom_frame()
def on_ratio_change(self): def on_ratio_change(self):
"""处理比例变化"""
if self.ratio_var.get() == "自定义": if self.ratio_var.get() == "自定义":
self.show_custom_frame() self.show_custom_frame()
# 更新比例显示为当前模板的比例
ratio_str = f"{self.current_template.ratio[0]}:{self.current_template.ratio[1]}"
self.ratio_var.set(ratio_str)
else: else:
self.hide_custom_frame() self.hide_custom_frame()
# 设置默认尺寸 if not (hasattr(self, '_loading_preset') and self._loading_preset):
w, h = map(int, self.ratio_var.get().split(":")) try:
self.width_var.set(1920 if w > h else 1080) w, h = map(int, self.ratio_var.get().split(":"))
self.height_var.set(int(self.width_var.get() * h / w)) current_width = self.width_var.get()
if current_width <= 0:
current_width = 100 # 避免宽度为0
new_height = int(current_width * h / w)
new_height = max(10, new_height) # 确保最小高度
self.height_var.set(new_height)
# 同步更新当前模板的尺寸
self.current_template.width_px = current_width
self.current_template.height_px = new_height
self.current_template.ratio = (w, h)
except (ValueError, ZeroDivisionError):
messagebox.showerror("错误", "无效的比例格式")
def show_custom_frame(self): def show_custom_frame(self):
"""显示自定义尺寸框""" """显示自定义尺寸框"""
@ -190,52 +218,96 @@ class TemplateDialog(tk.Toplevel):
"""隐藏自定义尺寸框""" """隐藏自定义尺寸框"""
self.custom_frame.grid_forget() self.custom_frame.grid_forget()
def on_custom_size_change(self, event=None):
"""自定义尺寸变化时的处理"""
if self.ratio_var.get() == "自定义":
try:
width = self.width_var.get()
height = self.height_var.get()
if width <= 0 or height <= 0:
return
# 更新当前模板的尺寸
self.current_template.set_custom_size(width, height)
# 更新比例显示
ratio_str = f"{self.current_template.ratio[0]}:{self.current_template.ratio[1]}"
self.ratio_var.set(ratio_str)
# 刷新主窗口画布
self.parent.get_canvas().draw_preview()
except (ValueError, TclError):
# 忽略无效输入
pass
def choose_bg_image(self): def choose_bg_image(self):
"""选择背景图""" """选择背景图"""
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
filetypes=[ title="选择背景图",
("图像文件", "*.jpg;*.jpeg;*.png;*.webp;*.bmp"), filetypes=[("图片文件", "*.png *.jpg *.jpeg *.gif *.bmp")]
("所有文件", "*.*")
]
) )
if file_path: if file_path:
if self.current_template.load_background(file_path): if self.current_template.load_background(file_path):
self.bg_image_var.set("已设置") self.bg_image_var.set("已设置")
# 刷新主窗口画布
self.parent.get_canvas().draw_preview()
else:
messagebox.showerror("错误", "加载背景图失败")
def clear_bg_image(self): def clear_bg_image(self):
"""清除背景图""" """清除背景图"""
self.current_template.clear_background() self.current_template.clear_background()
self.bg_image_var.set("") self.bg_image_var.set("")
# 刷新主窗口画布
self.parent.get_canvas().draw_preview()
def on_ok(self): def on_ok(self):
"""确认按钮""" """确认按钮"""
# 如果选择了预设,则应用预设 # 只有在用户主动点击"加载"按钮加载预设时才应用预设
selected_preset = self.preset_var.get() # 不再自动应用选中的预设
if selected_preset:
if not self.preset_manager.load_preset_to_template(selected_preset, self.current_template):
messagebox.showerror("错误", "加载预设失败")
return
try: try:
if self.ratio_var.get() == "自定义": if self.ratio_var.get() == "自定义":
width = self.width_var.get() try:
height = self.height_var.get() width = self.width_var.get()
height = self.height_var.get()
except tk.TclError:
messagebox.showerror("错误", "请输入有效的整数")
return
if width <= 0 or height <= 0: if width <= 0 or height <= 0:
messagebox.showerror("错误", "宽度和高度必须为正数") messagebox.showerror("错误", "宽度和高度必须为正数")
return return
# 更新当前模板
self.current_template.set_custom_size(width, height)
# 显式设置ratio属性为自定义比例
gcd = math.gcd(width, height)
ratio_w = width // gcd
ratio_h = height // gcd
self.current_template.ratio = (ratio_w, ratio_h)
self.current_template.bg_color = self.bg_color_var.get()
self.result = { self.result = {
"type": "custom", "type": "custom",
"width": width, "width": width,
"height": height, "height": height,
"bg_color": self.bg_color_var.get() "ratio": (ratio_w, ratio_h),
"bg_color": self.bg_color_var.get(),
"bg_image": self.current_template.bg_image,
"bg_image_path": self.current_template.bg_image_path
} }
else: else:
w, h = map(int, self.ratio_var.get().split(":")) w, h = map(int, self.ratio_var.get().split(":"))
# 更新当前模板
self.current_template.ratio = (w, h)
self.current_template.width_px = self.width_var.get()
self.current_template.height_px = self.height_var.get()
self.current_template.bg_color = self.bg_color_var.get()
self.result = { self.result = {
"type": "preset", "type": "preset",
"ratio": (w, h), "ratio": tuple([w, h]),
"width": self.width_var.get(), "width": self.width_var.get(),
"bg_color": self.bg_color_var.get() "height": self.height_var.get(),
"bg_color": self.bg_color_var.get(),
"bg_image": self.current_template.bg_image,
"bg_image_path": self.current_template.bg_image_path
} }
self.destroy() self.destroy()
except Exception as e: except Exception as e:
@ -250,5 +322,23 @@ class TemplateDialog(tk.Toplevel):

View File

@ -1,3 +1,9 @@
"""
作者太一
微信taiyi1224
邮箱shuobo1224@qq.com
"""
import math import math
import cv2 import cv2