这种情况我一般不急着换库。先打开 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 模式会把整页拍扁。
原来的可搜索文字、表单、超链接、书签和可编辑结构都可能丢失,数字签名也不能指望继续有效。合同、发票归档、带签章文件,我不会直接这么处理。先留原件,再拿副本压缩。
压缩完成也别只盯着文件大小。至少抽查首页、中间页、最后一页,再搜一段文字、放大看小字号。文件是小了,但内容不能跟着一起没了。
夜雨聆风