Home‎ > ‎

screen 4.1.0 で日本語の表示がおかしくなる件

2012/07/31

1. はじめに

Mac OS X には標準でscreenが導入されているけど,そのバージョンが 4.0.3 なので画面の縦分割(縦に分割線が入って画面が横に並ぶ)が出来ない.そこで,最新版である 4.1.0 開発版をコンパイルして使っていたのだが,日本語が正しく表示されないことがある.という訳で,原因を探ってみた...

2. 症状

いくつか症状があるのだけど,大きく分けて2つの不具合がある.
  • 記号などの全角文字が重なって表示される.カーソルをあてると表示が乱れ,縦の分割線が酷く崩れてしまい,使い物にならない.フォントを変えて記号が半角のように表示されたとしても,やはりカーソルをあてると乱れる.
  • 濁音・半濁音のひらがなやカタカナの後ろに ÿ (U+00FF) が表示され,その後の表示がずれていく.

3. 原因

3.1 全角幅の文字が半角幅になってしまう原因

根本的な原因は,Unicode における東アジア文字の文字幅の定義がAmbiguous (曖昧) になっている文字に関して,その扱いが,アプリやフォントで統一されていないからである.

例えば ■(U+25A0) の場合
25A0;A # BLACK SQUARE
と定義されている.この文字を扱う際に,screen は全角文字として処理し,ターミナルは半角文字として処理すると正しく表示されなくなる.そのため Ambiguous な文字をどう扱うのか,自分の環境にて統一しなくてはならない.
  • screen では encoding.c の utf8_isdouble() 関数で文字コードによって判断しており,ロケールが ja_ で始まっていれば(あと韓国と中国のロケールも)cjkwidth というグローバル変数が 1 にセットされ,その場合は Ambiguous な文字は utf8_isdouble() に対し true を返す.つまり,全角文字として処理される.
  • ターミナルでは,Mac OS X の標準についているターミナルは Ambiguous な文字を半角文字として処理する.これを変更するには SIMBL プラグインを導入してプラグインを入れる必要がある.プラグインは http://kita.dyndns.org/wiki/?TerminalEastAsianAmbiguousClearer で公開されている.なお Mac OS X 10.8 (Mountain Lion) のターミナルから,詳細設定で「Unicode 東アジアA (曖昧) の文字幅を W (広) にする」なるオプションが追加されたので,これのチェックを入れればよい.なんだか分かる人にしか分からないオプションではあるが...とても助かる!
  • 最後に使っているエディタで Ambiguous な文字を全角で扱うよう設定すればよい.vim であれば set ambiwidth=double とするなどである.
ちなみに,半角で統一するという手もあり,screen をそのように変更することも可能だと思うが,いかんせんマルチバイト文字と全角文字,つまり,バイト長と表示幅が混線しているような複雑な処理なので,どこにどういう影響がでるのか読み切れない.逆に,運が良ければ,screen のグローバル変数 cjkwidth を 0 にするだけでいいかもしれない.その際は,Mac OS X なら標準のターミナルで,フォントを Menlo あたりにすれば正しく表示されると思う.個人的には ※(U+203B) まで半角になると違和感がありますが...

3.2 濁音・半濁音の後の表示がおかしくなる原因

Mac OS X ではファイル名を UTF-8 で扱うのだが,正規化として NFD を採用している.例えば,ひらがなの「が」という文字の場合,Unicode では『「が」という文字』という表現と『「か」に濁音記号をつけた文字』という表現の二通りがある.後者の濁音記号というのが結合文字の一種であり,文字としてのコードが振られているが,実際はその前の文字と結合されるので,それ単独で表示されることはないというものである.この結合文字を使って,文字と濁音記号に分解する方に合わせる正規化を NFD という.

さて screen では,全角文字のような1文字で2カラム使う文字でややこしくなっている上に,結合文字といった1文字でカラムを使わない文字まで登場し,複雑な処理となっている.

ansi.c
693     if (curr->w_encoding == UTF8 && utf8_isdouble(c))
694         curr->w_mbcs = 0xff;
696     if (curr->w_encoding == UTF8 && c >= 0x0300 && utf8_iscomb(c))
697     {
        /* 結合文字の処理 */
722             utf8_handle_comb(c, &omc);
728         break;
729     }
        /* 多くのさまざまな処理 */
880     if (curr->w_mbcs)
881     {
882         curr->w_rend.mbcs = curr->w_mbcs = 0;
883         curr->w_x++;
884     }

例えば,693行目の utf8_isdouble(c) で全角文字か判断し,その場合 curr->w_mbcs に 0xff というフラグを設定している.そして,880行目でこのフラグを確認し,フラグをクリアした上で curr->w_x++ にてカラムを1つ余分に進めるといった処理である.ところが 696行目の utf8_iscomb(c) で結合文字と判断された場合,728行目で break してしまっている.これだと,全角文字でかつ結合文字の場合,w_mbcs に 0xff が入ったまま,次の文字に進む.次の文字がたまたま全角文字であれば問題ないが,普通の半角文字だとカラムを1つ余分に進めるということになってしまう.

さらに,
display.c
597     c = (c & 255) | (unsigned char)D_rend.font << 8;
599     if (D_mbcs)
600     {
601       c = D_mbcs;
604       D_mbcs = 0;
605     }
606     else if (utf8_isdouble(c))
607     {
608       D_mbcs = c;
609       D_x++;
610       return;
611     }
では,実際に表示する段階で,606行目で全角文字であるか判断し,その場合,文字コードをいったん D_mbcs に保存しておく.これは全角文字の左半分のカラム表示に相当する.そして次の右半分のカラム表示に際に,599行目で,保存してあった D_mbcs を取り出し,遅れて表示するという処理になっている.ちなみに,この全角文字の右半分のカラム位置には,0xff というダミーの文字コードデータが保存されているが,D_mbcs に保存しておいた文字コードで,変数 c が上書きされるため,本来はこのダミーの文字コードが画面に出てくることはない.「2 症状」での U+00FF が表示される不具合はこのあたりが関係する.
ともかく,全角文字に対しても,2カラム分の表示を1度だけ行うようにうまく処理しているが,ところが,先ほどの結合文字となると utf8_isdouble() で正しく判断されないのでうまくいかない.なぜなら,結合文字は ansi.c の722行目で,2つの文字コード(結合される文字と結合文字)に対し,Unicode の空き領域 (U+D800〜U+DFFF) に結合後の文字コードを1つ割り当てている.思うに,サロゲートペアな文字が来た場合,どうなるんだ?という疑問も湧くが,それは置いておいて,まずは utf8_handle_comb という関数だ.

encoding.c
1035 utf8_handle_comb(c, mc)
1043   isdouble = c1 >= 0x1100 && utf8_isdouble(c1);
       /* 空き領域管理の初期化など */
1060       combchars[0x800]->c1 = 0x000;
1061       combchars[0x800]->c2 = 0x700;
1064       combchars[0x801]->c1 = 0x700;
1065       combchars[0x801]->c2 = 0x800;
1069   root = isdouble ? 0x801 : 0x800;
1070   for (i = combchars[root]->c1; i < combchars[root]->c2; i++)
1071   {
       /* ここで空いているコードを探す */
1076   }
これが utf8_handle_comb 関数の中身の一部であるが,全角文字の場合 0x801 を起点として空きコードを探しているので,0x700〜0x800 が使われている.つまり Unicode の空き領域のうち U+D800 + 0x700 => U+DF00 から U+DFFF までが,全角文字を結合した時の文字コードになっている.(濁音・半濁音だけなら256文字で足りるけど...いいのか?)
そのため,display.c の 606 行目に,0xDF00 から 0xDFFF の場合も全角文字として判断するようにしないといけない.

補足だが @mrkn 氏が作成したパッチでは 0xD800 から 0xDFFF 全体を全角文字の条件にしていたので,半角文字の結合文字まで2カラム使うこと(結果,次の文字を飛ばすこと)になってしまう.

3.3 除算記号と乗算記号が正しく表示されない原因

最後に,インターネットで検索した限り報告している方を見つけることができなかったのだけど,除算記号と乗算記号に関して表示がおかしくなる問題もある.

screen では,マルチバイト文字を mchar 構造体の char 型である image と font メンバーにぞれぞれ,下位1バイトと上位1バイトに分けて保存する処理を行なっている.image が char 型なので仕方ないとはいえ,font というメンバーを借用するところ,そろそろ限界が近づいている気がする.ともかく,そういう訳で,除算記号÷(U+00F7) 乗算記号×(U+00D7) はマルチバイトなのに,font が 0 になってしまう.これが問題を引き起こす.

ansi.c
2375   if ((mc->font && ml->font == null)
2376   {
2377     if ((ml->font = (unsigned char *)calloc(p->w_width + 1, 1)) == 0)
これは MFixLine という関数の中身であるが,mline 構造体の処理化を行なっている.mline 構造体は名前の通り,一行分のデータを管理する構造体で,char 型の配列である image と font メンバーなどがある.2375行目では,mchar 構造体の font が 0 以外の時に,mline 構造体の font を確保している.
半角文字だけなら font がなくても問題ないが,全角文字はなんであれ,その右半分のカラム位置に,image と font を共に 0xff としたダミーの文字コードを保存して管理しているので font の領域が必要になる.

少しややこしいので,分かりやすく例をあげてみる.例えば,このような文字列を表示する場合

 表示文字 ÷  で  で 
 文字コード  0x0038   0x00F7  0x0034  0x3067  0x3066
 0x3099
(screen の内部では UTF-8 を UTF-16 に変換して処理している) 

この時の mline 構造体のメンバー image, font は下記のような状態になる

  [0]  [1]  [2]  [3]  [4]  [5]  [6]  [7] 
 image 0x38  0xF7  0xFF  0x34  0x67  0xFF  0x00  0xFF 
 font 0x00  0x00  0xFF  0x00  0x30  0xFF  0xDF  0xFF 

このようなデータ構造は,MPutChar 関数などを見れば分かる.

ansi.c
2696 MPutChar(p, c, x, y)
2700 {
2703     MFixLine(p, y, c);
2704     ml = &p->w_mlines[y];
         /* mchar 型の文字 c を mline型の文字列 ml の [x] に書き込む */
2709     if (c->mbcs)
2710       {
2713         ml->image[x + 1] = c->mbcs;
2718             ml->font[x + 1] = c->mbcs;
2713行目と2718行目で,0xff になっている c->mbcs を x+1 した全角文字の右半分のデータに保存している.

そういう訳で,上記の例の場合,文字単位で呼び出される MFixLine 関数は,[0],[1],[3],[4],[6] で実行されるが,font 領域が確保されるは,[4]の font が 0x30 の時である.つまり,[1] の font が 0 であるために,font 領域が確保されず,その直後の MPutChar の 2718行目の代入は意味を成さない.ちなみに null アドレスへの代入はエラーになりそうだが,screen での null とは unsigned char *null = (unsigned char *)xrealloc((char *)null, maxwidth); であり,空き領域に無駄に書き込むだけである.最初これに気が付かず,一度 0xff を代入したものがいつの間にか 0x00 になる現象に悩んだ.null が予約語だと他の言語で毒されて...メモリエラーにならない→null ではないと思い込み...orz

「シングルバイト文字は半角,マルチバイト文字は全角」という誤解が,例えば × (U+00d7) を,本来 Unicode は16ビット(あるいは21ビット)のマルチバイト文字なのにシングルバイト文字として処理してしまっている.まぁ 0x00ff 以下の多くは半角なので問題にはならないが,Ambiguous な文字を全角文字として扱うとするなら,ここはちゃんと区別しなくてはいけないということ.一文字を表現するのに必要なバイト数と,表示するのに必要なカラム数は,独立して管理すべきであると思う.

4 結論

縦分割しても日本語を正常に表示させるには Ambiguous な文字を全角で処理するようにすべてを統一する.
  • Mac OS X のターミナルであれば,Mountain Lion を使うか,SIMBL プラグインを導入する
  • 使っているエディタでそれぞれ設定するなり対処する
そして,結合文字を正しく処理させるために,screen に下記のパッチをあてる
diff --git a/src/ansi.c b/src/ansi.c
index d88e153..8d9703e 100644
--- a/src/ansi.c
+++ b/src/ansi.c
@@ -725,6 +725,10 @@ register int len;
                      LPutChar(&curr->w_layer, &omc, ox, oy);
                      LGotoPos(&curr->w_layer, curr->w_x, curr->w_y);
                    }
+                 if (curr->w_mbcs)
+            {
+                     curr->w_rend.mbcs = curr->w_mbcs = 0;
+            }
                  break;
                }
              font = curr->w_rend.font;
@@ -2371,7 +2375,7 @@ struct mchar *mc;
        }
     }
 #ifdef FONT
-  if (mc->font && ml->font == null)
+  if ((mc->font || mc->mbcs) && ml->font == null)
     {
       if ((ml->font = (unsigned char *)calloc(p->w_width + 1, 1)) == 0)
        {
diff --git a/src/display.c b/src/display.c
index 94c05f1..fb62d6d 100644
--- a/src/display.c
+++ b/src/display.c
@@ -603,7 +603,7 @@ int c;
            D_x += D_AM ? 1 : -1;
          D_mbcs = 0;
        }
-      else if (utf8_isdouble(c))
+      else if (utf8_isdouble(c) || (0xdf00 <= c && c < 0xe000))
        {
          D_mbcs = c;
          D_x++;

5 最後に

(上の画面で半角の結合文字 U+0305: COMBINING OVERLINE が正常に表示されていないが,これは使っているフォント Ritcy の問題で,vim, screen, ターミナルともに半角文字として正常に処理できている)

僕は自分の環境においては,この対処で問題なく作業ができている.しかし,UTF-8 でない環境だったり,自分が使わない文字(例えば,半角文字の結合 U+0305)において多少の不具合があっても,そんなに問題にならない.また,自分のソースを読み解く能力が低くて,誤解しているところもあるだろうし,ここに書いてあることが正しいとか全く保証できない.

ただ,一言言わせてもらうとすれば,ソースを読んでいて『全部書き換えてぇ!!!』という衝動に駆られたのは事実である.歴史のあるソフトだから仕方ないんだけど,過去を引きずり過ぎているような気がした.


Comments