ICUを使う

IBMによるUnicodeライブラリ、ICUを使う。

2005.4.29新規公開。2000.6.14の日記に加筆、修正。

2005.5.15 更新。

インストール

Unicodeを扱うためのライブラリはいくつかあるが、IBMによるUnicodeライブラリICU "International Components for Unicode" を試してみる。

ICUはC++版とjava版がある。2011年1月現在の最新版はバージョン4.6。CLDR (Unicode Common Locale Date Repository) 1.9, Unicode 6.0に対応している。

次のサイトから入手できる。

あるいは、Fedora Linuxには、パッケージが含まれている (rpm名=libicu-devel)。

Unicodeへの変換

ICUでは文字列はUnicodeStringクラスのオブジェクトになる。データがシフトJIS、日本語EUCなどUnicode以外のときは、何らかの方法で変換してやる必要がある。

UNIXであればiconvが使えるので、それを使うのがいい。変換表をあれこれ使うと、思わぬところで文字化けしたりする恐れがある。iconvを使わないのなら、ICUの変換器を使ってもいい。UConverterクラスが文字コードの変換器。

次のソースコードは、日本語EUCからUnicodeへ変換したうえで、UnicodeStringオブジェクトを作る。

まずは、必要なヘッダをincludeする。

  3| #undef USE_ICONV
  4| 
  5| #include <stdio.h>
  6| #include <string.h>
  7| #include <assert.h>
  8| #ifdef USE_ICONV
  9|   #include <iconv.h>
 10| #else
 11|   #include <unicode/ucnv.h>
 12| #endif
 13| #include <unicode/unistr.h>
 14| #include <unicode/uchar.h>
 15| #include <unicode/schriter.h>

次の部分は、iconvを使った変換。iconv_open()で文字コードを指定し、iconv()で変換する。ただ、変換後の大きさが1000バイトに収まるという仮定を置いているので、実用にするときは、修正が必要。

変換したバイト列と文字コード名をUnicodeStringに与えて文字列オブジェクトを生成する。

 17| #ifdef USE_ICONV
 18| static const char* UTF8_CES = "UTF-8";
 19| static const char* EUCJP_CES = "eucJP-open";
 20| #endif
 21| 
 22| int main()
 23| {
 24|     const char* euc = "ABC日本語のテキスト\\/¥/\";
 25| #ifdef USE_ICONV
 26|     // エンコーディングの変換:iconvを使う場合
 27|     iconv_t cd = iconv_open(UTF8_CES, EUCJP_CES);
 28|     size_t left = strlen(euc);
 29|     char buf[1000];
 30|     char* p = buf;
 31|     size_t bufleft = 1000;
 32|     printf("euc = %x, p = %x\n", euc, p);
 33|     size_t r = iconv(cd, const_cast<char**>(&euc), &left, &p, &bufleft);
 34|     printf("euc = %x, p = %x\n", euc, p);
 35|     printf("r = %d, left = %d, bufleft = %d\n", r, left, bufleft);
 36|     *p = '\0';
 37|     iconv_close(cd);
 38|     UnicodeString str(buf, UTF8_CES);

今度はICUのコンバータを使ってみる。ucnv_open()でUConverterオブジェクトを生成し、それと変換元のバイト列をUnicodeStringに与えるだけでいい。

ucnv_open()に渡す文字コード名だが、日本語EUCの場合は、mappings/convrtrs.txtを見ると、次の3つがある。ibm-954でいいだろう。

名称ucnv_open()に渡す文字コード名コメント
ibm-33722_P12A-1999 "ibm-33722_VPUA" OK
ibm-33722_P120-1999 "ibm-33722" '\' => yen sign. NG
ibm-954_P101-2000 "ibm-954" OK

その他、aliasなどは、次のページで見れる;

 39| #else
 49|     UErrorCode error = U_ZERO_ERROR;
 50|     UConverter* cnv = ucnv_open("ibm-954", &error);
 51|     assert(U_SUCCESS(error));
 52|     UnicodeString str(euc, strlen(euc), cnv, error);
 53|     assert(U_SUCCESS(error));
 54| #endif

イテレータ

文字列オブジェクトを作るだけでは面白くないので、1文字ごとに属性を表示してみる。ICUにはイテレータクラスがある。UnicodeStringオブジェクトのイテレータとしては、StringCharacterIteratorを使う。

次のソース片では、East_Asian_Width属性を表示する。

 56|     StringCharacterIterator it(str);
 57|     for (UChar uc = it.first(); uc != it.DONE; uc = it.next())
 58|         printf("%04x(%d) ", uc,
 59|                (UEastAsianWidth) u_getIntPropertyValue(uc, UCHAR_EAST_ASIAN_WIDTH));
 60|     printf("\n");
 61|     
 62|     return 0;
 63| }

正規化

(2005.4.30 この節を追加。)

正規化とは

Unicodeは、既存の多くの文字集合の文字を収録しているため、複数のコード列が同じ文字を表す場合がある。これらをそのまま扱うのは検索などで困難が多いので、正規化して特定のコード列にまとめたほうがいい。

Unicodeでの正規化には、NFD, NFKD, NFC, NFKCの4種類がある。NFD/NFKDはできるだけ文字を分解する。NFC/NFKCはできるだけコードを結合する。実用では、通常は NFC で正規化する。例えば、ペーストされた文字列を正規化したうえで取り込むなど。

例えば、U+0041 U+030Aというコード列をNFCまたはNFKCで正規化すると次のようになる。

A
0041
U+030A
COMBINING RING ABOVE
C/KC
Å
00c5

ひらがなでも同様。NFD/NFKDは逆向きの変換を行う。


304b
U+3099
COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK
C/KC

304c

NFKC / NFKD は、まず互換分解 Compatibility Decomposition を行う。NFKCはその上で結合を行う。互換分解は、互換文字をより基本的な文字に置換するほかに、丸付き文字などを展開する。

テキストの検索などで利用価値があるかもしれない。しかし、外部からのデータを取り込むときにこの方法で正規化するのはやりすぎ。


2460
KC/KD
1
0031

32a4
KC/KD

4e0a

3336
KC/KD

30d8

30af

30bf

30fc

30eb

正規化するには

ICUでUnicode文字列を正規化するには、Normalizerクラスを用いる。次の例は、UnicodeStringオブジェクトであるstrを正規化するコード片。Normalizerオブジェクトは、イテレータとしても使える。

    Normalizer norm(str, UNORM_NFKC);
    for (UChar uc = norm.first(); uc != norm.DONE; uc = norm.next())
        printf("%04x ", uc);
    printf("\n");

Normalizerのコンストラクタに正規化方法を渡す。

UNORM_NFD正規分解
UNORM_NFKD互換分解
UNORM_NFC正規分解したうえで正規結合
UNORM_NFKC互換分解したうえで正規結合

リンク

文字の幅

(2005.5.15 この節追加。) 2000.6.28の日記を修正、加筆。

本来は、文字コードは文字の幅を規定しない。文字の幅や大きさ、色というものは表示上のスタイルであって、プレーンテキストでは表現しない、というのが建前だから。しかし、現実には、文字の幅は全角文字・半角文字として使い分けている。

Unicodeでは各コードの属性として文字の幅を持っている (East Asian Width)。試しに、その属性を利用してフォントを使い分けて表示してみる。

このサンプルは、単純に二つのフォントを用意して使い分けるだけなので、複雑な合字や右から左(BIDI)には対応していない。

まずはフォントを決めたり、表示するテキストを用意する。

  4| #include <stdio.h>
  5| #include <string.h>
  6| #include <assert.h>
  7| #include <X11/Xlib.h>
  8| #include <unicode/normlzr.h>
  9| #include <unicode/uchar.h>
 10| 
 11| const char* FONT_NAME_FULL
 12|     = "-misc-fixed-medium-r-normal-ja-18-*-*-*-*-*-iso10646-1";
 13| const char* FONT_NAME_HALF
 14|     = "-misc-fixed-medium-r-normal--18-*-*-*-*-*-iso10646-1";
 15| 
 16| const UChar text[] = {
 17|     // UnicodeData-3.0.0.txt
 18|     0x0041, 0x030a, // U+00C5 N
 19|     0x0041, 0x0300, // U+00C0 N
 20|     0x0043, 0x0327, // U+00C7 N
 21|     0x0049, 0x0301, // U+00CD N
 22|     0x00fc, 0x0301, // U+01D8 A or 0075 0308 0301
 23|     0x0227, 0x0304, // U+01E1 N or 0061 0307 0304
 24|     0x3042, 0x3099, // あ W ゛ W
 25|     0x304b, 0x3099, // U+304C が W
 26|     };
 27| 
 28| GC gc = NULL;
 29| XFontStruct* font_full = NULL;
 30| XFontStruct* font_half = NULL;

日本語のテキストの場合には、次のルールでマッピングする。

  • Wide文字は、常に全角にする
  • NarrowおよびNeutralは、常に半角にする。
  • Half-widthは、常に半角にする
  • Ambiguousは、常に全角にする。

あとは、違う属性の文字が現れたときにフォントを切り替えればいい。文字の幅は、UCHAR_EAST_ASIAN_WIDTHで取れる。また、結合文字 (combining character) かどうかはUCHAR_CANONICAL_COMBINING_CLASSで取れるので、基底文字 (base character) の場合だけx座標を進める。

 32| void onExposed(const XExposeEvent& e)
 33| {
 34|     int x = 10; int y = 30;
 35| 
 36|     XChar2b xc;
 37|     memset(&xc, 0, sizeof(xc));
 38| 
 39|     // 正規化しておく
 40|     Normalizer it(text, sizeof(text) / sizeof(UChar), UNORM_NFC);
 41| 
 42|     UEastAsianWidth width = (UEastAsianWidth) -1;
 43|     int cx = 0;
 44|     for (UChar uc = it.first(); uc != it.DONE; uc = it.next()) {
 45|         UEastAsianWidth cell_width =
 46|             (UEastAsianWidth) u_getIntPropertyValue(uc, UCHAR_EAST_ASIAN_WIDTH);
 47|         printf("%04x (%d) ", uc, cell_width);
 48|         xc.byte1 = (uc >> 8) & 0xff;
 49|         xc.byte2 = uc & 0xff;
 50| 
 51|         if (u_getIntPropertyValue(uc, UCHAR_CANONICAL_COMBINING_CLASS) == 0)
 52|             x += cx;
 53| 
 54|         // 幅が変わるときにフォントを切り替える
 55|         if (cell_width != width) {
 56|             // UAX #11: East Asian Width
 57|             // http://www.unicode.org/reports/tr11/
 58|             width = cell_width;
 59|             switch (cell_width) {
 60|             case U_EA_WIDE:
 61|             case U_EA_FULLWIDTH:
 62|             case U_EA_AMBIGUOUS:     // 日本語テキストでは全角にする
 63|                 cx = XTextWidth16(font_full, &xc, 1);
 64|                 XSetFont(e.display, gc, font_full->fid);
 65|                 break;
 66|             case U_EA_NARROW:
 67|             case U_EA_NEUTRAL:
 68|             case U_EA_HALFWIDTH:
 69|                 cx = XTextWidth16(font_half, &xc, 1);
 70|                 XSetFont(e.display, gc, font_half->fid);
 71|                 break;
 72|             default:
 73|                 assert(0);
 74|                 break;
 75|             }
 76|         }
 77| 
 78|         XDrawString16(e.display, e.window, gc, x, y, &xc, 1);
 79|     }
 80|     printf("\n");
 81| }

残りは、ウィンドウを準備したり、フォントを生成して、イベントループをまわすだけ。

 83| int main() {
 84|     // ウィンドウを準備する
 85|     Display* disp = XOpenDisplay(NULL);
 86|     Window top = XCreateSimpleWindow(disp, XRootWindow(disp, 0),
 87|                                      400, 200, 300, 50, 2,
 88|                                      BlackPixel(disp, 0),
 89|                                      WhitePixel(disp, 0));
 90|     XSelectInput(disp, top, ExposureMask);
 91|     XMapWindow(disp, top);
 92|     gc = XCreateGC(disp, top, 0, NULL);
 93| 
 94|     // フォントを生成する
 95|     font_full = XLoadQueryFont(disp, FONT_NAME_FULL);
 96|     font_half = XLoadQueryFont(disp, FONT_NAME_HALF);
 97| 
 98|     while (true) {
 99|         XEvent e;
100|         XNextEvent(disp, &e);
101|         if (e.type == Expose)
102|             onExposed(e.xexpose);
103|     }
104| 
105|     XFreeGC(disp, gc);
106|     XUnloadFont(disp, font_full->fid);
107|     XUnloadFont(disp, font_half->fid);
108|     
109|     return 0;
110| }

実行結果は、次のようになる。一応、「あ」に濁点も表示できている。単純に重ねて表示しただけだが。

とはいうものの、自分でフォントを切り替えて表示するのは、手間が掛かるわりに得られるものが乏しい。レイアウトエンジンを使ったほうがいい。多言語に対応したレイアウトエンジンには、Pangoやm17n libraryがある。

サイト内関連文書