概要
全世界向けに提供しているサービスで、サーバーでユーザーが指定した文字列を画像化する必要がありました。
ImageMagickで行おうと思ったら、3時間はまりました。
これを読むと、人生の時間を3時間節約できます。
結論
Unifontを使ってください。
コマンド例1 (Ubuntuでapt-get installをする場合)
# Unifontをインストール
sudo apt-get install -y unifont
# unifont.ttfの場所を確認
fc-match --format=%{file} unifont
# input.pngにテキストを描画し、output.pngとして保存
convert input.png -gravity southeast -font $(fc-match --format=%{file} unifont) -draw "text 0,0 'うんこABC'" output.png
コマンド例2 (直にダウンロードする場合)
# Unifontをダウンロード
wget http://unifoundry.com/pub/unifont/unifont-11.0.01/font-builds/unifont-11.0.01.ttf
# input.pngにテキストを描画し、output.pngとして保存
convert input.png -gravity southeast -font /path/to/unifont-11.0.01.ttf -draw "text 0,0 'うんこABC'" output.png
経緯
日本語が表示されない
MiniMagickで以下のようなコードを実行しようとしたら、日本語が表示されなかった。
MiniMagick::Tool::Convert.new do |image|
image << input_path
image.gravity 'southeast'
image.draw "text 0,0 'うんこABC'"
image << output_path
end
順に以下の項目を確認した。
- 送っている文字列の文字コードは問題ないか?
- policy.xmlが問題ないか?
- フォントが問題ないか? (<- 結局これだった)
送っている文字コードはUTF8で問題なかった
二つの方法で確認した。
一つ目はネットで動作報告のあるコードと比較する方法。
MiniMagickで日本語が表示されないと見比べたが、問題なさそう。
二つ目は、ファイル経由で読み込ませる方法。
そのために、label機能を使う必要があった。
以下のようなコードになる。
MiniMagick::Tool::Convert.new do |image|
image << input_path
image.gravity 'southeast'
File.write('test.txt', 'うんこABC')
image.label '@test.txt'
image << output_path
end
実行したら、以下のようなエラーが出た。次項へ続く。
convert.im6: not authorized `@test.txt' @ error/property.c/InterpretImageProperties/3057.
policy.xmlとセキュリティホール
前項のエラーをぐぐったら、ImageMagickのセキュリティホールをふさぐためのものらしい。
たしかに、今回のユースケースのように、ユーザーの入力を複雑なロジックで処理するようなケースは、セキュリティホールが生まれやすいことに気付く。
※ ちゃんと守りたいならapparmorを使えば良い。以前、ユーザーが任意のネイティブコードを実行できるようなサービスを運営したときに使っていた。
ぐぐって、/etc/ImageMagick/policy.xmlの以下の行をコメントアウトする。
<policy domain="path" rights="none" pattern="@*" />
念のために/etc/ImageMagick/policy.xml全体も記載。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
<!ELEMENT policymap (policy)+>
<!ELEMENT policy (#PCDATA)>
<!ATTLIST policy domain (delegate|coder|filter|path|resource) #IMPLIED>
<!ATTLIST policy name CDATA #IMPLIED>
<!ATTLIST policy rights CDATA #IMPLIED>
<!ATTLIST policy pattern CDATA #IMPLIED>
<!ATTLIST policy value CDATA #IMPLIED>
]>
<!--
Configure ImageMagick policies.
Domains include system, delegate, coder, filter, path, or resource.
Rights include none, read, write, and execute. Use | to combine them,
for example: "read | write" to permit read from, or write to, a path.
Use a glob expression as a pattern.
Suppose we do not want users to process MPEG video images:
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
Here we do not want users reading images from HTTP:
<policy domain="coder" rights="none" pattern="HTTP" />
Lets prevent users from executing any image filters:
<policy domain="filter" rights="none" pattern="*" />
The /repository file system is restricted to read only. We use a glob
expression to match all paths that start with /repository:
<policy domain="path" rights="read" pattern="/repository/*" />
Any large image is cached to disk rather than memory:
<policy domain="resource" name="area" value="1GB"/>
Define arguments for the memory, map, area, and disk resources with
SI prefixes (.e.g 100MB). In addition, resource policies are maximums for
each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
exceeds policy maximum so memory limit is 1GB).
-->
<policymap>
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
<!-- <policy domain="resource" name="memory" value="2GiB"/> -->
<!-- <policy domain="resource" name="map" value="4GiB"/> -->
<!-- <policy domain="resource" name="area" value="1GB"/> -->
<!-- <policy domain="resource" name="disk" value="16EB"/> -->
<!-- <policy domain="resource" name="file" value="768"/> -->
<!-- <policy domain="resource" name="thread" value="4"/> -->
<!-- <policy domain="resource" name="throttle" value="0"/> -->
<!-- <policy domain="resource" name="time" value="3600"/> -->
<policy domain="coder" rights="none" pattern="EPHEMERAL" />
<policy domain="coder" rights="none" pattern="URL" />
<policy domain="coder" rights="none" pattern="HTTPS" />
<policy domain="coder" rights="none" pattern="MVG" />
<policy domain="coder" rights="none" pattern="MSL" />
<policy domain="coder" rights="none" pattern="TEXT" />
<policy domain="coder" rights="none" pattern="SHOW" />
<policy domain="coder" rights="none" pattern="WIN" />
<policy domain="coder" rights="none" pattern="PLT" />
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
</policymap>
これで実行してみたら、エラーは出ないが、なぜか普通のアルファベットとかも描画されなくなってしまった。理由はわからないが、一旦この方針はPEND。
フォントの問題を疑う
フォントが日本語に対応していない可能性を探る。
そのために日本語に対応しているフォントを探す。
全世界の文字に対応しているフォント NotoFontを見つける
googleがフォントを提供していた記憶から、全世界の文字に対応しているらしいNotoFontにたどりつく。
が、俺氏、ここでディスク容量をケチろうとするあまり、全世界の文字に対応していないほうのフォントをダウンロードしてしまうorz
結果、小一時間ハマる。
ややこしいのが、全世界の文字に対応していないフォントが582言語に対応していること。そこに日本語は含まれていない。Javaneseは含まれている。
全世界の文字に対応していないフォント(16MB, 582言語): https://noto-website-2.storage.googleapis.com/pkgs/NotoSans-hinted.zip
全世界の文字に対応しているフォント(1.1GB): https://noto-website-2.storage.googleapis.com/pkgs/Noto-hinted.zip
ImageMagickは一つのフォントファイルにしか対応していないことに気付く
NotoFontの構造を見ていたら、言語ごとにわかれた複数のフォントファイルで構成されていることに気付く。まあ、このへんはだれでも気付くわな。
ImageMagickはこれらをどう使い分けるのだろうか?
適当にfont fallbackとかでぐぐってみたら、フォントが対応していない文字に遭遇した場合に、別のフォントを探す仕組みがあるらしい。
ImageMagickがfont fallbackに対応しているかぐぐった。
I don't think you can specify automatic fallback fonts for "label:", You would have to find if the one(s) you want are available, with "-list font".
I was worried that might be the case... lol...
http://www.imagemagick.org/discourse-server/viewtopic.php?t=29928
However, when I try to use the font family (both listed by identify -list font and listed in type.xml) I get the default system font:
https://stackoverflow.com/questions/10577197/specifying-a-font-with-the-font-family-using-imagemagick
注意:要求されたフォントが見つからなかった場合、ImageMagickはArialやTimesなどのデフォルトフォントにそっと置き換えてきました。まだそうなってはいますが、最近は警告が表示されます。
https://qiita.com/mtakizawa/items/a74bd91f7b3835976461
対応していないみたいだ。
これの何がまずいか?
例えば、ImageMagickで日本語フォントを指定したところに、日本語とアラビア語と中国語が混じったテキストがやってくると、アラビア語と中国語が化けてしまう。
もう少しぐぐって、fallback fontに行き着く。
Thanks all! I ended up using the TTF version of GNU Unifont - http://aur.archlinux.org/packages.php?ID=33588
https://bbs.archlinux.org/viewtopic.php?id=112428
これが欲しかったものだ。
Unifontを使う
fallback fontの一つ、Unifontを使ったら、無事に日本語を表示できた。多分、他の言語も表示できるだろう。
まとめ
だれかが二度同じ罠にはまらないことを、せつに願う。