"Please wait... If this message is not eventually replaced by the proper contents of the document, your PDF viewer may not be able to display this type of document." 如果你的文档管线曾接收政府或保险表单,你一定见过这个页面。它不是损坏,而是动态 XFA 表单在缺少 XFA 处理器的查看器中显示的占位页。如今这几乎意味着除桌面 Acrobat 外的每个查看器。归档系统、Web 查看器和自动提取都只能看到一页无用内容。HotPDF 是 losLab 面向 Delphi 和 C++Builder 的 PDF 库,它用转换路径处理这个问题,把 XFA 表单转成任何符合规范的阅读器都能显示的原生 AcroForm 文档。
两种看起来相似的表单模型
AcroForm 定义于 ISO 32000-1 §12.7,它把每个字段存为带 widget 注释和 appearance stream 的 PDF 对象;你看到的页面是真实 PDF 内容,表单数据位于其上。XFA 采用相反方式:表单是 XML 文档,是位于 AcroForm 字典 /XFA 项中的 XDP 包,可见页面在打开时由 XFA 布局引擎生成。在动态 XFA 表单中,文件里的 PDF 页面只是 "Please wait" 占位页,因为真实内容根本从未以 PDF 形式存在。
两种模型在实践中互相排斥:一个文档要么按 XFA 处理,要么按 AcroForm 处理;忽略 /XFA 项的工具只能看到占位壳。ISO 32000-2 通过从 PDF 2.0 废弃 XFA 结束了争论,这也是“趁还来得及把它转换成 AcroForm”已经成为标准接收需求,而不再是罕见需求的原因。
并非每个 XFA 表单都会显示占位页,所以接收管线应先分类再转换。静态 XFA 表单携带预渲染 PDF 页面和 XML,因此可以到处显示,只是在填写时行为不一致;动态表单只携带占位页,必须转换才可用。可靠判别依据是文档本身,而不是扩展名或发送方:在非 Adobe 查看器中能渲染真实内容但仍带 /XFA 项的表单,是静态或混合表单;显示警告页的表单是动态表单。应把每个接收文件分类到其中一个桶中并记录分类,因为两类文件在下游失败方式不同;当支持工单询问空白归档表单时,接收记录写着“dynamic XFA, converted, 47 fields mapped, 2 warnings”,答案就能在几秒内给出。
把已加载 XFA 文档扁平化为原生字段
HotPDF 的转换入口作用于已加载文档。FlattenLoadedXFA 会解析 XFA template 和 data packets,布局表单,并把它重建为真实 PDF 页面上的真实 AcroForm 字段:
var
Pdf: THotPDF;
MappedCount, I: Integer;
Warnings: TStrings;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('dynamic_xfa.pdf');
MappedCount := Pdf.FlattenLoadedXFA(True); // True = fields stay editable
Warnings := Pdf.XFAFlattenWarnings;
for I := 0 to Warnings.Count - 1 do
Log('XFA flatten warning: ' + Warnings[I]); // unmapped elements
Pdf.SaveLoadedDocument('native_acroform.pdf');
Log(Format('Mapped %d fields', [MappedCount]));
finally
Pdf.Free;
end;
end;
应把返回值和警告列表视为输出的一部分,而不是调试噪声。转换本质上有损:XFA 脚本、计算字段和动态 subform 行为没有 AcroForm 等价物,XFAFlattenWarnings 会列出哪些 template 元素无法映射。一个归档转换文件却不归档警告列表的管线,迟早会面对“为什么归档副本中的 totals box 是空的”这个问题,而且没有记录可答。Editable 参数决定生成的 AcroForm 字段是否仍可填写;当下游用户还会继续处理表单时传 True,当目标是固定记录时使用等同于 False 的策略。
转换验证必然同时是视觉和结构验证。在仍会运行 XFA 引擎的桌面 Acrobat 中打开源表单,把它与任何普通查看器中的转换后文件并排比较;每个模板至少用一个代表性的已填写表单检查字段值和布局。结构检查可以确认字段数匹配 MappedCount;只有肉眼能确认 XFA 格式化为 2026-06-11 的日期,没有在 AcroForm 副本中变成原始未格式化值。
从 XDP 侧工作
有时输入不是已填充 PDF,而是表单设计工具导出的 XDP 包,或合作方系统发送的 XDP 包。ApplyXFAAsAcroForm 跳过加载步骤,直接把该包应用到当前文档:
XDPBytes := TFile.ReadAllBytes('benefit-claim.xdp');
MappedCount := Pdf.ApplyXFAAsAcroForm(XDPBytes, True);
同一组调用也覆盖生成方向,也就是你需要生成 XFA 而不是消费它时:AddXFAPacket 附加单个命名 packet,例如 'xdp' 或 'config';SetXFADocument 安装完整单 stream XFA payload;ClearXFAPackets 重置注册;AddXFASignaturePacket 为签署 XML 表单数据本身的工作流嵌入 XAdES 签名材料。2026 年生成 XFA 是小众需求,通常由某个遗留消费端驱动,但合同要求时,这些调用可以把它保持为配置细节,而不是另起一个工具。
另一种扁平化:诚实说明 AcroForm 内容
“Flatten”还有第二层含义,会反复造成混淆:把 AcroForm 字段外观烧录进页面内容流,使文件中不再有交互对象。HotPDF 目前没有提供该操作的 API,最好围绕这个事实规划,而不是在项目中途才发现。库提供的是创建时的字段级锁定,以及文档权限:
// Lock the value at field creation: read-only text field
Pdf.CurrentPage.AddTextField('CaseNumber', 'BC-2026-0117',
Rect(50, 700, 220, 720), 0, [ffReadOnly]);
// Belt and suspenders: restrict form filling document-wide
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aes256;
Pdf.OwnerPassword := 'records-owner';
Pdf.ProtectOptions := [prPrint, prInformationCopy, prExtractContent];
// fill permission withheld: prFillAnnotations is absent from the set
要理解这能做到什么,也要理解它做不到什么。只读字段仍然是表单对象:它会出现在查看器字段面板中,其值可以通过表单 API 提取,重写文件的工具也可以清除该标志。权限标志会进一步提高门槛,但它依赖查看器配合,ISO 32000-1 本身也承认这一点。如果监管方要求归档记录中完全没有表单对象,那么使用 HotPDF 时诚实的工程答案是重新生成文档,读取已加载值后,用普通 TextOut 内容把字段值绘制到新文档上,而不是假装只读标志就是扁平化。当你走权限路线时,记住 CryptKeyLength 必须在 BeginDoc 之前设置;细节见AES-256 加密和权限文章。
归档后果:XFA 和符合性标准
PDF/A 和 PDF/X 都会直接拒绝 XFA,因此进入 ISO 19005 归档的接收管线必须先转换再验证,而且操作顺序固定:加载、FlattenLoadedXFA、保存,然后在 AcroForm 结果上运行归档生成或验证过程。应使用 veraPDF 验证转换后的输出,而不是假定转换等于符合性;转换修复的是表单模型,不是字体、颜色或元数据。AcroForm 侧的字段行为、JavaScript 触发器、提交动作和验证脚本,有自己的工具箱,详见 HotPDF AcroForm 字段和动作文章。
FAQ
为什么我的 PDF 表单只显示 "Please wait" 页面?
它是动态 XFA 表单,而你的查看器没有 XFA 处理器。可见 PDF 内容只是占位页;应使用 FlattenLoadedXFA 转换文档,得到每个查看器都能渲染的页面和字段。
FlattenLoadedXFA 会保留计算和脚本吗?
不会。XFA 脚本和动态布局逻辑无法转换为 AcroForm;表单数据中已有的计算值会作为静态值带过去,XFAFlattenWarnings 会列出每个无法映射的元素。信任输出前应审阅该列表。
HotPDF 能让表单完全非交互吗?
不能通过一键 flatten 实现:没有 AcroForm 内容扁平化 API。需要抗篡改时可组合 ffReadOnly 字段和权限限制;如果零表单对象是硬性要求,应重新生成文档,把值绘制为普通文本。
产品参考
本文中的 XFA 注册、转换和表单 API 都属于面向 Delphi 和 C++Builder 的 HotPDF Component;其文档跟踪了近几个版本持续增长的 XFA 功能集。