PDF 并不是像 Word 或 RTF 那样的文档格式。那些格式存储了一系列内容,渲染器在显示时对这些内容进行解释,因此输出取决于恰好存在的字体和布局引擎。PDF 存储了该过程的结果:精确的渲染指令、字体程序、压缩图像流,以及将它们结合成对每一页的自包含描述的对象图。该文件携带足够的信息以在任何符合规范的渲染器上完全相同地重现每一页,这既是其主要的设计目标,也是您在尝试以编程方式生成、解析或修改它时遇到的大多数复杂性的根源。
对象模型
每个 PDF 都是带编号的对象的集合。对象可以是布尔值、整数、实数、名称、字符串、数组、字典、流或 null。几乎所有有趣的东西都是字典,它是一组键值对,其中键是名称,值是任何其他对象类型,包括通过编号和生成计数对其他对象的引用。流是一个字典后跟一个通常被压缩的字节序列。
目录(catalog)字典是根。它指向页面树,页面树将页面字典组织成平衡的树结构而不是扁平列表,因此导航到 10,000 页文档的第 5,000 页不需要遍历之前的每个页面描述符。每个页面字典都引用其内容流(一个或多个页面描述操作符序列)、其资源字典(它又引用字体描述符、颜色空间和图像 XObjects)及其媒体框(页面所在的坐标空间)。坐标原点在左下角,Y 轴正向向上,单位为 1/72 英寸。
文件末尾是交叉引用表,它将每个对象编号映射到文件中的字节偏移量。这正是实现随机访问的原因:查看器首先读取交叉引用表,然后直接定位到其需要的任何对象。PDF 1.5 引入了交叉引用流,它将表压缩为流对象并将相关对象打包到对象流中,从而显着减小了具有许多小对象的文档的文件大小。
内容流和图形模型
页面的视觉内容存在于一个或多个内容流中。每个流都是一个散布着其操作数的 PDF 操作符序列。文本操作符 BT 开始一个文本对象,Tf 从资源字典中选择字体和大小,Td 定位文本光标,Tj 或 TJ 绘制一个字符串,ET 关闭文本对象。矢量图形遵循类似的模式:m 设置路径起点,l 附加线段,c 附加贝塞尔曲线,f 或 S 填充或描边路径。
图形状态管理在操作符之间发生的所有事情:当前变换矩阵、线宽、颜色空间、填充颜色、描边颜色和裁剪路径。诸如 q 和 Q 之类的操作符将图形状态压入堆栈和从堆栈弹出,这是 PDF 实现局部坐标变换和临时状态覆盖而不影响它们周围上下文的方式。表单 XObjects 概括了这一点:带有其自己资源字典的自包含内容流,可以通过单个 Do 操作符在任意位置和比例下绘制到页面上。
字体嵌入和文本提取
PDF 可以按名称引用字体,并依赖查看器去替换一些东西,但在实践中,您打算共享的任何文档都必须嵌入字体数据。嵌入在 PDF 中的 Type 1 或 TrueType/OpenType 字体带有指向字体文件流的字体描述符字典。对于 TrueType 字体,该流包含二进制字体程序;对于 Type 1,它是 PFB 数据。子集化(这是每个严肃的 PDF 生成器都会做的事情)剥离了文档未引用的字形,即使对于大型 Unicode 字体也能保持文件大小在可控范围内。
文本提取是字体嵌入产生反作用的地方。字符的视觉表示由嵌入字体程序中的字形确定。该字符的 Unicode 值由附加到字体字典的 ToUnicode CMap 流确定。当 ToUnicode CMap 丢失或不正确时,PDF 查看器可以清晰地渲染文本,但无法将其提取为有意义的 Unicode,这就是为什么从某些 PDF 复制粘贴会产生乱码的原因。标记化 PDF (ISO 32000 §14.8) 添加了第二层:将页面内容映射到文档语义角色(如段落、标题和表格单元格)的逻辑结构树。屏幕阅读器和重排引擎使用结构树而不是原始内容流顺序,这解释了为什么如果缺少标记或标记错误,即使是视觉上排版良好的 PDF 也可能无法访问。
增量更新和数字签名
当您不从头重写就保存对现有 PDF 的更改时,新对象与新的交叉引用部分以及新的尾部字典一起附加在原始文件主体的后面。更新后的尾部指向新的交叉引用数据,而被取代的对象仍保留在文件中,只是不再被新的交叉引用链所引用。这就是增量更新,它产生了两个重要的结果。
首先,文件随着每一次保存周期而增长。经过反复编辑和保存的文档会积聚多层过时的对象。像 QPDF 这样的工具可以线性化或压缩并重写文件以回收该空间,但默认行为是积累。其次,数字签名依赖于增量更新来实现其完整性模型。ISO 32000 签名覆盖文件的字节范围,通常是除了签名值本身的占位符之外的所有内容。任何作为附加增量更新出现的签名后更改,对于验证阅读器来说都可见为签名后所做的修改,这正是您想要的审计跟踪。然而,这也意味着某些修改(例如添加批准签名或填写表单字段)在标准中是明确允许的,而不会使原始签名失效,前提是更改符合文档的权限设置 (ISO 32000-2 §12.7.6)。超出这些权限的修改将被标记为未授权。当您生成将在下游会签的文档时,正确理解这种区别非常重要。
兼容性级别和 ISO 32000 谱系
PDF 最初是在 1993 年作为专有的 Adobe 格式开始的,吸收了 PostScript 的成像模型,并在 15 个版本中积累了特性:1.1 中的加密,1.2 中的交互式表单,1.3 中的数字签名和逻辑结构,1.4 中的透明度,1.5 中的对象流,1.6 中的 AES 加密。Adobe 于 2007 年向 ISO 提交了 PDF 1.7,产生了 ISO 32000-1:2008。ISO 32000-2:2020 涵盖了 PDF 2.0,它收紧了几个未充分说明的领域,修订了 AES-256 密钥派生(版本 6 替换版本 5),并增加了对关联文件和富媒体的显式支持。
子标准源自同一个基础。PDF/A (ISO 19005) 用功能换取存档稳定性:无加密,无外部内容依赖,嵌入所有字体,颜色空间与设备无关,需要 XMP 元数据。PDF/A-1 基于 PDF 1.4,PDF/A-2 基于 PDF 1.7,PDF/A-3 允许嵌入任何格式的文件。PDF/X (ISO 15930) 是印刷生产的子集:输出意图,出血框和修剪框,较旧的兼容性级别中无透明度。PDF/UA (ISO 14289) 强制要求标记结构、Unicode 映射和语言元数据,以实现无障碍访问。这些不是相互竞争的格式;它们是核心 PDF 之上的一组附加约束,而且一个文件可以同时符合多个子标准,只要这些约束不冲突即可。
对于任何编写生成或处理 PDF 代码的人来说,实用的基准是 ISO 32000-2,并仔细关注涵盖交叉引用模型 (§7.5)、图形状态 (§8.4)、文本状态操作符 (§9.3)、字体描述符和 ToUnicode (§9.6 和 §9.10)、交互式表单 (§12.7) 以及数字签名 (§12.8) 的部分。标准很长,但大多数编程式 PDF 工作都会反复触及其中的一小部分。理解对象模型和交叉引用机制是切入点;其他的一切都是由此延伸的专门化。