技术文章

在 Delphi 中使用 PDFium 检测和提取 XFA 表单

是的——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 无法运行该引擎时让您优雅失败的能力探测

一份携带 XFA 包的 PDF 在 Delphi 中供给两种 PDFium 能力:任何现代 DLL 都能跑的静态包提取,以及还需启用 V8 构建的动态渲染
静态提取把 template、datasets 和 config 包当普通 XML 读,不需要引擎;动态 XFA 渲染则额外需要启用 V8 的 PDFium 构建

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

锚定在固定页面坐标的 AcroForm 部件,对比 PDFium 中的 XFA 表单——其 template、datasets 和 config 包在 /NeedsRendering true 下由 XFA 引擎排版
AcroForm 字段安放在成品页面上;XFA 表单把定义作为命名 XML 包挂在 AcroForm 的 /XFA 条目下,并可能置 /NeedsRendering 为 true

选择该构建版有一个您无法规避的硬性约束:这是每个进程一次性的决定。单个进程不能同时加载标准 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 的构建版,同时在此期间仍提供静态可读的数据。连接该处理程序是干净的回退与静默显示为空白的表单之间的区别

Delphi 中 PDFium 处理动态 XFA 文档的决策流程:探测 XfaRuntimeAvailable 决定用 V8 渲染还是钉死静态模式;触发一次 OnXfaRuntimeMissing 并仍提取包
XfaRuntimeAvailable 为 true 时 V8 引擎渲染活动页面;否则表单被钉在静态模式,OnXfaRuntimeMissing 触发一次,包仍可读取
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 的一部分