技術文章

用 Delphi 在 PDF 中直排日文與中文

PDFlibPas 能把日文與中文文字由頁面上方往下繪製。SetVerticalWritingMode 開啟直排書寫,之後一般的 DrawText 呼叫就會向下進行,而 GetVerticalWritingMode 會回報目前狀態。在這項功能出現之前,要把文字排成直排,意味著必須手動擺放每個字元,並祈禱間距看起來對

直排書寫並不是把水平文字旋轉九十度。字元保持直立,前進方向由橫向變成縱向,而且有一些字元會完全改變形狀——這正是把一份讀起來很自然的文件,與一份被日文讀者一眼認出是機器產生的文件區分開來的部分

PDF 內部發生了什麼改變

用這種方式繪製的文字,會經由一個處於直排書寫模式、且帶有字型本身直排度量資訊的 Type0 字型輸出。這在兩個方向上都很重要。閱讀器會依照設計師所意圖的距離來推進每個字元,而不是用一個均勻的步進,所以這一欄會擁有這套字型當初為其繪製的節奏。而把文字複製出來時會得到原始字元,因為直排的文字串流仍然是真正的文字、帶有正確的對映,而不是一連串被定位過的字形

不帶自身直排度量資訊的字型,會以每個字元推進一個 em 的方式進行,這也是閱讀器在預設下會做的事。這個後援機制值得了解,因為這正是你會看到的差異:同一份文件用一套正規 CJK 字型渲染時正確,改用一套恰好包含一些假名的拉丁字型時,看起來就機械式地被拉開

var
  Lib: TPDFlib;
  H: Double;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('MS Mincho', 1);      // 1 = embed the face
    Lib.SetTextSize(12);
    Lib.SetVerticalWritingMode(1);            // ordinary DrawText now runs down
    H := Lib.GetVerticalTextHeight('MS Mincho', 12, '第三章 保守点検');
    Lib.DrawText(480, 72, '第三章 保守点検');
    Lib.SetVerticalWritingMode(0);            // back to horizontal
    Lib.DrawText(72, 72 + H, 'Chapter 3');
    Lib.SaveToFile('manual-ja.pdf');
  finally
    Lib.Free;
  end;
end;

為什麼括號在直排文字裡看起來不對?

因為一個括號有兩種形式,而其中只有一種屬於直欄。當文字往下流動時,括號、長音符號與小假名會以不同的方式繪製——一個圓括號會旋轉成沿著欄位方向、而非橫跨欄位,而長音符號會變成一條垂直的筆畫。把水平形式放進直欄裡,每一個都會側躺

PDFlibPas 會從字型本身的 vertical 特徵裡取用這些形式,所以每套字型提供的是其設計師所繪製的內容,而不是從字元猜測出來的替代。這個區別對正確性至關重要:一份猜測式的替代表對常見情況是對的、但對那些對某個字元有不同處理的字型是錯的,而一套沒有命名任何直排形式的字型,會完全照先前那樣繪製,而不是被強迫套用一張它從未要求的對照表

GetVerticalTextHeight 測量的是將要實際繪製的形式,所以即使一欄的字元會改變形狀,測量仍然正確。測量水平形式、卻繪製直排形式,是欄位超出框框幾個字元的典型原因

不改變模式繪製單一直排文字串

DrawVerticalText 會以直排方式繪製單一文字串,接受位置、字型名稱、尺寸與文字,並讓書寫模式保持不動。在一份橫排文件裡遇到直排的例外情況時——一條書脊標籤、一個戳記、一欄姓名——很適合用它,因為圍繞每次呼叫切換全域模式,對這項工作而言是多餘的狀態管理

同一套字型的水平形式與直排形式在內部保持分開,所以一個頁面可以同時帶著兩者,互不干擾。這正是讓混合頁面可行的原因:一頁帶有直排內文與橫排頁首的日文書籍頁面,或一份標題直排、明細行橫排的中文證書

// One vertical run inside an otherwise horizontal page
Lib.DrawVerticalText(520, 96, 'MS Mincho', 14, '保守点検記録');
// The horizontal text around it is unaffected
Lib.DrawText(72, 96, 'Maintenance inspection record');

在做任何事之前先把字型搞對

直排書寫完全取決於字型。一套帶有正確直排度量資訊與 vertical 特徵的 CJK 字型,不需額外工作就能產生正確輸出;缺乏這兩者的字型,則會產生直立字元、每個推進一個 em、且完全不換形的結果。如果直排文字看起來有些微妙的不對,先檢查字型,再檢查程式碼

內嵌遵循一貫的規則與一貫的成本。一套完整的 CJK 字型體積很大,所以對任何要外送的文件來說,子集化不是可選項——PDF 檔案大小最佳化與字型子集化的筆記涵蓋了可以預期的情況,而將缺失字型內嵌進既有 PDF的逐步解說,則涵蓋了一份直排文件送達時未帶字型的修復情況

直排文字仍然需要你做版面決定的部分

欄序。日文直排文字以欄為單位由右向左進行,所以一個雙欄頁面會從右緣開始,而沒有任何書寫模式設定能從文字推斷出這一點。對於整體由右向左閱讀的文件而言,頁面順序也是如此,而振假名、腳註與圖片標題的位置亦然

函式庫保證的是,每一段文字串都能被正確排定:正確的形式、正確的前進量、可被抽取的文字。這些文字串落在頁面的哪個位置,則是版面問題,而對於從資料組裝出來的文件,文字搜尋與頁面元素列舉的逐步解說,對事後驗證落在頁面上的內容是否就是你想要的,相當有用

PDFlibPas 是一套原生 Pascal PDF 函式庫,支援 Delphi、C++Builder 與 Lazarus,而 CJK 直排書寫是繪圖 API 的一部分,而非附加元件——文字與字型功能清單請見 PDFlibPas 產品頁