在您的電腦上看起來完美,但在別人的電腦上卻算繪成一排空白方框的 PDF,是文件軟體中最常見的字型缺陷,而這幾乎不意味著文字本身有錯;字元是完整的,編碼也沒有問題,只是字形根本不存在;兩台電腦之間發生改變的是作業系統安裝了哪些字型,而可攜式檔案與脆弱檔案之間的差距,取決於編寫頁面時所做的一個決定:字型是隨同 PDF 一起傳輸,還是被假設存在於接收端
了解為什麼會發生這種情況,以及為什麼另一個獨立的失敗會產生看起來可搜尋但在複製出來時卻變成亂碼的文字,意味著需要研究 PDF 如何儲存文字;它不儲存句子;它儲存字形編碼加上字型程式,以及將兩者互相映射的表格,而每個算繪或擷取的 Bug 都存在於這三者之間的縫隙中;接下來是對該機制的導覽,基於 ISO 32000,並在關鍵之處配合控制它的 Delphi 呼叫進行說明
字元、編碼和字形是三種不同的東西
這些字彙容易讓人混淆,編碼因為日常言談常將三個截然不同的概念都簡化為「字母」;字元是書寫的抽象單元,即大寫 A 的概念,在 Unicode 中識別為 U+0041;字形是繪製出來的形狀,即特定字型用來描繪該字元的曲線與筆劃輪廓;介於它們之間的是編碼:內容串流中的一個或多個位元組,告訴檢視器要繪製目前字型中的哪一個字形
PDF 以編碼運作;當內容串流顯示字串時,這些位元組是作用中字型的索引,而不是 Unicode;字型的編碼決定了編碼 65 表示「繪製歸檔在 65 下的字形」,且該操作中沒有任何部分知道其結果對人類來說看起來像個 A;這就是為什麼 PDF 只要能找到字形就能在任何地方算繪出完全相同的效果,也是為什麼擷取文字與顯示文字是分開的問題:繪圖只需要「編碼到字形」的映射,而讀取需要「編碼到 Unicode」的映射,這是兩個不同的表格,它們可能獨立地產生分歧或丟失
您實際會遇到的字型類型
ISO 32000 定義了幾種字型字典類型,在實踐中,您接收或產生的文件使用的是其中三種之一;了解您所看到的是哪一種,就可以解釋大多數可能出錯的情況
Type 1 是 Adobe 原始的 PostScript 輪廓格式,由三次貝茲曲線建構而成;每個合規的閱讀器都必須提供的十四種標準字型(Helvetica、Times、Courier、Symbol 和 ZapfDingbats 系列)都是 Type 1,且命名其中之一的字型字典可以在法律上省略字型程式;這是規範中唯一一種不嵌入字型是安全的,而非憑運氣的情況;對於任何其他的 Type 1 字型,該程式必須嵌入,否則檢視器會使用替代字型,通常是度量衡相似但外觀明顯不同的字型
TrueType 使用二次曲線,源自於 Apple 和 Microsoft 的世界;它是大多數系統字型的格式,也是您最常嵌入的格式;PDF 中的簡單 TrueType 字型限制為單位元組編碼,因此一個此類字型一次最多只能定址 256 個字形;這個上限是中日韓(CJK)和其他大型文字無法使用簡單字型的結構性原因
Type 0(複合或以 CID 鍵定址的字型) 解決了這個限制;它使用多位元組編碼和 CMap 將它們路由引導到後代 CIDFont,其輪廓本身是 TrueType 或 CFF/Type 1;這是唯一能攜帶數千個字形的字型類型,因此任何包含中文、日文、韓文或廣泛多語言混合內容的 PDF 都使用 Type 0,無論作者是否有意識到這一點;代價是複雜性:有更多的移動部件,且其中更多部分必須在算繪和擷取時都正確無誤

該圖片背後的一個細節影響了檔案大小;字型是輪廓的函式庫,而不是固定大小的點陣圖,因此同一個嵌入的程式可以服務頁面上的每個點大小;縮放是繪製時應用的轉換,這就是為什麼標題及其內文共享一個嵌入的字型,以及為什麼嵌入的成本是按字型計算,而不是按大小計算
嵌入是可攜式與脆弱檔案之間的分水嶺
嵌入意味著字型程式(即實際的輪廓資料)被作為串流寫入到 PDF 中;在從未聽說過您的字型的電腦上的閱讀器會直接從檔案中讀取這些輪廓,並繪製出精確的字形;不嵌入字型,您就在賭接收端擁有同名的字型;如果沒有,檢視器會退而使用替代字型;對於標準的十四種字型,這種替代是明確定義且良性的;對於其他所有字型,其結果可能小至字型稍微有些微偏差,大至因為沒有任何替代字型覆蓋該文字而出現空白方框的結果
在 HotPDF 中,其控制是一個單一的屬性,在文件開啟前設定;FontEmbedding 告訴函式庫將它用來繪製的字型包裝到檔案中:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Compression := cmFlateDecode;
Pdf.FontEmbedding := True; // outlines travel inside the file
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Calibri', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
這種順序並非只是形式;BeginDoc 是 HotPDF 提交文件結構的地方,因此 FontEmbedding 必須在該呼叫之前設為 true;如果在之後指派,不會有錯誤,也不會有警告,只會靜默地輸出一個沒有字型的檔案;這是最糟糕的 Bug:它在開發人員的電腦上(剛好安裝了字型)通過了所有測試,但只在客戶的電腦上(未安裝字型)顯現出來
嵌入也是授權許可與工程交會的地方;字型程式攜帶了旗標,描述它是可以自由嵌入、僅供預覽,還是根本不能嵌入;尊重這些旗標是您的責任,而不是算繪器的責任,「能用」並不等於「被允許」
子集化:僅嵌入您使用的字形
完全嵌入會將整個字型程式寫入檔案;一個大型的中日韓 TrueType 字型可能達到數個百萬位元組,而為了顯示十幾個字元而將其整體嵌入,這種浪費在多頁文件中會累積擴大;子集化透過僅寫入文件參考的字形來解決此問題,然後用一個六個字母的標籤和一個加號重命名該字型,即任何子集化 PDF 字型清單中的 ABCDEF+Calibri 格式,這樣閱讀器就永遠不會將部分字型與同名的完整系統字型混淆
對於大多數產生的文件,子集化是正確的預設設定;它使檔案大小與內容成正比,而不是與來源字型成正比,這對於否則會佔用檔案主導地位的大型多語言字型最為重要;唯一需要注意的是,子集僅包含建置時使用的內容;如果後續的程序稍後嘗試向子集化的字型新增文字,它所需的字形可能不在檔案中,這是對編輯他人 PDF 的增量編輯的實際限制
Unicode 字型與中日韓方框問題
當文字不是純拉丁文時,簡單字型的路徑就走不通了,解決方法是明確註冊支援 Unicode 的字型,並讓 HotPDF 從中建置 Type 0字型;RegisterUnicodeTTF 透過路徑載入 TrueType 檔案;之後註冊的名稱就可以像其他名稱一樣在 SetFont 中使用:
Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;
有兩件事決定了這項操作的成敗;字型必須覆蓋字串中的文字:僅支援拉丁文的 TrueType 不會因為您要求就長出中文的字形,其結果仍是空白方框,這一次是因為字形在該字型中確實不存在;此外,嵌入必須保持開啟狀態,因為從註冊的 TTF 組合而成的 Type 0 字型對於找不到輪廓的閱讀器來說毫無意義;對於混合內容,持久的選擇是覆蓋範圍廣泛的字型(Noto 和 Arial Unicode MS 系列是通常的答案),並進行嵌入和子集化設定
由右至左和複雜的文字在覆蓋範圍之上新增了成形層;HotPDF 為阿拉伯文和希伯來文公開了 RtLTextOut,它處理方向性重排序,以便您傳遞邏輯順序並讓函式庫進行排版;要正確處理阿拉伯文需要覆蓋範圍、成形和方向這三件截然不同的事,此處的方框可能意味著其中任何一項失敗
ToUnicode 表:複製貼上的依據
以上所有內容都關乎繪圖;文字擷取是其鏡像,並會因其自身的原因而失敗;檢視器使用字型的「編碼到字形」映射來算繪頁面,但當使用者選取文字並複製它時,檢視器需要將這些相同的編碼轉換回 Unicode;該反向映射是 ToUnicode CMap,這是一個附加到字型上的選填串流
當它存在且正確時,複製的文字就會顯示為正確的字元;當它缺失、出錯,或者字型被子集化為自訂字形編碼且沒有寫入 ToUnicode 時,頁面看起來很完美,剪貼簿卻塞滿了垃圾:字形編碼被當作 Unicode 讀取,而對於自訂編碼的子集來說,它們並不是 Unicode;這就是為什麼帶有 OCR 文字層的掃描文件是可搜尋的,而來自不小心產生器的原生數位 PDF 卻不可搜尋的原因;算繪和擷取使用不同的表格,因此檔案可能滿足其中一個而無法滿足另一個;如果文字擷取對您的輸出很重要,請將正確 the ToUnicode 映射表視為一項要求,並透過從範例中複製文字來進行驗證,而不是相信它已經存在
如何快速診斷字型 Bug
失敗的模式會告訴您該看哪裡;在另一台電腦上出現的空白方框幾乎總是意味著字型沒有嵌入,因此請先檢查嵌入設定,其次檢查字形覆蓋範圍;即使在您自己的電腦上也出現的方框則指向覆蓋範圍:無論是否嵌入,該字型都不包含該指令碼;算繪正確但複製出來卻是亂碼的文字是 ToUnicode 的問題,而不是算繪問題,擺弄字型或嵌入設定是無法解決的,因為繪圖從未損壞;要讀取建置完成的檔案,請在 Acrobat 中開啟它,然後查看「文件屬性」中的「字型」:一個健康的項目會顯示類型、顯示「已嵌入」或「已嵌入子集」,並命名編碼;應該嵌入卻沒有嵌入的字型,會在客戶發現之前就在該處暴露出問題
一旦釐清了字元、編碼和字型之間的分離,這一切就都不難理解了;嵌入您用來繪製的字型、對大型字型進行子集化、在文字離開拉丁文時使用 Unicode 字型並進行 RegisterUnicodeTTF 註冊,如果有人需要擷取文字,則保留正確的 ToUnicode 映射表;做好這些,方框就不會再出現;對於周圍的力學結構,最小 PDF 結構解析 顯示了字型字典在物件樹中的位置,而 文件結構深入探索 則涵蓋了如何跨頁面共享資源
此處顯示的 SetFont、FontEmbedding 和 RegisterUnicodeTTF 呼叫,是適用於 Delphi 和 C++Builder 的 HotPDF 元件 的一部分