乐于分享
好东西不私藏

Python 压缩 PDF,别一上来就把整页转成图片

Python 压缩 PDF,别一上来就把整页转成图片
脚本跑完没报错,PDF 也正常生成了,文件大小却几乎没动。

这种情况我一般不急着换库。先打开 PDF,用鼠标拖一下里面的文字:能选中,大概率是文本型 PDF;完全选不中,每一页看着都像照片,基本就是扫描件。

这两种文件,压缩方式不能混着来。

PDF 不是一张大图片,它更像一个容器,里面可能塞着文字、字体、图片、表单、链接和各种历史对象。文本型 PDF 主要清理重复对象、无用数据和未压缩的数据流;扫描型 PDF 真正占空间的通常是图片,想明显变小,就得降低图片分辨率和 JPEG 质量。

我平时会把两种方式放进同一个脚本,不让调用方自己瞎猜。

先安装 PyMuPDF:

pip install -U pymupdf

代码如下:

from __future__ import annotations

import argparse
from pathlib import Path

import pymupdf


deffile_size_mb(path: Path) -> float:
return path.stat().st_size / 1024 / 1024


defcompact_structure(source: Path, target: Path) -> None:
"""保留文字、链接等页面结构,清理冗余对象并压缩数据流。"""
with pymupdf.open(str(source)) as pdf:
if pdf.needs_pass:
raise RuntimeError("PDF 已加密,请先提供密码或解除加密")

        pdf.save(
            str(target),
            garbage=4,
            clean=True,
            deflate=True,
            deflate_images=True,
            deflate_fonts=True,
        )


defrebuild_as_images(
    source: Path,
    target: Path,
    dpi: int,
    quality: int,
)
 -> None:

"""将每页重新渲染为 JPEG,适合体积较大的扫描件。"""
    old_pdf = pymupdf.open(str(source))
    new_pdf = pymupdf.open()

try:
for page_number, page in enumerate(old_pdf, start=1):
            pixmap = page.get_pixmap(
                dpi=dpi,
                colorspace=pymupdf.csRGB,
                alpha=False,
                annots=True,
            )

            jpeg_data = pixmap.tobytes(
                output="jpeg",
                jpg_quality=quality,
            )

            new_page = new_pdf.new_page(
                width=page.rect.width,
                height=page.rect.height,
            )
            new_page.insert_image(new_page.rect, stream=jpeg_data)

            print(
f"[处理] 第 {page_number}/{old_pdf.page_count} 页,"
f"{pixmap.width}x{pixmap.height}"
            )

        new_pdf.save(
            str(target),
            garbage=4,
            deflate=True,
        )
finally:
        new_pdf.close()
        old_pdf.close()


defmain() -> None:
    parser = argparse.ArgumentParser(description="压缩 PDF 文件")
    parser.add_argument("source", type=Path, help="原始 PDF")
    parser.add_argument("target", type=Path, help="输出 PDF")
    parser.add_argument(
"--mode",
        choices=("safe", "scan"),
        default="safe",
        help="safe 保留结构,scan 适合扫描件",
    )
    parser.add_argument("--dpi", type=int, default=130)
    parser.add_argument("--quality", type=int, default=72)
    args = parser.parse_args()

ifnot args.source.exists():
raise FileNotFoundError(f"找不到文件:{args.source}")

if args.source.resolve() == args.target.resolve():
raise ValueError("输出文件不能覆盖原文件")

ifnot72 <= args.dpi <= 300:
raise ValueError("dpi 建议设置在 72 到 300 之间")

ifnot30 <= args.quality <= 95:
raise ValueError("quality 建议设置在 30 到 95 之间")

    args.target.parent.mkdir(parents=True, exist_ok=True)

    before = file_size_mb(args.source)

if args.mode == "safe":
        compact_structure(args.source, args.target)
else:
        rebuild_as_images(
            args.source,
            args.target,
            dpi=args.dpi,
            quality=args.quality,
        )

    after = file_size_mb(args.target)
    ratio = (1 - after / before) * 100if before else0

    print(f"[完成] 原文件:{before:.2f} MB")
    print(f"[完成] 新文件:{after:.2f} MB")
    print(f"[完成] 体积变化:{ratio:.1f}%")


if __name__ == "__main__":
    main()

普通文档先跑安全模式:

python compress_pdf.py input.pdf output.pdf --mode safe

这个模式会清理未使用对象、合并重复内容,并压缩数据流。PyMuPDF 官方文档也把 garbage=4 配合 deflate=True 作为常用的压缩保存方式。

但这里有个很容易误判的地方:原 PDF 里的图片如果已经是 JPEG,再执行一次数据流压缩,效果可能很小。代码没失效,只是最大的那块数据已经压过了。

扫描件再用激进模式:

python compress_pdf.py scan.pdf scan-small.pdf \
  --mode scan \
  --dpi 130 \
  --quality 72

这段代码会把页面渲染成位图,再以 JPEG 写回 PDF。dpi 控制页面分辨率,quality 控制 JPEG 质量,数值越低,文件通常越小,字迹也越容易发虚。PyMuPDF 的 Pixmap 接口支持直接输出 JPEG,并通过 jpg_quality 调整质量。

参数别乱拧。

办公扫描件我一般从 130 DPI、质量 72 开始试。小字较多就把 DPI 提到 150,照片较多可以适当降低质量。不要直接压到 72 DPI,然后拿放大后的一团马赛克跟库较劲。

还有一条必须写在代码旁边:scan 模式会把整页拍扁。

原来的可搜索文字、表单、超链接、书签和可编辑结构都可能丢失,数字签名也不能指望继续有效。合同、发票归档、带签章文件,我不会直接这么处理。先留原件,再拿副本压缩。

压缩完成也别只盯着文件大小。至少抽查首页、中间页、最后一页,再搜一段文字、放大看小字号。文件是小了,但内容不能跟着一起没了。

相关学习资料