PDF底层结构之字体与编码系统

1. 引言

PDF(Portable Document Format)是一种以"视觉一致性"为核心的文档格式。

它与普通文本文件不同,PDF 并不直接存储"字符信息",而是存储"绘制指令"------包括字体、大小、坐标和字形路径等信息。

为了确保文档在任意平台上显示完全一致,PDF 引入了一个复杂的 字体系统(Font System)编码机制(Encoding System)

这种机制虽然保证了跨平台的稳定性,但也带来了诸如"乱码""复制错误""字体缺失"等常见问题。

要理解并修复这些问题,必须从 PDF 的底层结构出发,深入了解其字体与编码的设计逻辑。

2. 字体对象(Font Object)

2.1 字体对象的作用

PDF 中的字体对象定义了页面上使用的文字应如何被绘制。

它并不是具体的文字内容,而是描述文字外观的容器,包含字体名称、类型、编码方式、字形信息等。

一个典型的字体对象示例如下:

css 复制代码
7 0 obj
<<
  /Type /Font
  /Subtype /Type0
  /BaseFont /ABCDEE+SimSun
  /Encoding /Identity-H
  /DescendantFonts [8 0 R]
  /ToUnicode 9 0 R
>>
endobj

2.2 字体对象关键字段说明

字段 含义
/Subtype 字体类型:Type1、TrueType(简单字体)或 Type0(复合字体)
/BaseFont 字体名称;带前缀如 ABCDEE+ 表示"子集字体"
/Encoding 字符编码方式,如 /WinAnsiEncoding/Identity-H
/DescendantFonts 复合字体(Type0)中指向实际字形数据的 CIDFont 对象
/ToUnicode 定义字符代码与 Unicode 的对应关系(用于复制与搜索)

2.3 字体类型概览

类型 特征 适用场景
Type1 / TrueType / Type3 单字节字体(最多256个字符) 西文字体、符号
Type0 + CIDFontType0 / 2 多字节复合字体(支持上万字符) 中文、日文、韩文

Type0 是 PDF 为 CJK 文字设计的复合字体结构,通过 CID(Character ID)与 CMap 实现多字节字符支持。

3. 编码结构(Encoding Structure)

PDF 的编码系统定义了"字节 → 字形"的映射关系。

根据字体类型不同,PDF 采用不同的编码机制。

3.1 简单字体编码(Simple Encoding)

简单字体采用单字节映射表,常见定义如下:

复制代码
/Encoding /WinAnsiEncoding

或:

javascript 复制代码
/Encoding <<
  /BaseEncoding /WinAnsiEncoding
  /Differences [128 /Euro 130 /quotesinglbase]
>>

此类字体最多支持256个字符,常见于英文、符号字体。

3.2 复合字体编码(Composite Encoding)

复合字体通过 CMap 文件建立多字节映射关系,例如:

sql 复制代码
/Encoding /Identity-H
  • /Identity-H:水平书写的"直通映射",PDF 代码值直接作为 CID;
  • /Identity-V:垂直书写映射;
  • /GB-EUC-H/UniJIS-UCS2-H 等为具体语言映射表。

CMap 的作用是:

把输入的字节序列映射为字体内部的字符ID(CID)。

3.3 ToUnicode 映射

ToUnicode 是一种反向映射,用于将 PDF 内部的字符代码还原为标准 Unicode。

示例如下:

javascript 复制代码
9 0 obj
<< /Type /CMap /CMapName /ToUnicode >>
stream
begincmap
1 begincodespacerange
<00> <FF>
endcodespacerange
2 beginbfchar
<41> <0041>
<42> <0042>
endbfchar
endcmap
endstream
endobj

若 ToUnicode 缺失,则 PDF 仍可显示文字,但复制、搜索、提取文字都会乱码。

4. 字体渲染流程(Font Rendering Process)

PDF 的字体渲染本质上是一个"多层映射"过程。

4.1 显示流程

objectivec 复制代码
PDF 内容流 (文字代码)
    ↓
/Encoding 或 CMap
    ↓
字形 ID (CID)
    ↓
/FontFile(字形轮廓)
    ↓
渲染成图形

4.2 复制 / 搜索流程

markdown 复制代码
PDF 内容流 (文字代码)
    ↓
/ToUnicode
    ↓
Unicode 字符
    ↓
文本输出 / 搜索匹配

4.3 字体对象间的引用关系

css 复制代码
Page对象 → /Resources → /Font → [字体对象] 
                            ↓
                          /DescendantFonts → [CIDFont]
                            ↓
                          /FontDescriptor → /FontFile

通过这种层层引用,PDF 能在不依赖系统字体的前提下精确控制显示效果。

5. 字体乱码实例分析

上面理论都比较虚,现在我通过分析一个实际的pdf来展示像pdf.js这种程序到底是如何渲染页面文字内容的。

5.1 问题场景

如下图,这个pdf中中文显示正常,但英文字符部分乱码了。

通过对pdf底层的对象分析,发现页面对象引用了两个字体:

字体的obj对象如下所示

javascript 复制代码
5 0 obj  % 中文字体
<</BaseFont/SEHAGU+DengXian/Encoding/Identity-H/Subtype/Type0>>
endobj

13 0 obj  % 符号字体
<</BaseFont/JYIVPL+BookshelfSymbolSeven/Encoding/Identity-H/Subtype/Type0>>
endobj

根据上文对字体对象的分析,可以知道这两个字体一个是等线字体,一个是符号字体。然后我们再找到页面中Contents指向的对象流19 0 R

这是一个流式对象,解码为文本格式为:

这个其实很好看懂,以下面为例:

css 复制代码
BT
/F1 12 Tf
72.5 760.97 Td
(测试) Tj
/F2 12 Tf
140 737.52 Td
(123) Tj
ET
  • BT和ET是开始和结束;
  • Tj标明渲染的内容;
  • Tf标明这段内容使用F1字体,字体大小为12;
  • Td标明这段内容渲染的位置

而这篇PDF通过分析发现:

  • 中文部分使用 /F1 → DengXian 字体;
  • 数字部分使用 /F2 → BookshelfSymbolSeven 符号字体;
  • 因符号字体的字形映射不同,导致数字显示成乱码。

可以看到在实际的pdf中,内容部分是数据和字符串加密后的编码,你找到字体对象/ToUnicode指向的对象,一般是个流式对象,解压后可以看到他的明文大概是如下图样式,从beginbfrange开始的每一行含义是:<源编码><对应Unicode><实际字符>,意思是上面内容中的023b对应的Unicode编码也是023b,实际的字符是 3001(、号)。

5.2 底层修复方案

如上分析可知,这个pdf字符乱码的主要原因是符号字体的映射错误了,应该映射到f1字体,修改思路如下。

方法一:资源层重定向(推荐)

修改页对象的字体资源:

diff 复制代码
/Font <<
  /F1 7 0 R
- /F2 8 0 R
+ /F2 7 0 R
>>

此时 /F2 被重定向到正确字体,内容流无需改动。

方法二:内容流修正

直接将内容流中的 /F2 替换为 /F1

diff 复制代码
/F1 12 Tf
(测试) Tj
- /F2 12 Tf
+ /F1 12 Tf
(123) Tj

方法三:重新导出

在源文件中统一字体,确保数字和中文使用同一字体。

对于pdf的用户来说,直接选中乱码部分,修改字体为其他正常字体即可

6. 常见的字体错误类型

错误类型 症状 原因 修复建议
字体未嵌入 不同电脑显示不同字体 缺少 /FontFile 重新导出并启用"嵌入字体"
缺少 ToUnicode 复制、搜索乱码 /ToUnicode 映射 重新生成映射或导出时启用"Unicode支持"
混用字体 部分符号乱码 内容流切换错误字体 统一字体或重定向资源
错误编码声明 特殊字符显示异常 /Encoding 不匹配 修正编码或使用标准映射
Type3 字体兼容性差 显示模糊或错位 使用绘图命令定义字形 转为 TrueType / CIDFont
字体子集冲突 同页字体样式不一致 重复嵌入多个子集 合并字体子集或重新导出

7. 总结

PDF 的字体系统由三层结构构成:

  1. 字体对象层(Font Object)
    定义字体类型、名称与引用关系;
  2. 编码层(Encoding / CMap)
    决定字符代码与字形之间的映射;
  3. Unicode 层(ToUnicode)
    建立 PDF 内部代码与标准字符集的桥梁。

它们共同构成以下逻辑链:

markdown 复制代码
字符代码 → 编码映射 → 字形 → 显示
         ↘
          ToUnicode → Unicode → 复制/搜索

正是这套体系让 PDF 实现了跨平台的视觉一致性。

但由于机制复杂,任何环节出错都会导致乱码或字体异常。

理解 /Encoding/CMap/ToUnicode 三者的关系,

是彻底解决 PDF 字体问题的关键。

相关推荐
飘尘11 分钟前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆20 分钟前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师1 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆1 小时前
VSCode自动格式化三要素
前端
爱勇宝2 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen3 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518135 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode5 小时前
Redis 在生产项目的使用
前端·后端
LiaCode5 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战5 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github