糊としての Golang
まるぱく
手持ちの写真の高さを揃える必要があったので、画像を縮小するスクリプトを作成した。はじめに Ruby で実装して作業は終了したのだけれど、勉強のために golang で実装し直してみた。参考にしたのはまっつんさんのこれ。
Ruby では標準で画像のサイズを取得する方法が無かったので、identify
を使って結果をパースし、480 よりも大きい場合は convert
で縮小するという流れ。
golang では image
パッケージが標準でついているのでこれを利用。縮小する部分は無いので、これも convert
を使って縮小させた。
Ruby ではサクサク実装できたのだけれど、golang の方は調べながらやったので時間がかかってしまった。外部コマンドを実行させる部分、特に引数を渡すところにてこずってしまった。各要素をダブルクオート(および変数)で羅列してやれば良いのだった。つまり、
exec.Command("convert", "-thumbnail", "hoge", filename, filename)
のようにすれば良い。ただし、これでは外部コマンドが動いていないようだった。評価されないってことなのかな?例にあるように
hoge, err := exec.Command("convert", ... ).Output()
のようにしてやると外部コマンドを呼び出してくれる。hoge
を得るために exec.Command()
が評価されたってことなのかな?
自分は外部コマンドが実行した副作用(縮小された画像)が欲しいだけなので、hoge を捨てて
_, err := exec.Command("convert", ... ).Output()
のようにした。そしたらエラーも捨てろよって感じだけれども……
Ruby の方は identify
も動かすので遅いけれども、サクサクと実装できたのでトータルでは速い。golang の方は調べながら試行錯誤したので、処理速度は速いけれどもトータルでは数十倍もかかってしまった。(実行だけなら Ruby: 10s、Go: 4s なので、実装にかけた時間を考えると……)
今回のような使い捨てスクリプトを golang でサクサクっと作れるようになりたいな。今はまだ糊としての使い方ですら出来ていない。(糊と言えばシェルスクリプトだけど良くわからないし)
スクリプト
Ruby
THUMBSIZE = 480 IMG_FMT = "jpg" PAT = '["*.#{IMG_FMT}", "*.#{IMG_FMT.upcase}"]' trd = [] Dir.glob(eval(PAT)).each do |img| trd << Thread.new do imginfo = `identify #{img}` imgsize = imginfo.split(" ")[2] if (imgsize.split("x")[1].to_i > THUMBSIZE) && (IMG_FMT == "jpg") puts "#{img} のサイズは #{imgsize} なので縮小します。" `convert -thumbnail #{THUMBSIZE}x#{THUMBSIZE} #{img} #{img}` elsif (imgsize.split("x")[1].to_i > THUMBSIZE) && (IMG_FMT == "png") puts "#{img} のサイズは #{imgsize} なので縮小します。" `convert -define jpeg:size=#{THUMBSIZE}x#{THUMBSIZE} -thumbnail #{THUMBSIZE}x#{THUMBSIZE} #{img} #{img}` else puts "#{img} のサイズは #{imgsize} です。" end end end trd.map(&:join)
Go
package main import ( "fmt" "image" _ "image/jpeg" _ "image/png" "io/ioutil" "log" "os" "os/exec" "regexp" "strconv" "strings" ) var ( outputSize = 480 imgFormat = "jpg" regSeed = "."+imgFormat+"$|."+strings.ToUpper(imgFormat)+"$" regExt = regexp.MustCompile(regSeed) ) func outputInfo(files []os.FileInfo) { for _, file := range files { test := regExt.MatchString(file.Name()) if test == true { imgfile, err := os.Open(file.Name()) if err != nil { log.Fatal(err) } defer imgfile.Close() imginfo, _, err := image.DecodeConfig(imgfile) if err != nil { log.Fatal(err) } fmt.Printf("%s のサイズは ", file.Name()) fmt.Printf("W:%dxH:%d ですぞ\n", imginfo.Width, imginfo.Height) if (imginfo.Height > outputSize) && (imgFormat == "jpg") { fmt.Println("JPEG!") reduceJpegImage(file.Name()) } else if (imginfo.Height > outputSize) && (imgFormat == "png") { fmt.Println("PNG!") reducePngImage(file.Name()) } } } } func reduceJpegImage(filename string) { fmt.Printf("大きいから縮小するわ\n") _, err := exec.Command("convert", "-define", "jpeg:size="+strconv.Itoa(outputSize)+"x"+strconv.Itoa(outputSize), "-thumbnail", strconv.Itoa(outputSize)+"x"+strconv.Itoa(outputSize), filename, filename).Output() if err != nil { log.Fatal(err) } } func reducePngImage(filename string) { fmt.Printf("大きいから縮小するわ\n") _, err := exec.Command("convert", "-thumbnail", strconv.Itoa(outputSize)+"x"+strconv.Itoa(outputSize), filename, filename).Output() if err != nil { log.Fatal(err) } } func main() { files, err := ioutil.ReadDir("./") if err != nil { log.Fatal(err) } outputInfo(files) }
今回知ったこと
- import で
_
を付けたパッケージは、プログラム中で実際に使われていなくても怒られない。 - 数値と文字列の変換をするためには
strconv
パッケージが必要。 - 外部コマンドを実行するためには
os/exec
パッケージを読み込んでexec.Command()
を使う。 gofmt
便利。
追記
扱うフォーマットが JPEG だった場合は -define jpeg:size=000x000
オプションを付けるとかなり速くなるらしいので使うようにした。ちょっとした実験だけど 16.686 → 5.117 へと 3倍程速くなってる。枚数が増えたらもっと差が出るだろうね。
さらに
Ruby でスレッドを使うと速いらしいので使ってみた。使い方は each
のはじまる前に trd = []
を書いて、each
のすぐ下に trd << Thread.new do
を書いて、each
の end
の後に trd.map(&:join)
を書くだけ。
trd = [] hoge.each do |x| trd << Thread.new do 処理 x end end trd.map(&:join) # これは trd.each {|t| t.join} と同じ意味?
スレッドを使う前は 16.108s だったものが 7.085s と倍くらい速くなった。因みに golang だと 5.357s だった。
each
文は対象ファイルの情報を puts
で表示して、必要があれば convert
処理するという流れなのだが、Thread
を使った場合だと各ファイル情報が先にバーッと表示されて、裏で convert
処理がぐるぐる回って、しばらくするとプロンプトに戻ってくる感じになる。