28_PDF压缩优化
摘要:在实际业务中,用户上传的 PDF 文件往往体积庞大,包含未压缩的高清图片、冗余的字体数据以及无用的元数据。这不仅浪费了宝贵的存储空间(对象存储费用),还显著增加了网络传输延迟(CDN 费用和用户等待时间)。
本章将深入探讨 PDF 文件的 “瘦身” 技术。我们将分析 PDF 文件大小的构成,利用
pikepdf(QPDF) 进行无损压缩,使用ghostscript进行有损图像重采样,并结合字体子集化技术,构建一套智能的 PDF 压缩服务。
关联的小程序样例:薄荷百宝箱
28.1 PDF 文件大小分析
28.1.1 为什么 PDF 这么大?
PDF 是一个容器格式,其体积主要由以下几部分构成:
28.1.2 使用 pikepdf 分析
pikepdf 是基于 C++ 库 QPDF 的 Python 封装,速度极快且功能强大。
import pikepdf
import os
def analyze_pdf_structure(pdf_path: str):
"""
简单分析 PDF 内部对象数量
"""
with pikepdf.Pdf.open(pdf_path) as pdf:
num_pages = len(pdf.pages)
# 获取 PDF 版本
version = pdf.pdf_version
# 统计图片数量 (简化版,仅统计页面资源)
image_count = 0
for page in pdf.pages:
if "/XObject" in page.Resources:
xobjects = page.Resources["/XObject"]
for xobj in xobjects.values():
if xobj.get("/Subtype") == "/Image":
image_count += 1
file_size = os.path.getsize(pdf_path) / 1024 / 1024 # MB
print(f"File: {pdf_path}")
print(f"Size: {file_size:.2f} MB")
print(f"Pages: {num_pages}")
print(f"Images detected: {image_count}")
28.2 图片压缩策略
图片是压缩的核心。我们需要将高分辨率(300+ DPI)图片下采样到适合屏幕阅读的分辨率(如 150 DPI 或 72 DPI),并调整 JPEG 质量。
28.2.1 使用 Ghostscript (推荐)
Ghostscript 是处理 PDF 的瑞士军刀,虽然它是命令行工具,但效果远超纯 Python 库。
安装依赖:
RUN apt-get update && apt-get install -y ghostscript
Python 封装:
import subprocess
import shutil
def compress_pdf_ghostscript(
input_path: str,
output_path: str,
quality: str = "ebook"
) -> bool:
"""
调用 Ghostscript 压缩 PDF
:param quality: screen (72dpi), ebook (150dpi), printer (300dpi), prepress (300dpi, color preserve)
"""
if not shutil.which("gs"):
raise RuntimeError("Ghostscript (gs) command not found.")
gs_quality_map = {
"screen": "/screen",
"ebook": "/ebook",
"printer": "/printer",
"prepress": "/prepress",
"default": "/default"
}
gs_setting = gs_quality_map.get(quality, "/default")
cmd = [
"gs",
"-sDEVICE=pdfwrite",
"-dCompatibilityLevel=1.4",
f"-dPDFSETTINGS={gs_setting}",
"-dNOPAUSE",
"-dQUIET",
"-dBATCH",
f"-sOutputFile={output_path}",
input_path
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except subprocess.CalledProcessError as e:
print(f"Ghostscript error: {e.stderr.decode()}")
return False
28.2.2 纯 Python 方案 (pikepdf)
如果无法安装 Ghostscript,可以使用 pikepdf 遍历图像对象并重新编码。
from PIL import Image
from io import BytesIO
def compress_images_pikepdf(input_path: str, output_path: str, quality: int = 50):
with pikepdf.Pdf.open(input_path) as pdf:
for page in pdf.pages:
# 递归查找图片对象并替换(逻辑较复杂,需处理各种色彩空间)
# 这里仅展示思路:获取 Image XObject -> PIL -> Resize/Compress -> Replace
pass
# pikepdf save 时会自动移除未引用对象
pdf.save(output_path)
工程建议:生产环境首选 Ghostscript,它的图像重采样算法非常成熟,且不容易破坏 PDF 结构。
28.3 字体子集化
28.3.1 什么是字体子集化 (Subsetting)?
如果一个 PDF 嵌入了 “微软雅黑” 字体文件(15MB),但文档中只用了 “你好世界” 4 个字。子集化技术会生成一个只包含这 4 个字字形的新字体文件(可能只有 10KB),从而大幅减小体积。
28.3.2 实现方式
Ghostscript 在处理 PDF 时(dPDFSETTINGS 模式),会自动尝试进行字体子集化。
如果你使用 pikepdf 或 pypdf,它们通常保留原样,不做字体优化。这是选择 Ghostscript 的另一个重要原因。
28.4 元数据清理
隐私保护与体积优化同样重要。我们需要清除作者、编辑历史、缩略图等。
28.4.1 使用 pikepdf 清理
pikepdf 在保存时默认会重构对象树(Linearization / Fast Web View),这会自动剔除很多垃圾数据。
def optimize_pdf_structure(input_path: str, output_path: str):
with pikepdf.Pdf.open(input_path) as pdf:
# 1. 移除元数据
with pdf.open_metadata() as meta:
# 清空所有标准元数据项
for key in list(meta.keys()):
del meta[key]
# 2. 保存并优化
# linearize=True: 开启 "快速 Web 查看" (Fast Web View)
# object_stream_mode=pikepdf.ObjectStreamMode.generate: 压缩对象流
pdf.save(
output_path,
linearize=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate
)
Fast Web View (Linearization): 这是一种特殊的 PDF 结构,允许浏览器在下载完 PDF 的前几 KB 后就开始显示第一页,而不需要等待整个文件下载完成。这对于大文件在 Web 端的体验至关重要。
28.5 压缩率控制
用户对压缩的需求是多样的:有的用户需要极致小的体积(用于邮件附件),有的用户需要保持高清晰度(用于打印)。我们需要提供分级选项。
28.5.1 分级配置
我们定义三种压缩模式:
28.5.2 智能回退策略
有时候,压缩后的文件反而比源文件大了(例如源文件已经是极致压缩的黑白图,转为彩色 JPEG 后变大)。
import os
def smart_compress(input_path: str, output_path: str, mode: str = "ebook"):
# 1. 执行压缩
success = compress_pdf_ghostscript(input_path, output_path, quality=mode)
if not success:
raise Exception("压缩过程失败")
# 2. 检查结果
src_size = os.path.getsize(input_path)
dst_size = os.path.getsize(output_path)
# 3. 如果压缩后反而变大了,或者是 0 字节,则直接返回原文件(拷贝)
if dst_size == 0 or dst_size >= src_size:
print(f"压缩无效 (Src: {src_size} -> Dst: {dst_size}),保留原文件")
shutil.copy(input_path, output_path)
return output_path
本章面试题与知识点总结
1. 为什么 Ghostscript 是 PDF 压缩的首选工具?
参考答案:
●全能性:Ghostscript 不仅能处理图片下采样(Downsampling)和重编码(Re-encoding),还能处理字体子集化(Subsetting)和去除非标对象。纯 Python 库(如 pypdf)通常只能处理对象结构,难以处理图像像素数据和字体字形数据。●鲁棒性:Ghostscript 经过了几十年的工业验证,对各种损坏或不规范的 PDF 兼容性极好。
2. 什么是 PDF 的 “Fast Web View” (Linearization)?有什么作用?
参考答案:
●定义:这是一种优化过的 PDF 文件组织结构。标准的 PDF 将“目录”(XREF 表)放在文件末尾,浏览器必须下载完整个文件才能解析目录并渲染。线性化 PDF 将目录和第一页的数据移到了文件头部。●作用:允许浏览器(或 PDF 阅读器)进行流式渲染。用户打开 100MB 的 PDF 时,只要下载了前几十 KB,就能立即看到第一页,并在后台继续下载剩余页面。显著提升用户体验。
3. 如何在 Python 中判断 PDF 文件是否已压缩?
参考答案: 很难准确判断,因为“压缩”是一个相对概念。 但可以通过以下启发式方法检查:
1.图片过滤器:检查/XObject中的图片是否使用了/DCTDecode(JPEG) 或/JPXDecode(JPEG2000) 压缩。2.对象流:检查是否使用了/ObjStm(Object Streams),这是 PDF 1.5 引入的结构压缩技术。3.统计法:计算文件大小 / 页数,如果单页小于 50KB 且包含图片,通常说明已经是压缩过的。下篇预告:掌握了 PDF 的压缩优化后,我们将进入 第 29 章:PDF 水印与加密。将探讨如何在不破坏原有内容的前提下,为 PDF 添加文字/图片水印,并设置高强度的安全密码。
夜雨聆风
