技術記事

DelphiのHotPDFを使用した埋め込みPDFフォントのレンダリング

HotPDF は、システムにフォントを事前にインストールすることなく、Delphiで埋め込みPDFフォントのレンダリングを実行できます。HPDFGlyphRender.pas に実装された埋め込みグリフ描画パイプラインは、PDF自体に保存されているフォントデータを直接パースします。具体的には、FontFile2 にある TrueType の glyf アウトライン、FontFile3 の CFF Type 2 チャーストリング(Charstring)、および Type 3 フォントのグリフコンテンツストリームを読み込み、塗りつぶされたGDIベクターパスとして再現します。この記事では、HotPDFによるPDFページのTBitmapへのレンダリングで扱ったビューア全体の話を一歩掘り下げて、ページ上のテキストがどのように正確な形状に描画されるのか、そのフォント再現性の詳細について解説します。

PDFがテキストではなく豆腐(四角形)で描画される理由

レンダリングされたPDFの出力において、文字が四角形(いわゆる豆腐)になったり、空白になったり、微妙に異なる文字で表示されたりする場合、それは通常、レンダラーがファイル内の埋め込みフォントを使用せず、代わりにオペレーティングシステムのフォントを要求したことを意味します。この問題は常に同じような状況で発生します。ドキュメントを作成した開発環境では完璧に表示されるものの、顧客が何もないサーバー環境や権限の制限されたPCでファイルを開くと、日本語の請求書が四角形で表示されたり、代替された類似フォントによって行の折り返し位置が崩れたりします。フォントはそのPCにインストールされていたのではなく、PDFの内部にのみ存在しており、システムフォントの代替処理で妥協するレンダラーではそれらを処理できません。さらに、サブセットフォント(必要な文字だけを抽出したフォント)は事態を難しくします。サブセットには、そのファイル内だけで割り当てられたプライベートな文字コードで数十個のグリフが保持されていることがあり、インストールされている他のフォントがその割り当てを共有することはありません。

ISO 32000-1 §9.9 では、フォント記述子(Font Descriptor)内の埋め込みフォントデータとして3つのコンテナを定義しています。FontFile はオリジナルの Type 1 データを、FontFile2 は TrueType データを、FontFile3 は素の CFF(Type1C または CIDFontType0C)や OpenType コンテナを保持します。4つ目の形式である ISO 32000-1 §9.6.5 の Type 3 フォントは、バイナリデータを一切含まず、グリフごとに小さなPDFコンテンツストリームが実行時に描画されます。これら3つのコンテナはアウトラインの幾何学の計算方法(2次Bスプライン、3次チャーストリング、任意の描画オペレーター)がそれぞれ異なるため、正確に描画するには、それぞれ個別のインタープリターに加え、アウトラインを扱う前に文字コードを正しいグリフインデックスに変換するエンコーディングレイヤーが必要となります。

HotPDFがTrueTypeのglyfアウトラインをGDIパスに変換する仕組み

HPDFGlyphRender.pasTHPDFEmbeddedTTF は、loca テーブルを読み取って各グリフの記録場所を特定し、glyf 輪郭(Contour)を点単位で走査して、GDIパスを出力します。ここで、TrueTypeの2つの仕様を正しく処理する必要があります。第一に、曲線外(Off-curve)の点が連続している場合は、それらの中間点に曲線上の点が存在すると見なします。また、すべての点が曲線外にある輪郭は、終点と始点の中間点を開始点とします。このルールを無視すると、丸みのある文字が角張ったり崩れたりします。第二に、TrueTypeの曲線は2次のベジェ曲線(Quadratic Bezier)であるのに対し、GDIの PolyBezierTo は3次のベジェ曲線を要求するため、各2次セグメントは直線の集まりに平坦化するのではなく、幾何学的に正確に3次に次数を上げて(Degree Elevation)処理します。

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

次数引き上げは可逆(ロスレス)な処理です。3次曲線は元の2次曲線と同一の軌跡をたどるため、レンダリングされたアウトラインは、標準のビューアが同じテーブルから描画するものと、ズーム倍率に関係なく完全に一致します。残りの処理は配置です。各グリフはフォント単位(通常は1emあたり1000または2048単位のグリッド)で定義されており、レンダラーはパスを塗りつぶす前に、拡大縮小行列、テキスト行列、および現在の変換行列(CTM)を1つの「グリフからデバイスへの変換行列」に合成します。ここの合成順序は極めて重要であり、これら3つの行列の合成順序を誤ると、すべてのグリフが原点に向かって潰れてしまいます。ページ全体が崩れて表示されるバグの真の原因が、行列代数の1行のミスであることは珍しくありません。

CFFフォントを処理するType 2チャーストリングインタープリターの動作

THPDFEmbeddedCFF は、FontFile3 のデータに対して本物の Type 2 チャーストリングインタープリターを提供します。CFF INDEX 構造、Top DICT、および Private DICT をパースし、各チャーストリングを実行してGDIパスセグメントを直接出力します。OpenTypeコンテナ(OTTO)は、事前にアンラップされて素の CFF テーブルが取り出されます。素の CIDFontType0CType1C ストリームはそのまま消費されます。チャーストリングは非常に簡潔なスタック言語であり、バイトストリームとの同期を保つために3つの仕様が重要になります。第一に、任意の幅(width)プレフィックスは、最初のスタック消去オペレーターが追加の引数を1つ多く取ることを意味します。第二に、hintmask オペレーターは、スタックにまだ値が存在する場合に vstemhm を伴い、スキップすべきマスクのバイト数は累積されたステム(Stem)数によって決まります。このステム数を一度でも数え間違えると、それ以降のすべてのオペコードが誤読されます。第三に、サブルーチンの呼び出し(Subroutine Call)は、検索前にサブルーチン数に応じてバイアス(107、1131、または 32768)を引く必要があり、これを怠ると全く別のサブルーチンを呼び出してしまいます。

CIDキー(CID-keyed)CFFは、不慣れな実装が躓きやすい間接参照を含んでいます。文字コードはCIDを選択しますが、チャーストリングのインデックスはGID(Glyph ID)であり、フォントのcharset(文字セット)がGIDをCIDにマッピングします。そのため、レンダラーは描画前に逆引きの CID-to-GID マップを構築し、複数のPrivate DICTを持つフォントの場合は FDSelect を介してグリフごとの Private DICT を選択します。一方、名前キー(Name-keyed)の Type1C データ(シンプルな Type 1 フォントに多く見られる形式)は、1バイトの文字コードを CFF の組み込みエンコーディング、または後述するPDFレベルのエンコーディング処理を介して直接解決します。なお、インタープリターはストリームの同期のためにヒント演算子をパースしますが、ヒント処理(ヒンティング)自体は実行しません。この制限については最後で説明します。

Type 3フォントの概要と描画方法

Type 3 のグリフは、一般的なアウトラインデータではありません。ISO 32000-1 §9.6.5 の定義によると、これはコンテンツストリームそのものです。そのため、HotPDFはグラフィックス状態をスタックに退避させ、フォント行列、フォントサイズ、およびテキスト行列を現在の変換行列(CTM)に合成し、フォント自身の /Resources 辞書のスコープ内で、ページ描画時と同じオペレーター解釈処理によってグリフを描画します。仕様を正しく再現するためには、2つの詳細が重要になります。Type 3 の /Widths は、他のフォントで使用される 1/1000 テキスト空間ではなく、独自のグリフ空間で表現されるため、位置の移動には /FontMatrix を介す必要があります。これを怠ると、0.01の行列を持つバーコードフォントなどで移動距離が桁違いに狂ってしまいます。また、グリフ処理の冒頭で d1 オペレーターが実行された場合、レンダラーは2つの処理を強制します。描画は宣言された境界ボックス(Bounding Box)内にクリップされ、さらに ISO 32000-1 §9.6.5.2 に従い、グリフ内のカラー変更オペレーターを無視して呼び出し元のカラーで描画します。そのため、グリフストリーム内の rggk などのカラー設定オペレーターは一時的に無効化されます。このカラールールを無視すると、ページ側で青に指定された d1 バーコードフォントが黒で出力されてしまいます。クリップを無視すると、崩れたグリフがその描画エリアを越えて周囲を汚してしまいます。

文字コードがグリフIDに変換される仕組み

アウトラインのパースは処理の半分に過ぎません。PDF文字列のバイトデータは文字コードでありグリフインデックスではないため、ISO 32000-1 ではそのマッピング仕様に2つの節を割いています。シンプルなフォント(Simple Font)において、§9.6.6 は厳格な優先順位を定めています。/Differences 配列がベースエンコーディング(WinAnsiEncodingMacRomanEncoding、または StandardEncoding)を上書きし、それがさらにフォントデータ自体のマップを上書きします。HotPDFはこの変換チェーンを 256エントリの code-to-GID テーブルに落とし込み、CFFデータ内の完全マッチング、gNN / glyphNN といったインデックスの直接表記、および Adobe Glyph List による文字名からUnicodeへの変換とそれに続く TrueType の cmap テーブルの検索の3つのルートを用いてグリフインデックスに翻訳します。複合フォント(Composite Font)の場合は、§9.7 に従い CIDToGIDMap がマッピングを制御します。通常は /Identity ですが、CIDでインデックスされたビッグエンディアン値のペアのストリームであることもあります。HotPDFが自ら生成するUnicode出力のサブセットフォントはまさにこのストリーム形式を使用するため、これは稀な例外ケースではありません。

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

TrueTypeの cmap の検索が必要な場合、HotPDFは単一のサブテーブルを信用せず、フォールバックの仕組みを提供します。まず Windows の Unicode サブテーブル(フォーマット4、およびサプリメンタリープレーン向けのフォーマット12)を優先し、次に Wingdings などのシンボルフォントがASCIIコードに応答するために使用する F000 プライベート利用領域(PUA)を考慮した (3,0) シンボルサブテーブル、および古いフォーマット6および0の順に検索します。なお、フォーマット2のサブテーブルは解釈されません。これは Shift-JIS や Big5 などのレガシーなマルチバイトコードページをマップするためのものであり、現代のCJKフォントはほぼ確実にフォーマット4または12を併せ持っているためです。どのルートでもマッピングできなかった文字は、システムフォントを用いたGDI描画にフォールバックされるため、一文字の不良によってテキスト表示全体が失敗するのを防ぎます。

埋め込み描画が実行しない処理(制限事項)

制限事項について明確に説明します。ヒント処理(ヒンティング)は実行されません。アウトラインはデータ通りに塗りつぶされます。これは 150 DPI 以上の環境ではヒンティング適用時と見分けがつきませんが、非常に小さいフォントサイズでレンダリングする際にはドットの配置が1ピクセル程度異なる場合があります。また、FontFile にある暗号化されたオリジナルの Type 1 データや、OpenType のバリアブルフォント(Variable Font)の軸パラメータは解釈されません。これらのケースや、フォントデータが破損している場合、glyf テーブルに利用可能な cmap が含まれていない場合などは、描画の失敗を防ぐためにシステムフォントを用いた代替描画にフォールバックされます。この正確性を最優先するアプローチはレンダラーの他の部分にも共通しており、例えばグラデーションの軸状・放射状シェーディングパターンや、PDFの保存時におけるEndDocがフォントサブセットを並べ替える処理の仕様などにも反映されています。

このパイプラインを使用するにあたり、フォント関連のコードを個別に記述する必要はありません。ページのレンダリング処理の裏側で、これらすべての機能が自動的に有効になります。

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

これにより、お使いのアプリケーションの動作品質が向上します。フォントが埋め込まれたPDFは、ビルドサーバー、Windowsコンテナ、あるいはそのフォントがインストールされていないエンドユーザーの環境であっても、埋め込まれた本来のフォントで正しく描画されます。この埋め込みグリフ描画パイプラインは、DelphiおよびC++Builder向けの HotPDF Component に含まれています。これは外部DLLへの依存がなく、PDFの作成、編集、テキスト抽出、およびレンダリングをサポートするネイティブなVCLライブラリです。