PDFium Component 从 Delphi 经 TPdf.SaveAsLinearized 产出 Fast Web View(线性化)PDF。该方法先用 PDFium 保存当前文档,再按 ISO 32000-1 附录 F 重写结果:第一页对象挪到文件前部,写出两个交叉引用节,页面偏移和共享对象提示表告诉查看器任意页面该取哪些字节范围
大家在意它的原因是网络。一份从对象存储或文档门户发出的 40 MB 商品目录,应该在最开头几 KB 之后就能显示第一页;跳到第 380 页应该只花几个 HTTP Range 请求,而不是整份下载。只有文件线性化了,查看器才做得到。多数从编辑管线出来的 PDF 都不是——因为写 PDF 的自然方式就是把交叉引用表放在末尾,页面对象散落在写入器恰好输出的位置
为什么 PDFium 能读线性化文件却不能写
PDFium 有完整的线性化读取器,却没有对应的写入器。FPDFAvail_* 可用性 API 检测线性化字典、解码提示表、并只向宿主要取它需要的字节范围——PDFium Component 的渐进下载与取消工作流正建在这上面。公开的保存 ABI 却只提供普通拷贝保存和指定目标 PDF 版本的保存,里面没有任何重排对象或产出提示流的东西
PDFium Component 弥合这个缺口的方式,不是重写 PDFium 已经做得很好的部分。TPdf.SaveAsLinearized 先调用既有的 SaveAs,语义保存仍归 PDFium:编辑过的页面、表单字段状态和版本处理都走那条跑了多年的代码路径。随后组件对这份稳定的中介 PDF 跑一遍附录 F 重构,把线性化结果写到你的目标位置。这样拆分意味着线性化永远不用理解注释或表单,只需要懂对象、引用和字节偏移
从 Delphi 调用 TPdf.SaveAsLinearized
TPdf.SaveAsLinearized 有两个重载,一个接 TStream,一个接文件名,两者都接受与 SaveAs 相同的可选 Option: TSaveOption 和 PdfVersion: TPdfVersion 参数,成功时都返回 True。文件重载先写目标目录里的临时文件,再原子替换目的地,保存失败绝不会在原来好文件的位置留半份 PDF
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.Active := True;
// ... 编辑页面、填表单、盖章,管线要做什么都行 ...
if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
raise Exception.Create('Linearized save failed');
finally
Pdf.Free;
end;
end;
参数规则严格是刻意的。saIncremental 会报错,因为增量更新会在文件末尾追加新版本,瞬间毁掉前置布局。低于 PDF 1.2 的目标版本同样被拒;pvUnknown 保留源版本。加密输入需要 saRemoveSecurity,因为写入器要重排对象编号,而 PDF 对象级加密密钥正派生自对象编号。线性化是全量重写,所以既有数字签名一律失效。如果既要签名又要线性化,先线性化、后签名
写入器如何为 Fast Web View 分组对象
线性化不只是把第一页搬到文件顶部。写入器解析当前版本——无论其交叉引用数据是经典表、混合文件还是交叉引用流——并把活动对象流里的每个成员展开成独立对象。然后构建引用图,把对象分进五组:打开文件所需的文档级对象、第一页对象、单个后续页面私有的对象、若干后续页面共享的对象,以及其余一切
图遍历里的陷阱是 /Parent。每个页面字典都往页面树上指,朴素的遍历会顺着这个指针爬到父节点,再经 /Kids 钻进每个兄弟页面及其内容流。一步走错,第一页组里装下了整份文档,文件在技术上线性化了,对范围优先访问却毫无用处。PDFium Component 无视页面 /Parent 链接和被收集页面之外的页面树节点,第一页节因此保持小巧。在一份 51 页、页面树很深的回归样本上,第一页节 5289 字节,占 1105354 字节输出的 0.48%
输入里的对象流和交叉引用流被摊平成普通的 generation 0 间接对象,每条引用重写到新编号,输出采用经典交叉引用节。与压缩对象流相比这要费一些文件体积,但它简化了提示表覆盖,而且是每个懂线性化的查看器都处理的布局。如果某个输出里紧凑比 Fast Web View 更要紧,PDFium Component 另有专职的对象流写入器
线性化字典和两个 xref 节里装了什么
PDFium Component 写出的线性化文件带两个经典交叉引用节,线性化字典就在第一个对象里。第一个 xref 节跟在线性化字典后面,覆盖文档级和第一页对象,查看器只靠文件前部就能渲染第一页。主 xref 节在末尾,覆盖其余一切
线性化字典本身装着查看器在读任何东西之前需要的数字:/L 文件长度、/H 提示流偏移与长度、/O 第一页对象号、/E 第一页节结束位置、/N 页数、/T 主 xref 表首条目的偏移。这些都依赖最终布局,字典却坐在布局之前。用自然宽度的数字写它就形成了循环:长度从 999999 涨到 1000000 多出一个字节,推走后面每个偏移,这可能又改变另一个数字的宽度,如此往复
PDFium Component 的破环办法是为线性化字典预留固定 320 字节。每个对象、提示流、两个 xref 节和各个 trailer 先全部写出;所有偏移定稿后,字典原地填充并补齐到预留宽度。一个微妙细节是 /T:它必须指向主 xref 表的对象 0 条目,而不是它前面那个 xref 关键字。宽容的查看器照样打开文件,严格的验证器却会对关键字偏移亮牌
为什么提示表偏移无视提示流
线性化 PDF 里的提示表偏移按提示流在文件中不存在来解读。附录 F 就这么定义,好让提示流坐在第一页节与其余部分之间,而它自己的大小不会反馈进它包含的数字。对写入器来说,实际后果很容易搞错:任何位于提示流之后的真实偏移,在进入页面偏移或共享对象提示表之前都得先减一截
问题是减多少。诱人的答案是流数据的长度,因为那才像提示内容。正确的答案是整个提示间接对象的长度——包括 N 0 obj、流字典、stream 和 endstream 关键字、以及 endobj。只减数据长度,后面每个偏移就都错了几十个字节。第一页仍能渲染,因为它住在提示流之前,所以这个 bug 经得住快速目检,只在查看器跳到后面某页、请求到一个从错误对象中间开始的范围时才现形
两张提示表都按最高位在前的位打包,同样遵照附录 F。组件像真实客户端那样验证结果:稀疏范围存储只提供 51 页样本的前 1 KiB,PDFium 原生提示处理请求其余部分。最后一页在完整文件从未到场的情况下打开
在 Delphi 里验证线性化 PDF
最便宜的检查是直接问 PDFium。TPdf.InspectAvailability 不加载文档就对流跑可用性 API,其 Linearization 字段报告 plnLinearized、plnNotLinearized 或 plnUnknown
function IsFastWebView(const FileName: string): Boolean;
var
Stream: TFileStream;
Info: TPdfAvailability;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Info := TPdf.InspectAvailability(Stream);
Result := Info.Linearization = plnLinearized;
finally
Stream.Free;
end;
end;
这确认 PDFium 认识该布局,却不证明提示是对的。真正的端到端测试,是把文件经一个限制范围的传输送出、用 TPdfProgressiveDocument 打开靠后的页面——用 PDFium VCL 按需流式打开大 PDF 指南就是这么做的。只取文件的一小部分后页面就能渲染,提示表就在干活。在发布管线里再加一个独立的线性化检查器也值得,各家查看器对放错位置的 /T 或略偏的偏移宽容度不同
记住两个限制。对象物化器仍拒绝 2 GiB 及以上的数组,超大文档暂时需要别的策略。线性化只在服务器支持 HTTP Range 请求时才有收益;在剥掉 Accept-Ranges 的代理后面,线性化文件下载起来和任何其它文件一样
FAQ
TPdf.SaveAsLinearized 能保住数字签名有效吗
不能。TPdf.SaveAsLinearized 重编号并重写每个对象,既有签名因此失效。先线性化,再签名
PDFium Component 能线性化加密的 PDF 吗
只有传入 saRemoveSecurity 时才行。对象级加密密钥依赖对象编号,而写入器要改编号,所以输出不带加密写出
线性化输出支持哪些 PDF 版本
TPdf.SaveAsLinearized 接受保留源版本的 pvUnknown 或 PDF 1.2 起的任何目标版本,拒绝 saIncremental——追加的版本会破坏布局
Fast Web View 输出、渐进加载和可用性 API 都是 PDFium Component for Delphi and C++Builder 的一部分,它为 Delphi、C++Builder 和 Lazarus 项目封装 PDFium