HotPDF は CFF2 アウトラインを持つ OpenType 可変フォントを、選択された軸値での状態に変換し、PDF リーダーがすでに理解できる CID キー方式の CFF1 フォントプログラムとして埋め込みます。RegisterUnicodeTTF で登録された通常のネイティブテキストはフォント自身の cmap と hmtx テーブルを使い、ライタは変換済みプログラムを /FontFile3 配下に /Subtype /CIDFontType0C で埋め込みます。すべての CID は元のグリフ ID と等しく保たれます
素朴なルートの説明にも価値があります。誰にも告げずに失敗するからです。CFF2 テーブルを解析してキャッシュしたうえで、プレーンテキスト経路にフォント解決を任せると、TrueType 限定のマッチで Windows GDI を通ります。GDI は CFF2 アウトラインの扱いを知らないため、黙って ArialUnicodeMS へ置き換えます。置換フォントを読む下流の処理は、間違ったアウトラインと幅、補助平面文字の間違った抽出を得ます。PDF は普通に開きます。ただ、頼んだフォントで組まれていないだけ。だからこそテキスト経路は、GDI ルックアップではなく登録済みバイトから動く必要があります
なぜ PDF は CFF2 フォントを直接埋め込めないのか
PDF には CFF2 プログラムの受け皿がありません。ISO 32000-2 §9.9 はリーダーが対応すべき埋め込みフォントプログラムの型を列挙します。/FontFile に Type 1、/FontFile2 に TrueType、そして /FontFile3 に素の CFF プログラム(Type1C か CIDFontType0C)、または TrueType か CFF アウトラインをくるんだ OpenType ラッパーです。この一覧にあるすべての CFF 系は CFF バージョン 1、つまりリーダーが 20 年間解析してきた形式を意味します
CFF2 は名前を共有しているだけで、別の形式です。Name と String の INDEX を捨て、charset を捨て、グリフの送り幅を CharStrings から取り上げて hmtx に委ね、blend 演算子を加えて 1 本のアウトラインで設計空間全体を語れるようにしたものです。CFF2 テーブルに遭遇した CFF1 パーサには推測する根拠がなく、試みる義務を PDF 仕様のどこにも負いません。CFF2 フォントを OpenType ストリームにくるんで願うというのは、あるマシンでは描けて次では失敗する種類のファイルそのものです。HotPDF はそれをしません
変換は静的でなければなりません。可変フォントの静的インスタンス化に関する記事が説明する通り、PDF のフォントは固定メトリクスを持つ固定アウトライン集合であり、軸座標の適用をリーダーに要求することはできません。HotPDF はまず可変性を解決し、その後に CFF1 を書きます
CFF2 フォントでネイティブテキストを書く
アプリケーションコードから見れば、CFF2 フォントは登録済み Unicode フォントの他の仲間と同じように振る舞います。軸値を選び、ファイルを登録し、ファミリー名で選択して描くだけです。RegisterUnicodeTTF は元の name テーブルの複製を保持するため、SetFont はファミリー名、フルネーム、PostScript 名、タイポグラフィファミリー名のいずれかでマッチし、凍結された静的フォントバイトを選択します。Windows へのインストールも GDI ルックアップもありません
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'brochure.pdf';
Pdf.BeginDoc;
// 軸値はこの呼び出し以降に登録するフォントへ適用される
Pdf.SetVariableFontAxis('wght', 650);
Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
// name テーブルのファミリー、フル、PostScript、タイポグラフィファミリー名のいずれか
Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
グリフ ID は登録済み cmap から、送り幅は登録済み hmtx から来ます。送り幅が本当にゼロのグリフは、GDI 測定へのフォールバックをせずゼロのままであり、UTF-16 のサロゲートペアは送り幅を正確に 1 つだけ生みます。BMP 文字、補助平面文字、合成シェーピング用の運搬文字はフォントごとの Unicode マッピングを保つため、別の埋め込みフォントへの切り替えや新ページの開始が、別フォントにこのフォントのサブセット状態を更新させたり、グリフ ID を借りさせたりすることはありません
測定も同じバイトに従います。THPDFPage.GetWideTextWidth と THPDFPage.GetWideCharAdvances は選択されたバイト裏付けフォントのメトリクスを読むため、改行位置と右揃えは描かれるものと一致します。人が引っかかるのは順序の細部です。登録後の SetVariableFontAxis 変更は次に登録するフォントに効くのであって、すでに凍結されたインスタンスには効きません。2 つ目のウェイトが欲しければ、新しい値を設定して再登録します
HPDFConvertCFF2ToCFF はどう CID キー方式のプログラムを組み立てるのか
HPDFConvertCFF2ToCFF は、単一の Font DICT を持つ素の CID キー方式 CFF1 プログラムを生み出します。空のスロットや未使用のスロットを含め、すべてのスロットで CID が元のグリフ ID と等しくなるようにです。この一致こそ設計の核心です。フォント名、PDF の /W 幅、CFF の送り幅、ToUnicode CMap が同じ番号付けを参照するため、グリフ ID と CID の間を翻訳するレイヤーは不要で、ずれの起きようもありません
PDF 側では子孫フォントは Identity-H か Identity-V エンコーディングの CIDFontType0 で、プログラムストリームは /FontFile3 配下に /Subtype /CIDFontType0C で置かれます。CID がすでに GID と等しいため、子孫は /CIDToGIDMap を持たず、このストリームは登録 TrueType フォント用の後段のストリーム置換フックから除外されます。/Subtype /OpenType ラッパーと違い、変換済みストリームは PDF 1.6 バージョン要件を引き上げません
幅には小さな工夫が要りました。CFF1 の Type 2 CharString は送り幅を、nominalWidthX からの相対値という任意の最初のオペランドとして格納し、整数オペランドは符号付き 16 ビット範囲に限られます。一方 OpenType の送り幅は符号なし 16 ビットで、最大 65535 です。HotPDF は nominalWidthX を 32768 に、defaultWidthX を 0 に設定し、すべての送り幅を advance - 32768 として明示的に書きます。ゼロの送り幅は -32768 になり、65535 の送り幅は 32767 になります。1 つもグリフをクランプせずに uint16 全域を表現できるのです
幾何も同じ丁寧さで扱います。FontMatrix は [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0] なので、2048 ユニットのフォントはネイティブ座標を保ちます。1 オペランドに収まらない相対ムーブやラインは表現可能なステップへ分割され、3 次曲線は形状を保つ形で細分化します。元の著作権とライセンス記録、元の name テーブル、埋め込みフラグは CFF の Notice 文字列に乗って移動します
文書の外側でコンバータを呼ぶ
コンバータは HPDFOpenTypeAdvanced ユニットの公開関数で、Pascal で書かれ、HarfBuzz 依存もシステムフォントのインストール手順もありません。単体で使えるため、たとえばビルド時にフォントを前もって変換したり、PDF ライタが何を埋め込むかを検査したりできます
uses
System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;
procedure ConvertWeight650;
var
Source, Cff: TBytes;
Axes: THPDFVariableFontAxisValueArray;
begin
Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
SetLength(Axes, 1);
Axes[0].Tag := 'wght';
Axes[0].Value := 650;
Cff := HPDFConvertCFF2ToCFF(Source, Axes);
if Length(Cff) = 0 then
raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;
可変フォントの入力は、指定された座標での幾何のみの静的インスタンス化を通ります。アウトラインと送り幅の両方をカバーします。静的な CFF2 フォントは空の選択しか受け付けず、軸値を渡すのは無視ではなくエラーとして扱われます。結果は sfnt コンテナではなく素の CFF1 プログラムです。OpenType ファイルを期待するコードへ渡さないでください
想定内のケースでは、失敗は例外ではなく空の TBytes です。入力が CFF2 でない、インスタンサが受け付けない軸選択、NaN のような非有限値、そして OS/2 の fsType が制限付きライセンス埋め込み(0x0002)やビットマップ限定埋め込み(0x0200)を設定しているフォントです。必ず長さを検査してください。EndDoc の内部ではライタがそれを代行し、"StoreFont: CFF2 outline conversion failed" を投げます。これが正しい判断です。出荷された PDF に入る真っ白なフォントプログラムは、ログに見えるエラーよりはるかに悪いのです
サーバーワークロード向けの予算とキャンセル
省略可能な第 3 パラメータ THPDFVariableFontControl はメモリを制限し、長い変換をキャンセルできるようにします。省略時の既定は、フォントの入出力それぞれ 128 MiB の上限、通算 1 GiB の作業予算、CharString あたり 65535 バイトの CFF1 上限です
type
TFontJob = class
private
FCancelled: Boolean;
procedure CheckCancel;
public
function Convert(const Source: TBytes): TBytes;
property Cancelled: Boolean read FCancelled write FCancelled;
end;
procedure TFontJob.CheckCancel;
begin
if FCancelled then
Abort; // HPDFConvertCFF2ToCFF の外へ伝播する
end;
function TFontJob.Convert(const Source: TBytes): TBytes;
var
Control: THPDFVariableFontControl;
begin
Control := THPDFVariableFontControl.Create(
32 * 1024 * 1024, // 出力の最大バイト数
Int64(256) * 1024 * 1024, // 通算の作業予算
CheckCancel);
try
// nil 選択:可変フォントの既定インスタンス、または静的な CFF2 フォント
Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
finally
Control.Free;
end;
end;
予算超過とキャンセルは例外として報告され、空結果の経路とは意図的に区別されます。空配列はそのフォントが変換不能であること、例外はこの試みが止められたことを意味します。エラーハンドリングでこの 2 つを混ぜないでください。変換不能なフォントに予算を増やしてリトライしても、うまくいきません
埋め込んだ CFF2 フォントはどれくらい重いのか
計画に入れるべきはまずファイルサイズです。HotPDF は CFF2 フォントの変換済みグリフ集合全体を、EnableFontSubsetting のオンオフに関係なく埋め込みます。数千グリフのフォントは、ページが 1 行の見出しだけ表示していても、使う文書すべてに全グリフを乗せます。パンフレットなら許容範囲です。数千の小さな PDF を書き出すレポートサーバーでは、コミット前に実測すべき現実のコストです
関連するサイズ機能も同じ規則に従います。変換済み CFF2 フォントは丸ごと埋め込まれるため、CompressDocument が有効にするコンパクトなフォントサブセットも永続的なフォントサブセットキャッシュも適用対象外です。同じ文書内の TrueType フォントは従来通りサブセット化とキャッシュの対象です。全グリフを保つ唯一の利点は、サブセット化を禁じるライセンスのフォントが、それを尊重する形で埋め込まれることです
リソース上限は変換の有限上限であり、CFF1 自体が 1 つの CharString の容量に上限を設けています。変換後の CharString が 65535 バイトを超えるグリフや、作業予算を使い切るフォントは、空のアウトラインで書かれる代わりに見える形で失敗します。これが意図したトレードオフです。珍しいフォントでの大きな失敗は、黙って欠けたグリフが並ぶ文書に勝ります
ネイティブな CFF2 テキストと移植性のある CFF1 コンバータは、Delphi と C++Builder 向けの HotPDF Delphi PDF component に同梱されています。可変フォントのインスタンス化、カラーグリフ、フォントサブセット化の各機能も、このブログの他の記事で扱っています