Py编写-图片压缩小工具图片批量生成pdf

查看 59|回复 8
作者:hhh123555   
Py编写-图片压缩小工具图片批量生成到同个pdf,为了控制图片最终的大小!
打包exe下载地址:
[color=]链接:
https://pan.baidu.com/s/1h_PNAag2wDq36fzKT5_hDw 提取码: 52pj
打包py3.13.9 embed版本(绿色版本,支持win10 *64及以上,目录下有一键启动vbs,双击运行)下载地址:
[color=]链接:
https://pan.baidu.com/s/10J9UAZTQEDTJmpXWpjb8Vg 提取码: 52pj
注:embed版本中还有个pdf的转换工具,这个目前还不完善,欢迎提提建议。
源码在下,
[color=]来点热心值
就行,我是代码小白,调试了几个晚上。
下期分享纯py+ffmpeg,实现视频添加时间戳工具,看了下网上基本上搜不到,我们单位有需要就写了。
下下期分享统信uosAMD64架构
[color=]离线
安装py3.13.9
(单位已经陆续换国产机了,不得不说,没用过linux的人真用不习惯)。
背景:
1.为什么要控制图片大小?原因:银行柜面对公开户新模式上线,上传的依据图片大小有要求,客户经理发来的图片过大,导致手动压缩非常麻烦,故编写此py,大部分由ai编写,因行内系统监控,不能运行非授权exe文件,因此便于编写软件后下发柜员,故本人自己用的是embed绿色py版本
(透露一下,是国有银行,这么垃圾的系统,天天让我们底层员工干些不知道什么乱七八糟的工作),如果有同行的同事看到,可以下载embed版本直接下发柜员使用
2.吾爱老粉,一直在白拿的路上,想拿点贡献
实现功能:
1.压缩大小可预测,支持批量压缩
2.智能压缩,逻辑为先修改图片尺寸到1080*1920以下,后降低图片质量,若还是超过预测大小,则再降低图片尺寸,因为行里的要求是图片大小不超过100kb,故程序是100kb的,双击可以自定义大小,默认大小设置改下源码就好
3.微信或部分软件导出后自动被压缩过的jpg图片支持压缩还原,拖动质量到100%可尽可能还原一点点图片大小与质量
4.支持图片批量添加至pdf
5.双击列表明细可预览图片与单个设定图片压缩质量,或设置智能压缩大小
6.png图片勾选智能压缩默认改为jpg输出



[Python] 纯文本查看 复制代码import os
import sys
import threading
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import tempfile
import math
import io
# 尝试导入 tkinterdnd2,如果不可用则禁用拖放功能
try:
    from tkinterdnd2 import DND_FILES, TkinterDnD
    DND_AVAILABLE = True
except ImportError:
    DND_AVAILABLE = False
    print("注意: tkinterdnd2 未安装,拖放功能不可用。")
    print("请安装: pip install tkinterdnd2")
# 尝试导入 img2pdf
try:
    import img2pdf
    IMG2PDF_AVAILABLE = True
except ImportError:
    IMG2PDF_AVAILABLE = False
    print("注意: img2pdf 未安装,PDF功能将使用Pillow备用方案。")
    print("请安装: pip install img2pdf")
class ImageCompressor:
    def __init__(self):
        # 如果 tkinterdnd2 可用,使用 TkinterDnD 作为根窗口
        if DND_AVAILABLE:
            self.root = TkinterDnD.Tk()
        else:
            self.root = tk.Tk()
        self.root.title("图片压缩工具 v2.0.1 by8763")
        self.root.geometry("850x850")
        self.root.resizable(True, True)
        # 设置主题
        self.setup_ui()
        # 支持的图片格式
        self.supported_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
        # 存储图片详细信息
        self.image_data = []
   
    def setup_ui(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 标题
        title_label = ttk.Label(main_frame, text="图片压缩工具-吾爱破解专用", font=("Arial", 16, "bold"))
        title_label.pack(pady=10)
        
        # 说明文本
        desc_text = "拖放图片到下方区域或点击选择图片"
        if not DND_AVAILABLE:
            desc_text = "点击选择图片 (拖放功能不可用,请安装 tkinterdnd2)"
        desc_label = ttk.Label(main_frame, text=desc_text, font=("Arial", 10))
        desc_label.pack(pady=5)
        
        # 拖放区域
        self.drop_frame = tk.Frame(main_frame, bg="#f0f0f0", relief=tk.RAISED, bd=2, height=150)
        self.drop_frame.pack(fill=tk.X, pady=10)
        self.drop_frame.pack_propagate(False)
        self.drop_label = tk.Label(self.drop_frame, text="拖放图片到这里" if DND_AVAILABLE else "选择图片",
                                  bg="#f0f0f0", fg="#666666", font=("Arial", 12))
        self.drop_label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
        
        # 绑定拖放事件(仅在 tkinterdnd2 可用时)
        if DND_AVAILABLE:
            self.drop_frame.drop_target_register(DND_FILES)
            self.drop_frame.dnd_bind('>', self.on_drop)
        else:
            # 如果没有拖放功能,绑定点击事件
            self.drop_frame.bind("[B]", self.select_images)
            self.drop_label.bind("[B]", self.select_images)
        
        # 全局压缩设置框架
        global_settings_frame = ttk.LabelFrame(main_frame, text="全局压缩设置", padding="10")
        global_settings_frame.pack(fill=tk.X, pady=10)
        
        # 压缩质量设置
        settings_frame = ttk.Frame(global_settings_frame)
        settings_frame.pack(fill=tk.X, pady=5)
        ttk.Label(settings_frame, text="压缩质量:").pack(side=tk.LEFT, padx=5)
        self.quality_var = tk.IntVar(value=85)
        self.quality_scale = ttk.Scale(settings_frame, from_=10, to=100, variable=self.quality_var,
                                 orient=tk.HORIZONTAL)
        self.quality_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        self.quality_label = ttk.Label(settings_frame, text="85%")
        self.quality_label.pack(side=tk.LEFT, padx=5)
        
        # 绑定质量滑块事件
        self.quality_scale.configure(command=self.on_global_quality_change)
        
        # 输出格式选择
        format_frame = ttk.Frame(global_settings_frame)
        format_frame.pack(fill=tk.X, pady=5)
        ttk.Label(format_frame, text="输出格式:").pack(side=tk.LEFT, padx=5)
        self.format_var = tk.StringVar(value="保持原格式")
        self.format_combo = ttk.Combobox(format_frame, textvariable=self.format_var,
                                   values=["保持原格式", "JPEG", "PNG", "WEBP"], state="readonly")
        self.format_combo.pack(side=tk.LEFT, padx=5)
        self.format_combo.bind('>', self.on_global_format_change)
        
        # PDF导出选项
        pdf_frame = ttk.Frame(global_settings_frame)
        pdf_frame.pack(fill=tk.X, pady=5)
        self.pdf_var = tk.BooleanVar(value=False)
        self.pdf_check = ttk.Checkbutton(pdf_frame, text="PDF导出-全选",
                                        variable=self.pdf_var, command=self.on_pdf_toggle)
        self.pdf_check.pack(side=tk.LEFT, padx=5)
        
        # 智能压缩选项
        smart_compress_frame = ttk.Frame(global_settings_frame)
        smart_compress_frame.pack(fill=tk.X, pady=5)
        self.smart_compress_var = tk.BooleanVar(value=False)
        self.smart_compress_check = ttk.Checkbutton(smart_compress_frame, text="智能压缩至100KB以下-全选",
                                                   variable=self.smart_compress_var, command=self.on_smart_compress_toggle)
        self.smart_compress_check.pack(side=tk.LEFT, padx=5)
        
        # 按钮框架
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=10)
        self.select_btn = ttk.Button(button_frame, text="选择图片", command=self.select_images)
        self.select_btn.pack(side=tk.LEFT, padx=5)
        self.compress_btn = ttk.Button(button_frame, text="开始压缩", command=self.start_compression)
        self.compress_btn.pack(side=tk.LEFT, padx=5)
        self.clear_btn = ttk.Button(button_frame, text="清空列表", command=self.clear_list)
        self.clear_btn.pack(side=tk.LEFT, padx=5)
        
        # 进度条
        self.progress = ttk.Progressbar(main_frame, mode='determinate')
        self.progress.pack(fill=tk.X, pady=5)
        
        # 状态标签
        self.status_var = tk.StringVar(value="准备就绪")
        status_label = ttk.Label(main_frame, textvariable=self.status_var)
        status_label.pack(fill=tk.X)
        
        # 图片列表框架
        list_frame = ttk.LabelFrame(main_frame, text="图片列表 (双击项目可单独设置)", padding="10")
        list_frame.pack(fill=tk.BOTH, expand=True, pady=10)
        
        # 创建Treeview来显示图片列表
        columns = ("pdf_export", "smart_compress", "name", "original_size", "predicted_size", "compressed_size", "quality")
        self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=8)
        # 定义列
        self.tree.heading("pdf_export", text="PDF导出")
        self.tree.heading("smart_compress", text="智能压缩")
        self.tree.heading("name", text="文件名")
        self.tree.heading("original_size", text="原图大小")
        self.tree.heading("predicted_size", text="预测大小")
        self.tree.heading("compressed_size", text="压缩后大小")
        self.tree.heading("quality", text="质量")
        # 设置列宽
        self.tree.column("pdf_export", width=80, minwidth=80, anchor=tk.CENTER)
        self.tree.column("smart_compress", width=100, minwidth=100, anchor=tk.CENTER)
        self.tree.column("name", width=180, minwidth=150)
        self.tree.column("original_size", width=90, minwidth=90)
        self.tree.column("predicted_size", width=90, minwidth=90)
        self.tree.column("compressed_size", width=90, minwidth=90)
        self.tree.column("quality", width=70, minwidth=70)
        
        # 滚动条
        tree_scroll = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=tree_scroll.set)
        
        # 布局
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 绑定双击事件
        self.tree.bind("", self.on_item_double_click)
        # 绑定复选框点击事件
        self.tree.bind("[B]", self.on_tree_click)
        
        # 大小信息框架
        self.size_info_frame = ttk.Frame(main_frame)
        self.size_info_frame.pack(fill=tk.X, pady=5)
        self.total_original_size = ttk.Label(self.size_info_frame, text="原图总大小: 0 B")
        self.total_original_size.pack(side=tk.LEFT, padx=10)
        self.total_predicted_size = ttk.Label(self.size_info_frame, text="预测总大小: 0 B")
        self.total_predicted_size.pack(side=tk.LEFT, padx=10)
        self.total_compressed_size = ttk.Label(self.size_info_frame, text="压缩后总大小: 0 B")
        self.total_compressed_size.pack(side=tk.LEFT, padx=10)
        self.compression_ratio = ttk.Label(self.size_info_frame, text="压缩率: 0%")
        self.compression_ratio.pack(side=tk.LEFT, padx=10)
   
    def quality_to_png_compress_level(self, quality):
        """将质量值(10-100)转换为PNG压缩级别(0-9)"""
        return 9 - min(9, max(0, round((quality - 10) * 9 / 90)))
   
    def on_global_quality_change(self, value):
        """全局质量设置改变时的处理"""
        quality = int(float(value))
        self.quality_label.config(text=f"{quality}%")
        
        # 更新所有未自定义设置的图片的质量和预测大小
        for i, image_info in enumerate(self.image_data):
            if not image_info.get('custom', False):
                image_info['quality'] = quality
                image_info['predicted_size'] = self.predict_compressed_size(
                    image_info['path'], quality, image_info['format'],
                    image_info.get('smart_compress', False), image_info.get('custom_target_size', 102400)
                )
                # 更新Treeview显示
                item = self.tree.get_children()
                self.tree.set(item, "quality", f"{quality}%")
                self.tree.set(item, "predicted_size", self.format_file_size(image_info['predicted_size']))
        
        # 更新总大小信息
        self.update_size_info()
   
    def on_global_format_change(self, event):
        """全局格式设置改变时的处理"""
        format_setting = self.format_var.get()
        
        # 更新所有未自定义设置的图片的格式和预测大小
        for i, image_info in enumerate(self.image_data):
            if not image_info.get('custom', False):
                # PNG图片且勾选了智能压缩,则强制使用JPEG格式
                if image_info.get('smart_compress', False) and Path(image_info['path']).suffix.lower() == '.png':
                    image_info['format'] = "JPEG"
                else:
                    image_info['format'] = format_setting
               
                image_info['predicted_size'] = self.predict_compressed_size(
                    image_info['path'], image_info['quality'], image_info['format'],
                    image_info.get('smart_compress', False), image_info.get('custom_target_size', 102400)
                )
                # 更新Treeview显示
                item = self.tree.get_children()
                self.tree.set(item, "predicted_size", self.format_file_size(image_info['predicted_size']))
        
        # 更新总大小信息
        self.update_size_info()
   
    def on_pdf_toggle(self):
        """PDF导出复选框状态改变"""
        pdf_export = self.pdf_var.get()
        # 更新所有图片的PDF导出状态
        for i, image_info in enumerate(self.image_data):
            image_info['pdf_export'] = pdf_export
            # 更新Treeview显示
            item = self.tree.get_children()
            self.tree.set(item, "pdf_export", "☑" if pdf_export else "☐")
   
    def on_smart_compress_toggle(self):
        """智能压缩复选框状态改变"""
        smart_compress = self.smart_compress_var.get()
        # 更新所有图片的智能压缩状态
        for i, image_info in enumerate(self.image_data):
            image_info['smart_compress'] = smart_compress
            
            # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
            if smart_compress and Path(image_info['path']).suffix.lower() == '.png':
                image_info['format'] = "JPEG"
            
            # 更新预测大小
            image_info['predicted_size'] = self.predict_compressed_size(
                image_info['path'], image_info['quality'], image_info['format'],
                smart_compress, image_info.get('custom_target_size', 102400)
            )
            # 更新Treeview显示
            item = self.tree.get_children()
            self.tree.set(item, "smart_compress", "☑" if smart_compress else "☐")
            self.tree.set(item, "predicted_size", self.format_file_size(image_info['predicted_size']))
        
        # 更新总大小信息
        self.update_size_info()
   
    def on_tree_click(self, event):
        """处理Treeview点击事件,用于复选框"""
        item = self.tree.identify_row(event.y)
        column = self.tree.identify_column(event.x)
        
        if item:
            index = self.tree.index(item)
            if column == "#1":  # PDF导出列
                # 切换选中状态
                current_state = self.image_data[index].get('pdf_export', False)
                new_state = not current_state
                self.image_data[index]['pdf_export'] = new_state
                self.tree.set(item, "pdf_export", "☑" if new_state else "☐")
            elif column == "#2":  # 智能压缩列
                # 切换选中状态
                current_state = self.image_data[index].get('smart_compress', False)
                new_state = not current_state
                self.image_data[index]['smart_compress'] = new_state
               
                # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
                if new_state and Path(self.image_data[index]['path']).suffix.lower() == '.png':
                    self.image_data[index]['format'] = "JPEG"
               
                # 更新预测大小
                self.image_data[index]['predicted_size'] = self.predict_compressed_size(
                    self.image_data[index]['path'], self.image_data[index]['quality'], self.image_data[index]['format'],
                    new_state, self.image_data[index].get('custom_target_size', 102400)
                )
                self.tree.set(item, "smart_compress", "☑" if new_state else "☐")
                self.tree.set(item, "predicted_size", self.format_file_size(self.image_data[index]['predicted_size']))
                # 更新总大小信息
                self.update_size_info()
   
    def on_drop(self, event):
        # 获取拖放的文件路径
        files = self.root.tk.splitlist(event.data)
        self.add_images(files)
   
    def select_images(self, event=None):
        files = filedialog.askopenfilenames(
            title="选择图片",
            filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp *.tiff *.webp"), ("所有文件", "*.*")]
        )
        if files:
            self.add_images(files)
   
    def add_images(self, files):
        new_files = [f for f in files if Path(f).suffix.lower() in self.supported_formats]
        if not new_files:
            messagebox.showwarning("警告", "没有找到支持的图片格式!")
            return
        
        for file in new_files:
            if file not in [data['path'] for data in self.image_data]:
                # 获取文件大小
                file_size = os.path.getsize(file)
               
                # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
                format_setting = self.format_var.get()
                if self.smart_compress_var.get() and Path(file).suffix.lower() == '.png':
                    format_setting = "JPEG"
               
                # 预测压缩后大小
                predicted_size = self.predict_compressed_size(
                    file, self.quality_var.get(), format_setting,
                    self.smart_compress_var.get(), 102400
                )
                # 添加到数据列表
                self.image_data.append({
                    'path': file,
                    'size': file_size,
                    'compressed_size': None,
                    'predicted_size': predicted_size,
                    'quality': self.quality_var.get(),
                    'format': format_setting,
                    'custom': False,
                    'pdf_export': self.pdf_var.get(),
                    'smart_compress': self.smart_compress_var.get(),
                    'custom_target_size': 102400  # 默认100KB
                })
                # 添加到Treeview
                self.tree.insert("", "end", values=(
                    "☑" if self.pdf_var.get() else "☐",  # PDF导出列
                    "☑" if self.smart_compress_var.get() else "☐",  # 智能压缩列
                    Path(file).name,
                    self.format_file_size(file_size),
                    self.format_file_size(predicted_size),
                    "未压缩",
                    f"{self.quality_var.get()}%"
                ))
        
        self.update_size_info()
        self.status_var.set(f"已添加 {len(new_files)} 张图片,共 {len(self.image_data)} 张")
   
    def predict_compressed_size(self, image_path, quality, format_setting, smart_compress=False, custom_target_size=102400):
        """预测压缩后文件大小"""
        try:
            # 如果启用了智能压缩,直接返回目标大小
            if smart_compress:
                return custom_target_size
            
            with Image.open(image_path) as img:
                # 确定输出格式
                if format_setting == "保持原格式":
                    format = None
                else:
                    format = format_setting
               
                # 确定临时文件后缀
                if format == "PNG":
                    suffix = '.png'
                elif format == "WEBP":
                    suffix = '.webp'
                elif format == "JPEG":
                    suffix = '.jpg'
                else:
                    # 保持原格式的情况
                    if Path(image_path).suffix.lower() in ['.png']:
                        suffix = '.png'
                    elif Path(image_path).suffix.lower() in ['.webp']:
                        suffix = '.webp'
                    else:
                        suffix = '.jpg'  # 默认使用jpg
               
                with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
                    temp_path = temp_file.name
               
                # 保存预测文件
                if format == "PNG" or (format is None and Path(image_path).suffix.lower() in ['.png']):
                    compress_level = self.quality_to_png_compress_level(quality)
                    img.save(temp_path, format='PNG', compress_level=compress_level)
                else:
                    if (format == "JPEG" or (format is None and Path(image_path).suffix.lower() in ['.jpeg', '.jpg'])) and img.mode == 'RGBA':
                        img = img.convert('RGB')
                    
                    if quality == 100:
                        img.save(temp_path, format=format, quality=quality, optimize=False)
                    else:
                        img.save(temp_path, format=format, quality=quality, optimize=True)
               
                # 获取预测大小
                predicted_size = os.path.getsize(temp_path)
               
                # 删除临时文件
                os.unlink(temp_path)
                return predicted_size
        except Exception as e:
            print(f"预测压缩大小失败: {e}")
            # 如果预测失败,返回原图大小的80%作为估算
            original_size = os.path.getsize(image_path)
            return int(original_size * 0.8)
   
    def format_file_size(self, size):
        """格式化文件大小显示"""
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size >', lambda e: update_predicted_size())
        smart_compress_var.trace('w', update_format_state)
        custom_size_var.trace('w', lambda *args: update_predicted_size())
        
        # 初始更新预测大小
        update_predicted_size()
        
        # 按钮框架
        button_frame = ttk.Frame(settings_window)
        button_frame.pack(fill=tk.X, padx=10, pady=10)
        
        def apply_settings():
            # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
            if is_png and smart_compress_var.get():
                final_format = "JPEG"
            else:
                final_format = format_var.get()
            
            # 更新图片设置
            self.image_data[index]['quality'] = quality_var.get()
            self.image_data[index]['format'] = final_format
            self.image_data[index]['custom'] = True
            self.image_data[index]['pdf_export'] = pdf_var.get()
            self.image_data[index]['smart_compress'] = smart_compress_var.get()
            self.image_data[index]['custom_target_size'] = custom_size_var.get() * 1024  # 转换为字节
            
            # 更新预测大小
            new_predicted_size = update_predicted_size()
            self.image_data[index]['predicted_size'] = new_predicted_size
            
            # 更新Treeview显示
            current_item = self.tree.get_children()[index]
            self.tree.set(current_item, "pdf_export", "☑" if pdf_var.get() else "☐")
            self.tree.set(current_item, "smart_compress", "☑" if smart_compress_var.get() else "☐")
            self.tree.set(current_item, "quality", f"{quality_var.get()}%")
            self.tree.set(current_item, "predicted_size", self.format_file_size(new_predicted_size))
            
            # 更新总大小信息
            self.update_size_info()
            settings_window.destroy()
        
        ttk.Button(button_frame, text="应用", command=apply_settings).pack(side=tk.RIGHT, padx=5)
        ttk.Button(button_frame, text="取消", command=settings_window.destroy).pack(side=tk.RIGHT, padx=5)
   
    def start_compression(self):
        if not self.image_data:
            messagebox.showwarning("警告", "请先选择图片!")
            return
        
        # 检查是否有图片被选中导出PDF
        pdf_images = [info for info in self.image_data if info.get('pdf_export', False)]
        regular_images = [info for info in self.image_data if not info.get('pdf_export', False)]
        
        if not pdf_images and not regular_images:
            messagebox.showwarning("警告", "没有需要处理的图片!")
            return
        
        output_dir = filedialog.askdirectory(title="选择输出目录")
        if not output_dir:
            return
        
        # 在新线程中执行压缩
        thread = threading.Thread(
            target=self.compress_images,
            args=(output_dir, pdf_images, regular_images)
        )
        thread.daemon = True
        thread.start()
   
    def compress_images(self, output_dir, pdf_images, regular_images):
        """处理图片压缩和PDF生成"""
        total_tasks = len(pdf_images) + len(regular_images)
        if total_tasks == 0:
            return
        
        # 在GUI线程中更新按钮状态
        self.root.after(0, lambda: self.compress_btn.config(state=tk.DISABLED))
        self.progress['maximum'] = total_tasks
        current_progress = 0
        
        # 1. 先处理常规图片压缩
        if regular_images:
            self.root.after(0, lambda: self.status_var.set("开始压缩图片..."))
            
            for i, image_info in enumerate(regular_images):
                try:
                    image_path = image_info['path']
                    # 在GUI线程中更新状态
                    self.root.after(0, lambda: self.status_var.set(f"正在压缩: {Path(image_path).name}"))
                    
                    # 打开图片
                    with Image.open(image_path) as img:
                        # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
                        if Path(image_path).suffix.lower() == '.png' and image_info.get('smart_compress', False):
                            image_info['format'] = "JPEG"
                        
                        output_path = self.get_output_path(image_path, output_dir, image_info['format'])
                        
                        # 根据选择的格式保存
                        if image_info['format'] == "保持原格式":
                            format = None
                        else:
                            format = image_info['format']
                        
                        quality = image_info['quality']
                        
                        # 检查是否需要智能压缩
                        smart_compress = image_info.get('smart_compress', False)
                        custom_target_size = image_info.get('custom_target_size', 102400)
                        
                        if smart_compress:
                            # 使用智能压缩算法
                            compressed_img, actual_size = self.smart_compress_with_quality_adjustment(
                                img, image_info['format'], custom_target_size, output_path
                            )
                        else:
                            # 对于PNG格式,使用压缩级别
                            if format == "PNG" or (format is None and Path(image_path).suffix.lower() in ['.png']):
                                compress_level = self.quality_to_png_compress_level(quality)
                                img.save(output_path, format='PNG', compress_level=compress_level)
                            else:
                                # 如果是JPEG格式且图片是RGBA模式,转换为RGB
                                if (format == "JPEG" or (format is None and
                                    Path(image_path).suffix.lower() in ['.jpeg', '.jpg'])) and img.mode == 'RGBA':
                                    img = img.convert('RGB')
                                # 对于100%质量,不使用优化选项
                                if quality == 100:
                                    img.save(output_path, format=format, quality=quality, optimize=False)
                                else:
                                    img.save(output_path, format=format, quality=quality, optimize=True)
                        
                        # 获取压缩后文件大小
                        compressed_size = os.path.getsize(output_path)
                        image_info['compressed_size'] = compressed_size
                        
                        # 更新Treeview
                        for idx, data in enumerate(self.image_data):
                            if data['path'] == image_path:
                                item = self.tree.get_children()[idx]
                                self.root.after(0, lambda item=item, compressed_size=compressed_size: self.tree.set(item, "compressed_size", self.format_file_size(compressed_size)))
                                break
                                
                except Exception as e:
                    print(f"处理图片 {image_path} 时出错: {str(e)}")
               
                # 更新进度条
                current_progress += 1
                self.progress['value'] = current_progress
                self.root.update_idletasks()
        
        # 2. 再处理PDF生成 - 使用img2pdf库
        if pdf_images:
            self.root.after(0, lambda: self.status_var.set("开始生成PDF..."))
            
            try:
                # 使用img2pdf生成PDF
                self.generate_pdf_with_img2pdf(pdf_images, output_dir)
            except Exception as e:
                print(f"生成PDF时出错: {e}")
                self.root.after(0, lambda: self.status_var.set("PDF生成失败"))
        
        # 在GUI线程中更新状态
        success_count = len(regular_images) + (1 if pdf_images and len(pdf_images) > 0 else 0)
        self.root.after(0, lambda: self.status_var.set(f"处理完成! 图片: {len(regular_images)}, PDF: {1 if pdf_images else 0}"))
        self.root.after(0, lambda: self.compress_btn.config(state=tk.NORMAL))
        
        # 更新大小信息
        self.root.after(0, self.update_size_info)
        
        # 显示完成消息
        message = f"处理完成!\n"
        if regular_images:
            message += f"压缩图片: {len(regular_images)} 张\n"
        if pdf_images:
            message += f"生成PDF: 1 个 (包含 {len(pdf_images)} 张图片)"
        
        self.root.after(0, lambda: messagebox.showinfo("完成", message))
        
        # 打开输出目录
        try:
            if sys.platform == "win32":
                os.startfile(output_dir)
            elif sys.platform == "darwin":
                os.system(f'open "{output_dir}"')
            else:
                os.system(f'xdg-open "{output_dir}"')
        except:
            pass  # 如果打开目录失败,忽略错误
   
    def generate_pdf_with_img2pdf(self, pdf_images, output_dir):
        """使用img2pdf库生成PDF文件"""
        try:
            # 生成不重复的PDF文件名
            base_name = "compressed_images"
            pdf_path = os.path.join(output_dir, base_name + ".pdf")
            counter = 1
            while os.path.exists(pdf_path):
                pdf_path = os.path.join(output_dir, f"{base_name}_{counter}.pdf")
                counter += 1
            
            # 创建临时目录存储压缩后的图片
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_files = []  # 存储临时文件路径和大小
               
                # 先压缩所有图片到临时文件
                for i, image_info in enumerate(pdf_images):
                    image_path = image_info['path']
                    self.root.after(0, lambda: self.status_var.set(f"正在准备PDF图片: {Path(image_path).name}"))
                    
                    with Image.open(image_path) as img:
                        # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
                        if Path(image_path).suffix.lower() == '.png' and image_info.get('smart_compress', False):
                            image_info['format'] = "JPEG"
                        
                        # 临时文件路径 - 使用jpg格式,因为img2pdf对jpg支持最好
                        temp_path = os.path.join(temp_dir, f"temp_{i}.jpg")
                        
                        # 检查是否需要智能压缩
                        smart_compress = image_info.get('smart_compress', False)
                        custom_target_size = image_info.get('custom_target_size', 102400)
                        
                        if smart_compress:
                            # 使用智能压缩
                            compressed_img, actual_size = self.smart_compress_with_quality_adjustment(
                                img, "JPEG", custom_target_size, temp_path
                            )
                        else:
                            # 普通压缩
                            quality = image_info.get('quality', self.quality_var.get())
                           
                            # 确保图片是RGB模式(PDF需要)
                            if img.mode != 'RGB':
                                img = img.convert('RGB')
                           
                            # 保存为JPEG格式(img2pdf对JPEG支持最好)
                            img.save(temp_path, format='JPEG', quality=quality, optimize=True)
                        
                        # 记录临时文件大小
                        temp_size = os.path.getsize(temp_path)
                        temp_files.append(temp_path)
                        
                        # 更新进度条
                        self.progress['value'] += 1
                        self.root.update_idletasks()
               
                # 使用img2pdf创建PDF
                if temp_files:
                    # 使用img2pdf将图片列表转换为PDF
                    with open(pdf_path, "wb") as pdf_file:
                        pdf_file.write(img2pdf.convert(temp_files))
                    
                    # 获取PDF文件总大小
                    pdf_total_size = os.path.getsize(pdf_path)
                    
                    # 计算每张图片在PDF中的大小(按压缩后图片大小比例分配)
                    total_compressed_size = sum(os.path.getsize(f) for f in temp_files)
                    
                    for i, (image_info, temp_file) in enumerate(zip(pdf_images, temp_files)):
                        if total_compressed_size > 0:
                            # 按每张图片压缩后的大小比例分配PDF总大小
                            temp_size = os.path.getsize(temp_file)
                            ratio = temp_size / total_compressed_size
                            image_info['compressed_size'] = int(pdf_total_size * ratio)
                        else:
                            # 如果无法计算比例,平均分配
                            image_info['compressed_size'] = pdf_total_size // len(pdf_images)
                        
                        # 更新Treeview显示
                        for idx, data in enumerate(self.image_data):
                            if data['path'] == image_info['path']:
                                item = self.tree.get_children()[idx]
                                compressed_size = data['compressed_size']
                                self.root.after(0, lambda item=item, compressed_size=compressed_size:
                                              self.tree.set(item, "compressed_size", self.format_file_size(compressed_size)))
                                break
               
                self.root.after(0, lambda: self.status_var.set("PDF生成完成"))
               
        except Exception as e:
            print(f"使用img2pdf生成PDF时出错: {e}")
            # 如果img2pdf不可用或出错,回退到原来的方法
            if not IMG2PDF_AVAILABLE:
                self.root.after(0, lambda: messagebox.showwarning("警告", "img2pdf库未安装,使用Pillow备用方案生成PDF"))
                self.generate_pdf_with_accurate_sizes(pdf_images, output_dir)
            else:
                self.root.after(0, lambda: self.status_var.set("PDF生成失败"))
                raise
   
    def generate_pdf_with_accurate_sizes(self, pdf_images, output_dir):
        """备用PDF生成方法,使用Pillow(当img2pdf不可用时使用)"""
        try:
            # 生成不重复的PDF文件名
            base_name = "compressed_images"
            pdf_path = os.path.join(output_dir, base_name + ".pdf")
            counter = 1
            while os.path.exists(pdf_path):
                pdf_path = os.path.join(output_dir, f"{base_name}_{counter}.pdf")
                counter += 1
            
            # 创建临时目录存储压缩后的图片
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_files = []  # 存储临时文件路径和大小
               
                # 先压缩所有图片到临时文件
                for i, image_info in enumerate(pdf_images):
                    image_path = image_info['path']
                    self.root.after(0, lambda: self.status_var.set(f"正在准备PDF图片: {Path(image_path).name}"))
                    
                    with Image.open(image_path) as img:
                        # 如果是PNG图片且勾选了智能压缩,则强制使用JPEG格式
                        if Path(image_path).suffix.lower() == '.png' and image_info.get('smart_compress', False):
                            image_info['format'] = "JPEG"
                        
                        # 临时文件路径
                        temp_path = os.path.join(temp_dir, f"temp_{i}.jpg")
                        
                        # 检查是否需要智能压缩
                        smart_compress = image_info.get('smart_compress', False)
                        custom_target_size = image_info.get('custom_target_size', 102400)
                        
                        if smart_compress:
                            # 使用智能压缩
                            compressed_img, actual_size = self.smart_compress_with_quality_adjustment(
                                img, "JPEG", custom_target_size, temp_path
                            )
                        else:
                            # 普通压缩
                            quality = image_info.get('quality', self.quality_var.get())
                            format_setting = image_info.get('format', self.format_var.get())
                           
                            # 确定输出格式
                            if format_setting == "保持原格式":
                                output_format = 'PNG' if img.format == 'PNG' else 'JPEG'
                            elif format_setting == "JPEG":
                                output_format = 'JPEG'
                            elif format_setting == "PNG":
                                output_format = 'PNG'
                            elif format_setting == "WEBP":
                                output_format = 'WEBP'
                            else:
                                output_format = 'JPEG'
                           
                            # 应用压缩设置
                            if output_format == 'PNG':
                                compress_level = self.quality_to_png_compress_level(quality)
                                img.save(temp_path, format='PNG', compress_level=compress_level)
                            else:
                                # 确保图片是RGB模式(PDF需要)
                                if img.mode != 'RGB':
                                    img = img.convert('RGB')
                                img.save(temp_path, format='JPEG', quality=quality, optimize=True)
                        
                        # 记录临时文件大小
                        temp_size = os.path.getsize(temp_path)
                        temp_files.append((temp_path, temp_size))
                        
                        # 更新进度条
                        self.progress['value'] += 1
                        self.root.update_idletasks()
               
                # 使用临时文件创建PDF
                if temp_files:
                    # 打开所有临时图片
                    images = []
                    for temp_path, temp_size in temp_files:
                        img = Image.open(temp_path)
                        # 确保所有图片都是RGB模式
                        if img.mode != 'RGB':
                            img = img.convert('RGB')
                        images.append(img)
                    
                    # 保存为PDF
                    if images:
                        images[0].save(
                            pdf_path,
                            "PDF",
                            resolution=100.0,
                            save_all=True,
                            append_images=images[1:]
                        )
                    
                    # 获取PDF文件总大小
                    pdf_total_size = os.path.getsize(pdf_path)
                    
                    # 计算每张图片在PDF中的大小(按压缩后图片大小比例分配)
                    total_compressed_size = sum(size for _, size in temp_files)
                    
                    for i, (image_info, (temp_path, temp_size)) in enumerate(zip(pdf_images, temp_files)):
                        if total_compressed_size > 0:
                            # 按每张图片压缩后的大小比例分配PDF总大小
                            ratio = temp_size / total_compressed_size
                            image_info['compressed_size'] = int(pdf_total_size * ratio)
                        else:
                            # 如果无法计算比例,平均分配
                            image_info['compressed_size'] = pdf_total_size // len(pdf_images)
                        
                        # 更新Treeview显示
                        for idx, data in enumerate(self.image_data):
                            if data['path'] == image_info['path']:
                                item = self.tree.get_children()[idx]
                                compressed_size = data['compressed_size']
                                self.root.after(0, lambda item=item, compressed_size=compressed_size:
                                              self.tree.set(item, "compressed_size", self.format_file_size(compressed_size)))
                                break
               
                self.root.after(0, lambda: self.status_var.set("PDF生成完成"))
               
        except Exception as e:
            print(f"生成PDF时出错: {e}")
            self.root.after(0, lambda: self.status_var.set("PDF生成失败"))
            raise
   
    def smart_compress_with_quality_adjustment(self, img, format_setting, target_size_bytes, output_path=None):
        """
        智能压缩算法 - 按照要求重新定义逻辑
        1. 先检测像素,若大于1080*1920,则先缩放至这个大小以下
        2. 然后监测临时文件图片大小,从高到低调整压缩质量
        3. 直到图片刚好小于自定义值,不超过5kb(95kb-100kb之间)
        """
        # 定义目标大小范围
        max_target = target_size_bytes  # 100KB
        min_target = target_size_bytes - 5 * 1024  # 95KB
        
        # 步骤1: 检测图片尺寸,如果大于1080x1920则缩放
        original_width, original_height = img.size
        max_width, max_height = 1080, 1920
        
        if original_width > max_width or original_height > max_height:
            # 计算缩放比例,保持宽高比
            width_ratio = max_width / original_width
            height_ratio = max_height / original_height
            scale_ratio = min(width_ratio, height_ratio)
            
            new_width = int(original_width * scale_ratio)
            new_height = int(original_height * scale_ratio)
            
            # 使用高质量缩放
            img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
            print(f"图片已缩放: {original_width}x{original_height} -> {new_width}x{new_height}")
        
        # 确定输出格式
        if format_setting == "保持原格式":
            # 如果原图是PNG,则输出PNG;否则输出JPEG
            output_format = 'PNG' if img.format == 'PNG' else 'JPEG'
        elif format_setting == "JPEG":
            output_format = 'JPEG'
        elif format_setting == "PNG":
            output_format = 'PNG'
        elif format_setting == "WEBP":
            output_format = 'WEBP'
        else:
            output_format = 'JPEG'  # 默认
        
        # 步骤2: 从高质量到低质量逐步调整,找到合适的大小
        best_quality = None
        best_size = None
        best_buffer = None
        
        # 质量调整范围:从85开始逐步降低,直到找到合适的大小
        for quality in range(85, 9, -1):  # 从85到10,每次减少1(加快搜索速度)
            buffer = io.BytesIO()
            try:
                # 根据格式保存图片到内存缓冲区
                if output_format == 'PNG':
                    compress_level = self.quality_to_png_compress_level(quality)
                    img.save(buffer, format='PNG', compress_level=compress_level)
                else:
                    # 对于非PNG格式,需要确保图片是RGB模式
                    temp_img = img
                    if output_format in ['JPEG', 'WEBP'] and temp_img.mode in ('RGBA', 'LA', 'P'):
                        temp_img = temp_img.convert('RGB')
                    
                    # 使用明确的格式保存
                    if output_format == 'JPEG':
                        temp_img.save(buffer, format='JPEG', quality=quality, optimize=True)
                    elif output_format == 'WEBP':
                        temp_img.save(buffer, format='WEBP', quality=quality, optimize=True)
                    else:
                        temp_img.save(buffer, format='JPEG', quality=quality, optimize=True)
               
                current_size = buffer.tell()
                print(f"质量 {quality}% -> 大小: {current_size} 字节 (目标: {min_target}-{max_target})")
               
                # 检查是否在目标范围内
                if min_target  best_size):
                    # 记录最接近但不超过最小目标的值
                    best_quality = quality
                    best_size = current_size
                    best_buffer = buffer
                # 如果当前大小大于最大目标,继续尝试更低质量
                    
            except Exception as e:
                print(f"在质量 {quality}% 下压缩失败: {e}")
                continue
        
        # 如果找到了合适的大小
        if best_buffer is not None:
            print(f"最终使用质量: {best_quality}%, 文件大小: {best_size} 字节")
            best_buffer.seek(0)
            best_img = Image.open(best_buffer)
            
            if output_path:
                # 保存最终图片
                if output_format == 'PNG':
                    compress_level = self.quality_to_png_compress_level(best_quality)
                    best_img.save(output_path, format='PNG', compress_level=compress_level)
                else:
                    # 确保图片是RGB模式
                    if output_format in ['JPEG', 'WEBP'] and best_img.mode in ('RGBA', 'LA', 'P'):
                        best_img = best_img.convert('RGB')
                    
                    if output_format == 'JPEG':
                        best_img.save(output_path, format='JPEG', quality=best_quality, optimize=True)
                    elif output_format == 'WEBP':
                        best_img.save(output_path, format='WEBP', quality=best_quality, optimize=True)
                    else:
                        best_img.save(output_path, format='JPEG', quality=best_quality, optimize=True)
               
                # 验证最终文件大小
                final_size = os.path.getsize(output_path)
                return best_img, final_size
            else:
                return best_img, best_size
        
        # 如果所有质量都尝试过了还是没有找到合适的大小,则使用最低质量
        print("所有质量尝试失败,使用最低质量备选方案")
        buffer = io.BytesIO()
        if output_format == 'PNG':
            compress_level = self.quality_to_png_compress_level(10)
            img.save(buffer, format='PNG', compress_level=compress_level)
        else:
            temp_img = img
            if output_format in ['JPEG', 'WEBP'] and temp_img.mode in ('RGBA', 'LA', 'P'):
                temp_img = temp_img.convert('RGB')
            
            if output_format == 'JPEG':
                temp_img.save(buffer, format='JPEG', quality=10, optimize=True)
            elif output_format == 'WEBP':
                temp_img.save(buffer, format='WEBP', quality=10, optimize=True)
            else:
                temp_img.save(buffer, format='JPEG', quality=10, optimize=True)
        
        final_size = buffer.tell()
        buffer.seek(0)
        best_img = Image.open(buffer)
        
        if output_path:
            if output_format == 'PNG':
                compress_level = self.quality_to_png_compress_level(10)
                best_img.save(output_path, format='PNG', compress_level=compress_level)
            else:
                if output_format in ['JPEG', 'WEBP'] and best_img.mode in ('RGBA', 'LA', 'P'):
                    best_img = best_img.convert('RGB')
               
                if output_format == 'JPEG':
                    best_img.save(output_path, format='JPEG', quality=10, optimize=True)
                elif output_format == 'WEBP':
                    best_img.save(output_path, format='WEBP', quality=10, optimize=True)
                else:
                    best_img.save(output_path, format='JPEG', quality=10, optimize=True)
        
        return best_img, final_size
   
    def get_output_path(self, input_path, output_dir, format_setting):
        path = Path(input_path)
        name = path.stem
        suffix = path.suffix
        
        # 根据选择的格式确定输出后缀
        if format_setting == "保持原格式":
            output_suffix = suffix
        elif format_setting == "JPEG":
            output_suffix = ".jpg"
        elif format_setting == "PNG":
            output_suffix = ".png"
        elif format_setting == "WEBP":
            output_suffix = ".webp"
        else:
            output_suffix = suffix
        
        # 构建输出路径
        output_path = Path(output_dir) / f"{name}_compressed{output_suffix}"
        
        # 如果文件已存在,添加数字后缀
        counter = 1
        original_output_path = output_path
        while output_path.exists():
            output_path = original_output_path.parent / f"{original_output_path.stem}_{counter}{original_output_path.suffix}"
            counter += 1
        
        return output_path
   
    def update_size_info(self):
        """更新总大小信息"""
        total_original = sum(info['size'] for info in self.image_data)
        total_predicted = sum(info['predicted_size'] for info in self.image_data)
        total_compressed = sum(info['compressed_size'] or 0 for info in self.image_data)
        
        self.total_original_size.config(text=f"原图总大小: {self.format_file_size(total_original)}")
        self.total_predicted_size.config(text=f"预测总大小: {self.format_file_size(total_predicted)}")
        
        if total_compressed > 0:
            self.total_compressed_size.config(text=f"压缩后总大小: {self.format_file_size(total_compressed)}")
            # 计算压缩率
            if total_original > 0:
                ratio = (1 - total_compressed / total_original) * 100
                self.compression_ratio.config(text=f"压缩率: {ratio:.1f}%")
            else:
                self.compression_ratio.config(text="压缩率: 0%")
        else:
            self.total_compressed_size.config(text="压缩后总大小: 0 B")
            # 计算预测压缩率
            if total_original > 0:
                ratio = (1 - total_predicted / total_original) * 100
                self.compression_ratio.config(text=f"预测压缩率: {ratio:.1f}%")
            else:
                self.compression_ratio.config(text="压缩率: 0%")
   
    def run(self):
        self.root.mainloop()
if __name__ == "__main__":
    # 检查PIL是否安装
    try:
        from PIL import Image
    except ImportError:
        print("请先安装Pillow库: pip install Pillow")
        exit(1)
   
    app = ImageCompressor()
    app.run()

图片, 大小

baihedengge   

坐等lanzou哥哥
woaipojie23456   

这个好  实用
wyl0205   

楼主,版规里规定AI参与编写的,禁止发原创区的,申请迁移吧。小心领个违规
NoWshun   

实用,学习了
zhengzaixing153   

确实实用,感谢分享
尾翼   

经常会批量压缩证件照到指定大小,稍后下载来试试,非常感谢
天地人2019   

请问pdf导出是每一张图片导出到一个同名pdf,还是所有图片导出到一个pdf里面?
xy249125   

太厉害了,我的哥
您需要登录后才可以回帖 登录 | 立即注册