技術記事

HotPDFを使用してDelphiでPDFページをビットマップにレンダリングする

HotPDFは、1回の呼び出しRenderLoadedPageToBitmap(PageIndex, DPI)を実行することで、読み込まれたPDFページをDelphiのTBitmapにレンダリングします。この関数はページのコンテンツストリームを解釈し、選択した解像度で呼び出し側が所有する24ビットRGBビットマップを返します。これは、サムネイルリスト、印刷プレビュー、またはPDFから画像へのエクスポートパイプラインでまさに必要とされる機能です。本記事では、このAPIについて説明し、続いて実用的なレンダラーと単なるおもちゃを分ける重要な部分である、類似のシステムフォントではなく埋め込まれたフォントプログラム自体からテキストを描画する処理について解説します

なぜPDFページのレンダリングは画像を描画するよりも難しいのか?

PDFページは単なる画像ではなく、1つのプログラムです。これは、パスの構築、フォントの選択、色の設定、およびグリフの配置を行う一連のオペレータであり、ISO 32000-1 §8で定義されているグラフィックスモデルに対して実行されます。ファイル内のどこにも、各ピクセルがどのように見えるかは記述されていません。ビットマップを生成するには、そのプログラムを実行し、現在の変換行列、q/Q用のグラフィックス状態スタック、クリッピングパス、塗りつぶしおよび線描画用の色空間を維持し、その結果をラスタライズする必要があります。そのため、「単に3ページ目を画像として表示する」という処理は、ファイル形式の変換ではなく、コンテンツストリームのインタープリタ処理そのものなのです

v2.253.0で導入されたHotPDFのレンダラーは、このモデルを反映した6つの分離されたユニットで構築されています。すなわち、PDFの[a b c d e f]変換代数用の アフィン行列コア、グラフィックス状態スタック、色空間リゾルバ(DeviceRGB, DeviceGray, DeviceCMYK, Indexed)、PDFパスオペレータをGDIに橋渡しするパスビルダー、正しい送り幅を読み取るための/Widths配列読み込み用フォントメトリクスレイヤー、およびオペレータをディスパッチして他の5つを制御するインタープリタです。画像XObjectは、ライブラリが抽出時に使用するのと同じデコードスタックを経由するため、HotPDFが抽出時にデコードできるすべての画像フィルタJPXDecode圧縮されたJPEG 2000画像を含む)も、レンダリングされた出力に反映されます

読み込まれたページをTBitmapにレンダリングする

RenderLoadedPageToBitmapは、0から始まるページインデックスとDPI値を受け取ります。72 DPIは、PDFのユーザ空間の1ユニットを1ピクセルにマッピングします。失敗した場合(インデックス範囲外、リソースの欠落など)は例外を発生させるのではなくnilを返すため、ビューアは問題のあるページをスキップして処理を続行できます。呼び出し側は返されたビットマップを所有し、使用後に解放する必要があります

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // page 1 at 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

DPI引数は、あらゆる一般的なシナリオでスケーリング処理を担います。サムネイルリストは36または48 DPIでレンダリングされ、高速に処理できる小さなビットマップを取得します。画面上のプレビューは96または144 DPIで表示密度に合わせます。300 DPIでのエクスポート処理は、印刷品質の画像を生成します。/Rotateエントリによるページの回転や、/MediaBoxの原点の反転(PDFは原点が左下、GDIは左上)は、ページからデバイスへの変換行列の内部で処理されるため、72 DPIのUS Letterページは正しい向きで正確に612×792ピクセルとして返されます

なぜレンダリングされたPDFサムネイルに誤ったグリフが表示されるのか?

レンダリングされたPDF出力における誤ったグリフや近似されたグリフは、ほとんどの場合、レンダラーがファイルに埋め込まれたフォントを使用せずにシステムフォントで代用していることを意味します。HotPDFの初期のレンダラーはまさにこの処理を行っていました。/BaseFontからサブセット接頭辞を取り除き(ABCDEF+ArialArialに変換)、GDIにその名前のシステムフォントを要求し、それを使用してテキストを描画していました。標準エンコーディングでArialやTimes New Romanを使用するドキュメントの場合、結果は非常に近く見えますが、あくまで近似であり、特定の条件下で動作が破綻します

サブセットが埋め込まれたフォントは手遅れのケースです。サブセットフォントには、ドキュメントが実際に使用する40個程度のグリフのみが含まれ、そのファイル固有の順序で文字コードが割り当てられている場合があります(コード1が「T」、コード2が「h」など)。システムフォントはそのような固有の割り当てを関知しないため、テキストが消失するか、全く異なる文字として表示されてしまいます。カスタムエンコーディング、シンボルフォント、バーコードフォント、およびレンダリングを実行するマシンにインストールされていないフォントフェイスも同様に失敗します。システムフォントの代替処理で妥協するレンダラーは、一見そのページと分かるサムネイルを生成しますが、そもそもフォントの埋め込みが必要となった特殊なフォントをページが使用している場合には対応できません

埋め込みグリフのレンダリング:フォントプログラム自体からの描画

HotPDFは、5つのリリース(v2.268.0からv2.272.0)にわたり、埋め込まれたフォントプログラムを解析し、そのグリフアウトラインを塗りつぶしGDIベクターパスとして再現することでこの課題を解決しました。レンダリングされたページ内のテキストは、標準的なビューアが使用するのと同じアウトラインデータから生成されるようなり、サブセットフォント、カスタムエンコーディング、およびインストールされていないフォントフェイスが正確な形状でレンダリングされます。この対応範囲は、フォントの形式ごとに段階的に構築されました

埋め込まれたTrueTypeプログラム(FontFile2)を持つType0/CIDFontType2フォントの場合、レンダラーはglyfおよびlocaテーブルを直接解析します。2次スプライン曲線はGDIが認識できる3次ベジェ曲線に変換され、連続する制御点間の暗黙のオンカーブ点が再構築され、複合グリフが再帰的に再現されます。Identityおよび明示的なストリームのCIDToGIDMapレイアウトの両方がサポートされ、CIDの送り幅は/Wおよび/DWの幅エントリに準拠するため、2バイトのIdentity-Hテキストが正しく進行します

CFFプログラム(CIDFontType0CType1C、またはOpenTypeラッパーなどのFontFile3)は、完全なType 2 charstring interpreterによって処理されます(直線、曲線、Flexファミリー、ヒントマスク、および正しいサブルーチンバイアスを持つローカル/グローバルサブルーチン呼び出しに対応)。CIDキー指定されたCFFプログラムは、文字コードをフォントのcharsetを介してマッピングします。これは、グリフ順序がCID順序と異なるサブセットフォントで重要であり、FDArray/FDSelectによるグリフごとのfont-DICT選択も尊重されます。シンプル(非CID)TrueTypeフォントは、埋め込みフォント独自のcmapテーブルを介して1バイトコードを解決します。これには、堅牢なサブテーブルチェーン(Unicodeフォーマット4および12が最優先、次にプライベート使用領域F000のミラーを持つシンボルサブテーブル、続いてレガシーなMacintoshフォーマット)が使用され、シンプルなType1 fontsはCFFプログラムの内蔵エンコーディングを介して解決されます

2つの洗練された機能がこの実装を補完します。第1に、シンプルなフォントの/Encoding辞書は、ISO 32000-1 §9.6.6が規定する優先順位に従って解決されます。/Differences配列はベースエンコーディングを上書きし、それはさらにフォントプログラム自体のマップを上書きします。これはTeXやPostScript由来のツールチェーンが依存するパスであり、グリフ名はAdobe Glyph List、CFF charset、またはTrueTypeのcmapを介して解決されます。第2に、グリフ自体が小さなコンテンツストリームであるType3フォントは、フォント行列、フォントサイズ、およびテキスト行列を合成してレンダラーで再現されます。グリフ空間の/Widthsは、ISO 32000-1 §9.6.5の要求通り/FontMatrixを介して解釈され、d1バウンディングボックスを宣言するグリフプロシージャはその範囲内にクリップされるため、不正なバーコードグリフがセル外に描画されることはありません。何らかの理由でコードをマッピングできない場合(破損したプログラム、未定義の文字など)は、テキスト実行を中止するのではなく、そのグリフに対してシステムフォントによる描画にフォールバックします

繰り返されるレンダリングを高速化するには?

HotPDFが提供する解決策は、最も最近使用されたページを保持するキャッシュです。RenderLoadedPageToBitmapCachedは、ページインデックスとDPIをキーとして、最大でRenderCacheCapacity(デフォルトは8)までのレンダリング済みページを保持します。キャッシュヒット時は、コンテンツストリームにアクセスすることなく、呼び出し側が所有する新しいコピーを返します。これは通常、ページを再解釈するよりも数千倍高速です。このパターンは、ユーザーが2つのページを行き来する場合や、同じDPIで同じページを再要求するリサイズイベントが発生する場合など、ビューアの動作に完全に適合し、毎回キャッシュにヒットします

// Thumbnail strip: first pass renders, scrolling back hits the cache
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// After editing a loaded page in place:
Pdf.InvalidateRenderedPageCache;  // next render reflects the change

キャッシュ容量を増やす前に、メモリ消費量について慎重に検討する必要があります。300 DPIのUS Letterページは2550×3300ピクセルであり、24ビットビットマップとして約25 MBになります。したがって、エクスポート解像度で8ページキャッシュすると、約200 MBのメモリを消費します。一方、サムネイル解像度であれば、同じ8つのエントリでも1メガバイトを大幅に下回ります。実際にキャッシュするDPIに合わせてRenderCacheCapacityを設定し、その場での編集を行った後はInvalidateRenderedPageCacheを呼び出してください。キャッシュはページ番号とDPIのみをキーとしているため、内部のコンテンツが変更されたことを検出できません。新しいドキュメントをロードすると、キャッシュは自動的にクリアされます

ページキャッシュの背景では、2つ目のキャッシュが動作しています。デコードされた画像XObjectは、ImageCacheMaxBytes(デフォルトは32 MB)で制限されたバイト管理型のストアに保持され、LRU(最も長期間参照されていないデータを削除)方式で制御されます。すべてのページに繰り返し配置されるロゴやレターヘッド画像は、Doオペレータごとにデコードされるのではなく、ドキュメントロード時に一度だけデコードされます。これにより、共有画像を持つページのレンダリング時間がほぼ半減し、マルチページTIFFのエクスポートも同等に高速化されます。InvalidateRenderedPageCacheを実行すると、このキャッシュもクリアされます

近似レンダリングとなる範囲

レンダラーは一般的なドキュメントPDFサブセットを対象としており、その制限を把握しておくことが重要です。CalRGB、Lab、およびICCベースの色空間は、厳密なカラー管理ではなく近似処理されます。デバイス固有の色空間、インデックスパレット、およびサンプリングされたType 0関数によるカラー検索は処理されますが、ICCレンダリングインテントに依存する印刷制作ファイルの色彩は正確に再現されません。シェーディングパターン(sh)や、単純なアルファ値を超える描画ブレンドモードもサポート外であり、フォームXObjectの再帰は循環ガードのため深度が制限されます。請求書、レポート、契約書、フォームなどのテキスト、パス、および画像で構成されるページでは正確に出力されますが、グラデーションや透過グループを多用するデザインの校正刷りの場合は、ビットマップ出力を校正ではなくプレビューとして扱ってください

実務における要点:パイプラインでHotPDFを使用してドキュメントを生成する場合、または一般的なビジネスPDFを読み込む場合、RenderLoadedPageToBitmapは埋め込まれた正確なグリフ形状、正しいCID送り幅、および正確なページジオメトリを用いて完全に再現します。近似処理される部分は、ビジネスドキュメントでは滅多に使用されないグラフィックスモデルの極端な機能に限られます

RenderLoadedPageToBitmap、そのキャッシュ対応版、およびここで説明した埋め込みグリフレンダリングパイプラインは、DelphiおよびC++Builder向けのHotPDF Componentの一部として提供されています。外部DLLへの依存がないネイティブVCLライブラリであり、PDFの作成、編集、テキスト抽出、およびページレンダリングを1つのパッケージでカバーしています