ambiguous widthをもうちょっと何とかしたいメモ

emacs

一応解決している。

emacs23からは自動。内部的には(use-cjk-char-width-table)のお仕事。 ただしフォント設定によってはフォントの見た目と内部文字幅が乖離して悲しいことになる。

vim

以下を~/.vimrcとか/etc/vimrc.localあたりに書く。

if &encoding == 'utf-8'
  set ambiwidth=double
endif

gnome-terminal(libvte)

一応解決したことになっているが、不完全。

/etc/profile.d/vte_cjk_width.shてなファイルを作り、中身に以下の1行を書く(もしくは、同様の環境変数を設定した上でgnome-terminalを新しく or --disable-factory付きで起動する)。

export VTE_CJK_WIDTH=1

が、「□」などのambigous widthな文字に対するbackspace/delete/cursor処理等がうまくなく、Ctrl-L でclearしないとカーソルの表示位置と内部的なカーソル位置が合致しなくなる。 「□□□」などと並んだ場合、カーソル移動だけで正常に動作しなくなる。

「a□a」のときと「a漢a」のときで挙動が違うので、どこかで文字幅=1とみなしてしまうルーチンがある(widthを算出するときに_vte_iso2022_ambiguous_widthを呼ぶべきが抜けているルーチンがある)と仮定できるので、楽観的には直りそうに見えるが、たぶん全ケース列挙しないと思わぬ落とし穴にハマる。

libvteの中身

libvteのステートは以下の関数で管理されている。

struct _vte_iso2022_state *
_vte_iso2022_state_new(const char *native_codeset,
                       _vte_iso2022_codeset_changed_cb_fn fn,
                       gpointer data)
{
        struct _vte_iso2022_state *state;
        state = g_slice_new0(struct _vte_iso2022_state);
        state->nrc_enabled = TRUE;
        state->current = 0;
        state->override = -1;
        state->g[0] = 'B';
        state->g[1] = 'B';
        state->g[2] = 'B';
        state->g[3] = 'B';
        state->codeset = native_codeset;
        state->native_codeset = state->codeset;
        if (native_codeset == NULL) {
                g_get_charset(&state->codeset);
                state->native_codeset = state->codeset;
        }
        state->utf8_codeset = "UTF-8";
        state->target_codeset = VTE_CONV_GUNICHAR_TYPE;
        _vte_debug_print(VTE_DEBUG_SUBSTITUTION,
                        "Native codeset \"%s\", currently %s\n",
                        state->native_codeset, state->codeset);
        state->conv = _vte_conv_open(state->target_codeset, state->codeset);
        state->codeset_changed = fn;
        state->codeset_changed_data = data;
        state->buffer = _vte_buffer_new();
        if (state->conv == VTE_INVALID_CONV) {
                g_warning(_("Unable to convert characters from %s to %s."),
                          state->codeset, state->target_codeset);
                _vte_debug_print(VTE_DEBUG_SUBSTITUTION,
                                "Using UTF-8 instead.\n");
                state->codeset = state->utf8_codeset;
                state->conv = _vte_conv_open(state->target_codeset,
                                             state->codeset);
                if (state->conv == VTE_INVALID_CONV) {
                        g_error(_("Unable to convert characters from %s to %s."),
                                state->codeset, state->target_codeset);
                }
        }
        state->ambiguous_width = _vte_iso2022_ambiguous_width(state);
        return state;
}

こいつでステートを得て、最終的に以下のルーチンで「文字幅」として返す。

TODO: g_unichar_iswide_cjkは「wide幅の文字」+「ambiguous width」の集合でいいのか要確認。論理的にはそのはず。

int
_vte_iso2022_unichar_width(struct _vte_iso2022_state *state,
                           gunichar c)
{
        if (G_LIKELY (c < 0x80))
                return 1;
        if (G_UNLIKELY (g_unichar_iszerowidth (c)))
                return 0;
        if (G_UNLIKELY (g_unichar_iswide (c)))
                return 2;
        if (G_LIKELY (state->ambiguous_width == 1))
                return 1;
        if (G_UNLIKELY (g_unichar_iswide_cjk (c)))
                return 2;
        return 1;
}

現時点では_vte_iso2022_ambiguous_width(struct _vte_iso2022_state *state)でambiguous wideが処理されていて、UTF-8な環境でVTE_CJK_WIDTHにwideか1がセットされていれば問答無用で=2で返すようになる。

        if (strcmp (codeset, "utf8") == 0) {
          const char *env = g_getenv ("VTE_CJK_WIDTH");
          if (env && (g_ascii_strcasecmp (env, "wide")==0 || g_ascii_strcasecmp (env, "1")==0))
            return 2;
        }

ここで残る問題は、

  • 問題1: 日本語フォントの多くは、罫線幅が=2で構成されていて、グリフの幅も=2相当である。
    • たいていのterminfo+cursesでは罫線幅は=1が期待されており、=2で無理やり描画することでcursesが壊れる。
    • かといって=1で管理しようにも、罫線幅は=2なのでフォントレンダラが困る。
  • 問題2: libvte環境ではambiguous widthな文字を打ったあとCtrl-hするとキャレットの位置が狂う&文字が正常に消去されない。Ctrl-Lでclearさせると直る。

の2つ(だけか?)

ambiguous width関連の諸問題

ユニコード的なambiguous widthのお約束をいろいろ省略して書くと以下になる。

  • コンテキスト依存(ロケール&表示フォント&more)で幅を変更すべきグリフについては、ambiguous widthとして定義する。
  • ambiguous widthな文字のデフォルト値はnarrow(=1)とすべきである。
  • ambiguous widthな文字で、かつコンテキスト依存でwide幅(=2)として扱うもののみリストせよ。

see also: http://unicode.org/reports/tr11/

libvteのVTE_CJK_WIDTH(等)は、このambiguous width判定上、ambiguous widthと判定されたら問答無用で2を返せというhackである。

で、これをglibc::wcswidthならびにアプリケーション固有実装でそれぞれ実装している。みんながwcswidth()を使ってくれていればwcswidth()を直すだけという話になるが固有実装があるため、アプリケーションごとにアプローチを確認して直していく and/or wcswidthに置き換える、という対処が必要になる。

さらにwcswidth()とロケールの実装がかちあっていて、ja_JP.utf8でambiguous widthを拾いにいくと1が返ってくる。テーブルから適切に2が返ってくるように直すというのが手ではあるが、そうすると自前実装の既存のアプリケーションでは直らない(独自実装ものとwcswidthが見るテーブルは共有されていない)。

ここで「適切に」2を返すようにしても、行計算等でバグが出てくるアプリが相当数ありそうな気がする(実例がlibvteのCTE_CJK_WIDTH=1での異常動作)。要確認。

wcswidth()や各種アプリケーションが参照するテーブルはhttp://unicode.org/Public/UNIDATA/EastAsianWidth.txtをコンバートしたものが普通だが、このファイルは「理想フォント」(理想気体と同じニュアンスの「理想」)的なもので、各種等幅フォントの実情を反映していない。実際にはこのリストでNとされているがWなもの、WとされているがNなもの等がフォントファイル(に格納されたグリフ)に存在する。さらにAにいたってはフォントファイル依存でWかNか異なる。ここでいう「フォントファイル」は「フォントベンダ」に近い意味を持つが、イコールではないのでベンダごとブラックリストは機能せず、適用テーブルをもたせるならフォント名+バージョンから全パターン調査が必要。ホワイトリスト方式でも良さそうではあるが、選定するフォントがわりと宗教論争を招くと思われる。

面倒なことに、ここで2を返してもフォントに格納されているグリフによっては「見た目文字幅Nだが、内部的にはW」(ギリシャ文字・キリル文字等)というものが発生してしまい、桁揃えに異常を来たすことになる。

根本解決にはフォント毎にグリフ幅を取得する一種のテーブル生成プログラムを用いて「等幅」環境を実現すべきで、これを実装するととりあえず、一個人の利用環境としては「ズレない」表示が実現可能になる。ように思える。

……ところが、この方法はフォントに依存して文字幅が変動する結果を招くので、今度は「等幅テキスト」であるにもかかわらず表示するフォントによって表示桁がズレるカオスが発生する。これは困るので、理想的には

  • 「等幅フォントはこうあるべき」という等幅テーブル(理想的な等幅テーブル)をなんらかの合意のもとで作成し、
  • 表示に用いるフォントの幅を測定し、理想的な等幅テーブルに合致しないグリフはトーフとみなして別フォントで代替する

という仕組みを作ってレンダリングするようにしなくてはならない。これで相互可換性は確保できるが、ところが今度はその「理想的な等幅テーブル」と過去のドキュメントとの間に桁ズレが生じる余地が生まれてしまう。

ここまではCJK共通の事情だが、さらに日本特有の事情として、しばしば使われる罫線文字が日本国内でのみwide(=2)という呪いがある。結果、罫線を用いた幅揃えドキュメントはロケール依存で桁ズレを引き起こす。これを解決するには記述された文書の内容からヒューリスティックに言語判定を行い、そこで日本語文書と判断されたら罫線=2とする等のmachine unreadableな対処が必要になる上、理想的な等幅テーブルの策定上の齟齬となりうる。ので、このアプローチはうまくない。

そもそも論としてambiguous widthを含むテキストを等幅フォントで表示して整形文書として扱うのが負けの要因なので、現実的にユニコード系を捨てるのは厳しいので、もはやambiguous widthや罫線文字はある種の機種依存文字として扱い、可搬性の要求される文書から追放して、ambiguous width領域の既存の文字はステにして、各fullwide版を追加するほうがまだしもいいんじゃなかろうかと思えるが、このあたりunicode.org的な議論を確認する必要がある。

UbuntuJapaneseWiki: hito/WIP-ambiwidth (最終更新日時 2012-01-10 11:49:16 更新者 匿名)