將阿拉伯片語 يوضح ملف PDF 傳遞給 TextOut 並開啟結果。字母走向錯誤的方向,而且每個字母都以其獨立形式存在,在下一個字母之前有明顯的間隙,就好像有人倒著輸入英文並在每個字元之間按下空白鍵一樣。沒有引發任何例外。沒有印出任何警告。輸出結果就是錯的,之所以錯,是因為阿拉伯文所依賴的兩個獨立轉換從未發生。知道這兩個轉換是什麼,以及哪個呼叫會執行它們,幾乎就是複雜文字 PDF 輸出的全部內容
HotPDF 是用於 Delphi 和 C++Builder 的原生 VCL PDF 元件,它透過一個獨立的呼叫為您完成由右至左的工作。它也在您承諾支援一個地區設定之前需要知道的幾個特定地方停下了腳步,因此誠實的邊界在結尾附近有其專屬的章節
為什麼正確的字串印出來還是錯的
Unicode 將文字保持在邏輯順序,即您輸入和朗讀的順序。渲染器必須以視覺順序放下字形。對於由左至右的文字,這些順序是一致的,沒有人會去思考它。對於阿拉伯文和希伯來文則不然,當單一行混合了方向時(例如帶有拉丁標記 "PDF" 的阿拉伯文句子,或用數字寫成的價格),Unicode 雙向演算法(Unicode Bidirectional Algorithm,UAX #9)會精確決定由左至右的片段如何嵌套在由右至左的行中。這是第一個轉換,即重新排序(reordering),跳過它會使該行翻轉
第二個是上下文塑形(contextual shaping)。阿拉伯字母的繪製方式取決於它在單字中的位置:字首、字中、字尾或單獨存在。字碼點(codepoint)始終保持不變;只有字形(glyph)會改變。一個將每個字碼點直接交給其預設字形的管道,會精確地產生開頭段落中所述的那種斷開的、獨立形式的輸出。希伯來文跳過這一步,因為它的字母不相連,但它仍然需要重新排序。阿拉伯文兩者都需要,這就是為什麼您用來測試的字串是阿拉伯文,而不是希伯來文
在桌面上,這些都不是您的問題。當 VCL 表單將阿拉伯文繪製到 TEdit 時,作業系統的文字堆疊會悄悄地重新排序和塑形它,這正是為什麼在螢幕上看起來完美的字串,在原生的 PDF 中會出現破損的原因。內容串流不儲存可編輯的文字。它儲存定位好的字形,因此無論是誰發出該串流,都會繼承過去由作業系統處理的塑形工作。RtLTextOut 就是拿回這項工作的呼叫
RtLTextOut 負責重新排序和連接
HotPDF 將拉丁文路徑和複雜文字路徑保留為兩種不同的方法。TextOut 按照您給定的順序印出您給定的內容。RtLTextOut 首先重新排序並執行上下文分析,然後印出。它套用哪種文字規則來自於 SetFont 的字元集(charset)引數:178 代表阿拉伯文,177 代表希伯來文
// Arabic: pass logical order; RtLTextOut reorders and joins
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF');
// Hebrew: reordering only, no contextual joining
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');
有一個錯誤在這裡吃掉的除錯時間比其他任何錯誤都多。RtLTextOut 會自己反轉字串,所以如果您傳遞了您已經手動反轉的文字(通常是早期嘗試使用普通 TextOut 留下的權宜之計),它會再次反轉,您就回到了原點。殘酷的部分是,雙重反轉的文字對於單一的全阿拉伯文測試字串可能看起來是正確的,然後在行包含拉丁單字或數字的那一刻就會崩潰,因為那些嵌入的片段不再遵循 UAX #9。永遠傳遞邏輯順序,讓呼叫來整理它
同樣的混合方向行為絆倒審閱者的次數多於它絆倒程式碼的次數。在由右至左的行中,數字和嵌入的拉丁單字仍然由左至右閱讀。一個沒有處理過雙向排版的人會看著渲染出來的發票,看到帳號相對於周圍的阿拉伯文以「錯誤」的方向閱讀,然後將其記為一個錯誤。這是符合規範的正確結果。在第一次母語人士審閱之前寫在驗收標準中的一個簡短註解,可以省去那趟來回溝通
字形涵蓋範圍甚至在塑形執行前就決定了
塑形從字型中挑選字形。如果該字型沒有承載它們,就沒有什麼好挑的。這是一個會浪費一個下午時間的部署失敗:報表在開發人員的機器上完美無瑕,那裡碰巧安裝了 Arial Unicode MS,而在客戶的伺服器上卻出來一排空心方塊,那裡的 Windows 替換了一些完全沒有阿拉伯文的字型。解決之道是停止信任特定機器擁有的任何字型,並註冊一個您與應用程式一起發布的字型
// Ship a known font instead of relying on installed system fonts
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12);
// Audit coverage for the codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628); // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);
隨之而來的是兩個邊界。透過 RegisterUnicodeTTF 註冊的字型會被嵌入,而 HotPDF 的嵌入式 Unicode 處理需要 PDF 1.5 或更新版本的檔案。只有當下游某些東西堅持使用 PDF 1.4 時,這才會造成影響,但當它發生時,症狀是無聲無息的。另一個邊界是合法的:TrueType 檔案帶有嵌入權限位元,而在螢幕上繪製精美的字體,其授權方式可能仍然禁止將其發布在客戶檔案中。在發布之前檢查,而不是在被投訴之後
第二個呼叫,GetUnicodeGlyphForCodepoint,是您的預警系統。在服務啟動時,走過您的資料實際使用的字碼點範圍,並記錄傳回的字形 ID。這樣,涵蓋範圍的差距就會在推出期間顯示為啟動日誌中的一行,而不是已經送達客戶手中的發票上的遺失字元
屬於 Unicode 但非由右至左的文字、CJK 字串、帶有堆疊變音符號的越南文、混合的歐洲文字,都走普通路徑。TextOut 接受一個 WideString,並透過註冊的字型繪製它,完全沒有雙向分析。在報表程式碼中將兩條路徑物理上分開是有好處的,一個常式處理 RTL 執行,另一個處理其他所有事情,這樣地區邏輯在呼叫端就可見,而不是隱藏在某人最終會忘記設定的旗標後面
閱讀順序屬於檔案,而不是字形
把每個字形都弄對,仍然有一件事沒做。ISO 32000-1 §12.2 定義了一個名為 /Direction 的檢視器偏好設定,說明檔案的整體閱讀順序。它不涉及任何字形。它所做的是告訴檢視器如何排列雙頁跨頁,對頁排版應該從哪一側開始,以及閱讀 UI 應該偏向哪個方向。這些都不會在單頁上顯示,這正是它被遺忘的原因
// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft; // adds vpDirection to ViewerPreferences
設定 Direction 就是全部的工作:屬性 setter 會將 vpDirection 加入到檔案的 ViewerPreferences 中,因此一行程式碼就能將該偏好設定帶入檔案中。失敗的模式是遺漏了它,這感覺無害,因為您盯著看的單頁校對檔無論哪種方式看起來都一樣。然後有人印了一本雙面小冊子,跨頁出來變成鏡像,原因就是幾週前少了一行程式碼
HotPDF 塑形的極限
一份誠實的極限地圖抵得上一個星期的評估。RtLTextOut 自行涵蓋了雙向重新排序和阿拉伯文上下文連接。它不會自動執行的是一般 OpenType 功能的應用。可選的連字(ligature)和類似的排版功能透過 GetSingleSubstituteGlyph(GID, 'liga') 進行,它一次解析一個單一替換,首先是字形 ID,其次是功能標籤,當功能不適用時,會原封不動地傳回輸入的字形。這足以驅動一個由您自己維護的、已知的、有限的連字清清單。它不是一個完整的 GSUB 引擎。對於需要更多(而不僅僅是重新排序和連接)的文字,以具有重新排序母音符號的印度文字為標準範例,在您承諾該地區設定之前,請在真實的客戶字串上執行真正的試驗。阿拉伯文能運作並不能證明天城文(Devanagari)也能
進行端到端的驗證,因為頁面可能看起來正確,但對下游的一切仍然毫無用處。三項檢查可以發現大多數問題。將文字從 Acrobat 複製出來,並將字碼點與您的來源字串進行比較。執行檢視器中在檔案內搜尋您能在頁面上看到的一個單字。並且在沒有您開發字型的機器上開啟輸出,這最有可能暴露出替換。這些都無法取代母語人士查看一份真實檔案,這能抓住任何合成語料庫都無法抓住的問題。在格式發布之前,請將該審查排入日程
有目的地挑選測試字串,而不是回收翻譯人員去年發送的任何內容。每個地區設定可行的最低標準:一個純文字句子、一個嵌入拉丁品牌名稱的句子、一行帶有數字和貨幣的文字,以及帶有變音符號或組合標記的名稱。真實的客戶名稱會打破佔位文字(filler text)原封不動的假設,因此每當支援案例出現您從未見過的模式時,就讓迴歸測試集增加一個字串
字型註冊、子集化(subsetting)和日常的文字繪圖 API 在 關於使用 HotPDF 進行報表輸出、字型和圖片的文章 中有所涵蓋。當相同的檔案也必須符合無障礙設定檔時,PDF/A 和 PDF/UA 驗證文章 中的語言標記和結構規則就建立在此處的塑形工作之上
上述的由右至左和 Unicode 字型 API 隨附於 Delphi 和 C++Builder 適用的 HotPDF Component 中;產品頁面連結了完整的文字輸出參考