技術記事

HotPDFでEMF/WMFベクターをDelphi PDFへ取り込む

ネイティブDelphi/C++Builder用PDFコンポーネントであるHotPDFは、Windows EMFおよびWMFメタファイルを、ファイルをビットマップに平坦化するのではなく各GDIレコードを直接PDF演算子へ解釈することで取り込む:グラデーション塗りつぶしはPDFの軸方向シェーディングパターンになり、ハッチブラシはPDFのタイリングパターンになり、集中管理されたパス状態ゲートが不正なレコードによる出力破壊を防ぐ。TChart、GDI+サーフェス、あるいは単純なTCanvasが拡張メタファイルとしてエクスポートできるあらゆるチャートがこの経路の対象となり、その差はページを拡大表示した瞬間、あるいは高解像度プリンタに送った瞬間に現れる

ほとんどのDelphi開発者が既定で選びがちな代替手段は、ページに配置する前にメタファイルをビットマップにラスタライズすることであり、そのコストは後になってから現れる:画面上ではくっきりしていた棒グラフが、PDFを600 DPIで印刷したり会議室のスクリーンに投影したりした瞬間に目に見えてブロック状になり、塗り方式が引き継がれなければハッチ塗りのCAD領域は単なる平坦なグレーの矩形に潰れてしまう。メタファイルを絵ではなくプログラムとして読むことがこの両方の問題を回避する方法であり、それは正しく実装するにはより難しい経路である。だからこそ、レポートが出荷される前に以下の落とし穴を知っておく価値がある

なぜメタファイルをビットマップに平坦化せず解釈するのか

HotPDFがEMF/WMFの取り込みをベクター経路に保っているのは、Windowsメタファイルが絵ではなく記録されたGDI描画呼び出しの列であり、その呼び出しをPDFのパス・テキスト・シェーディング演算子として再生することがページの残りの部分と同じように拡大縮小できる結果をもたらすからである。THPDFPage.ShowMetafileとその仲間であるShowMetafileExはアプリケーションが呼び出すエントリポイントであり、いずれもメタファイルをTHPDFWmfクラスに渡す。このクラスがすべてのGDIレコードを辿り翻訳する。この区別は絶対的なものではなく、HotPDFもそのふりはしていない:メタファイルレコードが本当にラスターデータである場合(例えばStretchDIBitsのビットマップ転送)、それはページ上の他の絵と同じAddImageShowImageという呼び出しの組を通じて、写真を表現できないパス演算子に無理やり押し込むのではなく、本物のPDFイメージXObjectとして埋め込まれる。線、塗りつぶし、テキストはベクターのままであり、ソースですでにピクセルだったものは出力でもピクセルのままである。最もシンプルな呼び出しにはロード済みのメタファイル以外に何も必要としない

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

インタプリタはGDI座標をどのようにPDFページ空間へ変換するのか

HotPDFはこれに対し、GDIの二重実装を用意するのではなく、メタファイル自身のレコードストリームへの単一パスで答える。THPDFWmf.AnalyseはWin32のGetEnhMetaFileHeader呼び出しを通じてメタファイルヘッダーを読み、内部の描画状態をリセットし、メタファイルビューアが使うのと同じ列挙APIであるEnumEnhMetafileを呼び出す。これにより、すべてのEMR_*レコードが元々記録された順序でTHPDFWmf.ExecuteRecordに届く。GDIはメタファイル自身のマッピングモードが選ぶデバイスまたは論理単位でトップダウンに座標を表現するが、PDFページはユーザー空間ポイントでボトムアップである。この座標系はHotPDFのパスと塗りつぶしに関するキャンバス描画モデルで扱われている。各レコードハンドラはScaleXScaleYを通じてこの食い違いを解決する。これらはProjectXProjectYを呼び出して、異方性および等方性マッピングモード用のGDI自身のウィンドウ・ビューポート変換式を再生する。したがって論理単位で幅5と記録された図形は、ソースアプリケーションがどんなウィンドウ・ビューポート範囲を設定していたかに関わらず、正しい幅でPDFポイントに配置される

GDIのグラデーション塗りつぶしはどのようにPDFシェーディングパターンになるのか

EMR_GRADIENTFILLレコードは、GDIが2つの矩形モードのいずれかで記録している限り、本物のPDF Type 2軸方向シェーディングパターン(ISO 32000-1 §8.7.4.5)になる。THPDFWmf.VEMRGradientFillは、MS-EMF §2.3.1.6の構造に従い、生のバイトバッファから直接そのレコード自身のレイアウトを読み取る:16ビットRGBAの角を表す頂点配列と、それに続く各頂点2つを参照する矩形のリストである。GRADIENT_FILL_RECT_Hの場合、色は矩形の水平中央線に沿って左から右へ遷移する。GRADIENT_FILL_RECT_Vの場合は、垂直中央線に沿って上から下へ遷移する。いずれの場合も、2つの角の色と投影された矩形座標はそのままTHotPDF.RegisterAxialGradientに渡され、これがパターン名を返す。ページはその矩形を描画し、平坦なSetRGBFillColor呼び出しの代わりにそのパターン(SetFillPattern)を通じて塗りつぶす。そのため、表計算風の帯状ヘッダーやチャートのグラデーションプロット領域は、単一の平均色に潰れるのではなく、そのブレンドを保つ

グーロー三角形モードは正直に言って未対応の部分である。レコードのulModeフィールドがGRADIENT_FILL_TRIANGLEを報告すると、VEMRGradientFillはそれを認識し、三角形モードがまだ実装されていないとログに記録し、2色近似を推測するのではなく矩形をスキップする。任意の三角形メッシュ全体にわたる頂点単位・ピクセル単位の補間は2ストップの軸方向またはラジアルシェーディングには還元できず、それを正しく表現するにはPDF Type 4またはType 5のメッシュシェーディングを出力する必要がある。これはHotPDFのページレンダラーがPDFを読み戻す際にも未描画のまま残すのと同じシェーディングファミリーである。無関係な2つのコードパスが同じ境界にたどり着く:メッシュシェーディングは書き込み側でも読み取り側でもギャップであり、グーロー三角形を使って滑らかなラジアルグローを表現するソース図は、レンダリングされた近似ではなく最後に使われた単色ブラシの色にフォールバックする

ハッチブラシは平坦化されたグレーではなくタイリングパターンになる

GDIハッチブラシがPDFでその質感を保つのは、THPDFWmf.SetBrushColorが単色塗りつぶしにフォールバックする前にCurrentBrush.lbStyleBS_HATCHEDかどうかを確認し、その場合は代わりにSetHatchBrushPatternへ処理を回すためである。このメソッドは、GDIのハッチスタイルに応じて選ばれるストロークされた線演算子(mlS)から成る8x8単位のPDFコンテンツストリームを書き込む:HS_HORIZONTALHS_VERTICALには単一の水平または垂直ストローク、HS_FDIAGONALHS_BDIAGONALには3本の平行斜線、そしてHS_CROSSHS_DIAGCROSSには水平プラス垂直、または両斜線の組み合わせである。THotPDF.RegisterTilingPatternはそのコンテンツストリームを、8単位のXStepYStepを持つ着色タイリングパターン(PaintType 1、ISO 32000-1 §8.7.3.1)として登録し、ページは軸方向シェーディングと同じようにSetFillPatternを通じて塗りつぶす。材質を区別するためにハッチ塗りに頼るCADの平面図や工学図面は、すべての領域が同一のグレーに失われる代わりに、その視覚言語をPDF内で保持する

すべてのブラシがこの扱いを受けられるわけではなく、CADの取り込みを出荷する前に知っておく価値のあるギャップである。EMR_CREATEDIBPATTERNBRUSHPT、すなわちGDIの6種類の標準ハッチスタイルではなくカスタムビットマップ画像パターンブラシ用のレコードは、後続のSELECTOBJECTDELETEOBJECTレコードの整合性を保つためだけにそのハンドルを登録する。HotPDFはまだ任意のタイル画像用のPDF Patternリソースパイプラインを公開していないため、そのブラシを選択するとソースのテクスチャではなく単色フォールバックに流れ込む。塗りつぶしが、元は明らかに繰り返し画像テクスチャを使っていたはずの箇所で平坦にレンダリングされる場合、そのソースブラシはほぼ間違いなく標準ハッチではなくカスタムDIBパターンであり、それがまず手作業で確認する価値のある唯一のケースである。そのような図面の取り込みを設定する際も、同じオプションオブジェクトを経由する

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

不正なメタファイルによるページの破壊を何が防ぐのか

HotPDFの答えは、約80個あるレコードハンドラそれぞれに繰り返される防御的チェックではなく、ExecuteRecordの先頭にある単一のゲートである。EMR_BEGINPATHによって開かれEMR_ENDPATHまたはEMR_ABORTPATHによって閉じられるGDIパスブラケットは、FPathContinueフィールドに裏付けられたプライベートなPathContinueプロパティによって追跡される。そのブラケットが開いている間、ExecuteRecordはパス構築レコードのみを通過させる。それは移動・線・折れ線・多角形・ポリベジエ・ポリドローの各バリアント、CLOSEFIGURE、そしてSETWORLDTRANSFORMSAVEDCRESTOREDCといった変換・DC状態レコードの小さな集合である。ブラケットが開いている間にExecuteRecordに届くそれ以外のレコード型——たとえば紛れ込んだEXTTEXTOUTやビットマップ転送——は、届いた瞬間に単一のExitによって一括して破棄される

このゲートが存在するのは、手作業で作成された、あるいはツール生成された、または単に破損したメタファイル内のパスブラケットが、正しく整形されたファイルがその開始・終了レコードの間に置くであろうものだけを含んでいるとは保証されないからである。ゲートがなければ、EMR_BEGINPATHEMR_ENDPATHの間に着地したテキスト出力レコードは、構築中のパスジオメトリを汚染するか、純粋なパス構築であるべきシーケンスの途中でPDFテキスト表示演算子を出力してしまう。どちらの失敗モードも、通常のテストスイートがたまたまカバーしているようなものではなく、サードパーティツールから来た1つの不正な入力で表面化する類のものである。ExecuteRecordにこのチェックを集中させることで、個々のVEMR*ハンドラはそれぞれ誤ったタイミングで呼び出されることへの防御を持つ必要がなくなる。ゲートはディスパッチの前に一度だけそれを決定する、その後で80回決定するのではなく

1ページの中でベクターチャートをテキストや画像と並べる

レポートページがチャートだけを含むことは稀であり、ShowMetafileはHotPDFの他のページ演算子と、他のあらゆる描画呼び出しと全く同じように組み合わさる。TextOutで描かれた見出し、EMFとして取り込まれたハッチ塗りの棒グラフ、そしてShowImageで配置されたロゴは、いずれもネイティブの忠実度を保ったまま同じページの同じコンテンツストリームに収まる。この構成パターンはHotPDFのレポートにおけるテキスト・フォント・画像のレイアウトガイドで扱われている

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

EMF/WMFインタプリタ、それがグラデーション塗りつぶし用に登録する軸方向シェーディングパターン、そしてここで説明したハッチブラシ用のタイリングパターンへのマッピングは、いずれもDelphiおよびC++Builder向けHotPDFコンポーネントの標準機能として出荷される、外部DLL依存を一切持たないネイティブVCLライブラリである