本文是《在 XFree86 窗口系统中实现对 GB18030 的支持》的第二篇,将具体介绍如何在XFree86中实现对 GB18030的支持。
1. 在 libX11 中实现 GB18030 编码转换模块
前面已经介绍了在 XFree86 中支持一种文字编码的关键是在 libX11 中实现支持这种编码的一系列转换模块。 但是由于 GB18030 是单双四字节混和编码, 所以和其它常用的单双字节编码相比, 处理起来比较麻烦。 下面就简要介绍一下在 libX11 中实现 GB18030 编码转换函数的原理。 大家可以参考 XFree86 的源码和相关文档, 以便更好的理解以下内容。
1.1 如何将 GB18030 编码成 Compound Text
看过 Compound Text 文档, 大家就会知道它的最初用途是用来承载单字节或双字节编码的, 并不适合承载编码 GB18030 这样的多字节可变长度编码。其实 Compound Text 所能够承载的编码并不是通常意义的文本编码(encoding),而是字符集编码(CharSet encoding)。而 XFree86 系统最多仅支持双字节宽度的字符集编码。
要想用 Compound Text 承载 GB18030 编码的文本, 就必须先把 GB18030 转换成 Compound Text 所支持的编码。 目前可行的有以下两种方案:
1. 方案一、将 GB18030 拆分成两个双字节编码
首先需要注意的是 GB18030 编码中 0x0~0x7F 范围的单字节部分用 Compound Text 编码后就变成了 ISO8859-1:GL (GL 表示左半部分编码, 即 ISO8859-1 的七位码部分), 所以在拆分 GB18030 编码的时候, 这部分是不计入内的。
拆分 GB18030 的最直接的办法就是把其双字节部分作为一个独立的编码, 然后把四字节部分再编码成双字节, 成为另一个独立的编码。这样我们就可以将 GB18030 文本分解成一个单字节部分, 和两个双字节部分编码成 Compound Text 了。 GB18030 的双字节部分其实基本就是原来的 GBK 双字节编码部分, 它们之间仅有几十个码位的区别。 也就是说我们可以象处理 GBK 编码那样处理 GB18030 的双字节部分。 下面的任务就是处理四字节部分了。
将 GB18030 四字节部分编码成双字节最简单的办法就是把这部分编码从第一个码开始按顺序重新编码, 重新编码后 GB18030 的第一个四字节码 0x81308130 就变成了 0。 依次类推, 我们可以将 GB18030 四字节编码中与 Unicode 3.0 (即 ISO10646 第一平面) 相对应的部分编码成双字节。 但由于这种编码方式最多只能有 65536 个码位, 所以我们不可能将 GB18030 中与 Unicode 3.1 对应的所有码位都编成双字节码。 这也是这种编码方案最大的不足。 另外还有一种办法就是直接将 GB18030 四字节编码转换成对应的 UCS2 编码, 同样可以实现以上目的。
在实际实现时, 我们并没有使用将 GB18030 拆分成两个双字节编码的方案。 因为这种方案实现起来过于复杂, 而且还具有无法向 Unicode 3.1 标准过渡等问题。 实际使用的是下面一种方案。
2. 方案二、将 GB18030 编码成 UTF-8
前面已经介绍过,为了使 XFree86 平稳的从 Compound Text 过渡到 UTF-8,已经在标准的 Compound Text 中加入了一个特殊的模式用来编码 UTF-8文本。在 XFree86 中 Compound Text 编码转换函数会对这种编码模式做特别处理,以便能够正确转换编码在 Compound Text 中的 UTF-8 文本。详细细节请参见 XFree86 的源码(xc/lib/X11/lcCT.c)。
另外前面也介绍过,GB18030 在字汇上和 Unicode 3.0 标准是一一对应的,在码位上也给 Unicode 3.1 标准预留了足够的空间。也就是说,GB18030 编码和 Unicode 3.1 编码是一一对应关系,它们之间可以做双向转换而不损失任何信息。所以我们完全可以将 GB18030 编码的文本先转换成 UTF-8 编码,然后再转换成 Compound Text,而不丢失任何信息,反之亦然。
由于 libX11 中已经提供了通用的函数完成 CharSet encoding 与 Compound Text 之间的转换,并且支持 UTF-8 编码。所以我们只需要提供 GB18030 <-> UTF-8 的转换函数,就可以利用 libX11 的间接编码转换功能实现 GB18030 <-> Compound Text 的相互转换。这一过程对于应用程序来说是完全透明的,只要应用程序使用 libX11 所提供的标准的 Compound Text 处理函数,就可以正确无误的处理 GB18030 编码。目前常用的应用程序开发库,如 gtk+、QT 等都是这样做的。但不幸的是有少数应用软件是自己处理 Compound Text 的,如 emacs 和 xemacs。所以这种方案不能应用于这类软件。另外,gtk+ 从 1.2.9 开始便引入了一个错误,它在处理 Compound Text 文本时画蛇添足的做了一下过滤,把 0x80~0xFF 之间的一些控制字符过滤掉了,就破坏了编码成 UTF-8 的 Compound Text 文本。在最新发布的 TurboLinux 7.0 中已经修正了这个错误。
至此,将 GB18030 编码成 Compound Text 的问题已经解决了。接下来就要解决 GB18030 与其它编码之间的转换问题,以及显示的问题了。
1.2 GB18030 与其它编码之间的相互转换
以上介绍了如何将 GB18030 编码成 Compound Text 的问题,解决了这个问题后我们就可以在 GB18030 Locale 下运行的两个应用程序之间利用 Compound Text 交换数据了。但这还远远不够。
目前最常用的中文编码是 GB2312 和 GBK,台湾和香港地区常用的则是 BIG5 和 BIG5-HKSCS。GB18030 作为最新的汉字编码标准其实已经涵盖了前面这几种编码标准的绝大部分字汇,是名副其实的超集。因此,在使用传统中文编码的应用程序和使用 GB18030 的应用程序之间交换数据,是一个非常有用的功能。至少需要实现从传统编码程序到 GB18030 程序的单向数据传输。因此我们必须在 libX11 中实现把 GB2312、GBK、BIG5 等编码转换成 GB18030 编码的功能。更广泛的讲,GB18030 标准已经具备表示所有 Unicode 3.0 甚至 Unicode 3.1 字汇的能力,也就是说 GB18030 可以编码各国文字。所以将非中文编码转换成 GB18030 编码也是有意义的,尤其是日文和韩文。
要想实现从各种传统编码向 GB18030 编码转换可以借助于 glibc 的 iconv 模块,但这种方法的最大缺陷就是效率太低。幸运的是,在 libX11 中已经包含了许多常用字符集编码和 UCS4 之间的转换函数(参见 xc/lib/X11/lcUTF8.c 和 xc/lib/X11/lcUniConv/*)。所以我们可以利用这些函数通过 UCS4 编码做中介,实现 GB18030 和这些传统编码之间的转换。
1.3 GB18030 编码字符串的显示
前面已经提到,XFree86 仅支持双字节的字符集编码,所以直接用 GB18030 编码做字符集编码进行字符串显示是不行的。因此我们需要将 GB18030 编码的字符串先转换成多段单字节或双字节编码的字符串,然后才能送去显示。这个问题的解决办法其实已经在 1.1 节中提到了。
对于第一个方案,我们已经把 GB18030 编码的字符串分解为一个单字节编码部分和两个双字节编码部分,因此可以直接送去显示。单字节部分就是 ISO8859-1 编码,可以使用 ISO8859-1 编码的英文字库进行显示;双字节部分则需要中文字库的支持。在现有的应用软件中,mozilla 其实就是使用这种方法来显示 GB18030 文本的。mozilla 使用了 1.1 中所述的第一种方法编码 GB18030 文本,并将 GBK 兼容的双字节编码部分称做 gb18030.2000-0,将原四字节编码部分称做 gb18030.2000-1。因为 mozilla 不使用字符集来显示字符串,它使用自己的一套处理机制来完成类似于字符集的功能,因此只要 XFree86 支持这两种编码的字库,mozilla 就可以正确显示 GB18030 文本了。关于在 XFree86 中支持这两种字库的问题,我们将在后面介绍。
对于第二个方案,就更简单了。我们只需要用一个 iso10646-1 (UCS2-BE) 编码的 GB18030 中文字库就可以完成字符串的显示工作。而且现在所有符合 GB18030 标准的 TrueType 字库都使用了 Unicode (UCS2-BE) 编码作为字库索引,XFree86 的 xtt/freetype 字库模块可以直接访问这种字库而不用做任何编码转换。但是由于中文 TrueType 字库包含的字模数目太大,在 XFree86 中用变宽模式使用这种字库,速度太慢。所以我们只能用固定宽度的字符元(Char Cell)模式来使用中文字库,由此导致的直接后果就是所有英文字符变成全宽的了,效果非常难看。
解决这个问题的方法其实很简单,即使用字符集。将 GB18030 编码文本中 0x0~0x7F 的部分分离出来,使用半宽的英文字库进行显示。其余部分仍然使用中文字库进行显示。在实践中发现,由于 GB18030 还覆盖了 Unicode 中 0x80~0xFF 之间的编码,即 ISO8859-1 的右半部分。而这部分也是半宽的,所以同样不能使用中文字库进行显示。因此这部分也需要被分离出来使用英文字库进行显示。
TrueType 字库在显示低点阵字符的时候效果比较差,目前又没有可用的 GB18030 点阵字库,所以在实际应用中我们还把 GB18030 中常用的 GB2312 部分编码分离了出来单独用 GB2312 字库来显示,这样在低点阵的时候我们就可以用 GB2312 编码的点阵字库来显示这部分汉字,以获得比较好的显示效果。
1.4 实现 GB18030 编码转换模块
上面说了两种显示办法都需要将 GB18030 编码的字符串切割成多段单字节或双字节字符集编码的字符串才能调用相应的字库进行显示。好在 libX11 库中已经提供了一套现成的函数来完成类似的工作,这就是 libX11 中的 lcUTF8.c 模块。这个模块不仅实现了一整套传统字符集编码与UTF-8编码和UCS4编码之间的转换函数,还提供了将 Unicode 编码的字符串切分并转换成多段单、双字节字符集编码的函数。这个模块使用静态的转换码表来实现 Unicode 编码和传统字符集编码之间的转换。与使用 glibc 的 iconv 模块相比它的速度更快,而且占用的内存更小,因为所有 XFree86 软件仅在内存中共享一套转换码表。
前面已经介绍过,GB18030 编码和 Unicode 编码在字汇上有一一对应的关系,因此它和 UTF-8 编码也同样存在一一对应关系。而 lcUTF8.c 模块所提供的功能正是我们的 GB18030 编码转换模块所需要的。所以很自然的就可以想到我们可以在 lcUTF8.c 的基础上实现 GB18030 编码转换模块。
实际上我们并没有编写独立的 GB18030 模块,而是在 lcUTF8.c 中直接实现了对 GB18030 的支持。这样做的好处是不言而喻的。我们仅在 lcUTF8.c 中增加了几百行代码就解决了上面讲到的所有问题。
参照 lcUTF8.c 对 UTF-8 编码的处理方法,我们只需完成 GB18030<->UCS4 的相互转换工作就行了。GB18030<->Compound Text,GB18030<->CharSet encoding 等转换函数其实就是先将 GB18030 转换为 UCS4 然后在转换成 Compound Text 或 CharSet encoding。而 UCS4<->Compound Text,UCS4<->CharSet encoding 等转换函数和 lcUTF8.c 中原有的函数没有任何区别,可以共享同样的代码。因此关键问题就是如何实现 GB18030<->UCS4 的转换。解决这个问题的最直接的方法其实就是使用 glibc 的编码转换模块。由于使用 GB18030 编码的 X 应用软件都会将 Locale 设置为 GB18030,所以我们可以在 libX11 中直接使用 mbtowc 和 wctomb 两个函数在 libX11 中实现 GB18030 多字节字符和宽字节字符之间的转换。而且 glibc 中 wchar_t 使用的正是 UCS4 编码。由于 GB18030 是无状态编码,所以在 libX11 中使用 mbtowc 和 wctomb 是线程安全的。
在此仅列出 CharSet encoding 到 GB18030 编码(也就是 MultiByte 编码)的转换函数以供参考:
/* from XlcNCharSet to XlcNMultiByte */
static int
iconv_cstombs(conv, from, from_left, to, to_left, args, num_args)
XlcConv conv;
XPointer *from;
int *from_left;
XPointer *to;
int *to_left;
XPointer *args;
int num_args;
{
XlcCharSet charset;
char *name;
Utf8Conv convptr;
int i;
unsigned char const *src;
unsigned char const *srcend;
unsigned char *dst;
unsigned char *dstend;
int unconv_num;
if (from == NULL || *from == NULL)
return 0;
if (num_args < 1)
return -1;
charset = (XlcCharSet) args[0];
name = charset->encoding_name;
/* not charset->name because the latter has a ":GL"/":GR" suffix */
for (convptr = all_charsets, i = all_charsets_count-1; i > 0; convptr++, i--)
if (!strcmp(convptr->name, name))
break;
if (i == 0)
return -1;
src = (unsigned char const *) *from;
srcend = src + *from_left;
dst = (unsigned char *) *to;
dstend = dst + *to_left;
unconv_num = 0;
while (src < srcend) {
ucs4_t wc;
int consumed;
int count;
/* 调用 lcUTF8.c 提供的 CharSet encoding -> UCS4 编码转换函数
将 CharSet 编码字符转换成 UCS4 编码 */
consumed = convptr->cstowc(conv, &wc, src, srcend-src);
if (consumed == RET_ILSEQ)
return -1;
if (consumed == RET_TOOFEW(0))
break;
/* 调用 glibc 中的 wctomb 函数将 UCS4 字符转换成 GB18030 多字节字符 */
count = wctomb(dst, wc);
if (count == 0)
break;
if (count == -1) {
count = wctomb(dst, BAD_WCHAR);
if (count == 0)
break;
unconv_num++;
}
src += consumed;
dst += count;
}
*from = (XPointer) src;
*from_left = srcend - src;
*to = (XPointer) dst;
*to_left = dstend - dst;
return unconv_num;
}
另外,lcUTF8.c 中原先是没有 GBK<->UCS4 和 BIG5-HKSCS<->UCS4 转换模块的,我们参照 xc/lib/X11/lcUniConv 目录中其它转换模块的格式为 lcUTF8.c 增加了这两个模块。
至此,我们就在 libX11 中实现了 GB18030 的编码转换模块,XFree86 的 GB18030 内功已经修炼完成,下面就要开始修炼 Locale 描述文件、字库接口等外功了。
2. GB18030 Locale 的 XLC_LOCALE 文件
*nix 系统中编写国际化程序的关键就是底层 libc 库对 locale 的支持。每一种语言编码和国家/地域都对应一个 locale,其中提供了编码转换信息、日期货币格式、字符排序规则等信息。同样,要想让 XFree86 支持一个 locale,就必须有一个相应的 XLC_LOCALE 文件,其中描述了这种 locale 使用的文字编码、所有字符集编码以及字体集等信息。
GB18030 locale 对应的 XLC_LOCALE 文件内容如下:
# XFree86 NLS for Chinese encoding GB18030
# Modified from xc/nls/XLC_LOCALE/en_US.UTF-8
# by James Su
#
# XLC_FONTSET category
#
XLC_FONTSET
on_demand_loading True
object_name generic
# We leave the legacy encodings in for the moment, because we don't
# have that many ISO10646 fonts yet.
# 以下定义了 GB18030 locale 使用的所有字库和对应的字符集编码
# 字体集 fs0, fs1 对应单字节部分的 0x0~0x7F 和 0x80~0xFF
# fs0 class (7 bit ASCII)
fs0 {
charset {
name ISO8859-1:GL
}
font {
primary ISO8859-1:GL
vertical_rotate all
}
}
# fs1 class (ISO8859 families)
fs1 {
charset {
name ISO8859-1:GR
}
font {
primary ISO8859-1:GR
}
}
# 字符集 fs2, fs3 对应其它部分
# fs2 class (Chinese Han Character)
fs2 {
charset {
name GB2312.1980-0:GL
}
font {
primary GB2312.1980-0:GL
}
}
# fs3 class
fs3 {
charset {
name ISO10646-1
}
font {
primary GB18030-0
substitute GBK2K-0
}
}
END XLC_FONTSET
#
# XLC_XLOCALE category
#
XLC_XLOCALE
encoding_name GB18030
mb_cur_max 4
state_depend_encoding False
# 下面定义的是 GB18030 locale 使用的所有字符集的信息
# cs0 class
cs0 {
side GL:Default
length 1
ct_encoding ISO8859-1:GL
}
# cs1 class
cs1 {
side GR:Default
length 1
ct_encoding ISO8859-1:GR
}
# cs2 class
cs2 {
side GR
length 2
ct_encoding GB2312.1980-0:GL; GB2312.1980-0:GR
}
# cs3 class
cs3 {
side none
ct_encoding ISO10646-1
}
END XLC_XLOCALE
注意字符集 fs2 的定义,字符集编码使用的是 ISO10646-1,也就是 UCS2-BE,而字体是 GB18030-0。其实 GB18030-0 只是我们在 xtt 字库模块中增加的一个 ISO10646-1 的别名而已,目的是将 GB18030 字库与其它 ISO10646-1 字库区别开来。
字符集定义 cs0 ~ cs3 与字体集 fs0 ~ fs3 并没有直接的联系,它们的定义是独立的。定义 cs0 ~ cs3 的用途是告诉 libX11,在将 GB18030 字符串编码成 Compound Text 前先拆分成使用 ISO8859-1:GL,ISO8859-1:GR,GB2312.1980-0 以及 UTF-8 等不同字符集编码的四段字符串,然后在编码成一个 Compound Text 数据流。这样做的好处是,我们可以从 GB18030 locale 的应用程序向 GB2312 locale 的应用程序传输英文字符和 GB2312 中文字符;或向英文 locale 的应用程序传输纯 ASCII 码数据。
现在 XFree86 支持 GB18030 的工作已经基本完成,最后就差在 xtt 字体模块中添加对 gb18030-0 以及 gb18030.2000-0 和 gb18030.2000-1 的支持了。
3. 改造 xtt 模块
XFree86 中包含了两个常用的 TrueType 字体模块,xtt 和 freetype。相对而言,freetype 模块使用标准码表来实现对各种字符集编码的支持,改造起来相对简单些。但由于性能以及功能较 xtt 弱,所以人们常用 xtt 模块。xtt 模块采用编码转换模块来支持各种编码的字体。所有编码转换模块都被编译成动态库文件,可以在使用的时候动态加载和释放。下面就介绍一下怎样让 xtt 支持上述三种字体。
支持 gb18030-0 非常简单,因为它就是 ISO10646-1 (UCS2-BE编码)的一个别名而已。支持 gb18030.2000-0 也比较简单,只需把 xtt 原有的 GBK 模块拿来稍加改造就行了。gb18030.2000-1 就比较复杂了,因为它是 mozilla 专用模块,采用采用顺序编码的方式把 GB18030 的四字节部分重新编码,我们必须建立这种新编码和 UCS2-BE 编码之间的映射表才能编写 gb18030.2000-1 模块。好在 mozilla 的源码中有这张表,只需要转换一下格式就行了。按照 xtt 中其它编码转换模块的格式,我们可以将三种字体的代码写到一个模块中,主程序(main.c)的代码如下:
/* ===EmacsMode: -*- Mode: C; tab-width:4; c-basic-offset: 4; -*- === */
/* ===FileName: ===
Copyright (c) 1998 Takuya SHIOZAKI, All Rights reserved.
Copyright (c) 1998 X-TrueType Server Project, All rights reserved.
(省略注释)
Notice===
*/
#include "xttversion.h"
static char const * const releaseID =
_XTT_RELEASE_NAME;
#include "xttcommon.h"
#include "xttcap.h"
#include "xttcconv.h"
#include "xttcconvP.h"
typedef enum
{
GB18030_0,
GB18030_2000_0,
GB18030_2000_1
} CharSetMagic;
static CharSetRelation const charSetRelations[] = {
{ "gb18030", "2000", "0", GB18030_2000_0, { 0x40, 0xff, 0x81, 0xfe, 0x8140 } },
{ "gb18030", "2000", "1", GB18030_2000_1, { 0x00, 0xff, 0x00, 0x99, 0x0000 } },
{ "gb18030", NULL, "0", GB18030_0, { 0x00, 0xff, 0x00, 0xff, 0x3000 } },
{ "gbk2k", NULL, "0", GB18030_0, { 0x00, 0xff, 0x00, 0xff, 0x3000 } },
{ NULL, NULL, NULL, 0, { 0, 0, 0, 0, 0 } }
};
CODECONV_TEMPLATE(cc_gb18030_2000_0_to_ucs2);
CODECONV_TEMPLATE(cc_gb18030_2000_1_to_ucs2);
CODECONV_TEMPLATE(cc_font_gbk2k_to_ucs2);
static MapIDRelation const mapIDRelations[] = {
{ GB18030_2000_0, EPlfmISO, EEncISO10646,
cc_gb18030_2000_0_to_ucs2, NULL },
{ GB18030_2000_0, EPlfmUnicode, EEncAny,
cc_gb18030_2000_0_to_ucs2, NULL },
{ GB18030_2000_0, EPlfmMS, EEncMSUnicode,
cc_gb18030_2000_0_to_ucs2, NULL },
{ GB18030_2000_1, EPlfmISO, EEncISO10646,
cc_gb18030_2000_1_to_ucs2, NULL },
{ GB18030_2000_1, EPlfmUnicode, EEncAny,
cc_gb18030_2000_1_to_ucs2, NULL },
{ GB18030_2000_1, EPlfmMS, EEncMSUnicode,
cc_gb18030_2000_1_to_ucs2, NULL },
{ GB18030_0, EPlfmISO, EEncISO10646,
cc_font_gbk2k_to_ucs2, NULL },
{ GB18030_0, EPlfmUnicode, EEncAny,
cc_font_gbk2k_to_ucs2, NULL },
{ GB18030_0, EPlfmMS, EEncMSUnicode,
cc_font_gbk2k_to_ucs2, NULL },
{ -1, 0, 0, NULL, NULL }
};
STD_ENTRYFUNC_TEMPLATE(GB18030_entrypoint)
ft_char_code_t /* result charCodeDest */
cc_font_gbk2k_to_ucs2(ft_char_code_t codeSrc)
{
return codeSrc;
}
/* end of file */
可以看到 gb18030-0 到 UCS2-BE 的转换函数就是原封不动的将数据返回。gb18030.2000-0和gb18030.2000-1的转换函数由于太长就不列出来了。
至此,我们对 XFree86 的现代化改造已经彻底完工。接下来的任务就是要购买和安装符合 GB18030 标准的 TrueType 字库了,相信大家都会,就不做介绍了。
参考文献:
* glibc 的 infopage 中编码转换,locale 等章节;
* XFree86 的相关文档,XLFD,CTEXT,i18n 等,可以在 XFree86 的源码包中找到;
* XFree86 中 libX11 和 xtt 模块的源码(xc/lib/X11/*,xc/extras/X-TrueType/*)。