技術記事

DelphiでのHotPDFキャンバス描画:ベクターパスとカラー

HotPDFは、現在のページ上にパスを構築し、それを描画するように要求することでベクターグラフィックスを描画します。その間にビットマップのステップはありません。MoveToLineToで描画した線は、コンテンツストリーム内でPDFのパス演算子として終了するため、真のベクターとして維持されます:50%のズームでも1600%のズームでも鮮明であり、ラスタライズされたバージョンのサイズの数分の一になります。図、テーブルの罫線、グラフの軸、およびフォームの装飾については、まさにこれが必要なものであり、その背後にあるAPIは一度座って学ぶことができるほど小さいです

描画面全体はTHotPDF.CurrentPageに存在します。BeginDocEndDocの間で、そのページオブジェクトに色と線幅を設定し、ジオメトリを配置し、それをコミットするためのペイント演算子を呼び出します。最もよく使用する4つのプリミティブは、任意のパスに対するMoveToLineTo、ボックスに対するRectangle、円盤に対するCircle、および2つのペイント演算子であるStrokeFillです

座標系は左下です

これが、VCLから来た誰もがつまずくポイントです。コントロールをペイントするTCanvasは、原点を左上隅に置き、Yが下に向かって成長します。PDFはその逆を行います。HotPDFはページの左下隅からポイント(1/72インチ)で測定し、上に移動するにつれてYが増加します。Y := 720のポイントは、高さが792ポイントであるUSレターページの上部近くに位置し、Y := 50は下部近くに位置します。最初の描画が垂直に反転して出力された場合、これが理由です:画面グラフィックスから移植されたコードは、間違った方向を想定し、下端から外れてしまいます

同じ規則がTextOutにも適用されるため、テキストと形状は、一度それを内面化すれば、1つのメンタルモデルを共有します。上部ではなく、各要素の底部がどこにあるかを決定してレイアウトを計画すれば、残りは自ずと決まります

パス:MoveTo、LineTo、Stroke

ストロークされたパスは、持ち上げられ、配置され、ドラッグされたペンです。MoveToはペンを持ち上げ、何もマークせずに始点を設定します。各LineToは、現在のパスを新しいポイントまで延長します。Strokeを呼び出すまで、ページには何も表示されません。これは、現在のストロークカラーと線幅を使用して蓄積されたパスを描画し、次のMoveToが新しく開始できるようにパスをクリアします

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

2つの詳細が実際のデバッグ時間を節約します。線幅は状態であり、引数ではありません:SetLineWidthはそれを1回設定し、その後のすべてのStrokeは、再度変更するまでその値を使用します。これが、上記のポリラインがルールよりも太い理由です。また、パスは各Strokeの後にリセットされるため、忘れたStrokeは、慎重に配置したジオメトリがまったくレンダリングされないことを意味します。形状が出力から欠落している場合、ペイント呼び出しは最初に見るべき場所です

座標はポイントであり、ポイントは小数です。MoveToLineToSingle値を受け入れるため、0.5ポイントのヘアラインや72.25の位置は合法で意味があり、最も近い整数単位に丸められることはありません。その精度は2つの反対の方向で重要です。約0.5を下回る線幅は、画面では消え、印刷すると再び現れるデバイス依存の可能な限り細い線としてレンダリングされる可能性があるため、表示されるルールには、デフォルトではなく意図的に設定した幅が必要です。もう一方の端では、テーブルの罫線とグリッド線を整数の座標にスナップすることで、隣接する線がわずかに異なるように丸められる密なグリッドが不均一に見えるのを防ぎます。最初にグリッドの間隔をポイント単位で決定し、レイアウトの残りの部分がそれを継承します

塗りつぶされた形状と色

閉じたプリミティブは、輪郭を描く代わりに塗りつぶすことができます。Rectangleは位置とサイズを受け取り、Circleは中心と半径を受け取ります。どちらも、現在の塗りつぶし色で内部をペイントするFill、または輪郭のみのStrokeでコミットされます。塗りつぶし色とストローク色は別々の状態であり、SetRGBFillColorSetRGBStrokeColorで設定され、どちらも単一のTColorを受け取ります。これは、Delphiのカラー定数とRGBヘルパーを直接再利用できることを意味します

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Rectangleの引数の形状に注意してください。それは位置プラスサイズ、X, Y, Width, Heightであり、2つの対角ではありません。Delphiの開発者が知っているTCanvas.Rectangle(Left, Top, Right, Bottom)を受け取るため、筋肉の記憶は、HotPDFが幅と高さを期待している場所に2番目のコーナーを手渡し、ボックスは間違ったサイズで出力されます。(X, Y)のペアは左下隅であり、ページの原点と一致しています。円の場合、(X, Y)は中心であり、3番目の引数はポイント単位の半径です

元のサンプルが間違っていた1つの色の選択

この例の古いバージョンでは、すべての形状にRandom($FFFFFF)で色をシードしていました。それは活気があるように見えますが、生成されたドキュメントにとっては間違った本能です。コードから構築するPDFは、通常、テストしたいものでもあります。ランダムな塗りつぶし色を使用すると、実行ごとに出力を比較することが不可能になります。既知の良好なファイルに対するバイトごとの差分は、本当の理由もなく毎回失敗します。明示的な色を選んでください。一連の形状全体で多様性を持たせたい場合は、同じ入力が常に同じファイルを生成するように、データまたは固定のパレット配列からそれを駆動します。アーティファクトがリリースパイプラインを移動するとき、決定論は新奇さよりも価値があります

プリミティブを組み合わせる:コールアウトボックス

各プリミティブは単独ではシンプルです。見返りが現れるのは、それらのいくつかがレポートが実際に必要とするものに合成されるときです。コールアウト、つまり図を指し示して説明する注釈付きのボックスは、これまでに説明したすべてのものを使用します。境界線付きの塗りつぶされた長方形、ストロークされたポインター線、ポインターをアンカーするドット、および形状が使用するのと同じ左下の座標を使用してボックス内に配置されたテキストです。FillAndStrokeは、長方形を2回構築する代わりに、1回のコミットで1つのパスの内部と輪郭をペイントすることで、ここでその場所を獲得します

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. The box: pale fill plus a visible border, one path, one commit.
    //    Rectangle is lower-left corner plus size, Y measured from the bottom
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pale amber panel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // darker rim
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. The pointer: one stroked segment from the box edge down
    //    toward the thing being annotated
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // left edge of the box
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. A filled dot anchors the pointer at its target
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. The label, positioned relative to the box's lower-left corner.
    //    Text and shapes share one coordinate system, so the offsets
    //    are plain arithmetic against (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

複合材がいかに状態管理を必要としないかに注目してください。塗りつぶし色、ストローク色、および線幅はそれぞれ、それらを使用する形状の直前に設定されます。そのため、描画の各ブロックは自己完結型の単位として読み取られ、隠された状態を引きずることなく、再注文したり、ヘルパーに抽出したりできます。これをアンカーポイントと文字列を受け取るプロシージャにラップすれば、40行のコストで再利用可能な図の注釈が得られます

ベクター描画が報われる場所とそうでない場所

ジオメトリが生成される場合は、これらのパスとシェイプの呼び出しに手を伸ばしてください。グラフのグリッド線とバー、請求書テーブルの罫線、図上のコールアウトボックス、パスの握りとして表現されたロゴマークなどです。長方形は数千のピクセルではなくいくつかの数値であるため、これらすべてはぼやけることなくスケーリングし、ファイルサイズにほとんど何も追加しません。裏側も正直です。実際に持っているのが写真やスクリーンショットである場合、代わりにAddImageShowImageを使用して画像として描画します。ビットマップをベクター呼び出しでトレースしても何も得られません。上記の直線セグメント、長方形、および円は、実際のレポート作成作業の大部分を担っており、開発者が次に尋ねる3つの改善点、つまり曲線、ダッシュパターン、および透明度は、同じページオブジェクト上にあります

曲線、ダッシュ、および透明度について簡単に

フリーフォーム曲線は、すでに持っているのと同じパスの仕組みを拡張します。CurveToC(X1, Y1, X2, Y2, X3, Y3)は、現在の点から(X3, Y3)までの3次ベジェセグメントを追加し、2つのコントロールポイントに向かって曲がります。省略形のバリアントであるCurveToVCurveToYは、1つのコントロールポイントがエンドポイントと一致するケースをカバーしています。パスは、単一のStrokeまたはFillがコミットする前に、LineToCurveToCセグメントを自由に混在させることができます。これが、丸みを帯びた角と滑らかなグラフの線が構築される方法です

破線ストロークは線幅とまったく同じ状態です。SetDash([3, 3], 0)は、その後のすべてのストロークを3ポイントオン、3ポイントオフのパターンに切り替えます。配列はオン/オフの実行長さをポイントで示し、2番目の引数はサイクルが開始するフェーズを指定します。NoDashはペンを実線に戻します。それを設定し、それを必要とするグリッド線をストロークし、次の実線のルールの前にそれをリセットしてください。そうしないと、ダッシュはそれに続くすべてのものに静かに感染します

透明度は、カラー引数ではなく名前付きグラフィックス状態を介して実行されます。なぜなら、PDFのアルファはグラフィックス状態ディクショナリのプロパティだからです。RegisterExtGStateを使用してドキュメントに登録し、0から1の間の塗りつぶしアルファとストロークアルファを渡し、CurrentPage.SetGraphicsStateでそれが返す名前を適用します。そのポイントからの塗りつぶしとストロークは、登録された不透明度でペイントされます。これはカラーセッターよりも重い儀式であり、ハイライトバーがテキストを隠すことなくその上に配置される必要がある最初の段階で価値があります

保持する価値のある残りの習慣は検証です。生成されたジオメトリは、マシンでは通過するが、顧客のマシンでは失敗する可能性があります。通常は、混入したテキストでのフォント置換や、保持されないページサイズの想定が原因です。完成したファイルをいくつかのズームレベルで開いてエッジがきれいに保たれていることを確認し、すべての形状が意図したマージンボックス内に収まっていることを確認します。決定論的な配色を使用すれば、そのチェックは目視で行うのではなく、参照PDFに対して自動化できます

ここに示すMoveToLineToStrokeFill、およびカラー呼び出しは、DelphiおよびC++Builder用のHotPDF Componentの一部です