ICU4C 在 IBM Lotus Symphony 中的应用

使用 ICU4C 增强 Symphony 文本处理功能

付 兴旺, 软件开发工程师, EMC
贾 彦民, 软件开发工程师, EMC
来 竞, 软件开发工程师, EMC

 

简介: 本文描述了使用 ICU 来完善 Lotus Symphony 字符串处理机制的解决方案。ICU 是一个平台无关的、支持 Unicode 的软件全球化开发的专用开源工具库,应用于许多大型全球化软件产品。本文给出的方案大幅度简化了编码转换实现的复杂性,并支持更多的编码格式,增强了 Lotus Symphony 对非 BMP (Basic Multilingual Plane) 平面字符的解析能力,消除了 Symphony 产品中许多文本输出的缺陷。借助于 ICU 的 API,本地编码相关数据被封装在 ICU 的库中,大大节约了数据维护的成本。本文还将对方案中用到的 Unicode 相关知识作广泛深入的探讨。

文本处理的相关知识

字符编码

文本由一系列字符组成。为了处理字符,每个字符在计算机中都用一个二进制数来表示,字符与具体数字之间的对应关系就叫做字符编码。

编码字符集

为了方便表示各种各样的语言,不同的国家或机构制订了一系列编码标准,标准规定了编码范围以及支持的语言字符,这些编码字符的集合就叫做编码字符集。如英文的 ASCII 码,简体中文的 GB18030,日文的 JIS,阿拉伯语的 windows-1256 等。

Unicode

早期制定的编码标准都与语言和区域有关,不同的编码之间并不兼容,没有一种编码可以包含所有语言的字符,导致了同一个文本之中不能包含多种语言的字符。

随着国际交流的需要,国际组织制订了 Unicode 标准,将全世界常用文字都包含进去,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode 标准有三种编码形式,UTF-8,UTF-16,UTF-32,分别用 1 个、2 个、4 个字节作为编码单元。UTF-16 编码平衡了文本的执行效率和内存的占用,目前使用 Unicode 作为内码的应用程序大都采用 UTF-16 编码,Symphony 就使用 UTF-16 编码。

UTF-16 编码对于 Unicode 标准中定义的 BMP (Basic Multilingual Plane) 定义的字符用两个字节表示,这部分字符基本上涵盖了所有语言的常用字符。对于超出 BMP 平面的字符 UTF-16 用 4 个字节表示,叫做 surrogate pairs。图 1 示例了字符到 UTF-16 编码的映射。


图 1. 字符到 UTF-16 编码的映射
图 1. 字符到 UTF-16 编码的映射 

Lotus Symphony 中的文本处理

Symphony 内部处理使用的是 UTF-16 编码,在实际应用中,主要有两个方面:编码转换和文本操作。

编码转换

第一类编码转换是 Symphony 与操作系统交互时的转换。

操作系统都有默认的本地编码,例如中文 Windows 默认的本地编码是 GB2312, Linux 默认的本地编码为 UTF-8。例如我们使用 getenv 获取系统环境变量,得到的就是系统本地编码的字符串,必须转换为 UTF-16 编码的文本才能使用。

第二类编码转换是 Symphony 处理本地编码格式的文本文件时的转换。

Symphony 在处理这些文本文件时,首先要将文本文件的内容转化为 UTF-16 编码的文本装载进来,在操作结束后再将内容转换为文本原来的编码格式进行保存。

在此前的 Symphony 版本中,Symphony 自己维护了一套其它编码与 UTF-16 编码之间的转换机制,这样增加了维护成本,并且更新不及时。使用 ICU 替换原来的转换机制,不仅可以增强对其它编码的支持,而且大大减少了维护成本。

文本操作

Symphony 内部实现了基于 UTF-16 编码的字符串类,字符串类提供了对 utf-16 编码的字符串的构造,设定,插入,替换等基本操作。在此前的 Symphony 版本中,字符串类在处理非 BMP 平面的字符存在很大缺陷。使用 ICU 的一些 API,可以弥补这些缺陷,增强字符串的处理功能。

在 Symphony 中使用 ICU 进行编码转换

使用 ICU 进行文本转换主要流程包含:

构建 ICU 转换器  初始化转换文本信息  调用转换 API  获取结果信息。

下面分别具体介绍。

构建 ICU 转换器

使用 ICU 进行编码转换,是指将某种编码转换为 UTF-16 编码或者把 UTF-16 编码转化为其他某种编码。进行编码转换首先要创建转换器,示例代码如清单 1.


清单 1. 构建 GB2312 编码转换器
				
 UConverter* pConverter = ucnv_open(“GB2312”,&err); 

创建的转换器可以用来把 GB2312 编码的文本转换为 UTF-16 编码的文本,也可以用来把 UTF-16 编码的文本转换为 GB2312 编码的文本。

ICU 支持 200 多种编码格式的转换,可以通过 ICU 的 API 获取 ICU 支持的具体编码信息。示例代码如清单 2.


清单 2. 获取 ICU 支持的编码
				
 int num = ucnv_countAvailable( ); 
 for(int i=0;i<num;i++) 
 { 
 Char encoding[64] = {0}; 
 ucnv_getAvailableName(i, encoding); 
 } 

ICU 编码转换 API 的使用

ICU 常用的文本转换 API 有下面两对,分别为可逆转换。如表 1 和表 2.


表 1. ucnv_toUChars 与 ucnv_fromUChars
ucnv_toUChars( … )ucnv_fromUChars( … )说明
其他编码 UTF-16UTF-16 其他编码返回 int32_t,转换得到的字符个数
UConverter *cnvUConverter *cnv参数 1,转换器句柄
UChar *destchar *dest参数 2,目标缓冲区
int32_t destCapacityint32_t destCapacity参数 3,目标缓冲区容量
const char *srcconst UChar *src参数 4,需要转换的字符串
int32_t srcLengthint32_t srcLength参数 5,设定的转换长度
UErrorCode *pErrorCodeUErrorCode *pErrorCode参数 6,转换信息


表 2. ucnv_toUnicode 与 ucnv_fromUnicode
ucnv_toUnicode( … )ucnv_fromUnicode( … )说明
其他编码 UTF-16UTF-16 其他编码无返回值
UConverter *converterUConverter * converter参数 1,转换器句柄
UChar **targetchar **target参数 2,指向目标缓冲区首地址的指针
const UChar *targetLimitconst char *targetLimit参数 3,提供可用的目标缓冲区末地址
const char **sourceconst UChar ** source参数 4,指向源缓冲区首地址的指针
const char *sourceLimitconst UChar * sourceLimit参数 5, 标识源数据的末地址
int32_t *offsetsint32_t* offsets参数 6,源数据偏移索引数组,数组空间为实际转换的目标字符个数,元素标识了目标数据从哪位源数据转换而来。如果设为 NULL,则略过此参数
UBool flushUBool flush参数 7,如果需要多次调用转换,保证源数据全部被转换,设置为 true
UErrorCode *errUErrorCode * err参数 8,转换信息

总体来说,第一组函数的使用相对比较简单,但使用这组函数不能获取到具体源数据转换的数据信息,例如实际转换的源数据长度,以及目标数据与源数据的对应关系等;第二组数据使用起来相对麻烦,但在一些需要详细转换信息的情况下,就必须使用第二组转换函数。

下面以实际例子来展示如何使用 ICU 进行文本编码转换。图 2 展示了一个字符串使用 UTF-16 编码和 GB18030 编码的内存数据。


图 2. 示例字符串在使用不同编码时的内存数据
图 2. 示例字符串在使用不同编码时的内存数据 

使用 ucnv_toUChars 和 ucnv_toUnicode 能将 GB18030 编码的字符串转换为 UTF-16 编码的字符串;使用 ucnv_fromUChars 和 ucnv_fromUnicode 能将 UTF-16 编码的字符串转换为 GB18030 的字符串。表 3 和表 4 示例了两组 API 的使用。


表 3. 使用 ucnv_toUChars 将 GB18030 编码转换为 UTF-16 编码
 使用 ucnv_toUChars
源数据 
GB18030
Char src[11] = {0x48,0x69,0xD6,0xD0,0xB9,0xFA,0x92,0x32,0x82,0x36,0};
Int srcLength = 10;
目标 
UTF-16
UChar dest[10] = {0};
Int destCapacity = 6;
转换Int len = ucnv_toUChars(hConverter, dest, destCapacity, src, srcLength, &err);
结果dest 中的数据为:0048,0069,4E2D,56FD,D840,DC00
len 为 6,实际转换的目标数据长度;
转换正常,err 为 0;如果 destCapacity 小于 6,则 err 会返回缓冲区不足


表 4. 使用 ucnv_toUnicode 将 GB18030 编码转换为 UTF-16 编码
 使用 ucnv_toUnicode
源数据 
GB18030
Char src[11] = {0x48,0x69,0xD6,0xD0,0xB9,0xFA,0x92,0x32,0x82,0x36,0};
const char *cvtSrc = src, 
const char *srcLimit = src + 10;
目标 
UTF-16
UChar dest[10] = {0};
UChar *cvtDest = dest;
UChar *destLimit = dest + 6;
转换ucnv_toUnicode(hConverter, &cvtDest, destLimit, &cvtSrc, srcLimit, NULL, TRUE, &err);
结果dest 中的数据为:0048,0069,4E2D,56FD,D840,DC00
int destCvtCount = cvtDest - dest; // 实际转换的目标长度 
int srcCvtCount = cvtSrc - src; // 实际转换的源数据长度 
如果转换正常,err 为 0;如果 destLimit – dest < 6, err 会返回缓冲区不足

如何处理分段转换字符串中的字符截断问题

当 Symphony 读一个大的非 UTF-16 编码的文本文件时,不可能一次性把文件的全部内容装载到内存中去进行编码转换,于是只能分段读取文件内容,分段转换。一个字符编码可能由一个或多个字节组成,分段转换就可能出现字符的截断问题。以每次读入 512 字节为例,假如 512 字节处刚好是一个字符的中间位置,那么用拿到的这段文本去做转换,最后面截断的字符将会变成乱码。

我们可以利用 ICU 的 ucnv_toUnicode 的第 6 个参数返回的信息,来处理上述可能出现的截断问题。在表 2 中,ucnv_toUnicode 的第 6 个参数被设成了 NULL,现在打开这个参数。


清单 3. 获取转换信息
				
 Int offset[6] = {0}; 
 ucnv_toUnicode(hConverter, &cvtDest, destLimit, &cvtSrc, srcLimit, offset, TRUE, &err); 

offset 的数据依次为:0,1,2,4,6,6;每个数字代表的是源字符串中的索引,表示源字符串中 0-1,1-2,2-4,4-6,6-9 分别转换成目标串中的字符。图 3 显示了内存数据。


图 3. 转换数据信息
图 3. 转换数据信息 

如果指定 srcLength 为 5,则字符“国”被切断。此时 offset 的数据依次为:0,1,2,4;并且 dest[3] 为 0xFFFD。0xFFFD 是一个无效字符,可以用这个信息标识出现了字符截断情况。通过 offset[3] 可以得知,源字符串的前 4 个字节被成功转换。

在分段读取大文件时就可以根据这样的信息来判断有没有出现字符截断的情况,如果有截断,根据 offset 的信息,抽取成功转换的字符串,留下被截断的字符。然后继续从文件中读取一个字节,和前面截断的字符合并转换,直到把截断字符的全部从文件中读出。

ICU 在 Unicode 字符串操作中的应用

Symphony 内部使用的字符串类都为 Unicode 字符串,使用的是 UTF-16 编码。原来的字符串对支持 surrogate pair 存在很大缺陷,我们借助 ICU API 增强字符串类的功能。

构建非 BMP 字符

如果一个字符的 Unicode 编码大于 0xFFFF,在使用 UTF-16 编码时需要用四个字节表示,这类字符就是前面提到过的非 BMP 字符,需要用 surrogate pair 来表示。在 Symphony 原来的字符串版本中不支持直接对 surrogate pair 的构造。清单 4 给出了 ICU 构造 surrogate pair 的代码。


清单 4. 构造 surrogate pair
				
 UChar buff[2] = {0};  
 int i = 0; 
 U16_APPEND_UNSAFE(buff, i, 0x20000); 

上述构造的结果:buff[0] 为 0xD840,buff[1] 为 0xDC00。

获取字符个数

使用 UTF-16 编码的字符串,在定义字符串长度时有两种方式。一种是定义长度为编码单元的个数,一种是实际字符的个数。只有字符串中不含非 BMP 字符时,两种长度定义的值才相等,因为一个非 BMP 字符要占用两个编码单元。Symphony 原来的字符串类中没有实现获取实际字符个数的函数。清单 5 给出了 ICU 相应的 API。


清单 5. 获取字符串中的字符个数
				
 UChar buff[6] = {0x4e2d, 0x56fd,0xd840,0xdc00}; 
 u_countChar32(buff, 4); 

上例返回结果为 3。字符串占用 4 个编码单元,包含 3 个字符。

字符替换

做字符替换,有四种情况:

  1. 被替换字符和替换字符均为 BMP 字符。直接替换。
  2. 被替换字符为 BMP 字符,替换字符为非 BMP 字符。字符串长度变长,需要借助字符串提供的插入函数来实现。
  3. 被替换字符为非 BMP 字符,替换字符为 BMP 字符。字符串长度变短,后续字符要依次往前挪位。
  4. 被替换字符和替换字符都为非 BMP 字符。直接替换。

因此判断字符是否为 BMP 字符在做字符替换时必不可少。ICU 提供了 U16_IS_SURROGATE 函数来判定字符串中指定位置的字符是否是 surrogate pair。清单 6 给出了示例代码。


清单 6. 判断 surrogate pair
				
 UChar buff[6] = {0x4e2d, 0x56fd,0xd840,0xdc00}; 
 U16_IS_SURROGATE(buff[1]); //false;
 U16_IS_SURROGATE(buff[2]); //true;
 U16_IS_SURROGATE(buff[3]); //true;

图 4 给出了具体流程。


图 4. 字符替换
图 4. 字符替换 

字符串的插入和替换

在做字符串插入操作时,需要指定插入的位置。如果指定的插入位置是一个 surrogate pair 的中间,那么插入操作就会破坏原来的字符,把 surrogate pair 分成两个没有意义的编码。

在做字符串替换操作时,需要指定替换开始的位置和终止的位置,同样存在可能破坏 surrogate pair 的可能。

为了避免上述情况,需要对指定的位置做修正处理,保证这个位置不在 surrogate pair 中间。ICU 提供了 U16_SET_CP_START 来确定字符的起始位置。


清单 7. 确定字符起始位置
				
 UChar buff[6] = {0x4e2d, 0x56fd,0xd840,0xdc00}; 
 Int idx = 1; 
 U16_SET_CP_START(buff, 0 , idx);  //idx:1 
 Idx = 3;
 U16_SET_CP_START(buff, 0 , idx);  //idx:2 

总结

本文介绍了字符编码,Unicode,ICU 的相关知识;此外还介绍了 ICU 在 Symphony 中的一些实际应用,用具体代码展示了 ICU 的使用。


参考资料

作者简介

付兴旺,IBM 中国软件开发中心研发工程师,北航硕士研究生,Lotus Symphony 产品国际化开发组成员,从事包含处理字符编码、字体匹配、混合语言文字 layout 的算法处理、双向文字处理等国际化方向的开发工作,此外还从事产品 build 脚本的开发。

贾彦民,IBM 中国软件开发中心研发工程师,Lotus Symphony 国际化开发组的 Lead,中科院软件所计算机软件理论博士,具有多年多语言支持字处理软件的开发经验,积极参与开源社区 OpenOffice.org 的开发。

来竞,IBM 中国软件开发中心研发工程师,北航计算机学院硕士研究生,Lotus Symphony 产品国际化开发组成员,从事多语言和 Locale 信息处理、字体匹配、复杂文本布局、双向文字处理等国际化方面的开发工作,并为 Symphony 多语言版本的翻译测试(TVT)提供技术支持。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值