GoImagick でサムネール作成
〜 Golang & ImageMagick 〜
2016年4月8日(金)
“よや” yoya@awm.jp
自己紹介 (@yoya)
• プロファイル
– https://osdn.jp/users/yoya/
• ImageMagick のストーカーしてます
– http://d.hatena.ne.jp/yoya/searchdiary?word=Im
ageMagick
• 昔、PHP でバイナリを弄ってました
– https://github.com/yoya/IO_MIDI
– https://github.com/yoya/IO_JPEG
• Golang は触り始めて一年ちょっと
?
GoImagick とは
• https://github.com/gographics/imagick
• MagickWand API (C言語)の Go バインディング
– Go 言語で ImageMagick の機能が使えます
公式サイトからもリンク
• http://imagemagick.org/script/api.php#go
• MagickWand と MagickCore の Go バインディ
ング
– GoImagick はMagickCore の定義を取り入れるけ
ど、関数は MagickWand のだけ使います。
MagickWand と MagickCore
• GoImagick は MagickWand の関数を使う
ImageMagick
MagickCore
(magick)
coders
MagickWand
(wand)
utilities
画像処理
本体は
ココ
PerlMagick
PHP imagick
convert
コマンドはここ使い易くする
為のAPI
GoImagick
JPEGやPNG
の入出力
つまり?
• convert コマンドと PHP imagick のコードを見る
と GoImagick の使い方も分かる
– convert コマンド
• wand/mogrify.c
– PHP imagick
• http://php.net/manual/ja/imagick.transformimage.php
なぜ ImageMagick を使うのか?
• Golang 標準で image パッケージあるよね?
– 機能少ないし 対応形式は JPEG,GIF,PNG だけ
• libpng や jpeglib を直接使わないの?
– go-thumber がそうだけど cgo は難易度高い
• 他にも画像変換ツールがあるのでは?
– ImageMagick は困った時に検索で探しやすい
• (恐らく人による。自分は ImageMagick が楽)
ImageMagick を使う理由(1/2)
• 画像を処理したいメソッドが大体揃っている
– リサイズ
– フィルタ
– 画像合成
– 文字入れ Gopher
ImageMagick を使う理由(2/2)
• メジャーな画像フォーマットからマイナーなものま
で100種類以上に対応してる
• http://www.imagemagick.org/script/formats.php
png
jpeg
gif
inline
sixel
webp
svg
pdf
超メジャー 最近の
キワモノ系
(Webの base64画像とか)
ベクター画像
dcm
医療系
(DICOM等)
ここから本題
GoImagick 導入 (MacOS編)
• 少し前まで
• 今のやり方
$ sudo port install ImageMagick
$ go get github.com/gographics/imagick
# ImageMagick v6.8.8以前 (rpm や dpkg とかで古い場合)
$ go get gopkg.in/gographics/imagick.v1/imagick
# ImageMagick v6.8.9以降 (macports や最新版を使う場合)
$ go get gopkg.in/gographics/imagick.v2/imagick
以下のエラーが出ます
expects import "gopkg.in/gographics/imagick.v2/imagick"
GoImagick 使用例
• resize640x480.go
_ = 〜はエラーの値。ちゃんと拾って処理すべき
package main
import (
"gopkg.in/gographics/imagick.v2/imagick”
)
func main() {
imagick.Initialize()
defer imagick.Terminate()
mw := imagick.NewMagickWand()
defer mw.Destroy()
_ = mw.ReadImage(”input.png”)
_ = mw.ResizeImage(640, 480, imagick.FILTER_UNDEFINED, 1)
_ = mw.WriteImage("output.png")
}
ResizeImage(640,480,… 実行
• 縦横のアスペクト比が…
– サムネール画像としてはNG
$ go run resize640x480.go gopher.png
250px
340px
480px
640px
ふくよかな
Gopher!
アスペクト比を保つ方法(1/2)
• 出力サイズを変えてしまう
– はみ出ないように (内接)
• 480 × (250/340)
• => 352x480
• ResizeImage(352,480,…
– 減らさない (外接)
• 640 × (340/250)
• => 640x870
• ResizeImage(640,870,…
• ResizeImage だけで良い
250px
340px
480px
870px
640px
640px
352px
480px
アスペクト比を保つ方法(2/2)
• 出力サイズを変えない
– マージンをつける (内接)
• 480 × (250/340)
• => 352x480
– クロップする (外接)
• 640 × (340/250)
• => 640x870
• ResizeImage だけでは無理
250px
340px
480px
870px
640px
640px352px
480px
マージン(内接)の方法
• 描画領域を広げる (ExtentImage)
– (640 – 352) / 2) = 144 ⇦ 左右に144拡げる
480px
640px
144px
640px
352px
480px 480px
352px
_ = mw.ResizeImage(352, 480, imagick.FILTER_UNDEFINED, 1)
_ = mw.ExtentImage(-144, 0, 640, 480) // -extents
_ = mw.ResetImagePage(“”) // +repage
クロップ(外接)の方法(1/2)
• 描画領域を削る (ExtentImage)
– (640 – 352) / 2) = 144
_ = mw.ResizeImage(640, 870, imagick.FILTER_UNDEFINED, 1)
mw2 = mw.CropImage(0, 0, 640, 480) // -crop
defer mw2.Destory()
870px
640px 640px
480px480px
クロップ(外接)の方法(2/2)
• リサイズとクロップ同時 (TransformImage)
– 250 × (480/640) = 187.5
crop_src := “250x187+0+0”
geom_dst := “640x480”
mw2 = mw.TransformImage(crop_src, geom_dst)
defer mw2.Destroy()
640px
480px
250px
340px
187px
マージンやクロップの注意点
• マージンをどこに
つけるか、どこを
クロップするか
は画像次第
250px
340px
480px
870px
640px
640px
352px
480px
640px 352px
870px
480px
画像合成
• CompositeImage で合成できる
_ = mw1.ReadImage(“gopher.png”)
_ = mw2.ReadImage(“blind.png”)
_ = mw1.CompositeImage(mw2, imagick.COMPOSITE_OP_OVER, 45, 28)
CompositeImage
gopher.png
blind.png
文字入れ (1/5)
• DrawingWand と PixelWand を使う
– DrawingWand でフォントを指定
• (日本語を表示するなら必須)
– (蛇足) QueryFont で扱えるフォントが分かる
dw := imagick.NewDrawingWand()
defer dw.Destroy()
_ = dw.SetFont("Noto-Sans-CJK-JP-Medium”)
dw.SetFontSize(24)
fonts := mw.QueryFont(“*”)
fmt.Printf(“%#v”, fonts)
文字入れ (2/5)
• PixelWand で色を表現
• DrawingWand で色と文字を設定する
pw := imagick.NewPixelWand()
defer pw.Destroy()
_ = pw.SetColor(”rgb(0, 0, 0)”)
dw.SetFillColor(pw)
dw.Annotation(0, 0, “Gopher!!”)
文字入れ (3/5)
• DrawingWand の文字を MagickWand の画像に
描画
• (0,0)を基準に文字を貼るので殆ど見えない
– 見えてるのは p の下にはみ出た部分
• Gravity 方式で配置しよう
_ = mw.DrawImage(dw) あれれ?
文字入れ (4/5)
• CENTER 指定と SOUTH 指定
dw.setGravity(imagick.GRAVITY_CENTER)
dw.Annotation(0, 0, “Gopher!!!”)
mw.DrawImage(dw)
GRAVITY_SOUTH
文字入れ (5/5)
• まとめ
imagick.Initialize()
defer imagick.Terminate()
mw := imagick.NewMagickWand()
defer mw.Destroy()
dw := imagick.NewDrawingWand()
defer dw.Destroy()
pw := imagick.NewPixelWand()
defer pw.Destroy()
_ = mw.ReadImage(os.Args[1])
_ = dw.SetFont("Noto-Sans-CJK-JP-Medium")
dw.SetFontSize(24)
_ = pw.SetColor("rgb(255, 0, 0)")
dw.SetFillColor(pw)
dw.SetGravity(imagick.GRAVITY_CENTER)
dw.Annotation(0, 0, "Gopher!!!")
_ = mw.DrawImage(dw)
_ = mw.WriteImage("output.png”)
MagickWand の注意点
• メソッドが MagickWand を返した時にも
Destroy が必要
– 中で new 相当の処理が動いてる
crop_src := “250x187+0+0”
geom_dst := “640x480”
mw2 = mw.TransformImage(crop_src, geom_dst)
defer mw2.Destroy()
_ = mw.ResizeImage(640, 870, imagick.FILTER_UNDEFINED, 1)
mw2 = mw.CropImage(0, 0, 640, 480) // -crop
defer mw2.Destory()
GoImagick の中身
• 使うだけでなく中身も見よう
– https://github.com/gographics/imagick
imagick$ ls
CREDITS LICENSE env.sh imagick
History.md README.md examples
imagick$ ls imagick | wc
73 73 1298
imagick$ ls -R examples | wc
92 71 874
サンプルが沢山ある
GoImagick 本体
ところで
• http://imagemagick.org/script/api.php#go
• なぜ GoImagick と ImageMagick本家で説明に
食い違いがあるのか。
• なぜ GoImagick と ImageMagick本家で説明に
食い違いがあるのか
– GoImagick ⇨ MagickWand
– ImageMagick本家 ⇨ MagickWand + MagickCore
MagickWand と MagickCore
• おおまかな構造
ImageMagick
MagickCore
(magick)
coders
MagickWand
(wand)
utilities
画像処理
本体は
ココ
PerlMagick
PHP imagick
convert
コマンドはここ
使い易くする
為のAPI
GoImagick
???
JPEGやPNG
の入出力
grep include * で確認
• はい。MagickCore も include してます
MagickCore の利用例
• 型の定義を取り込みたいだけ。MagickCore
の関数を利用してる訳ではなさそう
つまりどういう事?
• 利用する関数は MagickWand だけ
– MagickCore ではない (型の取り込みで include し
てるだけ)
• PerlMagick より PHP imagick のサンプルが参
考になるという事
最近のトピック
• メモリ管理を色々と修正 (by yoya)
– fixed to memory leak, string array issue.
• https://github.com/gographics/imagick/pull/37
• https://github.com/gographics/imagick/pull/39
• Magick.Initialize() に Mutex をかけたい(協議中)
– Fix Initialize/Terminate race condition #43
• https://github.com/gographics/imagick/pull/43
• MagickWand 等の回収を GC に任せたい
– Make mw, pi, pw, dw objects destroyable in GO GC #62
• https://github.com/gographics/imagick/pull/62
メモリ管理を色々と修正 (1/3)
• フォント名一覧取得でメモリリークした
– 似たような漏れが他にもあるのでは?
– Malloc してる箇所が見当たらないのに、free してるけ
どそのポインタは大丈夫なの?
• 関連するバグを調査
– 似たようなリークがあちこちにあった
• 文字列のリストを取得する系メソッドが大体ダメ
– Wand API の中で
AcquireMagickMemory(ImageMagick のメモリ管理)で
取得したメモリを、標準の free で解放してた
• RelinquishMemory を使うべき
メモリ管理を色々と修正 (2/3)
• 調査した
• O 以外が
不具合の
あるメソッド
メモリ管理を色々と修正 (3/3)
• 結構大量に修正
– 余計な修正をcommitしちゃったけどマージしてく
れた
– CREDITS に名前載せてくれた
やさしい
Magick.Initialize() に Mutex
• Magick.Initialize() や Terminate() をマルチス
レッドで呼ぶと競合するので Mutex をかけた
い
• Sync.once で Initialize を一度だけ呼べばよく
ない?
• でもユーザに気をつけろというより仕組みを
入れた方がよくない?
• 協議続行中
GC に任せる(1/2)
• runtime.SetFinalizer を使う
• GC 対象になると SetFinalizer で指定したメソッ
ド(Destroy)が呼ばれる
func newMagickWand(mw *C.MagickWand) *MagickWand {
mw := &MagickWand{mw: cmw}
runtime.SetFinalizer(mw, Destroy)
mw.IncreaseCount()
return mw
}
GCに任せる(2/2)
• defer で destroy する問題点 (一般論)
– 毎回 defer 書くのは面倒だし忘れたりする
– 関数の外に return 出来ない
– 明示的に new するものはまだ良いけど、新しく
MagickWand を返すメソッドもあって漏れがち
• CropImage
• TransformImage
• これらの戻り値も Destroy しないとリークする
• defer mw.Destroy といちいち書かなくてもよく
なると嬉しい!
まとめ
• MagickWand で画像を処理する
• DrawingWand で文字を描画する
• PixelWand で文字の色を指定する
• defer mw.Destroy() を忘れずに
– CropImage や TransformImage が返すのも Destroy() をお
忘れずに
• ここまでの話を聞けば、Golang でサムネール画像を
作れるはず
– Let’s try!

GoImagickThumbnail