本文是《在 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/*)。
{
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/*)。