技术文章

HotPDF 中的 RtLTextOut:在 Delphi 中输出从右向左的 PDF 文本

将阿拉伯语句子 يوضح ملف PDF هذا 直接交给普通的 TextOut 会同时带来两个问题:词序从左向右,而不是从右向左,字形也无法按上下文连接。这样得到的结果会经过编译并打开,但阅读体验与预期完全不符。HotPDF 会通过 RtLTextOut 提供独立路径处理 RTL 排列

签名与参数

TextOut 以传入顺序直接绘制,而 RtLTextOut 用于需要从右到左可视顺序输出的文本。以下是该方法的两个重载

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

XY 使用页面原生坐标系,左下角为原点,Y 轴向上,和 TextOut 一致;angle 的定义也一致,0 表示水平文本。Text 是逻辑顺序字符串,第二个重载接受同一内容的 PWORD 缓冲区和长度,适合从 API 回调中直接消费 UTF-16 数据的场景。RtLTextOutTextOut 的分工是明确的:一个负责普通从左到右排版,一个负责 RTL 重排后的绘制

对于不支持重载解析的旧 Delphi,字符串版本可能以 RtLTextOutStr 暴露,参数列表与上面两个重载等价。新版代码优先用 RtLTextOut,但这个等价入口在迁移旧组件时代码时仍可保留

右到左与左右混排的细节、连接与方向判断规则在 HotPDF 的阿拉伯语与 RTL 文本塑形 中有更完整说明。本文聚焦可落地的参数、字体、链接动作与排查方式

Diagram of how RtLTextOut reorders a mixed Arabic and Latin line into visual right-to-left order before drawing it into a PDF
RtLTextOut 在绘制前先重排每一行,RTL 区段保持从右到左顺序,同时保留行内 Latin 与数字的从左到右阅读习惯

字符集参数决定文字体系

方向判断依赖字体设置而非方法名称本身。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 重置,因此遗漏重置在次页常见出现空白方框,尤其在 RTL 文本场景更易暴露

它不会再次倒转你已倒转的字符串

常见误区是先手工把逻辑顺序反向后再调用 RtLTextOut,这会让内部重排发生两次,结果看上去像“刚好对上”或“突然乱掉”都可能发生。请始终使用逻辑顺序输入,让 API 负责重排

这个问题有时非常隐蔽,因为一整段全阿拉伯语时被手工倒序后看似正常,直到混入 Latin 词或数字时才会断开,最终显示结果和期望背离。尽量把这一类临时兼容措施删掉,交给 RtLTextOut 按方法规则处理

Direction 的副作用值得先了解

第一次出现 RTL 绘制时,文档方向会改为从右到左,这与直接设置 Direction 的效果一致。该方向写入 ViewerPreferences 中的 vpDirection,影响双页与装订方向。对单页预览通常看不出明显差异,但在书本式打印中会体现成镜像行为

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

如果整份文档本身就是 RTL,就保留该默认,关键是理解这是文档级影响而不是单行局部效果

只注册你随应用发布的字体

部署环境很少与开发环境保持一致,字体可见性问题要通过自带字体规避。建议注册并使用你打包时就确定好的 Unicode 字体,而不是赌所有目标机器都装了同一套字体

// 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 هذا');

使用该接口会进行字体嵌入,通常要求文档版本不少于 PDF 1.5。还要额外确认字体许可条款中对嵌入与再分发的约束,否则在合规检查时会卡在发布阶段

一个完整的控制台示例

下面示例覆盖阿拉伯语、希伯来语和混合行输出,用一份可直接运行的控制台程序演示字体切换与 RTL 渲染

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.

运行后可见阿拉伯语与希伯来语均以 RTL 视觉顺序输出,字形连接按规则生效。混合行中的 HotPDF 仍以 LTR 方式呈现,其视觉结果属于预期行为

常见错误与修复

  • 输出顺序仍不对 — 请排查是否预先手工翻转字符串,RtLTextOut 需要逻辑顺序输入
  • 阿拉伯文字形断开 — 多见于未设置 charset 或未用 RtLTextOut 而直接走 TextOut
  • 开发机正常、客户机空框 — 注册并使用随程序分发的 Unicode 字体
  • 跨页后 RTL 样式丢失 — 每次 AddPage 后重设 SetFont 和 charset
  • 书本模式里页面方向异常 — 对以 LTR 为主的文档在完成 RTL 段落后恢复 Direction
  • 嵌入字体触发兼容问题 — 检查字体许可与文档版本,再决定是否启用嵌入

导出前建议做一轮真实验证:从渲染结果拷贝文本与源文本比对,使用内置搜索验证可见关键词,再在未安装目标字体的机器上打开检查。最终最好由母语读者复核,避免可视化测试遗漏语义错误

更深层的成形与边界规则可参考 HotPDF 的阿拉伯语与 RTL 文本塑形,本文展示的 RtLTextOutSetFontRegisterUnicodeTTF 都属于 HotPDF Component(适用于 Delphi 与 C++Builder)的一部分