技术文章

在 Delphi 中使用 HotPDF 自动化 PDF 预检检查

月度对账单在每个开发者屏幕上看起来都没问题,于是上线发布。两天后,印刷服务商退回整批文件:CMYK 作业里有 RGB 图像,而且没有 /Trapped 声明。成本不只是重印,而是监管期限里的两天。“Preflight”是印前术语,指在文件离开公司之前捕获这类问题;对 Delphi 团队来说,真正有意思的工程问题是:当 PDF 由你自己的代码和 HotPDF 这样的库生成,而不是由设计人员桌面工具生成时,这个检查应该放在哪里。

当生成器归你所有时,预防胜过检查

经典 preflight 假设输入是质量未知的外来文件,并在事后检查。当文档由你自己的应用生成时,这种架构是反的:检查器会检查的每个属性,例如字体嵌入、颜色空间使用、输出意图、元数据,都是你的代码在几毫秒前决定的。最便宜的 preflight 失败,是在生成时就不可能发生的失败。

需要准确说明 HotPDF 在这里提供和不提供什么。组件随 GUI 演示应用提供了一个 preflight report 窗口,但没有可由服务或构建脚本调用的程序化 preflight API。这并不像第一眼看上去那么缺失,因为对于生成型文档,健壮模式本来就有两个独立部分:约束生成器,让它不能输出不符合规范的结构;然后用你自己不维护的验证器检查输出。让库验证自己的输出是在给自己的作业打分;外部工具给出的证据更容易被客户或审计接受。

生成侧:把符合性做成配置,而不是审阅项

HotPDF 的标准属性就是预防层。当 PDFACompliancePDFXComplianceBeginDoc 之前设置时,组件会在生成期间执行相应规则,嵌入字体,跟踪 DeviceRGB 和 DeviceCMYK 对声明输出意图的使用,并拒绝配置文件禁止的功能。保存之后,这些同名属性还记录了已执行的配置,这正是管线日志需要的内容:

// After EndDoc: record the enforced profiles with the run metadata
if Pdf.PDFACompliance <> '' then
  Log('Generated as PDF/A level ' + Pdf.PDFACompliance);
if Pdf.PDFXCompliance <> '' then
  Log('Generated as PDF/X profile ' + Pdf.PDFXCompliance);

应把这些标志、输入数据哈希和 HotPDF 版本写入同一条日志。当验证器之后与生成器意见不一致时,这条日志能告诉你产生争议文件的是哪个模板 revision 和哪个库版本,一行日志纪律可以替代一整个下午的取证猜测。支撑这些标志、输出意图、ICC 配置文件和 tagging 的完整配置,见关于使用 HotPDF 输出 PDF/A、PDF/X 和 PDF/UA 的指南

在昂贵检查前先 triage 传入文件

许多管线并非纯生成型:客户上传 PDF,扫描仪投递 PDF,合作方通过邮件发送 PDF。对每个传入文件运行完整结构验证,会把队列时间浪费在根本打不开的输入上。HotPDF 的 Direct File API 可以在不加载完整对象树的情况下读取文件结构,因此可以作为廉价第一道门禁:

function TriagePdf(Pdf: THotPDF; const FileName: string): Boolean;
var
  Handle, Pages: Integer;
begin
  Result := False;
  Handle := Pdf.DAOpenFileReadOnly(FileName, '');
  if Handle <= 0 then
    Exit;  // structurally unreadable: quarantine, do not validate
  try
    Pages := Pdf.DAGetPageCount(Handle);
    Result := Pages > 0;
  finally
    Pdf.DACloseFile(Handle);
  end;
end;

这个 API 的两种行为会塑造外围逻辑。DAOpenFileReadOnly 只对未加密输入保持平坦内存;传入密码时它会在内部退回完整解析,因此已知加密文件应先通过 DecryptFile 生成普通工作副本。并且 DAGetPageCount 只对成功打开得到的句柄有效,因此应严格检查句柄,而不是假定会得到正值。更多 API 模式见面向大型 PDF 工作流的 Direct File API 文章

验证侧:把 veraPDF 作为构建步骤

对于 PDF/A 和 PDF/UA 声明,veraPDF 是值得接入管线的验证器:它可无界面运行,可处理批量文件,可输出机器可读 XML 或 JSON,并按 ISO 条款号报告每个失败,因此类似 ISO 19005-1 clause 6.2.2 的规则失败,可以直接映射到已知生成器设置。从 Delphi 调用它就是普通进程控制:

function RunVeraPdf(const PdfFile, ReportFile: string): Cardinal;
var
  Cmd: string;
  SI: TStartupInfo;
  PI: TProcessInformation;
begin
  Cmd := Format('cmd /c verapdf.bat --format xml "%s" > "%s"',
    [PdfFile, ReportFile]);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(SI);
  if not CreateProcess(nil, PChar(Cmd), nil, nil, False,
      CREATE_NO_WINDOW, nil, nil, SI, PI) then
    RaiseLastOSError;
  try
    WaitForSingleObject(PI.hProcess, 120000);  // bound the wait per file
    GetExitCodeProcess(PI.hProcess, Result);
  finally
    CloseHandle(PI.hThread);
    CloseHandle(PI.hProcess);
  end;
end;

超时不是装饰。畸形文件可能把任何解析器带入病态路径,而队列 worker 中无界等待会把整个队列拖垮。应限制等待,把超时作为带独立代码的失败,并将输入隔离供人工检查。解析 XML 时应读取规则标识符,而不是抓取面向人的消息;规则 ID 在验证器版本之间稳定,消息措辞不稳定,而稳定代码才是支持人员可以搜索历史工单的内容。

批处理行为需要和单文件正确性一样认真。应按每个文件启动一个验证器进程,而不是一批一个进程,这样病态输入只消耗该文件的超时,不会吞掉整批;将并发验证器进程限制到核心数,因为 XML 报告生成受 CPU 约束;并在接收入口设置文件大小上限,因为一个 2 GB 扫描巨物不管解析器多守规矩,都会主导队列。这些都不是 preflight 逻辑本身,但它们决定一个门禁能否撑过月底峰值,还是会在凌晨 2 点第一次堵住管线后被禁用。

PDF/X 是这个故事里的缺口:veraPDF 不覆盖它,实际检查仍然是带匹配 ISO 15930 配置的 Acrobat Preflight。Acrobat 是交互式工具,因此应把它用于抽样、新模板的第一件,以及每批少量随机抽取;自动门禁覆盖可自动化的内容。一个真正会发生的抽样人工检查,胜过一个没人完成的理论全自动方案。

报告必须包含什么才值得保留

预检门禁会产生两次价值:第一次是在阻止坏文件时,第二次是在几个月后有人询问为什么某个文件被接受时。第二种用途决定了报告格式。对每个检查过的文件,应保留:输入哈希、上面日志中的生成器符合性标志和版本、验证器名称和版本、检查使用的 profile、通过或失败结果,以及失败规则 ID 列表和验证器提供的页码。报告应存放在它描述的制品旁边,而不是放在一个会早于归档退役的独立系统里。

被接受的偏离同样需要书面记录。当客户坚持交付一个门禁不喜欢的文件时,应记录谁批准、为什么批准、批准到何时,并把 waiver 记录附到报告上,而不是全局削弱规则。带所有者和到期日的 waiver 是受管理的例外;被注释掉的检查是未来事故。

失败还应再走一步:把失败文件复制到命名回归文件夹。我们协助调试过的每个 preflight 事件,最终都归结为可复现输入;保留这些输入的团队能在数小时内修复回归,而不是在生产中重新发现它们。

FAQ

HotPDF 能以程序方式验证任意第三方 PDF 吗?

不能。产品中的 preflight report 是 GUI 演示功能,不是可调用 API。受支持的自动化模式是通过符合性属性在生成侧执行约束,再用 veraPDF 等外部验证器给出正式判定。

veraPDF 对印刷作业够用吗?

它覆盖 PDF/A 和 PDF/UA。对于 PDF/X 印刷母版,应使用印刷厂指定 profile 运行 Acrobat Preflight,并确认输出意图匹配他们期望的印刷 characterization。

什么应该让构建失败:只看错误,还是警告也算?

应以你声称符合的 profile 的规则失败作为门禁,并记录警告用于趋势监控。把每个警告都升级成阻塞项,会训练人们绕过门禁,这比没有门禁更糟。

产品参考

这条管线中使用的符合性属性和 Direct File API 属于面向 Delphi 和 C++Builder 的 HotPDF Component;其文档完整描述了本文展示的每个调用。