照規格的觀點寫出的第一行文字,會落在 y = 770。紙上推理無懈可擊:ISO 32000-1 把 user space 的原點放在左下角,A4 頁高 842 點,所以 770 正好是頂邊下方一英吋。拿這個程式去跑一座改從頂邊量測的元件,問候語就落在底邊上方約一英吋處,上面整頁空空蕩蕩。再加第二頁,精心挑選的字型又變回 12 點的 Arial。兩個意外都源自每套 PDF 程式在第一頁就要做的同兩個決定:目前字型是哪一個,數字從哪個角落數起
Delphi 裡最小的 HotPDF 程式
HotPDF 版的 Hello World 是一支主控台程式,四個呼叫按固定順序出場:設定文件屬性、BeginDoc、用 SetFont 與 TextOut 繪製,然後 EndDoc。其餘都是普通的 Delphi。程式如下,它寫出一頁以十一種語言向讀者問候的內容:
program HelloWorld;
{$APPTYPE CONSOLE}
uses
Winapi.Windows, // DEFAULT_CHARSET;XE2 之前的 Delphi 請改用原生的 Windows
HPDFDoc; // HotPDF 主 unit
procedure CreateHelloWorld(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
// 文件層級設定:全部都得在 BeginDoc 之前
Pdf.FileName := FileName;
Pdf.AutoLaunch := False; // 預設 True:EndDoc 之後開啟檢視器
Pdf.Compression := cmFlateDecode; // 預設是 cmNone
Pdf.FontEmbedding := True; // 本來就是預設值,寫出來給讀者看
Pdf.EnableFontSubsetting := True; // 只內嵌實際畫到的字符
Pdf.BeginDoc; // 建立第 1 頁(A4)
// 第 4 個引數預設是 ANSI_CHARSET;明確傳入 DEFAULT_CHARSET
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
// X、Y 以點為單位、從左上角起算;Y 是該行的上緣
Pdf.CurrentPage.TextOut(72, 72, 0, 'Hello, Delphi PDF world!'); // English
Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!'); // Spanish
Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!'); // German
Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!'); // French
Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!'); // Italian
Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!'); // Portuguese
Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // Russian
Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!'); // Japanese
Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!'); // Turkish
Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界'); // Chinese
Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!'); // Korean
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
begin
CreateHelloWorld('HelloWorld.pdf');
Writeln('Wrote HelloWorld.pdf');
end.

THotPDF.Create(nil) 配上 try/finally ... Free 是慣用的 Delphi 擁有權模式,產生 PDF 也改變不了它。各行從 Y = 72 一路向下、間隔 30 點,對 14 點的文字來說行距舒適。跑起來之後,工作目錄裡出現 HelloWorld.pdf:一頁、壓縮過的內容、字型以子集內嵌
文件屬性該放在 BeginDoc 之前
BeginDoc 啟動文件並建立第一頁,所以任何影響整個檔案的屬性都得在它之前指派。HotPDF 對部分屬性是強制執行這一點的,而不是只靠慣例。BeginDoc 之後才指派 Compression,setter 會丟出訊息為 Cannot set Compression value - document in progress. 的例外;AutoLaunch 同樣被看守。這裡吵一點是好事。把所有文件設定集中放在 BeginDoc 上方的一個區塊裡,那個訊息您永遠不會看到
有兩個預設值值得再看一眼。Compression 初始是 cmNone,從不碰它的程式寫出的內容串流就不壓縮;對一頁文字這只是浪費,對幾百頁的報告,這是任何人第一眼就注意到檔案大小的原因。AutoLaunch 初始是 True,意思是主控台工具在 EndDoc 回傳的那一刻,就用關聯的檢視器打開完成的 PDF。在自己的桌面上很愜意,放進服務、排程工作或建置代理程式裡就錯了。那些環境請在程式碼裡設 False;改不了程式碼的 CI 執行,把環境變數 HOTPDF_SUPPRESS_AUTO_LAUNCH 設為 1 也能關掉自動開啟
字型處理是我們明確表態的地方。FontEmbedding 預設 True,字型跟著檔案走,在從未裝過 Arial Unicode MS 的機器上照樣渲染。EnableFontSubsetting 則預設關閉,為的是讓編輯工作流拿得到完整字型。對產生出來的問候語、發票或報告,請打開子集化。Arial Unicode MS 涵蓋的文種範圍極廣,為了十一行短句內嵌整個字型,會讓檔案大上幾 MB 而看不到任何好處。只有當下游有人要編輯文字、需要您沒畫到的字符時,才留它關閉
HotPDF 頁面的 (0, 0) 在哪裡?
HotPDF 的頁面座標從左上角量起,Y 向下遞增,跟 VCL TCanvas 同一套框架。Resolution 為預設 72 時,一個單位就是一點,即 1/72 英吋。PDF 檔案本身仍存由下而上的座標,這是 ISO 32000-1 §8.3.2.3 的要求;HotPDF 寫出內容串流時,會把您傳入的每個 Y 用頁高減去它來完成轉換
開場範例背後的算術很短。BeginDoc 與 AddPage 建立的預設頁面是 A4,595 乘 842 點。想在頂邊下方一英吋放一行文字,HotPDF 的說法是 Y = 72,原始 PDF 的說法是 y = 770,因為 842 − 72 = 770。把 770 傳給 TextOut,文字就從頂邊往下 770 點,即底邊上方 72 點
同一個呼叫裡還有第二個、小一點的陷阱。預設情況下,您傳的 Y 是文字的上緣,HotPDF 會先往下移一個文字高度再放基準線。從 GDI 移植過來的程式碼通常正指望如此。從另一套 PDF 函式庫、或從您自己的內容串流寫入器移植過來的程式碼,通常預期 Y 是基準線,於是每一行都比計畫低了一個文字高度。THPDFPage.TextFromBaseLine(True) 把目前頁面切換成基準線定位:
// 給來自由下而上來源座標的橋接(PDF 版面、
// 規格圖、另一套函式庫)。本文示例用的輔助函式,
// 非 HotPDF 一部分。在預設 Resolution 72 下有效
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
Result := Page.Height - PdfY;
end;
procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
Pdf.CurrentPage.SetFont('Arial', [], 12);
// Y 現在指的是基準線,跟 PDF 自家文字運算元擺放字符的方式一致
Pdf.CurrentPage.TextFromBaseLine(True);
// 基準線在底邊上方 770 點,即從頂邊算 Y = 72
Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
'Baseline one inch below the top edge');
end;
Resolution 是另一個改變數字意義的設定。把 THotPDF.Resolution 設成 96,X/Y 座標、邊界與 TextWidth 的結果都改用 1/96 英吋表示,版面是按螢幕像素設計時很方便。不過頁面的 Width 與 Height 仍回報點數,所以混用頁面邊界與繪圖座標的版面程式碼,應該改讀 CurrentPage.UserWidth 與 CurrentPage.UserHeight,它們跟 X、Y 同單位。在 72 下兩組數字一致。若您想以公釐思考,MeasurementUnit 屬性(預設 muPoints,另有 muMM、muInches、muCentimeters、muTwips 與 muPixels)是更乾淨的開關。一套程式選一種約定,絕不疊加
為什麼第二頁印成 12 點 Arial?
因為每個新頁面都從已知狀態出發。THotPDF.AddPage 把新頁設為目前頁面、設成 A4,並對它呼叫 SetFont('Arial', [], 12),無論 Resolution 為何都保持 12 點。BeginDoc 建第一頁走的是同一條路,所以 Hello World 是在 BeginDoc 之後、而不是之前設字型。對多頁輸出的影響很容易被忽略:您在第一頁呼叫的 SetFont 不會延續。第二頁的拉丁文字在 Arial 裡仍然好看,只是變小。CJK 行則立刻穿幫,因為 Arial 沒有那些字符,檢視器只能顯示空框或什麼都不顯示
可預期的起始字型對頁面物件是正確的設計:第五頁的繪圖程式碼不必默默依賴第四頁恰好留下的東西。代價是頁面設定必須重複,而最乾淨的重複方式,是讓每個頁面(包括第一頁)都經過同一個程序:
uses
Winapi.Windows, Vcl.Graphics, HPDFDoc;
// 您自己的輔助函式:一個知道新頁面需要什麼的地方
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
if not IsFirst then
Pdf.AddPage; // 把字型重設為 Arial 12
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;
procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
TopMargin = 72;
Leading = 30;
LinesPerPage = 20;
var
I, Row: Integer;
begin
PreparePage(Pdf, True); // BeginDoc 之後第 1 頁已存在
Row := 0;
for I := 0 to High(Lines) do
begin
if Row = LinesPerPage then
begin
PreparePage(Pdf, False);
Row := 0;
end;
Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
Inc(Row);
end;
// 粗體標題要自己的 SetFont,之後再把內文字型設回來
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;
如果您的頁面也用 TextFromBaseLine 切換基準線定位,把那個呼叫也放進 PreparePage,緊挨著字型設定。每頁設定集中在一個程序裡,意味著新需求、新顏色或新尺寸只加一次,就到達每一頁
一套字型撐多種文種,以及它的極限
單一 SetFont 能在同一頁承載拉丁、西里爾、日文與中文,因為 Arial Unicode MS 把極廣文種的字符打包進同一副字型。SetFont 的第四個引數是 Windows 字元集,預設為 ANSI_CHARSET,這會把字型選擇綁在西歐字碼頁上。傳 DEFAULT_CHARSET(值 1)讓 Windows 改按名稱挑字型,多語頁面要的正是這個
字型必須存在於建 PDF 的那台機器上。HotPDF 內嵌的是 Windows 把名稱解析到的任何字型,Arial Unicode MS 不在時,Windows 二話不說換上替代字型。輸出於是內嵌了替代品,它蓋不住的文種就輸出成空框。組建伺服器是最常出事的地方,因為沒人在那裡裝 Office 字型。新機器產出的第一個檔案,先檢查它的內嵌字型清單,再交給批次運行去信任
當沒有任何一副字型蓋得住所有字串,TextOutWithFallback 接受與 TextOut 相同的引數,為每個字元解析字型,各段用能渲染它的字型繪製,事後再還原您原本的字型。阿拉伯文、希伯來文這類由右至左的文種,需要的比字符涵蓋更多:還要成形與反向的 run 順序,那屬於RtLTextOut 函式的領地,通常搭配透過 RegisterUnicodeTTF 載入的 TrueType 檔
接下來自然的下一步,是文字的大小、樣式與旋轉,以及在同一頁上刻意擺放多個區塊,TextOut 範例對這兩件事都有完整演練。線條、方框與圖片用的是同一個 CurrentPage 物件、同一套左上角座標,所以前文關於原點與每頁設定的一切,對它們原樣適用
等這個 Hello World 用您自己的字型乾淨跑通之後,HotPDF Delphi Component 頁面涵蓋其餘的繪圖 API,從圖形、圖片到表單,面向 Delphi 與 C++Builder