PDFium 作為檢視器引擎,即 Chrome 的 PDF 分頁背後的轉譯器而聞名,因此首先需要說明的是,PDFium 元件也可以建置以前從未存在過的文件;創作端封裝了 PDFium 的頁面物件 API:您建立一個空文件,新增具有明確尺寸的頁面,然後在您選擇的座標上將文字、向量路徑和影像放到每個頁面上;沒有需要學習的頁面描述語言,也沒有列印驅動程式參與;您呼叫方法,函式庫組合 PDF物件,最後 SaveAs 會將結果序列化
您所無法獲得的是排版引擎;這點非常重要需要先說明,因為它決定了以下每個範例的實作方式;PDFium 元件會將內容放置在您指示的位置,使用絕對座標,而不會放置在意想不到的角落;它不會將段落換行、不會使文字跨頁面中斷流動,也不會根據列與行計算出表格;這些都是您的工作;如果您期望能像文書處理軟體那樣自動重排散文,請現在調整您的預期:這是一個精確的低階放置 API,更接近在畫布上繪製圖形,而不是對文件進行排版;對於已產生且您已經知道每個元素所屬位置的發票、憑證、標籤和報告頁面,這種精確度正是您所需要的
產生檔案的最低要求
在空的 TPdf 和已儲存的 PDF 之間只需三個呼叫:建立文件、新增頁面、將其寫出;其他所有內容都是您在這之間分層加入的內容
uses
Vcl.Graphics, // for clBlack and TColor
PDFium; // TPdf lives here
procedure CreateBlankPdf(const FileName: string);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // empty in-memory document
Pdf.AddPage(0, 595, 842); // A4 portrait, in points
Pdf.AddText('First page', 'Arial', 18, 50, 780);
Pdf.SaveAs(FileName); // serialize to disk
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
看過舊程式碼片段的人容易混淆一個細節:在 CreateDocument 之後,您不需要指定 Pdf.Active := True;Active 屬性會回報文件控制代碼是否存在,而 CreateDocument 已經建立了一個,因此在呼叫傳回時該屬性即為 True;再次設定它充其量是不起作用,最壞的情況是會誤導下一位讀者;Active 在結束時發揮了作用:指定 False 會在 Free 之前釋放底層文件,這是乾淨的終止順序;請將 CreateDocument 與檔案載入的開啟視為互斥;該函式庫拒絕在已經開啟一個文件的 TPdf 上建立新文件,因此重複使用意味著必須先關閉當前文件
座標從左下角開始
AddText 的第二個參數組,以及每個放置呼叫的參數,都是 PDF 使用者空間中的一個點;原點位於頁面的左下角,X 軸向右延伸,Y 軸向上延伸;一個單位是一個點,即 1/72 英吋,因此 A4 頁面為 595 x 842 單位,而 US Letter 為 612 x 792;向上延伸的 Y 軸是「我的文字超出頁面」混淆最常見的來源,因為螢幕和點陣圖座標將原點設在頂部,Y 軸向下增長;在一個 842 點高的頁面上,靠近頂部的標題位於 Y 780 左右,而不是 Y 60;當文字落在意想不到的地方時,頁面高度減去您的 Y 軸值幾乎總是您實際想要的數值
AddPage 接受一個插入位置作為其第一個參數,以 1 為基準,並以 0 作為方便的「文件開始」簡寫;為第一頁傳遞 0 或 1,該頁面會插入到最前面;傳遞與您要附加的計數相符的值,以便在末尾新增頁面;新新增的頁面也會成為當前頁面,即後續繪製呼叫所針對的頁面,因此在新增頁面後沒有單獨的「選取此頁面」步驟;如果您新增了多個頁面,且稍後需要重新在較早的頁面上進行繪製,請設定 PageNumber 以移動游標;當您在建立頁面時依序填滿頁面時,可以不用去管它
寫入文字,以及會悄悄產生影響的字型規則
AddText 的簽章包含了單次寫入所需的一切:字串、字型名稱、以點為單位的大小、X 和 Y 錨點,以及選用的顏色、透明度 Alpha 位元組和以度為單位的旋轉角度
procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
// Title in black, default opacity, no rotation
Pdf.AddText(Title, 'Arial', 20, 50, 780);
// A lighter byline 24 points below it
Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
// A faint diagonal draft stamp across the page
Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;
Alpha 位元組從 $00(不可見)到 $FF(不透明),這使得草稿印記呈現浮水印的效果,而不是一個實心的區塊:$30 大約是百分之十九的不透明度,足以便於閱讀;角度會將文字圍繞其錨點逆時針旋轉,因此 45 度會呈現經典的對角線印記;這一切都不需要獨立的浮水印功能;浮水印只是一個大型的、半透明的、旋轉的 AddText呼叫,而在主體之前或之後繪製決定了它是在內容的後方還是在上方
字型非常需要注意,因為它的失敗模式是無聲的;當您傳遞一個字型名稱時,PDFium 元件會向作業系統要求該字型的 TrueType 資料,並將其內嵌在文件中,這就是為什麼在您的電腦上建置的檔案,在從未安裝過該字型的電腦上也能以完全相同的方式轉譯;問題在於當名稱無法解析時會發生什麼事:例如拼寫錯誤,或者建置電腦上根本不存在該字型;這並不會引發任何異常;函式庫會退而求其次地建立一個僅將名稱作為標籤的文字物件,而不內嵌任何內容,並讓檢視器自行替換它認為接近的字型;文字在您的測試中會出現且看起來合理,但在安裝了不同字型的其他地方開啟檔案的瞬間,其度量或字形就會發生偏移;請使用您知道在產生檔案的電腦上存在的字型名稱,將字型清單視為部署相依性,並在乾淨的系統上使用檢視器開啟樣本進行檢查,然後再信任輸出結果
向量圖形:建置路徑,然後提交
線條、矩形和填滿區域都透過路徑進行處理;您可以使用 CreatePath 開啟路徑,這會一次設定起始點和所有樣式、填滿模式、具有各自 Alpha 位元組的填滿和筆劃顏色、筆劃寬度、線條端點和連接方式;然後您可以使用 LineTo、BezierTo 和 ClosePath 來延伸它,最後 AddPath 會將完成的路徑提交到頁面上;提交步驟很容易被忘記,如果您跳過它,將不會產生任何內容
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
// A thin horizontal rule. The rectangle overload sets a box directly:
// X, Y, Width, Height, then fill mode and colors.
Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
True, clBlack, $FF, 1.0);
Pdf.AddPath;
end;
procedure DrawTriangle(Pdf: TPdf);
begin
// Point overload: start at the first vertex, line to the rest, close.
Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
Pdf.LineTo(300, 300);
Pdf.LineTo(250, 400);
Pdf.ClosePath;
Pdf.AddPath; // nothing is drawn until this runs
end;
兩個多載涵蓋了常見的情況;四座標形式接受 X、Y、寬度和高度,並在一次呼叫中提供軸對齊的矩形,這正是您用來繪製水平線、儲存格邊框或填滿的背景面板所需要的;雙座標形式僅設定一個起點,您需要自己使用 LineTo 和 BezierTo 來描繪其餘的輪廓;填滿模式控制著重疊區域的繪製方式:fmWinding(非零環繞)適用於大多數實心形狀,fmAlternate(奇偶)處理挖空和自我相交的輪廓,而 fmNone 則保留無填滿的僅描邊路徑,這正是上述分割線所使用的模式
表格:手動組合的路徑與文字
因為表格無現成元件可用,所以表格是一個迴圈;您決定資料欄的 X 位移和資料列高度,使用 AddText 寫入每個儲存格,並使用矩形路徑繪製格線;計算是由您負責,但這很簡單,而且一旦寫好,就可以推廣到您需要的任何格線上
procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
ColX: array[0..2] of Double = (0, 110, 210); // column offsets
RowH = 20;
var
Y: Double;
Row: Integer;
begin
// Header row
Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);
// Rule under the header
Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
Pdf.AddPath;
// Data rows, stepping Y downward each iteration
Y := Top;
for Row := 1 to 3 do
begin
Y := Y - RowH;
Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
end;
end;
注意 Y 軸在每次傳遞時都會依據資料列高度向下遞減,這同樣是因為向上為正;這也是缺乏文字測量所帶來的影響:沒有什麼能阻止較長的項目名稱溢出到下一欄,因為函式庫不知道您的字串轉譯出來有多寬;對於您控制資料的固定格式輸出,您可以寬裕地調整資料欄的大小然後繼續;對於真正多變的內容,您要麼限制輸入,要麼在放置字元之前自己測量其寬度,此時專用的排版函式庫就開始展現其價值了
影像與多個頁面
點陣內容是透過影像輔助程式匯入的;AddPicture 接受已載入的 TPicture 並將其放置在一個點上,並可選擇寬度和高度來對其進行縮放;AddImage 直接接受檔案路徑或 TBitmap,而 AddJpegImage 則直接以資料流傳輸 JPEG 位元組,而無需透過點陣圖進行多餘的轉換;與其他所有內容一樣,放置座標是影像在使用者空間中的左下角,而寬度和高度是頁面上的點(points)大小,而不是來源的像素尺寸
procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
Pdf: TPdf;
P: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
for P := 1 to PageCount do
begin
Pdf.AddPage(P, 595, 842); // append; the new page becomes current
Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
'Arial', 10, 50, 30); // footer near the bottom edge
// ... draw this page's body here ...
end;
Pdf.SaveAs(FileName);
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
多頁文件就是迴圈中的單頁模式;每個 AddPage 都會附加一個頁面並將其設為當前頁面,因此您接下來繪製的主體和頁尾就會落在您剛剛新增的頁面上;您不需要在該迴圈中重新指定 PageNumber,因為新增頁面已經將游標移到了該處;只有當您回到不按建立順序的頁面時,才需要 PageNumber;在最後一頁填滿後,在結尾呼叫一次 SaveAs 即可;如果您需要封存描述檔而不是單純的檔案,同一個文件物件會公開 SaveAsPdfA 和其他符合性變體,因此輸出標準的選擇是不同的儲存呼叫,而不是不同的建置路徑
適用場景
客觀的定位是,PDFium 元件的創作 API 是對 PDFium 頁面物件模型的一個忠實且輕量化的封裝:實現真實的文件建立、真實的內嵌字型、真實的向量和點陣內容,並序列化為符合標準的檔案;它不是、也不假裝是一個會自動重排的文件排版引擎;分界線在於文字的配置;如果您的輸出是樣板式的發票、憑證、標籤、在固定網格上呈現的儀表板,那麼絕對座標模型非常直接且快速,程式碼也能保持可讀性;如果您的輸出是必須自己換行和分頁的長篇散文,您將需要在這些呼叫之上面臨重建排版引擎的處境,這顯然不適合該項工作;了解您處於該分界線的哪一側,是決定採用它的關鍵
此處描述的建立方法是適用於 Delphi 的 PDFium 元件 的一部分,它將此創作路徑與 PDFium 更廣為人知的轉譯和文字擷取功能結合在一起