PDFium Component 讓 Delphi 應用程式能自行決定,當一份 PDF 參照了它沒有內嵌的字型時該使用哪些字型位元組。ConfigureSystemFontProvider 會安裝一個 IPdfSystemFontProvider 實作,接收 PDFium 提出的每一次字型對應要求,內含字面名稱、字重、斜體旗標、字元集與間距字族,並回傳應使用的 TrueType、TrueType 集合或 OpenType 位元組
這項功能存在的原因是,未內嵌字型本身就是一場轉譯彩券。一份命名了 Arial、卻沒有內嵌任何字型的 PDF,在工作站上會用 Arial 轉譯,在 Linux 伺服器上會用度量相容的替代字型轉譯,在封閉的容器映像上,則會用主機端對應器所能找到的任何字型轉譯。同一張發票在每個地方看起來都不一樣,斷行位置會跑掉,客戶收到的文件與存檔副本對不上
為什麼不乾脆把字型裝到伺服器上?
有時候那就是答案,若真是如此,就採用它。但在三種常見情境下這行不通。授權條款可能禁止為了自動化轉譯而在伺服器上安裝某款字型。容器映像經常重新建置,手動安裝的字型隨下一次部署就消失了。而受監管的流程需要轉譯堆疊能從受版本控制的產出物完整重現,而一台機器全域的字型安裝並不具備這個特性
供應器透過把這項決策移入你的應用程式來同時解決這三個問題。字型以你能掌控的資源形式出貨,對應政策是你能審閱的程式碼,同一份執行檔在任何地方轉譯結果都一致,因為它不依賴當下剛好安裝了什麼字型
安裝一個供應器
設定必須在函式庫載入之前完成。PDFium 會在初始化時接受一個系統字型資訊結構,之後就一直持有它發出的控制代碼,因此在文件仍開啟時更換供應器,會讓 PDFium 手上仍持有的字型控制代碼失效;元件會直接拒絕這麼做,而不是任由它破壞轉譯結果:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// 確定性對應:字面名稱加上字重與斜體,決定我們為此次
// 要求提供哪個檔案
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // 完整的 sfnt 或 TTC 位元組
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // 集合內的索引
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // 一切由主機端決定
Policy.AllowFaceSubstitution := False; // 拒絕不同的字面名稱
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// 只有到此刻才載入函式庫並開啟文件
end;
拆卸的順序則相反:先從 PDFium 卸下供應器,再卸載函式庫。跳過卸下這一步,會讓原生字型控制代碼指向即將被釋放的 Pascal 物件,這正是在混用參照計數介面與 C 函式庫的程式碼中,那種典型的關閉期存取違規
政策旗標實際決定了什麼?
AllowDefaultFallback 是兩種運作模式之間的切換開關。關閉時,供應器拒絕的要求就直接失敗,這在你要證明某個文件集中的每個字型都有著落時正是你要的行為:任何缺口都會立即顯現,而不是被悄悄掩蓋過去。開啟時,未解析的要求會轉交給 FPDF_GetDefaultSystemFontInfo 回傳的對應器,同時外部世界看到的仍是同一個統一的控制代碼封裝,字面名稱、字元集、資料表資料與字型刪除,都會依來源正確地路由
AllowFaceSubstitution 決定供應器是否可以用與要求不同的字面名稱來回應。關掉它,會讓替代成為一項明確決策而非意外,這在文件命名的字型與度量差異大到足以改變分頁結果時很重要
元件會在每個供應器回應到達 PDFium 之前先驗證:空資料會被拒絕,超過 MaxFontBytes 的過大字型會被拒絕,TTC 索引會被檢查,而當 PDFium 要求資料表時,各個 sfnt 資料表會直接從字型目錄中提供,而不是要求整個檔案。最後這項能力代表供應器可以交出一個完整的字型檔案,讓元件負責回答資料表層級的查詢,而不必把原始的 Pascal 物件暴露在 C ABI 邊界之外
快取而不留下懸空的字型資料
字型對應要求在轉譯期間會持續重複發生,因此回應會依涵蓋每個字型選定參數的鍵值來快取,並以有上限的最近最少使用順序淘汰。細微之處在於生命週期:PDFium 可能仍在讀取一個字型的位元組,而它的快取項目剛剛才被淘汰
快取儲存的是參照計數的動態陣列,每個原生控制代碼各自持有自己的快照,因此淘汰只是釋放一份參照,而不是釋放正在使用中的記憶體。刪除回呼會釋放控制代碼並維護一個作用中計數。實務上,這代表 MaxCacheEntries 可以為了節省記憶體而調整,完全不必擔心會從正在進行中的轉譯底下抽走資料
供應器是在我的執行緒上被呼叫的嗎?
不一定。PDFium 可能會從自己的工作執行緒呼叫這個對應器,因此實作必須是執行緒安全的。共用計數器、快取與設定觀察各自都由元件內部以自己的臨界區段保護,但 ResolveFont 內部的程式碼安全與否,得由你自己負責
最安全的做法,是讓供應器不觸碰任何可變的共用狀態:從啟動時建好的表格讀取、從檔案或資源載入位元組、回傳。若查詢需要你自己的共用快取,請加上保護。並把例外留在你的實作內部,因為一個 Pascal 例外絕不能穿過 PDFium 堆疊往外展開;元件會在 C ABI 邊界處攔截,並轉換為失敗或選用的預設回退,但把這件事當成正常控制流程來依賴,會拖累效能並掩蓋錯誤。元件其他部分的執行緒規則,遵循與 轉譯鎖紀律 一文相同的原則
在正式環境中證明對應結果
統計數據把字型替代從臆測變成可以斷言驗證的事實。GetSystemFontProviderStatistics 會回報是否已設定並安裝了供應器、發出了多少次對應要求,以及這些要求是如何被滿足的(拆分為快取命中、供應器命中與預設回退命中),連同被拒絕的回應、失敗的要求、作用中的控制代碼與已快取的字型:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// 在關閉回退功能的合規性測試中,任何一次回退命中或失敗要求,
// 都代表某份文件參照了我們沒有出貨的字型
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
RejectedProviderResponses 數量攀升,是供應器正以政策拒絕的資料來回應的訊號,通常是檔案過大或字面被替代,這值得設定警示,因為這類要求會悄悄降級成回退或失敗。若要在建立對應表之前,先診斷某份文件實際需要哪些字型,分析 PDF 字型屬性 一文所述的檢視路徑,能列出每份文件已內嵌與未內嵌的字型
字型供應、轉譯與文字擷取,在 Delphi、C++Builder 與 Lazarus 中共用同一個函式庫實例;部署細節說明於 PDFium Component for Delphi 頁面