PLT Scheme (DrScheme) で日本語フォントの表示が見にくいのを修正するパッチを提供します。
PLT Scheme が開発している無償の Scheme 統合開発環境である DrScheme は、日本語インターフェイスが提供されていますが、日本語の文字間が詰まりすぎていて非常に見にくく、 また、日本語がところどころ切れてしまっています。このような日本語表示を使うくらいなら、英語表示で使うほうがずっとましですし、 実際そうしている方が多いかと思います (下図参照)。
どうやらこれは、DrScheme が利用している GUI ライブラリである MrEd が、 日本語テキストに対して適切に横幅を計算してくれないことに原因があるようです。
実はこのことを、5年ほど前に PLT 開発チームに報告したのですが、未だに修正されていないんですよねぇ。
「まったくぅ~、お兄ちゃんはアタシがいないとナンにもできないんだからぁ~」(^^)
というわけで、ソースコードをハックして日本語表示を修正してみました。
動作環境は以下を想定しています。
結論から言いますと、plt-4.2.3/src/wxwindow/src/msw/wx_dc.cxx の wxTextSize 関数が原因でした (下図参照)。 この関数で、1文字の幅を計算しているのですが、Windows NT 系の場合、 GetCharABCWidthsFloat 関数を使用して文字幅の計算をしています。 この関数は、デバイスコンテキストに日本語フォントが選択されているなら、 正常に文字幅を計算してくれますが、日本語が含まれていないフォントが選択されていると、 正しく文字幅を計算してくれません。
Windows API のドキュメントに「既定文字の ABC 幅は現在選択されているフォントの範囲外の文字に対して使われます。」 と書かれていますね。MSDN の翻訳が下手なので意味が分かりづらいですが、つまり、フォントに含まれていない文字の幅は計算できないから、 何らかの既定の値を返しますよ、という意味です。
static void wxTextSize(HDC dc, Scheme_Hash_Table *ht, wchar_t *ustring, int d, int alen, double *ow, double *oh) { /* Gets the text size, caching the result in font when alen == 1 */ if ((alen == 1) && is_nt()) { double *sz; if (ht) { sz = (double *)scheme_hash_get(ht, scheme_make_integer(ustring[d])); } else { sz = NULL; } if (sz) { *ow = sz[0]; *oh = sz[1]; } else { ABCFLOAT cw; SIZE sizeRect; GetCharABCWidthsFloatW(dc, ustring[d], ustring[d], &cw); *ow = (cw.abcfA + cw.abcfB + cw.abcfC); GetTextExtentPointW(dc, ustring XFORM_OK_PLUS d, alen, &sizeRect); *oh = (double)sizeRect.cy; if (ht) { sz = (double *)scheme_malloc_atomic(sizeof(double) * 2); sz[0] = *ow; sz[1] = *oh; scheme_hash_set(ht, scheme_make_integer(ustring[d]), (Scheme_Object *)sz); } } } else { SIZE sizeRect; GetTextExtentPointW(dc, ustring XFORM_OK_PLUS d, alen, &sizeRect); *ow = (double)sizeRect.cx; *oh = (double)sizeRect.cy; } }
で、実際にこの関数に渡されてくるデバイスコンテキストに、 どんなフォントが設定されているか調べてみました。
wchar_t fontName[100]; GetTextFaceW(dc, 100, fontName);
そうしたら、デバイスコンテキストに選択されていたのは "Arial" フォントでした。 これでは日本語の文字幅を正しく計算できませんね。
じゃあ、どうしたらいいのかというと、 GetTextExtentPoint 関数で取得すればいいんでないの? と思うのですが。上記のコードでは、何故か横幅だけわざわざ GetCharABCWidthsFloat 関数で取得しているのが、 なんだか変なんですよねー。Windows NT 系でなければ、GetTextExtentPoint 関数を呼び出すだけのコードになっているのに。
まぁいいや。とにかく、GetTextExtentPoint 関数で文字の横幅を計算するようにしたら、 日本語がきれいに表示されるようになりました (下図)。
原因がわかったので、ソースコードを修正して再ビルドすれば、日本語がきれいに表示されます。
plt-4.2.3\src\worksp\mred\mred.sln を開いて、 wxwin プロジェクトの Source Files / WX_DC.cxx を開いて修正すればいいです。 後は、PLT Scheme の指示通りにビルドすれば、修正されたバイナリができあがります。
でもさー、こんな問題のためだけに PLT Scheme をいちいち再ビルドしたくないよねー。 まぁ、この修正を本家にフィードバックしておきましたけど、 いつ反映されるか分かりませんし。 そこでここでは、 PLT Scheme のサイトからダウンロードできるバイナリパッケージに対するバイナリパッチを作成してみましょう。
バイナリパッチって、どうやって作るんでしょうね! MrEd ライブラリは DLL として分離されていますから、この DLL だけ入れ替えれば、 一応はパッチを当てたことになりますね。 でも、そんなことをしたら、マイナーバージョン アップのたびに DLL を配布しないといけなくなり、 あんまりイケテないね!
じゃあ、wxTextSize 関数の部分だけを差し替えるパッチを作ってみようか。 でも、これをやるのは、大変なんだよね。 なんでかっていうと、wxTextSize 関数の逆アセンブルコードを見てみれば分かる。
static void wxTextSize(HDC dc, Scheme_Hash_Table *ht, wchar_t *ustring, int d, int alen, double *ow, double *oh) { 11017FC0 sub esp,14h /* Gets the text size, caching the result in font when alen == 1 */ if ((alen == 1) && is_nt()) { 11017FC3 cmp dword ptr [esp+1Ch],1 11017FC8 push ebp 11017FC9 mov ebp,dword ptr [esp+28h] 11017FCD push esi 11017FCE mov esi,eax 11017FD0 jne wxTextSize+0CBh (1101808Bh) 11017FD6 call is_nt (11016D10h) 11017FDB test eax,eax 11017FDD je wxTextSize+0CBh (1101808Bh) double *sz; if (ht) { 11017FE3 test edi,edi 11017FE5 je wxTextSize+57h (11018017h) sz = (double *)scheme_hash_get(ht, scheme_make_integer(ustring[d])); 11017FE7 mov eax,dword ptr [esp+20h] 11017FEB movzx ecx,word ptr [eax+esi*2] 11017FEF add ecx,ecx 11017FF1 or ecx,1 11017FF4 push ecx 11017FF5 push edi 11017FF6 call dword ptr [__imp__scheme_hash_get (11167700h)] 11017FFC add esp,8 } else { sz = NULL; } if (sz) { 11017FFF test eax,eax 11018001 je wxTextSize+57h (11018017h) *ow = sz[0]; 11018003 fld qword ptr [eax] 11018005 mov edx,dword ptr [esp+28h] 11018009 fstp qword ptr [edx] 1101800B pop esi *oh = sz[1]; 1101800C fld qword ptr [eax+8] GetCharABCWidthsFloatW(dc, ustring[d], ustring[d], &cw); *ow = (cw.abcfA + cw.abcfB + cw.abcfC); GetTextExtentPointW(dc, ustring XFORM_OK_PLUS d, alen, &sizeRect); *oh = (double)sizeRect.cy;
結局ね、scheme_make_integer とか GetTextExtentPoint とかは別の DLL に存在する関数なんだけど、 アセンブリコードはそれらの DLL 関数のサンクコード経由で呼び出されるわけね。 だけど、サンクコードのアドレスって、マイナーバージョン アップのたびに変わる可能性があるから、 決め打ちできない。とすると、それらのサンクコードのアドレスを解析して動的にコード生成するような パッチを作らないといけないことになる。そんな大変なことやりたくないよねー。
だから今回は、関数単位のパッチではなく、もっと簡単な方法でやることにした。 まず、wxTextSize 関数の次のコードに注目してみて。
if ((alen == 1) && is_nt()) {
GetTextExtentPoint 関数で文字幅を取得するんだったら、結局この条件ブロックは不要なんだよね。 このブロックをごそっと削除してしまえばいいはず。 だいたい、こういう条件分岐は、アセンブリコードでは test とか cmp とかの後で jnz とか je とかやってることが多いから、そういう条件付きジャンプ命令を無条件ジャンプ命令 jmp に書き換えてやればいいよね。
というわけで、次は、MrEd DLL 内の実際のアセンブリコードを抽出してみよう。 単純に逆アセンブルしてもいいけど、命令の書き換えを行うので、そのアセンブリコードも知る必要があることと、 正常動作することも確認したいので、ここでは OllyDbg を使ってみました。
MrEd ライブラリを使用している簡単なプログラムとして、PLT Scheme に付属の MrEd.exe を使います。 これを OllyDbg からデバッグ起動しましょう。OllyDbg のメニューから [File]-[Open] を選択して、 MrEd.exe を選択します。
実行前にブレークしますが、[Debug]-[Run] メニューで実行を継続しましょう。 PLT Scheme では MrEd DLL は実行時に動的にロードされるので、 実行前にブレークした段階では、MrEd DLL の中身がアドレス空間にマッピングされていません。 正常に起動すると MrEd REPL の次のような画面が表示されます。 この画面に日本語を入力しても、変な表示になります。
この段階で MrEd DLL がアドレス空間にマッピングされていますので、 中を覗いて wxTextSize 関数のアセンブリコードを抽出しましょう。 OllyDbg のメニューから [View]-[Executable modules] を選択して、 MrEd.exe が使用している DLL の一覧を表示します。
一覧から libmred3 をダブルクリックすると、DLL が逆アセンブルされます (実際のファイルは lib/libmred3m_6ncbi0.dll のような名前)。 次に、OllyDbg を右クリックして [Search for]-[All intermodular calls] メニューを選択します。
すると、モジュール間の呼び出しの一覧が現れます。次に、その一覧を右クリックして、 [Sort by]-[Destination] メニューを選択します。 そして、一覧の中から GetCharABCWidthsFlowW の行を見つけ出して、ダブルクリックしましょう。 この行は1つしかないと思います。つまり、libmred3 DLL からは、この関数を1か所でしか呼び出していないということです。 ということは、この呼び出しを行っているのが、wxTextSize 関数ということですね。
GetCharABCWidthsFlowW を呼び出しているアセンブリコードから、少し上に行くと、 wxTextSize 関数の先頭にたどり着きます。
予想したとおり、JNZ 命令による条件分岐がありますね。 これを無条件ジャンプに変えてみましょう。 JNZ の行を選択してスペースキーを押しますと、JNZ 命令の全文が出ますので、 JNZ の部分を JMP に変えて [Assemble] ボタンを押します。
そうすると、アセンブリコードが赤く変更されます。
この状態で、MrEd REPL に日本語を入力してみましょう。 きれいに表示されましたね!
上記のバイナリパッチを次からダウンロードできます。
これを使えば、PLT Scheme を再ビルドすることなく、日本語がきれいに表示されます。
DrScheme 4.0~4.2.3 で動作検証しています。
mred-font-fix.zip [41KB]