把「コード」逐字元沿頁面向下畫,每次呼叫比上一次低一個 em,五個字形裡有三個會出錯。兩個角括號以它們在橫排線上的坐姿落進欄裡,開口朝向側邊;長音符號 ー 是一道橫切過欄的扁平短橫線,而日文讀者期待的是一道短的垂直筆畫。只有コ與ド這兩個普通片假名,呈現出排版師會有的樣子
把整行旋轉九十度,多少算碰巧修好了括號與長音符號,卻弄壞其餘一切:每個漢字與假名都躺下了。直排書寫同時做三件事:字元保持直立、前進方向沿頁面向下而非橫向、少數幾個字元換用不同的畫法。三件事都做對,需要一套帶直排形式與直排度量的字型,以及一個設定好使用它們的 PDF 字型
在 PDF Library for Delphi 中開啟直排書寫
PDF Library for Delphi 把直排書寫當成一種繪製模式。SetVerticalWritingMode(1) 開啟它,之後每個 DrawText 呼叫都從你給的點向下執行;SetVerticalWritingMode(0) 換回橫排文字。setter 回傳呼叫前生效的模式,GetVerticalWritingMode 讀目前模式,所以輔助常式可以先切換模式、稍後把原樣放回去
兩個座標的意義與橫排文字略有不同。x 值指出直欄的中心線:每個字形以水平中點掛在這條線上,窄的標點與全寬漢字因此保持同一軸線對齊。y 值是第一個字元儲存格的頂端,直欄從那裡向下生長。用 SetOrigin(1) 把 y = 0 放在頁面上緣之後,這些算術讀起來就像你在紙上排版
var
Lib: TPDFlib;
H: Double;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.AddTrueTypeFont('MS Mincho', 1); // 1 = 嵌入字型面
Lib.SetTextSize(12);
Lib.SetVerticalWritingMode(1); // 一般的 DrawText 現在會向下書寫
H := Lib.GetVerticalTextHeight('MS Mincho', 12, '第三章 保守点検');
Lib.DrawText(480, 72, '第三章 保守点検'); // 直欄從 y = 72 開始
Lib.SetVerticalWritingMode(0); // 恢復為橫排
// Origin 1 把 y = 0 放在頁面頂端,所以 72 + H 是直欄的底端;
// 橫排說明文字落在它下方一行處
Lib.DrawText(456, 72 + H + 18, 'Chapter 3');
Lib.SaveToFile('manual-ja.pdf');
finally
Lib.Free;
end;
end;
GetVerticalTextHeight 吃字面名稱、大小與文字,回傳直欄的長度(點)。範例裡 72 + H 是直欄底端,英文說明放在它下方 18 點處,起點在直欄中心線左方 24 點,讓短的拉丁行大致落在直欄下方。函式無法以該名稱開啟字面時回傳 0,而以零高度為基礎的版面會把說明文字疊上直欄,所以用值之前先測它
為什麼括號在直欄裡側躺?
因為字型為這類字元保存了兩種畫法,而水平那種是預設。OpenType 字面把替代形式列在名為 vert 的 GSUB feature 裡,出貨時已為直排轉好字形的字面則用 vrt2。替換一行文字得到的是水平字形,之後得有東西套用直排 feature,一個字形換一個字形。角括號、圓括號與長音符號轉成沿欄方向;小假名,以及諸如「、」和「。」的標點,則移到其方框的另一個角落
PDF Library for Delphi 從它正在繪製的字面讀這個 feature,把每個字形換成字型指名的直排形式。字面兩個 feature 都有時,vrt2 優先、vert 被忽略。規範本意就是讓 vrt2 取代 vert,兩個都讀可能讓同一個字元拿到兩種不同直排字形,取決於哪個 lookup 先回答。兩個 feature 都沒有的字面保留普通字形。我們偏好這個結果甚於內建替代表:猜出來的表對常見字元是對的,卻會悄悄覆蓋那些設計師對某個字元有不同處理的字面
把 Windows 隨附的那幾份字型的 GSUB 表倒出來看,數量差距很大:MS Gothic 的直排 feature 映射 979 個字形,Yu Gothic 2,491 個,SimSun 72 個,Arial 一個都沒有(確切數字隨字型版本浮動)。碰巧含一些假名的拉丁字面,會畫出一個直立、但每個括號與長音符號都是水平形式的欄,因為字型裡沒有東西可以替換
替換也改變測量。直排形式可以有自己不同於被替換水平字形的前進量,所以 GetVerticalTextHeight 在累加前進量之前先做同一套替換。量水平形式、畫直排形式,欄會以每個括號幾點的速度超出你為它保留的框
這一切都不碰文字本身。從檢視器把直欄複製出來,你拿回「コード」,因為每個直排字形都映射回你傳入的字元。搜尋與文字抽取看到的是你的來源字串,永遠不是旋轉過的呈現形式
直排度量如何進到 PDF 裡
直排段落裡每個字元的前進距離,由字型自己的直排度量給定。用 PDF 檢查器打開輸出,你會找到一個帶 Identity-V 編碼的 Type0 字型,其 CIDFont 為每個用到的字形帶一個 W2 項:來自字面 vmtx 表的直排前進量、字形寬度一半作為到直欄中心線的偏移,以及直排原點。ISO 32000-1 §9.7.4.3 定義這三個數。沒有直排度量的字面就沒有 W2 陣列,DW2 預設值讓每個字元恰好前進一個 em
直排原點是第三方字型彼此分歧的地方。vhea 表有 ascender 與 descender 欄位,看起來像顯而易見的來源,但 descender 的正負號慣例並不通用。用任何字型表檢視器讀 Windows 隨附字型的這個欄位:MS Gothic 是 +140、Yu Gothic 是 +120,相對於 SimSun 的 −144 與 MingLiU 的 −500,而且數值會隨字型版本變動。從這個欄位衍生的任何位置,對每個字面漂移的量都不同。PDF Library for Delphi 從直排 ascender 取原點——上述 CJK 字面對它回報一致——字面回報不出合理值時,退回 880 單位,也就是 PDF 規範自己給 DW2 的預設
手工定位直排字形有自己的陷阱。橫排文字常用 TJ 陣列裡的數字調整間距;在直排書寫模式下,ISO 32000-1 §9.4.4 讓這些數字沿直欄作用,為橫排間距算出的修正會把每個字元往頁面下方再推。這就是為什麼內容串流裡的直排段落是一條沒有任何調整的純字形碼字串,所有間距都來自 W2
驗收輸出時,看墨水而不是字典。在字型裡找到 /Identity-V 只證明編碼有宣告;它對文字實際往哪個方向走隻字未提。把頁面轉譯出來、量畫出像素的邊界框才知道:七個字元的段落應該大約一個字元寬、七個字元高
橫排頁面上的一段直排文字
DrawVerticalText 畫一段直排文字,且不動書寫模式。它吃位置、字型名稱、大小與文字,所以目前的字型選擇也不受影響。書脊標籤、印章、表格旁的一欄姓名:這些是橫排文件裡的直排例外,為每一處反覆切換全域模式,狀態管理成本超過這件工作該有的
// 在一頁幾乎全橫排的頁面裡的一段直排文字
if Lib.DrawVerticalText(520, 96, 'MS Mincho', 14, '保守点検記録') = 0 then
begin
// 字面無法開啟,或無法設定這段文字。
// LogLayoutProblem 是你自己的常式,不是函式庫的一部分
LogLayoutProblem('vertical label on page ' + IntToStr(Lib.SelectedPage));
end;
// 周圍的橫排文字不受影響
Lib.DrawText(72, 96, 'Maintenance inspection record');
混合兩個方向的頁面,會把同一字面以兩種形式放進資源:一次橫排編碼、一次 Identity-V,兩者出自同一份字型程式。每個方向保留自己的度量,所以直排內文配橫排頁首的日文書頁、直排標題配橫排明細行的中文證書,兩個方向的間距都正確
回傳值是我們在以直排為主的文件裡也伸手用 DrawVerticalText 的理由。基於模式的繪製裡,無法直排的段落會改以普通橫排方式畫出而不是被丟棄,DrawText 兩種情況都回報成功。對內文這是更好的預設:游走的橫排行在打樣裡一眼可見,少掉的一段卻不容易察覺。方向就是重點的標籤,用 DrawVerticalText,把 0 當版面失敗
由右向左鋪排直欄
欄序由你決定。日文與繁體中文的直排文字按欄由右向左讀,所以頁面從右邊界起頭,每個新欄落在前一欄左側。任何書寫模式設定都無法從文字推斷這一點;右邊裝訂的書的頁序,以及振假名、腳註與圖說該在哪裡,也一樣
下面的常式把字串拆成符合固定高度的欄,邊走邊用 GetVerticalTextHeight 量測,每欄向左移一個半 em:
procedure DrawColumns(Lib: TPDFlib; const Face: WideString; Size: Double;
const Text: WideString; Right, Top, BoxH: Double);
var
Col, Trial: WideString;
I: Integer;
X: Double;
begin
// 建議的防護,不是函式庫檢查:高度為零代表字面
// 無法開啟,下面的迴圈永遠不會斷欄
if Lib.GetVerticalTextHeight(Face, Size, Text) <= 0 then
Exit;
X := Right; // 第一欄在右邊界
Col := '';
for I := 1 to Length(Text) do // 假設沒有代理對
begin
Trial := Col + Text[I];
if (Col <> '') and (Lib.GetVerticalTextHeight(Face, Size, Trial) > BoxH) then
begin
Lib.DrawVerticalText(X, Top, Face, Size, Col);
X := X - Size * 1.5; // 下一欄向左
Col := Text[I];
end
else
Col := Trial;
end;
if Col <> '' then
Lib.DrawVerticalText(X, Top, Face, Size, Col);
end;
每個字元都重新量測嘗試字串,比累加前進量慢,但它量到的正是即將畫出的東西,替換包含在內。這個迴圈沒做的是避頭尾(kinsoku):閉括號出現在欄頂、或開括號被留在欄底,對日文讀者都是排版錯誤;修法是在畫之前把一個字元拉過或推過斷點。代理對需要同樣的留心:用索引取 Text[I] 會把基本多文種平面以外的字元攔腰切成兩半,文字可能含這種字元時,請按碼位走
由資料組裝的文件,文字搜尋與頁面元素列舉 的逐步解說示範怎麼讀回每個頁面上落了什麼,是確認每個欄都帶著你想要的字元的快捷方式
直排文件該用哪套字型?
一套帶自身直排度量、且有 vert 或 vrt2 feature 的 CJK 字面。沒有它們,你得到的是直立字元一次前進一個 em、且毫無換形。直排文字看起來微妙地不對時,先檢查字型再檢查程式碼。一個能列出字面 GSUB feature 與 vhea、vmtx 表的表檢視器,一分鐘就能告訴你這個字面能不能勝任
DrawVerticalText 與 GetVerticalTextHeight 透過 Windows 按名稱開啟已安裝的字面。其他平台的 Lazarus 組建裡,直排書寫走 SetVerticalWritingMode 加 DrawText,以你載入並選取的字型程式為準;只要那套字型確實是你要的 CJK 字面,同樣的直排形式與度量照樣適用
內嵌遵循一般規則與一般成本。完整的 CJK 字面以數十 MB 計,所以任何要離開你機器的文件,子集化都是合理預設。PDF 檔案大小最佳化與字型子集化 的筆記說明可以預期的情況,把缺失字型內嵌進既有 PDF 的逐步解說涵蓋直排文件沒帶字型就送達的修復情況
CJK 直排書寫與 PDF Library for Delphi 其餘文字功能共用同一套繪製 API——一套適用 Delphi、C++Builder 與 Lazarus 的原生 Pascal PDF 函式庫。PDF Library for Delphi 產品頁 與之並列列出完整的字型與文字功能