HotPDF 能把 TrueType 與 OpenType 字型子集保留在磁碟上,並在文件之間與行程執行之間重用它們,所以一個用相同三套字型算繪一萬份對帳單的批次,只會為這三套字型各做一次子集化,而不是做一萬次。這個快取用兩個屬性設定、用一筆記錄檢視,而且可以放心常駐開啟:快取失敗會退回一般的記憶體內子集化,絕不會讓文件產不出來
子集化之所以昂貴,是有理由的。建構一個子集意味著走訪字形閉包、改寫 loca 與 glyf、重建 cmap 與 hmtx,並發出 PDF 能定址的 CID 對映。對單一文件,這份成本淹沒在雜訊裡。對一臺在迴圈裡產生文件的報表伺服器,它往往是整次執行中最大的一塊 CPU 時間
什麼讓快取命中成為可能
四件事必須相符:字型內容、已使用的字形集合、子集模式,以及快取綱要。任何一項不符,HotPDF 就從頭做子集化,因為一個子集只有在它本來就會位元組完全相同時,才得以重用
字形集合是會讓人意外的條件。兩份只差一個客戶名稱的發票,使用不同的字形集合,因此產生不同的子集與不同的快取項目。當文件共用同一份字形庫時——來自固定範本的對帳單、變數資料為數字的表單、從單一產品資料庫繪製的目錄——快取會有回報;而當每份文件各畫一大套 CJK 字型的不同切片時,快取一無所獲。先測量,再假設自己屬於哪一種情況
var
Pdf: THotPDF;
Info: THPDFFontSubsetCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.EnableFontSubsetting := True;
Pdf.FontSubsetCacheFolder := 'C:\ProgramData\Reports\fontcache';
Pdf.FontSubsetCacheMaxBytes := 64 * 1024 * 1024; // 64 MiB, default is 256
// ... generate the batch ...
Info := Pdf.GetFontSubsetCacheInfo;
LogFmt('subset cache: %d hits, %d misses, %d bytes in %d files',
[Info.HitCount, Info.MissCount, Info.CurrentBytes, Info.FileCount]);
finally
Pdf.Free;
end;
end;
你怎麼知道快取真的有作用?
GetFontSubsetCacheInfo 回傳九個計數器,而前兩個之間的比例直接回答了這個問題。HitCount 與 MissCount 給出命中率。WriteCount 與 EvictionCount 顯示項目是否活得夠久而被重用,還是正被一個過小的預算擠出去。CurrentBytes 與 FileCount 回報此刻磁碟上有什麼
剩下三個才是值得設警示的。CorruptCount 計數未通過驗證而被移除的項目——不正常關機後出現幾個是正常的,但穩定地流出代表儲存不可靠。RejectedCount 計數在使用前被拒絕的項目。WriteFailureCount 計數根本無法寫入的項目,這通常代表資料夾的權限問題,而非任何與字型有關的事。這三者都不會阻止文件產生,而這正是你必須主動去看的原因:一個悄悄從不寫入的快取,從外部看起來與一個運作正常的快取完全相同,除了 CPU 帳單之外
驅逐、預算,以及你縮減預算的那一刻
FontSubsetCacheMaxBytes 預設為 268435456 位元組,即 256 MiB,而且可以在執行時調降。調降會立即觸發最近最少使用的驅逐,而不是等到下一次寫入,所以一個對磁碟壓力做出反應的服務,能在它決定的那一刻釋放空間,而不是在某個它無法掌控的稍後時刻
把 FontSubsetCacheFolder 設為空字串,會停用磁碟層而不清除任何已儲存的內容,也不改變任何一位元組的字型輸出。當你想在排障時孤立快取,這就是你該伸手拿的屬性:關掉它、跑同一個批次、比較產出的 PDF。它們應該完全相同,因為快取存放的是結果,不是政策
當某個項目受損時,快取會怎麼做
它會移除該項目,並正常做子集化。格式錯誤或被截斷的項目會在子集有機會進入 PDF 串流之前被拒絕,而這正是整個設計中最重要的部分:一個受損的快取項目若混進文件,會產生一份帶著損壞字型程式的 PDF,而那次失敗會在遠離其成因之處浮現——在一個閱讀器裡、在一部客戶的機器上、幾週之後
寫入是原子的,所以讀者永遠不會觀察到寫到一半的項目,而寫入中途的當機會讓快取保持一致而非被毒化。精簡子集項目會保留 PDF/A 字型字典所需的 CID 重對映資料,所以快取子集仍然是一個符合規範的子集——封存輸出不必為了保持有效而繞過快取
// Reset the disk tier after a font upgrade or a schema change
Pdf.ClearFontSubsetCache;
// Or move it somewhere writable and let the budget apply immediately
Pdf.SetFontSubsetCacheFolder('D:\cache\fonts');
在實際部署中,資料夾該放在哪裡
三個性質決定了這件事:資料夾必須能被服務執行時所用的帳戶寫入、它應該位於本機儲存而非網路共用,而且它不該位於一個會被部署步驟清空的目錄內。一個放在共用上的快取,會把每次未命中變成一次往返,並把每次命中變成兩次;一個位於安裝程式會重新建立之應用程式資料夾下的快取,則是一個每次更新後都從冷啟動開始的快取
對多實例服務而言,除非你已確認儲存能如你預期般處理並行的原子替換,否則請給每個實例各自專屬的資料夾。一個重複項目的代價,是多一次子集化;而除錯一個共用快取競態的代價,是一整個下午
什麼時候該改用別的東西
快取減少的是重複的工作。它不會減少第一份文件的工作,也幫不上字形集合從不重複的工作負載。如果你的輸出由一套用在無法預測文字上的龐大 CJK 字型所主宰,更有效的槓桿是子集化閉包本身——拉進了哪些字形、為什麼——相關內容涵蓋於 字型子集閉包與字形塑造的筆記。如果你的批次很慢、而原因其實與字型無關,帶字型與圖片的報表輸出逐步解說展示了其他時間通常花在哪裡,而 EndDoc 字型子集排序臭蟲的案例研究則提醒我們,子集化的正確性與子集化的速度是兩個分開的問題
HotPDF 是一套支援 Delphi 與 C++Builder 的原生 VCL PDF 元件,而子集快取是函式庫的一部分,而非附加服務,所以報表伺服器只要設定一個資料夾路徑就能擁有它——完整的字型與效能功能清單請見 HotPDF 元件頁