是的——PDFium 支持 XFA 表单,但有一个重要的划分。PDFium 组件可以检测 XFA 表单并使用标准 DLL 将其 template、datasets 和 config 数据包提取为原始 XML,且具有零运行时依赖。运行“动态”XFA 表单(将其 XML 描述布局到活动的、交互式的页面中)是一个独立的问题,此外还需要启用 V8 的构建。社区一直在问 PDFium 是否“支持 XFA”,就好像它是一个非黑即白的功能一样,坦诚的答案是,读取 XFA 数据与渲染 XFA 表单是两件截然不同的事情,且具有非常不同的依赖足迹
本文将通过 PDFium 组件(Delphi 和 C++Builder 下围绕 PDFium 的原生 VCL 包装器)详细介绍这两个部分。首先是检测和静态提取路径——FormType、XFA 属性和 GetXfaPacket* 读取器,然后是带有 V8 约束的动态运行时路径,最后是当部署的 DLL 无法运行该引擎时让您优雅失败的能力探测
PDFium 支持 XFA 表单吗?
将它视为两个功能,而不是一个。静态 XFA 提取始终可用:支持 GetXfaPacketCount、GetXfaPacketName 和 GetXfaPacketContent 的底层 FPDF_GetXFAPacket* 导出已编译到每个相当现代的 pdfium.dll 中,甚至是关闭了 XFA 的构建。这就是为什么从表单中提取原始 template、datasets 和 config XML XML 完全不携带任何运行时依赖的原因——它是纯结构化的读取,不涉及引擎。动态 XFA 是另一个功能:实际运行表单、评估其计算并让 XFA 布局引擎对其进行分页。该路径仅存在于在 V8 JavaScript 引擎之上编译了 XFA 支持的 PDFium 构建中,而这正是大多数“PDFium 是否支持 XFA”讨论真正争论的部分
标准清晰地界定了这种划分。XFA 在 Adobe XFA 3.3 (2012) 中定义在核心 PDF 语法之外,并且 PDF 1.7 §8.6.7 描述了 XFA 表单是如何在 PDF 内部承载的:表单包存在于 AcroForm 字典的 /XFA 条目下,而必须在有意义之前由 XFA 处理器进行布局的文档在目录中设置 /NeedsRendering true。这两个标记正是 PDFium 组件寻找的,也正是您在推断从未见过的文件时可以依赖的
XFA 与 AcroForm 有什么不同?
AcroForm 是经典的 PDF 表单:小部件注释(文本字段、复选框、单选按钮组)锚定在普通 PDF 页面上的固定坐标处。您看到的页面就是文件中的页面,而字段坐落在其上方。XFA 采用了相反的方法。交互式表单完全在 XML 中描述,在完整的(动态)XFA 文档中,PDF 页面实际上是一个占位符——真实内容是在打开时由 XFA 引擎通过读取 XML 模板并对其进行布局而生成的,并根据数据进行增长或收缩。这就是 /NeedsRendering true 所宣告的:没有 XFA 处理器,静态页面可能为空,或者仅带有“请在兼容的查看器中打开”的通知
XFA 有效载荷本身是一组命名的包,其中三个包承载了大多数集成所需的一切。template 包是表单定义——字段、布局、计算和脚本。datasets 包保存实际 dwellings 实例数据,即作为 XML 树填写的字段值。config 包携带 XFA 引擎的处理指令。还存在其他包(localeSet、connectionSet、xdp 包装器),但对于审计表单或将其中的数据迁移出来,模板加数据集通常就是全部工作。因为该数据只是位于流中的 XML,读取 it 永远不需要执行任何操作,这是静态提取无依赖的根本原因
在 Delphi 中检测 XFA 表单
检测以每个 PDFium 组件任务相同的方式开始:设置 FileName,翻转 Active := True,并检查它是否实际打开。Active := True 永远不会引发异常——缺失文件、错误密码或缺失 DLL 只是让 Active 保持为 False,因此该防范是必须的。一旦文档打开,FormType 会告诉您它使用哪个表单模型。两个 TPdfFormType 值是 XFA:ftXfaFull 是完整的、动态的 XFA 表单(需要渲染的那种),而 ftXfaForeground 是 XFAF(前台子集,其中 XFA 内容绘制在其他方面正常的 AcroForm 页面上方,以便静态页面仍有意义)。XFA 布尔值是“这是任一风味的 XFA 文档”的快捷方式。如果您还处理 XFAF 或 AcroForm 文档上的小部件级字段,其机制在 PDFium 表单字段导航指南 中有所涵盖
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'application-form.pdf';
Pdf.Active := True;
if not Pdf.Active then
Exit; // 损坏、加密或 DLL 缺失
case Pdf.FormType of
ftXfaFull:
Log('Full (dynamic) XFA form');
ftXfaForeground:
Log('XFAF: XFA layered over static AcroForm pages');
ftAcroForm:
Log('Classic AcroForm');
else
Log('No interactive form');
end;
if Pdf.XFA then
Log('Document carries an XFA packet payload');
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
将 XFA 数据包提取为原始 XML
提取是高价值、零依赖的那一半。用 XfaStaticReadable 保护——它确认加载的 DLL 中解析了数据包读取器导出——然后按索引遍历包。GetXfaPacketCount 返回表单携带的包数,GetXfaPacketName 给出每个包的名称,而 GetXfaPacketContent 返回原始字节,对于众所周知的包来说它们是 UTF-8 XML。当您已经知道想要哪个包时,命名辅助方法 GetXfaTemplate、GetXfaDatasets 和 GetXfaConfig 会为您解析名称并直接交回 TBytes。对于数据迁移任务,最重要的是 GetXfaDatasets:它向您提供提交的字段值作为一个 XML 文档,您可以解析它并映射到您自己的模式中,这与 从 PDF 中提取文本 之后要做的下流工作相同
procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
I, Count: Integer;
PacketName: string;
Content, Datasets: TBytes;
begin
if not Pdf.XfaStaticReadable then
Exit; // 数据包读取器导出不存在
Count := Pdf.GetXfaPacketCount;
for I := 0 to Count - 1 do
begin
PacketName := Pdf.GetXfaPacketName(I);
Content := Pdf.GetXfaPacketContent(I); // 原始 UTF-8 XML
TFile.WriteAllBytes(
TPath.Combine(Dir, PacketName + '.xml'), Content);
end;
// 或通过名称直接跳转到填充的字段值
Datasets := Pdf.GetXfaDatasets;
if Length(Datasets) > 0 then
ParseAndMap(Datasets);
end;
为什么动态 XFA 渲染需要 V8 引擎?
首先说结论:因为动态 XFA 表单是一个小小的程序,而不是静态图画。模板包携带了 FormCalc 和 JavaScript 计算、验证以及事件脚本,而 XFA 布局引擎必须执行它们来决定页面长什么样。PDFium 仅在 V8(Google 的 JavaScript 引擎)之上编译了具有 XFA 支持的构建版中实现该引擎——这也是启用 V8 的 DLL(pdfium32v8.dll 或 pdfium64v8.dll)明显大于标准构建版的原因。没有 V8 就没有人来运行脚本,因此就没有渲染好的页面可展示,只有您已经可以静态读取的原始 XML
选择该构建版有一个您无法规避的硬性约束:这是每个进程一次性的决定。单个进程不能同时加载标准 pdfium.dll 和启用 V8 的构建版——它们导出相同的符号,且 V8 保持着全局隔离区状态——因此 PDFium 组件必须在库被首次加载前选择 V8 构建版。为了使该过程自动化,LoadDocument 会预先扫描文件中的 /XFA 和 /NeedsRendering true 标记(与暴露为独立的 PdfFileLooksXfa 函数相同的检查),并在发现它们时在加载前将 EnableV8Engine := True。一旦普通的 pdfium.dll 已经驻留,该切换就无法再生效,而这种情况正是运行时缺失路径所报告的
begin
// 在首次加载 PDFium 前窥视 XFA,以便
// 仍能选择启用 V8 的构建。PdfFileLooksXfa 扫描
// /XFA 和 /NeedsRendering true 而不解析整个文件
if PdfFileLooksXfa('dynamic-form.pdf') then
EnableV8Engine := True;
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'dynamic-form.pdf';
Pdf.Active := True;
if Pdf.Active and Pdf.XFA then
begin
if Pdf.XfaRuntimeAvailable then
RenderXfaPages(Pdf) // 引擎处于活动状态:动态布局
else
ExtractXfaData(Pdf, 'C:\out'); // 回退到静态数据包
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
优雅地处理缺失的 XFA 运行时
三个探测会确切地告诉您拥有多少 XFA 功能,诚实的代码会检查它们而不是假设。XfaFeaturesAvailable 是对加载的 DLL 中是否存在 XFA/V8 辅助导出进行的全局检查。XfaStaticReadable 针对每个文档确认数据包读取器已解析——提取的防范。XfaRuntimeAvailable 是强有力的一项:仅当该特定文档实际激活了 XFA 引擎时它才为 true,这意味着它是 XFA、加载了 V8 构建版,并且 FPDF_LoadXFA 成功了。每当部署的 DLL 缺乏 XFA 或已经加载了标准构建版时,文档都可以报告 XFA = True 但 XfaRuntimeAvailable = False
当 XFA 文档打开但引擎无法运行时,PDFium 组件不会冒险。它将表单固定在安全的静态模式,而不是请求它无法兑现的动态运行时,并且它会引发一次 OnXfaRuntimeMissing,以便宿主做出反应——通常是通过告诉用户重启并使用启用 V8 的构建版,同时在此期间仍提供静态可读的数据。连接该处理程序是干净的回退与静默显示为空白的表单之间的区别
procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
// 在 XFA 文档打开但引擎无法运行时触发一次:
// 普通的 pdfium.dll 已经加载,或构建版缺乏 XFA/V8
ShowMessage('This is a dynamic XFA form. Restart with the ' +
'V8-enabled build to render it; its packet data is still readable.');
end;
在您自己的产品中也要坦诚面对这一边界。如果您从未发运启用 V8 的 DLL,动态 XFA 表单将永远无法为您的用户渲染——但您仍然可以检测它们、提取每个数据包并拉出提交的数据,这对于主要旨在从遗留的政府和银行表单中获取数据(而不是进行交互式展示)的大量任务来说已经足够了。为真正需要活动的、可填充表单的场景保留更重的 V8 构建版,并让功能探测将每个文档路由到它实际能走的路径。如果您的提取管道还处理嵌入文件,相同的只读哲学也适用于 Delphi 中的 PDF 附件
这里展示的 FormType、XFA、GetXfaPacketCount、GetXfaPacketName、GetXfaPacketContent、GetXfaTemplate、GetXfaDatasets、GetXfaConfig 和功能探测 API 都是适用于 Delphi 和 C++Builder 的 PDFium Component 的一部分