[Python] 纯文本查看 复制代码
import os
import re
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk, UnidentifiedImageError
import threading
import logging
# 设置日志配置
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
class ImageCompressorApp:
def __init__(self, root):
self.root = root
self.root.title("图片压缩工具 by ke")
self.root.geometry("1024x650")
self.root.configure(bg="#f0f0f0")
# 初始化变量
self.image_path = None # 图片路径
self.output_location = None # 输出位置
self.target_size_kb = tk.IntVar(value=400) # 默认目标大小为400KB
self.new_filename = tk.StringVar() # 新文件名
self.output_format = tk.StringVar(value="JPG") # 修改默认输出格式为JPG
# 创建界面元素
self.create_widgets()
def create_widgets(self):
main_frame = tk.Frame(self.root, bg="#f0f0f0")
main_frame.pack(expand=True, fill='both', padx=20, pady=20)
# 文件选择和预览
img_frame = tk.Frame(main_frame, bg="#f0f0f0")
img_frame.pack(fill='x')
self.img_label = tk.Label(img_frame, text="请加载一张图片", bg="#f0f0f0")
self.img_label.pack(side=tk.LEFT, padx=10, pady=10)
browse_button = tk.Button(img_frame, text="选择图片...", command=self.browse_files)
browse_button.pack(side=tk.RIGHT, padx=10, pady=10)
# 目标大小输入框
size_frame = tk.Frame(main_frame, bg="#f0f0f0")
size_frame.pack(fill='x', pady=5)
tk.Label(size_frame, text="目标大小 (KB):", bg="#f0f0f0").pack(side=tk.LEFT, padx=5)
self.size_entry = tk.Entry(size_frame, textvariable=self.target_size_kb, width=10)
self.size_entry.pack(side=tk.LEFT, padx=5)
# 设置新文件名
filename_frame = tk.Frame(main_frame, bg="#f0f0f0")
filename_frame.pack(fill='x', pady=5)
tk.Label(filename_frame, text="新文件名:", bg="#f0f0f0").pack(side=tk.LEFT, padx=5)
tk.Entry(filename_frame, textvariable=self.new_filename, width=30).pack(side=tk.LEFT, padx=5)
# 选择输出格式
format_frame = tk.Frame(main_frame, bg="#f0f0f0")
format_frame.pack(fill='x', pady=5)
tk.Label(format_frame, text="输出格式:", bg="#f0f0f0").pack(side=tk.LEFT, padx=5)
format_combobox = ttk.Combobox(format_frame, textvariable=self.output_format,
values=["JPG", "JPEG", "PNG", "BMP", "GIF"], width=10)
format_combobox.current(0) # 设置默认选中项为第一个选项,即"JPG"
format_combobox.pack(side=tk.LEFT, padx=5)
# 输出位置选择
output_frame = tk.Frame(main_frame, bg="#f0f0f0")
output_frame.pack(fill='x', pady=5)
tk.Label(output_frame, text="输出位置:", bg="#f0f0f0").pack(side=tk.LEFT, padx=5)
self.output_location_var = tk.StringVar()
tk.Entry(output_frame, textvariable=self.output_location_var, width=50).pack(side=tk.LEFT, padx=5)
tk.Button(output_frame, text="选择...", command=self.select_output_location).pack(side=tk.LEFT, padx=5)
# 创建一个容器用于放置压缩并保存按钮和进度条
bottom_frame = tk.Frame(self.root, bg="#f0f0f0")
bottom_frame.pack(side=tk.BOTTOM, fill='x', pady=20)
# 压缩并保存按钮
compress_button = tk.Button(bottom_frame, text="压缩并保存", command=self.start_compress_and_save)
compress_button.pack(side=tk.LEFT, padx=10)
# 进度条放在最下方
self.progress = ttk.Progressbar(bottom_frame, orient="horizontal", length=700, mode="determinate")
self.progress.pack(side=tk.RIGHT, padx=10)
def load_image(self, path):
"""加载并显示图片预览"""
try:
self.image_path = path
img = Image.open(path)
img.thumbnail((300, 300))
self.photo = ImageTk.PhotoImage(img)
self.img_label.config(image=self.photo)
except Exception as e:
logging.error(f"加载图片时出错: {str(e)}")
self.show_message("错误", f"加载图片时出错: {str(e)}")
def browse_files(self):
"""打开文件对话框选择图片"""
filename = filedialog.askopenfilename(
title="选择图片",
filetypes=[("Image files", "*.png *.jpg *.jpeg *.bmp *.gif")]
)
if filename:
self.load_image(filename)
def select_output_location(self):
"""选择图片压缩后的输出位置"""
directory = filedialog.askdirectory(title="选择输出位置")
if directory:
self.output_location_var.set(directory)
def start_compress_and_save(self):
"""启动压缩线程并在UI上显示进度条"""
self.progress["value"] = 0 # 初始化进度条为0%
self.progress["maximum"] = 100 # 设置进度条的最大值为100%
compress_thread = threading.Thread(target=self.compress_and_save)
compress_thread.start()
def compress_and_save(self):
"""执行图片压缩并保存到指定位置"""
try:
if not self.image_path or not self.output_location_var.get():
self.show_message("警告", "请选择图片和输出位置!")
return
new_filename = sanitize_filename(self.new_filename.get().strip())
if not new_filename:
self.show_message("警告", "请输入有效的文件名!")
return
output_filename = f"{new_filename}.{self.get_output_extension()}"
output_path = os.path.join(self.output_location_var.get(), output_filename)
# 检查输出路径有效性
if not os.path.isdir(self.output_location_var.get()):
logging.error(f"输出位置不存在: {self.output_location_var.get()}")
self.show_message("错误", "选择的输出位置无效,请重新选择。")
return
# 检查是否有写权限
if not os.access(self.output_location_var.get(), os.W_OK):
logging.error(f"没有写入权限: {self.output_location_var.get()}")
self.show_message("错误", "没有足够的权限在选择的位置写入文件。")
return
target_size_kb = self.target_size_kb.get()
original_size_kb = os.path.getsize(self.image_path) / 1024
if original_size_kb <= target_size_kb:
self.show_message("提示", "原图大小已经小于或等于目标大小,无需压缩。")
return
self.compress_image(self.image_path, output_path, target_size_kb)
self.set_progress_value(100) # 完成后设置进度条为100%
self.show_message("成功", "图片已成功压缩并保存!")
except Exception as e:
logging.error(f"压缩失败: {str(e)}")
self.show_message("错误", f"压缩失败: {str(e)}")
finally:
self.progress.stop()
def compress_image(self, image_path, output_path, target_size_kb):
"""压缩图片至指定大小"""
try:
img = Image.open(image_path).convert('RGB') # 确保图片模式兼容
quality = 95
original_size_kb = os.path.getsize(image_path) / 1024
format_name = self.get_output_format() # 获取正确的格式名称
while original_size_kb > target_size_kb and quality >= 10:
try:
img.save(output_path, format=format_name, optimize=True, quality=quality)
original_size_kb = os.path.getsize(output_path) / 1024
quality -= 5
self.update_progress(quality)
except IOError as e:
logging.error(f"IO 错误: {str(e)}")
raise ValueError(f"IO 错误: {str(e)}")
except Exception as e:
logging.error(f"保存图片时出错: {str(e)}")
raise ValueError(f"保存图片时出错: {str(e)}")
if original_size_kb > target_size_kb:
raise ValueError("无法将图片压缩到指定大小!请尝试增加目标大小或减少图片复杂度。")
except UnidentifiedImageError:
logging.error("无法识别的图片格式")
self.show_message("错误", "无法识别的图片格式,请选择其他图片。")
except Exception as e:
logging.error(f"压缩图片时出错: {str(e)}")
self.show_message("错误", f"压缩图片时出错: {str(e)}")
def get_output_extension(self):
"""根据输出格式返回正确的文件扩展名"""
format_name = self.output_format.get().upper()
if format_name in ["JPG", "JPEG"]:
return "jpg"
elif format_name == "PNG":
return "png"
elif format_name == "BMP":
return "bmp"
elif format_name == "GIF":
return "gif"
else:
return "jpg"
def get_output_format(self):
"""根据输出格式返回正确的Pillow格式名称"""
format_name = self.output_format.get().upper()
if format_name in ["JPG", "JPEG"]:
return "JPEG"
elif format_name == "PNG":
return "PNG"
elif format_name == "BMP":
return "BMP"
elif format_name == "GIF":
return "GIF"
else:
return "JPEG"
def update_progress(self, quality):
"""更新进度条的值"""
max_quality = 95
min_quality = 10
progress_value = ((max_quality - quality) / (max_quality - min_quality)) * 100
self.root.after(0, lambda: self.set_progress_value(min(progress_value, 100)))
def set_progress_value(self, value):
"""设置进度条的值"""
self.progress["value"] = value
def show_message(self, title, message):
"""显示消息框并停止进度条"""
self.root.after(0, lambda: messagebox.showinfo(title, message))
def sanitize_filename(filename):
"""清理文件名中的非法字符"""
return re.sub(r'[\\/*?:"<>|]', "", filename)
if __name__ == "__main__":
root = tk.Tk()
app = ImageCompressorApp(root)
root.mainloop()