losLab PDF Library 通过 XFA 3.3 SOM 路径表达式读写动态 XFA 表单中的重复数据节点:GetXFAFormFieldValue 和 SetXFAFormFieldValue 接受 $data、$record 和 !data 根、[*] 出现通配符、后代和子通配符选择器、 parent 属性、#dataGroup/#dataValue 类选择器,以及简单的 FormCalc 风格谓词,因此 Delphi 程序只需一次调用即可更新政府税务表单中的每一行发票;本文是寻址参考,关于在文件之间移动整个数据集,请参阅关于 FDF、XFDF 和 XFA 表单数据交换的配套文章
当表单不再是扁平的时,问题就出现了;以动态 XFA 表单构建的增值税申报表或报关单没有命名为 Total_Price_1 到 Total_Price_12 的 12 个字段,它只有一个在模板 DOM 中声明一次,并根据数据需要实例化多次的 Detail 子表单;这些值活在第二个树中,即数据集包内的 Data DOM,其中每个行项目都是 <Receipt> 下重复的 <Detail> 元素;扁平字段名称无法寻址“第三行上的价格”或“超过 200 的每一行”;这正是 Adobe XFA 3.3 规范的 Scripting Object Model 章节赋予 SOM 表达式的工作,也是 losLab PDF Library XFA 字段值 API 所使用的语法
如何从 Delphi 寻址 XFA 数据节点?
每个数据集路径都从一个根开始,losLab PDF Library 可交替接受标准拼写:$data 是 xfa.datasets.data 的 XFA 3.3 简写形式,!data 是以 xfa.datasets 为根的简写形式,而在不使用记录处理的包中,$record 解析为外部数据记录,即数据节点下的第一个元素;像 SetXFAFormFieldAccess 这样的模板侧 API 以相同方式接受 $template 和 xfa.template;段可以是点分隔的或斜杠分隔的($data.Receipt.Tax 或 $data/Receipt/Tax),这很重要,因为 GetXFAFormFieldNames 将字段枚举为斜杠分隔的路径,可以直接反馈到值 and 模板 API 中;根据规范,出现索引是基于零的,因此 Detail[0] 是第一行;两条转义规则可保持不寻常的名称可寻址:在点分隔路径中,\. 表示字面句点(Line\.Item),而斜杠分隔路径将句点视为普通字符;携带自己 XML 命名空间前缀的业务数据通过本地名称进行匹配,因此 <m:Receipt> 仍对应于 Receipt
var
Lib: TPDFlib;
Tax: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('vat-return.pdf', '');
// Equivalent addresses for the same data node
Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('$record.Tax');
// Slash form with a zero-based occurrence index
Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
Lib.SaveToFile('vat-return-updated.pdf');
finally
Lib.Free;
end;
end;
如何以编程方式填充 XFA 表单中的重复行?
[*] 出现通配符是批量工具;在数字索引选择一个兄弟节点的地方,[*] 选择所有同名兄弟节点,因此 $data.Receipt.Detail[*].Total_Price 一次性寻址每个行项目上的价格字段;在读取时,GetXFAFormFieldValue 返回由 | 分隔符连接的所有匹配节点的值;在写入时,SetXFAFormFieldValue 用相同的值更新每个匹配节点并在成功时返回 1;不匹配任何内容的路径读回为空字符串,这是在写入前探测分支是否存在的廉价方式
var
Lib: TPDFlib;
Prices: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('invoice.pdf', '');
// All Detail rows at once: '250.00|60.00'
Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
// Reset one column across every repeated row in a single call
Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
Lib.SaveToFile('invoice-updated.pdf');
finally
Lib.Free;
end;
end;
后代、子通配符和父选择器
当您知道字段名称但不知道其确切深度时,三个结构选择器可以覆盖这些情况;后代选择器 .. 匹配当前节点下方任何深度的节点:$data..Total_Price 在数据根下方的任何地方找到第一个 Total_Price,并且通过后代路径进行的写入会更新该第一个匹配节点;注意边界:后代匹配后的出现索引不会跨越兄弟分支,因此如果 $data..Total_Price[0] 在第一个 Detail 行内解析,$data..Total_Price[1] 返回空,而不是跳转到下一行;当您需要所有行时,请使用 Detail[*];子通配符 .* 匹配每个直接子节点,并允许其余段进行过滤:$data.Receipt.*.Total_Price 到达 Receipt 的每个子分支内部 of Total_Price,而不会同时选择直接位于 Receipt 自身上的同名汇总字段;最后, parent 属性向上攀升一级,XFA 3.3 刻意将其与 .. 分开:$data.Receipt.Detail[1].parent.Tax 从第二个行项目开始,向上一步到达 Receipt,然后落在其同级的 Tax 值上
类选择器和谓词:#dataGroup、#dataValue、.[expression]
#class 语法通过对象类而不是名称来寻址 Data DOM 节点,当 XML 标签包含难以拼写为脚本名称的字符时,这是实用的途径;losLab PDF Library 将 #dataGroup 映射到拥有元素子节点的元素,将 #dataValue 映射到没有元素子节点的叶元素,两者都与数字或 [*] 出现相结合:$data.Receipt.#dataGroup[0].#dataValue[0] 选择 Receipt 下第一个组内的第一个值;谓词选择器 .[expression] 按内容过滤同名兄弟节点;支持的谓词是子节点值与字面量的简单比较,具有关系运算符(如 >、< 和 <=);当谓词匹配多行时,读取以 | 连接返回,写入更新每次匹配
// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
'$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');
// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
'$data.Receipt.Detail.[Total_Price <= 200].Description');
// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
'$data.Receipt.#dataGroup[0].#dataValue[0]');
哪些 XFA SOM 功能不受支持?
losLab PDF Library 中的 SOM 支持范围仅限于数据集和模板字段路径,且边界是明确的,而不是尽力而为的;预先了解它们可以避免调试静默返回空字符串的路径
- 谓词不执行任意的 FormCalc 或 JavaScript:没有函数调用(
contains(...)谓词返回空),没有复合布尔表达式,仅有单个child op literal比较 $record仅在非记录处理包中工作;未实现 dataWindow 记录分页和记录组循环- 直接针对 Data DOM 和模板 DOM 进行解析,不执行 Form DOM 相对解析(即查看器在运行时构建的合并视图)
- 未应用 Data DOM 属性加载语义,库将 datasets 数据包读取为原始 XML,因此路径寻址的是元素,而不是提升为节点的属性
这些边界在批量填充工作中很少会带来困扰,因为填充器是通过结构而不是通过脚本逻辑来寻址数据的;当它们确实带来困扰时,安全出口是数据包级别:读取整个 datasets XML,在 Delphi 中进行转换,然后写回
使用 SetXFAFromString 写入整个数据包
SetXFAFromString 是数据包级别的入口点:它一起安装完整的 XDP 字符串、模板和数据集,当新文档中尚不存在 AcroForm 容器时创建它,并且它接受带有字节顺序标记的 UTF-16 编码数据包以及 UTF-8;常见的生产形式是保留机构发布的 XDP 作为测试夹具,使用 SetXFAFromString 加载它,然后在保存前为易失的每个发票值运行 SOM 寻址的 SetXFAFormFieldValue 调用;由于 XFA 是 PDF 可以携带的两种表单模型之一,经典的 AcroForm 侧有其自有的自动化故事,在关于交互式表单动作和 JavaScript 的文章中进行了介绍
此处显示 XFA 字段值、枚举和包 API 随适用于 Delphi、C# 和 VB.NET 的 losLab PDF Library 一起提供,产品页面载有完整的表单处理参考