批量印前检查工具是一个没有窗口的控制台程序,它指向一个包含诸多 PDF 文件的文件夹,针对你指定的合规标准验证每一个文件,并留下关于它所发现内容的机器可读证据。没有人会坐着看它。它会在凌晨两点在 cron 或 Windows 任务计划程序下运行,或者作为 CI 管道中的一道门禁,下一个关心它输出的人,要么是读取退出代码的计划程序,要么是几周后打开报告的审核员。这改变了“正确”的含义。PDFium Component 的印前检查引擎(一个适用于 Delphi、C++Builder 和 Lazarus 的源代码 PDF 库)使验证调用本身变得几乎微不足道。决定该工具是否发挥作用的工作存在于那些调用周围:你检查了哪个配置文件、退出代码告诉计划程序什么信息,以及当有人去查找报告时,本应捕捉到错误的报告是否仍然存在
契约:计划程序实际能看到什么
CI 运行器或 Windows 任务计划程序从你的工具中能看到的只有两件事:退出代码以及它留下的任何文件。日志行、控制台颜色、进度输出:所有这些都是给现场观看的人看的,而在凌晨两点并没有人。因此,在触及 API 之前请先固定退出代码词汇表,并保持简单枯燥:
0:每个文件都符合每个请求的配置文件1:至少一个文件产生了验证发现项2:工具本身在至少一个文件上失败(损坏的输入、锁定、崩溃)
代码 1 和代码 2 之间的区别正是许多团队经常忽略并事后后悔的地方。一个无法打开的损坏 PDF 并非验证失败。若将其混入代码 1 中,那大量的损坏扫描件就会以合规性崩溃的形式突然在仪表板上显现出来,导致某些人去追踪从未发生过的标准回归问题,而真实情况是上游的扫描仪坏了
另外两项也属于契约。第一项是每文件超时。一种病态的 PDF(包含数千页和深度嵌套对象结构)能使单次验证传递耗时数分钟,而夜间运行窗口是不能容忍这种等待的。应该在最后期限强制结束该文件的作业,将其视为工具失败,并让批处理继续推进。第二项是隔离目录:将每一个超时或无法打开的输入文件移开,而不是将其留在原处。几个月后,该目录会悄然积累你的真实客户发送过的最糟糕的文档,对于发布测试而言,这个语料库的价值胜过你手动编写的任何合成样本
挑选标准,以及合规级别的意义
TPdfPreflightStandard 枚举涵盖了实践中出现的标准族:用于 ISO 19005 归档合规性的 ppsPdfA、用于 ISO 14289 可访问性的 ppsPdfUa、用于打印交换的 ppsPdfX,外加用于工程、光栅和可变数据工作的 ppsPdfE、ppsPdfR 和 ppsPdfVT。在一个标准族内,引擎会读取文档所声称的合规级别,并按标准将其报告在结果的 ConformanceName 中。仅指明标准族通常是不够的,因为真正的区别在于级别。PDF/A-2b 仅承诺视觉上的可重现性。而 PDF/A-3a 增加了对逻辑结构标记的要求,并允许嵌入源文件,对于完全没有标签树的扫描材料来说,这是一个很难达到的标准。在这个方面任何方向的错误都会导致批处理对你撒谎。如果你保留策略的初衷是要求满足 PDF/A-2b,但却因为缺失结构化标签判定文件验证失败,那么报告中将充满无人会去修复的问题项。在不检查合规级别的情况下接受任何带有 PDF/A 标签的文件,就等于批准通过了未满足你所承诺标准的文档。政府买家的可访问性指令越来越多地将 PDF/UA 堆叠在所有这些之上,但这并不会增加运行成本,因为 BuildPdfPreflightReport(来自 FPdfPreflightReport 单元)接受一个标准集合:
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
一次调用即可评估这两种标准,并返回一份合并的报告记录
为什么发现项为空并不意味着通过
报告按标准列举了发现项,而一个空的问题列表仅仅意味着“在实际运行的标准中未发现问题”。这比“文件符合您关心的标准”的声称要狭窄得多,这两者之间的差距正是批量印前检查默默失效的地方。配置拼写错误导致 ppsPdfA 从集合中丢失,这会产生与真正干净的文件完全相同的一个空问题列表。所以要对沉默保持怀疑。遍历 Report.Results 并为你打算检查的每个标准断言两件事:该标准的结果条目确实存在,并且由 Status = pfsPass 支持的其 IsCompliant 标志为真。如果一个夜间作业在从未确认评估了哪些标准的情况下,就把“没有发现项”等同于“准备好归档”,这是让整个文件夹的违规文件数月来畅通无阻的经典方式,直到某位外部审核员使用 veraPDF 打开了其中一个文件,整个归档库都遭到质疑
另一个陷阱隐藏在究竟什么是发现项之中。每个 TPdfPreflightIssue 都带有一个 Code、一个 Category、一个 Description 以及一个 Recommendation,而且它指明了被违反的规则,而不是一个页面或一个对象。这是具有反馈回路后果的一种设计选择。该报告告诉生产团队存在何种类别的缺陷,比如是未嵌入的字体或是缺失的 XMP 标识符,而找到具体的违规对象则是下游修复工具的任务,而非验证器的。针对稳定的 Code 值构建你的报告使用者,绝不能依赖人类可读的描述文本,因为它可能会在没有警告的情况下于两个版本之间被重新措辞
为机器和值班人员准备的报告文件
报告记录以五种格式写入相同的发现项:SaveJsonToFile、SaveCsvToFile、SaveHtmlToFile、SaveTextToFile 和 SaveMarkdownToFile,每一种格式在你想获取内存中的字符串而不是磁盘上的文件时都有一个匹配的 ToJson 风格的函数。不要忍痛只选择一种。为管道编写 JSON,以便 CI 可以在不解析文本的情况下将其附加到作业记录并解析问题代码和各项标准状态。为被传呼的人工编写 HTML,因为它可以在无需任何工具的任何浏览器中打开。两者结合仅会在每个文件上增加一行代码,但能够使值班工程师免除批处理中最糟糕的一项任务,即在凌晨两点逆向工程一个原始的 JSON 格式块,以弄清楚是哪个文件坏了。有一项准则比格式选择更重要:每个报告名称都要源于输入文件的名称,绝不能源于时间戳,否则两次并行运行就会把报告交叉混合,导致你无法将它们匹配回其输入
严重性阈值属于配置而非代码范畴。没有替代描述的注释在 PDF/UA 提交流程中是一次严重失败,而在内部归档中则是一条可忽略的注意事项,尽管在两者中它是完全相同的发现项。因此应该公布每个配置文件的“失败条件”级别,以便策略能在无需重新编译的情况下进行转换,并将生效时的级别刻录在作业摘要本身中。下个季度没有人会记得去年十月份那次批处理是在哪个阈值下运行的,而这个摘要是那段记忆幸存的唯一地点
隔离文件,使一个坏的 PDF 无法破坏整个批处理
procedure RunPreflightBatch(const InputDir, ReportDir: string;
out FilesWithFindings, ToolFailures: Integer);
var
SR: TSearchRec;
Pdf: TPdf;
Report: TPdfPreflightReport;
begin
FilesWithFindings := 0;
ToolFailures := 0;
if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
try
repeat
Pdf := TPdf.Create(nil); // fresh instance per file: no state bleed
try
try
Pdf.FileName := InputDir + SR.Name;
Pdf.Active := True;
if not Pdf.Active then // load failures are silent, not raised
raise EPdfError.Create('Cannot open ' + SR.Name);
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
if Report.TotalIssueCount > 0 then
Inc(FilesWithFindings);
except
on E: Exception do
begin
Inc(ToolFailures); // exit-code-2 territory, not a validation verdict
WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
end;
那个循环中有三个深思熟虑的选择。每个文件一个全新的 TPdf 保证了一个损坏了引擎状态的文档不能毒害其后的文件。明确的 Active 检查之所以重要,是因为 Active := True 会吞没加载错误而不是抛出它们;去掉这种防卫,被截断的文件就会直接进入验证调用,随后在下游的某个地方带着误导性的信息失败。内部的 try..except 故意保留在每个文件级别的作用域内,因此,单个异常只会使失败计数器递增,而循环能继续。你希望即便是第 5,000 个文件已经被破坏了,那 4,999 个好的文件仍然能得到干净的报告。且两种报告格式都在计算判定之前就写入到了磁盘,这就意味着即使后面的汇总逻辑中有个 Bug 算错了,证据也能被保留下来
这时的退出代码映射就精简为了项目文件中的寥寥几行代码:
begin
RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
if Failures > 0 then
Halt(2)
else if Findings > 0 then
Halt(1);
// falling through exits with 0: every file conformed
end.
印前检查无法为你做什么
引擎负责检测;而不负责修复。关于某个未被嵌入字体或者是某设备相关颜色空间的发现,那是对生成该文件的任何人的一项工作指令,而且验证器没有办法在原位去修补它。所以应认真规划反馈回路。报告必须落到生成团队实际能看到的地方,不然直到有人最终发问为什么合规率永远也提不高之前相同的那些发现项都会每晚重复出现。将某些判定样本通过某个独立验证程序(用于 PDF/A 的 veraPDF,用于 PDF/X 的 Acrobat 印前检查)做一下交叉检查是非常值得的,免得被外部的审查员先去做了。如果有两个引擎针对某真实客户的文件产生出不相符的结论,那该文档就不能看成是一件令人烦恼的事;它确切属于你们的发布测试遗漏掉了的一种回归用例。把这保留下,给它命了名,让其在每一次的构建过程里都跑一次
还有一种搭配也值得了解。交互式审查 UI 是由同一验证引擎驱动的,因此这种无头式的 CLI 能够与面向分析师的 PDF 摄入审查工作台 共享一套验证词汇表,从而不至于随着时间推移产生分化。而且,由于 [ppsPdfA, ppsPdfUa] 在同一通道内评估其可访问性,这种针对其无头检查方面的 PDF/UA 配置就能纯粹、干净地同像在 Delphi 内构建某可访问的 PDF 阅读器等那些侧重于查阅者的做法对齐了起来。这些配置文件,各类报告样式以及全套的印前检查的 API 在该款 PDFium Component 的产品专页之中都能被寻觅得到