448 lines
17 KiB
Python
448 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
JPG转PNG转换器 - GUI版本
|
||
使用tkinter创建图形界面,支持文件和文件夹选择
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import threading
|
||
from pathlib import Path
|
||
from tkinter import *
|
||
from tkinter import ttk, filedialog, messagebox
|
||
from tkinter.scrolledtext import ScrolledText
|
||
from PIL import Image
|
||
import datetime
|
||
|
||
class JPGtoPNGConverterGUI:
|
||
def __init__(self, root):
|
||
self.root = root
|
||
self.root.title("JPG转PNG转换器 v1.0")
|
||
self.root.geometry("800x600")
|
||
self.root.resizable(True, True)
|
||
|
||
# 设置窗口图标(如果需要)
|
||
try:
|
||
# 可以设置窗口图标
|
||
pass
|
||
except:
|
||
pass
|
||
|
||
# 变量
|
||
self.input_path = StringVar()
|
||
self.output_path = StringVar()
|
||
self.conversion_mode = StringVar(value="folder") # "file" 或 "folder"
|
||
self.delete_original = BooleanVar(value=False)
|
||
self.recursive = BooleanVar(value=True)
|
||
self.is_converting = False
|
||
|
||
# 统计变量
|
||
self.total_files = 0
|
||
self.converted_files = 0
|
||
self.failed_files = 0
|
||
|
||
self.create_widgets()
|
||
|
||
def create_widgets(self):
|
||
"""创建界面组件"""
|
||
# 主框架
|
||
main_frame = ttk.Frame(self.root, padding="10")
|
||
main_frame.grid(row=0, column=0, sticky=(W, E, N, S))
|
||
|
||
# 配置网格权重
|
||
self.root.columnconfigure(0, weight=1)
|
||
self.root.rowconfigure(0, weight=1)
|
||
main_frame.columnconfigure(1, weight=1)
|
||
|
||
# 标题
|
||
title_label = ttk.Label(main_frame, text="🎨 JPG转PNG转换器", font=("Arial", 16, "bold"))
|
||
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
|
||
|
||
# 转换模式选择
|
||
mode_frame = ttk.LabelFrame(main_frame, text="转换模式", padding="10")
|
||
mode_frame.grid(row=1, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
|
||
mode_frame.columnconfigure(0, weight=1)
|
||
|
||
ttk.Radiobutton(mode_frame, text="📁 转换文件夹(批量)",
|
||
variable=self.conversion_mode, value="folder",
|
||
command=self.on_mode_change).grid(row=0, column=0, sticky=W)
|
||
ttk.Radiobutton(mode_frame, text="📄 转换单个文件",
|
||
variable=self.conversion_mode, value="file",
|
||
command=self.on_mode_change).grid(row=0, column=1, sticky=W)
|
||
|
||
# 输入路径选择
|
||
input_frame = ttk.LabelFrame(main_frame, text="输入路径", padding="10")
|
||
input_frame.grid(row=2, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
|
||
input_frame.columnconfigure(1, weight=1)
|
||
|
||
ttk.Label(input_frame, text="输入:").grid(row=0, column=0, sticky=W, padx=(0, 5))
|
||
self.input_entry = ttk.Entry(input_frame, textvariable=self.input_path, width=50)
|
||
self.input_entry.grid(row=0, column=1, sticky=(W, E), padx=(0, 5))
|
||
self.input_browse_btn = ttk.Button(input_frame, text="浏览...", command=self.browse_input)
|
||
self.input_browse_btn.grid(row=0, column=2)
|
||
|
||
# 输出路径选择
|
||
output_frame = ttk.LabelFrame(main_frame, text="输出路径(可选)", padding="10")
|
||
output_frame.grid(row=3, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
|
||
output_frame.columnconfigure(1, weight=1)
|
||
|
||
ttk.Label(output_frame, text="输出:").grid(row=0, column=0, sticky=W, padx=(0, 5))
|
||
ttk.Entry(output_frame, textvariable=self.output_path, width=50).grid(row=0, column=1, sticky=(W, E), padx=(0, 5))
|
||
self.output_browse_btn = ttk.Button(output_frame, text="浏览...", command=self.browse_output)
|
||
self.output_browse_btn.grid(row=0, column=2)
|
||
|
||
ttk.Label(output_frame, text="💡 留空将在原位置生成PNG文件",
|
||
foreground="gray").grid(row=1, column=0, columnspan=3, sticky=W, pady=(5, 0))
|
||
|
||
# 选项设置
|
||
options_frame = ttk.LabelFrame(main_frame, text="转换选项", padding="10")
|
||
options_frame.grid(row=4, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
|
||
|
||
ttk.Checkbutton(options_frame, text="🗑️ 转换后删除原JPG文件",
|
||
variable=self.delete_original).grid(row=0, column=0, sticky=W)
|
||
|
||
self.recursive_check = ttk.Checkbutton(options_frame, text="📁 递归处理子文件夹",
|
||
variable=self.recursive)
|
||
self.recursive_check.grid(row=1, column=0, sticky=W)
|
||
|
||
# 控制按钮
|
||
button_frame = ttk.Frame(main_frame)
|
||
button_frame.grid(row=5, column=0, columnspan=3, pady=20)
|
||
|
||
self.start_btn = ttk.Button(button_frame, text="🚀 开始转换",
|
||
command=self.start_conversion, style="Accent.TButton")
|
||
self.start_btn.pack(side=LEFT, padx=(0, 10))
|
||
|
||
self.stop_btn = ttk.Button(button_frame, text="⏹️ 停止",
|
||
command=self.stop_conversion, state=DISABLED)
|
||
self.stop_btn.pack(side=LEFT, padx=(0, 10))
|
||
|
||
ttk.Button(button_frame, text="🗂️ 打开输出文件夹",
|
||
command=self.open_output_folder).pack(side=LEFT)
|
||
|
||
# 进度条
|
||
progress_frame = ttk.Frame(main_frame)
|
||
progress_frame.grid(row=6, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
|
||
progress_frame.columnconfigure(0, weight=1)
|
||
|
||
self.progress_var = DoubleVar()
|
||
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var,
|
||
maximum=100, mode='determinate')
|
||
self.progress_bar.grid(row=0, column=0, sticky=(W, E), padx=(0, 5))
|
||
|
||
self.progress_label = ttk.Label(progress_frame, text="准备就绪")
|
||
self.progress_label.grid(row=0, column=1)
|
||
|
||
# 状态信息显示
|
||
status_frame = ttk.LabelFrame(main_frame, text="转换日志", padding="5")
|
||
status_frame.grid(row=7, column=0, columnspan=3, sticky=(W, E, N, S), pady=(0, 10))
|
||
status_frame.columnconfigure(0, weight=1)
|
||
status_frame.rowconfigure(0, weight=1)
|
||
|
||
self.log_text = ScrolledText(status_frame, height=12, wrap=WORD)
|
||
self.log_text.grid(row=0, column=0, sticky=(W, E, N, S))
|
||
|
||
# 配置主框架的行权重
|
||
main_frame.rowconfigure(7, weight=1)
|
||
|
||
# 初始化界面状态
|
||
self.on_mode_change()
|
||
|
||
def on_mode_change(self):
|
||
"""转换模式改变时的处理"""
|
||
if self.conversion_mode.get() == "file":
|
||
self.recursive_check.configure(state=DISABLED)
|
||
self.input_browse_btn.configure(text="选择文件...")
|
||
self.output_browse_btn.configure(text="保存为...")
|
||
else:
|
||
self.recursive_check.configure(state=NORMAL)
|
||
self.input_browse_btn.configure(text="选择文件夹...")
|
||
self.output_browse_btn.configure(text="选择文件夹...")
|
||
|
||
def browse_input(self):
|
||
"""浏览输入路径"""
|
||
if self.conversion_mode.get() == "file":
|
||
# 选择文件
|
||
file_path = filedialog.askopenfilename(
|
||
title="选择JPG文件",
|
||
filetypes=[
|
||
("JPG文件", "*.jpg *.jpeg *.JPG *.JPEG"),
|
||
("所有文件", "*.*")
|
||
]
|
||
)
|
||
if file_path:
|
||
self.input_path.set(file_path)
|
||
else:
|
||
# 选择文件夹
|
||
folder_path = filedialog.askdirectory(title="选择包含JPG文件的文件夹")
|
||
if folder_path:
|
||
self.input_path.set(folder_path)
|
||
|
||
def browse_output(self):
|
||
"""浏览输出路径"""
|
||
if self.conversion_mode.get() == "file":
|
||
# 保存文件
|
||
file_path = filedialog.asksaveasfilename(
|
||
title="保存PNG文件为",
|
||
defaultextension=".png",
|
||
filetypes=[
|
||
("PNG文件", "*.png"),
|
||
("所有文件", "*.*")
|
||
]
|
||
)
|
||
if file_path:
|
||
self.output_path.set(file_path)
|
||
else:
|
||
# 选择输出文件夹
|
||
folder_path = filedialog.askdirectory(title="选择输出文件夹")
|
||
if folder_path:
|
||
self.output_path.set(folder_path)
|
||
|
||
def log_message(self, message):
|
||
"""添加日志信息"""
|
||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||
self.log_text.insert(END, f"[{timestamp}] {message}\n")
|
||
self.log_text.see(END)
|
||
self.root.update_idletasks()
|
||
|
||
def update_progress(self, current, total, status=""):
|
||
"""更新进度条"""
|
||
if total > 0:
|
||
progress = (current / total) * 100
|
||
self.progress_var.set(progress)
|
||
|
||
if status:
|
||
self.progress_label.configure(text=status)
|
||
else:
|
||
self.progress_label.configure(text=f"{current}/{total}")
|
||
|
||
self.root.update_idletasks()
|
||
|
||
def convert_single_file(self, input_file, output_file=None):
|
||
"""转换单个文件"""
|
||
try:
|
||
input_path = Path(input_file)
|
||
|
||
if not input_path.exists():
|
||
self.log_message(f"❌ 文件不存在: {input_file}")
|
||
return False
|
||
|
||
# 设置输出路径
|
||
if output_file:
|
||
output_path = Path(output_file)
|
||
else:
|
||
output_path = input_path.with_suffix('.png')
|
||
|
||
# 确保输出目录存在
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
self.log_message(f"🔄 转换: {input_path.name} -> {output_path.name}")
|
||
|
||
# 转换图片
|
||
with Image.open(input_path) as img:
|
||
if img.mode in ('RGBA', 'LA'):
|
||
img.save(output_path, 'PNG', optimize=True)
|
||
else:
|
||
if img.mode == 'P':
|
||
img = img.convert('RGBA')
|
||
elif img.mode in ('RGB', 'L'):
|
||
img = img.convert('RGBA')
|
||
img.save(output_path, 'PNG', optimize=True)
|
||
|
||
self.log_message(f"✅ 转换成功: {output_path}")
|
||
self.converted_files += 1
|
||
|
||
# 删除原文件(如果选择)
|
||
if self.delete_original.get():
|
||
try:
|
||
input_path.unlink()
|
||
self.log_message(f"🗑️ 已删除原文件: {input_path}")
|
||
except Exception as e:
|
||
self.log_message(f"⚠️ 删除原文件失败: {e}")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
self.log_message(f"❌ 转换失败 {input_file}: {e}")
|
||
self.failed_files += 1
|
||
return False
|
||
|
||
def find_jpg_files(self, folder_path, recursive=True):
|
||
"""查找JPG文件"""
|
||
folder = Path(folder_path)
|
||
jpg_files = []
|
||
|
||
patterns = ['*.jpg', '*.jpeg', '*.JPG', '*.JPEG']
|
||
|
||
if recursive:
|
||
for pattern in patterns:
|
||
jpg_files.extend(folder.rglob(pattern))
|
||
else:
|
||
for pattern in patterns:
|
||
jpg_files.extend(folder.glob(pattern))
|
||
|
||
return jpg_files
|
||
|
||
def conversion_worker(self):
|
||
"""转换工作线程"""
|
||
try:
|
||
self.total_files = 0
|
||
self.converted_files = 0
|
||
self.failed_files = 0
|
||
|
||
input_path = self.input_path.get().strip()
|
||
output_path = self.output_path.get().strip()
|
||
|
||
if not input_path:
|
||
self.log_message("❌ 请选择输入路径")
|
||
return
|
||
|
||
if self.conversion_mode.get() == "file":
|
||
# 单文件转换
|
||
self.total_files = 1
|
||
self.update_progress(0, 1, "转换中...")
|
||
|
||
output_file = output_path if output_path else None
|
||
self.convert_single_file(input_path, output_file)
|
||
|
||
self.update_progress(1, 1, "完成")
|
||
|
||
else:
|
||
# 批量转换文件夹
|
||
self.log_message(f"📁 扫描文件夹: {input_path}")
|
||
jpg_files = self.find_jpg_files(input_path, self.recursive.get())
|
||
|
||
if not jpg_files:
|
||
self.log_message("📷 未找到JPG文件")
|
||
return
|
||
|
||
self.total_files = len(jpg_files)
|
||
self.log_message(f"📷 找到 {self.total_files} 个JPG文件")
|
||
|
||
# 转换每个文件
|
||
for i, jpg_file in enumerate(jpg_files):
|
||
if not self.is_converting: # 检查是否被停止
|
||
break
|
||
|
||
# 计算输出路径
|
||
if output_path:
|
||
relative_path = jpg_file.relative_to(Path(input_path))
|
||
output_file = Path(output_path) / relative_path.with_suffix('.png')
|
||
else:
|
||
output_file = None
|
||
|
||
self.update_progress(i, self.total_files, f"转换中 {i+1}/{self.total_files}")
|
||
self.convert_single_file(jpg_file, output_file)
|
||
|
||
self.update_progress(self.total_files, self.total_files, "完成")
|
||
|
||
# 显示转换摘要
|
||
self.log_message("=" * 50)
|
||
self.log_message(f"🎯 转换摘要")
|
||
self.log_message(f"📊 总文件数: {self.total_files}")
|
||
self.log_message(f"✅ 成功转换: {self.converted_files}")
|
||
self.log_message(f"❌ 转换失败: {self.failed_files}")
|
||
self.log_message("=" * 50)
|
||
|
||
if self.converted_files > 0:
|
||
messagebox.showinfo("转换完成",
|
||
f"转换完成!\n"
|
||
f"成功: {self.converted_files} 个文件\n"
|
||
f"失败: {self.failed_files} 个文件")
|
||
|
||
except Exception as e:
|
||
self.log_message(f"❌ 转换过程出错: {e}")
|
||
messagebox.showerror("错误", f"转换过程出错: {e}")
|
||
|
||
finally:
|
||
self.is_converting = False
|
||
self.start_btn.configure(state=NORMAL)
|
||
self.stop_btn.configure(state=DISABLED)
|
||
|
||
def start_conversion(self):
|
||
"""开始转换"""
|
||
if self.is_converting:
|
||
return
|
||
|
||
# 验证输入
|
||
if not self.input_path.get().strip():
|
||
messagebox.showerror("错误", "请选择输入路径")
|
||
return
|
||
|
||
if not Path(self.input_path.get()).exists():
|
||
messagebox.showerror("错误", "输入路径不存在")
|
||
return
|
||
|
||
# 清空日志
|
||
self.log_text.delete(1.0, END)
|
||
|
||
# 开始转换
|
||
self.is_converting = True
|
||
self.start_btn.configure(state=DISABLED)
|
||
self.stop_btn.configure(state=NORMAL)
|
||
|
||
# 在新线程中执行转换
|
||
threading.Thread(target=self.conversion_worker, daemon=True).start()
|
||
|
||
def stop_conversion(self):
|
||
"""停止转换"""
|
||
self.is_converting = False
|
||
self.log_message("⏹️ 用户停止转换")
|
||
self.start_btn.configure(state=NORMAL)
|
||
self.stop_btn.configure(state=DISABLED)
|
||
|
||
def open_output_folder(self):
|
||
"""打开输出文件夹"""
|
||
output_path = self.output_path.get().strip()
|
||
|
||
if output_path and Path(output_path).exists():
|
||
os.startfile(output_path)
|
||
elif self.input_path.get().strip():
|
||
input_path = self.input_path.get().strip()
|
||
if Path(input_path).exists():
|
||
if Path(input_path).is_file():
|
||
os.startfile(Path(input_path).parent)
|
||
else:
|
||
os.startfile(input_path)
|
||
else:
|
||
messagebox.showwarning("警告", "没有有效的输出路径")
|
||
|
||
def main():
|
||
"""主函数"""
|
||
root = Tk()
|
||
|
||
# 设置样式
|
||
style = ttk.Style()
|
||
|
||
# 尝试使用现代主题
|
||
try:
|
||
style.theme_use('winnative') # Windows原生样式
|
||
except:
|
||
pass
|
||
|
||
# 创建应用
|
||
app = JPGtoPNGConverterGUI(root)
|
||
|
||
# 设置窗口关闭事件
|
||
def on_closing():
|
||
if app.is_converting:
|
||
if messagebox.askokcancel("退出", "正在转换中,确定要退出吗?"):
|
||
app.is_converting = False
|
||
root.destroy()
|
||
else:
|
||
root.destroy()
|
||
|
||
root.protocol("WM_DELETE_WINDOW", on_closing)
|
||
|
||
# 运行主循环
|
||
root.mainloop()
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except Exception as e:
|
||
print(f"程序启动失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|