交互式动态 XFA Widget 运行时

TXFAWidgetRuntime 为动态 XFA 表单提供一套宿主无关的交互模型,不把它们转换成 AcroForm 字段,也不 flatten 其内容

运行时暴露确定性的 widget 边界和状态,桌面程序、服务或自定义渲染器都可以据此接入自己的输入、绘制、无障碍和事件循环

创建运行时

可以直接从 XDP 字节创建运行时,也可以在加载一个 AcroForm 含有单流或 packet 数组 /XFA 条目的 PDF 之后,调用 THotPDF.CreateLoadedXFAWidgetRuntime

var
  Runtime: TXFAWidgetRuntime;
  State: TXFAWidgetState;
begin
  Runtime := PDF.CreateLoadedXFAWidgetRuntime;
  try
    if (Runtime <> nil) and (Runtime.WidgetCount > 0) then
    begin
      State := Runtime.Widgets[0];
      Runtime.FocusWidget(State.ID);
      Runtime.BeginEdit(State.ID);
      Runtime.ReplaceSelection(0, Length(State.Value), 'Updated value');
      if not Runtime.CommitEdit then
        raise Exception.Create(Runtime.LastDiagnostic);
    end;
  finally
    Runtime.Free;
  end;
end;

针对已加载文档的工厂对 PDF 对象图和既有 XFAFlattenWarnings 是只读的;保存文档时会原样保留其 /XFA 和 /NeedsRendering 条目

交互模型

交互式 commit 会把既有的原生标量值同步到 hidden 与 invisible 别名,包括真实的重复数据作用域和逐字节精确的回滚

原子化校验与计算

commit 在执行 validate 和 calculate 脚本之前,先解析显式 SOM 绑定和当前重复行数据上下文

编辑值、计算值、数据节点、widget 模型、焦点与编辑状态、警告和 pass 计数器,只有在校验和布局稳定之后才一起发布

被拒绝的脚本、耗尽的预算、非法的 UTF-16 选区边界,以及布局或宿主测量异常,都会完整恢复先前状态并设置 LastDiagnostic

预算

TXFAWidgetRuntimeOptions 限制 widget 数量、编辑值长度、计算 pass 数、reflow pass 数、布局操作数、脚本操作数、脚本耗时以及其他 FormCalc 或 JavaScript 资源

widget 上限在追加布局项和分页片段时生效;已加载 XFA 的解压与 packet 组装则在解析前就受 XFA DOM 输入上限约束

MaxLayoutOperations 默认 200000,同时约束文档遍历和布局工作;布局深度上限 128,事件目标与实例初始化深度上限 64,递归的事件派发会被拒绝

受限的动态事件

DispatchEvent 接受匹配字段事件脚本中以分号分隔的字面操作,内容类型为 FormCalc 或 JavaScript;这些操作被直接解析,不需要 JavaScript DLL

row.instanceManager.addInstance(true);
row.instanceManager.removeInstance(0);
target.presence = "hidden";

addInstance 追加一个实例,merge 参数接受 true、false、1 或 0;省略参数时按 true 处理,简写 _row.addInstance(1) 也被接受

受支持的模型会绑定所有同名的既有数据组,因此无论 merge 参数是什么,新实例都会得到一个按模板默认值初始化的新数据组;创建实例不会克隆上一行已输入的值

removeInstance 在所选 subform 的同名数据组中按从零开始的整数索引取值;该 subform 必须带有带可重复上限的 occur 元素,两种操作都会执行其最小值和最大值,包括 max="-1"

实例变更要求隐式命名数据集绑定和 ASCII XML 数据名;显式数据引用绑定和有歧义的父上下文会在发布事务之前被拒绝

目标通过事件所在封闭作用域内的命名模板子节点解析,包括点分路径和 this.parent;嵌在重复作用域内的 manager 使用事件自己的数据组

presence 接受模板字段、draw、subform 和 exclusion group 上的 visible、hidden 和 invisible;hidden 内容不占流式空间,invisible 内容保留空间但不进入 widget 和 flatten 输出

presence 变更作用于所选模板节点,因而作用于它的全部重复出现;按实例索引的 presence、inactive、任意表达式、变量、条件、循环和其他事件脚本操作都会被拒绝

事件批次在 reflow 稳定后把数据变更、计算、布局、焦点和编辑状态一起发布;任何被拒绝的操作,或输入、操作、实例、值、widget、布局、耗时预算耗尽,都会完整恢复此前的文档与交互状态

删除一行时,存续数据组的焦点和未提交编辑会得到保留,即使它们的 widget 索引发生变化;删除或隐藏持有焦点的 widget 会清除焦点,并在成功后发布相应的焦点 callback

通用 JavaScript 与 FormCalc 事件执行

设置 TXFAWidgetRuntimeOptions.ScriptOptions.EnableJavaScript 启用随库附带的有界 QuickJS 桥执行事件;此后 JavaScript 事件支持函数、闭包、数组、条件、循环和异常,而不只是默认的字面文法

Options := TXFAWidgetRuntimeOptions.Default;
Options.ScriptOptions.EnableJavaScript := True;
Runtime := TXFAWidgetRuntime.Create(XDPBytes, 595, 842, Options);

事件宿主暴露 this、外围命名字段与 subform、parent、可写的 rawValue、presence 与 access、instanceManager.count/min/max、addInstance、removeInstance、insertInstance、moveInstance 与 setInstances、xfa.resolveNode、xfa.resolveNodes、节点级解析以及 xfa.layout.relayout

节点列表支持数字索引、length 与 item;重复字段值保留各自的 dataset 目标,SOM 路径对活动宿主节点接受带数字或通配索引的命名子节点

const values = xfa.resolveNodes("main.row[*].amount[*]");
let total = 0;
for (const field of values) total += Number(field.rawValue);
xfa.resolveNode("main.total").rawValue = total;
if (total > 100) this.parent.warning.presence = "visible";

每个重复 subform 都有自己的活动对象与子字段;index、父节点子 getter 和后续 SOM 查询会在脚本内立即跟上 insert、move 与 remove 操作

addInstance 与 insertInstance 返回按模板默认值初始化的活动子树,脚本可以在事件发布前写入其字段值;被移除的句柄会拒绝后续读写

通用脚本的 presence 与 access 变更按出现持久化到标准 XFA form packet,字段值与重复操作持久化到 datasets;insert、move 与 remove 让对应 form 状态与数据组保持一致

同一可选项下,FormCalc 事件脚本编译到有界引擎,支持 var、if/elseif/else、带 upto 或 downto 的 for、while、foreach、隐式结果的 func、算术与比较、字符串拼接、通配节点聚合和隐式字段值赋值

事件函数库包含 Sum、Count、Avg、Min、Max、Round、常用数学函数、字符串切片、大小写转换、修剪、替换、HasValue、Exists、Within、Oneof 与 Choose;内置名不区分大小写,At(source, search) 遵循 XFA 参数顺序(含空搜索行为),财务函数遵循 XFA 参数顺序,日期与财务目录见 FormCalc Functions

脚本变更记录在引擎内部,在运行时事务内校验并 replay;异常、中断、非法 Unicode、非法宿主操作和预算耗尽都不会发布部分文档变更

通用脚本执行默认关闭,不使用浏览器或外部进程,不暴露文件系统、网络或应用 API;调用方可以改供 JavaScriptEvaluator 而不用内置桥

持久脚本对象

subform variables 元素内的 JavaScript script 对象经脚本名暴露其变量与函数;词法变量、对象状态和嵌套闭包在整个运行时会话内存活,正常保留词法遮蔽与严格指令

<variables>
  <script name="Helpers" contentType="application/x-javascript"><![CDATA[
    let count = 0;
    const nextPrivate = (() => { let value = 0; return () => ++value; })();
    function next() { return ++count + ":" + nextPrivate(); }
  ]]></script>
</variables>

事件代码可以调用 Helpers.next();重复 subform 出现各自拥有独立的脚本对象,函数按当前活动 form 上下文解析命名字段

捕获的字段节点与 manager 跨移动及新建实例的发布保持稳定的数据身份;访问已移除的节点会拒绝事件并回滚其模块状态

通用 calculate 与 validate 脚本共享会话与脚本对象;JavaScript 支持隐式完成值与显式 return,FormCalc 返回其最终表达式

失败的事务通过重放已提交的虚拟日志恢复词法与闭包状态,时间和随机输入均有记录;replay 与当前执行共享事务时限,默认日志上限为 64 MiB 脚本/结果和 8192 条目

脚本对象状态存于运行时会话,XDP 重载后重新初始化;标准文档值与 form 覆盖仍经 SaveToBytes 持久化

自定义 JavaScriptEvaluator 回调保留既有原生 calculate/validate 信封;持久会话由随库引擎提供

配置显式宿主传输

在 TXFAWidgetRuntimeOptions 中设置 HostTransport 与可选的 OnHostTransactionCompleted,用应用回调执行宿主对话框、打印、导航、提交与数据传输

显式 XFA 宿主传输提供 xfa.host.messageBox、response、beep、print、gotoURL、submitForm、importData、exportData 以及 FormCalc 的 Get、Post、Put;应用返回类型化结果并控制外部副作用

重试与会话 replay 使用记录的响应,因此每个已派发请求回调只执行一次;失败完成让应用丢弃暂存副作用,或对可逆操作做补偿

正的 HostTransportLimits 限制外围运行时事务内的请求数、参数个数与请求/响应聚合字节

locale 与 picture 执行

通用 FormCalc 事件与 calculate、validate 脚本支持 locale 与 picture 函数,包括 Format、Parse、本地化日期/时间转换、单位、编码、UUID 和英文数字词

动态 FormCalc Eval 执行动态提供的计算,变量与函数隔离、相对字段访问、原生编译、记录响应与事务化回滚

显式 FormCalc 引用支持直写赋值、重绑定、null 脱离、函数参数,以及脚本对象跨重复实例移动保留的稳定句柄

活动 SOM 路径支持推断、绝对与相对出现索引、类与后代选择器、透明容器、候选谓词和直接的 FormCalc 索引赋值

原生 Data DOM 经 $data 与 $record 暴露 datasets,立即同步绑定的表单读写,镜像重复实例,并经原生身份发布保留数据引用

原生 Property DOM 暴露声明的 value、font、UI 等属性子节点,同步类型化字段值,并持久化供字体度量与受支持 flatten 样式使用的逐实例属性

执行节点继承最近的 locale 属性;文档 localeSet 条目覆盖系统符号与模式,计算出的 locale 名经有界内部请求解析并保留在会话日志中

不带 picture 的 Num2Date 现在使用环境默认日期模式;需要 ISO 输出时请显式提供 YYYY-MM-DD

有状态宿主模型

XFA 宿主模型提供应用元数据、真实页数、页面导航、Unicode 标题、计算/校验标志、带事务化进入/退出生命周期的作用域 reset 与字段焦点

TXFAWidgetRuntimeOptions.HostModel 配置初始应用状态;TXFAWidgetRuntime.HostModel 返回当前状态,失败的事件会同文档与交互快照一起恢复它

没有进入/退出脚本、待提交编辑或已初始化生命周期的焦点移动只改焦点,不启动脚本计算或时限处理;脚本化焦点与待提交编辑提交仍使用事务预算、校验与重算

高级 picture 子句经同一运行时事务、locale 重试日志与计算生命周期执行,复合解析有界

初始化并维护生命周期

配置好运行时与回调后调用 InitializeForm,按模板顺序执行 initialize 事件,然后计算与校验、form-ready 事件和 layout-ready 事件;成功后该调用幂等

初始化或 ready 事件新建的实例先收到自己的 initialize 事件,再进入 ready 处理;layout-ready 只在布局变化时重复执行,受 reflow 与事务预算约束

初始化后,成功的事件派发与编辑提交会初始化新建实例并执行 calculate、validate 与 layout-ready;既有出现跨移动与事务回滚保留其初始化状态

生命周期失败会一并回滚文档字节、widget 交互与初始化簿记,回调只在外围事务成功后发布

保存更新后的表单

SaveToBytes 返回运行时完整更新过的 XDP,包括修改后的数据集和模板 presence 属性;写 PDF 时把这些字节交给 SetXFADocument,要得到更新后的 flatten 输出则交给 HPDFXFAFlatten

已加载文档的工厂创建的是独立运行时,因此运行时事件不会自动修改源 PDF 的对象图

可选实例呈现 profile

动态 XFA 实例呈现经独立辅助提供显式重复绑定、稳定逐实例 ID、带索引的 presence 与访问、有界条件事件、宿主编画布绘制与无障碍;默认运行时及其受限事件文法保持兼容

UpdateDocument 提供事务化文档动作;可选的运行时属性、绑定作用域、身份、解析开始与文档校验回调辅助该 helper,nil 默认值保持既有调用方不变

更低层的事件与表单 API

HPDFXFACompileFormCalcEvent 编译事件语法;HPDFXFAExecuteJavaScriptEvent 执行 TXFAEventBinding 与 TXFAEventBindings 描述的虚拟上下文

TXFAJavaScriptSession 为更低层调用方提供持久执行与有界检查点

返回的 TXFAEventAction 值使用 TXFAEventActionKind;事务化 replay 由运行时提供

HPDFXFAResolveFormInstanceNode 与 HPDFXFAResolveFormInstanceProperty 提供有界的标准 form packet 查找

当前边界

运行时是单线程宿主对象,不提供 GUI、绘制器,也不是通用的 XFA 事件脚本引擎

在没有匹配事件脚本时,焦点的进入与退出直接支持;受限的事件操作并不意味着支持完整的 JavaScript、FormCalc、XFA 生命周期事件,也不意味着支持带逐实例呈现覆盖的独立表单 DOM

参见 Bounded XFA FormCalc and JavaScript、XFA Packet DOM 和 Interactive Document Processing