技術記事

PDF内のWebフォント:DelphiにおけるWOFF2と可変軸

PDF Library for Delphiは、Webプロジェクトがすでに使っているフォントをそのまま受け入れる。WOFF2コンテナをインポートし、可変フォントのデザイン軸をJSONとして列挙し、選択した軸座標の組を、バリエーションについて何も知らなくても通常の埋め込み・サブセット化・抽出経路が扱える静的フォントへインスタンス化する。TrueTypeアウトラインとCFF2アウトラインの両方をカバーする

これが解決する状況はよくあることで、しかも苛立たしい。ブランドのタイポグラフィはデザインシステムで定義され、そのデザインシステムは可変WOFF2ファイルを提供するが、PDFジェネレーターはコンテナもバリエーションデータもどちらも読めない。よくある回避策——実際に使われているすべてのウェイトの静的TTFインスタンスをフォント制作元に依頼する——は数週間かかり、しかも誰も同期を保たないファイルを生み出す

WOFF2が単なる圧縮フォントではない理由

WOFF2をBrotliラッパーとして扱い展開しても、得られるバイト列は有効なsfntではない。このフォーマットはさらに、変換され並び替えられたテーブルも許容する:glyf、loca、hmtxは並び替えられた形式で保存されることがあり、フォントを解析可能にする前にそれを元に戻さなければならない。汎用の展開ツールにはそれができない

Windowsでは、Windows 10バージョン1703以降、プラットフォームがコンテナの解析と展開を提供しているため、このライブラリは別のデコーダーを同梱するのではなくシステム実装を使用する。この判断により、デプロイに余計なDLLが加わらずに済み、変換の巻き戻しはオペレーティングシステム自身のテキストスタックが使うのと同じコードによって処理される。他のプラットフォームでは、WOFF1は今も純粋なPascalの経路をたどり、WOFF2は明示的にサポート外として報告される。これは静かに壊れたフォントを返すのではなく正直な回答である

インポート経路はデコードの前後で検証を行う:48バイトのヘッダー、コンテナの長さ、予約フィールド、宣言されたsfntサイズ、512 MiBの上限がすべて最初にチェックされ、デコード後のストリームサイズは事後に宣言値と比較される。Webフォントはダウンロードされたコンテンツであり、ブロックする価値のある2つの失敗モードはサイズの不一致と展開爆弾である

このパイプライン図は、Delphi PDFライブラリがWOFF2 Webフォントを、ヘッダー検証・システムによるデコード・sfnt変換の巻き戻し・デコード後のサイズ監査を通じてインポートする様子を示している
WOFF2コンテナはダウンロードされたコンテンツとして届くため、インポート処理はデコードの前後でヘッダーと宣言されたサイズを検証する
uses
  PDFlibrary;

var
  Lib: TPDFlib;
  FontID, InstanceID: Integer;
  AxesJSON: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    // WOFF2、WOFF1、TTF、OTF、TTCはすべて同じcallから入る
    FontID := Lib.AddOpenTypeFontFromFile('Inter.woff2', 0);
    if FontID <= 0 then
      raise Exception.Create('font could not be imported');

    // このfontには何ができるか
    if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
      Log(AxesJSON);   // 各axisのtag、minimum、default、maximum

    // 1つのdesign locationをstatic fontに焼き込む
    InstanceID := Lib.CreateOpenTypeFontVariation(FontID,
      'wght=620,wdth=87.5');

    Lib.DrawOpenTypeText(56, 760, InstanceID, 24, 'Quarterly results', 0);
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

インスタンス化が再構築しなければならないもの、そして取り除くもの

可変フォントは、グリフごとに1つの既定アウトラインと、デザイン空間内の位置でインデックス付けられたデルタを保存する。静的インスタンスを生成するとは、要求された座標でそれらのデルタを補間し、点を移動させ、それらから導出されるすべてを再計算することを意味する

TrueTypeアウトラインについては、fvar、avar、gvar、HVAR、MVARを解析し、共有と個別の点番号付けを持つタプルデルタを適用し、明示的なデルタを持たない点を補間することを意味する。水平方向のファントムポイントはHVARとともに送り幅と左サイドベアリングを決定する。複合グリフはまずコンポーネントの依存関係と点のマッピングを解決してから、平行移動・拡大縮小・行列・アタッチメントオフセットを適用する必要がある——深さの上限と循環検出を伴って行われる。悪意あるコンポーネントグラフがあれば、そうでなければ無限に再帰してしまうからである

メタデータも移動し、これはほとんどの実装が省略する部分である。MVARはhhea、OS/2、post内のグローバルメトリクスを更新するため、行間、下線位置、イタリック角度はデフォルトではなくそのインスタンスに一致する。wght、wdth、ital、slntの各軸はさらにウェイトクラス、幅クラス、選択フラグ、macStyle、イタリック角度を更新するため、静的フォントを検査するツールは要求したスタイルを報告する。ジオメトリはボールドなのにメタデータではレギュラーと記載されているフォントは、下流のあらゆる箇所で誤ってラベル付けされてしまう

その後、出力は複合アウトラインをフラット化し、もはや適用されないテーブル——バリエーションテーブル、ヒント、デバイステーブル、そして署名——を取り除く。グリフの境界、hheaのサマリーメトリクス、maxpのヒント上限、テーブルごとのチェックサム、フォント全体のチェックサムがすべて再計算される

この図は、可変フォントのインスタンス化がgvarデルタの補間、複合グリフの展開、アドバンス幅の再計算、スタイルメタデータの書き換えによってTrueType書体を再構築し、その後バリエーションテーブルを取り除く様子を示している
インスタンス化はデルタが影響したものすべてを再構築し、その後バリエーションの仕組みを取り除くため、通常の埋め込み処理には単なる静的フォントとして見える

CFF2は同じ到達点へ異なる経路をたどる

CFF2はPDFフォントプログラムとして直接埋め込むことができない:そのチャーストリングは幅の情報を持たず、グリフ順序とメトリクスについては周囲のOpenTypeテーブルに依存する。そのためCFF2フォントのインスタンス化は、代わりにCIDキー付きCFF OpenTypeフォントを生成し、これは既存の描画・埋め込み・サブセット化の経路がそのまま扱える

CFF2フォントをロードすると、現在のドキュメント向けにただちに既定座標の静的インスタンスが生成される一方、元のCFF2 sfntはフォントオブジェクト上に保持されるため、軸の列挙と後のインスタンス化は引き続きデザイン空間全体を見ることができる。各グリフはインスタンス化中に個別に脱サブルーチン化され、これによりバリエーションの演算が完全に取り除かれ、既存のサブセット化処理を変更せずに使える。トレードオフは、静的インスタンスが共有サブルーチンを保持したものよりわずかに大きくなり得ることだが、2つ目のサブセット化実装を用意しなくて済むことを考えれば妥当な代償である

信頼する前にインスタンスを検証する

インスタンス化は、例外が一切報告されないような微妙な形で間違うことがある種の機能である。3つのチェックでほぼすべてを捕まえられる

インスタンスをエクスポートしてフォントツールで開くこと。SaveFontToFileは保持されているフォントプログラムを書き出すため、メモリ上のインスタンスはPDFを生成する前にダンプできる。エクスポートされたファイルはきれいに解析でき、バリエーションテーブルを含まず、要求したスタイルのメタデータを報告するはずである

デフォルト以外の位置でのアドバンス幅を比較すること。パイプラインが別の方法で同じインスタンスを生成できるなら、グリフごとにアドバンス幅とカーブの境界を比較するとよい。ここでの参照実装は、313グリフ・2軸のフォントの独立したインスタンス化と、すべてのグリフにわたって最大差分ゼロで一致した

そして生成されたPDFからテキストを抽出し直すこと。埋め込みはできるが抽出可能なテキストを生成しないインスタンスは、ToUnicodeマッピングを失っており、これは目視確認が通ったずっと後になって検索とアクセシビリティに影響する。より広い抽出経路についてはテキスト、画像、フォントの抽出で扱われている

この図は、生成された可変フォントインスタンスに対する3つの検証チェックを示している:エクスポートされたフォントファイルのダンプ、独立したインスタンス化とのグリフアドバンス幅の比較、そしてPDFからのテキストの抽出し直しである
微妙に誤ったインスタンスは例外を一切投げないため、エクスポートされたファイル、グリフのアドバンス幅、抽出されたテキストをそれぞれ順に検証する
// inspectionまたは再利用用にgenerated instanceをdump
Lib.SaveFontToFile('Inter-620-87.ttf');

// 宣言範囲外のcoordinatesを要求するとclampされ、
// fontのlimitsを決めつけずaxis JSONを読む
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}

ブランドフォントパイプラインのための実務上の注意点

座標の組ごとにインスタンスをキャッシュすること。異なる座標の組はそれぞれ異なるフォントプログラムであるため、ウェイト400と620を混在させるドキュメントは、1つの共有書体ではなく2つのサブセットを埋め込むことになる。同じインスタンスを繰り返し生成することは無駄な作業であり、ほぼ同一のウェイトを何十個も生成することは無駄なファイルサイズである

実際に描画に使うインスタンスで測定すること。コンデンス(幅の狭い)インスタンスは既定のものよりアドバンス幅が狭いため、インスタンス化する前に測定したレイアウトは誤った幅を測定してしまっている。これは、本番環境でのみテキストがボックスからはみ出す最も一般的な単一の原因である

ブランドフォントに含まれないグリフのためのフォールバック経路を用意しておくこと。可変のブランドフォントは通常ラテン文字だけをカバーしそれ以外はほとんどカバーしないため、キリル文字の顧客名を含むレポートでは、そうしなければグリフの欠落が表示される。自動フォールバック機構については欠落グリフに対する自動フォントフォールバックで説明されている。そして目標が特定のウェイトではなくファイルサイズの縮小である場合、サブセット化はインスタンス化よりも大きな効果を持つ。これはPDFファイルサイズの最適化とフォントのサブセット化で扱われている

Webフォントのインポート、可変インスタンス化、サブセット化、フォールバックは、Delphi、C++Builder、Free Pascal向けの同じフォントパイプラインの一部である。機能一覧の全体はPDF Library for Delphiページにある