图像工坊 - 一款多功能图像处理工具
开发背景
在日常工作中,经常需要处理大量图片,比如调整大小、添加水印、转换格式等。虽然市面上有很多图片处理软件,但要么功能太复杂,要么操作不够便捷。因此,我开发了这款轻量级的图片处理工具,专注于最常用的图片处理功能。
开发时间的实际不长,在错误的分析上有了AI的帮助,比自己排查快很多。
改名字了,基础功能图片预览,再才是图片编辑。
软件功能
1. 基础图片处理
- 裁剪:支持自由裁剪和固定比例裁剪
- 调整大小:支持按像素和按比例调整
- 旋转翻转:90°/180°/270°旋转,水平/垂直翻转
- 格式转换:支持主流格式(JPG、PNG、BMP、WEBP等)
- 基础调整:亮度、对比度、饱和度、锐化等
2. 高级功能
- 模板裁剪:支持自定义模板位置和大小
- 添加水印:支持文字水印和图片水印
- 防伪水印:支持全图平铺水印,适合版权保护
3. 批量处理
- 批量调整:同时处理多张图片的尺寸
- 批量转换:批量转换图片格式
- 批量水印:批量添加水印
开发过程
1. 技术选型
- 编程语言:Python 3
- GUI框架:Tkinter
- 图像处理:Pillow (PIL)
- 打包工具:PyInstaller
2. 开发难点及解决方案
2.1 字体加载优化
- 问题:程序启动时加载系统字体较慢
- 解决:
- 添加加载进度窗口
- 显示实时加载状态
- 优化字体搜索算法
2.2 图片预览性能
2.3 水印功能实现
- 问题:水印位置调整不直观
- 解决:
- 添加可视化预览
- 支持拖拽调整位置
- 实现网格对齐功能
2.4 批量处理
- 问题:处理大量图片时程序无响应
- 解决:
- 实现多步骤向导界面
- 添加进度显示
- 优化文件处理逻辑
2.5 水印的定位和同步
- 问题:位置不精准,现在也不,只是相对,因为字体的原因,定位有偏差
- 解决:
- 实现实时同步水印的参数
- 水印的角度和大小
- 之前想的是用到时候加载字体,实际是软件启动就加载字体最好
2.6 批量功能自动创建文件夹(4.12更新)
- 问题:解决批次处理图片混淆的问题
- 解决:
全新批次文件夹功能: 自动创建日期时间标记的文件夹,更好地组织输出
- 批量调整大小: 使用"resize_yyyyMMdd_HHmmss"格式文件夹
- 批量格式转换: 使用"convert_yyyyMMdd_HHmmss"格式文件夹
- 批量添加水印: 使用"watermark_yyyyMMdd_HHmmss"格式文件夹
2.7 加载2000+文件(4.14更新)
- 问题:解决加载文件多性能问题及获取缩略图方法
- 解决:
图片预览功能简单,但2000+文件性能的问题想了很多办法,处理方法是每次加载100张图片缩略图
用户界面
1. 主界面设计
- 左侧工具栏:常用功能快捷访问
- 中央预览区:实时预览处理效果
- 右侧参数区:调整处理参数
2. 交互优化
- 支持拖拽操作
- 实时预览效果
- 简洁的参数调整界面
- 清晰的操作提示
3. 简洁不美化
开发心得
-
用户体验至上
- 注重操作流程的简洁性
- 添加适当的视觉反馈
- 优化各项功能的响应速度
-
性能优化
- 合理使用缓存机制
- 优化大文件处理逻辑
- 注意内存资源管理
-
代码质量
未来计划
-
功能扩展
- 添加更多图片滤镜
- 支持更多图片格式
- 添加图片编辑历史记录
-
性能提升
-
界面优化
下载使用
系统要求
- Windows 7及以上系统
- 不需要安装Python环境
- 无需额外依赖
@hengzhenhui945 的建议,加了批量功能自动创建文件夹,同时修复了一下bug
@施施乐 的建议,基础功能是图片浏览,再才是图片编辑处理
新版下载地址
[https://wwyp.lanzoul.com/ilTgM2tj9n8b]
使用说明
- 下载并解压
- 运行图片处理小工具.exe
- 开始使用
反馈建议
如果您在使用过程中遇到任何问题,或有任何建议,欢迎反馈!
界面部分截图
新界面截图
[tr][td] [td] [td]
[tr][td]
[td]
[td]
[tr][td] [td] [td]
旧界面截图
[tr][td]
[td]
[td]
[tr][td]
[td]
[td]
[tr][td]
[td]
[td]
主体代码
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, colorchooser
from PIL import Image, ImageTk, ImageFont, ImageEnhance, ImageDraw
import os
import sys
import glob
import subprocess
import time
import io
import math
from image_utils import *
from batch_processor import BatchProcessor
class ImageProcessingApp:
def __init__(self, root):
self.root = root
self.root.title("图片处理小工具")
self.root.geometry("960x640")
loading_window = tk.Toplevel(self.root)
loading_window.title("正在加载")
loading_window.geometry("300x120")
loading_window.transient(self.root)
loading_window.grab_set()
self.center_window(loading_window, 300, 120)
loading_window.resizable(False, False)
ttk.Label(loading_window, text="正在加载系统字体...", font=("Arial", 10)).pack(pady=10)
progress = ttk.Progressbar(loading_window, mode='indeterminate')
progress.pack(fill='x', padx=20)
progress.start(10)
info_label = ttk.Label(loading_window, text="初始化中...", font=("Arial", 9))
info_label.pack(pady=5)
hint_label = ttk.Label(loading_window, text="首次加载可能需要较长时间,请耐心等待", font=("Arial", 8), foreground="gray")
hint_label.pack(pady=5)
loading_window.update()
try:
self.get_system_fonts(info_label)
except Exception as e:
messagebox.showerror("错误", f"加载字体时出错:{str(e)}")
finally:
loading_window.destroy()
self.current_image = None
self.display_image = None
self.original_image = None
self.history = []
self.history_index = -1
self.max_history = 20
self.batch_processor = BatchProcessor(self.root, self)
self.setup_ui()
self.create_app_icon()
def center_window(self, window, width, height):
"""将窗口居中显示"""
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
x = (screen_width - width) // 2
y = (screen_height - height) // 2
window.geometry(f"+{x}+{y}")
def setup_ui(self):
"""设置界面元素"""
self.create_app_icon()
self.create_menu()
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.tools_frame = ttk.LabelFrame(self.main_frame, text="工具", style="Toolbar.TLabelframe")
self.tools_frame.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)
style = ttk.Style()
style.configure("Toolbar.TLabelframe", background="#f5f5f5", borderwidth=1, relief="flat")
style.configure("Toolbar.TLabelframe.Label", font=("Segoe UI", 9, "bold"), foreground="#555555")
style.configure("Toolbar.TButton", font=("Segoe UI", 9), padding=4, background="#ffffff", foreground="#333333")
style.map("Toolbar.TButton",
background=[("active", "#e6f3ff")],
foreground=[("active", "#0078d7")]
)
file_frame = ttk.Frame(self.tools_frame)
file_frame.pack(fill=tk.X, padx=5, pady=(5, 0))
ttk.Button(file_frame, text="打开图片", command=self.open_image, style="Toolbar.TButton").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
ttk.Button(file_frame, text="保存图片", command=self.save_image, style="Toolbar.TButton").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
self.basic_frame = ttk.LabelFrame(self.tools_frame, text="基础处理", style="Toolbar.TLabelframe")
self.basic_frame.pack(fill=tk.X, padx=5, pady=(20, 0))
basic_left_frame = ttk.Frame(self.basic_frame)
basic_left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
basic_right_frame = ttk.Frame(self.basic_frame)
basic_right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
ttk.Button(basic_left_frame, text="裁剪", command=self.crop_image).grid(row=0, column=0, sticky="ew", pady=2)
ttk.Button(basic_left_frame, text="调整大小", command=self.resize_image).grid(row=1, column=0, sticky="ew", pady=2)
ttk.Button(basic_left_frame, text="旋转翻转", command=self.rotate_image).grid(row=2, column=0, sticky="ew", pady=2)
basic_left_frame.grid_columnconfigure(0, weight=1)
ttk.Button(basic_right_frame, text="基础调整", command=self.adjust_image).grid(row=0, column=0, sticky="ew", pady=2)
ttk.Button(basic_right_frame, text="格式转换", command=self.convert_format).grid(row=1, column=0, sticky="ew", pady=2)
ttk.Button(basic_right_frame, text="应用滤镜", command=self.apply_image_filter).grid(row=2, column=0, sticky="ew", pady=2)
basic_right_frame.grid_columnconfigure(0, weight=1)
advanced_frame = ttk.LabelFrame(self.tools_frame, text="高级功能", style="Toolbar.TLabelframe")
advanced_frame.pack(fill=tk.X, padx=5, pady=(25, 0))
advanced_left_frame = ttk.Frame(advanced_frame)
advanced_left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
advanced_right_frame = ttk.Frame(advanced_frame)
advanced_right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
ttk.Button(advanced_left_frame, text="模板裁剪", command=self.template_crop).grid(row=0, column=0, sticky="ew", pady=2)
ttk.Button(advanced_left_frame, text="添加水印", command=self.add_watermark).grid(row=1, column=0, sticky="ew", pady=2)
advanced_left_frame.grid_columnconfigure(0, weight=1)
ttk.Button(advanced_right_frame, text="防伪水印", command=self.add_tiled_watermark).grid(row=0, column=0, sticky="ew", pady=2)
advanced_right_frame.grid_columnconfigure(0, weight=1)
batch_frame = ttk.LabelFrame(self.tools_frame, text="批量处理", style="Toolbar.TLabelframe")
batch_frame.pack(fill=tk.X, padx=5, pady=(25, 5))
batch_left_frame = ttk.Frame(batch_frame)
batch_left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
batch_right_frame = ttk.Frame(batch_frame)
batch_right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
ttk.Button(batch_left_frame, text="批量调整", command=self.batch_processor.show_batch_resize_dialog).grid(row=0, column=0, sticky="ew", pady=2)
ttk.Button(batch_left_frame, text="批量转换", command=self.batch_processor.show_batch_convert_dialog).grid(row=1, column=0, sticky="ew", pady=2)
batch_left_frame.grid_columnconfigure(0, weight=1)
ttk.Button(batch_right_frame, text="批量水印", command=self.batch_processor.show_batch_watermark_dialog).grid(row=0, column=0, sticky="ew", pady=2)
batch_right_frame.grid_columnconfigure(0, weight=1)
self.image_frame = ttk.LabelFrame(self.main_frame, text="图片预览")
self.image_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
self.canvas = tk.Canvas(self.image_frame, bg="lightgray")
self.canvas.pack(fill=tk.BOTH, expand=True)
self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def open_image(self):
"""打开图片文件"""
file_path = filedialog.askopenfilename(
filetypes=[
("Image files", "*.jpg *.jpeg *.png *.bmp *.gif *.webp"),
("All files", "*.*")
]
)
if file_path:
try:
self.original_image = Image.open(file_path)
self.current_image = self.original_image.copy()
self.display_image_on_canvas()
self.status_bar.config(text=f"已加载图片: {os.path.basename(file_path)}")
except Exception as e:
messagebox.showerror("错误", f"无法打开图片: {str(e)}")
def display_image_on_canvas(self):
"""在画布上显示当前图片"""
if self.current_image:
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width <= 1 or canvas_height <= 1:
canvas_width = 800
canvas_height = 600
img_width, img_height = self.current_image.size
ratio = min(canvas_width/img_width, canvas_height/img_height)
new_width = int(img_width * ratio)
new_height = int(img_height * ratio)
resized_img = self.current_image.resize((new_width, new_height), Image.LANCZOS)
self.display_image = ImageTk.PhotoImage(resized_img)
self.canvas.delete("all")
self.canvas.create_image(
canvas_width//2, canvas_height//2,
image=self.display_image, anchor=tk.CENTER
)
self.status_bar.config(text=f"图片尺寸: {img_width} x {img_height} 像素")
def save_image(self):
"""保存当前图片"""
if self.current_image:
default_ext = ".png"
default_format = "PNG files"
if hasattr(self.current_image, 'format') and self.current_image.format:
format_map = {
"JPEG": (".jpg", "JPEG files"),
"PNG": (".png", "PNG files"),
"WEBP": (".webp", "WEBP files"),
"BMP": (".bmp", "BMP files"),
"GIF": (".gif", "GIF files"),
"TIFF": (".tif", "TIFF files")
}
if self.current_image.format in format_map:
default_ext, default_format = format_map[self.current_image.format]
file_path = filedialog.asksaveasfilename(
defaultextension=default_ext,
initialfile=f"image{default_ext}",
filetypes=[
(default_format, f"*{default_ext}"),
("PNG files", "*.png"),
("JPEG files", "*.jpg *.jpeg"),
("WEBP files", "*.webp"),
("BMP files", "*.bmp"),
("GIF files", "*.gif"),
("TIFF files", "*.tif"),
("All files", "*.*")
]
)
if file_path:
try:
file_ext = os.path.splitext(file_path)[1].lower()
save_format = None
ext_to_format = {
'.jpg': 'JPEG', '.jpeg': 'JPEG',
'.png': 'PNG', '.webp': 'WEBP',
'.bmp': 'BMP', '.gif': 'GIF',
'.tif': 'TIFF', '.tiff': 'TIFF'
}
if file_ext in ext_to_format:
save_format = ext_to_format[file_ext]
save_options = {}
if save_format == 'JPEG':
save_options['quality'] = 95
if save_format:
self.current_image.save(file_path, format=save_format, **save_options)
else:
self.current_image.save(file_path)
self.status_bar.config(text=f"图片已保存: {os.path.basename(file_path)}")
messagebox.showinfo("成功", "图片保存成功!")
except Exception as e:
messagebox.showerror("错误", f"保存图片时出错: {str(e)}")
else:
messagebox.showwarning("警告", "没有可保存的图片!")
def crop_image(self):
"""裁剪图片"""
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
crop_window = tk.Toplevel(self.root)
crop_window.title("裁剪图片")
crop_window.geometry("300x200")
crop_window.resizable(False, False)
crop_window.transient(self.root)
crop_window.grab_set()
self.center_window(crop_window, 300, 200)
crop_window.focus_set()
ttk.Label(crop_window, text="裁剪模式:").grid(row=0, column=0, padx=10, pady=10, sticky=tk.W)
crop_mode = tk.StringVar(value="free")
ttk.Radiobutton(crop_window, text="自由裁剪", variable=crop_mode, value="free").grid(
row=0, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Radiobutton(crop_window, text="固定比例", variable=crop_mode, value="fixed").grid(
row=1, column=1, padx=5, pady=5, sticky=tk.W)
ratio_frame = ttk.Frame(crop_window)
ratio_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky=tk.W)
ttk.Label(ratio_frame, text="选择比例:").pack(side=tk.LEFT, padx=5)
ratio_var = tk.StringVar(value="1:1")
ratio_combo = ttk.Combobox(ratio_frame, textvariable=ratio_var, width=10, state="readonly")
ratio_combo['values'] = ("1:1", "4:3", "16:9", "3:2", "2:3", "9:16")
ratio_combo.pack(side=tk.LEFT, padx=5)
def update_ratio_state(*args):
if crop_mode.get() == "fixed":
ratio_combo.config(state="readonly")
else:
ratio_combo.config(state="disabled")
crop_mode.trace_add("write", update_ratio_state)
update_ratio_state()
def start_crop():
crop_window.destroy()
self.start_crop_mode(crop_mode.get(), ratio_var.get())
button_frame = ttk.Frame(crop_window)
button_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=20)
ttk.Button(button_frame, text="开始裁剪", command=start_crop).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=crop_window.destroy).pack(side=tk.LEFT, padx=10)
def start_crop_mode(self, mode, ratio="1:1"):
"""开始裁剪模式,允许用户在图像上选择区域"""
if not self.current_image:
return
self.orig_image_size = self.current_image.size
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
img_width, img_height = self.current_image.size
self.display_ratio = min(canvas_width/img_width, canvas_height/img_height)
new_width = int(img_width * self.display_ratio)
new_height = int(img_height * self.display_ratio)
x_offset = (canvas_width - new_width) // 2
y_offset = (canvas_height - new_height) // 2
self.image_bbox = (x_offset, y_offset, x_offset + new_width, y_offset + new_height)
self.crop_mode = mode
self.crop_ratio = ratio
self.crop_rect = None
self.crop_start_x = 0
self.crop_start_y = 0
self.crop_current = None
self.status_bar.config(text="请在图片上拖动鼠标选择裁剪区域,按ESC键取消")
self.crop_indicator = ttk.Label(
self.canvas,
text="裁剪模式: " + ("自由裁剪" if mode == "free" else f"固定比例 ({ratio})"),
background="#ffffff",
padding=5
)
self.crop_indicator.place(x=10, y=10)
self.crop_confirm_btn = ttk.Button(
self.canvas,
text="确认裁剪",
command=self.apply_crop
)
self.crop_confirm_btn.place(x=10, y=50)
self.crop_cancel_btn = ttk.Button(
self.canvas,
text="取消",
command=self.cancel_crop
)
self.crop_cancel_btn.place(x=100, y=50)
self.canvas.bind("<ButtonPress-1>", self.crop_start)
self.canvas.bind("<B1-Motion>", self.crop_drag)
self.canvas.bind("<ButtonRelease-1>", self.crop_end)
self.root.bind("<Escape>", lambda e: self.cancel_crop())
def crop_start(self, event):
"""开始裁剪操作"""
if not (self.image_bbox[0] <= event.x <= self.image_bbox[2] and
self.image_bbox[1] <= event.y <= self.image_bbox[3]):
return
if self.crop_rect:
self.canvas.delete(self.crop_rect)
self.crop_start_x, self.crop_start_y = event.x, event.y
self.crop_rect = self.canvas.create_rectangle(
event.x, event.y, event.x, event.y,
outline="red", width=2
)
def crop_drag(self, event):
"""拖动过程中更新裁剪区域"""
if not self.crop_rect:
return
x = max(self.image_bbox[0], min(event.x, self.image_bbox[2]))
y = max(self.image_bbox[1], min(event.y, self.image_bbox[3]))
if self.crop_mode == "free":
self.canvas.coords(self.crop_rect, self.crop_start_x, self.crop_start_y, x, y)
else:
width, height = self.parse_ratio(self.crop_ratio)
dx = x - self.crop_start_x
dy = y - self.crop_start_y
sign_x = 1 if dx >= 0 else -1
sign_y = 1 if dy >= 0 else -1
if abs(dx) / width > abs(dy) / height:
new_height = abs(dy)
new_width = new_height * width / height
else:
new_width = abs(dx)
new_height = new_width * height / width
x2 = self.crop_start_x + sign_x * new_width
y2 = self.crop_start_y + sign_y * new_height
if x2 < self.image_bbox[0]:
x2 = self.image_bbox[0]
new_width = abs(x2 - self.crop_start_x)
new_height = new_width * height / width
y2 = self.crop_start_y + sign_y * new_height
elif x2 > self.image_bbox[2]:
x2 = self.image_bbox[2]
new_width = abs(x2 - self.crop_start_x)
new_height = new_width * height / width
y2 = self.crop_start_y + sign_y * new_height
if y2 < self.image_bbox[1]:
y2 = self.image_bbox[1]
new_height = abs(y2 - self.crop_start_y)
new_width = new_height * width / height
x2 = self.crop_start_x + sign_x * new_width
elif y2 > self.image_bbox[3]:
y2 = self.image_bbox[3]
new_height = abs(y2 - self.crop_start_y)
new_width = new_height * width / height
x2 = self.crop_start_x + sign_x * new_width
self.canvas.coords(self.crop_rect, self.crop_start_x, self.crop_start_y, x2, y2)
x1, y1, x2, y2 = self.canvas.coords(self.crop_rect)
crop_width = abs(x2 - x1) / self.display_ratio
crop_height = abs(y2 - y1) / self.display_ratio
self.status_bar.config(text=f"裁剪区域: {int(crop_width)} x {int(crop_height)} 像素")
def crop_end(self, event):
"""结束裁剪选择"""
if not self.crop_rect:
return
x1, y1, x2, y2 = self.canvas.coords(self.crop_rect)
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
if x2 - x1 < 10 or y2 - y1 < 10:
self.canvas.delete(self.crop_rect)
self.crop_rect = None
self.status_bar.config(text="裁剪区域太小,请重新选择")
return
self.canvas.coords(self.crop_rect, x1, y1, x2, y2)
self.crop_current = (x1, y1, x2, y2)
def parse_ratio(self, ratio_str):
"""解析比例字符串,返回宽高值"""
try:
w, h = ratio_str.split(":")
return int(w), int(h)
except:
return 1, 1
def apply_crop(self):
"""应用裁剪"""
if not self.crop_current:
self.status_bar.config(text="请先选择裁剪区域")
return
x1, y1, x2, y2 = self.crop_current
x1 = (x1 - self.image_bbox[0]) / self.display_ratio
y1 = (y1 - self.image_bbox[1]) / self.display_ratio
x2 = (x2 - self.image_bbox[0]) / self.display_ratio
y2 = (y2 - self.image_bbox[1]) / self.display_ratio
x1 = max(0, int(x1))
y1 = max(0, int(y1))
x2 = min(self.orig_image_size[0], int(x2))
y2 = min(self.orig_image_size[1], int(y2))
self.current_image = self.current_image.crop((x1, y1, x2, y2))
self.end_crop_mode()
self.display_image_on_canvas()
self.status_bar.config(text=f"图片已裁剪为 {x2-x1} x {y2-y1} 像素")
def cancel_crop(self):
"""取消裁剪操作"""
self.end_crop_mode()
self.display_image_on_canvas()
self.status_bar.config(text="裁剪已取消")
def end_crop_mode(self):
"""结束裁剪模式,清理UI和事件绑定"""
self.canvas.unbind("<ButtonPress-1>")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.root.unbind("<Escape>")
if hasattr(self, 'crop_indicator') and self.crop_indicator:
self.crop_indicator.destroy()
self.crop_indicator = None
if hasattr(self, 'crop_confirm_btn') and self.crop_confirm_btn:
self.crop_confirm_btn.destroy()
self.crop_confirm_btn = None
if hasattr(self, 'crop_cancel_btn') and self.crop_cancel_btn:
self.crop_cancel_btn.destroy()
self.crop_cancel_btn = None
if self.crop_rect:
self.canvas.delete(self.crop_rect)
self.crop_rect = None
def resize_image(self):
"""调整图片大小"""
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
resize_window = tk.Toplevel(self.root)
resize_window.title("调整图片大小")
resize_window.geometry("350x260")
resize_window.resizable(False, False)
self.center_window(resize_window, 350, 260)
resize_window.transient(self.root)
resize_window.grab_set()
resize_window.focus_set()
original_width, original_height = self.current_image.size
mode_frame = ttk.Frame(resize_window)
mode_frame.pack(fill=tk.X, padx=10, pady=5)
resize_mode = tk.StringVar(value="pixel")
ttk.Radiobutton(mode_frame, text="按像素调整", variable=resize_mode, value="pixel").pack(side=tk.LEFT, padx=10)
ttk.Radiobutton(mode_frame, text="按百分比调整", variable=resize_mode, value="percent").pack(side=tk.LEFT, padx=10)
pixel_frame = ttk.LabelFrame(resize_window, text="像素设置")
pixel_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(pixel_frame, text="原始尺寸:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
ttk.Label(pixel_frame, text=f"{original_width} x {original_height} 像素").grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(pixel_frame, text="新宽度:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
width_var = tk.StringVar(value=str(original_width))
width_entry = ttk.Entry(pixel_frame, textvariable=width_var, width=10)
width_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(pixel_frame, text="像素").grid(row=1, column=2, padx=0, pady=5, sticky=tk.W)
ttk.Label(pixel_frame, text="新高度:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
height_var = tk.StringVar(value=str(original_height))
height_entry = ttk.Entry(pixel_frame, textvariable=height_var, width=10)
height_entry.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(pixel_frame, text="像素").grid(row=2, column=2, padx=0, pady=5, sticky=tk.W)
percent_frame = ttk.LabelFrame(resize_window, text="百分比设置")
percent_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(percent_frame, text="调整百分比:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
percent_var = tk.StringVar(value="100")
percent_entry = ttk.Entry(percent_frame, textvariable=percent_var, width=10)
percent_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(percent_frame, text="%").grid(row=0, column=2, padx=0, pady=5, sticky=tk.W)
preview_label = ttk.Label(percent_frame, text=f"预览: {original_width} x {original_height} 像素")
preview_label.grid(row=1, column=0, columnspan=3, padx=5, pady=5, sticky=tk.W)
maintain_ratio = tk.BooleanVar(value=True)
ratio_check = ttk.Checkbutton(resize_window, text="保持宽高比", variable=maintain_ratio)
ratio_check.pack(padx=10, pady=5, anchor=tk.W)
def update_frames(*args):
if resize_mode.get() == "pixel":
pixel_frame.pack(fill=tk.X, padx=10, pady=5)
percent_frame.pack_forget()
else:
pixel_frame.pack_forget()
percent_frame.pack(fill=tk.X, padx=10, pady=5)
def update_percent_preview(*args):
try:
percent = float(percent_var.get())
if percent <= 0:
percent = 1
percent_var.set("1")
new_width = int(original_width * percent / 100)
new_height = int(original_height * percent / 100)
preview_label.config(text=f"预览: {new_width} x {new_height} 像素")
except ValueError:
preview_label.config(text="请输入有效的百分比")
def apply_resize():
try:
if resize_mode.get() == "pixel":
new_width = int(width_var.get())
new_height = int(height_var.get())
if new_width <= 0 or new_height <= 0:
messagebox.showerror("错误", "宽度和高度必须大于0!")
return
else:
percent = float(percent_var.get())
if percent <= 0:
messagebox.showerror("错误", "百分比必须大于0!")
return
new_width = int(original_width * percent / 100)
new_height = int(original_height * percent / 100)
self.current_image = resize_image(self.current_image, new_width, new_height, False)
self.display_image_on_canvas()
self.status_bar.config(text=f"图片已调整为 {new_width} x {new_height} 像素")
resize_window.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的数字!")
def update_height(*args):
if maintain_ratio.get() and resize_mode.get() == "pixel":
try:
new_width = int(width_var.get())
ratio = original_height / original_width
new_height = int(new_width * ratio)
height_var.set(str(new_height))
except ValueError:
pass
def update_width(*args):
if maintain_ratio.get() and resize_mode.get() == "pixel":
try:
new_height = int(height_var.get())
ratio = original_width / original_height
new_width = int(new_height * ratio)
width_var.set(str(new_width))
except ValueError:
pass
resize_mode.trace_add("write", update_frames)
width_var.trace_add("write", update_height)
height_var.trace_add("write", update_width)
percent_var.trace_add("write", update_percent_preview)
button_frame = ttk.Frame(resize_window)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10)
ttk.Button(button_frame, text="确定", command=apply_resize).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=resize_window.destroy).pack(side=tk.LEFT, padx=10)
update_frames()
update_percent_preview()
def rotate_image(self):
"""旋转图片"""
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
rotate_window = tk.Toplevel(self.root)
rotate_window.title("旋转与翻转")
rotate_window.geometry("300x300")
rotate_window.resizable(False, False)
self.center_window(rotate_window, 300, 300)
rotate_window.transient(self.root)
rotate_window.grab_set()
rotate_window.focus_set()
rotate_frame = ttk.LabelFrame(rotate_window, text="旋转")
rotate_frame.pack(fill=tk.X, padx=20, pady=5)
ttk.Button(rotate_frame, text="向左旋转90°",
command=lambda: self.apply_rotation(-90)).pack(fill=tk.X, padx=10, pady=5)
ttk.Button(rotate_frame, text="向右旋转90°",
command=lambda: self.apply_rotation(90)).pack(fill=tk.X, padx=10, pady=5)
ttk.Button(rotate_frame, text="旋转180°",
command=lambda: self.apply_rotation(180)).pack(fill=tk.X, padx=10, pady=5)
ttk.Button(rotate_frame, text="旋转270°",
command=lambda: self.apply_rotation(270)).pack(fill=tk.X, padx=10, pady=5)
flip_frame = ttk.LabelFrame(rotate_window, text="翻转")
flip_frame.pack(fill=tk.X, padx=20, pady=5)
ttk.Button(flip_frame, text="水平翻转",
command=lambda: self.apply_flip("horizontal")).pack(fill=tk.X, padx=10, pady=5)
ttk.Button(flip_frame, text="垂直翻转",
command=lambda: self.apply_flip("vertical")).pack(fill=tk.X, padx=10, pady=5)
ttk.Button(rotate_window, text="关闭",
command=rotate_window.destroy).pack(fill=tk.X, padx=20, pady=10)
def apply_rotation(self, angle):
"""应用旋转"""
self.current_image = rotate_image(self.current_image, angle)
self.display_image_on_canvas()
self.status_bar.config(text=f"图片已旋转 {abs(angle)}°")
def apply_flip(self, direction):
"""应用翻转"""
if direction == "horizontal":
self.current_image = self.current_image.transpose(Image.FLIP_LEFT_RIGHT)
self.display_image_on_canvas()
self.status_bar.config(text="图片已水平翻转")
elif direction == "vertical":
self.current_image = self.current_image.transpose(Image.FLIP_TOP_BOTTOM)
self.display_image_on_canvas()
self.status_bar.config(text="图片已垂直翻转")
def flip_image(self):
"""翻转图片 - 调用旋转界面,因为旋转界面已经包含了翻转功能"""
self.rotate_image()
def template_crop(self):
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
template_window = tk.Toplevel(self.root)
template_window.title("模板裁切")
template_window.geometry("900x580")
template_window.transient(self.root)
template_window.grab_set()
template_window.focus_set()
self.center_window(template_window, 900, 580)
main_frame = ttk.Frame(template_window)
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
settings_frame = ttk.Frame(main_frame)
settings_frame.pack(side="left", fill="both", padx=5, pady=5)
ttk.Label(settings_frame, text="请选择裁切模板:").pack(anchor=tk.W, padx=5, pady=5)
templates = [
("微信公众号封面 (900×383)", (900, 383)),
("微信朋友圈 (1080×1080)", (1080, 1080)),
("小红书 (1000×1000)", (1000, 1000)),
("抖音视频封面 (720×1280)", (720, 1280)),
("微博配图 (1200×900)", (1200, 900))
]
radio_frame = ttk.Frame(settings_frame)
radio_frame.pack(fill=tk.X, padx=5, pady=5)
template_var = tk.StringVar()
for i, (name, size) in enumerate(templates):
col = i % 2
row = i // 2
ttk.Radiobutton(radio_frame, text=name, variable=template_var, value=i).grid(row=row, column=col, sticky=tk.W, padx=5, pady=5)
custom_size_radio = ttk.Radiobutton(radio_frame, text="自定义分辨率", variable=template_var, value=len(templates))
custom_size_radio.grid(row=(len(templates)+1)//2, column=0, sticky=tk.W, padx=5, pady=5)
custom_size_frame = ttk.LabelFrame(settings_frame, text="自定义分辨率")
custom_size_frame.pack(fill=tk.X, padx=5, pady=10)
size_entry_frame = ttk.Frame(custom_size_frame)
size_entry_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(size_entry_frame, text="宽度:").grid(row=0, column=0, padx=5, pady=5)
custom_width_var = tk.StringVar(value="800")
custom_width_entry = ttk.Entry(size_entry_frame, textvariable=custom_width_var, width=6)
custom_width_entry.grid(row=0, column=1, padx=5, pady=5)
ttk.Label(size_entry_frame, text="高度:").grid(row=0, column=2, padx=5, pady=5)
custom_height_var = tk.StringVar(value="600")
custom_height_entry = ttk.Entry(size_entry_frame, textvariable=custom_height_var, width=6)
custom_height_entry.grid(row=0, column=3, padx=5, pady=5)
custom_size_button = ttk.Button(size_entry_frame, text="应用自定义尺寸",
command=lambda: template_var.set(str(len(templates))))
custom_size_button.grid(row=0, column=4, padx=5, pady=5)
position_frame = ttk.LabelFrame(settings_frame, text="裁切位置")
position_frame.pack(fill=tk.X, padx=5, pady=10)
position_inner_frame = ttk.Frame(position_frame)
position_inner_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(position_inner_frame, text="位置:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
position_var = tk.StringVar(value="center")
positions = [
("左上", "top-left"),
("居中", "center"),
("右上", "top-right"),
("左下", "bottom-left"),
("右下", "bottom-right"),
("自定义(拖动)", "custom")
]
position_combo = ttk.Combobox(position_inner_frame, textvariable=position_var, values=[p[0] for p in positions], state="readonly", width=10)
position_combo.grid(row=0, column=1, padx=5, pady=5)
position_combo.current(1)
drag_hint_label = ttk.Label(position_inner_frame, text="选择'自定义'后可拖动红框", foreground="blue")
drag_hint_label.grid(row=0, column=2, padx=5, pady=5)
scale_frame = ttk.LabelFrame(settings_frame, text="尺寸设置")
scale_frame.pack(fill=tk.X, padx=5, pady=10)
scale_template_var = tk.BooleanVar(value=True)
ttk.Checkbutton(scale_frame, text="当图片小于模板尺寸时,等比例缩小模板",
variable=scale_template_var).pack(anchor="w", padx=5, pady=5)
status_frame = ttk.Frame(settings_frame)
status_frame.pack(fill=tk.X, padx=5, pady=5)
preview_status_label = ttk.Label(status_frame, text="", foreground="red")
preview_status_label.pack(fill=tk.X, padx=5, pady=5)
button_frame = ttk.Frame(settings_frame)
button_frame.pack(padx=5, pady=15, fill=tk.X)
ttk.Button(button_frame, text="应用裁切", command=lambda: apply_template()).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=template_window.destroy).pack(side=tk.RIGHT, padx=10)
preview_frame = ttk.LabelFrame(main_frame, text="预览")
preview_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
preview_canvas = tk.Canvas(preview_frame, bg="gray90")
preview_canvas.pack(fill="both", expand=True, padx=5, pady=5)
preview_image = None
preview_crop_rect = None
preview_photo = None
current_template_size = None
drag_data = {
"item": None,
"x": 0,
"y": 0,
"start_x": 0,
"start_y": 0,
"crop_x": 0,
"crop_y": 0,
"crop_width": 0,
"crop_height": 0,
"img_x": 0,
"img_y": 0,
"img_width": 0,
"img_height": 0,
"dragging": False
}
def on_drag_start(event):
if position_var.get() != "自定义(拖动)" or preview_crop_rect is None:
return
drag_data["start_x"] = event.x
drag_data["start_y"] = event.y
x1, y1, x2, y2 = preview_canvas.coords(preview_crop_rect)
if x1 <= event.x <= x2 and y1 <= event.y <= y2:
drag_data["item"] = preview_crop_rect
drag_data["dragging"] = True
preview_canvas.config(cursor="fleur")
drag_data["crop_x"] = x1
drag_data["crop_y"] = y1
drag_data["crop_width"] = x2 - x1
drag_data["crop_height"] = y2 - y1
def on_drag_motion(event):
if not drag_data.get("dragging", False) or drag_data["item"] is None:
return
dx = event.x - drag_data["start_x"]
dy = event.y - drag_data["start_y"]
drag_data["start_x"] = event.x
drag_data["start_y"] = event.y
new_x = drag_data["crop_x"] + dx
new_y = drag_data["crop_y"] + dy
img_x = drag_data["img_x"]
img_y = drag_data["img_y"]
img_width = drag_data["img_width"]
img_height = drag_data["img_height"]
crop_width = drag_data["crop_width"]
crop_height = drag_data["crop_height"]
new_x = max(img_x, min(new_x, img_x + img_width - crop_width))
new_y = max(img_y, min(new_y, img_y + img_height - crop_height))
preview_canvas.coords(preview_crop_rect, new_x, new_y, new_x + crop_width, new_y + crop_height)
drag_data["crop_x"] = new_x
drag_data["crop_y"] = new_y
relative_x = new_x - img_x
relative_y = new_y - img_y
preview_status_label.config(text=f"裁切框位置: X={relative_x}, Y={relative_y} (预览坐标)")
def on_drag_end(event):
if drag_data.get("dragging", False):
drag_data["dragging"] = False
preview_canvas.config(cursor="")
preview_canvas.bind("<ButtonPress-1>", on_drag_start)
preview_canvas.bind("<B1-Motion>", on_drag_motion)
preview_canvas.bind("<ButtonRelease-1>", on_drag_end)
def update_preview(*args):
nonlocal preview_image, preview_crop_rect, preview_photo, current_template_size
preview_canvas.delete("all")
try:
if template_var.get() == str(len(templates)):
try:
width = int(custom_width_var.get())
height = int(custom_height_var.get())
if width <= 0 or height <= 0:
raise ValueError("尺寸必须为正数")
size = (width, height)
current_template_size = size
except ValueError as e:
preview_status_label.config(text=f"无效的尺寸: {str(e)}")
return
else:
index = int(template_var.get())
_, size = templates[index]
current_template_size = size
canvas_width = preview_canvas.winfo_width()
canvas_height = preview_canvas.winfo_height()
if canvas_width <= 1 or canvas_height <= 1:
template_window.after(100, update_preview)
return
img_ratio = self.current_image.width / self.current_image.height
template_ratio = size[0] / size[1]
preview_scale = min(canvas_width / self.current_image.width,
canvas_height / self.current_image.height)
preview_width = int(self.current_image.width * preview_scale)
preview_height = int(self.current_image.height * preview_scale)
preview_img = self.current_image.copy()
preview_img.thumbnail((preview_width, preview_height), Image.LANCZOS)
preview_image = ImageTk.PhotoImage(preview_img)
img_x = (canvas_width - preview_width) // 2
img_y = (canvas_height - preview_height) // 2
preview_canvas.create_image(img_x, img_y, anchor=tk.NW, image=preview_image)
drag_data["img_x"] = img_x
drag_data["img_y"] = img_y
drag_data["img_width"] = preview_width
drag_data["img_height"] = preview_height
img_too_small = self.current_image.width < size[0] or self.current_image.height < size[1]
scale_crop_preview = scale_template_var.get()
if img_too_small and scale_crop_preview:
scale_ratio = min(self.current_image.width / size[0],
self.current_image.height / size[1])
crop_width = int(size[0] * scale_ratio * preview_scale)
crop_height = int(size[1] * scale_ratio * preview_scale)
status_text = f"模板已等比缩小 ({int(size[0] * scale_ratio)}×{int(size[1] * scale_ratio)})"
preview_status_label.config(text=status_text)
else:
crop_width = int(size[0] * preview_scale)
crop_height = int(size[1] * preview_scale)
if img_too_small:
preview_status_label.config(text="警告: 图片尺寸小于模板尺寸")
else:
if template_var.get() == str(len(templates)):
preview_status_label.config(text=f"使用自定义分辨率: {size[0]}×{size[1]}")
else:
preview_status_label.config(text="")
pos_value = position_var.get()
pos_dict = {p[0]: p[1] for p in positions}
position = pos_dict.get(pos_value, "center")
if position == "custom" and preview_crop_rect is not None and drag_data["crop_x"] != 0:
crop_x = drag_data["crop_x"]
crop_y = drag_data["crop_y"]
crop_x = max(img_x, min(crop_x, img_x + preview_width - crop_width))
crop_y = max(img_y, min(crop_y, img_y + preview_height - crop_height))
drag_data["crop_width"] = crop_width
drag_data["crop_height"] = crop_height
drag_data["crop_x"] = crop_x
drag_data["crop_y"] = crop_y
elif position == "center":
crop_x = img_x + (preview_width - crop_width) // 2
crop_y = img_y + (preview_height - crop_height) // 2
elif position == "top-left":
crop_x = img_x
crop_y = img_y
elif position == "top-right":
crop_x = img_x + preview_width - crop_width
crop_y = img_y
elif position == "bottom-left":
crop_x = img_x
crop_y = img_y + preview_height - crop_height
elif position == "bottom-right":
crop_x = img_x + preview_width - crop_width
crop_y = img_y + preview_height - crop_height
elif position == "custom":
crop_x = img_x + (preview_width - crop_width) // 2
crop_y = img_y + (preview_height - crop_height) // 2
drag_data["crop_width"] = crop_width
drag_data["crop_height"] = crop_height
drag_data["crop_x"] = crop_x
drag_data["crop_y"] = crop_y
crop_x = max(img_x, min(crop_x, img_x + preview_width - crop_width))
crop_y = max(img_y, min(crop_y, img_y + preview_height - crop_height))
preview_crop_rect = preview_canvas.create_rectangle(
crop_x, crop_y, crop_x + crop_width, crop_y + crop_height,
outline="red", width=2
)
preview_canvas.create_text(
canvas_width // 2,
img_y - 10 if img_y > 20 else img_y + preview_height + 20,
text=f"红色框表示裁切区域 ({size[0]}×{size[1]})",
fill="blue"
)
if position == "custom":
preview_canvas.create_text(
canvas_width // 2,
img_y - 30 if img_y > 40 else img_y + preview_height + 40,
text="点击红框并拖动可调整位置",
fill="red"
)
except (ValueError, IndexError) as e:
preview_status_label.config(text=f"预览错误: {str(e)}")
import traceback
traceback.print_exc()
custom_width_var.trace_add("write", lambda *args: template_var.get() == str(len(templates)) and update_preview())
custom_height_var.trace_add("write", lambda *args: template_var.get() == str(len(templates)) and update_preview())
template_var.trace_add("write", update_preview)
position_var.trace_add("write", update_preview)
scale_template_var.trace_add("write", update_preview)
def on_resize(event):
update_preview()
preview_canvas.bind("<Configure>", on_resize)
def apply_template():
nonlocal current_template_size, preview_crop_rect
if current_template_size is None:
messagebox.showerror("错误", "请选择一个模板!")
return
try:
target_width, target_height = current_template_size
img_width, img_height = self.current_image.width, self.current_image.height
pos_value = position_var.get()
pos_dict = {p[0]: p[1] for p in positions}
position = pos_dict.get(pos_value, "center")
img_too_small = img_width < target_width or img_height < target_height
scale_template = scale_template_var.get()
if img_too_small:
if scale_template:
scale_ratio = min(img_width / target_width, img_height / target_height)
target_width = int(target_width * scale_ratio)
target_height = int(target_height * scale_ratio)
status_msg = f"模板已等比缩小至 {target_width}×{target_height}"
else:
messagebox.showwarning("警告",
f"图像尺寸({img_width}×{img_height})小于模板尺寸({target_width}×{target_height}),\n"
"将自动调整大小以适应模板。这可能导致图像质量下降。")
scale = max(target_width / img_width, target_height / img_height)
new_width = int(img_width * scale)
new_height = int(img_height * scale)
self.current_image = self.current_image.resize((new_width, new_height), Image.LANCZOS)
img_width, img_height = new_width, new_height
status_msg = f"图像已放大至 {img_width}×{img_height} 以适应模板"
else:
status_msg = "已裁切为模板尺寸"
if position == "custom" and preview_crop_rect is not None:
if drag_data["img_width"] > 0 and drag_data["img_height"] > 0:
crop_coords = preview_canvas.coords(preview_crop_rect)
preview_x = crop_coords[0] - drag_data["img_x"]
preview_y = crop_coords[1] - drag_data["img_y"]
x_ratio = preview_x / drag_data["img_width"]
y_ratio = preview_y / drag_data["img_height"]
left = int(x_ratio * img_width)
top = int(y_ratio * img_height)
status_msg += f" (自定义位置 X:{left}, Y:{top})"
else:
left = (img_width - target_width) // 2
top = (img_height - target_height) // 2
elif position == "center":
left = (img_width - target_width) // 2
top = (img_height - target_height) // 2
elif position == "top-left":
left = 0
top = 0
elif position == "top-right":
left = img_width - target_width
top = 0
elif position == "bottom-left":
left = 0
top = img_height - target_height
elif position == "bottom-right":
left = img_width - target_width
top = img_height - target_height
left = max(0, min(left, img_width - target_width))
top = max(0, min(top, img_height - target_height))
right = left + target_width
bottom = top + target_height
self.current_image = self.current_image.crop((left, top, right, bottom))
self.display_image_on_canvas()
template_info = ""
if template_var.get() == str(len(templates)):
template_info = f"自定义分辨率 ({target_width}×{target_height})"
else:
template_index = int(template_var.get())
template_info = templates[template_index][0]
self.status_bar.config(text=f"{status_msg}: {template_info}")
template_window.destroy()
except Exception as e:
messagebox.showerror("错误", f"裁切出错: {str(e)}")
import traceback
traceback.print_exc()
template_var.set("0")
template_window.update_idletasks()
template_window.after(100, update_preview)
def add_watermark(self):
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
def debug_fonts():
try:
print("\n===== 字体调试信息 =====")
if hasattr(self, 'font_paths'):
print(f"字体路径字典包含 {len(self.font_paths)} 个字体")
for name, path in self.font_paths.items():
print(f"字体 '{name}' -> '{path}'")
test_fonts = list(self.font_paths.items())[:3]
from PIL import Image, ImageDraw, ImageFont
for name, path in test_fonts:
print(f"\n测试字体: {name} -> {path}")
try:
font = ImageFont.truetype(path, 24)
print(f"字体加载成功: {font}")
img = Image.new('RGB', (200, 50), color=(255, 255, 255))
d = ImageDraw.Draw(img)
d.text((10, 10), "测试文字ABC", font=font, fill=(0, 0, 0))
print("渲染成功!")
temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
os.makedirs(temp_dir, exist_ok=True)
img_path = os.path.join(temp_dir, f"font_test_{name}.png")
img.save(img_path)
print(f"测试图像已保存到: {img_path}")
except Exception as e:
print(f"字体测试失败: {e}")
else:
print("self.font_paths属性不存在")
common_fonts = ["微软雅黑", "宋体", "黑体", "楷体", "Arial"]
for font_name in common_fonts:
try:
font = ImageFont.truetype(font_name, 24)
print(f"直接加载字体名称成功: {font_name}")
except Exception as e:
print(f"直接加载字体名称失败: {font_name}, 错误: {e}")
print("===== 字体调试结束 =====\n")
except Exception as e:
print(f"字体调试过程出错: {e}")
debug_fonts()
watermark_window = tk.Toplevel(self.root)
watermark_window.title("添加水印")
watermark_window.geometry("800x600")
watermark_window.grab_set()
self.center_window(watermark_window, 800, 600)
main_frame = ttk.Frame(watermark_window)
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
settings_frame = ttk.Frame(main_frame)
settings_frame.pack(side="left", fill="both", expand=True, padx=5, pady=5)
preview_frame = ttk.LabelFrame(main_frame, text="预览")
preview_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
preview_controls = ttk.Frame(preview_frame)
preview_controls.pack(fill="x", padx=5, pady=5)
ttk.Label(preview_controls, text="缩放:").pack(side="left", padx=5)
zoom_var = tk.DoubleVar(value=1.0)
zoom_scale = ttk.Scale(preview_controls, from_=0.5, to=3.0, orient="horizontal",
variable=zoom_var, length=120)
zoom_scale.pack(side="left", padx=5)
zoom_label = ttk.Label(preview_controls, text="100%")
zoom_label.pack(side="left", padx=5)
drag_data = {
"start_x": 0,
"start_y": 0,
"offset_x": 0,
"offset_y": 0,
"dragging": False
}
keep_offset = False
def update_zoom_label(*args):
zoom_label.config(text=f"{int(zoom_var.get() * 100)}%")
update_preview()
zoom_var.trace_add("write", update_zoom_label)
grid_var = tk.BooleanVar(value=True)
grid_check = ttk.Checkbutton(preview_controls, text="显示网格", variable=grid_var,
command=lambda: update_preview())
grid_check.pack(side="right", padx=10)
preview_container = ttk.Frame(preview_frame, style="Preview.TFrame")
preview_container.pack(fill="both", expand=True, padx=10, pady=10)
style = ttk.Style()
style.configure("Preview.TFrame", background="#f0f0f0", relief="sunken", borderwidth=1)
preview_canvas = tk.Canvas(preview_container, bg="#f0f0f0", highlightthickness=1,
highlightbackground="#cccccc")
preview_canvas.pack(fill="both", expand=True)
def on_drag_start(event):
if zoom_var.get() <= 1.0:
return
drag_data["start_x"] = event.x
drag_data["start_y"] = event.y
drag_data["dragging"] = True
preview_canvas.config(cursor="fleur")
def on_drag_motion(event):
if not drag_data.get("dragging", False):
return
delta_x = event.x - drag_data["start_x"]
delta_y = event.y - drag_data["start_y"]
drag_data["offset_x"] += delta_x
drag_data["offset_y"] += delta_y
drag_data["start_x"] = event.x
drag_data["start_y"] = event.y
nonlocal keep_offset
keep_offset = True
update_preview()
keep_offset = False
def on_drag_end(event):
drag_data["dragging"] = False
preview_canvas.config(cursor="")
preview_canvas.bind("<ButtonPress-1>", on_drag_start)
preview_canvas.bind("<B1-Motion>", on_drag_motion)
preview_canvas.bind("<ButtonRelease-1>", on_drag_end)
status_var = tk.StringVar(value="准备添加水印")
status_label = ttk.Label(preview_frame, textvariable=status_var, anchor="center")
status_label.pack(fill="x", padx=10, pady=5)
type_frame = ttk.LabelFrame(settings_frame, text="水印类型", padding=5)
type_frame.pack(fill="x", padx=10, pady=5)
watermark_type = tk.StringVar(value="text")
text_radio = ttk.Radiobutton(type_frame, text="文字水印", variable=watermark_type, value="text")
image_radio = ttk.Radiobutton(type_frame, text="图片水印", variable=watermark_type, value="image")
text_radio.grid(row=0, column=0, padx=5, pady=5)
image_radio.grid(row=0, column=1, padx=5, pady=5)
position_mode_var = tk.StringVar(value="preset")
position_frame = ttk.LabelFrame(settings_frame, text="位置设置", padding=5)
position_frame.pack(fill="x", padx=10, pady=5)
mode_frame = ttk.Frame(position_frame)
mode_frame.pack(fill="x", padx=5, pady=5)
ttk.Radiobutton(mode_frame, text="预设位置", variable=position_mode_var, value="preset").pack(side="left", padx=5)
ttk.Radiobutton(mode_frame, text="自定义坐标", variable=position_mode_var, value="custom").pack(side="left", padx=20)
preset_frame = ttk.Frame(position_frame)
preset_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(preset_frame, text="预设位置:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
position_var = tk.StringVar(value="右下")
position_combo = ttk.Combobox(preset_frame, textvariable=position_var,
values=["左上", "右上", "左下", "右下", "中心", "顶部居中", "底部居中", "左侧居中", "右侧居中"])
position_combo.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
position_combo.current(3)
ttk.Label(preset_frame, text="边距:").grid(row=1, column=0, sticky="w", padx=5, pady=5)
margin_var = tk.IntVar(value=10)
margin_spinbox = ttk.Spinbox(preset_frame, from_=0, to=100, textvariable=margin_var, width=10)
margin_spinbox.grid(row=1, column=1, sticky="w", padx=5, pady=5)
custom_frame = ttk.Frame(position_frame)
ttk.Label(custom_frame, text="X坐标:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
x_position_var = tk.IntVar(value=30)
x_spinbox = ttk.Spinbox(custom_frame, from_=0, to=9999, textvariable=x_position_var, width=10)
x_spinbox.grid(row=0, column=1, sticky="w", padx=5, pady=5)
ttk.Label(custom_frame, text="Y坐标:").grid(row=1, column=0, sticky="w", padx=5, pady=5)
y_position_var = tk.IntVar(value=30)
y_spinbox = ttk.Spinbox(custom_frame, from_=0, to=9999, textvariable=y_position_var, width=10)
y_spinbox.grid(row=1, column=1, sticky="w", padx=5, pady=5)
def update_position_ui(*args):
if position_mode_var.get() == "preset":
preset_frame.pack(fill=tk.X)
custom_frame.pack_forget()
for child in preset_frame.winfo_children():
child.configure(state="normal")
for child in custom_frame.winfo_children():
child.configure(state="disabled")
else:
preset_frame.pack_forget()
custom_frame.pack(fill=tk.X)
for child in preset_frame.winfo_children():
child.configure(state="disabled")
for child in custom_frame.winfo_children():
child.configure(state="normal")
position_mode_var.trace_add("write", update_position_ui)
position_var.trace_add("write", lambda *args: update_preview())
margin_var.trace_add("write", lambda *args: update_preview())
x_position_var.trace_add("write", lambda *args: update_preview())
y_position_var.trace_add("write", lambda *args: update_preview())
text_frame = ttk.LabelFrame(settings_frame, text="文字水印设置", padding=5)
text_frame.pack(fill="x", padx=10, pady=5)
ttk.Label(text_frame, text="水印文字:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
watermark_text = tk.StringVar(value="版权所有")
text_entry = ttk.Entry(text_frame, textvariable=watermark_text, width=30)
text_entry.grid(row=0, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
ttk.Label(text_frame, text="字体:").grid(row=1, column=0, sticky="w", padx=5, pady=5)
system_fonts, font_paths = self.get_system_fonts()
self.font_paths = font_paths
font_var = tk.StringVar()
if system_fonts:
font_combo = ttk.Combobox(text_frame, textvariable=font_var, values=system_fonts)
font_combo.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
if len(system_fonts) > 0:
font_combo.current(0)
else:
font_entry = ttk.Entry(text_frame, textvariable=font_var)
font_entry.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
ttk.Label(text_frame, text="字号:").grid(row=2, column=0, sticky="w", padx=5, pady=5)
font_size_var = tk.IntVar(value=36)
font_size_spinbox = ttk.Spinbox(text_frame, from_=8, to=120, textvariable=font_size_var, width=10)
font_size_spinbox.grid(row=2, column=1, sticky="w", padx=5, pady=5)
ttk.Label(text_frame, text="字间距:").grid(row=2, column=2, sticky="w", padx=5, pady=5)
letter_spacing_var = tk.IntVar(value=0)
letter_spacing_spinbox = ttk.Spinbox(text_frame, from_=-10, to=50, textvariable=letter_spacing_var, width=10)
letter_spacing_spinbox.grid(row=2, column=3, sticky="w", padx=5, pady=5)
ttk.Label(text_frame, text="颜色:").grid(row=3, column=0, sticky="w", padx=5, pady=5)
color_var = tk.StringVar(value="#000000")
color_preview = tk.Canvas(text_frame, width=30, height=20, bg=color_var.get())
color_preview.grid(row=3, column=1, sticky="w", padx=5, pady=5)
def choose_color():
color = colorchooser.askcolor(color_var.get())
if color[1]:
color_var.set(color[1])
color_preview.config(bg=color[1])
update_preview()
color_button = ttk.Button(text_frame, text="选择颜色", command=choose_color)
color_button.grid(row=3, column=2, sticky="w", padx=5, pady=5)
bold_var = tk.BooleanVar(value=False)
italic_var = tk.BooleanVar(value=False)
ttk.Label(text_frame, text="文字样式:").grid(row=4, column=0, sticky="w", padx=5, pady=5)
style_frame = ttk.Frame(text_frame)
style_frame.grid(row=4, column=1, columnspan=2, sticky="w", padx=5, pady=5)
bold_check = ttk.Checkbutton(style_frame, text="粗体", variable=bold_var)
bold_check.pack(side="left", padx=5)
italic_check = ttk.Checkbutton(style_frame, text="斜体", variable=italic_var)
italic_check.pack(side="left", padx=20)
bold_var.trace_add("write", lambda *args: update_preview())
italic_var.trace_add("write", lambda *args: update_preview())
image_frame = ttk.LabelFrame(settings_frame, text="图片水印设置", padding=10)
image_frame.pack(fill="x", padx=10, pady=5)
watermark_image_path = tk.StringVar()
watermark_img_obj = [None]
ttk.Label(image_frame, text="图片路径:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
path_entry = ttk.Entry(image_frame, textvariable=watermark_image_path, state="readonly", width=30)
path_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
def select_watermark_image():
filename = filedialog.askopenfilename(
title="选择水印图片",
filetypes=(
("PNG图片(透明背景)", "*.png"),
("图片文件", "*.jpg;*.jpeg;*.bmp;*.gif"),
("所有文件", "*.*")
)
)
if filename:
try:
from PIL import Image
img = Image.open(filename)
watermark_img_obj[0] = img
watermark_image_path.set(filename)
img_width, img_height = img.size
img_info_var.set(f"原始大小: {img_width}×{img_height}像素")
wm_size_percent_var.set(100)
update_ui()
update_preview()
except Exception as e:
messagebox.showerror("错误", f"无法加载图片: {str(e)}")
browse_button = ttk.Button(image_frame, text="浏览...", command=select_watermark_image)
browse_button.grid(row=0, column=2, padx=5, pady=5)
img_info_var = tk.StringVar(value="未选择图片")
ttk.Label(image_frame, textvariable=img_info_var).grid(row=1, column=0, columnspan=3, sticky="w", padx=5, pady=5)
size_frame = ttk.Frame(image_frame)
size_frame.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
ttk.Label(size_frame, text="水印大小:").pack(side="left", padx=5)
wm_size_percent_var = tk.IntVar(value=100)
size_scale = ttk.Scale(size_frame, from_=5, to=200, orient="horizontal",
variable=wm_size_percent_var, length=200)
size_scale.pack(side="left", fill="x", expand=True, padx=5)
ttk.Label(size_frame, textvariable=tk.StringVar(value="%")).pack(side="right", padx=2)
size_spinbox = ttk.Spinbox(size_frame, from_=5, to=200, textvariable=wm_size_percent_var, width=5)
size_spinbox.pack(side="right", padx=2)
wm_size_percent_var.trace_add("write", lambda *args: update_preview())
opacity_frame = ttk.LabelFrame(settings_frame, text="透明度设置", padding=5)
opacity_frame.pack(fill="x", padx=10, pady=5)
opacity_var = tk.DoubleVar(value=0.5)
opacity_label_var = tk.StringVar(value="50%")
def update_opacity_label(*args):
opacity_label_var.set(f"{int(opacity_var.get() * 100)}%")
update_preview()
opacity_var.trace_add("write", update_opacity_label)
ttk.Label(opacity_frame, text="不透明度:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
opacity_scale = ttk.Scale(opacity_frame, from_=0.0, to=1.0, orient="horizontal",
variable=opacity_var, length=200)
opacity_scale.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(opacity_frame, textvariable=opacity_label_var).grid(row=0, column=2, padx=5, pady=5, sticky="w")
preview_image = None
preview_photo = None
def update_preview():
"""更新预览图像"""
nonlocal preview_image, preview_photo
if not self.current_image:
status_var.set("请先打开一张图片")
return
try:
position = None
if position_mode_var.get() == "preset":
position = position_var.get()
margin = margin_var.get()
else:
position = (x_position_var.get(), y_position_var.get())
margin = 0
canvas_width = preview_canvas.winfo_width() - 20
canvas_height = preview_canvas.winfo_height() - 20
if canvas_width <= 1 or canvas_height <= 1:
preview_canvas.after(100, update_preview)
return
img_copy = self.current_image.copy()
img_width, img_height = img_copy.size
try:
zoom_factor = zoom_var.get()
except (NameError, AttributeError):
zoom_factor = 1.0
base_scale = min(canvas_width / img_width, canvas_height / img_height)
scale = base_scale * zoom_factor
new_width = int(img_width * scale)
new_height = int(img_height * scale)
if zoom_factor <= 1.0 and not keep_offset:
drag_data["offset_x"] = 0
drag_data["offset_y"] = 0
preview_base = img_copy.resize((new_width, new_height), Image.LANCZOS)
preview_margin = int(margin * scale)
if watermark_type.get() == "text":
text = watermark_text.get()
font = font_var.get()
size = max(int(font_size_var.get() * scale), 8)
letter_spacing = int(letter_spacing_var.get() * scale)
font_path = None
if hasattr(self, 'font_paths'):
print(f"字体路径字典包含 {len(self.font_paths)} 个条目")
if font in self.font_paths:
font_path = self.font_paths[font]
print(f"找到字体路径: {font_path} (对应字体: {font})")
else:
print(f"字体 '{font}' 不在字体路径字典中")
print(f"可用字体: {list(self.font_paths.keys())}")
else:
print("font_paths 属性不存在")
try:
from PIL import ImageColor
color = ImageColor.getcolor(color_var.get(), "RGB")
except:
color = (0, 0, 0)
preview_position = position
if position_mode_var.get() == "custom":
custom_x, custom_y = position
preview_position = (custom_x, custom_y)
from image_utils import add_watermark
preview_image = add_watermark(
preview_base,
watermark_text=text,
position=preview_position,
opacity=opacity_var.get(),
font_name=font_path if font_path else font,
font_size=size,
font_color=color,
letter_spacing=letter_spacing,
margin=preview_margin,
bold=bold_var.get(),
italic=italic_var.get()
)
if position_mode_var.get() == "preset":
status_var.set(f"预览文字水印: {text} (边距: {margin}px)")
else:
status_var.set(f"预览文字水印: {text} (坐标: {position})")
elif watermark_type.get() == "image" and watermark_img_obj[0] is not None:
wm_img = watermark_img_obj[0].copy()
if wm_img:
wm_w, wm_h = wm_img.size
percent = wm_size_percent_var.get() / 100.0
wm_new_width = int(wm_w * scale * percent)
wm_new_height = int(wm_h * scale * percent)
wm_new_width = max(1, wm_new_width)
wm_new_height = max(1, wm_new_height)
wm_img = wm_img.resize((wm_new_width, wm_new_height), Image.LANCZOS)
preview_position = position
if position_mode_var.get() == "custom":
custom_x, custom_y = position
preview_position = (custom_x, custom_y)
from image_utils import add_watermark
preview_image = add_watermark(
preview_base,
watermark_image=wm_img,
position=preview_position,
opacity=opacity_var.get(),
margin=preview_margin,
bold=False,
italic=False
)
if position_mode_var.get() == "preset":
status_var.set(f"预览图片水印: {os.path.basename(watermark_image_path.get())} ({wm_size_percent_var.get()}%) (边距: {margin}px)")
else:
status_var.set(f"预览图片水印: {os.path.basename(watermark_image_path.get())} ({wm_size_percent_var.get()}%) (坐标: {position})")
else:
preview_image = preview_base
status_var.set("水印图片加载失败")
else:
preview_image = preview_base
if watermark_type.get() == "image":
status_var.set("请选择水印图片")
else:
status_var.set("请配置水印选项")
preview_photo = ImageTk.PhotoImage(preview_image)
canvas_width = preview_canvas.winfo_width()
canvas_height = preview_canvas.winfo_height()
x_pos = max(0, (canvas_width - new_width) // 2) + drag_data["offset_x"]
y_pos = max(0, (canvas_height - new_height) // 2) + drag_data["offset_y"]
if x_pos > canvas_width - 50:
x_pos = canvas_width - 50
drag_data["offset_x"] = x_pos - max(0, (canvas_width - new_width) // 2)
if y_pos > canvas_height - 50:
y_pos = canvas_height - 50
drag_data["offset_y"] = y_pos - max(0, (canvas_height - new_height) // 2)
if x_pos + new_width < 50:
x_pos = 50 - new_width
drag_data["offset_x"] = x_pos - max(0, (canvas_width - new_width) // 2)
if y_pos + new_height < 50:
y_pos = 50 - new_height
drag_data["offset_y"] = y_pos - max(0, (canvas_height - new_height) // 2)
preview_canvas.delete("all")
try:
show_grid = grid_var.get()
except (NameError, AttributeError):
show_grid = False
if show_grid:
for y in range(0, canvas_height, 20):
preview_canvas.create_line(0, y, canvas_width, y, fill="#dddddd", dash=(2, 4))
for x in range(0, canvas_width, 20):
preview_canvas.create_line(x, 0, x, canvas_height, fill="#dddddd", dash=(2, 4))
center_x = canvas_width // 2
center_y = canvas_height // 2
preview_canvas.create_line(center_x, 0, center_x, canvas_height, fill="#999999", dash=(4, 4))
preview_canvas.create_line(0, center_y, canvas_width, center_y, fill="#999999", dash=(4, 4))
border_color = "#3399ff"
preview_canvas.create_rectangle(
x_pos - 1, y_pos - 1,
x_pos + new_width + 1, y_pos + new_height + 1,
outline=border_color, width=2
)
img_item = preview_canvas.create_image(x_pos, y_pos, image=preview_photo, anchor="nw")
preview_canvas.tag_raise(img_item)
if position_mode_var.get() == "custom":
custom_x, custom_y = position
scaled_x = int(custom_x * scale) + x_pos
scaled_y = int(custom_y * scale) + y_pos
if 0 <= scaled_x < canvas_width and 0 <= scaled_y < canvas_height:
mark_size = 5
preview_canvas.create_line(
scaled_x - mark_size, scaled_y,
scaled_x + mark_size, scaled_y,
fill="red", width=2
)
preview_canvas.create_line(
scaled_x, scaled_y - mark_size,
scaled_x, scaled_y + mark_size,
fill="red", width=2
)
preview_canvas.create_text(
scaled_x, scaled_y + mark_size + 10,
text=f"({custom_x}, {custom_y})",
fill="red", font=("Arial", 8)
)
try:
if zoom_var.get() > 1.0:
help_text = "鼠标拖动可平移图像"
if drag_data.get("dragging", False):
help_text = "正在平移..."
preview_canvas.create_text(
canvas_width - 10, canvas_height - 10,
text=help_text,
anchor="se", fill="#666666", font=("Arial", 8)
)
except (NameError, AttributeError):
pass
except Exception as e:
status_var.set(f"预览出错: {str(e)}")
import traceback
traceback.print_exc()
def update_ui():
if watermark_type.get() == "text":
text_frame.pack(fill="x", padx=10, pady=5)
image_frame.pack_forget()
else:
text_frame.pack_forget()
image_frame.pack(fill="x", padx=10, pady=5)
update_preview()
watermark_type.trace_add("write", lambda *args: update_ui())
watermark_text.trace_add("write", lambda *args: update_preview())
font_var.trace_add("write", lambda *args: update_preview())
font_size_var.trace_add("write", lambda *args: update_preview())
letter_spacing_var.trace_add("write", lambda *args: update_preview())
def on_resize(event):
if event.widget == preview_canvas:
update_preview()
preview_canvas.bind("<Configure>", on_resize)
update_ui()
update_position_ui()
button_frame = ttk.Frame(watermark_window)
button_frame.pack(side="bottom", fill="x", padx=10, pady=10)
def apply_watermark():
if not self.current_image:
messagebox.showerror("错误", "请先打开一张图片")
return
try:
img_copy = self.current_image.copy()
position = None
if position_mode_var.get() == "preset":
position = position_var.get()
margin = margin_var.get()
else:
position = (x_position_var.get(), y_position_var.get())
margin = 0
if watermark_type.get() == "text":
text = watermark_text.get()
font = font_var.get()
font_path = None
if hasattr(self, 'font_paths'):
print(f"字体路径字典包含 {len(self.font_paths)} 个条目")
if font in self.font_paths:
font_path = self.font_paths[font]
print(f"找到字体路径: {font_path} (对应字体: {font})")
else:
print(f"字体 '{font}' 不在字体路径字典中")
print(f"可用字体: {list(self.font_paths.keys())}")
else:
print("font_paths 属性不存在")
try:
from PIL import ImageColor
color = ImageColor.getcolor(color_var.get(), "RGB")
except:
color = (0, 0, 0)
from image_utils import add_watermark
self.current_image = add_watermark(
img_copy,
watermark_text=text,
position=position,
opacity=opacity_var.get(),
font_name=font_path if font_path else font,
font_size=font_size_var.get(),
font_color=color,
letter_spacing=letter_spacing_var.get(),
margin=margin,
bold=bold_var.get(),
italic=italic_var.get()
)
self.display_image_on_canvas()
watermark_window.destroy()
self.status_bar.config(text=f"已添加文字水印: {text}")
else:
if watermark_img_obj[0] is None:
messagebox.showerror("错误", "请选择水印图片")
return
watermark_img = watermark_img_obj[0].copy()
if wm_size_percent_var.get() != 100:
wm_w, wm_h = watermark_img.size
percent = wm_size_percent_var.get() / 100.0
new_width = int(wm_w * percent)
new_height = int(wm_h * percent)
new_width = max(1, new_width)
new_height = max(1, new_height)
watermark_img = watermark_img.resize((new_width, new_height), Image.LANCZOS)
opacity = opacity_var.get()
from image_utils import add_watermark
self.current_image = add_watermark(
img_copy,
watermark_image=watermark_img,
position=position,
opacity=opacity,
margin=margin
)
self.display_image_on_canvas()
watermark_window.destroy()
size_info = ""
if wm_size_percent_var.get() != 100:
size_info = f" ({wm_size_percent_var.get()}%)"
self.status_bar.config(text=f"已添加图片水印: {os.path.basename(watermark_image_path.get())}{size_info}")
except Exception as e:
messagebox.showerror("错误", f"添加水印时出错: {str(e)}")
import traceback
traceback.print_exc()
separator = ttk.Separator(button_frame, orient="horizontal")
separator.pack(fill="x", pady=5)
cancel_button = ttk.Button(button_frame, text="取消", command=watermark_window.destroy)
cancel_button.pack(side="right", padx=5)
apply_button = ttk.Button(button_frame, text="应用水印", command=apply_watermark)
apply_button.pack(side="right", padx=5)
watermark_window.after(200, update_preview)
def adjust_image(self):
"""基础调整:亮度、对比度、饱和度、色调"""
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
original_image = self.current_image.copy()
adjust_window = tk.Toplevel(self.root)
adjust_window.title("图像基础调整")
adjust_window.geometry("400x250")
adjust_window.resizable(False, False)
adjust_window.transient(self.root)
adjust_window.grab_set()
adjust_window.focus_set()
self.center_window(adjust_window, 400, 250)
adjustments = [
("亮度", 0.0, 2.0, 1.0),
("对比度", 0.0, 2.0, 1.0),
("饱和度", 0.0, 2.0, 1.0),
("色调", 0, 360, 0)
]
sliders_vars = []
for i, (name, min_val, max_val, default) in enumerate(adjustments):
frame = ttk.Frame(adjust_window)
frame.pack(fill=tk.X, padx=20, pady=5)
ttk.Label(frame, text=f"{name}:").pack(side=tk.LEFT, padx=5)
value_var = tk.StringVar(value=str(default))
ttk.Label(frame, textvariable=value_var, width=4).pack(side=tk.RIGHT, padx=5)
slider_var = tk.DoubleVar(value=default)
slider = ttk.Scale(
frame,
from_=min_val,
to=max_val,
variable=slider_var,
orient=tk.HORIZONTAL,
length=300
)
slider.pack(fill=tk.X, padx=5, expand=True)
sliders_vars.append((slider_var, value_var))
def update_value(var, value_var, _):
value_var.set(f"{var.get():.1f}")
slider_var.trace_add("write", lambda *args, var=slider_var, value_var=value_var: update_value(var, value_var, args))
preview_delay = 300
last_update_time = [0]
def update_preview():
current_time = int(time.time() * 1000)
if current_time - last_update_time[0] < preview_delay:
adjust_window.after(preview_delay - (current_time - last_update_time[0]), update_preview)
return
last_update_time[0] = current_time
try:
img = original_image.copy()
brightness_factor = sliders_vars[0][0].get()
if brightness_factor != 1.0:
enhancer = ImageEnhance.Brightness(img)
img = enhancer.enhance(brightness_factor)
contrast_factor = sliders_vars[1][0].get()
if contrast_factor != 1.0:
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(contrast_factor)
saturation_factor = sliders_vars[2][0].get()
if saturation_factor != 1.0:
img = img.convert("HSV")
h, s, v = img.split()
s = ImageEnhance.Brightness(s).enhance(saturation_factor)
img = Image.merge("HSV", (h, s, v)).convert("RGB")
hue_shift = int(sliders_vars[3][0].get())
if hue_shift != 0:
img = img.convert("HSV")
h, s, v = img.split()
h_data = list(h.getdata())
h_data = [(x + hue_shift) % 360 for x in h_data]
h.putdata(h_data)
img = Image.merge("HSV", (h, s, v)).convert("RGB")
self.current_image = img
self.display_image_on_canvas()
except Exception as e:
print(f"预览更新错误: {str(e)}")
def on_slider_change(*args):
update_preview()
for slider_var, _ in sliders_vars:
slider_var.trace_add("write", on_slider_change)
button_frame = ttk.Frame(adjust_window)
button_frame.pack(padx=20, pady=10)
def apply_adjustments():
adjust_window.destroy()
def reset_sliders():
for i, (slider_var, _) in enumerate(sliders_vars):
slider_var.set(adjustments[i][3])
def cancel_adjustments():
self.current_image = original_image
self.display_image_on_canvas()
adjust_window.destroy()
ttk.Button(button_frame, text="应用", command=apply_adjustments).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="重置", command=reset_sliders).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=cancel_adjustments).pack(side=tk.LEFT, padx=10)
def on_window_close():
cancel_adjustments()
adjust_window.protocol("WM_DELETE_WINDOW", on_window_close)
def convert_format(self):
"""转换图片格式"""
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
convert_window = tk.Toplevel(self.root)
convert_window.title("转换图片格式")
convert_window.geometry("400x480")
convert_window.resizable(False, False)
convert_window.transient(self.root)
convert_window.grab_set()
convert_window.focus_set()
self.center_window(convert_window, 400, 480)
main_frame = ttk.Frame(convert_window)
main_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(main_frame, text="请选择目标格式:").pack(anchor=tk.W, padx=15, pady=10)
formats = [
("JPEG/JPG - 常见照片格式,不支持透明", "JPEG", ".jpg"),
("PNG - 支持透明,适合图形和截图", "PNG", ".png"),
("WEBP - 谷歌开发的高压缩比格式", "WEBP", ".webp"),
("BMP - 无损位图格式", "BMP", ".bmp"),
("GIF - 支持动画的格式", "GIF", ".gif"),
("TIFF - 专业图像格式", "TIFF", ".tif")
]
format_var = tk.StringVar()
for i, (desc, _, _) in enumerate(formats):
ttk.Radiobutton(main_frame, text=desc, variable=format_var, value=i).pack(anchor=tk.W, padx=20, pady=3)
current_format = "未知格式"
if hasattr(self.current_image, 'format') and self.current_image.format:
current_format = self.current_image.format
img_byte_arr = io.BytesIO()
self.current_image.save(img_byte_arr, format=current_format if current_format != "未知格式" else "PNG")
current_size = len(img_byte_arr.getvalue()) / 1024
info_frame = ttk.Frame(main_frame)
info_frame.pack(fill=tk.X, padx=15, pady=5)
ttk.Label(info_frame, text=f"当前格式: {current_format}").pack(side=tk.LEFT, padx=5)
ttk.Label(info_frame, text=f"当前大小: {current_size:.1f} KB").pack(side=tk.LEFT, padx=15)
jpg_frame = ttk.LabelFrame(main_frame, text="JPEG设置")
ttk.Label(jpg_frame, text="质量:").pack(side=tk.LEFT, padx=5, pady=5)
quality_var = tk.IntVar(value=90)
quality_scale = ttk.Scale(jpg_frame, from_=1, to=100, variable=quality_var, orient=tk.HORIZONTAL)
quality_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
quality_label = ttk.Label(jpg_frame, text="90")
quality_label.pack(side=tk.LEFT, padx=5, pady=5)
def update_quality_label(*args):
quality_label.config(text=str(quality_var.get()))
quality_var.trace_add("write", update_quality_label)
png_frame = ttk.LabelFrame(main_frame, text="PNG设置")
transparency_var = tk.BooleanVar(value=True)
ttk.Checkbutton(png_frame, text="保留透明度", variable=transparency_var).pack(anchor=tk.W, padx=5, pady=5)
webp_frame = ttk.LabelFrame(main_frame, text="WebP设置")
webp_quality_var = tk.IntVar(value=80)
ttk.Label(webp_frame, text="质量:").pack(side=tk.LEFT, padx=5, pady=5)
webp_quality_scale = ttk.Scale(webp_frame, from_=1, to=100, variable=webp_quality_var, orient=tk.HORIZONTAL)
webp_quality_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
webp_quality_label = ttk.Label(webp_frame, text="80")
webp_quality_label.pack(side=tk.LEFT, padx=5, pady=5)
webp_quality_var.trace_add("write", lambda *args: webp_quality_label.config(text=str(webp_quality_var.get())))
compress_frame = ttk.LabelFrame(main_frame, text="压缩到指定大小")
compress_enable_var = tk.BooleanVar(value=False)
ttk.Checkbutton(compress_frame, text="启用压缩到指定大小",
variable=compress_enable_var).pack(anchor=tk.W, padx=5, pady=5)
size_frame = ttk.Frame(compress_frame)
size_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(size_frame, text="目标大小:").pack(side=tk.LEFT, padx=5)
target_size_var = tk.DoubleVar(value=100)
target_size_entry = ttk.Entry(size_frame, textvariable=target_size_var, width=10)
target_size_entry.pack(side=tk.LEFT, padx=5)
size_unit_var = tk.StringVar(value="KB")
unit_combo = ttk.Combobox(size_frame, textvariable=size_unit_var, values=["KB", "MB"], width=5, state="readonly")
unit_combo.pack(side=tk.LEFT, padx=5)
def update_compress_ui(*args):
state = "normal" if compress_enable_var.get() else "disabled"
combo_state = "readonly" if compress_enable_var.get() else "disabled"
try:
target_size_entry.config(state=state)
except:
pass
try:
unit_combo.config(state=combo_state)
except:
pass
compress_enable_var.trace_add("write", update_compress_ui)
update_compress_ui()
last_settings_frame = None
def update_format_frames(*args):
nonlocal last_settings_frame
try:
index = int(format_var.get())
_, fmt, _ = formats[index]
for frame in [jpg_frame, png_frame, webp_frame, compress_frame]:
frame.pack_forget()
if fmt == "JPEG":
jpg_frame.pack(fill=tk.X, padx=15, pady=5)
last_settings_frame = jpg_frame
elif fmt == "PNG":
png_frame.pack(fill=tk.X, padx=15, pady=5)
last_settings_frame = png_frame
elif fmt == "WEBP":
webp_frame.pack(fill=tk.X, padx=15, pady=5)
last_settings_frame = webp_frame
else:
last_settings_frame = None
compress_frame.pack(fill=tk.X, padx=15, pady=5)
if fmt in ["BMP"]:
compress_enable_var.set(False)
for widget in compress_frame.winfo_children():
if isinstance(widget, (ttk.Entry, ttk.Combobox, ttk.Checkbutton, ttk.Button)):
widget.config(state="disabled")
target_size_entry.config(state="disabled")
unit_combo.config(state="disabled")
else:
for widget in compress_frame.winfo_children():
if isinstance(widget, (ttk.Entry, ttk.Combobox, ttk.Button)):
widget.config(state="normal")
elif isinstance(widget, ttk.Checkbutton):
widget.config(state="normal")
update_compress_ui()
except (ValueError, IndexError):
for frame in [jpg_frame, png_frame, webp_frame, compress_frame]:
frame.pack_forget()
format_var.trace_add("write", update_format_frames)
format_var.set("0")
button_frame = ttk.Frame(convert_window)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=15, pady=15)
def compress_to_target_size(img, fmt, target_kb, save_options=None, min_quality=5):
"""压缩图像到指定大小"""
if save_options is None:
save_options = {}
img_byte_arr = io.BytesIO()
if fmt not in ["JPEG", "WEBP", "PNG"]:
img.save(img_byte_arr, format=fmt)
return img, len(img_byte_arr.getvalue()) / 1024
processed_img = img.copy()
if fmt == "JPEG" and processed_img.mode in ['P', 'RGBA', 'LA']:
processed_img = processed_img.convert('RGB')
elif fmt == "PNG" and processed_img.mode not in ['RGB', 'RGBA', 'P']:
processed_img = processed_img.convert('RGBA')
elif fmt == "WEBP" and processed_img.mode in ['P']:
processed_img = processed_img.convert('RGBA')
quality = 95
max_quality = 100
if "quality" in save_options:
quality = save_options["quality"]
max_quality = quality
save_opts = save_options.copy()
if fmt in ["JPEG", "WEBP"]:
save_opts["quality"] = quality
processed_img.save(img_byte_arr, format=fmt, **save_opts)
current_size = len(img_byte_arr.getvalue()) / 1024
if current_size <= target_kb:
low_q = quality
high_q = max_quality
best_q = quality
best_size = current_size
while low_q <= high_q:
mid_q = (low_q + high_q) // 2
img_byte_arr = io.BytesIO()
save_opts = save_options.copy()
if fmt in ["JPEG", "WEBP"]:
save_opts["quality"] = mid_q
processed_img.save(img_byte_arr, format=fmt, **save_opts)
mid_size = len(img_byte_arr.getvalue()) / 1024
if mid_size <= target_kb:
best_q = mid_q
best_size = mid_size
low_q = mid_q + 1
else:
high_q = mid_q - 1
img_byte_arr = io.BytesIO()
save_opts = save_options.copy()
if fmt in ["JPEG", "WEBP"]:
save_opts["quality"] = best_q
processed_img.save(img_byte_arr, format=fmt, **save_opts)
img_byte_arr.seek(0)
result_img = Image.open(img_byte_arr)
return result_img, best_size
iterations = 0
max_iterations = 20
while current_size > target_kb and quality >= min_quality and iterations < max_iterations:
quality -= 5
img_byte_arr = io.BytesIO()
save_opts = save_options.copy()
if fmt in ["JPEG", "WEBP"]:
save_opts["quality"] = quality
elif fmt == "PNG" and quality < 50:
processed_img = processed_img.quantize(colors=256)
if fmt == "JPEG" and processed_img.mode != 'RGB':
processed_img = processed_img.convert('RGB')
processed_img.save(img_byte_arr, format=fmt, **save_opts)
current_size = len(img_byte_arr.getvalue()) / 1024
iterations += 1
scale_factor = 1.0
while current_size > target_kb and scale_factor > 0.1:
prev_scale = scale_factor
scale_factor = min(prev_scale * 0.95, math.sqrt(target_kb / current_size))
if abs(scale_factor - prev_scale) < 0.01:
scale_factor = prev_scale * 0.9
new_width = max(100, int(img.width * scale_factor))
new_height = max(100, int(img.height * scale_factor))
processed_img = img.resize((new_width, new_height), Image.LANCZOS)
if fmt == "JPEG" and processed_img.mode != 'RGB':
processed_img = processed_img.convert('RGB')
img_byte_arr = io.BytesIO()
save_opts = save_options.copy()
if fmt in ["JPEG", "WEBP"]:
save_opts["quality"] = quality
processed_img.save(img_byte_arr, format=fmt, **save_opts)
current_size = len(img_byte_arr.getvalue()) / 1024
if current_size > target_kb:
if fmt == "PNG":
processed_img = processed_img.quantize(colors=64)
img_byte_arr = io.BytesIO()
processed_img.save(img_byte_arr, format=fmt, optimize=True)
current_size = len(img_byte_arr.getvalue()) / 1024
elif fmt == "JPEG":
if processed_img.mode != 'RGB':
processed_img = processed_img.convert('RGB')
img_byte_arr = io.BytesIO()
save_opts = {"quality": 1, "optimize": True, "subsampling": 2}
processed_img.save(img_byte_arr, format=fmt, **save_opts)
current_size = len(img_byte_arr.getvalue()) / 1024
elif fmt == "WEBP":
img_byte_arr = io.BytesIO()
save_opts = {"quality": 1, "method": 6, "lossless": False}
processed_img.save(img_byte_arr, format=fmt, **save_opts)
current_size = len(img_byte_arr.getvalue()) / 1024
img_byte_arr.seek(0)
result_img = Image.open(img_byte_arr)
return result_img, current_size
def apply_conversion():
try:
index = int(format_var.get())
_, fmt, ext = formats[index]
save_options = {}
if fmt == "JPEG":
save_options["quality"] = quality_var.get()
save_options["optimize"] = True
elif fmt == "PNG":
if transparency_var.get() and self.current_image.mode in ('RGBA', 'LA'):
save_options["format"] = "PNG"
else:
self.current_image = self.current_image.convert('RGB')
elif fmt == "WEBP":
save_options["quality"] = webp_quality_var.get()
save_options["method"] = 6
processed_image = self.current_image.copy()
compressed_size = None
if compress_enable_var.get():
target_size = target_size_var.get()
if size_unit_var.get() == "MB":
target_size *= 1024
processed_image, compressed_size = compress_to_target_size(
processed_image, fmt, target_size, save_options
)
if compressed_size > target_size * 1.05:
messagebox.showwarning(
"压缩警告",
f"无法将图像压缩到指定大小: {target_size:.1f} KB。\n"
f"实际大小: {compressed_size:.1f} KB。\n"
f"这可能是因为图像内容太复杂或格式限制。"
)
temp_path = f"temp_converted{ext}"
if compressed_size is None:
processed_image.save(temp_path, format=fmt, **save_options)
else:
processed_image.save(temp_path, format=fmt)
actual_size = os.path.getsize(temp_path) / 1024
converted_image = Image.open(temp_path)
self.current_image = converted_image.copy()
self.current_image.format = fmt
try:
os.remove(temp_path)
except:
pass
status_text = f"图片格式已转换为 {fmt}"
if compressed_size:
unit = "KB" if actual_size < 1024 else "MB"
disp_size = actual_size if unit == "KB" else actual_size/1024
status_text += f",大小为 {disp_size:.1f} {unit}"
self.display_image_on_canvas()
self.status_bar.config(text=status_text)
convert_window.destroy()
if messagebox.askyesno("保存提示", f"图片已转换为{fmt}格式。是否现在保存?"):
self.save_image()
except Exception as e:
messagebox.showerror("错误", f"转换格式时出错: {str(e)}")
import traceback
traceback.print_exc()
ttk.Button(button_frame, text="转换", command=apply_conversion).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=convert_window.destroy).pack(side=tk.LEFT, padx=10)
def create_menu(self):
"""创建菜单"""
menu_bar = tk.Menu(self.root)
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label="打开", command=self.open_image, accelerator="Ctrl+O")
file_menu.add_command(label="保存", command=self.save_image, accelerator="Ctrl+S")
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
menu_bar.add_cascade(label="文件", menu=file_menu)
edit_menu = tk.Menu(menu_bar, tearoff=0)
edit_menu.add_command(label="撤销", command=self.undo, accelerator="Ctrl+Z")
edit_menu.add_command(label="重做", command=self.redo, accelerator="Ctrl+Y")
edit_menu.add_separator()
edit_menu.add_command(label="裁剪", command=self.crop_image)
edit_menu.add_command(label="调整大小", command=self.resize_image)
edit_menu.add_command(label="旋转", command=self.rotate_image)
edit_menu.add_command(label="翻转", command=self.flip_image)
menu_bar.add_cascade(label="编辑", menu=edit_menu)
effect_menu = tk.Menu(menu_bar, tearoff=0)
effect_menu.add_command(label="调整亮度/对比度/饱和度", command=self.adjust_image)
effect_menu.add_command(label="应用滤镜", command=self.apply_image_filter)
menu_bar.add_cascade(label="效果", menu=effect_menu)
tools_menu = tk.Menu(menu_bar, tearoff=0)
tools_menu.add_command(label="模板裁切", command=self.template_crop)
tools_menu.add_command(label="格式转换", command=self.convert_format)
tools_menu.add_command(label="添加水印", command=self.add_watermark)
tools_menu.add_command(label="添加防伪水印", command=self.add_tiled_watermark)
tools_menu.add_separator()
tools_menu.add_command(label="批量调整大小", command=self.batch_processor.show_batch_resize_dialog)
tools_menu.add_command(label="批量格式转换", command=self.batch_processor.show_batch_convert_dialog)
tools_menu.add_command(label="批量添加水印", command=self.batch_processor.show_batch_watermark_dialog)
menu_bar.add_cascade(label="工具", menu=tools_menu)
help_menu = tk.Menu(menu_bar, tearoff=0)
help_menu.add_command(label="关于", command=self.show_about)
help_menu.add_command(label="使用帮助", command=self.show_help)
menu_bar.add_cascade(label="帮助", menu=help_menu)
self.root.config(menu=menu_bar)
def get_system_fonts(self, info_label=None):
"""获取系统中所有可用的字体,以中文名称优先显示"""
available_fonts = []
font_paths = {}
font_name_mapping = {
"msyh": "微软雅黑",
"msyhbd": "微软雅黑 粗体",
"msyhl": "微软雅黑 细体",
"simhei": "黑体",
"simsun": "宋体",
"simkai": "楷体",
"simfang": "仿宋",
"simyou": "幼圆",
"stkaiti": "华文楷体",
"stfangsong": "华文仿宋",
"stsong": "华文宋体",
"stzhongsong": "华文中宋",
"sthupo": "华文琥珀",
"stxinwei": "华文新魏",
"stliti": "华文隶书",
"dengxian": "等线",
"dengxianlight": "等线 细体",
"dengxianmedium": "等线 中等",
"dengxianbold": "等线 粗体",
"yahei": "微软雅黑",
"microsoft yahei": "微软雅黑",
"microsoftyahei": "微软雅黑",
"arial": "Arial",
"pingfang": "苹方",
"pingfangsc": "苹方",
".pingfang": "苹方",
"pingfang-sc": "苹方",
"pingfanghk": "苹方港版",
"pingfangtc": "苹方繁体",
"songti": "宋体",
"songti sc": "宋体",
"songtitc": "宋体繁体",
"heiti": "黑体",
"heiti sc": "黑体",
"heitisc": "黑体",
"heititc": "黑体繁体",
"kaiti": "楷体",
"kaiti sc": "楷体",
"kaitisc": "楷体",
"kaititc": "楷体繁体",
"yuanti": "圆体",
"yuanti sc": "圆体",
"yuantisc": "圆体",
"yuantitc": "圆体繁体",
"liukai": "柳体",
"weibei": "魏碑",
"weibeisc": "魏碑",
"weibeitc": "魏碑繁体",
"xingkai": "行楷",
"xingkaisc": "行楷",
"baoli": "报隶",
"baolisc": "报隶",
"baolitc": "报隶繁体",
"wqy-microhei": "文泉驿微米黑",
"wqy-zenhei": "文泉驿正黑",
"wqymicrohei": "文泉驿微米黑",
"wqyzenhei": "文泉驿正黑",
"notosanscjk": "思源黑体",
"notosanscjksc": "思源黑体",
"notosanscjktc": "思源黑体繁体",
"notosanscjkhk": "思源黑体港版",
"notosansc": "思源黑体",
"notoserif": "思源宋体",
"notoserifcjk": "思源宋体",
"notoserifcjksc": "思源宋体",
"notoserifcjktc": "思源宋体繁体",
"notoserifcjkhk": "思源宋体港版",
"regular": "常规",
"light": "细体",
"medium": "中等",
"bold": "粗体",
"heavy": "特粗",
"thin": "极细",
"-regular": "常规",
"-light": "细体",
"-medium": "中等",
"-bold": "粗体",
"-heavy": "特粗",
"-thin": "极细"
}
def test_font(font_path, size=12):
try:
from PIL import Image, ImageDraw, ImageFont
font = None
try:
font = ImageFont.truetype(font_path, size)
except:
try:
font = ImageFont.truetype(font_path, size, index=0)
except:
return False
if not font:
return False
img = Image.new('RGB', (30, 30), color=(255, 255, 255))
d = ImageDraw.Draw(img)
d.text((5, 5), "测试", font=font, fill=(0, 0, 0))
return True
except Exception as e:
if info_label:
info_label.config(text=f"字体测试失败: {os.path.basename(font_path)}")
info_label.update()
return False
if info_label:
info_label.config(text="正在搜索系统字体...")
info_label.update()
font_dirs = []
if os.name == 'nt':
if 'WINDIR' in os.environ:
font_dirs.append(os.path.join(os.environ['WINDIR'], 'Fonts'))
if 'LOCALAPPDATA' in os.environ:
font_dirs.append(os.path.join(os.environ['LOCALAPPDATA'], 'Microsoft', 'Windows', 'Fonts'))
elif sys.platform == 'darwin':
font_dirs.extend([
'/Library/Fonts',
'/System/Library/Fonts',
os.path.expanduser('~/Library/Fonts')
])
else:
font_dirs.extend([
'/usr/share/fonts',
'/usr/local/share/fonts',
os.path.expanduser('~/.fonts'),
os.path.expanduser('~/.local/share/fonts')
])
total_fonts = 0
processed_fonts = 0
for font_dir in font_dirs:
if os.path.exists(font_dir):
for root, _, files in os.walk(font_dir):
total_fonts += len([f for f in files if f.lower().endswith(('.ttf', '.ttc', '.otf'))])
for font_dir in font_dirs:
if not os.path.exists(font_dir):
if info_label:
info_label.config(text=f"目录不存在: {font_dir}")
info_label.update()
continue
if info_label:
info_label.config(text=f"搜索字体目录: {os.path.basename(font_dir)}")
info_label.update()
for root, _, files in os.walk(font_dir):
for filename in files:
if filename.lower().endswith(('.ttf', '.ttc', '.otf')):
processed_fonts += 1
if info_label:
info_label.config(text=f"正在加载字体 ({processed_fonts}/{total_fonts}): {filename}")
info_label.update()
font_path = os.path.join(root, filename)
name_without_ext = os.path.splitext(filename)[0].lower()
display_name = None
for key, value in font_name_mapping.items():
if key.lower() in name_without_ext:
display_name = value
break
if not display_name:
display_name = name_without_ext
if test_font(font_path):
available_fonts.append(display_name)
font_paths[display_name] = font_path
if info_label:
info_label.config(text=f"已加载字体 ({processed_fonts}/{total_fonts}): {display_name}")
info_label.update()
if len(available_fonts) == 0 and os.name == 'nt' and 'WINDIR' in os.environ:
if info_label:
info_label.config(text="未找到可用字体,添加常见Windows中文字体...")
info_label.update()
font_dir = os.path.join(os.environ['WINDIR'], 'Fonts')
common_fonts = [
("微软雅黑", "msyh.ttc"),
("宋体", "simsun.ttc"),
("黑体", "simhei.ttf"),
("楷体", "simkai.ttf"),
("仿宋", "simfang.ttf")
]
for display_name, filename in common_fonts:
font_path = os.path.join(font_dir, filename)
if os.path.exists(font_path) and test_font(font_path):
available_fonts.append(display_name)
font_paths[display_name] = font_path
if info_label:
info_label.config(text=f"已加载字体: {display_name}")
info_label.update()
if info_label:
info_label.config(text=f"字体加载完成,共加载 {len(available_fonts)} 个字体")
info_label.update()
return available_fonts, font_paths
def apply_image_filter(self):
"""应用图像滤镜效果"""
if not self.current_image:
messagebox.showwarning("警告", "请先打开一张图片!")
return
filter_window = tk.Toplevel(self.root)
filter_window.title("应用滤镜")
filter_window.geometry("600x400")
filter_window.resizable(False, False)
filter_window.transient(self.root)
filter_window.grab_set()
self.center_window(filter_window, 600, 400)
filters = [
("模糊", "BLUR"),
("轮廓", "CONTOUR"),
("锐化", "SHARPEN"),
("边缘增强", "EDGE_ENHANCE"),
("浮雕", "EMBOSS"),
("平滑", "SMOOTH"),
("灰度", "GRAYSCALE"),
("棕褐色", "SEPIA")
]
main_frame = ttk.Frame(filter_window)
main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
filter_frame = ttk.LabelFrame(main_frame, text="选择滤镜")
filter_frame.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.Y)
selected_filter = tk.StringVar()
preview_image = self.current_image.copy()
canvas_width, canvas_height = 320, 280
preview_image.thumbnail((canvas_width, canvas_height))
preview_photo = ImageTk.PhotoImage(preview_image)
preview_frame = ttk.LabelFrame(main_frame, text="预览")
preview_frame.pack(side=tk.RIGHT, padx=5, pady=5, fill=tk.BOTH, expand=True)
preview_canvas = tk.Canvas(preview_frame, width=canvas_width, height=canvas_height, bg="white")
preview_canvas.pack(padx=5, pady=5)
preview_image_id = preview_canvas.create_image(
canvas_width // 2, canvas_height // 2,
image=preview_photo
)
def preview_filter():
filter_name = selected_filter.get()
if filter_name:
filtered_image = apply_filter(preview_image.copy(), filter_name)
nonlocal preview_photo
preview_photo = ImageTk.PhotoImage(filtered_image)
preview_canvas.itemconfig(preview_image_id, image=preview_photo)
def apply_selected_filter():
filter_name = selected_filter.get()
if filter_name:
self.save_current_state()
filtered_image = apply_filter(self.current_image, filter_name)
self.current_image = filtered_image
self.display_image_on_canvas()
filter_display_name = None
for display_name, code in filters:
if code == filter_name:
filter_display_name = display_name
break
filter_window.destroy()
self.show_status(f"已应用{filter_display_name}滤镜")
for filter_text, filter_name in filters:
rb = ttk.Radiobutton(
filter_frame,
text=filter_text,
value=filter_name,
variable=selected_filter,
command=preview_filter
)
rb.pack(anchor=tk.W, padx=10, pady=5)
selected_filter.set("BLUR")
preview_filter()
button_frame = ttk.Frame(filter_window)
button_frame.pack(pady=10)
apply_button = ttk.Button(button_frame, text="应用", command=apply_selected_filter)
apply_button.pack(side=tk.LEFT, padx=5)
cancel_button = ttk.Button(button_frame, text="取消", command=filter_window.destroy)
cancel_button.pack(side=tk.LEFT, padx=5)
def save_current_state(self):
"""保存当前图像状态用于撤销/重做"""
if self.current_image is None:
return
if self.history_index < len(self.history) - 1:
self.history = self.history[:self.history_index + 1]
self.history.append(self.current_image.copy())
self.history_index = len(self.history) - 1
if len(self.history) > self.max_history:
self.history.pop(0)
self.history_index -= 1
def undo(self, event=None):
"""撤销上一步操作"""
if self.history_index > 0:
self.history_index -= 1
self.current_image = self.history[self.history_index].copy()
self.display_image_on_canvas()
self.show_status("已撤销")
else:
self.show_status("无法撤销")
def redo(self, event=None):
"""重做操作"""
if self.history_index < len(self.history) - 1:
self.history_index += 1
self.current_image = self.history[self.history_index].copy()
self.display_image_on_canvas()
self.show_status("已重做")
else:
self.show_status("无法重做")
def show_status(self, message):
"""在状态栏显示消息"""
print(message)
def show_about(self):
"""显示关于对话框"""
about_window = tk.Toplevel(self.root)
about_window.title("关于")
about_window.geometry("500x400")
about_window.resizable(False, False)
about_window.transient(self.root)
about_window.grab_set()
self.center_window(about_window, 500, 400)
ttk.Label(about_window, text="图片处理小工具", font=("Arial", 18, "bold")).pack(pady=15)
ttk.Label(about_window, text="版本: 2.0", font=("Arial", 10)).pack(pady=5)
frame = ttk.Frame(about_window)
frame.pack(fill="both", expand=True, padx=20, pady=10)
scrollbar = ttk.Scrollbar(frame)
scrollbar.pack(side="right", fill="y")
description_text = tk.Text(frame, wrap="word", height=10, width=50,
yscrollcommand=scrollbar.set, borderwidth=0,
font=("Arial", 9))
description_text.pack(side="left", fill="both", expand=True)
scrollbar.config(command=description_text.yview)
description = """图片处理小工具是一款功能全面的图像编辑软件,专为日常图片处理需求设计。
主要功能:
• 基础图像编辑: 裁剪、调整大小、旋转和翻转
• 图像增强: 调整亮度、对比度、饱和度和色温
• 多种滤镜效果: 模糊、锐化、灰度、复古等
• 水印功能: 支持文字和图片水印,可调整位置、透明度等
• 批量处理: 批量调整大小、格式转换和添加水印
• 特色功能: 模板裁切、平铺式防伪水印
• 格式支持: JPG、PNG、WEBP、GIF、BMP等多种格式
最新更新:
• 改进的批量水印功能,支持自动预览
• 优化的用户界面,操作更加直观
• 增强的图像处理算法,提高处理质量和速度
• 新增平铺式防伪水印功能
• 性能优化,减少资源占用
您可以通过"帮助"菜单查看完整的使用指南。"""
description_text.insert("1.0", description)
description_text.config(state="disabled")
tech_frame = ttk.Frame(about_window)
tech_frame.pack(fill="x", padx=20, pady=5)
ttk.Label(tech_frame, text="技术支持: ", font=("Arial", 9, "bold")).pack(side="left")
ttk.Label(tech_frame, text="Python + Tkinter + Pillow", font=("Arial", 9)).pack(side="left")
ttk.Label(about_window, text="Copyright © 2023-2025 nobiyou",
font=("Arial", 9)).pack(pady=5)
ttk.Button(about_window, text="确定", command=about_window.destroy, width=15).pack(pady=15)
def show_help(self):
"""显示使用帮助对话框"""
help_window = tk.Toplevel(self.root)
help_window.title("使用帮助")
help_window.geometry("600x480")
help_window.transient(self.root)
help_window.grab_set()
self.center_window(help_window, 600, 480)
notebook = ttk.Notebook(help_window)
notebook.pack(fill="both", expand=True, padx=10, pady=10)
basic_tab = ttk.Frame(notebook)
notebook.add(basic_tab, text="基础操作")
basic_text = """
基础操作:
- 打开图片: 点击"文件"菜单 -> "打开",或使用快捷键Ctrl+O
- 保存图片: 点击"文件"菜单 -> "保存",或使用快捷键Ctrl+S
- 退出程序: 点击"文件"菜单 -> "退出",或使用快捷键Alt+F4
图像浏览:
- 缩放图像: 使用鼠标滚轮放大或缩小图像
- 平移图像: 按住鼠标左键拖动图像
- 重置视图: 点击"视图"菜单 -> "重置视图"
- 适应窗口: 点击"视图"菜单 -> "适应窗口"
编辑功能:
- 裁剪: 点击"编辑"菜单 -> "裁剪",可通过拖动选择裁剪区域
- 调整大小: 点击"编辑"菜单 -> "调整大小",可设定具体尺寸或百分比
- 旋转: 点击"编辑"菜单 -> "旋转",支持90°旋转或自定义角度
- 翻转: 点击"编辑"菜单 -> "翻转",可水平或垂直翻转图像
撤销与重做:
- 撤销: 点击"编辑"菜单 -> "撤销",或使用快捷键Ctrl+Z
- 重做: 点击"编辑"菜单 -> "重做",或使用快捷键Ctrl+Y
- 每个操作都会被记录,最多支持10步的撤销/重做操作
"""
basic_scroll = ttk.Scrollbar(basic_tab, orient="vertical")
basic_scroll.pack(side="right", fill="y")
basic_text_widget = tk.Text(basic_tab, yscrollcommand=basic_scroll.set, wrap="word",
height=15, width=65, borderwidth=0, font=("", 9))
basic_text_widget.insert("1.0", basic_text)
basic_text_widget.config(state="disabled")
basic_text_widget.pack(side="left", fill="both", expand=True, padx=20, pady=20)
basic_scroll.config(command=basic_text_widget.yview)
effect_tab = ttk.Frame(notebook)
notebook.add(effect_tab, text="效果功能")
effect_text = """
图像调整:
- 亮度/对比度: 点击"效果"菜单 -> "调整亮度/对比度"
拖动滑块可实时预览效果,点击"应用"确认修改
- 饱和度/色温: 点击"效果"菜单 -> "调整饱和度/色温"
调整图像的色彩饱和度和冷暖色调
- 色阶调整: 点击"效果"菜单 -> "色阶调整"
调整图像的阴影、中间调和高光,提高图像质量
滤镜效果:
- 应用滤镜: 点击"效果"菜单 -> "应用滤镜"
提供多种滤镜效果,包括:
• 模糊(Blur): 使图像柔和,减少细节和噪点
• 锐化(Sharpen): 增强图像边缘,使图像更加清晰
• 灰度(Grayscale): 将彩色图像转换为黑白图像
• 复古(Sepia): 添加棕褐色色调,创造复古效果
• 浮雕(Emboss): 创造浮雕效果,突出图像轮廓
• 边缘检测(Edge Enhance): 突出图像边缘
- 自定义滤镜: 点击"效果"菜单 -> "自定义滤镜"
可组合多种滤镜效果,调整参数创建独特效果
颜色调整:
- 自动增强: 点击"效果"菜单 -> "自动增强"
自动优化图像的对比度和颜色平衡
- 颜色平衡: 点击"效果"菜单 -> "颜色平衡"
调整红、绿、蓝三个通道的平衡
"""
effect_scroll = ttk.Scrollbar(effect_tab, orient="vertical")
effect_scroll.pack(side="right", fill="y")
effect_text_widget = tk.Text(effect_tab, yscrollcommand=effect_scroll.set, wrap="word",
height=15, width=65, borderwidth=0, font=("", 9))
effect_text_widget.insert("1.0", effect_text)
effect_text_widget.config(state="disabled")
effect_text_widget.pack(side="left", fill="both", expand=True, padx=20, pady=20)
effect_scroll.config(command=effect_text_widget.yview)
tools_tab = ttk.Frame(notebook)
notebook.add(tools_tab, text="工具")
tools_text = """
常用工具:
- 模板裁切: 点击"工具"菜单 -> "模板裁切"
支持多种社交媒体和设备的常用尺寸:
• 微信公众号封面(900x383)
• 朋友圈正方形(1080x1080)
• 小红书(1000x1000)
• 微博(440x245)
• 可自定义尺寸
- 格式转换: 点击"工具"菜单 -> "格式转换"
支持多种图片格式之间的转换:
• JPG(JPEG): 压缩率高,适合照片
• PNG: 支持透明背景,适合图标和标志
• WEBP: 谷歌开发的格式,比JPG更小
• GIF: 支持动画效果
• BMP: 无损格式,文件较大
可设置压缩质量,优化图像大小和质量平衡
- 添加水印: 点击"工具"菜单 -> "添加水印"
支持两种水印模式:
• 单个水印: 在图像的指定位置添加一个水印
• 平铺水印: 在整张图像上重复添加水印,防伪效果强
支持文字水印和图片水印,可调整透明度、大小、旋转角度等
批量处理:
- 批量调整大小: 点击"工具"菜单 -> "批量调整大小"
可一次性处理多张图片,调整为统一尺寸或按比例缩放
支持保留原文件名或使用序号重命名
- 批量格式转换: 点击"工具"菜单 -> "批量格式转换"
可一次性转换多张图片的格式,支持调整质量和压缩率
可保持原始分辨率或在转换时调整尺寸
- 批量添加水印: 点击"工具"菜单 -> "批量添加水印"
可一次性为多张图片添加统一的水印
批量操作时会自动加载第一张图片作为预览,实时调整效果
支持单水印和平铺水印两种模式
高级功能:
- 图像修复: 点击"工具"菜单 -> "图像修复"
去除图像中的小瑕疵、划痕或不需要的物体
- 防伪水印: 点击"工具"菜单 -> "添加防伪水印"
创建难以去除的平铺式水印,可用于版权保护
"""
tools_scroll = ttk.Scrollbar(tools_tab, orient="vertical")
tools_scroll.pack(side="right", fill="y")
tools_text_widget = tk.Text(tools_tab, yscrollcommand=tools_scroll.set, wrap="word",
height=15, width=65, borderwidth=0, font=("", 9))
tools_text_widget.insert("1.0", tools_text)
tools_text_widget.config(state="disabled")
tools_text_widget.pack(side="left", fill="both", expand=True, padx=20, pady=20)
tools_scroll.config(command=tools_text_widget.yview)
shortcuts_tab = ttk.Frame(notebook)
notebook.add(shortcuts_tab, text="快捷键")
shortcuts_text = """
文件操作:
- Ctrl+O: 打开图片
- Ctrl+S: 保存当前图片
- Ctrl+Shift+S: 另存为
- Ctrl+P: 打印图片
- Alt+F4: 退出程序
编辑操作:
- Ctrl+Z: 撤销上一步操作
- Ctrl+Y: 重做上一步操作
- Ctrl+X: 剪切
- Ctrl+C: 复制
- Ctrl+V: 粘贴
- Delete: 删除选中内容
视图操作:
- Ctrl+数字键盘 +: 放大图像
- Ctrl+数字键盘 -: 缩小图像
- Ctrl+0: 重置视图到100%
- F11: 全屏显示
工具快捷键:
- Ctrl+R: 调整大小
- Ctrl+T: 裁剪图片
- Ctrl+Shift+R: 旋转图片
- Ctrl+W: 添加水印
- Ctrl+B: 批量处理
使用技巧:
- 按住空格键并拖动可快速平移图像
- 双击图像可快速恢复到100%显示
- 右键点击图像可显示上下文菜单
"""
shortcuts_scroll = ttk.Scrollbar(shortcuts_tab, orient="vertical")
shortcuts_scroll.pack(side="right", fill="y")
shortcuts_text_widget = tk.Text(shortcuts_tab, yscrollcommand=shortcuts_scroll.set, wrap="word",
height=15, width=65, borderwidth=0, font=("", 9))
shortcuts_text_widget.insert("1.0", shortcuts_text)
shortcuts_text_widget.config(state="disabled")
shortcuts_text_widget.pack(side="left", fill="both", expand=True, padx=20, pady=20)
shortcuts_scroll.config(command=shortcuts_text_widget.yview)
ttk.Button(help_window, text="关闭", command=help_window.destroy).pack(pady=10)
def add_tiled_watermark(self):
"""添加防伪水印(平铺式水印覆盖整张图片)"""
if not self.current_image:
messagebox.showerror("错误", "请先打开一张图片")
return
tiled_watermark_window = tk.Toplevel(self.root)
tiled_watermark_window.title("添加防伪水印")
tiled_watermark_window.geometry("800x600")
tiled_watermark_window.resizable(True, True)
tiled_watermark_window.transient(self.root)
tiled_watermark_window.grab_set()
self.center_window(tiled_watermark_window, 800, 600)
main_frame = ttk.Frame(tiled_watermark_window, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
content_frame = ttk.Frame(main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
left_frame = ttk.Frame(content_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
right_frame = ttk.Frame(content_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
type_frame = ttk.LabelFrame(left_frame, text="水印类型")
type_frame.pack(fill=tk.X, padx=5, pady=5)
watermark_type = tk.StringVar(value="text")
ttk.Radiobutton(type_frame, text="文字水印", variable=watermark_type,
value="text").grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)
ttk.Radiobutton(type_frame, text="图片水印", variable=watermark_type,
value="image").grid(row=0, column=1, padx=10, pady=5, sticky=tk.W)
text_frame = ttk.LabelFrame(left_frame, text="文字水印设置")
text_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(text_frame, text="水印文字:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
watermark_text = tk.StringVar(value="防伪水印")
ttk.Entry(text_frame, textvariable=watermark_text, width=25).grid(
row=0, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Label(text_frame, text="字体:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
font_var = tk.StringVar()
fonts, font_paths = self.get_system_fonts()
self.font_paths = font_paths
font_combo = ttk.Combobox(text_frame, textvariable=font_var, values=fonts, width=20)
font_combo.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
if fonts:
font_combo.current(0)
def on_font_selected(event):
tiled_watermark_window.after(10, update_preview)
font_combo.bind("<<ComboboxSelected>>", on_font_selected)
ttk.Label(text_frame, text="字体颜色:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
color_var = tk.StringVar(value="#000000")
color_frame = ttk.Frame(text_frame)
color_frame.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W)
color_preview = tk.Label(color_frame, bg=color_var.get(), width=3, height=1, relief="ridge")
color_preview.pack(side=tk.LEFT, padx=2)
def choose_color():
color = colorchooser.askcolor(color_var.get())[1]
if color:
color_var.set(color)
color_preview.config(bg=color)
tiled_watermark_window.after(10, update_preview)
ttk.Button(color_frame, text="选择颜色", command=choose_color).pack(side=tk.LEFT, padx=5)
style_frame = ttk.Frame(text_frame)
style_frame.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky=tk.W)
bold_var = tk.BooleanVar(value=False)
italic_var = tk.BooleanVar(value=False)
def update_style():
tiled_watermark_window.after(10, update_preview)
ttk.Checkbutton(style_frame, text="粗体", variable=bold_var, command=update_style).pack(side=tk.LEFT, padx=5)
ttk.Checkbutton(style_frame, text="斜体", variable=italic_var, command=update_style).pack(side=tk.LEFT, padx=5)
image_frame = ttk.LabelFrame(left_frame, text="图片水印设置")
ttk.Label(image_frame, text="水印图片:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
watermark_image_path = tk.StringVar()
ttk.Entry(image_frame, textvariable=watermark_image_path, width=25).grid(
row=0, column=1, padx=5, pady=5, sticky=tk.W)
watermark_img_obj = [None]
def select_watermark_image():
file_path = filedialog.askopenfilename(
filetypes=[
("图片文件", "*.png *.jpg *.jpeg *.gif *.bmp"),
("PNG文件", "*.png"),
("所有文件", "*.*")
]
)
if file_path:
try:
from PIL import Image
img = Image.open(file_path)
watermark_img_obj[0] = img
watermark_image_path.set(file_path)
update_preview()
except Exception as e:
messagebox.showerror("错误", f"无法打开图片: {str(e)}")
ttk.Button(image_frame, text="浏览...", command=select_watermark_image).grid(
row=0, column=2, padx=5, pady=5)
tiled_frame = ttk.LabelFrame(left_frame, text="平铺水印设置")
tiled_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(tiled_frame, text="不透明度:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
opacity_var = tk.DoubleVar(value=0.2)
opacity_scale = ttk.Scale(tiled_frame, from_=0.05, to=1, variable=opacity_var, length=200)
opacity_scale.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW)
opacity_scale.bind("<B1-Motion>", lambda e: update_preview())
opacity_scale.bind("<ButtonRelease-1>", lambda e: update_preview())
opacity_label = ttk.Label(tiled_frame, text=f"{opacity_var.get():.2f}")
opacity_label.grid(row=0, column=2, padx=5, pady=5)
def update_opacity_label(*args):
opacity_label.config(text=f"{opacity_var.get():.2f}")
tiled_watermark_window.after(10, update_preview)
opacity_var.trace_add("write", update_opacity_label)
ttk.Label(tiled_frame, text="旋转角度:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
rotation_var = tk.IntVar(value=30)
rotation_scale = ttk.Scale(tiled_frame, from_=0, to=90, variable=rotation_var, length=200)
rotation_scale.grid(row=1, column=1, padx=5, pady=5, sticky=tk.EW)
rotation_scale.bind("<B1-Motion>", lambda e: update_preview())
rotation_scale.bind("<ButtonRelease-1>", lambda e: update_preview())
rotation_label = ttk.Label(tiled_frame, text=f"{rotation_var.get()}°")
rotation_label.grid(row=1, column=2, padx=5, pady=5)
def update_rotation_label(*args):
rotation_label.config(text=f"{rotation_var.get()}°")
tiled_watermark_window.after(10, update_preview)
rotation_var.trace_add("write", update_rotation_label)
ttk.Label(tiled_frame, text="水印大小:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
tile_scale_var = tk.DoubleVar(value=0.15)
tile_scale_scale = ttk.Scale(tiled_frame, from_=0.05, to=1, variable=tile_scale_var, length=200)
tile_scale_scale.grid(row=2, column=1, padx=5, pady=5, sticky=tk.EW)
tile_scale_scale.bind("<B1-Motion>", lambda e: update_preview())
tile_scale_scale.bind("<ButtonRelease-1>", lambda e: update_preview())
tile_scale_label = ttk.Label(tiled_frame, text=f"{int(tile_scale_var.get()*100)}%")
tile_scale_label.grid(row=2, column=2, padx=5, pady=5)
def update_tile_scale_label(*args):
tile_scale_label.config(text=f"{int(tile_scale_var.get()*100)}%")
tiled_watermark_window.after(10, update_preview)
tile_scale_var.trace_add("write", update_tile_scale_label)
ttk.Label(tiled_frame, text="间距系数:").grid(row=3, column=0, padx=5, pady=5, sticky=tk.W)
spacing_var = tk.DoubleVar(value=1.5)
spacing_scale = ttk.Scale(tiled_frame, from_=0.1, to=3.0, variable=spacing_var, length=200)
spacing_scale.grid(row=3, column=1, padx=5, pady=5, sticky=tk.EW)
spacing_scale.bind("<B1-Motion>", lambda e: update_preview())
spacing_scale.bind("<ButtonRelease-1>", lambda e: update_preview())
spacing_label = ttk.Label(tiled_frame, text=f"{spacing_var.get():.1f}x")
spacing_label.grid(row=3, column=2, padx=5, pady=5)
def update_spacing_label(*args):
spacing_label.config(text=f"{spacing_var.get():.1f}x")
tiled_watermark_window.after(10, update_preview)
spacing_var.trace_add("write", update_spacing_label)
preview_frame = ttk.LabelFrame(right_frame, text="预览")
preview_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
preview_control_frame = ttk.Frame(preview_frame)
preview_control_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(preview_control_frame, text="缩放:").pack(side=tk.LEFT, padx=2)
zoom_var = tk.DoubleVar(value=1.0)
zoom_scale = ttk.Scale(preview_control_frame, from_=0.5, to=3.0, variable=zoom_var, length=150)
zoom_scale.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
zoom_scale.bind("<B1-Motion>", lambda e: tiled_watermark_window.after(10, update_preview))
zoom_scale.bind("<ButtonRelease-1>", lambda e: update_preview())
zoom_label = ttk.Label(preview_control_frame, text="100%", width=5)
zoom_label.pack(side=tk.LEFT, padx=2)
def update_zoom_label(*args):
zoom_label.config(text=f"{int(zoom_var.get() * 100)}%")
zoom_var.trace_add("write", update_zoom_label)
grid_var = tk.BooleanVar(value=False)
ttk.Checkbutton(preview_control_frame, text="显示网格", variable=grid_var,
command=lambda: update_preview()).pack(side=tk.LEFT, padx=10)
reset_zoom_btn = ttk.Button(preview_control_frame, text="重置缩放",
command=lambda: (zoom_var.set(1.0), update_preview()))
reset_zoom_btn.pack(side=tk.RIGHT, padx=5)
preview_canvas = tk.Canvas(preview_frame, bg="white")
preview_canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
drag_data = {"start_x": 0, "start_y": 0, "offset_x": 0, "offset_y": 0, "dragging": False}
def on_drag_start(event):
if zoom_var.get() > 1.0:
drag_data["start_x"] = event.x
drag_data["start_y"] = event.y
drag_data["dragging"] = True
preview_canvas.config(cursor="fleur")
def on_drag_motion(event):
if zoom_var.get() > 1.0:
dx = event.x - drag_data["start_x"]
dy = event.y - drag_data["start_y"]
drag_data["start_x"] = event.x
drag_data["start_y"] = event.y
drag_data["offset_x"] += dx
drag_data["offset_y"] += dy
update_preview(keep_offset=True)
def on_drag_end(event):
drag_data["dragging"] = False
preview_canvas.config(cursor="")
preview_canvas.bind("<ButtonPress-1>", on_drag_start)
preview_canvas.bind("<B1-Motion>", on_drag_motion)
preview_canvas.bind("<ButtonRelease-1>", on_drag_end)
status_var = tk.StringVar(value="就绪")
preview_status = ttk.Label(preview_frame, textvariable=status_var, anchor=tk.W)
preview_status.pack(fill=tk.X, padx=5, pady=2)
preview_img_ref = [None]
def update_preview(keep_offset=False):
if not self.current_image:
status_var.set("请先打开一张图片")
return
img_copy = self.current_image.copy()
try:
canvas_width = preview_canvas.winfo_width()
canvas_height = preview_canvas.winfo_height()
if canvas_width <= 1 or canvas_height <= 1:
preview_canvas.after(100, update_preview)
return
img_width, img_height = img_copy.size
base_scale = min(canvas_width / img_width, canvas_height / img_height) * 0.9
scale = base_scale * zoom_var.get()
new_width = int(img_width * scale)
new_height = int(img_height * scale)
if zoom_var.get() <= 1.0 and not keep_offset:
drag_data["offset_x"] = 0
drag_data["offset_y"] = 0
img_thumb = img_copy.resize((new_width, new_height), Image.LANCZOS)
if watermark_type.get() == "text":
text = watermark_text.get()
font = font_var.get()
print(f"\n预览更新 - 水印文本: '{text}', 选择字体: '{font}'")
font_path = None
if hasattr(self, 'font_paths'):
print(f"预览更新 - 字体路径字典包含 {len(self.font_paths)} 个条目")
if font in self.font_paths:
font_path = self.font_paths[font]
print(f"预览更新 - 找到字体路径: {font_path}")
else:
print(f"预览更新 - 字体 '{font}' 不在字体路径字典中")
if self.font_paths:
fonts_list = list(self.font_paths.keys())
print(f"预览更新 - 可用字体(前5个): {fonts_list[:min(5, len(fonts_list))]}")
lower_font = font.lower()
for f in fonts_list:
if f.lower() == lower_font:
font_path = self.font_paths[f]
print(f"预览更新 - 找到不区分大小写的匹配: {f} -> {font_path}")
break
else:
print("预览更新 - font_paths 属性不存在")
try:
from PIL import ImageColor
color = ImageColor.getcolor(color_var.get(), "RGB")
print(f"预览更新 - 颜色值: {color}")
except Exception as e:
print(f"预览更新 - 颜色解析错误: {e}")
color = (0, 0, 0)
opacity = opacity_var.get()
tile_scale = tile_scale_var.get()
rotation = rotation_var.get()
spacing = spacing_var.get()
is_bold = bold_var.get()
is_italic = italic_var.get()
print(f"预览更新 - 参数: 不透明度={opacity}, 缩放={tile_scale}, 旋转={rotation}, 间距={spacing}")
print(f"预览更新 - 文本样式: 粗体={is_bold}, 斜体={is_italic}")
from image_utils import add_tiled_watermark
try:
final_font = font_path if font_path else font
print(f"预览更新 - 最终使用的字体: {final_font}")
base_font_size = 24
scaled_font_size = int(base_font_size * (tile_scale / 0.15))
scaled_font_size = max(8, scaled_font_size)
print(f"预览更新 - 水印缩放比例: {tile_scale}, 计算的字体大小: {scaled_font_size}")
preview_img = add_tiled_watermark(
img_thumb,
watermark_text=text,
opacity=opacity,
tile_scale=tile_scale,
rotation=rotation,
spacing=spacing,
font_name=final_font,
font_size=scaled_font_size,
font_color=color,
bold=is_bold,
italic=is_italic,
letter_spacing=0
)
status_var.set(f"文字防伪水印预览: {text}")
except Exception as e:
print(f"预览更新 - 水印应用错误: {e}")
import traceback
traceback.print_exc()
preview_img = img_thumb
status_var.set(f"水印应用错误: {str(e)}")
else:
if watermark_img_obj[0] is None:
status_var.set("请选择水印图片")
preview_img = img_thumb
else:
from image_utils import add_tiled_watermark
try:
preview_img = add_tiled_watermark(
img_thumb,
watermark_image=watermark_img_obj[0],
opacity=opacity_var.get(),
tile_scale=tile_scale_var.get(),
rotation=rotation_var.get(),
spacing=spacing_var.get()
)
img_name = os.path.basename(watermark_image_path.get()) if watermark_image_path.get() else "未命名"
status_var.set(f"图片防伪水印预览: {img_name}")
except Exception as e:
print(f"图片水印应用错误: {e}")
preview_img = img_thumb
status_var.set(f"水印应用错误: {str(e)}")
preview_img_tk = ImageTk.PhotoImage(preview_img)
preview_img_ref[0] = preview_img_tk
x_pos = max(0, (canvas_width - new_width) // 2) + drag_data["offset_x"]
y_pos = max(0, (canvas_height - new_height) // 2) + drag_data["offset_y"]
if x_pos > canvas_width - 50:
x_pos = canvas_width - 50
drag_data["offset_x"] = x_pos - max(0, (canvas_width - new_width) // 2)
if y_pos > canvas_height - 50:
y_pos = canvas_height - 50
drag_data["offset_y"] = y_pos - max(0, (canvas_height - new_height) // 2)
if x_pos + new_width < 50:
x_pos = 50 - new_width
drag_data["offset_x"] = x_pos - max(0, (canvas_width - new_width) // 2)
if y_pos + new_height < 50:
y_pos = 50 - new_height
drag_data["offset_y"] = y_pos - max(0, (canvas_height - new_height) // 2)
preview_canvas.delete("all")
if grid_var.get():
for y in range(0, canvas_height, 20):
preview_canvas.create_line(0, y, canvas_width, y, fill="#dddddd", dash=(2, 4))
for x in range(0, canvas_width, 20):
preview_canvas.create_line(x, 0, x, canvas_height, fill="#dddddd", dash=(2, 4))
center_x = canvas_width // 2
center_y = canvas_height // 2
preview_canvas.create_line(center_x, 0, center_x, canvas_height, fill="#999999", dash=(4, 4))
preview_canvas.create_line(0, center_y, canvas_width, center_y, fill="#999999", dash=(4, 4))
border_color = "#3399ff"
preview_canvas.create_rectangle(
x_pos - 1, y_pos - 1,
x_pos + new_width + 1, y_pos + new_height + 1,
outline=border_color, width=2
)
img_item = preview_canvas.create_image(x_pos, y_pos, image=preview_img_tk, anchor="nw")
preview_canvas.tag_raise(img_item)
try:
if zoom_var.get() > 1.0:
help_text = "鼠标拖动可平移图像"
if drag_data.get("dragging", False):
help_text = "正在平移..."
preview_canvas.create_text(
canvas_width - 10, canvas_height - 10,
text=help_text,
anchor="se", fill="#666666", font=("Arial", 8)
)
except (NameError, AttributeError):
pass
except Exception as e:
print(f"预览错误: {str(e)}")
import traceback
traceback.print_exc()
status_var.set(f"预览错误: {str(e)}")
def update_type_ui(*args):
if watermark_type.get() == "text":
text_frame.pack(fill=tk.X, padx=5, pady=5, after=type_frame)
if image_frame.winfo_manager():
image_frame.pack_forget()
else:
if text_frame.winfo_manager():
text_frame.pack_forget()
image_frame.pack(fill=tk.X, padx=5, pady=5, after=type_frame)
update_preview()
watermark_type.trace_add("write", update_type_ui)
def delayed_update(*args):
tiled_watermark_window.after(50, update_preview)
watermark_text.trace_add("write", delayed_update)
font_var.trace_add("write", delayed_update)
color_var.trace_add("write", delayed_update)
bold_var.trace_add("write", delayed_update)
italic_var.trace_add("write", delayed_update)
opacity_var.trace_add("write", update_preview)
rotation_var.trace_add("write", update_preview)
tile_scale_var.trace_add("write", update_preview)
spacing_var.trace_add("write", update_preview)
def reset_zoom_and_offset():
zoom_var.set(1.0)
drag_data["offset_x"] = 0
drag_data["offset_y"] = 0
update_preview()
reset_zoom_btn.configure(command=reset_zoom_and_offset)
button_frame = ttk.Frame(main_frame)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10)
separator = ttk.Separator(button_frame, orient="horizontal")
separator.pack(fill="x", pady=5)
def apply_tiled_watermark():
if not self.current_image:
messagebox.showerror("错误", "请先打开一张图片")
return
try:
img_copy = self.current_image.copy()
if watermark_type.get() == "text":
text = watermark_text.get()
font = font_var.get()
font_path = None
if hasattr(self, 'font_paths'):
print(f"应用水印 - 字体路径字典包含 {len(self.font_paths)} 个条目")
if font in self.font_paths:
font_path = self.font_paths[font]
print(f"应用水印 - 找到字体路径: {font_path}")
else:
print(f"应用水印 - 字体 '{font}' 不在字体路径字典中")
if self.font_paths:
fonts_list = list(self.font_paths.keys())
print(f"应用水印 - 可用字体(前5个): {fonts_list[:min(5, len(fonts_list))]}")
lower_font = font.lower()
for f in fonts_list:
if f.lower() == lower_font:
font_path = self.font_paths[f]
print(f"应用水印 - 找到不区分大小写的匹配: {f} -> {font_path}")
break
else:
print("应用水印 - font_paths 属性不存在")
try:
from PIL import ImageColor
color = ImageColor.getcolor(color_var.get(), "RGB")
print(f"应用水印 - 颜色值: {color}")
except Exception as e:
print(f"应用水印 - 颜色解析错误: {e}")
color = (0, 0, 0)
opacity = opacity_var.get()
tile_scale = tile_scale_var.get()
rotation = rotation_var.get()
spacing = spacing_var.get()
is_bold = bold_var.get()
is_italic = italic_var.get()
print(f"应用水印 - 参数: 不透明度={opacity}, 缩放={tile_scale}, 旋转={rotation}, 间距={spacing}")
print(f"应用水印 - 文本样式: 粗体={is_bold}, 斜体={is_italic}")
final_font = font_path if font_path else font
print(f"应用水印 - 最终使用的字体: {final_font}")
orig_width, orig_height = img_copy.size
print(f"应用水印 - 原图尺寸: {orig_width}x{orig_height}")
canvas_width = preview_canvas.winfo_width()
canvas_height = preview_canvas.winfo_height()
print(f"应用水印 - 预览画布尺寸: {canvas_width}x{canvas_height}")
base_scale = min(canvas_width / orig_width, canvas_height / orig_height) * 0.9
preview_scale = base_scale * zoom_var.get()
print(f"应用水印 - 预览基础缩放比例: {base_scale}, 用户缩放因子: {zoom_var.get()}")
preview_width = int(orig_width * preview_scale)
preview_height = int(orig_height * preview_scale)
print(f"应用水印 - 预览图大小: {preview_width}x{preview_height}")
scale_factor = orig_width / preview_width if preview_width > 0 else 1
print(f"应用水印 - 预览到原图的比例因子: {scale_factor}")
preview_font_size = 24
actual_font_size = round(preview_font_size * scale_factor)
print(f"应用水印 - 预览字体大小: {preview_font_size}, 实际应用字体大小: {actual_font_size}")
from image_utils import add_tiled_watermark
self.current_image = add_tiled_watermark(
img_copy,
watermark_text=text,
opacity=opacity,
tile_scale=tile_scale,
rotation=rotation,
spacing=spacing,
font_name=final_font,
font_size=actual_font_size,
font_color=color,
bold=is_bold,
italic=is_italic,
letter_spacing=0
)
self.display_image_on_canvas()
tiled_watermark_window.destroy()
self.status_bar.config(text=f"已添加防伪文字水印: {text}")
self.save_current_state()
else:
if watermark_img_obj[0] is None:
messagebox.showerror("错误", "请选择水印图片")
return
from image_utils import add_tiled_watermark
self.current_image = add_tiled_watermark(
img_copy,
watermark_image=watermark_img_obj[0],
opacity=opacity_var.get(),
tile_scale=tile_scale_var.get(),
rotation=rotation_var.get(),
spacing=spacing_var.get()
)
self.display_image_on_canvas()
tiled_watermark_window.destroy()
self.status_bar.config(text=f"已添加防伪图片水印: {os.path.basename(watermark_image_path.get())}")
self.save_current_state()
except Exception as e:
import traceback
traceback.print_exc()
messagebox.showerror("错误", f"添加水印失败: {str(e)}")
ttk.Button(button_frame, text="取消", command=tiled_watermark_window.destroy).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="应用水印", command=apply_tiled_watermark).pack(side=tk.RIGHT, padx=5)
update_type_ui()
tiled_watermark_window.after(200, update_preview)
def create_app_icon(self):
"""创建并设置应用图标"""
try:
from PIL import Image, ImageTk
icon_image = Image.open('icon.png')
photo_image = ImageTk.PhotoImage(icon_image)
self.root.iconphoto(True, photo_image)
self.icon_image = photo_image
except Exception as e:
print(f"创建图标时出错: {e}")
if __name__ == "__main__":
root = tk.Tk()
app = ImageProcessingApp(root)
root.bind("<Control-z>", app.undo)
root.bind("<Control-y>", app.redo)
root.bind("<Control-o>", lambda e: app.open_image())
root.bind("<Control-s>", lambda e: app.save_image())
def on_resize(event):
if event.widget == root:
app.display_image_on_canvas()
root.bind("<Configure>", on_resize)
root.mainloop()
希望这款工具能帮助到需要批量处理图片的朋友们!