PDFコンテンツストリームの例から写したフッター罫線は72 72 m 540 72 l Sと読めます。(72, 72)へ移動し、(540, 72)へ線を引き、ストロークする。素のPDFではこの線はUS Letterページの下端から1インチ上に座ります。同じ4つの数字を左上原点で測る描画APIに渡すと、罫線は上端から1インチ下、レターヘッドを真っ直ぐに貫く位置に着地します。線自体は正常です。変わったのは参照フレームだけで、792ポイントのページで2つのフレームを行き来するのは1回の減算、792引くYです
HotPDFページのY = 0はどこにあるのか
HotPDFのページ描画呼び出しは原点をページの左上に置き、Xは右へ、Yは下へ伸びます。VCLのTCanvasと同じフレームです。ISO 32000-1 §8.3.2.3で定義されるPDFユーザー空間は左下起点でYは上向きに伸び、HotPDFはコンテンツストリームを書くときにあなたが渡したすべてのYを変換します。だからTHotPDF.CurrentPage上のY := 72は、ページサイズが何であれ、上端から1インチ下を意味します
もう片方の端にはページサイズが効いてきます。612 × 792ポイントのUS Letterページでは、Y := 720は下端から1インチ上です。新しいHotPDFページは、指定しない限りA4で、A4は高さ842ポイントなので、同じY := 720では下に122ポイント、およそ1.7インチが残ります。792から引いて配置したフッターがA4で上にずれるのは、まさにこのためです。高さはハードコードせずCurrentPage.Heightから読むか、BeginDocの直後にCurrentPage.Sizeを明示的に設定してください
単位はポイント、つまり1/72インチです。THotPDF.Resolutionの既定値が72だからです。Resolutionを上げると、座標とサイズは1/Resolutionインチ単位で数えられます。ベクター作業では72のままにしてください。線幅、ダッシュ長、座標がすべて、PDFファイルが保存するのと同じ単位で読め、コンテンツストリームで見た値がソースコードの値と一致します
テキストも同じモデルに従います。既定ではTextOutに与えるYはテキストの上端なので、ラベルとその周りの箱を1つのグリッドで計画できます。いくつかの呼び出しは描画面ではなくPDFファイルに対して働き、下端原点でYが上向きのPDF座標を使います。SetCropBox(Left, Bottom, Right, Top)のようなページボックスのセッターと、読み込み済み文書に刻印するStampLoadedPageText / StampLoadedPageImageファミリーです。これらをパス呼び出しと混ぜることが、レイアウトがひっくり返る原因の大半なので、別のルーチンに分けてください
パスを組み立て、塗って仕上げる
パスはまず組み立てられ、次に塗られます。MoveToは何も印を付けずに始点を設定し、各LineToは直線セグメントを追加します。そしてStrokeのような描画呼び出しが、現在のストロークカラーと線幅でページに届けるまで、何もページには現れません。PDFの用語では、StrokeはS演算子を出し、すべての描画演算子は現在のパスを終わらせるので、次のMoveToはきれいな状態から始まります
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'DrawPaths.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.Size := Letter; // 612 x 792 pt。デフォルトはA4
// 幅と色はページ状態。1度設定すれば、以後のすべてのStrokeが使います
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// 上端から1インチ下の罫線。左右に72 ptのマージン
Pdf.CurrentPage.MoveTo(72, 72);
Pdf.CurrentPage.LineTo(540, 72);
Pdf.CurrentPage.Stroke;
// 冒頭のフッター罫線。下端から1インチ上
Pdf.CurrentPage.MoveTo(72, Pdf.CurrentPage.Height - 72);
Pdf.CurrentPage.LineTo(540, Pdf.CurrentPage.Height - 72);
Pdf.CurrentPage.Stroke;
// 1つのパスに連結された3セグメント。太く青く
Pdf.CurrentPage.SetLineWidth(3);
Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
Pdf.CurrentPage.MoveTo(72, 152);
Pdf.CurrentPage.LineTo(172, 102);
Pdf.CurrentPage.LineTo(272, 172);
Pdf.CurrentPage.LineTo(372, 112);
Pdf.CurrentPage.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
線幅は引数ではなく状態です。SetLineWidthはw演算子を書き、ページ上のその後のすべてのストロークが、変更するまでその幅を使います。折れ線が3ポイントで出て2本の罫線が1.5のままでいるのはこのためです。一度も設定しなければ、PDFの既定値1.0が適用されます(ISO 32000-1 §8.4.3.2)。描かれない図形の原因はもっと地味です。描画呼び出しなしに置かれたジオメトリは、決してレンダリングされません
座標と幅はSingleなので、72.25も0.5ポイントの極細線もファイルまで生き残ります。ゼロが特殊ケースです。仕様は幅0を、出力デバイスが描ける最細の線と定義します。画面ではどんなズームでも1ピクセル、600 dpiプリンタでは1/600インチ。同じ罫線がビューアでは太く見え、紙の上ではほぼ消えます。極細線には0.25や0.5のような実幅を選べば、線はどのデバイスでも太さを保ちます
閉じた三角のはずの一隅に、なぜ欠けがあるのか
3つのLineToが始点に戻る三角は、見た目は閉じていますが、PDFでは依然として開いたサブパスです。ISO 32000-1 §8.5.3.2はラインジョインをサブパスの内側でセグメントが交わる場所だけに適用し、自由な2つの端にはラインキャップが付きます。既定のバットキャップと6ポイントのペンでは、他の2つの角がきれいなマイターを得るのに対し、始点の角に四角い食い込みが現れます。ClosePath(h演算子)は最後のセグメントを追加し、レンダラにそれを最初のセグメントとジョインさせます
Pdf.CurrentPage.SetLineWidth(6);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// 閉じて見えて閉じていない。バットキャップが(72, 500)で出会い、欠けを残す
Pdf.CurrentPage.MoveTo(72, 500);
Pdf.CurrentPage.LineTo(132, 420);
Pdf.CurrentPage.LineTo(192, 500);
Pdf.CurrentPage.LineTo(72, 500);
Pdf.CurrentPage.Stroke;
// 閉じたサブパス。3つの角すべてが同じマイタージョインを得る
Pdf.CurrentPage.MoveTo(232, 500);
Pdf.CurrentPage.LineTo(292, 420);
Pdf.CurrentPage.LineTo(352, 500);
Pdf.CurrentPage.ClosePath;
Pdf.CurrentPage.Stroke; // 1回の呼び出しならClosePathStroke
// 塗りにClosePathは不要。塗りはすべてのサブパスを暗黙に閉じます
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.MoveTo(392, 500);
Pdf.CurrentPage.LineTo(452, 420);
Pdf.CurrentPage.LineTo(512, 500);
Pdf.CurrentPage.Fill;
ストロークするつもりの多角形は、最後の点がすでに始点と一致していても、ClosePathかClosePathStrokeで閉じてください。1ポイントなら欠けは画面で見えません。6ポイントなら通常のズームで見え、印刷したチャート凡例ではレンダリング不良に読めます。塗る図形ではこの問題は起きず、パスが自己交差して偶奇規則を使いたいときのためにEoFillが用意されています
矩形、円、そしてTCanvasの癖
閉じたプリミティブはMoveToの帳簿づけを省きます。Rectangle(X, Y, Width, Height)は左上隅とサイズを受け取り、Circle(X, Y, Radius)は中心と半径を受け取り、どちらもFill、Stroke、FillAndStrokeで塗られます。フィルカラーとストロークカラーは独立です。SetRGBFillColorとSetRGBStrokeColorはそれぞれTColorを受け取るので、Delphiの定数もRGB(...)もそのまま使え、SetRGBColorは両方を一度に設定します
Rectangleの引数の形が罠です。TCanvas.Rectangleは(Left, Top, Right, Bottom)を受け取るので、長年のVCL描画コードは指に2つ目の隅を覚えさせています。HotPDFにRectangle(72, 202, 232, 292)を渡すと、160 × 90の代わりに幅232ポイント、高さ292の箱が得られます。手元の数字が本当に2つの隅なら、隅座標を受け取ってサイズを計算してくれるMFRectangle(X, Y, X1, Y1)を呼び、Rectangleはサイズで考えるコードに取っておいてください
PDFファイルには円の演算子がありません。Circleはいつもの0.5523制御点係数で4本の3次ベジエを書きます。ページのコンテンツストリームを伸長すれば分かります。1つのmの後に4つのc演算子が続く形です。Ellipse(X, Y, Width, Height)は、左上隅が(X, Y)のバウンディングボックスの内側で同じことをします
HotPDFは現在のページ用の標準TCanvasであるTHotPDF.Canvasも公開していて、GraphicデモのCanvasDrawプログラムは、ありふれたCanvas.Rectangle、Ellipse、RoundRectの呼び出しでLedgerページを埋めます。既存のVCL描画コードを移植するときにはこちらが理にかないます。新しい描画コードはCurrentPageに置くほうが得です。各呼び出しがポイント単位の1つのPDF演算子に対応し、出力は数字の粒度まで予測できるからです
4回のコミットで作るコールアウト箱
コールアウトは上のすべてを一度に使います。塗りと輪郭のパネル、ポインタ線、アンカー点、そして同じ左上座標でパネル内に置かれた2行のテキストです。FillAndStroke(B演算子)は1つのパスの内側と縁を1回の呼び出しで塗るので、矩形は2回ではなく1回組み立てればすみます
procedure DrawCallout(Page: THPDFPage);
begin
// 1. パネル。淡い塗りと濃い縁、1パス、1コミット
Page.SetRGBFillColor(RGB(255, 244, 214));
Page.SetRGBStrokeColor(RGB(180, 130, 40));
Page.SetLineWidth(1);
Page.Rectangle(90, 122, 240, 70); // 左上(90, 122)、240 x 70
Page.FillAndStroke;
// 2. パネルの左端からターゲットへのポインタ
Page.SetLineWidth(1.5);
Page.MoveTo(90, 177);
Page.LineTo(66, 246);
Page.Stroke;
// 3. ポインタの先端のアンカー点。縁の色で
Page.SetRGBFillColor(RGB(180, 130, 40));
Page.Circle(64, 250, 3);
Page.Fill;
// 4. ラベル。各Yはテキストの上端で、同じグリッドで測ります
Page.SetFont('Arial', [fsBold], 10);
Page.TextOut(102, 134, 0, 'Check this total');
Page.SetFont('Arial', [], 9);
Page.TextOut(102, 152, 0, 'Rounded to two decimals per line,');
Page.TextOut(102, 164, 0, 'then summed');
end;
// BeginDoc と EndDoc の間で呼びます
// DrawCallout(Pdf.CurrentPage);
各ブロックは、必要とする図形の直前で、自分のフィルカラー、ストロークカラー、幅を設定します。直前のブロックが同じ値を残していてもです。ファイルには数個の冗長な演算子が増えます。引き換えに、どのブロックも、先行する何かから迷子の赤い塗りを受け継ぐことなく、移動も削除も独立したプロシージャ化もできます。レポートの変化とともに描画コードが変わる世界では、このトレードは毎回取る価値があります
カラーにも同じ規律を。Random($FFFFFF)で塗りを種付けすると、デモページは賑やかになり、生成レポートは実行間で比較できなくなります。同一データからの2つのビルドが違うバイトを生むからです。色はデータから、あるいはCanvasDrawデモが8エントリのCanvasColorsテーブルでやっているように固定パレット配列から駆動すれば、同じ入力は同じファイルを生みます
ダッシュ、曲線、透過
SetDashは線幅と同じくページ状態です。SetDash([3, 3], 0)はその後のすべてのストロークを3点オン3点オフにし、第2引数はパターンの開始位置をずらします。NoDashが実線のペンへ戻します。ダッシュ配列はarray of Byteなので、長さは1〜255の整数ポイントで、与えられたとおりにコンテンツストリームへ書かれます。あるグリッド線グループのために設定したダッシュは、その後のすべての罫線に居座ります。だからダッシュ区間をGStateSaveとGStateRestoreで囲んでください。これらはqとQを出し、保存状態の復元が前のダッシュと幅と色を1ステップで取り戻します
曲線は同じパスを伸ばします。CurveToC(X1, Y1, X2, Y2, X3, Y3)は現在点から(X3, Y3)への3次ベジエを2つの制御点で追加し、CurveToVとCurveToYは、最初か2番目の制御点が端点と一致するケースを担当します。1つのパスは、1回のStrokeやFillの前にLineToとCurveToCのセグメントを混ぜられます。角の丸めと滑らかにされたチャート線はこう描かれます
透過は名前付きグラフィックス状態に住みます。PDFはアルファを色ではなくExtGState辞書に保存するからです。THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha)は0〜1の値を受け取り名前を返し、CurrentPage.SetGraphicsState(Name)はその後の描画すべてに適用します。各不透明度を文書ごとに1度登録して名前を再利用すれば、ファイルはいくつの図形が使っても不透明度ごとに1つの辞書しか運びません。アルファにはPDF 1.4が必要で、HotPDFは必要なら出力バージョンを1.4へ引き上げます。StrictVersionLockが設定されているときはバージョンに手を付けず、空の名前を返します。PDFAComplianceがPDF/A-1プロファイルを選ぶと、1未満のアルファは例外になります。ISO 19005-1 §6.4が透過を禁じるからです。PDF/A-2とPDF/A-3のプロファイルは受け付けます
var
Half: AnsiString;
begin
// 文書ごとに1度登録し、返ってきた名前をどのページでも再利用します
Half := Pdf.RegisterExtGState(0.5, 1.0); // フィル50%、ストロークは不透明
Pdf.CurrentPage.GStateSave; // q
Pdf.CurrentPage.SetDash([3, 3], 0);
Pdf.CurrentPage.SetLineWidth(0.5);
Pdf.CurrentPage.SetRGBStrokeColor(clGray);
Pdf.CurrentPage.MoveTo(72, 400);
Pdf.CurrentPage.LineTo(540, 400);
Pdf.CurrentPage.Stroke;
Pdf.CurrentPage.SetGraphicsState(Half);
Pdf.CurrentPage.SetRGBFillColor(clYellow);
Pdf.CurrentPage.Rectangle(72, 380, 200, 16); // テキスト行の上のハイライト
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.GStateRestore; // Q。実線のペン、完全な不透明度へ戻る
end;
ベクター呼び出しは、プログラムが計算するジオメトリに向いています。表の罫線、チャートの軸と棒、コールアウト、1ダースのパスで組んだロゴ。写真やスクリーンショットはAddImageとShowImageの管轄です。ピクセルをパスでなぞってもファイルが太るだけです。出力を確認するときは、太いストロークの角とダッシュ線で1600%にズームしてください。閉じていないパスの欠けも、区間から漏れたダッシュも、この倍率では一目瞭然で、100%では見逃しやすいのです
この記事のすべての呼び出しは、HotPDF Delphi Componentのページオブジェクト上にあり、DelphiとC++BuilderのVCLプロジェクトで動き、結果を素のPDFパス演算子として書き出します