2024-11-26T13:59:58.png
2024-11-26T13:59:58.png

import os
import threading
from PIL import Image
from io import BytesIO
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinterdnd2 import DND_FILES, TkinterDnD
import platform
import subprocess
import traceback

# 支持的图片格式
SUPPORTED_FORMATS = ['JPEG', 'JPG', 'PNG']


def is_valid_path(path):
    """验证路径的合法性"""
    directory = os.path.dirname(path)
    if not directory:
        directory = '.'
    return os.path.isdir(directory) and os.access(directory, os.W_OK)


def open_directory(path):
    """跨平台打开文件夹"""
    try:
        if platform.system() == "Windows":
            os.startfile(path)
        elif platform.system() == "Darwin":  # macOS
            subprocess.Popen(["open", path])
        else:  # Linux and others
            subprocess.Popen(["xdg-open", path])
    except Exception as e:
        messagebox.showerror("错误", f"无法打开文件夹: {e}")


def get_image_size(img: Image.Image, format: str, quality: int = None):
    """获取图片在指定格式和质量下的大小(KB)"""
    buffer = BytesIO()
    try:
        save_kwargs = {}
        if format.lower() in ['jpeg', 'jpg']:
            save_kwargs['format'] = format
            if quality is not None:
                save_kwargs['quality'] = quality
        elif format.lower() == 'png':
            save_kwargs['format'] = format
            save_kwargs['optimize'] = True
            save_kwargs['compress_level'] = 9  # 最大压缩
        img.save(buffer, **save_kwargs)
        size_kb = buffer.tell() / 1024
    except Exception as e:
        buffer.close()
        raise e
    buffer.close()
    return size_kb


def compress_image_thread(image_path: str, target_size_kb: int, gui, item_id):
    """
    线程中执行的压缩函数
    """
    try:
        with Image.open(image_path) as img:
            img_format = img.format.upper()  # 保持原始格式(大写)
            if img_format not in SUPPORTED_FORMATS:
                gui.update_item_status(item_id, False, f"暂不支持 {img_format} 格式的图片。")
                return

            # 确定初始条件
            if img_format in ['JPEG', 'JPG']:
                quality = 95
                step = 5
                min_quality = 10

                current_size = get_image_size(img, img_format, quality)
                gui.update_item_progress(item_id, 10)
                gui.update_item_status(item_id, True, "开始压缩...")

                # 如果图片已经小于目标,无需压缩
                if current_size <= target_size_kb:
                    gui.update_item_status(item_id, True, "图片已经小于或等于目标大小,无需压缩。")
                    gui.update_item_progress(item_id, 100)
                    gui.update_item_result(item_id, "无需压缩", image_path, current_size)
                    return

                # 压缩循环 - 质量调整
                while current_size > target_size_kb and quality > min_quality:
                    quality -= step
                    current_size = get_image_size(img, img_format, quality)
                    progress_val = 10 + ((95 - quality) / (95 - min_quality)) * 60  # 10-70%
                    gui.update_item_progress(item_id, progress_val)

                # 如果质量压缩不足,尝试缩放图片尺寸
                if current_size > target_size_kb:
                    gui.update_item_status(item_id, True, "进一步压缩通过调整图片尺寸。")
                    width, height = img.size
                    while current_size > target_size_kb and width > 100 and height > 100:
                        width = int(width * 0.9)
                        height = int(height * 0.9)
                        img_resized = img.resize((width, height), Image.Resampling.LANCZOS)
                        current_size = get_image_size(img_resized, img_format, quality)
                        progress_val = 70 + ((95 - quality) / (95 - min_quality)) * 20  # 70-90%
                        gui.update_item_progress(item_id, progress_val)
                        if current_size <= target_size_kb:
                            img = img_resized  # 更新到缩放后的图片
                            break
                    else:
                        img = img_resized  # 最后一次调整后的图片

            elif img_format == 'PNG':
                current_size = get_image_size(img, img_format)
                gui.update_item_progress(item_id, 10)
                gui.update_item_status(item_id, True, "开始压缩...")

                # 如果图片已经小于目标,无需压缩
                if current_size <= target_size_kb:
                    gui.update_item_status(item_id, True, "图片已经小于或等于目标大小,无需压缩。")
                    gui.update_item_progress(item_id, 100)
                    gui.update_item_result(item_id, "无需压缩", image_path, current_size)
                    return

                # 尝试通过调整图片尺寸压缩
                gui.update_item_status(item_id, True, "压缩通过调整图片尺寸。")
                width, height = img.size
                while current_size > target_size_kb and width > 100 and height > 100:
                    width = int(width * 0.9)
                    height = int(height * 0.9)
                    img_resized = img.resize((width, height), Image.Resampling.LANCZOS)
                    current_size = get_image_size(img_resized, img_format)
                    progress_val = 10 + (1 - (current_size / target_size_kb)) * 80  # 10-90%
                    gui.update_item_progress(item_id, min(progress_val, 90))
                    if current_size <= target_size_kb:
                        img = img_resized  # 更新到缩放后的图片
                        break
                else:
                    img = img_resized  # 最后一次调整后的图片

            # 确定输出路径
            base, ext = os.path.splitext(image_path)
            output_path = f"{base}_compressed{ext}"

            # 检查路径合法性
            if not is_valid_path(output_path):
                gui.update_item_status(item_id, False, f"输出路径无效: {output_path}")
                return

            # 保存最终压缩的图片
            try:
                if img_format in ['JPEG', 'JPG']:
                    img.save(output_path, format=img_format, quality=quality)
                else:
                    img.save(output_path, format=img_format, optimize=True, compress_level=9)
            except Exception as e:
                gui.update_item_status(item_id, False, f"保存压缩图片时出错: {e}")
                return

            # 获取最终大小
            final_size = os.path.getsize(output_path) / 1024

            gui.update_item_progress(item_id, 100)
            gui.update_item_result(item_id, "压缩完成", output_path, final_size)

    except Exception as e:
        error_message = traceback.format_exc()
        gui.update_item_status(item_id, False, f"压缩过程中出错:\n{error_message}")


class ImageCompressorGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("图片压缩工具")
        self.root.geometry("900x600")
        self.root.configure(bg="#f0f2f5")
        self.root.resizable(True, True)

        self.initialize_styles()
        self.initialize_widgets()

        # 初始化选定文件变量
        self.selected_files = []

    def initialize_styles(self):
        """初始化样式"""
        self.style = ttk.Style()
        self.style.theme_use('clam')

        # 按钮样式
        self.style.configure("TButton",
                             padding=6,
                             relief="flat",
                             background="#4CAF50",
                             foreground="white",
                             font=("Helvetica", 10, "bold"))
        self.style.map("TButton",
                       background=[('active', '#45a049')])

        # 标签样式
        self.style.configure("TLabel",
                             background="#f0f2f5",
                             font=("Helvetica", 10))

        self.style.configure("Header.TLabel",
                             background="#f0f2f5",
                             font=("Helvetica", 16, "bold"))

        # 输入框样式
        self.style.configure("TEntry",
                             padding=6,
                             font=("Helvetica", 10))

        # 进度条样式
        self.style.configure("Progressbar.Horizontal.TProgressbar",
                             troughcolor="#d3d3d3",
                             background="#4CAF50")

        # Treeview样式
        self.style.configure("Treeview",
                             background="#ffffff",
                             foreground="black",
                             rowheight=25,
                             fieldbackground="#ffffff",
                             font=("Helvetica", 10))
        self.style.configure("Treeview.Heading",
                             background="#4CAF50",
                             foreground="white",
                             font=("Helvetica", 10, "bold"))

    def initialize_widgets(self):
        """初始化界面组件"""
        # Header
        header = ttk.Label(self.root, text="图片压缩工具", style="Header.TLabel")
        header.pack(pady=10)

        # 选择图片区域(支持拖放)
        self.frame_drop = ttk.Frame(self.root, borderwidth=2, relief="ridge", padding=10)
        self.frame_drop.pack(pady=10, padx=20, fill='x', expand=False)

        self.label_drop = ttk.Label(self.frame_drop,
                                    text="将图片拖放到此处或点击浏览按钮选择",
                                    foreground="#888",
                                    font=("Helvetica", 12))
        self.label_drop.pack(expand=True)

        # 注册拖放目标
        self.frame_drop.drop_target_register(DND_FILES)
        self.frame_drop.dnd_bind('<<Drop>>', self.drop)

        # 浏览按钮
        self.button_browse = ttk.Button(self.root, text="浏览图片", command=self.browse_images)
        self.button_browse.pack(pady=5)

        # 输入目标大小
        frame_size = ttk.Frame(self.root)
        frame_size.pack(pady=10)
        ttk.Label(frame_size, text="目标大小 (KB):", font=("Helvetica", 10)).pack(side=tk.LEFT, padx=(0, 5))
        self.target_size = tk.StringVar(value="200")  # 默认目标大小
        self.entry_size = ttk.Entry(frame_size, width=20, textvariable=self.target_size)
        self.entry_size.pack(side=tk.LEFT)

        # 压缩按钮
        self.button_compress = ttk.Button(self.root, text="开始压缩", command=self.start_compression)
        self.button_compress.pack(pady=10)

        # Treeview 显示文件列表
        columns = ("filename", "status", "progress", "path", "size")
        self.tree = ttk.Treeview(self.root, columns=columns, show='headings', selectmode='none')
        self.tree.heading("filename", text="文件名")
        self.tree.heading("status", text="状态")
        self.tree.heading("progress", text="进度")
        self.tree.heading("path", text="输出路径")
        self.tree.heading("size", text="压缩后大小 (KB)")

        self.tree.column("filename", width=200, anchor='center')
        self.tree.column("status", width=100, anchor='center')
        self.tree.column("progress", width=100, anchor='center')
        self.tree.column("path", width=300, anchor='center')
        self.tree.column("size", width=150, anchor='center')

        self.tree.pack(pady=10, padx=20, fill='both', expand=True)

        # 添加垂直滚动条
        scrollbar = ttk.Scrollbar(self.tree, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscroll=scrollbar.set)
        scrollbar.pack(side='right', fill='y')

        # 状态栏
        self.status_label = ttk.Label(self.root, text="准备压缩...", foreground="#333", font=("Helvetica", 10))
        self.status_label.pack(pady=5)

    def browse_images(self):
        """浏览并选择多张图片"""
        filetypes = (
            ("Image files", "*.jpg *.jpeg *.png"),
            ("All files", "*.*")
        )
        filenames = filedialog.askopenfilenames(title="选择图片", initialdir=os.getcwd(), filetypes=filetypes)
        if filenames:
            self.add_files(filenames)

    def drop(self, event):
        """处理拖放事件"""
        files = self.root.splitlist(event.data)
        valid_files = [f for f in files if os.path.isfile(f) and f.lower().endswith(('.jpg', '.jpeg', '.png'))]
        if valid_files:
            self.add_files(valid_files)
        else:
            messagebox.showerror("错误", "仅支持 .jpg, .jpeg, .png 格式的图片。")

    def add_files(self, filenames):
        """将选中的文件添加到列表中"""
        for file in filenames:
            if file not in self.selected_files:
                self.selected_files.append(file)
                filename = os.path.basename(file)
                self.tree.insert("", tk.END, values=(filename, "待压缩", "0%", "", ""))
        self.label_drop.config(text=f"{len(self.selected_files)} 张图片已选择", foreground="#000")

    def update_item_status(self, item_id, success, message):
        """更新单个文件的状态"""
        status = "成功" if success else "失败"
        self.tree.set(item_id, "status", message if not success else status)

    def update_item_progress(self, item_id, progress):
        """更新单个文件的压缩进度"""
        progress_text = f"{int(progress)}%"
        self.tree.set(item_id, "progress", progress_text)

    def update_item_result(self, item_id, message, output_path, size_kb):
        """更新单个文件的压缩结果"""
        self.tree.set(item_id, "status", message)
        self.tree.set(item_id, "path", output_path)
        self.tree.set(item_id, "size", f"{size_kb:.2f}")
        # 绑定双击事件打开文件夹
        if message == "压缩完成":
            self.tree.item(item_id, tags=("success",))
            self.tree.tag_bind("success", "<Double-1>", self.open_file_folder)

    def open_file_folder(self, event):
        """打开压缩后图片所在的文件夹"""
        selected_item = self.tree.selection()
        if selected_item:
            item = selected_item[0]
            output_path = self.tree.set(item, "path")
            if output_path and os.path.exists(output_path):
                open_directory(os.path.dirname(output_path))
            else:
                messagebox.showerror("错误", "找不到输出路径。")

    def start_compression(self):
        """开始压缩所有选中的图片"""
        if not self.selected_files:
            messagebox.showwarning("警告", "请先选择至少一张图片。")
            return

        try:
            target_size_kb = int(self.target_size.get())
            if target_size_kb <= 0:
                raise ValueError
        except ValueError:
            messagebox.showerror("错误", "请输入有效的目标大小 (KB)。")
            return

        # 禁用按钮以防多次点击
        self.button_compress.config(state=tk.DISABLED)
        self.button_browse.config(state=tk.DISABLED)

        # 更新状态
        self.update_status("开始压缩...")

        # 启动压缩线程
        threading.Thread(target=self.compress_all_images, args=(target_size_kb,), daemon=True).start()

    def compress_all_images(self, target_size_kb):
        """压缩所有选中的图片"""
        for idx, image_path in enumerate(self.selected_files):
            # 获取 Treeview 项目ID
            item_id = self.tree.get_children()[idx]
            # 更新状态为压缩中
            self.tree.set(item_id, "status", "压缩中...")
            self.tree.set(item_id, "progress", "0%")
            # 启动单个文件的压缩线程
            threading.Thread(target=compress_image_thread, args=(image_path, target_size_kb, self, item_id), daemon=True).start()

        # 压缩完成后恢复按钮
        self.root.after(100, self.enable_buttons_after_compression)

    def enable_buttons_after_compression(self):
        """压缩完成后重新启用按钮"""
        self.button_compress.config(state=tk.NORMAL)
        self.button_browse.config(state=tk.NORMAL)
        self.update_status("压缩完成。")

    def update_status(self, message):
        """更新状态栏消息"""
        self.status_label.config(text=message)


def main():
    root = TkinterDnD.Tk()
    app = ImageCompressorGUI(root)
    root.mainloop()


if __name__ == "__main__":
    main()

功能概述

1.支持的图片格式:

支持 JPEG, JPG, PNG 格式的图片压缩。

2.目标大小设定:

用户可以通过输入框指定目标压缩大小(单位:KB)。

3.拖放功能:

使用 TkinterDnD 实现了拖放功能,用户可以直接将图片拖到应用窗口中。

4.文件浏览:

提供文件选择对话框,支持一次选择多张图片。

5.压缩逻辑:

对于 JPEG/JPG 图片,优先通过调整图片质量进行压缩;如果质量调整不足,再尝试缩放图片尺寸。
对于 PNG 图片,直接通过调整尺寸进行压缩。

6.多线程:

使用 threading 实现多线程处理,防止主线程阻塞,提升用户体验。

7.进度显示:

使用 Treeview 显示每张图片的压缩状态、进度、输出路径以及压缩后的大小。
跨平台文件夹打开:
支持 Windows、macOS 和 Linux 平台打开文件夹功能。

8.界面美化:

使用 ttk.Style 对按钮、进度条、Treeview 等组件进行了美化。

推荐文章

欢迎使用 Typecho

如果您看到这篇文章,表示您的 blog 已经安装成功.

怎么使Excel表格复制出图片?

Excel复制粘贴功能解析

可按 ESC 键退出搜索

0 篇文章已搜寻到~