PDF瘦身,告别WPS收费压缩PDF

使用方法

python pdf_compressor.py -i ".\11" -o ".\12"

或者下载exe版本pdf_compressor.exe -i ".\11" -o ".\12"

其中11这个文件夹是原PDF所在目录,12是压缩后PDF所在目录,文件夹你们也可以用别的,意思对了就行
EXE版本在这里下载

pdf_compressor.py内容如下

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PDF压缩工具
功能:将PDF文件压缩至900KB以内
"""

import os
import sys
import argparse
from tqdm import tqdm
from PIL import Image
import io
import tempfile

def compress_pdf(input_path, output_path, target_size_kb=900):
    """
    压缩PDF文件至目标大小以下
    
    Args:
        input_path (str): 输入PDF文件路径
        output_path (str): 输出PDF文件路径
        target_size_kb (int): 目标文件大小(KB)
        
    Returns:
        bool: 压缩是否成功
    """
    try:
        # 检查输入文件是否存在
        if not os.path.exists(input_path):
            print(f"错误:找不到输入文件 {input_path}")
            return False
            
        # 检查文件类型是否为PDF
        if not input_path.lower().endswith('.pdf'):
            print(f"错误:文件 {input_path} 不是PDF文件")
            return False
        
        # 获取原始文件大小
        original_size = os.path.getsize(input_path) / 1024  # KB
        print(f"原始文件大小: {original_size:.2f} KB")
        
        # 如果原始文件已经小于目标大小,直接复制
        if original_size <= target_size_kb:
            print(f"文件已经小于目标大小,无需压缩")
            import shutil
            shutil.copy2(input_path, output_path)
            return True
        
        # 压缩策略1: 使用PyPDF2进行基础压缩
        try:
            from PyPDF2 import PdfReader, PdfWriter
            
            reader = PdfReader(input_path)
            writer = PdfWriter()
            
            # 添加所有页面并压缩内容流
            for page in tqdm(reader.pages, desc="基础压缩处理"):
                page.compress_content_streams()  # 压缩页面内容
                writer.add_page(page)
            
            # 写入临时文件
            temp_output = output_path + ".temp"
            with open(temp_output, "wb") as f:
                writer.write(f)
            
            # 检查压缩后大小
            compressed_size = os.path.getsize(temp_output) / 1024  # KB
            
            if compressed_size <= target_size_kb:
                os.replace(temp_output, output_path)
                print(f"PyPDF2压缩成功!压缩后大小: {compressed_size:.2f} KB")
                return True
            else:
                print(f"PyPDF2压缩后大小: {compressed_size:.2f} KB,继续尝试更高级压缩")
                os.remove(temp_output)
                
        except Exception as e:
            print(f"PyPDF2压缩失败: {e}")
        
        # 压缩策略2: 使用pikepdf进行更高级压缩
        try:
            import pikepdf
            from pikepdf import Pdf
            
            # 尝试不同级别的压缩
            compression_strategies = [
                # 策略1: 基本压缩
                {'desc': '基本压缩', 'compress_streams': True},
                # 策略2: 中等压缩
                {'desc': '中等压缩', 'compress_streams': True, 'normalize_content': True},
                # 策略3: 高级压缩
                {'desc': '高级压缩', 'compress_streams': True, 'object_stream_mode': 'generate'},  
            ]
            
            for strategy in compression_strategies:
                print(f"尝试{pikepdf.__name__} {strategy['desc']}")
                
                pdf = pikepdf.open(input_path)
                # 简化pikepdf参数,避免不支持的参数
                pdf.save(output_path, compress_streams=True)  # 仅使用基础压缩参数
                
                compressed_size = os.path.getsize(output_path) / 1024  # KB
                
                if compressed_size <= target_size_kb:
                    print(f"压缩成功!压缩后大小: {compressed_size:.2f} KB")
                    return True
                else:
                    print(f"当前压缩后大小: {compressed_size:.2f} KB,继续尝试...")
                    
        except Exception as e:
            print(f"pikepdf压缩失败: {e}")
        
        # 压缩策略3: 使用ghostscript进行强力压缩(如果可用)
        try:
            import subprocess
            import shutil
            
            # 检查ghostscript是否安装
            gs_path = shutil.which('gswin64c') or shutil.which('gswin32c') or shutil.which('gs')
            
            if gs_path:
                print("尝试使用Ghostscript进行强力压缩")
                
                # 定义不同的压缩级别
                gs_levels = [
                    {'desc': '屏幕质量', 'settings': ['-dPDFSETTINGS=/screen', '-dDownsampleColorImages=true', '-dColorImageResolution=72']},
                    {'desc': ' ebook质量', 'settings': ['-dPDFSETTINGS=/ebook', '-dDownsampleColorImages=true', '-dColorImageResolution=150']},
                    {'desc': '打印机质量', 'settings': ['-dPDFSETTINGS=/printer', '-dDownsampleColorImages=true', '-dColorImageResolution=300']},
                    {'desc': '最低质量', 'settings': ['-dPDFSETTINGS=/screen', '-dDownsampleColorImages=true', '-dColorImageResolution=30', '-dCompressFonts=true']},
                ]
                
                for level in gs_levels:
                    print(f"尝试Ghostscript {level['desc']}压缩")
                    
                    # 构建命令
                    cmd = [
                        gs_path,
                        '-sDEVICE=pdfwrite',
                        '-dNOPAUSE',
                        '-dBATCH',
                        '-dQUIET',
                        '-dCompatibilityLevel=1.4',
                        '-dEmbedAllFonts=true',
                        '-dSubsetFonts=true',
                        '-sOutputFile=' + output_path,
                    ] + level['settings'] + [input_path]
                    
                    # 执行命令
                    try:
                        subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                        
                        compressed_size = os.path.getsize(output_path) / 1024  # KB
                        
                        if compressed_size <= target_size_kb:
                            print(f"Ghostscript压缩成功!压缩后大小: {compressed_size:.2f} KB")
                            return True
                        else:
                            print(f"Ghostscript当前压缩后大小: {compressed_size:.2f} KB,继续尝试...")
                    except subprocess.CalledProcessError as e:
                        print(f"Ghostscript命令执行失败: {e}")
            else:
                print("未找到Ghostscript,跳过此压缩方法")
                print("提示:安装Ghostscript可获得更好的压缩效果: https://www.ghostscript.com/")
                
        except Exception as e:
            print(f"Ghostscript压缩过程出错: {e}")
        
        # 图像压缩函数
        def compress_image(img_data, quality=85):
            """压缩图像数据,降低质量和分辨率"""
            try:
                # 打开图像
                img = Image.open(io.BytesIO(img_data))
                
                # 根据质量级别调整不同的压缩策略
                if quality <= 30:  # 低质量
                    max_size = 768  # 最大边长
                elif quality <= 50:  # 中等质量
                    max_size = 896  # 最大边长
                else:  # 高质量
                    max_size = 1024  # 最大边长
                
                # 如果图像很大,降低分辨率
                if max(img.size) > max_size:
                    ratio = max_size / max(img.size)
                    new_size = tuple(int(dim * ratio) for dim in img.size)
                    img = img.resize(new_size, Image.LANCZOS)
                
                # 保存为压缩后的图像,使用更高效的压缩选项
                output = io.BytesIO()
                img_format = img.format if img.format else 'JPEG'
                
                # 针对不同质量级别使用不同的压缩参数
                if quality <= 20:  # 最低质量,使用最高压缩率
                    img.save(output, format=img_format, quality=quality, optimize=True, progressive=True)
                else:  # 其他质量级别
                    img.save(output, format=img_format, quality=quality, optimize=True)
                
                # 获取压缩后的数据
                compressed_data = output.getvalue()
                
                # 如果是彩色图像,尝试转换为RGB模式以减少颜色空间
                if img.mode == 'RGBA' and quality <= 50:
                    # 转换为RGB(忽略透明度)
                    img_rgb = img.convert('RGB')
                    output_rgb = io.BytesIO()
                    img_rgb.save(output_rgb, format='JPEG', quality=quality, optimize=True)
                    rgb_data = output_rgb.getvalue()
                    # 如果转换后更小,使用RGB版本
                    if len(rgb_data) < len(compressed_data):
                        compressed_data = rgb_data
                
                return compressed_data
            except Exception:
                # 如果压缩失败,返回原始数据
                return img_data
                
        # 压缩策略4: 作为最后手段,尝试非常激进的压缩(使用PIL深度压缩图像)
        try:
            print("尝试最终激进压缩方案...")
            
            # 这个方法会尝试提取并压缩所有图像,然后重建PDF
            with open(input_path, 'rb') as file:
                from PyPDF2 import PdfReader, PdfWriter
                reader = PdfReader(file)
                writer = PdfWriter()
                
                # 压缩级别递减的质量参数
                quality_levels = [70, 50, 30]
                success = False
                
                for quality in quality_levels:
                    writer = PdfWriter()  # 重置writer
                    
                    # 复制所有页面并尝试压缩图像
                    for page_num in range(len(reader.pages)):
                        page = reader.pages[page_num]
                        
                        # 不再移除可能包含重要功能的属性
                        # 保留页面的交互功能和脚本
                        
                        # 尝试处理页面中的图像
                        resources = page.get('/Resources', {})
                        if '/XObject' in resources:
                            xobjects = resources['/XObject'].get_object()
                            if xobjects:
                                for obj_name in list(xobjects.keys()):
                                    obj = xobjects[obj_name]
                                    if obj.get('/Subtype') == '/Image':
                                        try:
                                            # 尝试提取和压缩图像数据
                                            if '/Filter' in obj:
                                                # 获取图像数据
                                                img_data = obj._data if hasattr(obj, '_data') else b''
                                                # 压缩图像
                                                compressed_data = compress_image(img_data, quality=quality)
                                                # 替换图像数据
                                                if hasattr(obj, '_data'):
                                                    obj._data = compressed_data
                                        except Exception:
                                            # 如果处理失败,跳过此图像
                                            continue
                        
                        writer.add_page(page)
                    
                    # 保存当前质量级别的压缩结果
                    with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
                        temp_path = temp_file.name
                        writer.write(temp_file)
                    
                    # 检查当前压缩结果大小
                    current_size = os.path.getsize(temp_path) / 1024
                    print(f"尝试质量级别 {quality}%: {current_size:.2f} KB")
                    
                    if current_size <= target_size_kb:
                        # 如果达到目标大小,复制到最终输出
                        import shutil
                        shutil.copy2(temp_path, output_path)
                        os.unlink(temp_path)  # 删除临时文件
                        success = True
                        break
                    else:
                        # 否则继续尝试更低的质量级别
                        os.unlink(temp_path)  # 删除当前临时文件
                
                # 如果所有质量级别都尝试过但还是太大,使用最低质量级别再次尝试
                if not success:
                    print("所有质量级别都尝试过,使用最低质量级别(20%)进行最终压缩")
                    writer = PdfWriter()
                    
                    for page_num in range(len(reader.pages)):
                        page = reader.pages[page_num]
                        
                        # 尝试处理页面中的图像
                        resources = page.get('/Resources', {})
                        if '/XObject' in resources:
                            xobjects = resources['/XObject'].get_object()
                            if xobjects:
                                for obj_name in list(xobjects.keys()):
                                    obj = xobjects[obj_name]
                                    if obj.get('/Subtype') == '/Image':
                                        try:
                                            # 尝试提取和压缩图像数据
                                            if '/Filter' in obj:
                                                # 获取图像数据
                                                img_data = obj._data if hasattr(obj, '_data') else b''
                                                # 使用最低质量进行压缩
                                                compressed_data = compress_image(img_data, quality=20)
                                                # 替换图像数据
                                                if hasattr(obj, '_data'):
                                                    obj._data = compressed_data
                                        except Exception:
                                            # 如果处理失败,跳过此图像
                                            continue
                        
                        writer.add_page(page)
                    
                    # 保存最终压缩结果
                    with open(output_path, 'wb') as output_file:
                        writer.write(output_file)
                else:
                    # 如果已成功,直接使用成功的临时文件
                    pass
                
            # 检查最终大小
            final_size = os.path.getsize(output_path) / 1024  # KB
            print(f"最终压缩后大小: {final_size:.2f} KB")
            
            if final_size <= target_size_kb:
                print("激进压缩成功!")
                return True
            else:
                print(f"警告:已尝试所有压缩方法,但文件仍大于目标大小")
                print(f"建议:1. 安装Ghostscript以获得更好压缩\n2. 考虑将PDF拆分为多个文件\n3. 检查PDF是否包含大量高分辨率图片")
                return True  # 返回True因为文件已压缩,只是未达到理想大小
                
        except Exception as e:
            print(f"激进压缩失败: {e}")
            # 回退到原始的激进压缩方法
            try:
                # 尝试使用pikepdf压缩图像资源
                try:
                    import pikepdf
                    from pikepdf import Pdf
                    
                    # 创建一个新的PDF,尝试降低图像质量
                    pdf = pikepdf.open(input_path)
                    
                    # 尝试压缩所有图像资源
                    for i, page in enumerate(pdf.pages):
                        for name, obj in page.images.items():
                            try:
                                # 获取图像对象
                                img_obj = obj.obj
                                if img_obj.Subtype == pikepdf.Name('/Image'):
                                    # 移除可能的高质量解码参数
                                    if hasattr(img_obj, 'DecodeParms'):
                                        img_obj.DecodeParms = pikepdf.Dictionary()
                            except Exception:
                                # 跳过无法处理的图像
                                continue
                    
                    # 保存压缩后的PDF,使用压缩流
                    pdf.save(output_path, compress_streams=True)
                except Exception:
                    # 如果pikepdf图像压缩失败,回退到PyPDF2方法
                    pass
                
                # 检查压缩后大小,如果仍然太大,使用PyPDF2进行更激进的压缩
                current_size = os.path.getsize(output_path) / 1024 if os.path.exists(output_path) else float('inf')
                if current_size > target_size_kb:
                    # 使用PyPDF2进行保守压缩,保留所有重要内容
                    from PyPDF2 import PdfReader, PdfWriter
                    
                    reader = PdfReader(input_path)
                    writer = PdfWriter()
                    
                    # 添加所有页面内容,保留所有重要属性
                    for page in reader.pages:
                        # 压缩内容流但保留所有页面属性和功能
                        page.compress_content_streams()
                        writer.add_page(page)
                    
                    # 写入最终文件
                    with open(output_path, "wb") as f:
                        writer.write(f)
                
                # 检查最终大小
                final_size = os.path.getsize(output_path) / 1024  # KB
                print(f"回退方法压缩后大小: {final_size:.2f} KB")
                
                if final_size <= target_size_kb:
                    print("回退方法压缩成功!")
                    return True
                else:
                    print(f"警告:已尝试所有压缩方法,但文件仍大于目标大小")
                    print(f"建议:1. 安装Ghostscript以获得更好压缩\n2. 考虑将PDF拆分为多个文件\n3. 检查PDF是否包含大量高分辨率图片")
                    return True  # 返回True因为文件已压缩,只是未达到理想大小
                    
            except Exception as inner_e:
                print(f"回退压缩也失败: {inner_e}")
                return False
        
        # 如果所有方法都失败
        print("所有压缩方法均失败")
        return False
        
    except Exception as e:
        print(f"压缩过程中出错: {e}")
        return False

def batch_compress_pdfs(input_folder, output_folder, target_size_kb=900):
    """
    批量压缩文件夹中的所有PDF文件
    
    Args:
        input_folder (str): 输入文件夹路径
        output_folder (str): 输出文件夹路径
        target_size_kb (int): 目标文件大小(KB)
    """
    # 创建输出文件夹
    os.makedirs(output_folder, exist_ok=True)
    
    # 获取所有PDF文件
    pdf_files = [f for f in os.listdir(input_folder) if f.lower().endswith('.pdf')]
    
    if not pdf_files:
        print(f"错误:在文件夹 {input_folder} 中未找到PDF文件")
        return
    
    print(f"找到 {len(pdf_files)} 个PDF文件待处理")
    print(f"目标文件夹: {output_folder}")
    print("="*50)
    
    # 初始化统计信息
    success_count = 0
    failed_count = 0
    total_original_size = 0
    total_compressed_size = 0
    
    # 处理每个文件
    with tqdm(total=len(pdf_files), desc="总体进度", unit="文件") as pbar:
        for i, pdf_file in enumerate(pdf_files):
            input_path = os.path.join(input_folder, pdf_file)
            output_path = os.path.join(output_folder, pdf_file)
            
            # 获取原始文件大小
            original_size = os.path.getsize(input_path) / 1024  # KB
            total_original_size += original_size
            
            # 显示当前正在处理的文件信息
            tqdm.write(f"\n[{i+1}/{len(pdf_files)}] 正在处理: {pdf_file}")
            tqdm.write(f"原始大小: {original_size:.2f} KB")
            
            # 执行压缩
            if compress_pdf(input_path, output_path, target_size_kb):
                success_count += 1
                
                # 获取压缩后文件大小
                if os.path.exists(output_path):
                    compressed_size = os.path.getsize(output_path) / 1024  # KB
                    total_compressed_size += compressed_size
                    
                    # 计算压缩率
                    compression_ratio = (1 - compressed_size / original_size) * 100
                    tqdm.write(f"压缩状态: 成功 ✅ | 压缩率: {compression_ratio:.1f}%")
                else:
                    tqdm.write(f"压缩状态: 成功 ✅ | 但输出文件不存在")
            else:
                failed_count += 1
                tqdm.write(f"压缩状态: 失败 ❌")
            
            # 更新进度条
            pbar.update(1)
    
    # 计算总体统计信息
    print("\n" + "="*50)
    print("批量压缩完成!")
    print(f"成功: {success_count} 个文件")
    print(f"失败: {failed_count} 个文件")
    
    if total_original_size > 0:
        total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
        print(f"原始总大小: {total_original_size:.2f} KB")
        print(f"压缩总大小: {total_compressed_size:.2f} KB")
        print(f"总体压缩率: {total_compression_ratio:.1f}%")
        print(f"节省空间: {total_original_size - total_compressed_size:.2f} KB")
    
    print("="*50)

def main():
    """
    主函数,处理命令行参数
    """
    parser = argparse.ArgumentParser(description='PDF压缩工具 - 将PDF文件压缩至900KB以内')
    parser.add_argument('-i', '--input', required=True, help='输入PDF文件或文件夹路径')
    parser.add_argument('-o', '--output', help='输出路径(文件或文件夹)')
    parser.add_argument('-s', '--size', type=int, default=900, help='目标文件大小(KB),默认900KB')
    parser.add_argument('-f', '--force', action='store_true', help='强制使用所有可用的压缩方法')
    
    args = parser.parse_args()
    
    # 检查输入路径
    if not os.path.exists(args.input):
        print(f"错误:找不到输入路径 {args.input}")
        sys.exit(1)
    
    # 确定输出路径
    if not args.output:
        if os.path.isfile(args.input):
            # 如果输入是文件,输出文件名添加_compressed后缀
            base_name, ext = os.path.splitext(args.input)
            args.output = f"{base_name}_compressed{ext}"
        else:
            # 如果输入是文件夹,输出到compressed_pdfs子文件夹
            args.output = os.path.join(args.input, "compressed_pdfs")
    
    # 显示压缩配置
    print(f"压缩配置:")
    print(f"  输入: {args.input}")
    print(f"  输出: {args.output}")
    print(f"  目标大小: {args.size} KB")
    print()
    
    # 执行压缩
    if os.path.isfile(args.input):
        # 单个文件压缩
        compress_pdf(args.input, args.output, args.size)
    else:
        # 批量压缩
        batch_compress_pdfs(args.input, args.output, args.size)

if __name__ == "__main__":
    print("=== PDF压缩工具 ===")
    print("功能:将PDF文件压缩至指定大小以下")
    print("注意:请确保已安装必要的库: pip install PyPDF2 pikepdf tqdm")
    '''
    可选参数说明:
    - -o/--output :指定输出路径(文件或文件夹)
    - 如果不指定,对于单文件压缩,将在原文件名后添加_compressed后缀
    - 对于批量压缩,将在输入文件夹下创建compressed_pdfs子文件夹
    - -s/--size :指定目标文件大小(KB),默认900KB
    - -f/--force :强制使用所有可用的压缩方法
    '''
    print()
    main()
相关推荐
i***66504 小时前
SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
spring boot·后端·pdf
vvoennvv4 小时前
【Python TensorFlow】 TCN-LSTM时间序列卷积长短期记忆神经网络时序预测算法(附代码)
python·神经网络·机器学习·tensorflow·lstm·tcn
nix.gnehc4 小时前
PyTorch
人工智能·pytorch·python
小殊小殊5 小时前
【论文笔记】视频RAG-Vgent:基于图结构的视频检索推理框架
论文阅读·人工智能·深度学习
z***3355 小时前
SQL Server2022版+SSMS安装教程(保姆级)
后端·python·flask
I'm Jie5 小时前
从零开始学习 TOML,配置文件的新选择
python·properties·yaml·toml
hans汉斯5 小时前
【数据挖掘】基于深度学习的生产车间智能管控研究
人工智能·深度学习·数据挖掘
brave and determined5 小时前
可编程逻辑器件学习(day34):半导体编年史:从法拉第的意外发现到塑造现代文明的硅基浪潮
人工智能·深度学习·fpga开发·verilog·fpga·设计规范·嵌入式设计
二川bro6 小时前
Scikit-learn全流程指南:Python机器学习项目实战
python·机器学习·scikit-learn
代码的乐趣6 小时前
支持selenium的chrome driver更新到142.0.7444.175
chrome·python·selenium