Technical Article

Delphi 中的 PDF 向量圖形:路徑與漸層

大多數接觸 PDF 的 Delphi 程式碼都將該格式視為兩種內容的容器:文字段和一些放置的點陣圖。這種看法在一定程度上是正確的,但它使該格式功能最強大的部分未被使用。PDF 頁面是一個與解析度無關的 2D 畫布,建立在與 PostScript 相同的成像模型上。它可以繪製線條、曲線、填滿區域、漸層和重複圖樣,所有這些都是向量,在任何縮放比例下都能保持清晰,並以裝置的完整解析度進行列印。如果您正在繪製標誌、圖表、浮水印或證書邊框,向量路徑幾乎總是正確的基元,並且它比許多程式所使用的點陣化影像更小、更清晰。

本文將介紹 ISO 32000-1 定義的向量模型,並展示相符的 PDFlibPas 呼叫。其目的是使規格具體化,因為 API 與其密切對應,且理解其中一個就能學會另一個。

頁面是個路徑機器

ISO 32000-1 §8.5 將圖形描述為兩個永不重疊的階段。首先,您建立一條路徑,這是純粹的幾何圖形,沒有可見的結果。然後,您在單個操作中繪製該路徑,對其輪廓進行筆劃、填滿其內部,或兩者兼施。在建立期間,頁面上不會出現任何內容。路徑是在繪圖狀態中保留的點和線段的抽象序列,直到繪製運算子使用它為止,此時它會被演算並丟棄。

路徑由一個或多個子路徑組成。子路徑從一個點開始,並透過附加線段來增長:直線、三次貝茲曲線,以及在某些平台上作為其自身封閉子路徑新增的整個矩形。在 PDFlibPas 中,您可以使用 StartPath 開啟路徑,這會設定起點,然後使用 AddLineToPathAddCurveToPath 將其延伸。每次呼叫都會推進一個隱含的目前點,因此下一個線段會從上一個線段結束的地方繼續。ClosePath 會繪製一條回到子路徑起點的最終直線線段,這對筆劃很重要,因為它會在關閉頂點處產生真正的線條接合,而不是兩個鬆散的端帽。

// 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,它接受兩個貝茲控制點和一個終點:AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY)。曲線從目前點執行到 (EndX, EndY),沿途被拉向兩個控制點。圓弧可透過 AddArcToPath(CenterX, CenterY, TotalAngle) 取得,其中半徑取自目前點與中心之間的距離,且引擎將圓弧發送為貝茲線段鏈。矩形有一個捷徑 AddBoxToPath(Left, Top, Width, Height),它會附加一個完整的封閉矩形作為其自身的子路徑,而不需要前置 StartPath

兩種填滿規則,以及它們為何不一致

當您填滿自我交叉或包含內部迴圈的路徑時,轉譯器需要一個規則來決定哪些區域在形狀內部,哪些是孔洞。ISO 32000-1 §8.5.3.3 定義了兩種規則,它們可以對相同的幾何圖形進行不同的繪製。非零環繞數規則計算從測試點發射到無限遠的射線的有號交叉點數,對於每個從左到右交叉的線段加一,對於每個以另一種方式交叉的線段減一;當總數不為零時,該點在內部。奇偶規則忽略方向並單純計算交叉點數,當計數為奇數時稱該點在內部。

它們分歧的典型例子是有孔的形狀,例如甜甜圈或墊圈。繪製一個外部邊界並在其內部繪製一個內部邊界。在奇偶規則下,內部迴圈總是會挖出一個孔,因為兩個邊界之間的任何點都會被交叉一次,而內部迴圈內部的任何點都會被交叉兩次。在非零環繞數規則下,只有當內部迴圈的環繞方向與外部迴圈相反時,才會出現孔洞;如果它們以相同的方式環繞,則環繞會增強而不是抵消,且內部區域會填滿實心。以單個自我相交輪廓繪製的五角星顯示出相同的分裂:奇偶規則使中央五邊形保持空白,而非零環繞數規則將其填滿。

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

軸向漸層沿著線條改變顏色

單色填滿在整個區域中是一個值。漸層連續改變顏色,最簡單的一種是軸向(或線性)漸層。ISO 32000-1 §8.7.4.5 將其指定為類型 2 軸向著色:您給出定義軸的兩個點,第一個點處的起點顏色和第二個點處的終點顏色,且轉譯器沿該軸插補顏色。填滿區域中的每個點都採用其垂直投影到軸上的顏色,因此漸層以與兩點之間的線條成直角的帶狀執行。

在 PDFlibPas 中,漸層是您建立一次然後選擇作為作用中繪製的具名檔案資源。NewRGBAxialShader 註冊它。特徵標記為 NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend):兩個軸端點、兩端的 RGB 三元組(值在 0 到 1 的範圍內)以及 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 to y=260,因此色帶水平執行,且矩形從底部的藍色褪色到頂部的白色。因為著色器是按名稱鍵值的,所以一個定義可以填滿頁面上的任何數量的形狀,而切換回單色只是下一個路徑之前的另一個 SetFillColor 呼叫。

並排圖樣重複一個單元格

漸層流暢地改變單個顏色,而並排圖樣在整個區域重複一小塊美工圖案。ISO 32000-1 §8.7.3.1 將並排圖樣定義為圖樣單元格(一個獨立的內容片段),轉譯器在固定的網格上複製該單元格,以並排方式填滿要繪製的區域。這就是您建立工程填滿的剖面線、頁首後面的重複品牌主題,或無論區域多大都保持向量清晰且幾乎不佔用任何空間的紋理背景的方式,因為單元格僅儲存一次並隨處引用。

PDFlibPas 從擷取的頁面內容建立圖樣單元格。您使用 CapturePage 擷取頁面或區域,使用 NewTilingPatternFromCapturedPage(PatternName, CaptureID) 將擷取的內容轉換為具名圖樣,然後使用 SetFillTilingPattern(PatternName) 選擇該圖樣作為目前填滿。從那時起,您填滿的任何路徑都會使用重複的單元格而不是單色繪製,這與著色器填滿的運作方式完全相同,但以並排的單元格作為繪製來源。此順序比單個呼叫更複雜,因此如果擷取步驟不熟悉,請將該圖樣視為兩階段操作:先產生擷取的單元格,然後在繪製您要並排的區域之前將其按名稱繫結為填滿。

將基元組合在一起

這些部分可直接組合。填滿的貝茲形狀是使用 DrawPath 繪製的曲線路徑。新增內部迴圈後使用 DrawPathEvenOdd 繪製的相同輪廓顯示了一個孔洞,而環繞填滿原本會將其關閉。漸層填滿的矩形是繫結到着色器的方框。下面的範例按順序繪製這三者,以便在一個頁面上可以看到這兩種填滿規則之間的差異,然後在它們下方放置一個漸層面板。

// 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);

有兩個細節值得記住。繪製呼叫決定了填滿規則,因此在 DrawPathDrawPathEvenOdd 之間的選擇就是非零環繞數和奇偶之間的選擇,對於有孔的形狀,奇偶規則免去了您推算子路徑方向的麻煩。而且繪圖狀態是在您繪製的瞬間進行取樣的:在繪製呼叫之前設定您的顏色、線條寬度和著色器繫結,因為這是引擎讀取的狀態。先建立,後設定狀態,最後繪製,這樣向量模型每次都能如預期般運作。

從這裡開始,很自然的下一步是從現有檔案中讀回向量和文字,這在我們關於文字、影像與字型擷取的文章中介紹,以及將相同的繪圖模型演算到 Windows 裝置內容以進行螢幕預覽和列印,這在列印與預覽逐步解說中介紹。此處介紹的路徑、著色器和圖樣呼叫作為 Delphi PDF 函式庫 的一部分出貨,同時也提供本部落格其他地方介紹的文字、影像、表單和簽名 API。