losLab PDF Library 仅需一次调用即可嵌入已加载 PDF 中缺失的字体程序:EmbedMissingFonts 会遍历文档中的每个字体字典,根据其 BaseFont 名称定位匹配的已安装系统字体,并将字体程序写回文件中。对于修复因字体嵌入而导致 PDF/A 验证失败的第三方文档的团队来说,这就是消除预检错误 00030 的解决方法
这种情况非常普遍。归档接收管道从供应商、客户或扫描机构接收 PDF;这些文档在公司内的每台电脑上都能正常渲染;但随后 PDF/A 验证器拒绝了整批文件,并且每个文件都重复同样的抱怨:至少有一个字体未嵌入。上游没有人会重新生成这些文件,因此管道必须对它们进行修复。本文将介绍该修复路径。它是 预检文章(介绍如何检测 PDF/A 和 PDF/UA 违规)的配套篇章:那一篇会告诉你哪些文档损坏了,而这一篇则会修复最常见的损坏方式
为什么 PDF/A 要求嵌入每个字体?
ISO 19005-1 §6.3.4 要求符合标准的文档所使用的每个字体都必须在文件内部携带其字体程序,因为 PDF/A 的核心承诺是可重现性:文档在五十年后的机器上渲染时必须完全一致,即使那台机器与生产文档的机器没有共享任何字体。未嵌入的字体相当于在查看系统上寻找 Arial 的指令,而该标准的立场是,“查看系统上的某处”并不是一种归档保证。无论替代字体具有什么字形、度量标准和覆盖范围,这都是读者所看到的,而且它可能与作者看到的并不一致
历史上的罪魁祸首是 Standard 14 约定。PDF 1.0 承诺每个查看器都自带 Helvetica、Times、Courier、Symbol 和 ZapfDingbats,因此生成器学会了按名称引用这些字体而不进行任何嵌入,三十年来的工具至今仍在这样做。losLab PDF Library 非常重视这一要求,以至于在 PDF/A 创建模式下,AddStandardFont 被刻意设计为无操作:该库不附带 Standard 14 字体程序,无法嵌入它没有的东西,并拒绝将未嵌入的引用写入声称符合标准的文档中。它返回 0 且不选择字体,因此 PDF/A 文档必须改用带有嵌入功能的 AddTrueTypeFont,并且在 PDF/A 模式激活时,任何 Embed=0 的请求都会被默默提升为 Embed=1。这就是写入端。更难的问题是读取端:别人已经写好的文档,里面充满了你没有创建的字体字典
EmbedMissingFonts 如何修复加载的文档?
losLab PDF Library 是就地修复字体,而不是重建它们。当 PDF 生成器写入未嵌入的 TrueType 字体时,它生成的 FontDescriptor 字典已经是完整的:FontName、FontBBox、Flags、Ascent、Descent、StemV 统统存在。将其与嵌入字体区分开来的唯一要素就是缺少了一个条目,即保存实际字体程序的 /FontFile2 流引用。因此,EmbedMissingFonts 不会触及字体字典、编码、宽度数组或任何通过资源名称引用该字体的内嵌流。它从系统中读取匹配的字体程序,将其压缩为新的流对象,并在已有的 FontDescriptor 中追加一个 /FontFile2 引用(对于 CIDFontType0 字体则是 /FontFile3)。文档页面指向的所有内容都保持在原位,这使得该操作在处理您无法控制的文件时是安全的
范围涵盖了您在实践中会遇到的两种字体架构:简单的 TrueType 字体和复合的 Type0/CID 字体(用于中日韩文本和现代 Unicode 输出的字体)。遍历过程会刻意枚举文档对象树中的每个 Font 字典,而不是依赖逐页的资源遍历,因此从批注中引用或跨页面共享的字体也会被捕获。该 API 是对已加载文档的一次单一调用
var
PDF: TPDFlib;
Repaired: Integer;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('supplier-invoice.pdf', '') <> 1 then
raise Exception.Create('Could not load PDF');
// 遍历每个 Font 字典;返回有多少个字体
// 获取了字体程序。系统上找不到其程序的
// 字体会被跳过,而不是失败。
Repaired := PDF.EmbedMissingFonts;
Writeln(Format('%d font program(s) embedded', [Repaired]));
PDF.SaveToFile('supplier-invoice-repaired.pdf');
finally
PDF.Free;
end;
end;
使用预检报告验证修复
CreatePreflightReport 是验证步骤,且该循环是故意闭合的:谴责该文件的同一个审计也应该是清除其嫌疑的审计。错误代码 00030 是 PDF/A 深度审计结果,其内容为“至少有一个字体未嵌入(缺少 FontFile/FontFile2/FontFile3)”,它是针对整个文件报告的,因此只要有一个被忽视的字体,该错误就会保持活跃。在源文件上运行报告,进行修复、保存,然后在输出文件上再次运行它
function HasFontEmbeddingViolation(PDF: TPDFlib;
const FileName: string): Boolean;
var
Report: string;
begin
// ComplianceTests = 1 选择 PDF/A 检查
Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
Result := Pos('00030', Report) > 0;
end;
若要获得每个字体的视图而非每个文件的结论,请重新加载修复后的文档并进行枚举:FindFonts 之后是 SelectFont 和 GetFontIsEmbedded,它们会逐个字体报告嵌入状态,当批处理任务需要记录究竟是哪个文件中的哪种字体无法修复时,这是正确的工具。相同的枚举模式也出现在 从已加载 PDF 中提取文本、图像和字体的文章中,在该文章中它用于提取而不是修复
如果系统上未安装该字体会怎样?
EmbedMissingFonts 会跳过任何找不到其程序的字体,并通过其返回值报告跳过情况:如果返回的计数低于您计算的未嵌入字体的数量,则差值就是系统没有的字体。这是合理的失败模式,并且比其他选择更好,因为为文档中命名的字体发明一个替代程序会改变渲染,这正是归档修复绝不能做的事情。对于这些情况,losLab PDF Library 提供了 EmbedFontProgramFromFile,它将调用者提供的 .ttf 或 .otf 嵌入到命名的字体中,因此管道可以分发它预期会遇到的企业字体,并刻意回退到这些字体
var
I, FontID: Integer;
begin
PDF.FindFonts;
for I := 1 to PDF.FontCount do
begin
FontID := PDF.GetFontID(I);
if (FontID > 0) and (PDF.SelectFont(FontID) = 1) then
if PDF.GetFontIsEmbedded = 0 then
// 先尝试安装的系统字体,然后回退
// 到随应用程序一起分发的字体文件
if PDF.EmbedFontProgram(PDF.FontName) = 0 then
PDF.EmbedFontProgramFromFile(PDF.FontName,
'fonts\CorporateSans.ttf');
end;
end;
有两个界限值得坦率说明。首先,Type1 字体在当前实现中无法修复:它们的 /FontFile 条目需要带有显式长度键的三段式 PFB 结构,库会跳过它们而不是写入畸形的流;它们在现代文档中很少见,但确实会出现在旧的归档文件中。其次,嵌入字体是一种许可行为。TrueType 字体的嵌入权限归其字型厂商所有,如果修复管道将受许可限制的字体程序填充到离开组织的文档中,应当有人确认字体许可实际上允许这样做。库会执行您的请求;至于您是否可以请求,这是您的法务部门而不是您的编译器需要解答的问题
嵌入是必要的,但不是充分的
修复字体仅能清除错误 00030,别无其他。如果一个文档在加密、缺少 XMP 元数据、没有 OutputIntent 的设备相关色彩空间或缺失 ToUnicode 映射方面不符合 PDF/A 标准,那么在嵌入每个字体后它仍然会失败,这就是为什么修复应当属于预检驱动循环的一部分,而不是取代它。运行完整报告,修复它命名的内容,然后让报告告诉你何时完成。还有一个成本维度:一个完整的中日韩字体程序大小可达数兆字节,因此嵌入其中几个会使一个小文档急剧膨胀。其制衡手段是子集化(详见 关于 PDF 文件大小优化和字体子集化的文章),它将每个嵌入程序削减为文档实际渲染的字形
防止新文档退化
SetEmbedAllFonts 是同一功能的预防部分:一个写入端保护,用于阻止您自己的代码生成本文所修复的那类文档。在 SetEmbedAllFonts(1) 激活的情况下,任何随后请求 Embed=0 的 AddTrueTypeFont 调用都会被提升为嵌入式引用,这会将 PDF/A 模式已经强制执行的保证延伸到每个文档。它会影响调用后添加的字体,而不是已加载文件中已有的字体,因此分工很明确:为您创建的文档使用 SetEmbedAllFonts,为您继承的文档使用 EmbedMissingFonts
PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// 从这里开始,AddTrueTypeFont(Name, 0) 的行为
// 类似于 AddTrueTypeFont(Name, 1):没有未嵌入的
// 引用能进入输出文件
写入端保护和“加载-修复-保存”路径这两个部分都是适用于 Delphi、C# 和 VB.NET 的 losLab PDF Library 的一部分,同时也包括验证结果的预检引擎;产品页面包含完整的字体 API 参考,包括每个字体的嵌入和子集化调用