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
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/ 网站上下载。
下面以 ICU4J 的 Bidi 算法实现为例,简要介绍 Bidi 实现中的概念和算法。
一段 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 算法将一段文本根据嵌套级别分解为多个字符序列,同一个级别的相邻字符序列称为一个 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 对象根据调用参数的设置,计算每个字符的输出顺序和映射结果并依次输出。
表 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
可以看到由于设置 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
由于设置 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
使用 DIRECTION_DEFAULT_LEFT_TO_RIGHT 和 DIRECTION_DEFAULT_RIGHT_TO_LEFT 参数的时候以遇到第一个强方向字符作为文本的方向,因此对于该文本数据两种方式的调用结果是一致的,即以“ l ”的方向作为文本方向,与调用 testBiDi(text, BiDi.DIRECTION_LEFT_TO_RIGHT,BiDi.DO_MIRRORING) 一样。
本文介绍了 BiDi 的背景知识和相关概念,并介绍了开源项目 ICU4J 中 BiDi 算法的实现和使用。
声明:
本文仅代表个人观点。
学习
- ICU 用户指南:http://icu-project.org/userguide/icu.pdf。
- 全球化随需应变业务:http://www-306.ibm.com/software/globalization/topics/BiDi/characteristics_bi.jsp。
- 有关 ICU4C 方面的知识,请参考文章 ICU4C 介绍: C/C++ 平台强大的国际化应用开发组件
- 访问 developerWorks 开放源码专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用。
- 查看免费的 developerWorks 演示中心,观看并了解 IBM 及开源技术和产品功能。
获得产品和技术
- 使用 IBM 试用软件 改进您的下一个开发项目。