將阿拉伯文句子 يوضح ملف PDF هذا 傳送給普通的 TextOut,傳回的頁面在兩個方面同時出錯。單字是從左到右而非從右到左延伸,且字母以孤立形式(isolated forms)分開,而不是連接成連貫的單字。沒有發生錯誤。Delphi 順利編譯,檔案可以開啟,但閱讀阿拉伯文的審查人員會告訴您輸出結果無法使用。修復方法是一次呼叫,而不是更換程式庫:HotPDF 透過一個單獨的方法 RtLTextOut 來路由從右到左的文字,該方法可以處理普通 TextOut 無法處理的重新排序。本頁面是該方法的實用參考:包括函式簽名及其參數、選擇文字系統的字元集(charset)引數、文件層級的副作用、必須置於首位的字型設定,以及實際到達支援人員的失效案例與其修復方法
函式簽章與參數
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 只改變字形順序,不改變頁面的量測基準。X 也是繪出文字列的左端,而不是右緣:這個呼叫先把字形重排成視覺順序,再從 X 起由左至右排開,所以靠右對齊的一行,起點是右邊界減去行寬,行寬要在將繪製它的那次 SetFont 之後,用 CurrentPage.GetWideTextWidth 量測。angle 旋轉基準線的方式和在 TextOut 中完全一樣,所以 0 會畫出水平的一行。Text 是邏輯順序的字串,也就是您鍵入時的順序;第二個多載接收同樣的 UTF-16 資料,但改以原始 PWORD 緩衝區加上明確的字碼單元數傳入,文字來自 API 而不是 Delphi 字串時,就用這個形式。在還不支援這些型別多載解析的舊版 Delphi 上,字串形式改以 RtLTextOutStr 為名提供,參數清單完全相同
這兩個輸出呼叫的分工很嚴格。TextOut 依您傳入的順序繪製字碼點,對拉丁文、西里爾文和中日韓文字是對的,對阿拉伯文和希伯來文則是錯的。RtLTextOut 會先把每一行重排成視覺上的從右到左順序再繪製,同時讓行內嵌入的拉丁單字和數字維持由左至右閱讀。HotPDF 刻意把這兩個方法分開,而不是從字元去猜方向,所以呼叫哪一個,就決定了您得到哪一種文字系統的行為:從右到左的文字段用 RtLTextOut,其他一律用 TextOut,絕不要把其中一個繞道給另一個。重排為什麼存在、Unicode 雙向演算法與阿拉伯文的上下文連字實際上做了什麼,以及 HotPDF 的字形塑形如何處理它們,是姊妹篇使用 HotPDF 進行阿拉伯文與 RTL 文字字形塑形的主題;以下全是實務設定

charset 引數決定文字系統
告訴 RtLTextOut 它是正在配置阿拉伯文還是希伯來文的不是該方法,而是字型。SetFont 接收一個 Windows 字元集(charset)作為其第四個引數,該值將文字系統規則帶入從右到左的呼叫中:178 選擇阿拉伯文,177 選擇希伯來文。設定好字元集,然後進行繪製,以下兩行將以正確的閱讀順序呈現,而不需要任何進一步的設定
// 阿拉伯文:charset 178 告訴 RtLTextOut 套用阿拉伯文規則
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 100, 0, 'يوضح ملف PDF هذا');
// 希伯來文:charset 177 將規則切換為希伯來文
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 140, 0, 'קובץ PDF זה');
一個容易被忽略的順序細節:SetFont 必須置於首位,且在每次 AddPage 之後必須重複,因為目前的字型(包括字元集)在分頁後無法保留。忘記重複呼叫,第二頁就會退回到當時作用中的任何字型,對於阿拉伯文而言,這通常意味著空白方塊
它不會反轉您已經手動反轉的文字
在這裡耗費最多偵錯時間的單一錯誤是向 RtLTextOut 傳送一個您已經手動翻轉的字串。人們是在首次嘗試使用普通的 TextOut 輸出結果相反之後才使用此方法的,常見的權宜之計是在繪製前於程式碼中反轉字元。RtLTextOut 自身會在內部進行反轉,因此預先反轉的字串會被反轉第二次,結果又回到了起點。請以邏輯順序(即您鍵入並大聲朗讀它的順序)傳遞文字,並讓此呼叫執行重新排序
這個陷阱比單純的翻轉更為棘手,因為對於一個全阿拉伯文的測試詞組,雙重反轉的字串看起來可能是正確的,但一旦行中包含拉丁單字或數字,它就會立即出錯。在從右到左的行中,這些嵌入的文字段應該自左向右閱讀,而手動反轉破壞了這種巢狀結構,而純阿拉伯文的情況恰好能存活。因此,該 Bug 順利通過了您的第一次冒煙測試(smoke test),並在稍後出現在包含帳號的真實發票上。在您切換到 RtLTextOut 的那一刻起,請清除所有的手動反轉
值得瞭解的 Direction 副作用
呼叫 RtLTextOut 不僅僅會改變您繪製的那一行。它還會將文件的閱讀方向偏好切換為從右到左,這與您自己透過 Direction 屬性設定的效果相同。該設定器將 vpDirection 加入到文件的 ViewerPreferences 中,這會告訴檢視器如何排列雙頁跨頁,以及對開頁版面配置從哪一側開始。當整個文件都是阿拉伯文或希伯來文時,這正是您想要的,且您是免費獲得的
值得瞭解,正是因為它在單一頁面上看不出來。如果文件大多是由左至右,只有一個從右到左的區塊,第一次 RtLTextOut 呼叫仍會扳動整個檔案的偏好,而您的單頁打樣上看不出任何跡象。症狀會在幾週後才出現:有人印了一本雙面小冊子,跨頁卻是鏡像的。如果這不是您要的,請在從右到左的文字段之後,明確地把 Direction 設回來:
// RtLTextOut 已將文件方向設定為從右到左;
// 如果文件大部分是從左到右,請將其還原
Pdf.Direction := LeftToRight;
對於真正從右到左閱讀的文件,請保留它。關鍵是要知道該呼叫具有文件範圍的影響,這樣就不會發生雙面小冊子跨頁鏡像的意外
註冊您出貨隨附的字型,而非您希望使用者安裝的字型
如果字型沒有字形(glyphs)可繪製,重新排序就沒有任何意義。經典的失效是報表在開發人員的電腦上完美渲染(因為那裡剛好有 Arial Unicode MS),但在客戶的伺服器上卻呈現為一排排空白方塊(因為 Windows 默默替換了完全不支援阿拉伯文或希伯來文的字型)。解決之道是不要再信任已安裝的系統字型,而是註冊一個您隨應用程式出貨的字型
// 內附一個已知的阿拉伯文字型,並在繪製前註冊它
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 100, 0, 'يوضح ملف PDF هذا');
註冊還伴隨著兩個界限。透過 RegisterUnicodeTTF 載入的字型會被內嵌,且 HotPDF 的內嵌 Unicode 處理需要 PDF 1.5 或更新版本的文件;這只有在下游某些環節堅持使用 PDF 1.4 時才會產生影響,但當它發生時,失效是默默進行的。另一個限制是法律而非技術上的:TrueType 檔案帶有內嵌權限位元,而在螢幕上看起來很好的字型,其授權方式可能禁止將其隨附在客戶文件中。請在內嵌之前確認授權,而不是在收到投訴之後
完整的主控台範例
將這些部分組合在一起,這裡有一個獨立的程式,它寫入一個包含阿拉伯文行、希伯來文行和帶有拉丁產品名稱混合行的頁面。每個區塊都設定了其字元集,然後以邏輯順序進行繪製
program RtLTextOutDemo;
{$APPTYPE CONSOLE}
uses
HPDFDoc; // HotPDF 主單元
const
RightMargin = 545; // A4 寬 595 pt:50 pt 的右邊界
// RtLTextOut 會把重排後的文字列從 X 起由左至右排開,因此
// 靠右對齊的一行,起點是右邊界減去量測得的行寬
procedure RightAlignedRtL(Page: THPDFPage; Y: Single; const S: WideString);
begin
Page.RtLTextOut(RightMargin - Page.GetWideTextWidth(S), Y, 0, S);
end;
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'RtLTextOut.pdf';
Pdf.BeginDoc;
// 拉丁文標題會走一般的 TextOut 路徑
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(40, 60, 0, 'Right-to-left text with HotPDF');
// 阿拉伯文:charset 178,邏輯順序,RtLTextOut 負責重排
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
RightAlignedRtL(Pdf.CurrentPage, 100,
'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');
// 希伯來文:charset 177
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
RightAlignedRtL(Pdf.CurrentPage, 140,
'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');
// 混排行:內嵌的拉丁文字仍由左至右閱讀
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
RightAlignedRtL(Pdf.CurrentPage, 180,
'مرحبا بالعالم! تم إنشاؤه بواسطة 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 元件的一部分