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()
相关推荐
青 春 记 忆2 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO32 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
南风微微吹2 小时前
计算机二级MS Office历年真题试题及答案解析40套电子版PDF(含操作题和选择题)
pdf
临沂GEO4 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白4 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
美狐美颜SDK开放平台5 小时前
视频美颜SDK是什么?直播APP开发中美颜功能实现方式介绍
深度学习·架构·实时互动·音视频·视频美颜sdk
荷蒲6 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
柳絮飞祭奠6 小时前
Dify Windows Docker Desktop 部署文档
人工智能·深度学习·语言模型·数据分析·transformer·边缘计算·集成学习
咖啡星人k6 小时前
2026 AI Agent 长期记忆:为什么 AI 助手总“聊完就忘“?MonkeyCode 免费上手
人工智能·深度学习·机器学习·语言模型·自然语言处理
数据狐(Datafox)7 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json