技術文章

Delphi 中的 HotPDF 畫布繪圖:向量路徑與色彩

HotPDF 透過在目前頁面上建立路徑,然後要求對其進行繪製來繪製向量圖形。這中間沒有點陣圖步驟。您使用 MoveToLineTo 繪製的一條線,最後會成為內容流中的 PDF 路徑運算符,因此它保持為真正的向量:在 50% 縮放時清晰,在 1600% 時清晰,且大小只有光柵化版本的一小部分。對於圖表、表格線條、圖表軸和表單裝飾來說,這正是您想要的,而且其背後的 API 夠小,一次就能學會

整個繪圖表面位於 THotPDF.CurrentPage 上。在 BeginDocEndDoc 之間,您可以在該頁面物件上設定顏色和線條寬度、放下幾何形狀,並呼叫繪圖運算符來提交它。您最常使用的四個基本元素是針對任意路徑的 MoveToLineTo、針對方塊的 Rectangle、針對圓盤的 Circle,以及 StrokeFill 這兩個繪圖運算符

座標系統為左下角

這是每一個來自 VCL 的人都會遇到困難的地方。您用來繪製控制項的 TCanvas 將原點置於左上角,且 Y 向下增加。PDF 恰恰相反。HotPDF 從頁面的左下角以點(1/72 英吋)進行測量,隨著您向上移動,Y 隨之增加。Y := 720 的點位於高度為 792 點的 US Letter 頁面頂部附近,而 Y := 50 位於底部附近。如果您的第一張圖形出來是垂直鏡像的,這就是原因:從螢幕圖形移植的程式碼假設了錯誤的方向,並超出了下邊緣

相同的慣例管理著 TextOut,因此一旦您內化它,文字和形狀就會共用一個思維模型。透過決定每個元素的底部(而不是頂部)所在的位置來規劃佈局,其餘的就會順理成章

路徑: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;

有兩個細節可以節省實際的除錯時間。線條寬度是一種狀態,而不是引數:SetLineWidth 設定一次,後續的每個 Stroke 都使用該值,直到您再次變更為止,這就是為什麼上面的折線比水平線還要粗的原因。而且路徑在每次 Stroke 後都會重設,因此如果忘記 Stroke,這意味著您精心佈局的幾何圖形根本無法渲染。如果輸出中缺少某個形狀,繪圖呼叫是您首先該看的地方

座標是點 (points),而點是帶有小數的。MoveToLineTo 接受 Single 值,因此 0.5 點的極細線或 72.25 的位置是合法且有意義的,不會被捨去為最接近的整數單位。這種精準度在兩個相反的方向上很重要。低於大約 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,而不是兩個相對的角落。Delphi 開發人員所熟知的 TCanvas.Rectangle 接受 (Left, Top, Right, Bottom),因此肌肉記憶會將第二個角落交給 HotPDF,而它預期的是寬度和高度,於是產生出來的方塊就會是錯誤的大小。(X, Y) 配對是左下角,與頁面原點一致。對於圓形,(X, Y) 是中心,而第三個引數是半徑點數

原始範例弄錯的一個顏色選擇

此範例的較舊版本在每個形狀上以 Random($FFFFFF) 設定顏色種子。它看起來很生動,但這對產生的文件來說是錯誤的直覺。您從程式碼建置的 PDF 通常也是您想要測試的對象,而隨機填滿顏色使得輸出無法在不同的執行之間進行比較:與已知正常檔案的逐位元組比較每次都會失敗,而且沒有實質的理由。選擇明確的顏色。當您想要在一系列形狀之間變化時,從您的資料或固定的色盤陣列來驅動它,這樣相同的輸入就能永遠產生相同的檔案。當構件通過發布管線時,確定性比新穎性更有價值

將基本圖形組合在一起:標註方塊

每個基本圖形本身都很簡單;當少數幾個基本圖形組合成報表實際需要的東西時,就會展現出回報。標註 (callout),即指向圖表並解釋它的註解方塊,使用了迄今為止涵蓋的所有內容:具有邊框的實心矩形、描邊的指標線條、用於錨定指標的圓點,以及使用形狀所用之相同左下角座標佈局於方塊內部的文字。FillAndStroke 在這裡贏得了一席之地,在一次提交中繪製單一物件的內部和輪廓,而不是建立矩形兩次

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 將其繪製為影像;使用向量呼叫來描摹點陣圖並不能為您帶來任何好處。上述的直線段、矩形和圓形承擔了絕大多數實際報表工作,開發人員接下來詢問的三個改進,即曲線、虛線圖案和透明度,都在同一個頁面物件上

簡述曲線、虛線與透明度

自由形式曲線延伸了您已有的相同路徑機制。CurveToC(X1, Y1, X2, Y2, X3, Y3) 從目前點附加一條三次貝茲 (Bezier) 曲線段到 (X3, Y3),朝向兩個控制點彎曲,簡寫變數 CurveToVCurveToY 涵蓋一個控制點與端點重合的情況。在單個 StrokeFill 提交之前,路徑可以自由混合 LineToCurveToC 區段,這就是圓角和滑順圖表線條的建置方式

虛線描邊是一種狀態,完全就像線條寬度一樣。SetDash([3, 3], 0) 將後續的每個描邊切換為畫三點、停三點的圖案,陣列以點拼寫出開啟/關閉的執行長度,第二個引數則調整循環開始的相位;NoDash 將筆觸返回為實線。設定它,描繪需要的網格線,然後在下一個實心線條之前重設它,否則虛線會悄悄地感染隨後的所有內容

透明度透過命名的圖形狀態運作,而不是顏色引數,因為 PDF 中的 Alpha 是圖形狀態字典的屬性。使用 RegisterExtGState 在文件上註冊一個,傳遞 0 到 1 之間的填滿 Alpha 和描邊 Alpha,然後套用它透過 CurrentPage.SetGraphicsState 傳回的名稱;從該點開始的填滿和描邊會以註冊的不透明度進行繪製。它是一個比顏色設定器更繁重的儀式,但當螢光強調列第一次必須覆蓋在文字上而不隱藏文字時,這一切都是值得的

剩下值得保持的習慣是驗證。產生的幾何圖形可以在您的機器上通過,但在客戶端失敗,通常是因為您混合的任何文字中的字型替換,或者是不成立的頁面大小假設。在幾個縮放等級下開啟完成的檔案以確認邊緣保持清晰,並檢查每個形狀是否都落在您預期的邊緣方塊內。透過確定性的顏色配置,您可以針對參考 PDF 自動進行此檢查,而不是用肉眼查看

此處顯示的 MoveToLineToStrokeFill 和色彩呼叫是適用於 Delphi 和 C++Builder 的 HotPDF 元件 的一部分