車輪の再発明であり、無駄とは思いつつも、勉強のためと使い勝手の良いコードコンバーターを実現するため、自前でsjis2004とunicodeのコード変換器を作りました。その際に、いろいろ解ったことをまとめておきます。
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ビットが持つ意味を並べてみます。
意味 | データ | マスク値 | マスク演算結果 | |
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 |
続いてビット構成です。
サイズビットのところで、utf8のバイトの先頭nビットがどういう意味を持つ予約データかがわかりましたので、次は残りのデータそのものがnバイトにどういう風に割り振られるかを表にまとめてみます。
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 |
ビット分割すればunicode番号とutf-8間でのデコードに事足ります。
ただし、この場合は、(読み書き時の)エンディアンに気をつけて下さい。
私は、IntelとPPC両方で動作する事を念頭に置いてコーディングしたため、データをバイト単位で扱い、表のような細かい分割をすることにしました。
(オーバーヘッドはどちらが大きいかは検証していません)
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と同じように、予約ビットの意味とデコードの仕方を以下にまとめます。
サロゲートペアは、サロゲートペアであることを示すビットを持っています。下表にまとめます。
意味 | データ | マスク値 | マスク演算結果 | |
2進数 | 16進数 | |||
サロゲートペアの1文字目 | 110110xx xxxxxxxx | 0b1111110000000000 | 0xfc00 | 0x6c00 |
サロゲートペアの2文字目 | 110111xx xxxxxxxx | 0b1111110000000000 | 0xfc00 | 0x6e00 |
上記でサロゲート文字だと言うことが解ったら、マスクビットを除外して、2バイト+2バイトを組み合わせ、unicode番号に変換しなければなりません。
これも、UTF-8の表2.の様に、bit分解と合成の仕方を表にまとめます。
unicode番号 | サイズ | UTF-16フォーマット |
ビット割り当て | ||
aaabb ccccddeeffgggggg |
1文字目 | 1101 10ww wwcc ccdd |
2文字目 | 1101 11ee ffgg gggg |
unicodeは合成文字を「そのまま」保持しても「基となる文字と結合文字に分解して」保存しても構わないという約束になっています。
どういうことか?というと、ëをeと¨の2文字に分解したり、「だ」を「た」と「゛」に分解して保存しても良いのです。
これは一見嬉しくないですが、(文字コードとして)eに¨や^などが付くものを沢山用意しなくても、基底文字と合成用文字を組み合わせることで、文字コードやフォントの節約になると考えたらしいです。