將阿拉伯文句子 يوضح ملف PDF هذا 傳送給普通的 TextOut,傳回的頁面在兩個方面同時出錯。單字是從左到右而非從右到左延伸,且字母以孤立形式(isolated forms)分開,而不是連接成連貫的單字。沒有發生錯誤。Delphi 順利編譯,檔案可以開啟,但閱讀阿拉伯文的審查人員會告訴您輸出結果無法使用。修復方法是一次呼叫,而不是更換程式庫:HotPDF 透過一個單獨的方法 RtLTextOut 來路由從右到左的文字,該方法可以處理普通 TextOut 無法處理的重新排序。本頁面是該方法的實用參考:包括函式簽名及其參數、選擇指令碼的字元集(charset)引數、文件層級的副作用、必須置於首位的字型設定,以及實際到達支援人員的失效案例與其修復方法
Signature and parameters
procedure RtLTextOut(X, Y: Single; angle: Extended;
Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
Text: PWORD; TextLength: Integer); overload;
X 和 Y 座標以點為單位定位於頁面自身的座標系統,從左下角量測且 Y 軸向上增加,這與每個 TextOut 呼叫所使用的原點相同;RtLTextOut 只會改變字元順序,而不改變頁面的量測基準。angle 如同在 TextOut 中一樣旋轉基準線,因此 0 會繪製水平線。Text 是以邏輯順序表示的字串(即您輸入的順序),第二個多載接收相同的 UTF-16 資料作為原始 PWORD 緩衝區,並帶有明確的字碼單元(code-unit)計數,這適用於文字來自 API 而非 Delphi 字串的情況。在不支援這些類型多載解析的較舊 Delphi 版本上,此字串形式會以 RtLTextOutStr 為名提供,並帶有相同的參數清單
這兩個輸出呼叫之間的分工非常嚴格。TextOut 按照您傳入的順序繪製字碼點(codepoints),這對於拉丁文、西里爾文和中日韓文字是正確的,但對於阿拉伯文和希伯來文則是錯誤的。RtLTextOut 首先將每一行重新排序為視覺上的從右到左順序,然後進行繪製,同時保持行中嵌入的拉丁單字和數字從左到右閱讀。HotPDF 刻意將這兩個方法分開,而不是從字元中猜測方向,因此選擇呼叫哪一個就是選擇獲取哪種指令碼行為;對於從右到左的文字執行,請使用 RtLTextOut,其他情況使用 TextOut,切勿將一者路由至另一者。重新排序為什麼會存在、Unicode 雙向演算法和阿拉伯文語境連接實際上做些什麼,以及 HotPDF 的字形塑形在哪裡停止,是使用 HotPDF 進行阿拉伯文與 RTL 文字字形塑形的姊妹篇主題;以下所有內容均為實際設定

charset 引數決定指令碼
告訴 RtLTextOut 它是正在配置阿拉伯文還是希伯來文的不是該方法,而是字型。SetFont 接收一個 Windows 字元集(charset)作為其第四個引數,該值將指令碼規則帶入從右到左的呼叫中:178 選擇阿拉伯文,177 選擇希伯來文。設定好字元集,然後進行繪製,以下兩行將以正確的閱讀順序呈現,而不需要任何進一步的設定
// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');
// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');
一個容易被忽略的順序細節:SetFont 必須置於首位,且在每次 AddPage 之後必須重複,因為目前的字型(包括字元集)在分頁後無法保留。忘記重複呼叫,第二頁就會退回到當時作用中的任何字型,對於阿拉伯文而言,這通常意味著空白方塊
它不會反轉您已經手動反轉的文字
在這裡耗費最多偵錯時間的單一錯誤是向 RtLTextOut 傳送一個您已經手動翻轉的字串。人們是在首次嘗試使用普通的 TextOut 輸出結果相反之後才使用此方法的,常見的權宜之計是在繪製前於程式碼中反轉字元。RtLTextOut 自身會在內部進行反轉,因此預先反轉的字串會被反轉第二次,結果又回到了起點。請以邏輯順序(即您鍵入並大聲朗讀它的順序)傳遞文字,並讓此呼叫執行重新排序
這個陷阱比單純的翻轉更為棘手,因為對於一個全阿拉伯文的測試詞組,雙重反轉的字串看起來可能是正確的,但一旦行中包含拉丁單字或數字,它就會立即出錯。在從右到左的行中,這些嵌入的文字段應該自左向右閱讀,而手動反轉破壞了這種巢狀結構,而純阿拉伯文的情況恰好能存活。因此,該 Bug 順利通過了您的第一次冒煙測試(smoke test),並在稍後出現在包含帳號的真實發票上。在您切換到 RtLTextOut 的那一刻起,請清除所有的手動反轉
值得瞭解的 Direction 副作用
呼叫 RtLTextOut 不僅僅會改變您繪製的那一行。它還會將文件的閱讀方向偏好切換為從右到左,這與您自己透過 Direction 屬性設定的效果相同。該設定器將 vpDirection 加入到文件的 ViewerPreferences 中,這會告訴檢視器如何排列雙頁跨頁,以及對開頁版面配置從哪一側開始。當整個文件都是阿拉伯文或希伯來文時,這正是您想要的,且您是免費獲得的
這正是值得瞭解的原因,開它在單個頁面上是看不見的。如果文件大多是自左向右且帶有一個從右到左的區塊,第一次 RtLTextOut 呼叫仍會傾斜整個檔案的偏好,而您的單頁打樣中不會顯示任何跡象。此症狀會在數週後出現(當有人列印雙面小冊子時,跨頁結果會呈鏡像顯示)。如果這不是您想要的,請在從右到左的文字執行後明確地將 Direction 設定回來:
// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;
對於真正從右到左閱讀的文件,請保留它。關鍵是要知道該呼叫具有文件範圍的影響,這樣就不會發生雙面小冊子跨頁鏡像的意外
註冊您出貨隨附的字型,而非您希望使用者安裝的字型
如果字型沒有字形(glyphs)可繪製,重新排序就沒有任何意義。經典的失效是報表在開發人員的電腦上完美轉譯(因為那裡剛好有 Arial Unicode MS),但在客戶的伺服器上卻呈現為一排排空白方塊(因為 Windows 默默替換了完全不支援阿拉伯文或希伯來文的字型)。解決之道是不要再信任已安裝的系統字型,而是註冊一個您隨應用程式出貨的字型
// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');
註冊還伴隨著兩個界限。透過 RegisterUnicodeTTF 載入的字型會被內嵌,且 HotPDF 的內嵌 Unicode 處理需要 PDF 1.5 或更新版本的文件;這只有在下游某些環節堅持使用 PDF 1.4 時才會產生影響,但當它發生時,失效是默默進行的。另一個限制是法律而非技術上的:TrueType 檔案帶有內嵌權限位元,而在螢幕上看起來很好的字型,其授權方式可能禁止將其隨附在客戶文件中。請在內嵌之前確認授權,而不是在收到投訴之後
完整的主控台範例
將這些部分組合在一起,這裡有一個獨立的程式,它寫入一個包含阿拉伯文行、希伯來文行和帶有拉丁產品名稱混合行的頁面。每個區塊都設定了其字元集,然後以邏輯順序進行繪製
program RtLTextOutDemo;
{$APPTYPE CONSOLE}
uses
HPDFDoc; // HotPDF main unit
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'RtLTextOut.pdf';
Pdf.BeginDoc;
// A Latin heading goes through the ordinary TextOut path
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');
// Arabic: charset 178, logical order, RtLTextOut does the reordering
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 720, 0,
'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');
// Hebrew: charset 177
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 680, 0,
'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');
// Mixed line: the embedded Latin word still reads left to right
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 640, 0,
'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');
Pdf.EndDoc;
Writeln('Wrote RtLTextOut.pdf');
finally
Pdf.Free;
end;
end.
執行它並開啟結果。阿拉伯文和希伯來文行自右向左閱讀,字母在指令碼連接的地方進行連接,且在最後一行中,Token HotPDF 在阿拉伯文段落中自左向右放置。儘管首次審查的人員通常會將其歸檔為 Bug,但這種巢狀結構是正確的雙向結果,而不是 Bug;上面連結的字形塑形文章解釋了為什麼 Unicode 規則要求這樣做,以及如何撰寫您的驗收標準以使該報告永遠不會被提交
常見錯誤及其修復方法
每個失敗下方都有真實的支援討論軌跡,每個都可追溯到上述各節:
- 輸出結果相反或在混合行中顯得雜亂。字串在呼叫前被手動反轉,這通常是
TextOut嘗試時遺留的權宜之計。請刪除所有的手動反轉並傳遞邏輯順序;RtLTextOut會在內部進行反轉 - 字母以孤立形式分開列印。文字使用了普通的
TextOut,或者是呼叫SetFont時沒有指定從右到左的字元集。請使用RtLTextOut進行繪製,並傳入 178(阿拉伯文)或 177(希伯來文)作為SetFont的第四個引數 - 客戶的電腦上顯示空白方塊。Windows 替換了完全不支援阿拉伯文或希伯來文的字型。不要再命名已安裝的字型;請註冊一個您出貨隨附的字型(使用
RegisterUnicodeTTF),並使用該名稱對其進行SetFont - 第二頁以錯誤的字型轉譯。目前的字型無法跨
AddPage保留。請在每次分頁後重複SetFont呼叫(包括字元集) - 在大多為 LTR 的文件中,雙面跨頁呈鏡像列印。第一次
RtLTextOut呼叫產生了將文件Direction翻轉的副作用。請在從右到左的文字執行後將Pdf.Direction := LeftToRight設定回來 - 內嵌 Unicode 文字在下游默默退化。管線中的某些環節強制使用 PDF 1.4,而 HotPDF 的內嵌 Unicode 處理需要 1.5 或更新版本。請提升文件版本或移除下游限制
在格式出貨前,請進行目視檢查之外的驗證:從檢視器中將文字複製出來、執行文件內搜尋、在沒有您開發字型的電腦上開啟檔案,並將一份真實的文件放在母語讀者面前。完整的驗證檢查清單、每個指令碼的覆蓋地圖以及值得建立的測試字串庫,均位於使用 HotPDF 進行阿拉伯文與 RTL 文字字形塑形的姊妹篇中
這裡顯示的 RtLTextOut、SetFont 和 RegisterUnicodeTTF 呼叫均屬於適用於 Delphi 和 C++Builder 的 HotPDF 元件的一部分