技术文章

在 Delphi 中的 FDF、XFDF 和 XFA 表单数据交换

losLab PDF Library 通过三种不同的方式在 PDF 中导入和导出表单数据:用于 AcroForm 字段值的 FDF 和 XFDF、用于注释评论的相同两种格式,以及直接写入表单的完整 XFA XDP 数据包。ExportFormDataToXFDFImportFormDataFromXFDFImportAnnotationsFromFDFExportAnnotationsToXFDFSetXFAFromString 是入口点,且它们每一个都有文件变体和字符串变体

之所以有这么多方法,是因为 PDF 表单数据并非单一形态。填充 of AcroForm 包含字段值,它还可能带有评审注释,而传统的 XFA 表单则嵌入了与这两者都无关的完整 XML 应用程序描述。losLab PDF Library 特意将这三个关注点保留在独立的 API 上,因为合并它们会将错误的数据模型强加给其中至少两个。决定您需要哪一系列是第一个设计选择,并且通常在您了解另一侧的接收系统实际消耗什么时就已经确定了

FDF、XFDF 和 XFA 表单数据有什么区别?

FDF 和 XFDF 在两种不同的语法中承载相同的信息,而 XFA 则是一个完全独立的世界。FDF 是一个微型 PDF 语法文档 (ISO 32000-2 §12.7.8):一个由 << /T (name) /V (value) >> 字典组成的 /Fields 数组,其字符串的转义方式与 PDF 内部字面量字符串的转义方式完全一致。XFDF 是相同数据的 XML 形式 (ISO 19444),是一个 Acrobat 和大多数表单后端原生读写的 <fields> 树。XFA 两者都不是:它是 XML 表单架构(XML Forms Architecture)模板及其数据,存储为 XML 数据包 (XDP),PDF 从 /AcroForm/XFA 引用它。当您对字段值进行互操作时,请选择 FDF 或 XFDF;只有在维护最初作为 XFA 表单编写的文档时,才需要使用 XFA

在 FDF 和 XFDF 内部, losLab PDF Library 划定了第二条界线:字段值与批注。表单数据系列(ExportFormDataToFDFImportFormDataFromFDFExportFormDataToXFDFImportFormDataFromXFDF)读写 /Fields 子树且不触及其他内容。注释系列(ExportAnnotationsToFDFImportAnnotationsFromFDFExportAnnotationsToXFDFImportAnnotationsFromXFDF)则读写 /Annots 子树,这正是 Acrobat 所指的“导出注释”。两者从不重叠,因此导出字段值不会席卷杂乱的评审说明,导入注释也不会打扰用户已经键入的值。小部件在被单击或重新计算时什么则是第三个关注点,在关于 交互式表单操作和 JavaScript 的随附说明中有所涵盖

如何在 Delphi 中从 FDF 或 XFDF 填充 PDF 表单?

加载文档,调用一个导入方法并保存。ImportFormDataFromXFDFImportFormDataFromFDF 各自解析传入的表单数据,通过其完全限定名称将每个条目与 AcroForm 字段匹配,设置该值并返回实际更新的字段数。这两种方法都只更新目标 PDF 中已存在的字段;它们都不会为表单未定义的名称创建新字段,从而防止杂乱或恶意的外部数据文件默默地扩展您的表单

var
  Lib: TPDFlib;
  FieldsSet: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('application-blank.pdf', '');
    // 案件管理系统生成的 XFDF,在磁盘上已经是 UTF-8 编码
    FieldsSet := Lib.ImportFormDataFromXFDF('applicant-1042.xfdf');
    if FieldsSet > 0 then
      Lib.SaveToFile('application-filled.pdf');
  finally
    Lib.Free;
  end;
end;

两个细节决定了非平凡数据能否在往返中幸存。第一个是转义:FDF 将值存储为 PDF 字面量字符串,因此包含括号、反斜杠或不可打印字节的值在到达时被包裹在 ISO 32000-2 §7.3.4.2 中定义的八进制转义中,而 losLab PDF Library 在导入时反转该转义,以便括号和斜杠作为用户输入的字面字符恢复。第二个是编码:基于文件的方法将 XFDF 和 FDF 作为 UTF-8 读写,这正是 XFDF 声明所承诺的,也是任何非 ASCII 字段值(带重音的名称、货币符号、CJK 地址)在不损坏的情况下进行往返所需的。如果您自己构建 XFDF,请声明 UTF-8 并写入 UTF-8,导入将会正常接收

分层字段、多值选择和富文本

分层字段名称是幼稚的导出器最容易出错的地方。AcroForm 使用带点的完整标题(如 Applicant.FullName)来定位嵌套字段,但 XFDF 不会将该带点的字符串放在单个 name 属性中;ISO 19444 对其进行嵌套,表示为 <field name="Applicant"><field name="FullName">。losLab PDF Library 在导出时将带点的标题拆分为嵌套的 <field> 元素,并在导入时将嵌套元素重新组装回完整标题,从而使两个方向保持对称。在导出时,它还会跳过非终端的父字段,因为 AcroForm 中的父节点仅承载命名级别而自身没有值;发送它会产生一个与真实表单不匹配的空 <value></value>。多选列表框是第二个陷阱:一个选择字段可以同时保存多个选定的值,XFDF 将其表示为重复的 <value> 元素,而 FDF 将其表示为 /V 数组,losLab PDF Library 仅在字段真正是多选选择时才将其拆分为多个值,因此普通的多行文本字段会保留其换行符,而不是破碎成虚假的值

富文本是第三点,也是人们最常弄错的一点。带格式的字段将其标记作为 XHTML 子树存储在 RV 条目中,而 XFDF 将其承载在 <value-richtext> 中。losLab PDF Library 将该子树作为活动 XML 片段写入,而不是对其进行转义,因此下游工具读取的是真实的富文本,而不是一串可见的标签;在导入时,它保留了原始的 RV 子树,并且仍将纯 <value> 应用于 V。当字段同时提供两者时,纯值在 V 中胜出,而富文本则在 RV 中并行,这是阻止富文本导入悄悄重写另一个工具已经设置的值的互操作性规则。当您的富文本用于向辅助技术传达文档结构时,请按照 标记 PDF 和可访问性结构 说明中讨论的阅读顺序的相同方式处理

var
  Lib: TPDFlib;
const
  XFDF =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">' +
    '<fields>' +
    '  <field name="Applicant">' +
    '    <field name="FullName"><value>Alice Example</value></field>' +
    '  </field>' +
    '  <field name="Skills">' +
    '    <value>Delphi</value><value>PDF</value>' +
    '  </field>' +
    '  <field name="Notes">' +
    '    <value>See attachment</value>' +
    '    <value-richtext><body><p>See <b>attachment</b></p></body></value-richtext>' +
    '  </field>' +
    '</fields></xfdf>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('intake.pdf', '');
    // Applicant.FullName 重新组装;Skills 填充 /V + /I 数组
    // Notes 从  获取 V,从  获取 RV
    Lib.ImportFormDataFromXFDFString(XFDF);
    Lib.SaveToFile('intake-filled.pdf');
  finally
    Lib.Free;
  end;
end;

如何使批注在 FDF 或 XFDF 中进行往返?

注释通过注释系列进行传输,且支持的子集被刻意缩小。ExportAnnotationsToXFDFImportAnnotationsFromXFDF 及其 FDF 对应项,承载了能够真正干净地进行往返的字段的文本样式标记:注释子类型、它的矩形、它基于 0 的页面索引、T 中的作者、Subj 中的主题、纯 Contents,以及颜色(它在 PDF 的 /C DeviceRGB 三元组和 XFDF #RRGGBB 属性之间进行映射)。导入仅接受已知的元素名称,并跳过任何页面超出范围或矩形缺失的条目,因此手工编辑或国外的 XFDF 无法将格式错误的批注放入文档中。该子集尚不支持的内容值得明确说明:富文本内容 (RC)、弹出窗口、象限点、墨水列表、顶点和烘焙的外观流目前均超出了范围,因此高亮几何形状和自定义图章外观在此路径下将无法幸存。要确认实际落地的具体内容,请遍历页面注释或 文本搜索和页面元素枚举 中包含的更广泛的元素树

var
  Src, Dest: TPDFlib;
  Xfdf: WideString;
begin
  Src := TPDFlib.Create;
  Dest := TPDFlib.Create;
  try
    Src.LoadFromFile('reviewed.pdf', '');
    Xfdf := Src.ExportAnnotationsToXFDFString;   // 仅限  子集
 
    Dest.LoadFromFile('clean-copy.pdf', '');     // 页面必须通过索引对齐
    Dest.ImportAnnotationsFromXFDFString(Xfdf);
    Dest.SaveToFile('clean-copy-commented.pdf');
  finally
    Dest.Free;
    Src.Free;
  end;
end;

使用 SetXFAFromString 写入完整的 XFA XDP 数据包

XFA 是个特例,SetXFAFromString 是您一次性写入整个内容的方法。该方法接收一个完整的 XML 数据包(带有模板和数据集包的 <xdp:xdp> 文档),并将其存储为由 /AcroForm/XFA 引用的流。当文档没有 AcroForm 容器时,losLab PDF Library 会根据需要创建它,因此您不必为了给 XFA 提供挂载点而添加一个一次性的字段,并且它会丢弃其保留的任何已解析的 XFA 状态,以便稍后的读取能反映您刚刚写入的数据包,而不是陈旧的缓存。因为 XFA 数据包是 XML 并且不一定是 UTF-8,因此该库在解析前会检测 UTF-16 字节顺序标记并正确解码数据包,这对于默认使用 UTF-16 的工具生成的数据包至关重要

一旦数据包就位,GetXFAFormFieldValue and SetXFAFormFieldValue 就会通过其脚本对象模型(Scripting Object Model)路径定位到单个字段。losLab PDF Library 接受标准 SOM 根以及裸相对路径,因此 form1.FullName$data.form1.FullNamexfa.datasets.data.form1.FullName 都会解析为相同的数据节点,并且模板端也以相同方式接受 $templatexfa.template。当 SOM 路径是由始终发出完全限定格式的另一个系统生成时,这种宽容度至关重要

var
  Lib: TPDFlib;
const
  Xdp =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">' +
    '  <template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">' +
    '    <subform name="form1">' +
    '      <field name="FullName"><ui><textEdit/></ui></field>' +
    '    </subform>' +
    '  </template>' +
    '  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">' +
    '    <xfa:data><form1><FullName>Alice Example</FullName></form1></xfa:data>' +
    '  </xfa:datasets>' +
    '</xdp:xdp>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetXFAFromString(Xdp, 0);      // 如果不存在 AcroForm 容器则创建
    // 使用 SOM 路径通过数据 DOM 读回
    if Lib.GetXFAFormFieldValue('form1.FullName') = 'Alice Example' then
      Lib.SetXFAFormFieldValue('form1.FullName', 'Bob Example');
    Lib.SaveToFile('xfa-packet.pdf');
  finally
    Lib.Free;
  end;
end;

坦率地说,字段值可以通过 FDF 和 XFDF 完美地往返,包括层级结构、多选和富文本;注释评论则作为具有明显空白的已定义文本标记子集进行往返;而 XFA 则是作为整个数据包进行读写,并在顶部带有每个字段的 SOM 访问。将格式与使用者相匹配,为您手动构建的任何内容声明 UTF-8,并记住导入方法只触及已符合该文档的字段和批注。此处描述的表单数据、批注和 XFA 方法是适用于 Delphi 和 C++Builder 的 losLab PDF Library 的一部分,其参考文档中包含了每个导出和导入入口点的完整参数列表