BiDi 算法的实现及应用

来源:developerWorks 中国 作者:张 顺
  
软件的国际化是软件发展的趋势和挑战,软件国际化要处理编码转换、日期、数字和货币格式以及双向阅读顺序(Bidirection)等问题。世界上有二十多个国家和地区的超过 3 亿的人口使用从右到左的阅读顺序,因此很有必要在软件中加入对 BiDi 的支持。本文首先介绍软件国际化中 BiDi 的问题及算法,然后介绍 ICU4J 中 BiDi 算法的实现和使用。

BiDi 简介

BiDi 的英文全称是 bi-directional language,即双向字符集语言。这种语言主要包括希伯来语、阿拉伯语和乌尔都语等。它们的最大特点就是允许双向文本—也就是说,他们的本土语言书写顺序是从右往左,而其中的英文单词或商标符号从左向右显示。

BiDi 算法是为了在计算机世界里实现 BiDi 效果的而产生的。 BiDi 算法用于指定文本的文字方向。 BiDi 文本是指通常在一段文本中包含两种文字方向,水平 LTR 方向(从左到右)的文本中包含 RTL 方向(从右到左)的文本,或是 RTL 方向的文本中包含 LTR 方向的文本,主方向称为全局方向。

使用 RTL 方向的语言主要是在中东地区,如阿拉伯语和希伯来语。在这些语言中全局方向是 RTL,但是文本中嵌入的数字和其它 LTR 方向语言的地址、缩写以及引用会使用 LTR 方向。实现 BiDi 算法和对字符串进行重排序的类库称为存储布局引擎(Storage Layout Engine)。

逻辑顺序(Logical Order) 和 视觉顺序(Visual Order)

逻辑顺序和视觉顺序是 BiDi 中两个重要的概念,逻辑顺序指的是人们阅读和从键盘上输入的文字顺序,文本在内存里也是以逻辑顺序存储的。视觉顺序则是文本在屏幕或是打印机中显示的顺序。

如下面的例子,小写字母代表英语等 LTR 方向语言,大写字母代表阿拉伯语等 RTL 方向语言,假设全局方向为 LTR 。

视觉顺序:English CIBARA text

逻辑顺序:English ARABIC text

在输入的时候采用逻辑顺序,由于全局方向是 LTR,因此以 LTR 方向将 English 输入和显示之后,输入 ARABIC 时会以 RTL 方向对文本进行显示,即 CIBARA,但是会以 ARABIC 的顺序进行存储。输入 text 时又会以 LTR 方式进行输入和显示。

在显示或打印文本的时候,需要对文本进行重排序,将逻辑顺序转换为视觉顺序,某些文本以 LTR 方向显示,某些文本以 RTL 方向显示。 Unicode 标准规定了一种从逻辑顺序到视觉顺序的转换算法,通常以整个段落作为输入,屏幕的换行也会影响 BiDi 算法输出文本的实际显示位置。重排序算法的输出也用于光标的移动和选择。

除了显示文本以外,Unicode 对于计算机内部文本的处理,如拷贝,排序,查找等,都是以逻辑顺序处理的。这些操作依赖于字符匹配的顺序,因此必须以统一的顺序进行存储和处理。有的遗留系统为了避免在显示的时候对文本重排序,以视觉顺序对文本进行存储,在跟这些系统交换数据的时候,需要将数据从视觉顺序转换为逻辑顺序或是从逻辑顺序转换为视觉顺序。这种不是为了显示目的的转换称为存储布局转换(Storage Layout transformation)。

除了对键盘 , Locale, 字体等 NLS(National Language Support)基本的支持之外,BiDi 还支持:

  • Text reordering
  • Shaping
  • Geometry mirroring, Right_to_Left geometry




ICU4J

ICU(International Component for Unicode) 是 IBM 与开源组织合作研究 , 基于 "IBM 公共许可证 " 的用于支持软件国际化的开源项目。 ICU 实现了对数字、日期、货币等提供国际化支持,提供了强大的 BIDI 算法,对阿拉伯语和希伯来语等 BiDi 语言提供了完善的支持。 ICU 分为 ICU4J 和 ICU4C,分别对应 Java 和 c/c++ 平台。 ICU4J 被 Sun 的 JDK1.1 采用并随 JDK 版本更新。最新的 ICU4J 库可以从 http://icu-project.org/ 网站上下载。





BiDi 算法

下面以 ICU4J 的 Bidi 算法实现为例,简要介绍 Bidi 实现中的概念和算法。

BiDi Level

一段 BiDi 文本里可以有不同的文字方向,如在 RTL 方向的文本中包含 LTR 方向的字符串,或是在 LTR 方向的文本中包含 RTL 方向的字符串,在理论上还可能多重嵌套,但一般来说不会超过两层。 BiDi 算法使用 Level 来记录文本的方向。偶数为 LTR,奇数为 RTL,最外层一般规定为 0 或 1 。

如下面的文本是一个地址信息,大写字母代表阿拉伯语等 RTL 方向语言,全局方向为 RTL:

B ECNARTNE   25    TEERTS ELPAM 
 <-------------->   <->    <--------------------> 
     1            2             1

如果这段地址信息被一个使用英语的人引用,这时全局方向为 LTR,嵌套级别变为:

address is   B ECNARTNE    25  TEERTS ELPAM   today 
 <---------->  <----------------->  <->  <------------------>   <------> 
 0               1          2          1               0

通过计算每个字符的嵌套级别,BiDi 算法可以确定每个字符的文字方向,从而将逻辑顺序转换成视觉顺序,或是用于与遗留系统交换数据进行存储布局转换。

BiDi Run

BiDi Run 用来表示相同嵌套级别的字符序列,主要用途是为了避免单独记录每个字符的嵌套级别,节省内存空间。 BiDi 算法将一段文本根据嵌套级别分解为多个字符序列,同一个级别的相邻字符序列称为一个 BiDi Run 。 BiDi Run 记录了序列的开始和结束位置、嵌套级别以及一个标志位。 BiDi Run 没有公有构造函数,只能由 BiDi 算法解析文本的时候产生,并且没有 setter 方法,成员是不能被修改的。一个 BiDi Run 对象只需占用 8 个字节,通过 BiDi Run 来记录文本嵌套级别可以减少内存使用,只有在所有 BiDi Run 的平均字符数小于 2 个的情况下使用 BiDi Run 才会比单独记录每个字符的嵌套级别占用更多内存。

算法简介

BiDi 算法实现了对输入文本的解析,构造 BiDi 对象以及对文本进行重排序,对数字及特殊字符的映射等操作。对于输入的字符串,BiDi 算法首先根据参数的设置解析每个字符的嵌套级别,可以显示设定文本的全局方向,也可以由程序自动扫描,以第一个遇到的强方向字符的方向作为文本全局方向。解析完后,每个字符都会被设置级别,并通过 BiDi Run 来记录,解析之后创建的相关数据和原始文本都保存在 BiDi 对象中。在调用重排序操作的时候,BiDi 对象根据调用参数的设置,计算每个字符的输出顺序和映射结果并依次输出。





使用 ICU4J 进行 BiDi 开发

构造函数


表 1. BiDi 构造函数表
函数签名 详细信息
BiDi() 默认构造函数,调用 this(0,0)
BiDi(int maxLength, int maxRunCount) 以文本的最大长度和 Run 的最大个数构造 BiDi 对象,预先分配内存,运行时超出最大限制则出错,如果参数为 0 则根据输入文本自动分配内存。
BiDi(String paragraph, int flags) 以文本和文本方向创建 BiDi 对象,flags 的取值范围见表 2
BiDi(AttributedCharacterIterator paragraph) 以带属性的字符迭代器创建 BiDi 对象
BiDi(char[] text,int textStart,byte[] embeddings,int embStart,int paragraphLength,int flags) 以字符数组的方式创建 BiDi 对象,
textStart:构造 BiDi 对象的字符起始位置
embeddings:级别数组
embStart:相对开始级别
paragraphLength:文本长度
flags:文本方向


表 2. 文本方向标志 flags 参数说明
DIRECTION_LEFT_TO_RIGHT 从左到右
DIRECTION_RIGHT_TO_LEFT 从右到左
DIRECTION_DEFAULT_LEFT_TO_RIGHT 以第一个 BiDi 算法规定的强方向字符的方向作为文本方向,如果没有这种字符则使用从左到右方向
DIRECTION_DEFAULT_RIGHT_TO_LEFT 以第一个 BiDi 算法规定的强方向字符的方向作为文本方向,如果没有这种字符则使用从右到左方向

下面的代码对 BiDi 的主要函数进行了测试:


清单1 BiDi测试用例
   public void testBiDi(String text,int flag,int options){       
       BiDi BiDi = new BiDi(text, flag);
       byte paraLevel = BiDi.getParaLevel();
       int baseLevel = BiDi.getBaseLevel();
       
       boolean isBaseLeftToRight = BiDi.baseIsLeftToRight();
       boolean isLeftToRight = BiDi.isLeftToRight();
       boolean isRightToLeft = BiDi.isRightToLeft();
       boolean isMixed = BiDi.isMixed();
       boolean requiresBiDi = BiDi.requiresBiDi(text.toCharArray(), 0, text.length());
      
       int len = BiDi.getLength();
       int levels[] = new int[len];
       for (int i = 0; i < len; i++) {
          levels[i] = BiDi.getLevelAt(i) ;           
       }
       
       int runCount = BiDi.getRunCount();
       for (int i = 0; i < runCount; i++) {
           BiDi.getRunLevel(i);
           BiDi.getRunStart(i);
           BiDi.getRunLimit(i);
       }
       
       String writeReordered =  BiDi.writeReordered(options);
       
       StringBuffer sb = new StringBuffer(); sb.append("\n");
       sb.append("input text:" + text); sb.append("\n");
       sb.append("paraLevel:" + paraLevel); sb.append("\n");
       sb.append("baseLevel:" + baseLevel); sb.append("\n");
       sb.append("isBaseLeftToRight:" + isBaseLeftToRight); sb.append("\n");
       sb.append("isLeftToRight:" + isLeftToRight); sb.append("\n");
       sb.append("isRightToLeft:" + isRightToLeft); sb.append("\n");
       sb.append("isMixed:" + isMixed); sb.append("\n");
       sb.append("requiresBiDi:" + requiresBiDi);sb.append("\n");
       sb.append("levels:"); 
       for(int i = 0; i < levels.length; i++)
           sb.append(levels[i] + " ");
       sb.append("\n");
       sb.append("runCount:" + runCount); sb.append("\n");
       for(int i = 0; i < runCount; i++){
           sb.append("  run " + i + ":");
           sb.append("level-" + BiDi.getRunLevel(i));
           sb.append("  start-" + BiDi.getRunStart(i));
           sb.append("  limit-" + BiDi.getRunLimit(i));
           sb.append("\n");
       }      
       sb.append("writeReordered(" + options + "):"+ writeReordered);
       
       System.out.println(sb.toString());
}

writeReordered 函数根据 BiDi 对象设置的参数对文本进行重排序,该函数的 options 参数说明如表 3:


表 3. options 参数说明
DO_MIRRORING 在 RTL 方向的 BiDi Run 中用镜像字符替换原字符,但是有的字符在 Unicode 里并没有镜像字符
INSERT_LRM_FOR_NUMERIC 在必要的时候插入 LRM
KEEP_BASE_COMBINING 在 RTL 方向的 BiDi Run 中保持组合的字符在基本字符之后
OUTPUT_REVERSE 以逆向顺序输出字符
REMOVE_BIDI_CONTROLS 移除 BiDi 控制字符,不影响 INSERT_LRM_FOR_NUMERIC
OPTION_STREAMING 将输出作为一个未结束的流处理,指明是一个大文本的一部分,只有在最后一部分的时候关闭选项

假设输入文本如下:

String text = "\u006c\u0061\u0028\u0074\u0069\u006e\u0020\u05d0\u05d1\u0029\u05d2\u05d3";

调用 testBiDi(text, BiDi.DIRECTION_LEFT_TO_RIGHT,BiDi.DO_MIRRORING)

输出如下:


图 1. 测试结果 1
测试结果 1

可以看到由于设置 BiDi 文本方向为 DIRECTION_LEFT_TO_RIGHT,paraLevel 和 baseLevel 为 0,isBaseLeftToRight 为 true 。又由于文本中包含 LTR 和 RTL 字符,isLeftToRight 和 isRightToLeft 为 false,isMixed 为 true,requiresBiDi 为 true 。 requiresBiDi() 函数用来确定文本是否需要进行 BiDi 算法的转换,它可以避免额外的转换。由于 base level 为 0,RTL 的字符为 1 。该文本共有两个 BiDi Run,并记录了每个 BiDi Run 的起止位置。可以看到整个文本以 LTR 方向输出,RTL 的文本已经以从右到左的顺序输出,由于用 DO_MIRRORING 作为参数调用 writeReordered 函数,RTL 部分输出的“ ( ”变成了它的镜像字符“ ) ”,但是 LTR 部分的仍以原字符输出。

调用 testBiDi(text, BiDi.DIRECTION_RIGHT_TO_LEFT,BiDi.DO_MIRRORING)

输出如下:


图 2. 测试结果 2
测试结果 2

由于设置 BiDi 文本方向为 DIRECTION_RIGHT_TO_LEFT,paraLevel 和 baseLevel 为 1,isBaseLeftToRight 为 false 。文本中包含 LTR 和 RTL 字符,isLeftToRight 和 isRightToLeft 为 false,isMixed 为 true,requiresBiDi 为 true 。由于 base level 为 1,RTL 的字符为 1,嵌套的 LTR 文本级别为 2 。整个文本以 RTL 的顺序输出,由于用 DO_MIRRORING 作为参数调用 writeReordered 函数,输出的“ ( ”变成了它的镜像字符“ ) ”,LTR 部分文本仍以原顺序输出,也未作镜像处理。

调用 testBiDi(text, BiDi.DIRECTION_DEFAULT_LEFT_TO_RIGHT,BiDi.DO_MIRRORING)

和 testBiDi(text, BiDi.DIRECTION_DEFAULT_RIGHT_TO_LEFT,BiDi.DO_MIRRORING)

输出如下:


图 3. 测试结果 3
测试结果 3

使用 DIRECTION_DEFAULT_LEFT_TO_RIGHT 和 DIRECTION_DEFAULT_RIGHT_TO_LEFT 参数的时候以遇到第一个强方向字符作为文本的方向,因此对于该文本数据两种方式的调用结果是一致的,即以“ l ”的方向作为文本方向,与调用 testBiDi(text, BiDi.DIRECTION_LEFT_TO_RIGHT,BiDi.DO_MIRRORING) 一样。





结束语

本文介绍了 BiDi 的背景知识和相关概念,并介绍了开源项目 ICU4J 中 BiDi 算法的实现和使用。(责任编辑:A6)


时间:2009-05-18 08:44 来源:developerWorks 中国 作者:张 顺 原文链接

好文,顶一下
(1)
100%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量