SourceForge.jp

Unicodeのテキストエンコーディング

 車輪の再発明であり、無駄とは思いつつも、勉強のためと使い勝手の良いコードコンバーターを実現するため、自前でsjis2004とunicodeのコード変換器を作りました。その際に、いろいろ解ったことをまとめておきます。

UTF-8

 unicode自体は16ビットのコード体系で、1文字を表すのに2バイト必要です。
しかし、データ量にケチなアメリカ人が、ANSI/iso-8859-xなら1バイトで済むものを、何故倍のファイルサイズに増やさねばならないのか!
と、クレームを付けた結果生まれた(?)のがutf-8というエンコーディングです。
特徴はデータの単位が1バイト×n個で1文字を構成し、データのサイズも文字の中に持っていること。です。 この約束により、8ビット目が0の文字は1バイトで表すことが出来るので、7bitで通常使う文字を充分表現出来るアメリカ人には、 UTF-8 = iso-8859-1となり、(BOMとかあるので違いますが)ファイルサイズを増やすことなく、unicodeへ移行できるというメリットがあります。
反面、7bitで表現できない文字については、文字サイズ情報分だけ1文字に必要なビット数が増えますので、1文字が8ビットでも2バイト消費し、16ビット文字は3バイト消費すると言うように、累進課税のようなファイルサイズ増加をもたらすことも特徴です。

サイズビット

 UTF-8に置いてはMSBからnバイトがサイズを示すビットとして機能します。そう、サイズビットの長さが固定長ではないのです。
このため、単純にビットマスクをかけて、得られた値からcase文で分岐してという単純なアルゴリズムでは、文字単位の取得、変換は出来ません。
この下に先頭からのnビットが持つ意味を並べてみます。

表1.UTF-8の先頭nビットが持つ意味
意味 データ マスク値 マスク演算結果
2進数 16進数
データサイズ=1 0xxx xxxx 0b10000000 0x80 0x00
続きバイト 10xx xxxx 0b11000000 0xc0 0x80
データサイズ=2 110x xxxx 0b11100000 0xe0 0xc0
データサイズ=3 1110 xxxx 0b11110000 0xf0 0xe0
データサイズ=4 1111 0xxx 0b11111000 0xf8 0xf0

 と、こんな感じになっています。

 続きデータ記号以外は、マスクした後に"1"が立っている数がデータの長さと言うことも出来ますが、ビットシフトしなければ数にならないため、ハードコードした方が早いのであまり意味がありません。
只の目印と割り切りましょう。

 現在のバイトと0x80, 0xe0, 0xf0, 0xf8とのビットマスク演算の結果を見比べて、続きnバイトを拾って初めて、エンコード変換という作業に入ることができるのです。面倒ですね。

ビット構成

 続いてビット構成です。
サイズビットのところで、utf8のバイトの先頭nビットがどういう意味を持つ予約データかがわかりましたので、次は残りのデータそのものがnバイトにどういう風に割り振られるかを表にまとめてみます。

表2.UTF-8エンコーディングのビット対応
unicode番号 サイズ 1バイト目 2バイト目 3バイト目 4バイト目
範囲 ビット割り当て
0x0000〜0x007f 0aaaaaaa 1byte 0aaaaaaa - - -
0x0080〜0x07ff 00000aaa bbccdddd 2byte 110aaabb 10ccdddd - -
0x0800〜0xffff aaaabbbb ccddeeee 3byte 1110aaaa 10bbbbcc 10ddeeee -
0x10000〜
0x2ffff
000aaabb
ccccddee ffgggggg
4byte 11110aaa 10bbcccc 10ddeeff 10gggggg
表中の赤字はutf-8バイト長用の予約ビットです。
 上表ではビットを細かく分割していますが、これは1バイトのままユニコード番号の中のビットと対応させるのに、バイト境界をまたがないように区切っているためです。
uint16,uint32型へ放り込んだ後デコードするのであれば、ここまで細かく分割する必要はありません。
それぞれ、データ長が、

  • 2byteなら、a,bとそれ以外に
  • 3byteなら、aと、b,cと、d,eに
  • 4byteなら、aと、b,cと、d,e,fと、gに
  •  ビット分割すればunicode番号とutf-8間でのデコードに事足ります。
    ただし、この場合は、(読み書き時の)エンディアンに気をつけて下さい。
    私は、IntelとPPC両方で動作する事を念頭に置いてコーディングしたため、データをバイト単位で扱い、表のような細かい分割をすることにしました。 (オーバーヘッドはどちらが大きいかは検証していません)

    UTF-16

     UTF-16は、基本的にはunicode番号=文字コードと思えばいいので、そんなに難しくないでしょう。
    と、思ったら大間違いで、1文字が2byteなので、エンディアンの影響をモロに受けます。
    このため、BOMによるバイトオーダーの判定が必須になります。

    そうです。ファイルの中にデータが「上位バイト-下位バイト」と並んでいても、「下位バイト-上位バイト」と並んでいても、どちらもUTF-16なのです。
    ちなみに、Windowsでは、UTF-16LEをUTF-16と呼び、MacintoshではUTF-16BEをUTF-16と呼ぶのが普通みたいです。
    データを直接いじる人間から見ると「ふざけんな!!」ってな感じです。使う人を舐めきった規格です。

    また、UTF-8の表2.で「あれ?unicodeって、16ビットのはずなのに、何故0x2ffffなんて数字が出てくるんだ?と思った人、正解です。 ここがunicodeの暗黒面(?)サロゲートペアと呼ばれるものです。
    unicodeは16ビットなので、そのままでは65536文字しか表現できません。
    しかし、世界中のありとあらゆる文字を詰め込んだunicodeがこんなに少ない範囲で世界中の文字にユニークなコードを割り当てることなど出来なかったのです。
    ところが、UTF-16というエンコーディングは既に決まって動き出していたので、16ビットで他の文字の邪魔をせず、16ビット以上の数字を割り当てる必要が出てきました。
    そこで、文字コードが0x6c00〜0x6dffの文字をサロゲートペア文字として、続くもう1文字を読み込み、UTF-8のように、ビット分解〜合成することで、 u-10000〜u-2ffffの文字を表現するようにしました。
    お陰で、文字コード=unicode番号というUTF-16の利点が台無しです。やれやれ。

     と言ってもいられないので、UTF-8と同じように、予約ビットの意味とデコードの仕方を以下にまとめます。

    サロゲート記号

     サロゲートペアは、サロゲートペアであることを示すビットを持っています。下表にまとめます。

    表3.サロゲートペアデータの先頭nビットが持つ意味
    意味 データ マスク値 マスク演算結果
    2進数 16進数
    サロゲートペアの1文字目 110110xx xxxxxxxx 0b1111110000000000 0xfc00 0x6c00
    サロゲートペアの2文字目 110111xx xxxxxxxx 0b1111110000000000 0xfc00 0x6e00

    ので、読み込んだデータは、必ず上記マスク演算により、サロゲート文字で無いことを確認してから、unicode番号として扱って下さい。
    これで、読み込み時に必ずサロゲート文字か?の判定というオーバーヘッドが発生することになりました。やれやれ

    サロゲート文字のデコード

     上記でサロゲート文字だと言うことが解ったら、マスクビットを除外して、2バイト+2バイトを組み合わせ、unicode番号に変換しなければなりません。
    これも、UTF-8の表2.の様に、bit分解と合成の仕方を表にまとめます。

    表4.サロゲートペアのビット対応
    unicode番号 サイズ UTF-16フォーマット
    ビット割り当て
    aaabb
    ccccddeeffgggggg
    1文字目 1101 10ww wwcc ccdd
    2文字目 1101 11ee ffgg gggg
    表中の赤字はutf-8バイト長用の予約ビットです。
    また、wwwwは wwww = (uuuuu = aaabb) - 1 の下4ビットという計算式で求めます。
     例によって、1バイト単位でビット分割しているので見づらいですが、ここでややこしいのは、突然、wwww,uuuuuという謎のビット列が出てくることです。 これは、表4でいうaaaabbという5ビットから、1を引いたものの下位4ビットをwwwwとしています。
    逆に言えば、UTF-16からunicode番号に変換するには、wwwwに1を足してからビット合成をしないと、正しいunicode番号にならないと言うことです。

    正規化(もどき)

     unicodeは合成文字を「そのまま」保持しても「基となる文字と結合文字に分解して」保存しても構わないという約束になっています。
    どういうことか?というと、ëをeと¨の2文字に分解したり、「だ」を「た」と「゛」に分解して保存しても良いのです。
    これは一見嬉しくないですが、(文字コードとして)eに¨や^などが付くものを沢山用意しなくても、基底文字と合成用文字を組み合わせることで、文字コードやフォントの節約になると考えたらしいです。