PDFを扱うほとんどのDelphiコードは、このフォーマットをテキストのブロックと配置されたいくつかのビットマップを格納するコンテナとして扱っています。その見方はそれ自体正しいものですが、フォーマットの中で最も強力な部分を使用しないままにしてしまっています。PDFページは、PostScriptと同じ描画モデルに基づいて構築された、解像度に依存しない2Dキャンバスです。直線、曲線、塗りつぶされた領域、グラデーション、繰り返されるパターンをすべてベクターとして描画できます。これらは拡大してもシャープさを保ち、デバイスの最大解像度で印刷されます。ロゴ、グラフ、透かし、または証明書の枠線などを描画する場合、ベクターパスはほぼ常に適切なプリミティブであり、多くのプログラムが選択するラスタライズされた画像よりも軽量で鮮明です。
この記事では、ISO 32000-1が定義するベクターモデルについて解説し、対応するPDFlibPasの呼び出し方法を示します。APIは仕様に密接に対応しているため、仕様を具体的に理解し、一方を学ぶことで他方を理解できるようにすることを目的としています。
ページはパスを処理するマシンである
ISO 32000-1 §8.5は、グラフィックスを重複しない2つのフェーズで説明しています。まず、純粋な幾何学的形状であり視覚的な結果を持たないパスを構築します。次に、その輪郭を描画(ストローク)するか、内部を塗りつぶす(フィル)か、あるいはその両方を実行する単一の操作でパスを描画します。構築中にはページ上には何も表示されません。パスは、描画オペレータがそれを消費するまでグラフィックス状態に保持される、抽象的な点とセグメントのシーケンスであり、描画された時点でレンダリングされ、破棄されます。
パスは1つ以上のサブパスで構成されます。サブパスは1つの点から始まり、直線セグメント、3次ベジェ曲線、一部のプラットフォームでは独立した閉じたサブパスとして追加される矩形などを追加して拡張されます。PDFlibPasでは、StartPathでパスを開始して開始点を設定し、AddLineToPathやAddCurveToPathで拡張します。各呼び出しは暗黙の「カレントポイント」を進めるため、次のセグメントは前のセグメントが終了した場所から継続します。ClosePathはサブパスの開始点へと戻る最後の直線セグメントを描画します。これはストローク処理において重要であり、2本の緩いエンドキャップではなく、閉じた頂点において本物のラインジョイン(線の結合部)を生成します。
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
曲線はAddCurveToPathを使用し、2つのベジェコントロールポイントとエンドポイントを受け取ります:AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY)。曲線はカレントポイントから(EndX, EndY)まで走り、その途中で2つのコントロールポイントに向かって引っ張られます。円弧はAddArcToPath(CenterX, CenterY, TotalAngle)を介して利用できます。半径はカレントポイントと中心点との間の距離から取得され、エンジンは円弧をベジェセグメントの連鎖として出力します。矩形にはショートカットであるAddBoxToPath(Left, Top, Width, Height)があり、先行するStartPathなしで、独立した閉じた矩形を独自のサブパスとして追加します。
2つの塗りつぶしルールと、それらが一致しない理由
自身と交差するパスやインナーループを含むパスを塗りつぶす場合、レンダラーはどの領域が形状の内部で、どれが穴であるかを決定するルールを必要とします。ISO 32000-1 §8.5.3.3は2つのルールを定義しており、同じ幾何学的形状であっても異なって描画される場合があります。ノンゼロワインディング規則(nonzero winding rule)は、テストポイントから無限遠に向かうレイ(光線)とセグメントの交差を符号付きでカウントします。セグメントが左から右に交差する場合は1を加え、逆方向に交差する場合は1を引きます。合計がゼロでない場合、その点は内部にあります。奇偶ルール(even-odd rule)は方向を無視して単に交差回数をカウントし、カウントが奇数の場合にその点を内部と判定します。
これらが分岐する典型的なケースは、穴のある形状(ドーナツやワッシャー)です。外側の境界線と、その内部に内側の境界線を描画します。奇偶ルールでは、2つの境界線の間にある点は1回交差し、内側のループの内側にある点は2回交差するため、内側のループは常に穴として切り抜かれます。ノンゼロワインディング規則では、内側のループが外側のループと逆方向に巻かれている場合にのみ穴が現れます。同じ方向に巻かれている場合、ワインディングは相殺されずに強化され、内側の領域は塗りつぶされます。1本の自己交差する輪郭として描画された五芒星も同じ違いを示します。奇偶ルールでは中央の五角形が空になりますが、ノンゼロワインディング規則では塗りつぶされます。
PDFlibPasはフラグではなく、描画に使用する呼び出しによってルールを選択します。DrawPathはノンゼロワインディング規則で塗りつぶし、DrawPathEvenOddは奇偶ルールで塗りつぶします。どちらも同じ整数モードを受け取ります。0はアウトラインのストロークのみ、1は塗りつぶしのみ、2は塗りつぶしとストロークを実行します。奇偶ルールは、サブパスの方向を考慮する必要がないため、切り抜きの穴を作成する上で簡単なツールです。
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
線形グラデーション:線に沿って色を変化させる
単一の塗りつぶし色は、領域全体で1つの値です。グラデーションは継続的に色を変化させ、最も単純な種類は線形(アキシャル)グラデーションです。ISO 32000-1 §8.7.4.5は、これをType 2アキシャルシェーディングとして規定しています。軸を定義する2つの点、始点での開始色、終点での終了色を指定すると、レンダラーはその軸に沿って色を補間します。塗りつぶされた領域内の各点は、軸への垂直投影上の色を取るため、グラデーションは2点を結ぶ線に対して直角の帯状に走ります。
PDFlibPasでは、グラデーションは一度作成すればアクティブなペイントとして選択できる、名前付きのドキュメントリソースです。NewRGBAxialShaderでこれを登録します。シグネチャは NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend) です。軸の2つの端点、各端点での0〜1の範囲 of RGB値、およびExtendフラグを指定します。Extendを1に設定すると、軸の端点を超えて開始色と終了色が単色で拡張されます。これは、軸の外側にある領域の角が未描画のまま残らないようにするために、通常求められる動作です。0に設定すると、それらは処理されません。シェーダーが登録されたら、塗りつぶし領域にはSetFillShader、アウトラインのストロークにはSetLineShader、テキストにはSetTextShaderでバインドします。バインドは後続の描画呼び出しに対して有効に残り、次に描画するパスは単色ではなくグラデーションになります。
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
ここでの軸は垂直であり、固定されたxに対してy=100からy=260まで伸びるため、色の帯は水平に走り、矩形は底部が青色、上部が白色にフェードします。シェーダーは名前で管理されるため、1つの定義でページ上の任意の数の形状を塗りつぶすことができ、単色の塗りつぶしに戻すには、次のパスを描画する前に別のSetFillColorを呼び出すだけです。
パターンタイル処理:セルを繰り返す
グラデーションが単一の色を滑らかに変化させるのに対し、パターンタイル処理は小さなアートワークを領域全体に繰り返して配置します。ISO 32000-1 §8.7.3.1は、パターンタイル処理をパターンセル(独立したコンテンツの断片)として定義しており、レンダラーはペイントされる領域をタイル状に埋めるために固定のグリッド上にそれを複製します。これは、エンジニアリング用のハッチング、ヘッダーの背景にある繰り返しのブランドモチーフ、あるいは拡大してもベクターのシャープさを失わず、データ容量がほとんど増加しないテクスチャ背景などを構築する方法です。セルが一度だけ保存され、各地から参照されるためです。
PDFlibPasは、キャプチャされたページコンテンツからパターンセルを構築します。CapturePageでページまたは領域をキャプチャし、NewTilingPatternFromCapturedPage(PatternName, CaptureID)でキャプチャを名前付きパターンに変換し、SetFillTilingPattern(PatternName)でそのパターンを現在の塗りつぶしとして選択します。それ以降、塗りつぶすパスは単色ではなく、繰り返されるセルで描画されます。これはシェーダーと同様に機能しますが、タイル状のセルがペイントソースとなります。この手順は1回の呼び出しよりも複雑であるため、キャプチャ手順に馴染みがない場合は、パターンを2段階の操作として扱ってください。まずキャプチャされたセルを生成し、次にタイル状に描画したい領域を描画する前に名前で塗りつぶしにバインドします。
プリミティブの組み合わせ
各パーツは直接組み合わせることができます。塗りつぶされたベジェ形状は、DrawPathで描画される曲線のパスです。同じアウトラインにインナーループを追加してDrawPathEvenOddで描画すると、ワインディングフィルでは閉じられていた場所に穴が現れます。グラデーションで塗りつぶされた矩形は、シェーダーにバインドされたボックスです。以下の例では、これら3つを順番に描画し、2つの塗りつぶしルールの違いが1つのページ上で確認できるようにし、その下にグラデーションパネルを配置します。
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
2つのディテールを覚えておいてください。描画の呼び出しによって塗りつぶしルールが決定されるため、DrawPathとDrawPathEvenOddの選択はノンゼロワインディング規則と奇偶ルールの選択となり、穴のある形状の場合は奇偶ルールを使用することでサブパスの方向を考慮する手間が省けます。また、グラフィックス状態は描画を実行した瞬間にサンプリングされます。カラー、線幅、シェーダーバインドを描画呼び出しの前に設定してください。エンジンはその状態を読み取るためです。まず構築し、次に状態を設定し、最後に描画することで、ベクターモデルは常に予測通りに動作します。
ここから、自然な次のステップは既存のドキュメントからベクターとテキストを読み取ることであり、これについてはテキスト、画像、フォントの抽出に関する記事でカバーしています。また、同じ描画モデルを画面上のプレビューや印刷のためにWindowsのデバイスコンテキストにレンダリングすることについては、印刷およびプレビューのチュートリアルでカバーしています。ここで説明したパス、シェーダー、パターンの呼び出しは、テキスト、画像、フォーム、署名APIと並んで、Delphi PDF Libraryの一部として提供されています。