sci

最果て風呂

糊としての 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 を書いて、eachend の後に 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 処理がぐるぐる回って、しばらくするとプロンプトに戻ってくる感じになる。