PDF

PDF字体子集化的黑魔法:从300KB到30KB的瘦身之旅

admin
2026年01月15日
24 分钟阅读
1 次阅读

文章摘要

探索PDF字体子集化(Font Subsetting)的实现细节,解析CIDFont、字形索引重映射、以及如何用PyMuPDF手动实现比Adobe更激进的字体优化策略。

TECHNICAL DEEP DIVE

PDF字体子集化的黑魔法

上周帮客户优化一批电子发票PDF,原始文件平均280KB,但实际内容就几行字。打开一看,好家伙,嵌入了完整的思源黑体——15000多个汉字字形,实际只用了不到50个。这就是今天要聊的:字体子集化

什么是字体子集化?

简单说就是:PDF只保留实际用到的字符字形,把字体文件里其他99%的数据都扔掉。一个完整的中文字体动辄几MB,但一份文档可能只用到几十个字,为什么要带着整个字体库?

PDF里字体是怎么存储的

PDF支持两种字体嵌入方式:

📦 完整嵌入
把整个TTF/OTF文件塞进PDF,简单粗暴,但文件巨大。适合需要编辑的场景。
✂️ 子集嵌入
只保留用到的字形,体积可以缩小95%以上。生成后的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,那是另一个故事了——文字层和图像层的对齐才是关键。

工具推荐

1
fontTools (Python)
Google开源的字体处理库,子集化功能最完善,支持各种edge case
2
QPDF (C++)
低层PDF操作神器,可以直接修改对象流,适合批量处理
3
Ghostscript
老牌工具,通过-dSubsetFonts=true参数自动处理

字体子集化看起来是个小优化,但在大规模PDF生成场景下能省下不少带宽和存储成本。我们的电子发票系统每天生成20万份PDF,优化后每月节省了接近2TB的流量。细节决定成败,说的就是这种事。

最后更新: 2026年01月15日

admin

PDF工具专家,致力于分享实用的PDF处理技巧

73
文章
431
阅读

相关标签

PDF

推荐工具

使用WSBN.TECH的专业PDF工具,让您的工作更高效

立即体验

相关推荐

发现更多PDF处理技巧和实用教程