ImageMagickの文字入れ機能のやっかいな挙動
ImageMagickにはannotationという便利機能があって、画像の上に文字を入れることができます。
しかしこの機能には1つ難点があって、フォントに存在しない文字を使うとその文字が?に変換されてしまいます。
オリジナル
Osakaフォントで"Hīhīwai ハワイ"を文字入れした結果
convert -pointsize 40 -gravity south -annotate 0 "Hīhīwai ハワイ" -font /Library/Fonts/Osaka.ttf -fill red original.jpg out1.jpg
ごらんのとおり、"ī"という文字が"?"に変換されてしまいます。
Arielフォントで"Hīhīwai ハワイ"を文字入れした結果
convert -pointsize 40 -gravity south -annotate 0 "Hīhīwai ハワイ" -font /Library/Fonts/Arial.ttf -fill red original.jpg out2.jpg
欧米フォントを使えば"ī"は大丈夫ですが今度は日本語が"?"になってしまいます。
問題の本質は何か?
単純な解決策としては、文字種の多いフォントを使えばたいていはうまくいきます。
例えばメイリオを使えばハワイ語と日本語と同時に表示できます。
しかしメイリオとてあらゆる文字に対応しているわけではありません。
真の問題は、文字化けしているのにエラーにならず正常終了してしまうことです。
成功したのか失敗したのかが全く検知できない。これは痛い。
ImageMagick本体にパッチをあててみる
というわけでImageMagick魔改造計画スタートです。
目標は、「フォントに対応する文字がない場合は、"?"に変換するのではなくエラーを吐いて死ぬ」挙動に変えること。
調査開始
ImageMagick-6.9.1-4のソースコードを取得して、GDBでデバグしながら挙動を追ってみました。
まず、文字入れ機能は"annotate.c"というファイルで処理されてることがわかります。
さらに追っていくとAnnotateImage()という関数があり、それが文字入れ処理をやっているようです。
この関数の中で、
/*
Annotate image with text.
*/
status=RenderType(image,annotate,&offset,&metrics);
if (status == MagickFalse)
break;
というそれっぽい処理があります。
さらに中を追っていって、RenderType() → RenderFreetype() と読み進めたところで
1324行目あたりにあやしい行が見つかりました。
/*
Render UTF-8 sequence.
*/
glyph.id=FT_Get_Char_Index(face,GetUTFCode(p));
if (glyph.id == 0)
glyph.id=FT_Get_Char_Index(face,'?');
if ((glyph.id != 0) && (last_glyph.id != 0))
この'?'があやしい。
めっちゃあやしい。
ここをじっと見ていると、「FT_Get_Char_Indexの戻り値がゼロ」=「フォントに当該文字が存在しない」→ 「'?'文字に置き換えられる」という仮説が浮かんできました。
FT_Get_Char_Indexを調べてみる
検索してみると、"FT_Get_Char_Index"というのはImageMagickが持っている関数ではなくて別のライブラリの関数であることがわかります。
ぐぐったら定義が出てきました。
FT_EXPORT( FT_UInt )
FT_Get_Char_Index( FT_Face face,
FT_ULong charcode );
Return the glyph index of a given character code. This function uses a charmap object to do the mapping.
input
face
A handle to the source face object.
charcode
The character code.
return
The glyph index. 0 means ‘undefined character code’.
ほぅほぅ。
「戻り値の0は"未定義文字"を意味する」と書いてあります。
さっき立てた仮説は正しいような気がしてきました。
仮説をためしてみる
さてannotate.cに戻って、
glyph.id=FT_Get_Char_Index(face,'?');
の?を%に変更して、ビルドして実行してみれば、仮説を検証できそうです。
というわけでやってみます。
- glyph.id=FT_Get_Char_Index(face,'?');
+ glyph.id=FT_Get_Char_Index(face,'%');
ImageMagickを再ビルドして、さっきと同じコマンドで文字入れしたところ、
ビンゴーーーーー!!!!
できました!!!
'?'が'%'に変わりました!!!
仮説が正しいことが実証されました。
当該箇所で例外を発生させる
そうすると、あとはこのタイミングでエラーを吐いて異常終了させればよいことになります。
?になる代わりに例外を投げればよさそうです。
周辺のコードを見ながら、見よう見まねでこんな例外処理を書いてみました。
if (glyph.id == 0)
- glyph.id=FT_Get_Char_Index(face,'?');
+ ThrowBinaryException(TypeError,"UnrecognizedCharacter", p);
ビルドしなおして、文字入れを実行してみます。
$ convert -pointsize 40 -gravity south -annotate 0 "Hīhīwai ハワイ" -font /Library/Fonts/Osaka.ttf -fill red original.jpg out3.jpg
convert: UnrecognizedCharacter `īhīwai ハワイ' @ error/annotate.c/RenderFreetype/1325.
$ echo $?
1
できたーーーーーー!!!!!
エラーメッセージを吐いてexit status=1で終了させることができました。
見事、文字入れの失敗を検知することができました。
まとめ
- ImageMagickでは画像に文字を入れることができるよ。
- 特殊文字を使うと"?"に変換されてしまうよ。でもエラーは出ないよ。
- ImageMagickにパッチを当てれば、そこをエラーで異常終了にすることができたよ