某政府归档机构曾退回一位客户的 Delphi 计费系统生成的整批 1,400 张发票。客户尝试的每个查看器都能完美打开这些文件;veraPDF 却因为同一个原因拒绝全部文件:缺少 OutputIntent。这个事件概括了标准符合性 PDF 工作的核心教训:视觉正确性什么也证明不了,因为 PDF/A、PDF/X 和 PDF/UA 约束的是文件内部结构,而不是它看起来怎样。HotPDF 是 losLab 的原生 VCL PDF 库,它把这些约束纳入文档生成,让符合性在第一页出现之前就被配置,而不是事后修补。
三个 ISO 标准,三个不同承诺
PDF/A(ISO 19005)承诺文件在几十年后仍会以相同方式渲染,因此它要求完全自包含:每个字体都嵌入,每种颜色都通过 OutputIntent 设备无关化,完整 XMP 元数据,并禁止任何依赖环境行为的内容,包括加密和 JavaScript。PDF/X(ISO 15930)承诺设计方与印刷厂之间的盲交换,所以它的规则关注颜色和几何:特征化印刷条件、强制 /Trapped 键、定义 trim 和 bleed 几何,在 X-1a 变体中还禁止 live transparency。PDF/UA(ISO 14289)承诺辅助技术可以读取文档,因此它要求逻辑结构:完整 tag tree、正确阅读顺序、声明文档语言,以及非文本内容的替代文本。
这些承诺会拉向不同方向。交互式表单在 PDF/UA 中可以存在,却无法与归档配置对动态行为的限制组合;只面向 CMYK 的印刷母版,对永远看不到颜色的屏幕阅读器用户则完全不合适。应按输出通道决定由哪个标准主导,而不是追求一个文件满足所有标准。
HotPDF 中的 PDF/A:OutputIntent 就是关键
上面的归档退回归根结底是缺少一个结构,而且它正是多数生成器会忘掉的结构,因为没有任何可见内容依赖它。ISO 19005 要求 OutputIntent:嵌入式 ICC 配置文件,为设备颜色提供无歧义含义。HotPDF 把该配置文件作为显式输入:
var
Pdf: THotPDF;
ICC: TFileStream;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-archival.pdf';
Pdf.PDFACompliance := 'B'; // level B: visual fidelity
Pdf.Lang := 'en-US';
Pdf.StandardFontEmulation := False; // embed real fonts, no Base-14 emulation
ICC := TFileStream.Create('sRGB.icc', fmOpenRead);
try
Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', '', ICC, 3, 'DeviceRGB');
finally
ICC.Free;
end;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Archival invoice body');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
三个配置细节决定通过或失败。StandardFontEmulation 必须关闭,因为模拟 Base-14 字体按定义没有嵌入,而嵌入在 ISO 19005 下没有商量空间。加密必须保持禁用;不要把 PDFACompliance 与 ActivateProtection 组合,因为加密归档文件是验证器会抓住的自相矛盾。传给 AddPDFAOutputIntent 的组件数必须与配置文件匹配,例如 sRGB IEC61966-2.1 这样的 RGB 配置文件应为 3。HotPDF 会在生成过程中跟踪 DeviceRGB 和 DeviceCMYK 使用,并与声明的 intent 对比,因此 RGB-intent 文档中的 CMYK 填充会表现为验证问题,而不是静默不一致。
应把 ICC 配置文件本身视为带版本的部署制品,而不是某人曾经复制到构建服务器的一份文件。配置文件字节会嵌入每个生成文档,因此损坏或截断的配置文件会一次性污染整批文件,失败只会在验证时出现。应随安装程序交付该配置文件,在运行日志中记录其校验和,并通过上面的 TFileStream 模式加载,这样缺失文件会在生成阶段大声失败,而不是在归档门口悄悄失败。
面向印刷的 PDF/X:Trapped、CMYK 和印刷配置文件
印刷母版会把颜色故事反过来:印刷机需要特征化 CMYK,而标准坚持要求你声明是否已经做 trapping。/Trapped 键是强制性的,即使诚实答案是你不知道:
Pdf.PDFXCompliance := 'X-1a';
Pdf.Trapped := 'Unknown'; // mandatory key under ISO 15930
ICC := TFileStream.Create('FOGRA39.icc', fmOpenRead);
try
Pdf.AddPDFXOutputIntent('FOGRA39 (ISO 12647-2:2004)', '', ICC, 4, 'DeviceCMYK');
finally
ICC.Free;
end;
Pdf.BeginDoc;
// draw with CMYK-safe colors, no transparency, no encryption
Pdf.EndDoc;
注意 CMYK 印刷配置文件的组件数变成了 4。X-1a 还禁止 live transparency,因此应审阅任何叠加半透明元素的绘制代码;查看器在屏幕上合成出的效果,正是 RIP 拒绝猜测的内容。如果印刷厂给你另一种 characterization,就替换配置文件和标识符,但保持结构相同。
PDF/UA:结构是在生成时产生的,不能事后补
无障碍是团队最常试图在最后补上的标准,也是事后补最容易失败的标准,因为 tag tree 必须镜像内容创建时的逻辑顺序。在 HotPDF 中,设置 PDFUACompliance 会打开 tagged output,结构 API 会把每个绘制调用绑定到其语义角色:
Pdf.PDFUACompliance := True; // auto-enables tagged PDF
Pdf.Lang := 'en-US'; // set explicitly; empty falls back to 'en'
Pdf.BeginDoc;
Root := Pdf.AddStructureElement(sstDocument, nil);
H1 := Pdf.EmitTaggedHeading(1, Root, 50, 700, 'Quarterly Report');
Para := Pdf.BeginTaggedContent('P', Root);
Pdf.CurrentPage.TextOut(50, 650, 0, 'Revenue grew in all regions.');
Pdf.EndTaggedContent;
Pdf.EndDoc;
需要防范的错误模式,是在任何 BeginTaggedContent/EndTaggedContent 配对之外绘制内容:它正常渲染,却对屏幕阅读器不可见,这是最糟糕的失败,因为有视力的测试人员永远不会注意到。如果模板使用自定义结构角色名,应使用 AddStructRoleMap('MyHead', 'H1') 把它们映射到标准集合,让符合规范的阅读器知道如何解释。ISO 14289 也要求声明语言;HotPDF 会替换成 'en',当 Lang 为空时,这是安全网,不是跳过设置真实文档语言的理由。
验证:相信验证器,不要相信查看器
发票事件的教训很简单:把检查结构而不是检查渲染的工具纳入发布路径。对 PDF/A 和 PDF/UA,veraPDF 是参考级开放验证器,并按 ISO 条款报告失败,因此它的输出可以直接映射到上面展示的配置。对 PDF/X,Adobe Acrobat 的 Preflight 配置仍是实用检查,因为印刷条件既关乎语法,也关乎颜色 intent。在生成器内部,HotPDF 会在保存时把功能标志与配置的 PDF 版本调和,静默降级版本无法表达的内容,例如 PDF 1.7 以下的 AES-256;同时 EndDoc 中的符合性门禁会对硬冲突直接抛出,例如把 PDFACompliance 与加密组合。两者都不能替代外部验证器;合起来可以防止不可能的配置抵达验证器。
应把整个符合性配置一起版本化:HotPDF 版本、模板 revision、ICC 配置文件校验和,以及签署通过的验证器版本。只要其中任何一个在其他项底下变化,符合性就会漂移;我们见过最痛苦的审计,都是没人能说清五年前某个归档文件由哪一组组合产生。每批一个配置记录,就能永久关闭这个问题。
验证器应跑在真实生产输出上,而不是手工构造的样本上。真正重要的失败来自数据:客户 logo 是 CMYK,而 intent 声明 RGB;模板 revision 引入未嵌入字体;新代码路径绘制未标记文本。每个历史事件保留一个已知失败文件作为回归输入,符合性门禁才会保持诚实。关于这些管线的渲染侧,参见使用 HotPDF 输出报表、字体和图像的文章;关于把验证器接入构建,参见自动化 PDF 预检检查的配套文章。
FAQ
一个 PDF 能同时符合 PDF/A 和 PDF/X 吗?
有时可以,但很少值得在约束之间来回协调:归档配置要求设备无关颜色和完整元数据,印刷配置要求特征化 CMYK 和 trapping 声明。应从同一源数据为每个通道分别生成,而不是强迫一个文件服务两边。
为什么 veraPDF 会拒绝所有查看器都能正常打开的文件?
查看器刻意宽容;验证器刻意严格。缺失 OutputIntent、未嵌入字体和缺少 XMP 元数据都不会影响渲染,因此只有结构验证器会报告它们。
发票应该使用 PDF/A level A 还是 level B?
Level B 保证视觉复现,是大多数归档机构对扫描或生成业务文档的要求。Level A 增加 tagged-structure 要求,实际把 PDF/UA 工作量也拉进来;当无障碍义务适用于归档本身时,它才是正确选择。
延伸阅读
本文中的符合性属性、输出意图和 tagging API 都属于面向 Delphi 和 C++Builder 的 HotPDF Component;产品页链接了这里使用的每个调用的完整参考。