HotPDF 从 Delphi 和 C++Builder 写出原生 PDF 2.0 文档,包括三个 PDF/A-4 归档子集,以及带命名空间结构元素的 PDF/UA-2 无障碍输出。选择它们只是两个属性的事,但这些属性背后的标准变化比版本号所暗示的要大:PDF/A-4 抛弃了大家跟着 PDF/A-2 学到的一致性字母,PDF/UA-2 则引入了子集 1 文档从未有过的结构命名空间
本文覆盖生成文件中究竟发生了什么变化,以及 HotPDF 把哪些错误变成了 EndDoc 时的一次异常,而不是一份在客户现场才通不过校验的文档
PDF/A-4 的标识与子集 2、3 有何不同
PDF/A-4 以子集号和修订年份来标识,基础子集没有一致性字母。把 PDFACompliance 设为 '4',HotPDF 就写出 pdfaid:part=4 配上 pdfaid:rev=2020,根本没有 pdfaid:conformance 条目。字母不是丢了——子集 4 没有 A/B/U 级别,因为过去用以区分它们的要求已经并入基础子集
两个扩展保留了字母。'4E' 选择用于工程文档的 PDF/A-4e 并写出一致性 E,它允许其他子集禁止的 3D 和 RichMedia 注解路径。'4F' 选择 PDF/A-4f 并写出一致性 F,它允许嵌入任意格式的文件。三者都强制 PDF 2.0 头,要求通常的 PDF/A 输出意图(output intent)和元数据检查,并禁止加密——一份加密的归档文件是标准根本不予考虑的矛盾
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-archive.pdf';
Pdf.PDFACompliance := '4F'; // PDF/A-4f: associated files of any format
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
'Structured invoice data', 'Data', LoadInvoiceBytes);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
AddPDFAssociatedFile 嵌入文件,用 /AFRelationship 构建它的 FileSpec,并把它同时登记到 Catalog 的 /AF 数组和 EmbeddedFiles 名称树里。两处登记都是必需的;只在其中一处列出的文件,是混合发票能通过一眼瞄过的检查、却通不过真正校验器的最常见原因。关系字符串接受 Source、Data、Alternative、Supplement 或 Unspecified,且当前 profile 必须是 PDF/A-3、PDF/A-4e 或 PDF/A-4f——基础子集 4 profile 不接受关联文件。旧的 AddPDFA3AssociatedFile 名字对既有代码仍然有效
PDF/UA-2 要求了哪些 PDF/UA-1 没要求的东西
PDF/UA-2 强制 PDF 2.0,并写出 pdfuaid:part=2 配上 pdfuaid:rev=2024,它还把命名空间引入结构树。一份子集 1 文档只有一个扁平的标准角色词表。一份子集 2 文档可以携带自定义角色,只要每个角色都属于一个已声明的命名空间——这正是让领域专属标签对辅助技术可读、而不是只能靠猜的关键
两个方法实现这一点。RegisterStructureNamespace 创建或复用一个间接的 /Type /Namespace 字典,并把它列入 StructTreeRoot /Namespaces,返回该字典以便复用。AddStructureElementNS 创建一个 /NS 条目指向该字典的结构元素,正是它为标准集合之外的角色名授了权。对同一 URI 的重复调用会复用一个字典,而不是堆出重复项
var
Root: THPDFDictionaryObject;
begin
Pdf.PDFUACompliance := True;
Pdf.PDFUAPart := 2; // part 2 forces PDF 2.0
Pdf.Lang := 'en-US';
Pdf.BeginDoc;
Root := Pdf.AddStructureElement('Document', nil);
Pdf.AddStructureElementNS('WidgetGroup',
'https://example.com/ns/widgets', Root);
Pdf.EndDoc;
end;
Lang 在这里不是装饰。一份已标记但没有声明自然语言的文档,会让屏幕阅读器去猜测发音,而 PDF/UA 把这种缺失视为缺陷而不是偏好
EndDoc 会抓出哪些结构错误
四种,每一种都对应一份否则就会带着破损到达校验器的文档。结构根必须恰好包含一个顶层 Document 元素。每个命名空间字典必须是间接对象,类型为 Namespace,并携带唯一的非空 URI。每个结构元素的 /NS 引用必须解析到一个确实列在根 /Namespaces 数组里的字典。而无命名空间的角色必须是 PDF 2.0 标准角色,或能通过 RoleMap 解析
这些检查在 EndDoc 触发,因为那是整棵树还完整存在于内存里的最后时刻,也是它第一次完整存在的时刻。更早抓意味着拒绝合法的中间状态;更晚抓意味着根本抓不到。对你的代码来说实际后果是:一处结构 bug 在生成结束时带着点名问题的消息浮现,而不是几周之后作为某人从客户那里转发过来的一份 veraPDF 报告浮现
值得了解的 PDF 2.0 角色
带类型的角色枚举增加了 DocumentFragment、Aside、Title、FENote、Sub、Em、Strong 和 Artifact。其中三个改变了你给普通商务文档打标签的方式。Aside 终于给侧栏和引文摘录(pull quote)安了一个不再被误用 Sect 的家。FENote 把脚注和尾注标明为它们本来的样子,让阅读器可以单独提供它们,而不是把它们和正文交错。Em 和 Strong 取代了过去把强调当成 span 级格式来打标签的语义猜测
字符串重载额外接受开放式的 Hn 形式,包括 H7 及更深的级别。PDF 1.7 停在 H6,逼着深层次的技术文档要么压扁大纲、要么复用级别。如果你生成的是标准文档、法律条文或零件目录,仅此一条就可能成为把输出迁到 PDF 2.0 的理由
切换生产输出之前要检查什么
PDF 2.0 是一次头部的改动,后面跟着长长的尾巴。老旧的归档入库工具、某些印刷 RIP,以及数量惊人的业务线阅读器只接受到 PDF 1.7 为止,它们失败在头部,而不是失败在你做错了什么。切换之前,先确认消费侧系统,并记住选择任何 PDF/A-4 profile 都会顺带选择 PDF 2.0,无论你有没有要求
稳妥的顺序是:面向未知读者的对外文档仍保留 PDF/A-3,内部归档在你控制入库的地方使用 PDF/A-4f,只有在可访问性策略明确点名的地方才采用 PDF/UA-2。如果你打算先啃归档那一侧,那么 PDF/A、PDF/X 与 PDF/UA 校验和 PDF/A-3 上的 ZUGFeRD 与 Factur-X 混合发票的指南覆盖了比版本号更要紧的那些 profile 选择,而 自动化预检报告的笔记展示了如何让判定结论成为构建的一部分而不是一道手工步骤
HotPDF 以面向 Delphi 和 C++Builder 的原生 VCL 代码交付整套 PDF 2.0 创作面,所以 PDF/A-4 和 PDF/UA-2 输出无需外部引擎或可再发布件——HotPDF 组件页列出了支持的 profile 与 RAD Studio 版本