ConvertToPDFA 用一次调用就把普通文档变成可归档文档:它移除所选子集禁止的内容,补上子集要求的内容,声明文档声称的子集,然后检查结果。只有在检查通过时,这一声称才会被报告为已达成,而 GetPDFAConversionReport 列出做了什么、以及还有什么挡在路上
最后这一条是值得展开的设计决策。一个不检查就盖上声称的转换器,比没有转换器更糟糕,因为一个自称可归档却并非如此的文件,会径直穿过那些本该拦下它的系统。失败要在多年之后、在一次审计中、在一份谁也无法重新生成的文档上才会暴露
为什么一份看起来合规的 PDF 过不了 PDF/A 检查
最常见的原因,是 PDF 写明作者身份的两处地方彼此对不上。校验器会同时读取文档信息字典(Info dictionary)和 XMP 包,并拒绝两者不一致的文件——而大多数在这一项上失败的文件,根本就从未写入过 XMP 那一半
RepairDocumentMetadata 让两边对齐,并返回它修复了多少条目。当只有一边携带某个值时,另一边从它填充,所以已经记录下来的信息不会被丢弃。没人需要决定哪一份副本是权威的,因为实践中总有一份是空的
同一次调用里还有第二项修复,捕获更隐蔽的一种情况。设为 PDF/A 模式的文档会找回它丢失的标准标识——每当调用方自行提供一个 XMP 包时就会发生这种丢失。没有这个标识,校验器会把文件当作普通 PDF 来读,并把声称子集的每一条规则都报告为未达成——一种原因很小、表象很夸张的失败
var
Lib: TPDFlib;
Repaired: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('incoming.pdf', '');
Repaired := Lib.RepairDocumentMetadata;
Log(Format('%d metadata entries brought into agreement', [Repaired]));
Lib.SaveToFile('incoming-fixed.pdf');
finally
Lib.Free;
end;
end;
转换之前先选好子集
SetPDFAMode 和 ConvertToPDFA 共用同一套模式编号,其中三个是较新的值。模式 9 是 PDF/A-4,建立在 PDF 2.0 之上的子集。模式 10 是 PDF/A-4e,额外允许 3D 和富媒体;模式 11 是 PDF/A-4f,允许嵌入任意格式的文件
子集 4 与之前的子集在自我标识上不同:它用子集号和该子集发布的年份来标识,普通的 PDF/A-4 没有一致性字母,两个扩展则带字母 E 或 F。检查识别子集 4,按 PDF 2.0 而不是 1.7 来评判它的文件,并报告一份没有写明修订年份的子集 4 文件
子集 4 文档里的每一个嵌入文件都要写明它与文档的关系,这是子集 3 和 4 都要求的。这条规则过去经常卡住普通附件:关系只为第一个之后的附件写入,从不为最后一个写入,所以只有单个附件的文档——最常见的情况——反倒一条关系都没带,并恰恰因此通不过校验
var
Verdict: Integer;
begin
Lib.LoadFromFile('report.pdf', '');
Verdict := Lib.ConvertToPDFA(9); // 9 = PDF/A-4, 10 = 4e, 11 = 4f
Memo1.Lines.Text := Lib.GetPDFAConversionReport;
if Verdict = 1 then
Lib.SaveToFile('report-pdfa4.pdf')
else
Log('conversion incomplete - see the report for what stands in the way');
end;
转换报告是用来做什么的
用来决定下一步。一次成功的转换不需要报告;一次没成功的转换才是报告存在的全部理由。有些障碍转换器能移除,有些不能——加密、携带含义的禁用内容、本机上压根不存在的字体程序。报告区分了已做之事与未尽之事,把"转换失败"变成一条工作项
把判定结论当成批量流水线里的那道关卡。转换、读结论、分流文件:通过的归档,其余的连同报告一起排给人工处理。你不该做的,是把一次失败转换的输出存进归档,只因为它看起来比输入更好看——它现在带着一项检查拒绝确认的声称
读取文件已经携带的标识
转换任何东西之前,先搞清楚文档是怎么描述自己的。一个读不出既有标准标识的 PDF/A 检查,会把每一份文件都按子集 1 来评判,无论它声明了什么——这意味着一份完全合规的 PDF/A-2 或 PDF/A-3 文档会被报告为没有标识、版本过高,与事实正好相反
无论生产者把标识写成 XMP 元素还是属性,都能被读取。两种形式都是普通 XMP,只接受其中一种会让其他生产者产出的文件看起来像没有标识。如果你曾经纳闷过为什么一份在别处校验通过的文档在你的流水线里失败,这里是值得最先排查的地方
归档前消毒,以及一个值得知道的 bug
归档转换和消毒常常一起跑,因为安全策略要移除的内容与 PDF/A 禁止的内容大量重叠。SanitizeDocument 会移除 JavaScript,而移除最后一个脚本时也会顺手移除它留下的空名称树——否则这棵树仍会告诉阅读器这份文档带有脚本
后半段是付出代价才学到的:包列表里的一处 off-by-one 让消毒在报告移除了脚本的同时其实一个都没移除,于是被消毒过的文档打开时仍会运行脚本。这是对本文整篇所立基的那条原则的有力佐证——在你的流水线里,正如在库里一样,要校验结果而不是信任操作
围绕归档的其他工作,参见 PDF/A 与 PDF/UA 预检、真正的修订(redaction)与内容移除、以及 Factur-X 的 PDF/A-3 XMP 扩展 schema——后者覆盖了当归档文档同时携带结构化发票数据时的元数据那一面
PDFlibPas 是面向 Delphi、C++Builder 和 Lazarus 的原生 Pascal PDF 库,所以转换、修复和校验全部在你自己的进程内完成,链条中没有任何外部工具——所支持的 PDF/A 子集与平台见 PDFlibPas 产品页