阿拉伯语、希伯来语、波斯语这些语言是从右往左书写的,而里面混杂的阿拉伯数字、英文等又是正常从左往右读的,所以也叫双向文本。此外,阿拉伯语的字符实际显示的字形会依赖上下文,即边上的字符,发生连写、重叠等变化,叫做复杂文本。图片OCR和翻译软件碰到这类语言,就需要额外处理一串问题:界面怎么显示、OCR输出的字符顺序怎么还原、单词怎么按正确顺序拼成行、导出PDF时又怎么让文字按阅读顺序排列。

这篇讲一下ImageTrans里对应的处理。

界面显示和文字渲染

JavaFX的Node有一个nodeOrientation属性。把它设成RIGHT_TO_LEFT之后,JavaFX会自动处理文本对齐、光标移动方向、标点符号落在哪一侧这些细节,不需要手工干预。

ImageTrans在显示双向文本的控件上都设了这个属性,包括原文框、译文框,以及文字排版引擎。

除了显示,还有一个从右往左阅读的项目设置,它影响的是排序。

阿拉伯语的段落,第一个词在最右边。如果按我们习惯的方式,用X坐标从小到大排序,整段的阅读顺序会完全反过来。所以这个开关会传给排序和合并相关的多个模块:

  • 文本框排序
  • 行内框排序
  • 分镜检测
  • 框合并

OCR的字符处理

ImageTrans用的是PaddleOCR系列模型(通过RapidOCR集成)。这类模型识别阿拉伯语时,输出的是视觉顺序——它是按图像像素从左往右扫的。

视觉顺序意味着:字母的实际书写顺序是反的,但数字因为视觉上本来就从左往右排,顺序反而是对的。

所以要做一次反转,但保留数字段的转换:

  • 纯数字串:原样返回
  • 其它:先整串反转,再把每一段连续的数字倒回来

举例来说,م٢٠٢٦要还原成٢٠٢٦م。

同时还要做一次括号镜像,把左右括号换过来。

但其它OCR不需要这个操作。

单词合并成行:从右往左

OCR出来的是一堆单词框,要先合并成行,再合并成段。

合并的做法是从一个种子框开始,不断往某个方向吃相邻的框。往哪个方向先吃,阿语要反过来:

growRightFirstRound = True              ' 默认先往右
If right2left And horizontal Then
    growRightFirstRound = False         ' 阿语先往左
End If

先往左把同一行的词收进来;这一轮收不动了,再换个方向试一次;两个方向都收不动,这一行就结束了。

但只用合并方向还不够,还有一类混排的情况要单独处理。

阿拉伯语的行从右往左合并,这个顺序对阿拉伯词是正确的,但夹在行里的拉丁文必须保持从左往右——否则一个hello world合并完就变成了world hello。

所以当第一个框的末尾拖着两个字符以上的非阿拉伯字母,而第二个框也不是阿拉伯词时,就认为这两个框同属一个LTR文本段,需要把顺序对调。实现上就是把第一个框的尾段摘下来,接到第二个框后面:

source1 = source1 去掉尾巴  +  " "  +  source2  +  尾巴
source2 = ""

比如第一个框是world、第二个框是hello,处理完就还原成了hello world。

输出PDF:shape和reverse

这是最麻烦的一步。

PDF本身不做复杂文本排版。PDFBox的showText拿到一个字符串,会逐个Unicode码点去查字体的cmap,查到字形就画出来。它不管阿拉伯语的上下文变形——同一个字母在词首、词中、词尾、单独出现时形状不同,这件事PDFBox不会帮你做。

所以ImageTrans自己做了两步。

第一步是shape:把每个字母替换成它在当前位置对应的变形。Unicode里有一块「阿拉伯语表述形式」区(U+FE70–FEFF)专门放这些变形字形。比如ت单独出现是FE95,在词首是FE97,词中是FE98,词尾是FE96。判断依据是前后字母能不能连写——像ا、د、ر这些字母只能跟前面连,后面连不上。

第二步是reverse:上一步的产物是一串「字形码」而不是「字符」。字符的顺序是逻辑序,而字形码的顺序必须是视觉序,也就是画在纸上的顺序。所以要把整串倒过来,这样从左到右画出来才是对的。

反转有个坑:数字不能跟着反转。所以这里不是简单的一步反转,而是按空格切词、逐词处理:

  • 纯数字词:还原回去
  • 阿拉伯词:保留,但把词内部的连续数字段倒回来
  • 其它词(拉丁词等):攒成一组,整组反转

最后再镜像一次括号、清掉方向控制字符,才交给PDFBox画出去。

对字体的要求

这套做法依赖字体自带U+FE80–FEFC的表述形式字形。Arial、Tahoma、Segoe UI这些常见系统字体都有(历史原因,为了兼容老软件)。但也有一些字体没有,比如Windows自带的Arabic Typesetting,用它导出会直接报错。

可搜索性

有个意外的好处:PDF里存的明明是表述形式字形,但复制出来的却是正常的阿拉伯文。

原因是提取文本时会对每个词做一次NFKC归一化,而NFKC恰好会把U+FE70–FEFF兼容分解回基础字母。这是通用行为,不是某个库特有的,所以其它PDF阅读器也一样。导出的PDF既能正常显示,也能复制和搜索。

已知限制

  • 字体必须包含表述形式字形(U+FE80–FEFC),否则导出会失败。如果你的字体踩到了这个问题,换一个覆盖完整的字体即可。
  • 如果原文带变音符(harakat),它们会一起进入文字层。搜索时可能需要带上变音符才能精确匹配,具体行为取决于阅读器的实现。
  • 连字(比如لا)目前是用两个连着的字形拼出来的,视觉效果接近,提取时也能正确还原成两个字母。

双向与复杂文本列表

下面把双向性和复杂性合并到一张表格里,两个维度分别用「是/否」标注。

语言/文字系统 双向文本(RTL/LTR混排) 复杂文本(上下文变形/重排) 主要使用地区 核心特性说明
阿拉伯语 是 是 中东、北非 RTL方向;字母按词首/词中/词尾/独立变形,有连字
希伯来语 是 是 以色列 RTL方向;元音符号(niqqud)需定位,常混排英文
波斯语 (Farsi) 是 是 伊朗 阿拉伯字母变体,RTL,上下文变形
乌尔都语 (Urdu) 是 是 巴基斯坦、印度 阿拉伯字母,RTL,上下文变形
维吾尔语 (Uighur) 是 是 中国新疆 阿拉伯字母,RTL,上下文变形
叙利亚语 (Syriac) 是 是 中东部分地区 RTL;复杂的连写与元音标记
迪维希语 (Thaana) 是 否 马尔代夫 RTL方向;字形基本独立,无复杂整形
意第绪语 (Yiddish) 是 是 全球犹太社区 希伯来字母书写,RTL;元音符号定位
库尔德语 (Kurdish) 是 是 伊拉克、叙利亚等 部分方言用阿拉伯字母,RTL,上下文变形
信德语 (Sindhi) 是 是 巴基斯坦、印度 阿拉伯字母,RTL,上下文变形
印地语 (天城文) 否 是 印度 LTR;辅音簇、元音符号重新排序
孟加拉语 否 是 孟加拉国、印度 LTR;辅音簇与变形规则
泰米尔语 否 是 印度南部、斯里兰卡 LTR;辅音-元音组合与连字
泰语 否 是 泰国 LTR;元音上下堆叠、声调符号定位
藏语 否 是 中国西藏、青海等 LTR;辅音上下叠加、元音符号变形
蒙古文(传统) 否 是 中国内蒙古、蒙古国 竖写;词首/词中/词尾变形(方向与RTL不同,属竖排)
高棉语 否 是 柬埔寨 LTR;复杂辅音簇与元音重排
缅甸语 否 是 缅甸 LTR;类似高棉语的复杂整形
英语、汉语等 否 否 全球 基础 LTR,字符与字形基本一一对应

几个要点

有专门的整形shaping库Harfbuzz和双向算法库可以支持各种语言的处理。下面是一些要点。

  1. 双向 ⊂ 复杂,但不完全重合:阿拉伯语、希伯来语等既双向又复杂;迪维希语是双向但不复杂;藏语、泰语是复杂但不双向。
  2. HarfBuzz 负责的范畴:所有标「复杂=是」的语言,HarfBuzz 都能提供整形支持(字形选择、连字、定位)。
  3. Bidi 算法负责的范畴:所有标「双向=是」的语言,需要额外的 Bidi 算法(如 ICU、FriBidi)做方向分段,HarfBuzz 本身不做这件事。
  4. 蒙古文的特殊性:传统蒙古文是竖写,方向既不是标准 LTR 也不是 RTL,但它同样是复杂文本,HarfBuzz 也支持其上下文变形。

相关阅读:如何确定图片中文字的阅读顺序