把阿拉伯语句子 يوضح ملف PDF هذا 交给普通的 TextOut,返回的页面会以两种方式同时出错。词从左向右跑而不是从右向左,而且字母以孤立形式各自分开,而不是连接成连贯的词。没有任何报错。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 改变的是字形顺序,而非页面从何处测量。angle 像 TextOut 中那样旋转基线,所以 0 画一条水平线。Text 是逻辑顺序的字符串,也就是你键入它的顺序,而第二个重载以一个原始 PWORD 缓冲区加上显式码元计数接收同样的 UTF-16 数据,这是文本来自一个 API 而非 Delphi 字符串时该用的形式。在早于这些类型重载解析的更老 Delphi 版本上,字符串形式以名字 RtLTextOutStr 暴露,参数列表完全相同
两个输出调用之间的分工是严格的。TextOut 按你传入的顺序绘制码点,这对拉丁文、西里尔文和 CJK 是正确的,对阿拉伯文和希伯来文是错的。RtLTextOut 先把每行重排成视觉上的从右到左顺序再绘制,让行内嵌入的拉丁词和数字保持从左向右读。HotPDF 故意把这两个方法分开,而不是根据字符猜测方向,所以调用哪一个的选择就是你要哪种脚本行为的选择;对从右到左的运行用 RtLTextOut,对其他一切用 TextOut,永远不要把一个经由另一个路由。重排序为何存在、Unicode 双向算法和阿拉伯文上下文连接实际做了什么、以及 HotPDF 的塑形在哪里止步,是配套文章HotPDF 的阿拉伯文与 RTL 文本塑形的主题;下面全是实用的设置

charset 参数决定脚本
告诉 RtLTextOut 它在排版的是阿拉伯文还是希伯来文的,不是方法,而是字体。SetFont 把一个 Windows charset 作为它的第四个参数,而那个值把脚本规则带进从右到左的调用:178 选择阿拉伯文,177 选择希伯来文。设好 charset 再绘制,下面两行无需任何进一步配置就能以正确的阅读顺序出来
// 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 之后重复,因为当前字体(包括 charset)挺不过一次分页。忘了重复,第二页就回退到当时活动的任何字体,对阿拉伯文来说通常意味着空框
它不会反转你已经反转过的文本
这里吞掉最多调试时间的单个错误,是把一个你已经手工翻转过的字符串喂给 RtLTextOut。人们是在普通 TextOut 的第一次尝试出来是反的之后到达这个方法的,而一个常见的权宜之计是在绘制前用代码反转字符。RtLTextOut 内部自行反转,所以一个预先反转的字符串被第二次反转,又落回它开始的地方。以逻辑顺序传入文本——你键入和朗读它的顺序——让调用来做重排
这个陷阱比一次单纯翻转更险恶,因为一个双重反转的字符串可能对一个全阿拉伯文的测试短语看起来正确,然后在一行携带一个拉丁词或数字的那一刻坏掉。在一个从右到左的行内,那些嵌入的运行本应从左向右读,而手工反转破坏了那种嵌套,而纯阿拉伯文的情况碰巧能挺过它。所以这个 bug 顺利通过你的第一次冒烟测试,而在很久之后的一份带账号的真实发票上浮现。切换到 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;
对于一个确实从右向左读的文档,让它留着别管。关键是要知道这个调用有一项全文档范围的效果,这样小册子的意外就永远不会发生
注册你分发的字体,而不是你指望已安装的字体
如果字体没有可绘制的字形,任何重排序都无济于事。经典的失败是一份在开发者机器上完美渲染的报告——那里碰巧装了 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 文件带有嵌入许可位,一个在屏幕上看起来没问题的字体可能以一种禁止在客户文档中分发的方式授权。在嵌入之前而非在一次投诉之后确认许可
一个完整的控制台示例
把各部分拼起来,下面是一个自包含的程序,它写一页,含一行阿拉伯文、一行希伯来文,以及一行带拉丁产品名的混排行。每个块设好它的 charset,然后以逻辑顺序绘制
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.
运行它并打开结果。阿拉伯文和希伯来文行从右向左读,字母在脚本连接它们的地方连接,而在最后一行中词元 HotPDF 在阿拉伯文运行内部从左向右坐着。那种嵌套是正确的双向结果,不是 bug,尽管初次评审者常常把它当作 bug 提交;上面链接的塑形文章解释了为何 Unicode 规则要求它,以及如何措辞你的验收标准好让报告永远不会被错提
常见错误及其修复
下面的每一种失败都出现在真实的支持帖子里,而且每一种都可追溯到上面的一节
- 输出读起来是反的,或在混排行上乱序 —— 字符串在调用前被手工反转了,通常是
TextOut尝试留下的一种权宜之计。删除每一处手工反转并以逻辑顺序传入;RtLTextOut内部自行反转 - 字母以孤立形式断开打印 —— 文本经过了普通的
TextOut,或者SetFont被调用时没有带一个从右到左的 charset。用RtLTextOut绘制,并把 178(阿拉伯文)或 177(希伯来文)作为SetFont的第四个参数传入 - 客户机器上是空框 —— Windows 替换了一个不覆盖阿拉伯文或希伯来文的字体。停止命名已安装字体;通过
RegisterUnicodeTTF注册一个你分发的字体,并用那个名字SetFont - 第二页以错误的字体渲染 —— 当前字体挺不过
AddPage。在每次分页后重复SetFont调用,包括 charset - 双联展开在一个大部分为 LTR 的文档上镜像打印 —— 第一次
RtLTextOut调用作为副作用翻覆了文档的Direction。在从右到左的运行之后设Pdf.Direction := LeftToRight - 嵌入的 Unicode 文本在下游静默退化 —— 流水线中某处强制 PDF 1.4,而 HotPDF 的嵌入 Unicode 处理需要 1.5 或更高。提高文档版本或移除下游约束
在格式出厂之前,超出肉眼的校验:从查看器把文本复制出来、运行文档内搜索、在一台没有你开发字体的机器上打开文件,并把一份真正的文档放在一位母语读者面前。完整的校验清单、按脚本的覆盖映射,以及值得构建的测试字符串语料库,全都位于配套文章HotPDF 的阿拉伯文与 RTL 文本塑形中
此处展示的 RtLTextOut、SetFont 和 RegisterUnicodeTTF 调用属于面向 Delphi 和 C++Builder 的 HotPDF Component