PDFium Component 在 Delphi 里经 FPdfDocumentXfaFlatten 单元的 FlattenPdfXfaToAcroForm 把静态 XFA 表单转成普通 AcroForm 字段。函数读取 XFA 的 template 与 datasets packet,把每个绝对定位字段映射成一个带自己外观流的 AcroForm 终端字段,再把结果作为增量修订版追加——转换出的 PDF 在从未支持过 XFA 的阅读器里照常作为表单打开
动机通常是张支持工单,而不是路线图条目。某份政务或保险表单出自一个存成 XFA 的设计器,在某个桌面阅读器里好使,到哪儿都打开成空白页、说文档需要更新的阅读器。XFA 在 PDF 2.0(ISO 32000-2)里已废弃,浏览器和移动阅读器从未实现它,PDFium 自己也要 V8 启用的构建才渲染 XFA。静态 XFA 表单——设计时布局就定死的那种——在 template 里带着足够的几何信息,可以重建为 AcroForm 字段,这正是这个转换器处理的那一片
FlattenPdfXfaToAcroForm 实际产出什么?
FlattenPdfXfaToAcroForm 产出的是追加到未改动源副本之上的字节级 AcroForm 修订版,绝不是重写原文件。PDFium 没有创建表单字段的公开 API,所以组件干这活完全不经 PDFium:它用自己的对象读取器读活动修订版,把字段字典、widget 注解、外观 XObject 和 /Helv 字体资源构建成原始 PDF 对象,再按 ISO 32000-1 §7.5.6 的精神以稀疏增量更新序列化
新的 /AcroForm 字典带 /Fields、/DA 和 /DR,不带 /XFA 条目。源的 AcroForm 若是间接对象,该对象被原位重写;内联字典则在目录里被替换。无论哪种,活动修订版不再指向 XFA packet,两种模型都懂的阅读器会打开 AcroForm 字段、不再去摆 XFA 表单的版。原始 XFA 字节仍在更早的修订版里——增量更新的本义如此——但当前修订版没有任何东西引用它们
uses
System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;
procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
Source: TFileStream;
Options: TPdfXfaFlattenOptions;
OutputPdf: TBytes;
Report: TPdfXfaFlattenReport;
Fields: TPdfXfaFlattenFields;
begin
Options := TPdfXfaFlattenOptions.Default;
Options.MaxFields := 1024; // 默认 512,允许范围 1..4096
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
try
if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
finally
Source.Free;
end;
TFile.WriteAllBytes(TargetFile, OutputPdf);
Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
[Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;
对任何被拒或畸形的输入,函数返回 False 并填 Report.ErrorText,退出时恢复源流位置。取消是唯一向外传播的异常路径:Options.CancellationToken 触发时,报告填好之后重新抛出 EPdfOperationCancelled。TPdfXfaFlattenReport 还带 SourceSha256 与 OutputSha256,批处理任务要证明哪个输入产出哪个转换文件时正好用。TPdfXfaFlattenOptions.Default 设 8 MiB packet 预算、512 个字段和 512 MiB 输出上限;源不得加密
读 template 与 datasets packet
PDFium Component 接受 ISO 32000-1 §12.7.8 允许的 /XFA 条目两种布局:包名与流引用交替出现的数组形态,以及单流 XDP 形态——一个流装下整个 <xdp:xdp> 文档。packet 流可以未压缩,或为不带 predictor 的 /FlateDecode,解压受 MaxPacketBytes 约束
单流情形是朴素 XML 处理翻车的地方。设计器常给 <xfa:datasets> 带命名空间前缀,而 template 不带前缀,按字面标签名切块就会找到一个 packet、错过另一个。转换器按本地名匹配元素与属性名,之前另有一道检查保证开闭标签按完整前缀名配对。只想看 packet 不想转换的话,用 PDFium VCL 检测 XFA 表单并提取其 packet的指南覆盖那条只读路径
字段值来自两处。template 里的 <value> 元素供默认值,datasets packet 供填好的数据,按字段名绑定。真实表单里字段名会重复,绑定器拿具名 <subform> 祖先链与数据里的祖先路径比较,取最长的无冲突匹配。两个叶字段在不同 subform 下共享名字时,AcroForm 的 /T 变成 SubformPath.Name,转换后两个字段保持不同
choiceList 为什么需要显式的 Combo 标志?
XFA 与 PDF 在选择字段上的默认恰好相反,一个不复制任何标志的转换器会把每个下拉变成列表框。默认 open="userControl" 或 open="onEntry" 的 XFA <choiceList> 是下拉;不带标志的 PDF /FT /Ch 字段是可滚动列表框,只有 Combo 标志(ISO 32000-1 表 230 的位 18,值 131072,即 PDFium 命名的 FPDF_FORMFLAG_CHOICE_COMBO 那一位)才让它成为组合框。因此转换器对下拉情形设置 Combo 标志,只在 template 写着 open="always" 或 open="multiSelect"——列表永久展开的两种——时留空标志。3.181.1 之前的构建把 Combo 与 Pushbutton 的值搞反过、还找过非标准的 open="on",用那些构建压平过的表单值得重跑
其余 UI 映射遵循 ISO 32000-1 §12.7.4 的字段标志表:
textEdit、numericEdit和dateTimeEdit变成/FT /Tx;多行文本字段带 Multiline 标志(/Ff 4096,表 228)和按行拆分的外观流passwordEdit变成带 Password 标志(/Ff 8192)的/FT /Tx,外观为每个 Unicode 标量画一个星号——不是每个 UTF-16 码元checkButton变成/FT /Btn复选框,绑定数据为1时值是/Yes,否则/Off<exclGroup>变成一个/FT /Btn /Ff 32768的 radio 父字段(表 226),带/V、/DV、/Opt和/Kids,每个成员另有带自己/AS和两态/AP的子 widgetimageEdit变成外观绘制图像的 pushbutton;基线 JPEG 在/DCTDecode下原样嵌入,PNG 则解码后重编码为/FlateDecode像素,PNG 带 alpha 或tRNS透明时外加/SMask
不是纯可打印 ASCII 的值——包括换行——写作 <FEFF...> 的 UTF-16BE 字符串,所以日文名字或两行地址能活着走完全程。Fields 输出镜像写出的内容,正好当审计线索
procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
KindNames: array[TPdfXfaFlattenFieldKind] of string = (
'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
I: Integer;
F: TPdfXfaFlattenField;
begin
for I := 0 to High(Fields) do
begin
F := Fields[I];
Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
[F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
case F.Kind of
fxfkChoice:
Writeln(Format(' %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
fxfkRadio:
Writeln(Format(' %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
fxfkImage:
Writeln(Format(' %d image bytes', [Length(F.ImageData)]));
else
Writeln(' value "' + F.Value + '"');
end;
end;
end;
几何、度量单位与页面路由
XFA 几何转换到 PDF 用户空间的办法,是把每个字段对照其所在页面的 MediaBox 高度翻转——XFA 从页顶向下量,PDF 从页底向上量。嵌套 <subform> 元素的偏移沿 template 向下累加,每个字段最终必须有正的宽和高,否则转换被拒
度量接受真实设计器会发出的单位:裸数字和 pt,外加 in(72 点)、mm(72 / 25.4)、cm(72 / 2.54)、pc(12)、px(0.75)和 mp(0.001,毫点)。解析与 locale 无关,德语 Windows 不会把 8.5in 变成胡话。未知单位后缀被拒,而不是当点数读——当点数读会无声把字段放错地方
页面路由跟随 <pageSet>:兄弟 <pageArea> 元素按声明顺序从 1 编号、认领嵌在各自之下的字段;没有 <pageArea> 祖先的字段留在第一页。每页有自己的 /Annots 更新。路由超出 PDF 最后一页的字段是错误,而不是把 widget 丢在第一页
转换器拒绝哪些 XFA 表单?
凡不跑 XFA 布局引擎就无法知道布局的,转换器一律拒绝。也就是说:<subform> 或 <exclGroup> 上 layout 属性不是 position 的(流式的 tb、lr-tb 之类,动态表单正是靠它们搭起来的)、任何 <script> 元素、以及上列七种之外的任何 UI 种类。运行时长出页面的动态表单属于另一条工具链;PDFium Component 的动态 XFA 重分页与页数一文展示了那种实时布局涉及什么
拒绝是刻意的。流式 subform 没有固定坐标,任何"转换"都得编位置;带脚本的表单常算总额或切换可见性,静态 AcroForm 复现不了。产出一个看起来完整、却无声丢了一次合计的 PDF,比 Report.ErrorText 里一条清楚的错误更糟。XFA 3.3 规范定义的远不止这个子集,转换器也不假装相反
写并验证 AcroForm 修订版的坑
构建写入器时冒出两个问题,对手工产出增量更新的人都适用。第一:目录必须始终在更新集合里,哪怕它的字节没变——AcroForm 是在别处重写的间接对象时就是这样。稀疏更新写入器从它携带的对象推导 trailer 的 /Root,不带根对象的更新产出的修订版,阅读器无处锚定
第二是验证。经 PDFium 封装加载转换后的文件,只要文档带 /AcroForm 就会触发 form 环境初始化,而测试环境里的宿主 pdfium.dll 缺 FORM_GetTextDirection 导出,初始化在检查到任何一个字段之前就失败了。因此读回检查用组件自己的对象读取器——解析活动修订版、不碰 form 层
uses
System.SysUtils, System.Classes, FPdfCompress;
function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
Stream: TBytesStream;
Objects: TPdfActiveObjects;
Revision: TPdfSparseRevisionInfo;
ErrorText: string;
begin
Stream := TBytesStream.Create(Pdf);
try
if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
Objects, Revision, ErrorText) then
raise Exception.Create(ErrorText);
// 活动目录必须引用一个 AcroForm;转换器写的内联 AcroForm
// 不带 /XFA 条目
Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
finally
Stream.Free;
end;
end;
外观流刻意简单:文本与选择值用 10 点的 Helvetica,复选框一个描边方框,radio 选中态一个实心内方框。它们让转换出的表单哪儿都能读,但不是原始 XFA 渲染的像素级拷贝——template 里定义的字体、颜色和边框不带过来。表单必须与设计器输出长得一模一样的话,先用 XFA 构建渲染一遍、对比之后再把生产管线切过来
FAQ
FlattenPdfXfaToAcroForm 需要 V8 或 XFA 版的 PDFium 吗?
不需要。FlattenPdfXfaToAcroForm 用 PDFium Component 代码解析 packet 并写出修订版,所以标准 pdfium.dll 就能跑,也不执行任何 XFA 脚本
转换器能处理加密的 XFA 文档吗?
不能直接处理。加密源以错误拒绝,所以先解密文档,再转换解密后的副本
转换出的字段还能编辑吗?
能。输出包含真正的 AcroForm 文本、复选框、radio、选择与 pushbutton 字段,带值和外观流,用户可以在任何支持 AcroForm 的阅读器里继续填表
静态 XFA 转换、XFA packet 提取和 V8 启用的 XFA 运行时都是 PDFium Component for Delphi and C++Builder 的一部分,它为 Delphi、C++Builder 与 Lazarus 项目封装 PDFium