PDF/A 与 PDF/UA 回答的是两个互不相干的问题,而把它们当成同一个「无障碍兼归档」的勾选框,正是坏文件贴着合规标签混进归档的方式。PDF/A 问的是这份文件二十年后还能不能忠实渲染。PDF/UA 问的是辅助技术今天能不能读懂它。一份文档可以在其中一项上完胜、在另一项上完败,因此唯一诚实的裁决来自两者都跑,而且要在文件写出之前跑,而不是等下游系统去信任烘焙进元数据里的那个合规标识符。那个标识符是一句自我声明。格式本身并不要求它为真,而一个不做标准校验就把「PDF/A-1b」写进 XMP 的应用,产出的文件对每一个只读标签的消费者来说都像是合规的。losLab PDF Library(PDF Library for Delphi)把两个校验器都内建进库里,为 Delphi 和 C++Builder 补上了这道缺口,于是检查在进程内运行,无需另外架设外部服务
两个标准,两套相反的判败理由
ISO 19005(PDF/A)是一份复现契约。合规文件必须在几十年后、在从未见过其生产系统的软件上渲染出完全相同的结果,因此规则直指外部依赖:字体全部嵌入,颜色锚定到嵌入的 ICC OutputIntent 或以设备无关的色彩空间表达,PDF/A-1 中不得加密,不得含 JavaScript,XMP 元数据必须与文档信息字典一致。ISO 14289(PDF/UA)则是一份语义契约。辅助技术必须能遍历文档并从中得到意义,而意义住在完全不同的一层:完整的结构树、图上的替代文本、为显示而设定的文档标题、不跳级的标题层次,以及在页面离开屏幕之后仍然成立的表头关系
正因为两个标准管的是不同的层,真正咬人的是夹在两者之间的那些文件。一份归档意义上完美的文档,对屏幕阅读器可能哑口无言。一份标签打得漂亮的文档,可能引用了十年后不会存在的桌面字体。公共部门出版通常是两项要求同时落地的地方,而那里的流水线不能把它们坍缩成一道关卡。检查结果要送到不同的人手上。未嵌入字体是生成 PDF 那段代码的缺陷,而缺失替代文本归属于内容模板的负责人,一份把两者混在一起的报告只会被转发两次
你瞄准 PDF/A 的哪个部分,与你有没有达标同样要紧。PDF/A-1 冻结在 PDF 1.4 上,拒绝透明度和 JPEG2000,而现代报表输出对这两者都是想都不想就用。PDF/A-2(ISO 19005-2,建立在 ISO 32000-1 之上)两者都接受,是新建归档时理智的默认选择。PDF/A-3 走得更远,允许任意类型的嵌入文件,受监管的电子发票格式正是靠它。一个到 2026 年还把 PDF/A-1b 当标准的团队,通常背着某位十五年前写下的要求,而重新商定目标部分,往往比把系统输出的每张图表里的透明度都剥掉更省事
在接入环节拿到结构化的检查项
扁平 API 的入口是 CheckFileCompliance,测试选择器取 1 表示 PDF/A,取 2 表示 PDF/UA。它交还一个字符串列表句柄,其中每一条目就是一项检查发现,一行一条,这正是自动化关卡想要遍历的形态:
function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
ListId, I: Integer;
begin
ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0); // 1 = PDF/A
if ListId = 0 then
begin
// 0 既可能表示「没有问题」也可能表示「文件读不了」——放行前先分清
Result := Pdf.LastErrorCode = 0;
Exit;
end;
for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
Pdf.ReleaseStringList(ListId);
Result := False;
end;
有两处细节决定这段代码能不能无人值守地跑。第一处是一个含义完全相反的返回值。文件完全合规时 CheckFileCompliance 返回 0,文件压根打不开时它同样返回 0,因为在内部这两种情形下空结果列表都坍缩成 0。一个把 0 读作通过的关卡,会把损坏的上传直接放进归档,所以请像上面那个关卡那样,在信任这个零之前先用 LastErrorCode 把它分辨清楚。第二处关系到文件正处在生命周期的哪一步。检查器跑在库的流式读取器上而不是完整文档模型上,它以读共享方式直接打开文件,从不调用 LoadFromFile,这也是它能在不构建对象树的情况下啃下数 GB 输入的原因。同样是这个流式打开,在另一个进程仍以写方式持有该文件时会失败,而正在进行中的上传恰恰就是这种状态。请在传输结束之后再开闸
流式设计在高负载下再次得到回报。每次检查都以只读方式打开输入并允许共享读取,因此一次语料级审计可以横向扩展到多个工作线程或进程上,每个工作线程配一个 TPDFlib 实例,彼此之间没有争用。真正需要纪律的资源是句柄本身。CheckFileCompliance 每一个非零结果都会一直占用,直到你调用 ReleaseStringList;一个忘记释放它们的长期运行关卡不会崩溃,它只会慢慢渗血,直到有人去追查这是为什么
给人看的报告,给构建关卡看的差异
一份检查项列表对关卡来说形态正确,对发给模板团队的邮件来说形态错误。CreatePreflightReport 把同一份分析渲染成可读的文字,CreatePreflightReportEx 增加一个报告格式选择器,而 SavePreflightReport 把它写到磁盘,好让报告随交付的文档包一起流转。不少归档合同会把这份报告本身列为交付物,而不只是内部产物
这一族里默默称职的成员是 ComparePreflightReports。合规性和任何其他行为一样,也是一个会回归的面。改一次模板、新授权一款企业字体、升一次库版本,每一样都可能引入上个版本没有的检查项,而它们都不会自报家门。请把一组代表性文档的黄金报告纳入版本控制,每次变更后重新生成,再用 ComparePreflightReports 算出差异。一份空的 diff 是值得留存的发布产物。一项意外出现的检查项会让构建失败,而那是比审计便宜得多的发现地点
让输出第一次就通过
预检的价值主要体现在从别处送来的文件上。对于你自己代码生成的文档,生成完再去找违规并回头打补丁是绕远路的做法。PDF Library for Delphi 为每个标准都提供了生成侧的模式,而且你可以给同一份文档同时打开两者:
var
Pdf: TPDFlib;
Diag: WideString;
begin
Pdf := TPDFlib.Create;
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1);
Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
Pdf.SetPDFUAMode('en-US');
Pdf.SetInformation(1, 'Quarterly Statement'); // /Title:PDF/UA 必需
// ... 在这里绘制带标签的内容 ...
Diag := Pdf.GetPDFUADiagnostics;
if Diag <> '' then
Writeln('fix before shipping: ', Diag);
Pdf.SaveToFile('statement.pdf');
// 算数的那次预检跑在已保存的文件上:
Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
finally
Pdf.Free;
end;
end;
陷阱藏在保存这一刻。若干项合规修补是在文档被序列化的过程中发生的,而不是在你启用模式的那一刻:强制打开注释上的打印标志、为 PDF/A-3 嵌入文件写入默认的 AFRelationship、为 PDF/UA 归一化 Tab 顺序与表单字段描述。躺在内存里的文档与落到磁盘上的那份并非逐字节相同,因此唯一有意义的预检裁决,是从已保存文件算出来的那一个。请校验 statement.pdf 本身。不要从仍在内存里的对象推断合规性,因为你所评判的那些字节并不是你交付出去的那些字节
在可视文档旁边携带机器可读 XML 的发票场景遵循 ZUGFeRD 与 Factur-X 模式,而它建立在 PDF/A-3 之上。这类文件应当用 SetPDFA3DefaultAFRelationship 显式设定附件关系,因为 ISO 19005-3 要求每个嵌入文件都声明自己相对于文档的角色。不设它,嵌入的 XML 就只是一团没有说明用途的数据块,而校验器会注意到这一点
独立裁判:veraPDF 与 Acrobat
生产者不该是自己输出的唯一裁判。PDF Library for Delphi 的检查器在进程内给你快速、结构化的裁决,这正是热路径上所需要的;但归档批次的发布关卡,仍应让输出通过一个不是你团队里任何人写的校验器。veraPDF 是 PDF/A 的社区维护参考实现,也是多数归档在验收条件里点名的工具,因此它就是要对齐的那一个。当 veraPDF 与进程内检查意见相左时,Acrobat 的预检配置文件是有用的第三方裁断。请在每份存档报告旁边记下校验器名称及其版本。一句「这份文件通过了 veraPDF」如果不带上通过时的构建号,其实说明不了什么,因为这个工具会在版本之间收紧规则
校验器在标准的边缘地带确实会有分歧,而分歧出现时,答案不是挑你顺眼的那个工具。请把文件缩到仍能触发该分歧的最小样本,再对照标准原文去读。花上一小时,通常会浮出两样东西之一:一个值得上报的真实工具缺陷,或者一条你的团队一直读错的条款,而后者应当写进合规笔记里,免得下一个人再吵一遍
加密输入有条捷径。两个检查器都接受口令参数,但一份带加密字典的 PDF/A-1 文件本身就已不合规,因为 ISO 19005-1 彻底禁止加密,所以加密的投稿可以在任何更深入的分析开始之前就被挡回去。弄清一个加密字典究竟授予了什么,本身是另一件活儿,见 PDF 加密与权限审计
PDF/UA 的检查项几乎总能追溯到结构树最初是怎么写的,而背后的标签技术见 在 Delphi 中构建 Tagged PDF 结构树。同时还要求数字签名的归档,应当把这道关卡与 PAdES 签名与校验中的工作流配套使用。完整的预检 API 参考位于 losLab PDF Library for Delphi 产品页