Unicode 文字—十六進表記相互変換

昨夜,漢字の拡大表示の JavaScript 自作プログラム『老眼鏡』について書いた。その後,漢字の入力を,漢字そのものではなく,Unicode コードポイントでも可能とした。つまり,例えば森鷗外の「鷗」という文字を拡大表示したいとき,「鷗」の Unicode コードポイント U+9DD7 の 9dd7 を入力してもよいようにした。Enter キーを押しても実行されるようにも改修した。老眼鏡 Reading Glasses for the Aged からお使いください。もしブックマークに登録するならこのリンクをドラッグしてください。

かな漢字変換で出て来ない文字も,Unicode コードポイントがわかっていれば,これで表示してコピペして入力するという使い方もできる。牛丼の吉野家の「吉」は,商標では異体字の「𠮷」(上部が士ではなく土のキチ,いわゆる土吉。画面に文字が出てますか?)が本来のものだが,「𠮷」(U+20BB7)は Unicode CJK Unified Ideographs Extension B という拡張領域に定義されていて,ちょっと古いエディタ,ワープロでは扱えない文字である。最近のかな漢字変換ソフトウェアでも変換候補に出て来ないものが多いのではないだろうか。このように入力するにやっかいな文字があるとき,『漢字源』で Unicode を調べ『老眼鏡』にこれを入れれば,その文字が表示され,これをコピペして入力することができる。もちろん逆に,漢字を入力して Unicode コードポイントを調べることも可能である。文字は漢字だけでなく 4 オクテットまでの Unicode 文字なら何でも(キリル文字でも,古典ギリシア語複式アクセント文字でも,アラビア文字でも,サンスクリット文字でも,ハングルでも)OKのはずである。

20120224-jiten-yosi.png
『漢字源』で「𠮷」Unicode 値は 20BB7
20120224-tutiyosi.png
20bb7 を入力すると「𠮷」が表示される

さて,『老眼鏡』のために Unicode コードポイント十六進数と文字との間で変換を行う関数を二本書いた。unitohex 関数は引数に文字(「𠮷」)をとって,これを Unicode コードポイント十六進数文字列(「20BB7」)を返す。hextouni 関数はこの逆を行う。この二つの関数コードを以下に掲載しておく。こんなのはネットにごろごろ転がっていそうだが,適当なものがなかなか見当たらず,自分で書くことにした。

// -*- coding: utf-8; mode: js2; -*-
// Unicode 十六進 / 文字 相互変換
// - unitohex('文字'):        文字 -> Unicode 十六進コードポイント
// - hextouni('十六進文字列'): Unicode 十六進コードポイント -> 文字
// 2012 (c) coded by isao yasuda.
/*
UTF-8 符号化方式 (1-4 オクテットのみ)
Code point        octet1    octet2    octet3    octet4
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+0000-U+007F     0xxx xxxx
 (min)U+0000       000 0000
 (max)U+007F       111 1111
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+0080-U+07FF     110y yyyx 10xx xxxx
 (min)U+080          0 0010   00 0000
 (max)U+7FF          1 1111   11 1111
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+0800-U+FFFF     1110 yyyy 10yx xxxx 10xx xxxx
 (min)U+0800           0000   10 0000   00 0000
 (max)U+FFFF           1111   11 1111   11 1111
-----------------+0123 4567+0123 4567+0123 4567+0123 4567 
U+010000-U+1FFFFF 1111 0yyy 10yy xxxx 10xx xxxx 10xx xxxx
 (min)U+010000          000   01 0000   00 0000   00 0000
 (max)U+1FFFFF          111   11 1111   11 1111   11 1111
*/
// 文字 -> Unicode 十六進コードポイント変換
function unitohex(c) {
    // %xx%yy%zz 形式にエスケープする
    var w = encodeURI(c);
    if (w.length > 9) {// 4 オクテット以上の Unicode 文字
        // U+010000 - U+1FFFFF 拡張領域文字
        // UTF-8 デコード
        // 1. 符号化 UTF-8 ビットパターンの取得
        w = w.replace(/%/gi, "");
        var h = "";
        for (var i = 0; i < w.length; i++) {
            // 二進数文字列に変換
            var bstr = parseInt(w.charAt(i), 16).toString(2);
            // 4 bit 前ゼロ付加
            var dig0 = "";
            for (var j = 0; j < (4 - bstr.length); j++) {
                dig0 += "0";
            }
            h += dig0 + bstr;
        }
        // 2. Code Point 部の bit を抽出
        var uar = [];
        uar[0] = parseInt(h.charAt(5),2);
        uar[1] = parseInt(h.substring(6,8) + h.substring(10,12),2);
        uar[2] = parseInt(h.substring(12,16),2);
        uar[3] = parseInt(h.substring(18,22),2);
        uar[4] = parseInt(h.substring(22,24) + h.substring(26,28),2);
        uar[5] = parseInt(h.substr(28,4),2);
        // 3. Code Point digit (4 bit) 毎に十六進数文字列に変換
        w = "";
        if (uar[0] != 0) {// 0, 1 以外ありえない
            w = uar[0].toString(16);
        }
        for (var i = 1; i < uar.length; i++) {
            w += uar[i].toString(16);
        }
    } else {// 3 オクテット (エンコード形式で 9 文字) 以下
        // U+FFFF までの BMP 領域文字: オクテット毎に Latin1 と同じ扱いでよい
        w = (c.charCodeAt(0)).toString(16);
        var dt = 4 - w.length;
        for (var i = 0; i < dt; i++) {
            w = '0' + w;
        }
    }
    return w;
}
// Unicode 十六進コードポイント -> 文字 変換
function hextouni(cp) {
    // 入力チェック [0-9a-fA-F] 以外の入力を弾く]
    if (cp.match(/[^0-9a-fA-F]/)) {
        alert("入力に十六進数以外の文字が含まれています");
        return;
    }
    // 一文字ずつ bit 変換 (4 bit 境界)
    cp = cp.replace(/^0*/, ""); // 前ゼロサプレス
    var bits = "";
    for (var i = 0; i < cp.length; i++) {
        var bs = parseInt(cp.charAt(i), 16).toString(2);
        var dt = 4 - bs.length;
        for (var j = 0; j < dt; j++) {
            bs = '0' + bs;
        }
        bits += bs;
    }
    // 有効桁数で 1-4 オクテット符号の場合分け
    var wb = bits.replace(/^0*/g, "");
    var u8bits = "";
    // 各グループのビットパターンに埋め込み UTF-8 にエンコードする
    if (wb.length < 8) {
        // 1 octet Unicode U+0000-U+007F
        return unescape('%' + cp);
    } else {
        if (wb.length < 12) {
            // 2 octet Unicode U+0080-U+07FF
            bits = regbits(bits, 12);
            // 0123 4567+0123 4567
            // 110y yyyx 10xx xxxx
            u8bits = '110' + bits.substring(1,6) + 
                '10' + bits.substring(6,12);
        } else {
            if (wb.length < 17) {
                // 3 octet Unicode U+0800-U+FFFF
                bits = regbits(bits, 16);
                // 0123 4567+0123 4567+0123 4567
                // 1110 yyyy 10yx xxxx 10xx xxxx
                u8bits = '1110' + bits.substring(0,4) +
                    '10' + bits.substring(4,10) +
                    '10' + bits.substring(10,16);
            } else {
                if (wb.length < 22) {
                    // 4 octet Unicode U+010000-U+1FFFFF
                    bits = regbits(bits, 24);
                    // 0123 4567+0123 4567+0123 4567+0123 4567 
                    // 1111 0yyy 10yy xxxx 10xx xxxx 10xx xxxx
                    u8bits = '11110' + bits.substring(3,6) +
                        '10' + bits.substring(6,12) + '10' +
                        bits.substring(12,18) + '10' +
                        bits.substring(18,24);
                } else {
                    alert("5バイト以上のUnicode文字は未サポート");
                    return;
                }
            }
        }
    }
    // 4 bit 毎に十六進変換
    var u8hex = "";
    for (var i = 0; i < u8bits.length; i+=4) {
        u8hex += parseInt(u8bits.substring(i,i+4), 2).toString(16);
    }
    // 十六進文字 2 個毎に % を付加し,urlencode 形式に変換
    var u8enc = "";
    for (var i = 0; i < u8hex.length; i+=2) {
        u8enc += '%' + u8hex.substring(i,i+2);
    }
    // urldecode で文字に復元し返却
    return decodeURI(u8enc);
}
// bit 正規化 (4 bit 境界にする)
function regbits(bs, bc) {// bit 文字列,正規化数
    var dt = bc - bs.length;
    if (dt < 0) {// 仮に要求よりも短い場合そのまま返却
        return bs;
    }
    // 短い数だけ前0を付加する
    for (var i = 0; i < dt; i++) {
        bs = '0' + bs;
    }
    return bs;
}

UTF-8 の符号化方式は,コードポイントの領域によってオクテット(バイト)数が変化するので,その判定とビット列の取り扱いとがオペレーションの核である。JavaScript コードの先頭コメントに 1 〜 4 オクテットの符号化方式を整理してある。これまで Perl なんかではしょっちゅうこのオペレーションが必要だったので,サブルーチンを書いて使っていたが,JavaScript でははじめてだったので勉強になった。Perl だと pack 関数と ord 関数を用いて UTF-8 Unicode / 十六進数の間をラクに往来できるのだが,JavaScript だと馴れないためか苦労した。もう少し賢いやり方があるかも知れない。それでも encodeURI, decodeURI, unescape などの便利な関数のお世話になった。

久しぶりにトニー・グラハムの書いた『Unicode標準入門』を引っ張り出して UTF-8 符号化方式をおさらいした。最近,技術評論社から文字コードのよい解説書が出た。ともにアマゾンリンクをあげておきます。

Unicode標準入門
トニー・グラハム
関口正裕 監修
乾和志,海老塚徹 訳
翔泳社