losLab PDF Library 可以透過單一呼叫,將已載入之 PDF 的缺失字型程式嵌入:EmbedMissingFonts 會遍歷文件中的每個字型字典,根據其 BaseFont 名稱找出相符的已安裝系統字型,然後將字型程式寫回檔案中。對於需要修復因字型嵌入而未能通過 PDF/A 驗證的第三方文件的團隊來說,這就是能讓 preflight 錯誤 00030 消失的修復方法
這種情況令人沮喪地普遍。封存擷取管線會收到來自供應商、客戶或掃描服務商的 PDF;這些文件在公司每張辦公桌上都能正常渲染;然後 PDF/A 驗證器就會以同樣的抱怨拒絕整批文件,每個檔案都重複一次:至少有一個字型未嵌入。上游沒有人會重新產生這些檔案,因此管線必須修復它們。本文探討了該修復路徑。它是 涵蓋偵測 PDF/A 和 PDF/UA 違規的 preflight 文章 的姊妹篇:那篇文章告訴您哪些文件損壞了,這篇文章則修復了它們最常見的損壞方式
為什麼 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 刻意地被設為無作用 (no-op):該函式庫不隨附 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 不會更動字型字典、編碼、寬度陣列,或任何按資源名稱參照該字型的內容串流。它從系統讀取相符的字型程式,將其壓縮到新的串流物件中,並將單一的 /FontFile2 參照(對於 CIDFontType0 字型為 /FontFile3)附加到已經存在的 FontDescriptor 中。文件頁面所指向的一切都精確保持在原來的位置,這就是使得對您無法控制的檔案執行此操作是安全的原因
其涵蓋範圍包含了您實際上會遇到的兩種字型架構:簡單的 TrueType 字型和複合的 Type0/CID 字型,後者是為中日韓 (CJK) 文字和現代 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');
// Walks every Font dictionary; returns how many fonts
// gained a font program. Fonts whose program cannot be
// found on the system are skipped, not failed.
Repaired := PDF.EmbedMissingFonts;
Writeln(Format('%d font program(s) embedded', [Repaired]));
PDF.SaveToFile('supplier-invoice-repaired.pdf');
finally
PDF.Free;
end;
end;
有一個值得了解的細節,因為它解釋了為什麼名稱比對比簡單的字串比較效果更好:函式庫會在尋找 BaseFont 名稱之前先對其進行標準化。子集前綴(六個大寫字母加上加號的 ABCDEF+ 模式)會被剝離,像是 ArialMT 這樣的 PostScript 樣式後綴會被解析為 Arial,並且會偵測並解開 TrueType Collection 檔案,因此位於 .ttc 內部的字體仍然可以正確嵌入
透過 preflight 報告驗證修復
CreatePreflightReport 是驗證步驟,而這個循環是刻意封閉的:定罪該檔案的同一項稽核應該也是為其放行的稽核。錯誤碼 00030 是 PDF/A 深度稽核的發現,其內容為「至少有一個字型未嵌入 (缺少 FontFile/FontFile2/FontFile3)」,而且它是針對整個檔案所回報的,因此只要有一個被忽略的字型就會讓它存在。在來源檔案上執行報告、修復、儲存,然後在輸出檔案上再次執行報告
function HasFontEmbeddingViolation(PDF: TPDFlib;
const FileName: string): Boolean;
var
Report: string;
begin
// ComplianceTests = 1 selects the PDF/A checks
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
// Try the installed system font first, then fall back
// to a font file shipped alongside the application
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 驗證的文件仍會失敗,這就是為什麼修復作業應屬於 preflight 驅動之迴圈的一部分,而不是取代它。執行完整的報告,修復它所指出的問題,並讓報告告訴您何時完成。還有一個成本考量:完整的 CJK 字型程式大小高達數 MB,因此嵌入數個這類字型可能會戲劇性地膨脹一個小文件。作為抗衡的是子集化,涵蓋於 關於 PDF 檔案大小最佳化與字型子集化的文章中,它會將每個嵌入的程式縮減至文件實際渲染的字形
防止新文件發生退化
SetEmbedAllFonts 是這項功能在預防方面的另一半:這是一個寫入端的防護措施,可阻止您自己的程式碼產生本文所修復的那種文件。在啟用 SetEmbedAllFonts(1) 的情況下,任何後續要求 Embed=0 的 AddTrueTypeFont 呼叫都會被提升為嵌入式參照,這會將 PDF/A 模式已經強制的保證延伸到每個文件。它會影響在呼叫之後所加入的字型,而不是已載入檔案中原有的字型,因此分工非常明確:SetEmbedAllFonts 負責您所建立的文件,EmbedMissingFonts 負責您所繼承的文件
PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// From here on, AddTrueTypeFont(Name, 0) behaves
// like AddTrueTypeFont(Name, 1): no non-embedded
// reference can reach the output file
這兩個部分,即寫入端的防護措施和載入-修復-儲存路徑,都是適用於 Delphi、C# 和 VB.NET 的 losLab PDF Library 的一部分,並與驗證結果的 preflight 引擎並列;產品頁面上載有完整的字型 API 參考,包括逐個字型的嵌入與子集化呼叫