PDF字体子集化的黑魔法:从300KB到30KB的瘦身之旅
文章摘要
探索PDF字体子集化(Font Subsetting)的实现细节,解析CIDFont、字形索引重映射、以及如何用PyMuPDF手动实现比Adobe更激进的字体优化策略。
TECHNICAL DEEP DIVE
PDF字体子集化的黑魔法
上周帮客户优化一批电子发票PDF,原始文件平均280KB,但实际内容就几行字。打开一看,好家伙,嵌入了完整的思源黑体——15000多个汉字字形,实际只用了不到50个。这就是今天要聊的:字体子集化。
什么是字体子集化?
简单说就是:PDF只保留实际用到的字符字形,把字体文件里其他99%的数据都扔掉。一个完整的中文字体动辄几MB,但一份文档可能只用到几十个字,为什么要带着整个字体库?
PDF里字体是怎么存储的
PDF支持两种字体嵌入方式:
在PDF对象结构里,子集化字体有个特征:字体名称会带个随机前缀,像这样:
/BaseFont /ABCDEE+SourceHanSans-Regular
/FontDescriptor 15 0 R
/Subtype /CIDFontType0
/CIDToGIDMap /Identity
那个ABCDEE+就是子集化的标记,6位随机字符防止字体名冲突。
字形索引重映射的坑
这是最容易踩的坑。原始字体里,每个字符对应一个Glyph ID(字形索引)。比如"中"这个字在思源黑体里可能是GID 4523。子集化后,你只保留了50个字形,"中"的GID就得重新映射到0-49之间。
PDF通过CIDToGIDMap来处理这个映射。有两种方式:
方式1:Identity映射
直接用Unicode码点当GID,简单但浪费空间。如果文档里有emoji(U+1F600这种六位数),GID表会非常稀疏。
方式2:自定义映射表
维护一个CID→GID的字典,GID从0开始连续分配。需要额外存储映射关系,但字体文件能压到最小。
实战:用PyMuPDF手动子集化
标准库的做法比较保守,我们可以更激进一点:
import fitz
from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter
def aggressive_subset(pdf_path, output_path):
doc = fitz.open(pdf_path)
used_chars = set()
# 第一遍:收集所有用到的字符
for page in doc:
text = page.get_text()
used_chars.update(text)
# 提取嵌入的字体
for xref in range(1, doc.xref_length()):
if doc.xref_get_key(xref, "Type") == "/Font":
font_obj = doc.xref_object(xref)
if "/FontDescriptor" in font_obj:
# 这里可以获取原始字体数据
font_data = doc.xref_stream(xref)
# 用fontTools重新子集化
ttfont = TTFont(io.BytesIO(font_data))
subsetter = Subsetter()
subsetter.populate(text=''.join(used_chars))
subsetter.subset(ttfont)
# 替换回PDF
new_font_data = io.BytesIO()
ttfont.save(new_font_data)
doc.xref_set_stream(xref, new_font_data.getvalue())
doc.save(output_path, garbage=4, deflate=True)
doc.close()
- 这种方法会破坏数字签名
- 某些字体有License限制,不允许修改或子集化
- 复杂的OpenType特性(如连字)可能失效
压缩效果对比
| 处理方式 | 文件大小 | 压缩比 |
|---|---|---|
| 原始PDF(完整字体) | 287 KB | - |
| Adobe Acrobat标准子集化 | 43 KB | 85% |
| 激进子集化 + Deflate | 28 KB | 90% |
一些冷知识
为什么有些PDF复制出来是乱码?
子集化时如果没有正确设置ToUnicode映射表,PDF阅读器就不知道GID对应哪个Unicode字符。视觉上显示正常,但复制粘贴就变成了这种Private Use Area的乱码。
子集化会影响搜索吗?
不会。只要ToUnicode映射正确,全文搜索完全没问题。但如果用OCR生成的PDF,那是另一个故事了——文字层和图像层的对齐才是关键。
工具推荐
-dSubsetFonts=true参数自动处理字体子集化看起来是个小优化,但在大规模PDF生成场景下能省下不少带宽和存储成本。我们的电子发票系统每天生成20万份PDF,优化后每月节省了接近2TB的流量。细节决定成败,说的就是这种事。