技術記事

Delphiでfontsub.dllによるTrueTypeフォントサブセット化

DelphiおよびC++Builder用ExcelコンポーネントであるHotXLSは、TrueTypeフォントのサブセット化を通じて埋め込みPDFフォントのサイズを削減する:PDFエクスポート時に、書体ファイル全体を出荷するのではなく、Windowsのシステムライブラリfontsub.dllのCreateFontPackage関数を呼び出し、ワークシートが実際に使用したUnicodeコードポイントだけを中心に埋め込みTrueTypeフォントを再構築する。中国語の商品名が200行あるレポートは、おそらく数百個の異なる漢字しか必要としないが、Windowsが同梱するCJKフォントは1つあたり日常的に5〜20MBもある。それをまるごと1つ埋め込めば、そのフォントだけでPDF内の他のすべてのオブジェクトを合わせたよりも重くなり得る

fontsub.dllは、ほとんどのDelphi開発者が耳にしたことのないライブラリであり、それには理由がある:マイクロソフトはこれを、目立つWin32 APIとしてではなく、小さくほとんど文書化されていないユーティリティDLLとして出荷している。HotXLSはこれを必須の依存関係ではなくオプションの機能として扱う。そのため、エクスポータがそれをどうロードし、どう呼び出し、それが存在しないときにどうフォールバックするかは、フォント形式そのものと同じくらい防御的なWindowsプログラミングについて多くを物語っており、その両方を一通り見る価値がある

なぜUnicodeテキストはHotXLSのPDFエクスポートを膨張させるのか

HotXLSのPDFエクスポータは、ワークシートのテキストがWinAnsiの範囲外に出たときにのみ埋め込みTrueTypeフォントに手を伸ばし、それ以外の時は組み込みのHelveticaファミリーにとどまる。これはワークシートからPDFへのエクスポートの解説で詳しく扱われている既定の経路である。WinAnsiは西欧のテキストを十分にカバーするため、多くのワークブックはそもそもフォント埋め込みを一切引き起こさない:PDFは単にHelveticaを名前で参照し、リーダーがそれをローカルに供給するため、ファイルは小さいままである。あるセルがWinAnsiでは表現できない何か——中国語の商品名、韓国語のメモ、コメント内の紛れ込んだ記号——を保持した瞬間、エクスポータは実際のフォントプログラムを埋め込まなければならなくなる。なぜならPDFリーダーには、標準14フォントの範囲外の文字に対するフォールバックのグリフソースが存在しないからだ

HotXLSはそのフォントを自動的に見つける。エクスポータのUnicodeFontFileプロパティがすでに特定のファイルを指していない限り、中国語と韓国語のレンダリング用にWindowsが同梱するCJK対応の書体を含む、インストール済みの候補の短いリストについてWindowsのFontsフォルダをスキャンする。そして最終的にどのフォントに落ち着こうとも、サブセット化が実行される前にそれはまるごと埋め込まれる。この埋め込み要件はPDFに特有のものである:HotXLSのRTFおよびHTMLエクスポート経路は、フォントプログラムを出荷するのではなくコードポイントをバイトストリームにエスケープすることでUnicodeテキストをそのまま保つ。これが本稿で扱うサイズの問題がこの2つの形式には存在しない理由である

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コードポイントのリストを渡すと、あらゆるフォント形式の制約を依然として満たす最小限のフォントを返してくれる:グリフインデックスは振り直され、glyflocaは保持されたアウトラインだけを中心に再構築され、hmtxcmapはそれに合わせて書き換えられる。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テーブル内の埋め込み許可ビットを尊重し、顧客のマシンにたまたまインストールされているどんな風変わりなフォントに対してもすべてを正しく行う必要がある。マイクロソフトはすでにその問題を解決し、その解決策をWindows自体の一部として出荷している。そのため、マイクロソフトが保守し、自社のフォントレンダリングスタックに対してテストし、すべてのマシンに無料で配布しているシステムDLLを呼び出すコストは、動的ロード1つと関数ポインタ1つで済む。同じロジックを再実装するということは、フォントが大きい場合にのみ重要になる機能のために、数十年分のエッジケースを持つバイナリ形式のパーサーを自前で持つことを意味する

実際にレンダリングされたグリフから保持リストを構築する

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はその同じマップを2度目に歩き、CreateFontPackageが期待する保持リスト、すなわちAPIの保持リスト引数が要求する16ビット形式で保持すべきUnicodeコードポイントの単純な配列を構築する。この引数は16ビットワードの配列であるため、基本多言語面をきれいにアドレスできる。これは通常のCJK、キリル文字、ギリシャ文字、アラビア語のテキストを複雑さなしにカバーする。補助面の文字、特定の絵文字、あるいは稀な歴史的文字を多用するワークシートは、単一の保持リストエントリが直接指名できるものの外側に位置する。これは欠陥というよりは、知っておく価値のある境界である。なぜなら、Unicodeを多用する業務用スプレッドシートの大多数は、そもそもその面には一切近づかないからだ

fontsub.dllが存在しない場合はどうなるか

HotXLSはfontsub.dllが存在すると決して仮定せず、PDFエクスポートがそれが存在しないために失敗することは決してない。このライブラリは、サブセットが必要になった瞬間に、静的なインポートではなくSafeLoadLibraryGetProcAddressで動的にロードされる。これはまさに、fontsub.dllがkernel32.dllのような文書化され存在が保証された公開APIではないからだ:それは同梱されたフォント埋め込みツールであり、マイクロソフトの契約は、それがあらゆる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を、サブセット化されていないサイズの20分の1から8分の1程度、すなわち8倍から20倍の削減へと縮小する。その規模は、ある文書が実際にフルフォントのうちどれだけに触れるかに追随する:数百個の異なる漢字を中心に組まれた注文書は、CJK書体が出荷する数万個のうちその数百個のグリフだけを保持し、より広い文字の組み合わせにまたがるシートは、それに比例してより多くを保持する。HotXLSはサブセットフォントのバイト列にさらにFlate圧縮のパスを重ね、それをPDFの/FontFile2ストリームに書き込む前にかける。これは文書の他のコンテンツストリームがすでに経ているのと同じ圧縮であり、呼び出し側のコードには何一つ追加を求めない:WinAnsiを一度も離れないワークシートはこの経路に一切触れず、通常のHelveticaでのエクスポートを続け、Unicodeフォント経路を実際に引き起こすワークシートは、設定すべきプロパティも別途呼び出すべき呼び出しもなく自動的にサブセット化を受ける。ここで関わる唯一のプロパティであるUnicodeFontFileは、どのフォントが埋め込まれサブセット化されるかを選ぶだけであり、サブセット化が行われるかどうかを選ぶものではない

フォントサブセット化は、ページ割り、ワークシートの印刷メタデータ、そして同梱されているCSV・HTML・RTFエクスポート経路とともに、HotXLS Delphi Excelコンポーネントのより広いPDFエクスポート面の中の一つの詳細である