HotXLS 是 Delphi 和 C++Builder 的 Excel 元件,可透過 TrueType 字型子集化縮小嵌入 PDF 的字型體积:匯出 PDF 時,它呼叫 Windows 係統庫 fontsub.dll 的 CreateFontPackage 函式,根據工作表實際使用的 Unicode 碼點重建立嵌入的 TrueType 字型,而不是攜帶完整字型檔案。一份包含兩百行中文產品名稱的報表可能只需要几百個不同的汉字,但 Windows 隨附的 CJK 字型通常每個就有 5 到 20 MB。完整嵌入一個字型後,單是字型大小就可能超過 PDF 中其他所有對象的總和
大多數 Delphi 開發者從未听說過 fontsub.dll,這並非偶然:Microsoft 將它作為體积小、檔案稀少的工具 DLL 隨係統提供,而不是广為人知的 Win32 API。HotXLS 將它視為可選能力而非硬依賴,因此匯出器如何加載和呼叫它,以及缺少它時如何回退,既體現了防御式 Windows 编程,也體現了對字型格式的處理,這兩方面都值得展開說明
Unicode 文字為什麼會讓 HotXLS 匯出的 PDF 迅速膨脹
只有當工作表文字超出 WinAnsi 範圍時,HotXLS 的 PDF 匯出器才會使用嵌入的 TrueType 字型,其余情況會繼續使用內置的 Helvetica 字型係列,這正是工作表匯出為 PDF 的操作說明詳細介绍的預設路徑。WinAnsi 對西欧文字的覆盖已經足夠好,因此許多活頁簿根本不會觸發字型嵌入:PDF 只需按名稱引用 Helvetica,由閱讀器在本地提供字型,檔案自然維持较小。一旦儲存格包含 WinAnsi 無法表示的內容,例如中文產品名稱、韩文備註或註釋中的特殊符號,匯出器就必須嵌入實際字型程序,因為 PDF 閱讀器無法為標準 14 種字型之外的字元提供備用字形
HotXLS 會自動定位該字型,掃描 Windows Fonts 檔案夹中的一小組已安装候選字型,其中包括 Windows 為中文和韩文顯示提供的 CJK 字型,刪除非匯出器的 UnicodeFontFile 屬性已經指向特定檔案。無论找到哪種字型,都會先完整嵌入,再執行子集化。這項嵌入要求是 PDF 特有的:HotXLS 的RTF 和 HTML 匯出路徑透過將碼點轉義到位元組流中來維持 Unicode 文字,而不是攜帶字型程序,因此本文讨论的體积問題在這兩種格式中沒有對應情況
uses
lxHandle, lxPDF;
var
Book: TXLSWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSWorkbook.Create;
try
Book.Open('catalog-cn.xlsx');
Exporter := TXLSPDFExport.Create;
try
// Optional: pin a specific CJK-capable font instead of the
// exporter's automatic Windows\Fonts scan.
Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
finally
Exporter.Free;
end;
finally
Book.Free;
end;
end;
fontsub.dll 是什麼,為什麼不從頭編寫子集化器
fontsub.dll 是一個隨 Windows XP 起提供的 Windows 係統庫,在這里相關的函式只有一個:CreateFontPackage。將源 TrueType 字型的位元組和要保留的 Unicode 碼點列表交給它,它就會回傳一個仍滿足所有字型格式約束的最小字型:重新编號字形索引,只圍绕保留的轮廓重建立 glyf 和 loca,並相應重寫 hmtx 與 cmap。HotXLS 直接依據這一約定聲明函式指针類型
const
TTFCFP_FLAGS_SUBSET = 1;
TTFMFP_SUBSET = 0;
TTFCFP_MS_PLATFORMID = 3;
TTFCFP_UNICODE_CHAR_SET = 1;
type
TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;
如果不呼叫 CreateFontPackage 而手工實作它的工作,就必須編寫一個正確的 TrueType 子集化器:遍歷複合字形以補入保留字形引用的所有元件字形,在丢弃轮廓後重建立 loca 偏移,遵守字型 OS/2 表中的嵌入許可位,並讓這些逻辑在客户机器安装的各種特殊字型上都正確執行。Microsoft 已經解决了這個問題,並將解决方案作為 Windows 的一部分隨係統提供,因此呼叫由 Microsoft 維護、會针對其字型渲染堆疊測試並免费分發到每台机器的係統 DLL,只需讓 HotXLS 负责動態加載和函式指针;重新實作同样的逻辑,则意味著要為一個拥有數十年邊界情況的二進製格式維護解析器,而這個功能只在字型碰巧很大時才有价值
根據實際渲染的字形建立保留清單
HotXLS 從一個原本就因其他用途而維護的映射中建立子集化保留清單,因此無需额外的統計成本。每當頁面渲染代碼繪製需要嵌入 Unicode 字型的字元時,它都會尋找該字元的字形索引,並在 FUnicodeGlyphMap 中記錄這組對應關係。這個字形到碼點的表還會驅動 PDF 的 ToUnicode CMap,使成品檔案中的複製貼上回傳原始文字,而不是原始字形 ID。頁面內容流完成時,該映射已經準確列出檔案使用過的 Unicode 碼點集合,不多不少
var
keepList: array of Word;
keepCount, i: Integer;
codePoint: LongWord;
begin
SetLength(keepList, FUnicodeGlyphMap.Count);
keepCount := 0;
for i := 0 to FUnicodeGlyphMap.Count - 1 do
begin
codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
if codePoint > 0 then
begin
keepList[keepCount] := Word(codePoint);
Inc(keepCount);
end;
end;
end;
在最终處理阶段,HotXLS 會再次遍歷同一映射,建立 CreateFontPackage 所需的保留清單,也就是以該 API 的保留清單參數要求的 16 位形式儲存要保留 Unicode 碼點的普通陣列。由於該參數是 16 位字陣列,它可以直接處理基本多文種平面,其中包括普通 CJK、西里尔文、希腊文和阿拉伯文;如果工作表大量使用補充平面字元、某些表情符號或罕見歷史文字,就超出了單個保留清單項可以直接命名的範圍。了解這一邊界很重要,但它並不是缺陷,因為绝大多數 Unicode 使用量较高的業務試算表根本不會涉及該平面
缺少 fontsub.dll 時會發生什麼
HotXLS 從不假設 fontsub.dll 一定存在,PDF 匯出也不會因為缺少它而失敗。只有在確實需要子集化時,庫才會使用 SafeLoadLibrary 和 GetProcAddress 動態加載它,而不是静態匯入,原因正是 fontsub.dll 不像 kernel32.dll 那样屬於有檔案且保證存在的公共 API:它是隨係統提供的字型嵌入工具,Microsoft 的約定並未承诺它會在每個 SKU、每個服務分支或尝試模拟 Windows 的每個相容層中持續存在
var
hFontSub: HMODULE;
CreateFontPackage: TCreateFontPackage;
begin
hFontSub := SafeLoadLibrary('FontSub.dll');
if hFontSub = 0 then
Exit; // no subsetting available - keep the full embedded font
try
@CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
if not Assigned(CreateFontPackage) then
Exit;
// ... call CreateFontPackage, check its return code ...
finally
FreeLibrary(hFontSub);
end;
end;
所有失敗路徑都會回到同一個結果。DLL 缺失、匯出函式缺失、回傳碼非零,或者字型的 OS/2 表透過嵌入許可位禁止子集化時,HotXLS 都會保留已經嵌入的完整字型並繼續執行。不會抛出異常,不會中止匯出,呼叫代碼也不需要為字型優化额外編寫異常處理;無论哪種情況,匯出的 PDF 都有效,唯一的區別只是檔案最终较小還是稍大
PDF 實際能縮小多少
HotXLS 的 TrueType 字型子集化通常可以將 Unicode 使用量较高的工作表匯出的 PDF 縮小到未子集化大小的二十分之一至八分之一,相當於縮小 8 到 20 倍,具體幅度取决於檔案實際使用了完整字型的多少內容:一份只包含几百個不同中文字元的采购订單,只需從 CJK 字型提供的數万個字形中保留這几百個,而覆盖更广字元範圍的工作表则會按比例保留更多字形。HotXLS 還會在將子集字型位元組寫入 PDF 的 /FontFile2 流之前再執行一次 Flate 壓縮,檔案其他內容流也使用同样的壓縮方式,而且這一切都不要求呼叫代碼做额外處理:從未超出 WinAnsi 的工作表不會進入這條路徑,繼續使用普通 Helvetica 匯出;觸發 Unicode 字型路徑的工作表會自動完成子集化,不需要設定屬性,也不需要單獨呼叫,而涉及的唯一屬性 UnicodeFontFile 只決定嵌入並進行子集化的字型,不決定是否進行子集化
字型子集化只是HotXLS Delphi Excel 元件完整 PDF 匯出能力中的一個細節,該能力還包括分頁、工作表打印元資料,以及隨產品提供的 CSV、HTML 和 RTF 匯出路徑