跳到內容

字型:棘手之處

Evidence: Mixed evidence

字型是 PDF 可能看似完全正確、卻悄悄出錯的地方。一個頁面可以呈現正確的字形,卻無法搜尋、無法以文字形式複製,也不符合典藏設定檔。這些問題可能同時存在,而且沒有任何可見跡象提醒你。本頁談的是必須做對的三件事——內嵌、子集化、編碼——以及 NextPDF 對每一項的處理方式。

「看起來沒問題」是 PDF 工作中最危險的一句話,而字型正是它造成最大傷害的地方。有三件彼此獨立的事情必須成立:

  1. 內嵌——字型程式會隨檔案一起傳遞,因此即使機器未安裝該字型,也能呈現相同結果。
  2. 子集化——只攜帶實際用到的字形,因此一個 20 MB 的 CJK 字型不會讓每份文件變得臃腫。
  3. 編碼——存在一份正確的對應,可從頁面上的字元碼回到 Unicode,因此文字能被搜尋、複製、索引,並由輔助技術讀取。

視覺呈現只能部分證明第一項。一份文件可以顯示完美的字形,卻完全無法通過第三項;這時文字只是文字的圖片,而不是文字。這正是能通過每一次「看起來沒問題」審查,之後卻在合規稽核或調閱請求中失敗的錯誤。

  • PDF 中的字型是一個字典,通常再加上一個內嵌字型程式串流。
  • 子集化會重寫該程式,使其只包含用到的字形。子集字型名稱會加上一個六個大寫字母的標籤與一個 +,讓閱讀器將其視為獨立字型。
  • 編碼是另一個獨立問題:將字元碼對應到 Unicode。一份 /ToUnicode CMap 是讓文字可搜尋、可複製的關鍵,而且它獨立於字形看起來是否正確。
  • 看似正確,卻沒有(或含有錯誤的)/ToUnicode 的文字,是典型的無聲失敗:螢幕上完美,實務上卻無法搜尋。
  • NextPDF 會對 TrueType 字型進行子集化、保留字形識別以確保正確呈現,並輸出一份 /ToUnicode CMap 讓擷取能正常運作;它也能強制執行 PDF 2.0 的內嵌規則,而不只是發出警告。

子集化。 FontSubsettersrc/Typography/FontSubsetter.php)會解析原始 TrueType 表目錄,並讀取 cmap 以將 Unicode 碼位對應到字形 ID。它同時處理 BMP 的格式 4 與全 Unicode 的格式 12;後者正是 CJK 所需要的。接著,它會執行過於簡化的子集化工具容易漏掉的步驟:以遞移閉包解析複合字形相依關係。由一個基底字母加上一個組合記號構成的重音字形,會以元件形式參照其他字形。若丟棄那些元件,字形就會呈現錯誤。子集化工具會走訪該圖,直到不再出現新元件,並設有循環防護,使格式錯誤的字型無法讓流程永遠迴圈下去。

該檔案中有兩項值得一提的工程選擇。第一,字形 ID 是保留、而非重新對應——未使用的位置會在 glyf/loca 中以零填補,因此內容串流的原始字形索引在 CIDToGIDMap /Identity 下仍然有效。重新對應雖然更小,卻需要重寫每一處字形參照。保留識別在設計上就是正確做法。第二,走訪是排序過的(依 gid 遞增),因此子集在位元組層級具備確定性——相同的字型與相同的已用字形會產生相同的子集位元組,這正是可重現建置所要求的。如果子集化能節省的空間不到檔案的約 10%,就會原封不動地傳回原始字型。為了邊際收益付出這份額外成本並不值得。

內嵌。 字型程式是否要隨檔案攜帶,由明確政策決定——絕不臆測。Pdf20FontEmbeddingPolicysrc/Writer/Pdf20FontEmbeddingPolicy.php)有兩種模式。在 PDF 2.0 設定檔下,Strict 會以一個具型別的例外,拒絕未內嵌的標準 Type 1(「Base14」)參照;這是符合規範的正確行為。AllowBase14 則保留歷來的建議性路徑。作為遷移視窗,它會輸出標準仍要求的最小字型描述子,並派送一則警告,而不是拋出例外。呼叫端必須在文件上明確做出此選擇;它絕不會由字型推斷出來。

編碼。 對於複合(Type 0)字型,EmbeddedTtfFontDictBuildersrc/Writer/EmbeddedTtfFontDictBuilder.php)會輸出 CIDFontType2 後代、Type0 父代,以及一份 /ToUnicode CMap 串流,讓字元碼能解析回 Unicode。/ToUnicode 串流只有在一種情況下可以正當缺席:一份自我描述的預定義 CJK CMap 已經為閱讀器提供字元到 Unicode 的對應時。在那種情況下,該 CMap 就是編碼,因此純設定檔會省略多餘的 /ToUnicode 串流以節省位元組。除此之外,/ToUnicode 串流正是讓文字維持為文字的關鍵。

考量項目它保證的內容保證的內容出錯時的無聲失敗
內嵌未安裝字型時也能相同呈現文字可被搜尋替代字型;在另一台機器上度量錯誤
子集化小型檔案;只含用到的字形任何與編碼相關的事缺少複合元件 → 重音字形損壞
編碼(/ToUnicode可搜尋、可複製、可存取的文字字形能正確呈現頁面看似完美,卻無法搜尋/複製時變成亂碼

這三項字型考量彼此獨立。內嵌與子集化關乎 外觀與大小;編碼關乎意義。一個頁面可以通過前兩項,卻在第三項失敗,而且沒有任何可見跡象顯示出來。

子集命名規則具有規範性,而且非常精確。 Spec: ISO 32000-2, §9.9.2 要求字型子集的 PostScript 名稱——BaseFont 與描述子的 FontName—— 必須以恰好六個大寫字母的標籤開頭,後接一個加號, 再接原始字型的 PostScript 名稱。它也要求同一檔案中同一字型的不同子集,必須使用不同標籤。正是這條規則,讓閱讀器能分辨兩個子集,並在合併文件時正確處理它們。 Evidence: Standard-backed

編碼與呈現分屬不同條款。 Spec: ISO 32000-2, §9.10.3 /ToUnicode 定義為一個串流,內含一份將字元碼對應到 Unicode 值的 CMap, 而下列文字擷取程序 Spec: ISO 32000-2, §9.10.2 使用該 CMap 將字元碼轉換為 Unicode,以供搜尋與索引。字形繪製機制中, 沒有任何部分會碰觸 /ToUnicode;這正是為什麼文字可以看起來正確,擷取卻錯誤。

關於內嵌,標準指出,大多數字型字典帶有字型描述子,而內嵌字型檔串流是選用,但強烈建議的。PDF 2.0 針對那十四個標準 Type 1 字型特別收緊此要求。NextPDF 的 Strict 政策正是對該收緊的合規解讀。AllowBase14 則是明確、需主動選用的向後相容出口——引擎絕不會無聲降級。

Strict PDF 2.0 font-embedding enforcement — edition availability
Edition Availability
Core

可用。子集化、/ToUnicode 輸出,以及明確的 Strict / AllowBase14 內嵌政策,都是核心引擎行為。

Pro

在設定檔層級,增加更深入的字型內嵌規範強制執行與報告。

Enterprise

在企業營運介面下,提供相同的規範強制執行。

以下是正確內嵌、子集化且可搜尋的複合字型中的兩個部分。子集標籤遵循標準的六字母規則;/ToUnicode 參照則是讓文字維持可擷取的關鍵。

% The Type 0 (composite) font dictionary
20 0 obj
<< /Type /Font /Subtype /Type0
/BaseFont /ABCDEF+NotoSans % six-letter subset tag + '+'
/Encoding /Identity-H
/DescendantFonts [21 0 R]
/ToUnicode 23 0 R >> % the map that makes text searchable
endobj
% The descendant CIDFontType2 (carries the subsetted program)
21 0 obj
<< /Type /Font /Subtype /CIDFontType2
/BaseFont /ABCDEF+NotoSans
/CIDToGIDMap /Identity % glyph IDs preserved, not remapped
/FontDescriptor 22 0 R >>
endobj

物件 20 的 /ToUnicode 23 0 R,正是可搜尋文件與文件圖片之間的差別。把它丟掉(在預定義 CMap 的情況之外),每個字形仍會完美繪製,但在頁面上搜尋任何字詞都會一無所獲。

直白地說,這個陷阱是:**字形正確呈現,完全不代表文字就是文字。**呈現走的是從編碼到字形的路徑。搜尋與複製走的是從字元碼到 Unicode 的路徑(/ToUnicode)。它們是讀取字型字典中不同部分的不同機制。因此,一份文件可以有完美無瑕的視覺輸出,卻有缺失或錯誤的 /ToUnicode。結果是一個看起來權威、實際上卻無法搜尋的頁面——這種錯誤能逃過每一次視覺審查,因為它依定義就沒有任何可看之處。

另一個相關陷阱,是假設「字型已內嵌,所以我們的典藏沒問題」。內嵌是必要的,但並不充分。像 PDF/A 這樣的設定檔還會要求子集依六字母規則命名,並具備正確編碼。已內嵌但無法搜尋,仍然是失敗。

NextPDF 的子集化工具專門用於 TrueType 子集化。它需要必要的 TrueType 表;若這些表缺失,或收益低於約 10% 門檻,就會原封不動地傳回原始字型。子集化與 /ToUnicode CMap 能讓文字可擷取,但無法挽救缺乏資訊、無法將字形對應回有意義字元的來源字型。在無法判定任何 Unicode 值之處,再多的 CMap 輸出也無法憑空造出一個。

本頁談的是在 NextPDF 所寫的文件中產生正確的字型結構。它不是用來修復任意傳入 PDF 的字型修復工具。而輸出符合規範的子集與編碼,本身並不會讓文件通過完整典藏設定檔的認證;那是另一項更廣泛的檢查。

為什麼要六字母標籤——為什麼不用字型名稱? 這樣閱讀器才能分辨同一字型的兩個不同子集,並在合併文件時不讓它們的字形集相撞。不同子集會依規則使用不同標籤。

何時可以沒有 /ToUnicode 當一份自我描述的預定義 CJK CMap 已經提供字元到 Unicode 的對應時。在那種情況下,該 CMap 就是編碼。再提供一份 /ToUnicode 便是多餘的。除此之外,它的缺席就是缺陷。

子集化會造成傷害嗎? 只有在做錯時才會。丟棄複合字形的元件會破壞重音字形。重新對應字形 ID,卻不重寫參照,會破壞呈現。NextPDF 透過解析元件閉包並保留字形識別,避免了這兩種情況。

  • 內嵌字型程式——以串流形式攜帶於 PDF 內部的實際字型檔(TrueType/CFF/Type 1),因此呈現不依賴閱讀器已安裝的字型。
  • 子集化——重寫字型程式,使其只包含文件實際使用的字形,以縮減大小。
  • 子集標籤——子集字型名稱上強制要求的六個大寫字母前綴加上 +(例如 ABCDEF+NotoSans)。
  • /ToUnicode——一份將字元碼對應到 Unicode 值的 CMap 串流;讓 PDF 文字可搜尋、可複製、可存取的關鍵。
  • 複合字形——藉由參照其他字形作為元件而構成的字形;子集化時必須保留這些元件。
  • CIDToGIDMap /Identity——一種模式,其中內容串流的字形索引就是字型自身未變更的字形 ID;NextPDF 保留字形識別以維持其有效性。
  • Base14——那十四個標準 Type 1 字型;PDF 2.0 期望字型被內嵌,而非僅以名稱參照。