一个能自动累计明细项目并自动格式化日期的发票或报销单 PDF,其 AcroForm 需要绑定三样东西,而 HotPDF 为 Delphi 中加载的文件添加了所有这三样:用于 AFSimple_Calculate 合计的 ApplyLoadedFieldCalculation、用于 Acrobat AF 格式和验证辅助程序的 ApplyLoadedFieldValidation,以及用于烘焙查看器实际绘制的值的 EnsureLoadedFieldAppearanceStream
HotPDF 是适用于 Delphi 和 C++Builder 的原生 VCL PDF 组件,这三个方法作用于您使用 LoadFromFile 打开的文档,而不是您从头开始构建的文档。每个方法都接受 AcroForm /Fields 数组中基于 0 的字段索引(这与 GetLoadedFormFieldNames 返回的顺序相同),并直接写入字段字典,因此整个工作就是加载、索引、附加、SaveLoadedDocument。构建字段集本身是一项单独的任务,在 在 Delphi 中向加载的 PDF 添加 AcroForm 字段 中有涵盖,而本页是关于为现有字段集赋予算法和格式化
为什么 PDF 表单需要脚本来自动计算其自己的字段?
因为 PDF 没有电子表格层。文本字段保存的是字符串而不是公式,并且除非附加了脚本来执行此操作,否则当明细项目更改时,文件中的任何内容都不会重新计算总计。该机制是字段附加操作字典 /AA (ISO 32000-1 §12.7.5),它将触发事件映射到操作:字符到达时的按键触发器 (K)、用于显示的格式触发器、提交时的验证触发器 (V),以及每当表单上的任何字段更改时触发的计算触发器 (C)。每个操作都是一个 JavaScript 操作 /S /JavaScript (ISO 32000-1 §12.6.4.14),它运行的脚本正是实际执行求和或重新格式化的脚本。Acrobat 随附了一个现成的辅助程序库(AFSimple_Calculate、AFNumber_Format、AFDate_FormatEx 等),因此附加的脚本通常是对该库的单行调用,而不是定制代码
有一个细节决定了多步骤表单是否能给出正确的答案:计算顺序。Acrobat 运行每个带有计算操作的字段,并且它按照 AcroForm /CO 数组列出的顺序运行它们,而不是按照页面顺序。ApplyLoadedFieldCalculation 将计算操作附加到您指定的字段;当一个计算字段喂给另一个计算字段时(例如税金行读取子计,然后总计读取这两者),您必须确认 /CO 顺序将每个输入列在消耗它的字段之前,否则总计将读取陈旧的子计,并在编辑时滞后一步
如何使 PDF 表单自动计算总计?
ApplyLoadedFieldCalculation 接受目标字段索引、操作以及它应该聚合的源字段的名称。HotPDF 将 AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) 写入目标字段的计算操作中,而 THPDFFieldCalcOp 枚举则用于选择函数:fccSum、fccAverage、fccProduct、fccMinimum 和 fccMaximum 分别映射到 Acrobat 的 SUM、AVG、PRD、MIN 和 MAX。源名称是 AcroForm 字段名称(即 GetLoadedFormFieldNames 传回的相同字符串),因此对三个明细项目和运费进行总计只需单次调用
var
Pdf: THotPDF;
Names: THPDFAnsiStringArray;
i, TotalIdx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
Names := Pdf.GetLoadedFormFieldNames;
TotalIdx := -1;
for i := 0 to High(Names) do
if Names[i] = 'grandTotal' then TotalIdx := i;
if TotalIdx >= 0 then
begin
Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
end;
Pdf.SaveLoadedDocument('invoice-live.pdf');
finally
Pdf.Free;
end;
end;
在总计字段上调用 EnsureLoadedFieldAppearanceStream 并非装饰性的。计算在查看器内部运行,因此在支持脚本的阅读器打开文件之前,总计字段本身没有渲染的内容。烘焙外观流可以为它提供一个在此期间显示的数据,这正是当打印作业或缩略图生成器从不运行脚本时会读取的内容
使用 AF 辅助程序套件格式化和验证字段
ApplyLoadedFieldValidation 在单次调用中附加格式、按键和验证脚本,您传递的 THPDFFieldValidationKind 决定了它连接哪一个 Acrobat 辅助程序。传入 fvkCurrency,HotPDF 会写入 AFNumber_Format(2,0,0,0,"$ ",true) 作为格式 script,并配有匹配 of AFNumber_Keystroke;fvkNumber 使用 "." 作为分隔符参数,fvkPercentage 发出 AFPercent_Format,而 fvkDate 发出 AFDate_FormatEx("dd/mm/yyyy"),除非您在 DateFormat 参数中传递了自己的掩码。特殊格式(fvkZipCode、fvkSSN 和 fvkPhone)分别映射到 AFSpecial_Format 并带有 Acrobat 为每种格式保留的代码,而 fvkArbitraryMask 则通过 Mask 参数为那些预设不涵盖的任何内容接收掩码字符串。对于数值类型,HotPDF 还会附加 AFRange_Validate 脚本,以便在提交时拒绝超出范围的输入
// 将发票日期格式化为 ISO,金额格式化为货币,并掩码税号
Pdf.ApplyLoadedFieldValidation(DateIdx, fvkDate, 'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency, '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx, fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);
AF 辅助程序涵盖了常见形态,因此您永远不需要为它们手写 JavaScript。当某个字段需要预设无法表达的逻辑时(例如校验和、跨字段规则、自定义正则表达式),请使用 使用 HotPDF 构建 AcroForm 字段和操作 中描述的字段级操作方法自行附加原始脚本,这些方法直接接受 JavaScript 字符串。这两条路线都写入相同的 /AA 字典中,因此一个字段上预设的货币格式与另一个字段上手动编写的验证可以共存于同一个文档中
为什么必须重新生成外观流?
因为在 PDF 中,值字符串和查看器绘制的像素是两样不同的东西,改变其中一个并不会改变另一个。字段将其值存储在 /V 中,但阅读器渲染的内容来自字段外观流 /AP /N。通过对象图编辑 /V,严格的查看器仍会显示旧的外观,直到重新生成。表单字典带有一个 /NeedAppearances 标志,该标志要求查看器在打开时重建每个外观,Acrobat 遵守该标志——但嵌入浏览器的渲染器、移动阅读器、打印服务器和缩略图生成器经常忽略它并绘制已经存在的任何流,或者根本不绘制。EnsureLoadedFieldAppearanceStream 通过从字段的 /DA、/V 和 /Rect 自行构建 /AP /N Form XObject 弥补了这一差距,从而使渲染的值随文件一起传输,而不是依赖查看器重新绘制它
// 在保存前为每个字段烘焙外观,以便值可以在任何地方渲染
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');
AF 脚本在何处停止工作
每个 AF* 脚本都依赖于查看器具有 JavaScript 引擎,而大多数查看器并没有。Acrobat 和少数桌面产品运行这些辅助程序;内置于 Chrome、Edge、Firefox 以及各大移动平台的 PDF 查看器,以及服务器端光栅化器和打印管道,根本不运行任何表单 JavaScript。在这些阅读器中,计算永远不会触发,格式也永远不会应用——该字段仅显示最后保存的任何外观流,这正是烘焙 /AP /N 的意义所在。请将 AF* 层视为对支持脚本的阅读器上的用户子集的便利,绝非保证。任何必须在服务器上正确计算的总计(您将据此计费的发票金额,您将申报的税收数据)都必须在表单返回后在您这一侧重新计算,因为您不能假设填充它的阅读器运行了您脚本的任何单行
还有一个情况值得提及:作为 XFA 传入的表单。动态 XFA 表单在 XML 有效载荷中携带其自己的计算引擎,而 AF* 辅助程序对此一无所知,因此将 AFSimple_Calculate 附加到 XFA 字段毫无效果。请先将 XFA 表单扁平化为普通的 AcroForm(如 Delphi 中的 XFA 到 AcroForm 扁平化 中所涵盖的),然后此处的三个方法即能完全适用于生成的 AcroForm 字段,就像适用于任何其他加载的表单一样
自动计算和自我格式化的 AcroForm 归结为对已加载文档的三次调用(附加计算、附加格式和验证、烘焙外观),然后调用 SaveLoadedDocument。这里展示的加载表单 API 是适用于 Delphi 和 C++Builder 的 HotPDF Component 的一部分,它在 AcroForm 其它界面的同时,也记录了完整的 THPDFFieldValidationKind and THPDFFieldCalcOp 枚举