技術記事

HotPDFによるDelphiでのPDFタイリングパターンレンダリング

1枚の平坦な灰色ブロックとしてレンダリングされるハッチングは、典型的なタイリングパターンの失敗だ。Delphi・C++Builder向けのネイティブVCL PDFコンポーネントであるHotPDFは、現在のパスを一時的なクリップに変換し、可視タイルごとにパターンコンテンツストリームを1回ずつ再生することでPatternType 1を描画する。パターン選択はグラフィックス状態に保持され、qQによって復元される

症状は2つの形で現れ、原因を知るまでは無関係に見える。CAD図面はセクションハッチングを失い、レンダラーがパターンを平均色に解決してその色を塗った結果として、代わりにベタ塗りとして戻ってくる。あるいはハッチングが漏れ出す:本来は無地の白であるべきタイトルブロックが、少し前の詳細図の斜めの線を拾ってしまう。どちらもパターン状態の問題だが、そのうちの片方だけがタイルの描画についての問題だ

なぜタイリングパターンは次のパスに漏れ出すのか

選択されたパターン名は、それを使ったオペレータのプロパティではなく、グラフィックス状態の一部だからだ。ISO 32000-1 §8.6.6.2は、パターンカラースペースを、scnまたはSCNに供給されるパターン名がその色の値であるものとして定義しており、色状態の他のあらゆる構成要素はqによって保存されQによって復元される。パターン名も同じルールに従わなければならない。HotPDFはそれを状態レコード内にFillPatternNameStrokePatternNameとして保持し、塗りとストロークのカラースペースファミリーと並べているので、Qは以前の選択を、CTMを元に戻すのとまったく同じように元に戻す

その名前を代わりにオペレータディスパッチャ内のローカル変数に保存すると、それはストリーム中のあらゆるQを生き残ってしまう。すると失敗は予期しない場所に現れる:パターン付きのパスのあとに描画されたフォームXObjectが、自身のコンテンツストリームでは一切行っていないパターン選択を継承し、その塗りはハッチング付きになって出てくる。ネストされたフォームはこれをさらに悪化させる。ネストの各階層は、迷ったままの変数が無視する状態のプッシュ・ポップを行うからだ。csCSで非パターンのカラースペースを設定すること、あるいは単純なg/rg/kを発行することも、パターン名をクリアしなければならない。そうしないと、古い選択が、それに意味を与えたカラースペースより長く生き残ってしまう

q
  /Pattern cs              % pattern colour space, ISO 32000-1 8.6.6.2
  /P1 scn                  % coloured tiling pattern, PaintType 1
  10 10 200 120 re f       % this rectangle is hatched
Q
0 0 300 200 re f           % must be black again, not hatched

q
  /Cs2 cs                  % [/Pattern /DeviceCMYK] array
  0 0.6 1 0 /P2 scn        % uncoloured pattern plus its underlying colour
  20 20 160 90 re f*
Q

パターンはクリップを通じて描画され、決して塗りとしては描画されない

正しいモデルは減算的だ:デバイスのクリップを描画される形状に制限してから、その内側でパターンのコンテンツを実行する。HotPDFは決して最初に固体近似を描画してから上書きすることはしない。中間の固体はタイル間の隙間から見えてしまい、タイルコンテンツ内のどんな透明度とも衝突してしまうからだ。§8.7.3.2は、タイリングパターンを固定された水平・垂直間隔で複製されるコンテンツストリームとして記述しており、複製はすでに正しい形状を持つクリップに対してのみ意味を成す。塗りについては変換は直接的だ:HPDFSelectFillPathClipは、f*B*b*についてポリゴンの塗りつぶしモードをALTERNATEに、非ゼロの変種についてはWINDINGに設定し、GDIパスを構築し、SelectClipPathでそれをクリップに交差させる。その1行だけで、偶奇規則のパターン付きの塗りが、偶奇規則の固体の塗りと同じ穴を残すようになり、それこそがドーナツ形のハッチング領域に必要なものだ

ストロークは間違えやすい部分だ。ストロークされたパスには内部がないので、パスそのものをクリップに交差させると空の領域になり、何も描画されなくなる。したがってHPDFSelectStrokePathClipは、まず現在の状態から幾何学的なペンを構築する。PS_GEOMETRICを使い、Jから端のキャップ、jから結合、Mからマイター制限を取り、ダッシュ配列が有効な場合はPS_USERSTYLEを使う。それからWidenPathを呼び出して、ストロークされたアウトラインを、クリップの前に塗りつぶし可能な領域に変換する。キャップ、結合、マイター、ダッシュの挙動は、2つ目の実装によってではなく、構築によって通常のストロークと一致する。ここには2つの正直な限界がある:デバイス単位1未満の線幅は1ピクセルにクランプされ、ダッシュ配列はExtCreatePenが受け付ける上限である16エントリで切り詰められる

実際にどのタイルが見えているのか

可視範囲は、変換を逆方向に実行することから得られる。タイル配置はパターン空間で行われるが、ページのどれだけが触れられているかを知っているのはデバイス空間にあるデバイスクリップボックスだけだ。HotPDFはBaseMatrix := CTM * PatternMatrixを合成し、それを反転し、GDIクリップボックスの4隅をその逆行列で写し戻す。それら4つの写像された隅の軸並行境界が、覆われ得るパターン空間の矩形を与え、その矩形をパターンBBoxに対するXStepYStepで割ることで、閉じたインデックス範囲が得られる。各セルはその後、CTM * PatternMatrix * Translate(i * XStep, j * YStep)のCTMでレンダリングされ、自身の変換されたBBoxポリゴンに対して2回目のクリップを受ける。この2回目のクリップは、重複するタイルデザインが表現される方法である、XStepがバウンディングボックス幅より小さい場合に重要になる。それがなければ、隣接するセルは宣言された範囲の外側で互いに上書きしてしまうだろう。セルごとのクリップがNULLREGIONとして戻ってきた場合、そのセルはトークン化も実行もされずにスキップされる

// Map the device clip box back into pattern space through the inverse of
// CTM * PatternMatrix, then convert those bounds into tile index ranges.
BaseMatrix := HPDFMatMul(FGSStack.State.CTM, PatternMatrix);
if not HPDFMatInvert(BaseMatrix, InverseMatrix) then Exit;   // singular: refuse
if GetClipBox(FDC, ClipRect) = ERROR then Exit;

// MinX..MaxY are the axis-aligned bounds of the four mapped clip corners.
I0 := Floor((MinX - BBox[2]) / StepXAbs);
I1 := Ceil ((MaxX - BBox[0]) / StepXAbs);
J0 := Floor((MinY - BBox[3]) / StepYAbs);
J1 := Ceil ((MaxY - BBox[1]) / StepYAbs);

PlannedTiles := Int64(I1 - I0 + 1) * Int64(J1 - J0 + 1);
if (PlannedTiles <= 0) or (PlannedTiles > FPatternTilesRemaining) then Exit;
Dec(FPatternTilesRemaining, Integer(PlannedTiles));

無色パターンと外部から来る色

PaintType 2のパターンは形状を運ぶが色は運ばず、色はパターン名とともに到着する。§8.7.3.2は、無色パターンは基礎となる空間を宣言するパターンカラースペースとのみ使用されると規定しているので、scnはまず成分値を受け取り、最後にパターン名を受け取る。HotPDFは、これらの成分をパターンカラースペースエントリに格納されている基礎空間を通じて解決するので、無色のハッチングは他のどんな塗りとも同じようにSeparationインキやDeviceNの組み合わせで着色できる。その解決の仕組みはSeparationとDeviceNのスポットカラーのレンダリングで扱っている。タイルの内側では、2つのペイントタイプは大きく分岐する。PaintType 2の場合、レンダラーはタイルの間だけ色オペレータ抑制フラグを設定するので、パターンコンテンツ内のgrgkscnはどれも無視され、すべてのマークは外部から供給された色を取る。PaintType 1では逆になる:塗りとストロークの状態はPDFの既定値、アイデンティティのカラースペースを持つDeviceGrayの黒にリセットされ、タイルは自分自身を着色する。そのリセットをスキップすると、fオペレータの時点でたまたま現在有効だった色が、本来自己完結しているはずのパターンに漏れ出してしまう

なぜグラフィックス状態のスタック深さはタイルごとに復元されなければならないのか

パターンコンテンツストリームは不均衡であることが許されており、その被害はセルをまたいで積み重なるからだ。3つのqオペレータと2つのQオペレータを含むタイルは、スタックを開始時より1フレーム深いままにする。セル間で現在の状態レコードだけを復元すると、深さは増え続け、200番目のセルは199番目のセルに属するスタックフレームから、そのフレームが持っていたどんなCTMやクリップとともに実行されることになる。したがってHotPDFは、タイルループの前に状態レコードとスタック深さをスナップショットし、各イテレーションの先頭でRestoreSnapshotを呼び出す。これはスタックを保存された長さまで切り詰め、保存された状態を1ステップで再インストールする。ページのResources辞書と色オペレータ抑制フラグは同じ境界で復元される。タイルは自分自身のリソースを参照することがあり、それを隣のタイルに渡してはならないからだ。GDIのクリップ状態は、各セルの周りのSaveDC / RestoreDCのペアで同じ扱いを受けるので、自分自身のW nクリップをインストールするタイルが次のタイルに利用可能な領域を縮小することはできない

予算、拒否、そしてレンダラーが描画しないもの

タイリングパターンはPDFの中で最も簡単にサービス拒否ファイルを書ける場所なので、その上限はヒューリスティックではなくハードな数値になっている。パターンのネストは深さ4に制限されている。これはフォームXObjectの再帰に使われるのと同じガードであり、自分自身のリソース辞書を通じて自己参照するパターンを止める。1回のパス描画は、ネストされたパターンにわたって数え下げられる合計最大16,384タイルまで実行でき、最外パターンの描画が始まったときにのみリセットされる。予算の残りを超える計画セル数を持つタイルグリッドは、1つのセルも実行される前に無条件で拒否される

退化した幾何は近似されるのではなく拒否される。欠落したまたは面積ゼロのBBox、大きさが1e-6未満のXStepYStep、逆行列を持たないCTM * PatternMatrixの積、1e9を超える写像されたクリップ座標、または100万を超える大きさのインデックス、これらはすべてパターンの描画を、何も描画せずに返させる。結果は、実行が止まったレンダースレッドの代わりに描画されない領域になり、これはバッチコンバーターにおいて望ましいトレードオフだ。パフォーマンスは1つの決定から来る:パターンストリームは描画ごとにHPDFTokenizeContentStreamで一度だけトークン化され、トークン配列はすべての可視セルにわたって再利用されるので、タイル数は実行コストを増やすが字句解析コストは増やさない

Delphiからパターン付きページをレンダリングする

パターンサポートについて、呼び出し側のコードで変わることは何もない。ドキュメントを読み込み、ページを要求すれば、タイリングの作業は、ページからビットマップへのレンダリングがすでに駆動しているコンテンツストリームインタープリタの内側で行われる。同じインタープリタがビットマップ、メタファイル、プリンタのデバイスコンテキストに供給しているので、プレビューサムネイルで正しく見えるハッチングされた図面は、同じタイル形状で印刷される。PatternType 2のシェーディングパターンは異なる分岐を取り、それは裸のshオペレータと評価パスを共有しており、軸方向・放射状シェーディングレンダリングで詳しく説明されている

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('assembly-drawing.pdf') > 0 then
    begin
      // Section hatching that previously flattened to a solid block now
      // replays the tile content once per visible cell.
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 200);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('sheet1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

パターン付きの領域が今なお正しくない場合は、3つの失敗クラスを順にチェックしよう。完全に空白の領域は通常拒否を意味する:XStepYStepBBoxに退化した値がないか調べるか、そのグリッドが必要とするタイル数を16,384の上限に対してカウントしよう。単一の平坦な色で塗りつぶされた領域は、パターン名がペイントオペレータに届かなかったことを意味し、それはストリーム中のcsscnの順序を指し示す。あるべきでない場所にパターンが現れる場合は状態復元の問題であり、見るべき場所は、それを継承したフォームやパスの周りのq / Q処理だ

タイリングパターンは、それを必要とするファイルが受信箱に届くまで見えないままでいるPDF機能の1つであり、届いた瞬間に仕事のすべてになる。DelphiやC++Builderで描画ビューア、エンジニアリングドキュメントコンバーター、レポートレンダラーを構築しているなら、コンポーネント全体とそのレンダリングAPIはHotPDF Delphi PDFコンポーネントのページに文書化されている