技術文章

Delphi 中的 HotPDF TextOut:大小、樣式、旋轉及間距

HotPDF 文件中的每個可見字串都是透過一次呼叫來實現的:TextOut(X, Y, angle, Text)。Hello World 範例以最簡單的方式使用它,字型僅設定一次,四個引數皆保留為合理的預設值。在第一頁之後,這相同的四個引數承擔了版面配置的全部工作。第三個引數用於旋轉文字。在它之前設定的字型決定了大小和樣式。而以點為單位、從頁面邊角量測的 X, Y 座標對,是區隔乾淨報表與重疊、裁剪或在別人的印表機上漂移到更下一行文字的唯一要素。這正是 TextOut 的價值所在,也是預設值限制不夠的地方

在進行其他操作之前,值得先記住函式簽名(signature):XY 是以點為單位的 Singleangle 是以度為單位的 Extended,而 TextWideString,因此 Unicode 不需要單獨呼叫即可傳遞。第二個多載(overload)接收 PWORD 加上長度,適用於您已經擁有字形代碼(glyph codes)的情況,但對於普通字串,您需要使用的是 WideString 形式

大小和樣式來自 SetFont,而非 TextOut

TextOut 沒有大小參數(parameter)。大小、字寬(weight)、傾斜度(slant),所有這些都存在於文字執行(run)之前的 SetFont 呼叫中,並且在下一個 SetFont 取代它之前一直有效。這一個事實解釋了大多數人第一天遇到的困惑:某一行字呈現粗體,是因為在三次呼叫之前有些代碼設定了 [fsBold] 且沒有任何代碼清除它

Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
Pdf.CurrentPage.TextOut(72, 740, 0, 'Quarterly Report');        // 24pt regular

Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
Pdf.CurrentPage.TextOut(72, 712, 0, 'Revenue');                 // 12pt bold

Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
Pdf.CurrentPage.TextOut(72, 694, 0, 'figures in thousands');    // 11pt italic

Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsItalic], 10);
Pdf.CurrentPage.TextOut(72, 676, 0, '  +18.4% YoY');            // styles combine

第二個引數是一個 TFontStyles 集合,因此 [fsBold, fsItalic] 是粗斜體,而 [] 是標準體。大小以點為單位,與座標的單位相同,這使得垂直間距易於推理:12 點的行大約需要 14 到 16 點的垂直間隔來呼吸,因此每行將 Y 降低 14 是一個合理的起始行距(leading)。這裡沒有自動換行。您需要自己計算每個基準線,這對於段落來說很繁瑣,但對於表單來說很精確,因為每個欄位都位於固定的座標上

關於字型名稱的兩點實用說明。它會針對建置電腦上安裝的字型進行解析,且作業系統傳回的字型就是被內嵌的字型,因此在您的桌上型電腦解析的名稱與在建置伺服器上解析的名稱不能保證是同一個字型。此外,字型必須覆蓋字串中的語系。在僅限拉丁語系的字型下,一行西里爾文(Cyrillic)或中日韓(CJK)文字會呈現為遺失字形的方塊且不報錯,這就是為什麼 Hello World 頁面在混合多種語言時會選用支援廣泛的 Unicode 字型的原因

HotPDF TextOut 頁面顯示在數個字元集下以標準、粗體和斜體樣式呈現的 Arial、Times New Roman 和 Courier New

angle 引數圍繞錨點進行旋轉

第三個引數是大多數程式碼永遠保留為零的引數。傳入一個非零值,文字執行將圍繞其自身的 (X, Y) 錨點(文字的左下角)逆時針旋轉相應的度數。錨點本身不會移動,因此放置水平標籤的相同座標會放置其旋轉後的孿生標籤;只有字形延伸的方向改變了

Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);

// A vertical axis label down the left margin: 90 degrees reads bottom-to-top.
Pdf.CurrentPage.TextOut(40, 300, 90, 'Units sold');

// A diagonal DRAFT watermark across the page body.
Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');

// Column headers tilted 60 degrees so long labels fit a narrow table.
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');

九十度是常見的例子,例如沿著圖表邊緣延伸的標籤或書脊標題。四十五度適用於傾斜的資料欄標頭,這個技巧可以讓較寬的標籤置於較窄的資料欄上方而不會溢出到相鄰的欄位中。旋轉不會改變對錨點的解讀方式,這常讓人感到困惑:旋轉 90 度的文字執行仍從 (X, Y) 開始並向上延伸,因此要將旋轉後的標籤置中,您需要調整錨點而非角度。當多個旋轉的文字執行共用同一個基準線時,請給予它們相同的 Y 並以 X 為步長,就像您在繪製堆疊水平線時以 Y 為步長一樣

精確放置座標而不需要猜測

座標是順利通過審核或默默出錯的部分。HotPDF 從頁面的左下角開始量測,Y 軸向上延伸,單位為點(每英吋 72 點)。US Letter 頁面為 612 x 792 點;A4 為 595 x 842 點。因此,Letter 頁面上的一英吋頂部邊距會將您的第一個基準線放在 Y = 792 減去 72 再減去字型大小附近,而不是放在頂部附近的某個較小數字。任何習慣於螢幕座標(Y 軸從零開始向下延伸)的人,都會把第一行寫在底邊以外,並花十分鐘納悶字跑到哪裡去了

請將版面配置視為針對命名錨點的算術運算,而不是一列魔術數字(magic numbers)。左邊距、每行遞減的動態基準線以及固定的行距,可以將一個標籤區塊變成一個簡短的迴圈,而不是一大堆字面常數(literals):

const
  LeftMargin = 72;        // 1 inch in
  TopBaseline = 720;       // first line, ~1 inch down on Letter
  Leading = 16;            // vertical step between lines
var
  Y: Single;
  Line: string;
begin
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Y := TopBaseline;
  for Line in ReportLines do
  begin
    Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Line);
    Y := Y - Leading;
    if Y < 72 then            // bottom margin reached
    begin
      Pdf.AddPage;
      Pdf.CurrentPage.SetFont('Arial', [], 11);  // font resets on a new page
      Y := TopBaseline;
    end;
  end;
end;

分頁防護是每個人最先忘記的一行,也是對應用領域影響最深的地方。TextOut 底下沒有流式版面配置(flow layout)。一旦遞減超過底邊距,文字就會繼續繪製到裝訂邊、超出頁面、進入無處,且沒有任何警告。因此您必須自己注意 Y,並在它低於底限時呼叫 AddPage,然後重設基準線。AddPage 之後的 SetFont 不是可有可無的點綴:目前字型無法跨頁保留,如果您跳過它,新頁面上的第一行文字將會以檢視器的預設字型呈現

用於合適度與對齊的字元和單字間距

有時字串是正確的,但寬度不對:必須橫跨固定線條的標題、需要以更寬鬆數字顯示的代碼,或者需要微調其數值以進行對齊的資料欄。PDF 為此攜帶了兩個文字狀態運算子,即字元間距(character spacing,Tc,在每個字形後添加額外空間)和單字間距(word spacing,Tw,在每個空格字元處添加額外空間),兩者都以未縮放的文字空間單位表示(實際上是在目前字型大小下的點數)。它們是狀態,而非 TextOut 的引數,因此您需要設定它們、繪製,然後將它們設定回去

// Letter-space a short heading so it stretches across a rule.
Pdf.CurrentPage.SetCharacterSpacing(4);
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(72, 740, 0, 'S U M M A R Y');
Pdf.CurrentPage.SetCharacterSpacing(0);   // reset before normal body text

// Open up the gaps between words on a single wide line.
Pdf.CurrentPage.SetWordSpacing(6);
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 712, 0, 'Name        Department        Extension');
Pdf.CurrentPage.SetWordSpacing(0);

單字間距僅作用於空格字元(代碼 32),這有一個值得瞭解的後果:它在沒有 ASCII 空格的中日韓(CJK)文字執行中不起作用,並且它與編碼為字形索引而非位元組的文字互動方式很奇特。對於拉丁表格輸出,這是加寬間距而無需重新鍵入字串的廉價方法。字元間距是必須達到目標寬度的標題的更好工具,因為它將調整均勻地分散在每個字形上,而不是將其集中在空格處

重設是整個流程的關鍵。間距與字型一樣,都是頁面繪圖狀態的一部分,且狀態會一直保留到您變更它為止。如果為一個標題設定了字元間距卻忘記將其歸零,底下的每個段落都會繼承這種延伸,這在閱讀時會有一種難以察覺、難以定位的違和感,往往能透過隨機抽查卻通不過仔細的校對。可靠的習慣是設定一個間距值,繪製需要它的文字執行,然後在下一行將其設回零,這樣隨後的程式碼就無需知道先前的部分做了什麼

HotPDF TextOut 頁面比較水平文字縮放、字元間距、單字間距,以及填滿與筆劃轉譯模式

在實際容易出錯的地方檢查輸出

文字版面配置往往在第二台電腦上失效,而不是在第一台,因此重要的檢查發生在您辦公桌之外的地方。在未安裝開發人員字型集的系統上開啟產生的檔案,並一次性確認內嵌字型仍能正常轉譯(包括帶有重音的拉丁字母、任何非拉丁語系的指令碼及標點符號),而不是只隨機抽查容易處理的字元。選取並複製幾行,以確認文字是真正的文字而非外框,這在搜尋或擷取範圍內非常重要。在版面配置中填入具有代表性的資料(例如最長的德文標籤和最寬的數字,而不是整潔的佔位符),因為超出欄位的文字往往是您沒有手動輸入的那一個。而且,如果頁面必須落在預先印好的表單上,請列印或點陣化一個樣本並與原始表單對照;四分之一毫米的基準線漂移在螢幕上是看不見的,但在紙上卻很明顯

如果您還沒有撰寫過任何一頁,請從 HotPDF Hello World 範例開始,它設定了本機文件、字型以及上述一切所依賴的左下角座標系統。這裡展示的 TextOutSetFont 和間距呼叫均屬於適用於 Delphi 和 C++Builder 的 HotPDF 元件的一部分