技术文章

PDF 线性化和快速 Web 视图:它是如何工作的

将一个 80 MB 的扫描报告放在链接后面,在浏览器中打开它,看看会发生什么:阅读器停留在一个空白窗格上,直到这些字节的很大一部分到达,然后一次性绘制第一页。跳转到第 40 页,在一个构建不良的文件上,整个下载可能会重新开始。令人沮丧的是,阅读器其实只想要第一页。线性化就是解决这个问题的结构性答案。它重新排列 PDF,以便阅读器可以从文件的一个小前缀渲染打开的页面,并按需获取其余部分,这就是为什么 Adobe 将此功能称为“快速 Web 视图”。

这些都不是不同的文件格式。线性化 PDF 是一种普通的 PDF,符合规范的阅读器无需特殊处理即可打开。这里的诀窍完全在于字节是如何排序的以及文件所带的两个额外结构。ISO 32000-1 在附录 F 中规定了整个安排,一旦你看到了布局,这种行为就不再像魔术,而像是一种用文件顺序换取首次绘制延迟的刻意交易。

线性化实际重新排列了什么

普通的 PDF 可以以几乎任何顺序分散其对象。文件末尾的交叉引用表使之发挥作用:阅读器定位到末尾,读取 startxref 指针,加载 xref,然后从那里可以通过偏移量定位每个对象。该设计对于本地文件非常出色(寻道到末尾不花时间),但对于通过网络流式传输的文件则很糟糕,因为末尾正是最后到达的部分。要渲染第一页,传统的阅读器需要页面对象、其内容流、它引用的字体以及它绘制的任何图像,而在一个无序文件中,这些内容可能位于任何位置,包括最后的一兆字节。

线性化固定了顺序。显示第一页所需的对象被聚集到靠近前面的一个连续块中,紧跟在一个小头部部分之后,因此它们在字节流中很早到达。其他所有内容,剩余的页面及其共享的资源,都遵循一个可预测的序列。对于忽略该优化的阅读器,末尾仍然存在第二个完整的交叉引用表,但是线性化文件还在前面放置了第一页交叉引用和流式阅读器所需的参数。阅读器不再需要到达尾部才能绘制任何内容。

第一页对象集和线性化参数字典

%PDF 头部之后,线性化文件中的第一个对象是线性化参数字典。流式阅读器就是通过寻找它来决定优化是否存在以及如何使用它。该字典记录了整个文件的长度、主交叉引用部分开始的字节偏移量、第一页的对象号,以及随后的提示流的位置和长度。有了这些数字,阅读器仅从开头的几千字节就能知道,为了显示第一页必须获取多少内容,以及在哪里寻找允许其跳转到其他位置的索引。

附录 F 对于此处的“第一页”的含义有严格的规定。第一页部分必须包含页面对象本身、其内容流,以及这些流引用的资源,以便在该前缀下载后,页面能够自给自足。共享资源,如每一页都使用的字体、在页眉中重复出现的徽标等,都会得到特殊处理:它们出现得足够早,以便为第一页服务,但被标记为共享,因此阅读器在稍后渲染第 30 页时不会重新获取它们。页面私有对象和共享对象之间的区别,正是大多数自制的“优化器”容易出错的部分,而弄错这点就会产生一个号称是线性化的,但仍然会卡顿的文件。

提示流:让页面跳转代价低廉的索引

快速显示第一页只是价值的一半。另一半是跳转到任意一页而无需下载中间的所有内容,而这正是提示流所提供的。线性化文件带有页面偏移提示表和共享对象提示表,存储为参数字典中引用的流。页面偏移表为每一页记录了其对象在文件中开始的位置以及它们的长度。共享对象表为跨多个页面使用的资源执行相同的操作。

有了这些表,想要第 40 页的阅读器就不会按顺序解析文件。它会查阅提示表以了解第 40 页占据的字节范围,向服务器请求准确的该范围,一旦这些字节到达便渲染该页面,通过相同的机制拉取它尚未持有的任何共享资源。提示流实际上是覆盖在文档上的随机访问映射,这就是为什么在一个缓慢的链接上,一个良好线性化的 500 页文件感觉响应很快,而同样大小的未优化文件却并非如此的原因。

为什么服务器必须合作

线性化假定传输系统能够交付文件的任意切片,在您因糟糕的结果而归咎于格式之前,这个假设值得检查。该机制是 HTTP 字节服务:阅读器发出范围请求,服务器用 206 Partial Content 响应来回答。如果服务器不通告 Accept-Ranges: bytes,或者它前面的代理或 CDN 将范围请求合并为完整传输,则阅读器无法孤立地获取第 40 页,只能退回到下载整个文件。这样一来,PDF 内部的结构就完全正确但也完全浪费了。

这正是最常被误诊为“线性化不起作用”的故障。文件没问题;传输路径有问题。在重建文档之前,请通过条件请求确认主机确实针对阅读器访问的 URL 返回了部分内容。许多静态主机默认执行此操作,而许多配置错误的应用程序服务器和缓存层则没有。

增量更新悄无声息地破坏了线性化

这是让那些正确生成了线性化文件,却纳闷为什么优化消失的人感到惊讶的限制。线性化依赖于前面有其索引的单一、精心排序的布局。增量更新在设计上就违背了这一点。当工具通过增量保存添加签名、填写表单字段或附加注释时,它不会重写该文件。它将更改的对象、新的交叉引用部分和新的尾部追加到末尾,保持原始字节原封不动。这种追加正是增量更新的全部意义:它很快,并且保留了较早的修订版以进行审核或签名验证。

副作用是,该文件现在将其最新的交叉引用数据放在尾部,位于精心放置的第一页块之后,而前面的线性化参数字典描述的布局不再与该文件匹配。符合规范的阅读器会检测到不匹配,并将文档视为普通的非线性化 PDF。快速 Web 视图消失了,即使原始的线性化结构仍然在文件的上半部分中。如果附加几个更新,每一个更新都在末尾堆叠另一个修订版,那么陈旧的前部索引与真实状态之间的差距就会扩大。

如果您的工作流既需要编辑又需要快速 Web 视图,则规则直接遵循该结构:在文档处于不断变化中时进行增量编辑,然后在最后重新线性化一次。全面重写才能恢复布局。用 HotPDF 的术语来说,这意味着正在进行的编辑会通过 BeginIncrementalUpdateSaveIncrementalUpdate 追加增量,而完成步骤会加载整个文档,然后使用 LoadFromFile 及其随后的 SaveLoadedDocument 将其重新序列化,这会丢弃累积的旧修订版并发出一个干净的布局。同样的情况也出现在对象流中:同时启用 UseObjectStreamsUseXRefStream 会压缩交叉引用并将对象紧密打包,这有助于减小文件大小,但就像任何结构上的选择一样,它必须在最终重写期间应用,而不是硬塞在附加的修订版上。

// In-flight edits: append a delta, keep prior revisions intact.
// This leaves the file NOT linearized.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');

// Finishing step: full re-serialization produces one clean layout,
// dropping the stacked revisions. Re-run your linearizer on the output.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');

HotPDF 并没有提供调用一次即“线性化”的例程,所以比较实际的模式是先生成一个干净的、完全重写后的文件,然后再运行一个专门的优化器来处理它。命令行工具可以直接处理重新排列。qpdf 只需一个标志就能将文件重写为线性化格式:

qpdf --linearize report-final.pdf report-web.pdf

如何判断文件是否已线性化

不要相信文件名或声称生成了该文件的工具;要验证字节。最直接的检查是文件头部:打开它并寻找线性化参数字典,它作为头部之后的第一个对象,带有 /Linearized 键。面向阅读器的快捷方式是 Acrobat 的“文档属性”对话框,它仅在结构真正存在且是最新的时才报告“快速 Web 视图:是”。

对于脚本化检查,qpdf 报告结构的存在性和完整性,这很重要,因为一个文件可以带有一个不再反映其布局的线性化字典,这恰好是增量更新留下的状态:

# Reports "File is linearized" and validates hint tables against the layout
qpdf --check report-web.pdf

# Dumps the linearization parameters and hint data in detail
qpdf --show-linearization report-web.pdf

验证步骤才是体现其价值的地方。仅确认字典存在的检查会愉快地同意索引指向错误偏移量的文件;将提示表与实际对象位置进行核对的检查才能告诉您,该优化在真实的阅读器范围请求下能否经得起考验。

线性化仍然值得应用于在 Web 上提供的任何大型文档,特别是对于处于不稳定连接状态的移动阅读器,并且对于前置加载索引需要花费百分之几的文件大小代价。需要弄清楚的两件事是,PDF 内部的结构和外部的字节服务都必须是正确的,并且事后的任何编辑都会撤消优化,直到您重写该文件为止。在所有其他修改确定之后,将重新线性化视为流水线中的最后一步。此处描述的交叉引用、对象流和增量更新行为是针对 Delphi 和 C++Builder 的 HotPDF Component 实现的结构模型的一部分;有关更广泛的文件布局背景,请参阅 PDF 是如何构建的,有关代码中的增量更新和大型文件工作流,请参阅 使用 Delphi 处理大型 PDF