PDF Library for Delphi 用一套显式策略合并两份共享字段名的 AcroForm 文档。MergeDocumentEx 接受源文档标识符,以及三种策略之一:dfsReject 拒绝这次合并,dfsMerge 保留共享的名称并同步各自的值,dfsAutoNumber 以确定性的方式重命名传入的字段。名称扫描发生在任何对象编号发生偏移之前,因此一次被拒绝的合并会让两份文档都保持完全可用
任何组装过 PDF 申请材料包的人都遇到过这种情况。三份表单,各自都有一个叫 Signature 或 Date 或 Total 的字段,被合并进一个文件。在 AcroForm 中,完全限定的字段名就是该字段的身份标识,因此两个同名字段根本不是两个字段:填一个会同时填另一个,而应用在其中一个上的签名,覆盖的范围会超出任何人的本意
为什么名称冲突要在合并之前就决定?
较早的 MergeDocument 会直接把两份 AcroForm 根字段数组拼接在一起,不提供任何选择。更糟的是,当结果不可用时,这个发现是在对象编号已经重新编排、页面树已经拼接之后才会发生,这会让调用方手里的文档陷入一个两份原文档都从未有过的状态
MergeDocumentEx 反转了这个顺序。它会先收集两份文档的顶层字段名,做比较,然后在任何内容移动之前应用策略。因此一次拒绝是一次干净的空操作:目标文档不受影响,源文档不受影响,两者都保持打开且可用,合并测试正是通过在一次被拒绝的合并之后,从源文档中把某个字段值读回来加以验证的
这项比较使用一个有序、区分大小写的名称集合,因此开销与两份文档字段总数乘以一个对数因子成正比,而不是与两个数量的乘积成正比。区分大小写在这里是正确的选择,因为 PDF 字段名本身就是区分大小写的;把它们折叠在一起,会把规范认为不同的字段合并成一个
三种策略,以及各自适用的场合
dfsReject 适用于绝不能产生歧义文档的自动化流水线。合并调用返回零,LastErrorCode 报告 705,这是一个专门的错误码,让重复名称能够与其他所有合并失败区分开来,并路由到一个特定的处理方式,通常是在上游重命名字段
dfsMerge 会刻意保留共享的名称,并把目标值和默认值同步进传入的字段,这样一个合规的查看器会把这几个部件视为一个逻辑命名字段,这正是一个带有多个部件注释的字段在 AcroForm 中的标准行为。它不会做的是把不同的字段字典折叠成单个对象。每个字段依旧保有各自的页面关联、外观和动作,因为把它们合并在一起,会悄悄丢弃属于传入文档的格式和行为
dfsAutoNumber 会通过附加一个从 _2 开始、取第一个空闲值的数字后缀来重命名传入的重复字段。这个结果是可复现的:它只取决于当前存在的名称,从不取决于字段对象编号,因此合并同一对文档两次,两次得到的名称完全相同。当下游代码,比如一次 FDF 导入或一个数据库映射,是按名称引用字段时,这一点很重要
uses
PDFlibrary;
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.SelectedDocument;
Lib.LoadFromFile('application-part1.pdf', '');
SourceDoc := Lib.NewDocument;
Lib.LoadFromFile('application-part2.pdf', '');
Lib.SelectDocument(TargetDoc);
if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
begin
if Lib.LastErrorCode = 705 then
begin
// 两份文档依旧完好 - 换一个策略重试
Log('duplicate field names; retrying with auto-numbering');
Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
end;
end;
Lib.SaveToFile('application-complete.pdf');
finally
Lib.Free;
end;
end;
请留意这段代码中的两步模式,这只有在拒绝是无损操作的前提下才可能实现。先尝试严格策略,检查错误,再做决定。如果合并中途失败,回退方案就得靠重新加载两个文件从头再来
合并后的表单是什么样子
在 dfsMerge 下,一个目标文档中名为 Shared、携带 "Target value" 的字段,和一个源文档中同名的字段,会产生两个字段,都叫 Shared,都报告目标值,因为目标值和默认值都被同步进了传入的字段。这正是共享名称的预期语义:一个逻辑字段,若干个部件,一个值
在 dfsAutoNumber 下,同样的输入会产生 Shared 和 Shared_2 两个各自独立取值的字段。在两者之间做选择只需回答一个问题:填一个字段是否应该同时填另一个?对于在申请材料包每一部分都重复出现的签名人姓名,答案是"是",dfsMerge 就是对的。对于每份表单上含义各不相同的合计数,答案是"否",自动编号才是对的
// 合并之后,枚举一下你实际得到了什么
for I := 1 to Lib.FormFieldCount do
Log(Format('%d: %s = %s',
[I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));
组装表单包的实用提示
成功的合并会消耗掉源文档:它会从库的文档列表中移除,这也是为什么 DocumentCount 会从二降为一。之后不要再使用源文档标识符。文档版本会被提升为两者中较高的一个,因此把一份 PDF 2.0 表单合并进一份 1.7 文档,会得到一份 2.0 文件
名称的结果与合并顺序有关。把 A 合并进 B,和把 B 合并进 A,会产生不同的自动编号结果,因为发起合并的那份文档会保持自己的名称不变。当一个材料包有一份规范意义上的主表单时,应把那一份作为目标文档
签名字段值得单独考虑。在合并之前应用的签名只覆盖它签署时的那一次修订,因此从实际效果来说,合并会让该签名失效,因为文件在签署之后已经发生了变化。正确的做法是先组装、再对组装完成的文档签名,而不是合并已签名的部分。当合并的对象是页面内容而不是表单时,通过字节引用偏移实现的快速 PDF 合并 中描述的方案是更好的工具
最后,请把材料包的数据这一侧和合并操作一起规划。如果字段值来自外部系统,在选择自动编号之前先确认那个系统是否按名称寻址字段,因为 Shared_2 不会匹配一个预期是 Shared 的映射关系。导入导出格式见 FDF、XFDF 和 XFA 表单数据交换,字段级脚本行为同样可能受重命名影响,见 交互式表单动作与 JavaScript
表单合并、数据交换和签名都运行在适用于 Delphi、C++Builder 和 Free Pascal 的同一个库中;完整功能列表见 PDF Library for Delphi 页面