ディレクトリ内の画像からコンタクトシートを作るツールを作った(Go 言語で)

コンタクトシートってのは、画像(写真)のサムネイルを一覧にして1枚のシート(あるいは1ページ)に並べたもののこと。説明するまでもないか。

とにかく、コンタクトシートを作る必要ができて方法を調べてみたら、Windows の標準機能でできることを知った。ところが、確かに簡単に PDF に出力できるんだけど、どういうわけかファイル名の順に並んでくれない。用が足りないわけじゃないんだけどなんとも気持ちが悪い。

なので、ツールを自作することにした。最近は Python を使うことが多いので今回も Python で、と思って PDF の作り方を調べている途中で、そういえば以前、ディレクトリ内の画像をサムネイルのリストを表示する html ファイルを作るツールを作ったな、と思い出した。↓これ。

Go 言語だった。

そういうわけで、久しぶり(最後のコミットは2019年の2月、なんと3年半前だ)に Go を書いて、自分でも忘れてたツールを機能拡張することにした。

方針はこうだ。

mkphotoindex コマンドに --contactsheet オプションを追加して、指定したディレクトリに含まれる画像のサムネイルとファイル名を並べた PDF ファイルを出力する。

PDF を出力できるパッケージを探してみると、github.com/signintech/gopdf というのが見つかった。おおまかな使い方は、ページを追加し、テキストや画像を配置、PDF に出力する、というシンプルなもの。あるいは原始的ともいう。

もう少し楽にできそうなのはないかと探して見つけたのが、github.com/mikeshimura/goreport。これは内部では github.com/signintech/gopdf を使っているけど、帳票を作るのに便利なように作られている。決まったフォーマットの PDF を作るにはちょうどよさそうだと思って、これを使って実装を始めた。参考にしたページは以下。

ところが、なかなか思うようにはいかなかった。Qiita の記事を読むと、結構複雑な帳票も簡単にできるように思えるんだけど、サムネイルを例えば 4列×5行とかに並べるのがうまくできない。多分理解が足りないだけで、いいやり方があるんだと思うんだけど。

で、結局のところ、A4 のページにサムネイル(とファイル名)を並べたいだけだと思いなおして、github.com/signintech/gopdf を直接使うことにした。サムネイルのサイズや位置の調整に時間をとられたけど、とりあえず用は足りるものはできた。

とはいえ問題が一つ残った。github.com/signintech/gopdf は ttf フォントを読み込んで使える(もちろん日本語も)ので、IPAex フォントをダウンロードして使ったんだけど、現状の実装ではカレントディレクトリに フォントファイル ipaex.ttf を置いておく必要がある。これは結構面倒だ。今回に限っては Windows で実行するのが前提なので Windows に標準でついているフォントファイルのパスをコードに埋め込んでしまえば回避できる。でも、ほかの OS でも使うことを考えるとそれは避けたい。コマンドラインオプションでフォントファイルを指定してやることも考えたけど、デフォルトのフォントがあったほうがいい。OS ごとにデフォルトのフォントを決めてやるにはどうしたらいいんだろうか。

Ubuntu 20.04に最新のGolangをインストール

といっても公式サイトからダウンロードして展開して PATH を通すだけ。バージョンは 1.15.2 だった。

インストール手順。まずはダウンロードした tarball を /usr/local 以下に展開。

takatoh@apostrophe:Downloads$ sudo tar -C /usr/local -xzf go1.15.2.linux-amd64.tar.gz

/usr/loca/go/bin に PATH を通す。

export PATH=$PATH:/usr/local/go/bin

ついでに GOPATH を設定しておく。

export GOPATH=$HOME/go/thirdparty:$HOME/go/projects

go version コマンドでバージョンが表示されれば OK。

takatoh@apostrophe:~$ go version
go version go1.15.2 linux/amd64

すんごい楽だな。

フィボナッチ数列のうち、各桁の数字を足した数で割り切れる(ry

たまたま見つけたネタ。

 cf. フィボナッチ数列 – utthi_fumiの日記

タイトルが長くなったので省略しちゃったけど、ちゃんと引用すると、こう。


フィボナッチ数列のうち、各桁の数字を足した数で割り切れる数を以下の例に続けて小さいほうから5個求めてください。

2 → 2 ÷ 2

3 → 3 ÷ 3

5 → 5 ÷ 5

8 → 8 ÷ 8

21 → 21 ÷ 3・・・2 + 1= 3で割る

144 → 144 ÷ 9・・・1 + 4 + 4 = 9で割る

引用してもわかりにくいな。要するに、フィボナッチ数列を {Fn} (n=0,1,2…)として、Fn の各桁の数を足し合わせたものを S(Fn) とすると、S(Fn) で割り切れる Fn を小さい順に5つ(ただし、例に出ている 114 までを除く)求めよ、ってことだ。

「 これはメモ化と大きな桁を扱う事についての問題です 」なんて書いてあるけど、大きな桁はその通りだけど、n から Fn を求めようとするからメモ化なんかが必要になるんであって、この問題では小さい順に調べればいいんだから素直にフィボナッチ数列の頭から生成すればいい。

というわけで、Go でやってみた。ただやってみるだけじゃなくて、あんまり使ったことのないゴルーチンとチャネルを使ってみたよ。

まずは単純にフィボナッチ数列を生成するところから。

package main

import (
	"fmt"
)

func main() {
	s := makeFib()
	for i := 0; i < 10; i++ {
		f := <- s
		fmt.Println(f)
	}
}

type Stream chan int

func makeFib() Stream {
	s := make(Stream)
	go func() {
		a, b := 0, 1
		for {
			f := a
			a, b = b, a + b
			s <- f
		}
	}()
	return s
}

関数 makeFib は、チャネルを返す。で、このチャネルからフィボナッチ数列を一つずつ取り出せる。makeFib の中のゴルーチンは無限ループになっているので、(アルゴリズム上は)無限に取り出すことができる(実際はどうだかわからない)。実行結果は次の通り。

^o^ > go run fib_chan.go
0
1
1
2
3
5
8
13
21
34

さて、それじゃ本題に入ろう。といってもフィボナッチ数列はもう得られているんだから、あとは条件に合う数 Fn を5つ出力するだけだ。こうなった。

package main

import (
	"fmt"
)

func main() {
	s := makeFib()
	i := 0
	for i < 5 {
		f := <- s
		if f > 144 && isDivisible(f) {
			i++
			fmt.Println(f)
		}
	}
}

type Stream chan int

func makeFib() Stream {
	s := make(Stream)
	go func() {
		a, b := 0, 1
		for {
			f := a
			a, b = b, a + b
			s <- f
		}
	}()
	return s
}

func sumOfPlaces(n int) int {
	sum := 0
	for n > 0 {
		sum += n % 10
		n /= 10
	}
	return sum
}

func isDivisible(n int) bool {
	return (n % sumOfPlaces(n)) == 0
}

実行結果。

^o^ > go run fib_divisible.go
2584
14930352
86267571272
498454011879264
160500643816367088

OK。ちゃんとできた。

リスト(配列)の中で隣り合う同じ値をグループ化する(3)

しつこいようだけど、今度は Go でやってみた。

package main

import (
    "fmt"
)

func main() {
    var l1 = []int{1, 1, 2, 2, 3, 1, 1}
    var l2 = []int{}

    fmt.Printf("%v\n", adjacentGroup(l1))
    fmt.Printf("%v\n", adjacentGroup(l2))
}

func adjacentGroup(l []int) [][]int {
    var result [][]int

    if len(l) == 0 {
        return result
    }

    var current = []int{l[0]}
    for i := 1; i < len(l); i++ {
        if current[0] == l[i] {
            current = append(current, l[i])
        } else {
            result = append(result, current) current = []int{l[i]}
        }
    }
    result = append(result, current) return result
} 
^o^ > go run adjacentGroup.go
[[1 1] [2 2] [3] [1 1]]
[]

defer

defer 文は、関数を抜けるときに実行する処理を登録する。defer 文で登録する処理は関数またはメソッド呼び出しでなければならない。
例を示そう。

package main

import "fmt"

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start")
}

func foo() {
    defer fmt.Println("foo end")
    fmt.Println("foo start")
    bar()
}

func main() {
    foo()
}

関数 foobar でそれぞれ defer 文で処理を登録している。これを実行するとこうなる。

^o^ > go run defer.go
foo start
bar start
bar end
foo end

関数を抜けるときに処理が実行されているのがわかる。

defer 文で登録した処理は、ランタイムエラーが起こっても有効だ。次の例では一番深い関数呼び出し(baz)で panic を起こしている。この例でも defer 文で登録された処理は実行され、その後、エラーメッセージを表示する。

package main

import "fmt"

func baz() {
    panic("oops!")
}

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start")
    baz()
}

func foo() {
    defer fmt.Println("foo end")
    fmt.Println("foo start")
    bar()
}

func main() {
    foo()
}
^o^ > go run defer2.go
foo start
bar start
bar end
foo end
panic: oops!

goroutine 1 [running]:
main.baz()
        C:/Users/takatoh/Documents/w/learning-go/defer2.go:6 +0x6b
main.bar()
        C:/Users/takatoh/Documents/w/learning-go/defer2.go:12 +0x141
main.foo()
        C:/Users/takatoh/Documents/w/learning-go/defer2.go:18 +0x141
main.main()
        C:/Users/takatoh/Documents/w/learning-go/defer2.go:22 +0x27
exit status 2

panic

組み込み関数 panic は、エラーメッセージを表示して、プログラムを中断する。何か致命的なエラーが起こった場合に使用する。
簡単な例を示そう。関数 fact は 0 以上の整数に値を返し、それ以外の場合には panic を呼ぶ。

package main

import "fmt"

func fact(n int) (int, error) {
    if n < 0 { panic("fact : domain error") } a := 1 for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

実行例。

^o^ > go run panic.go
3628800
362880
40320
5040
720
120
24
6
2
1
1
panic: fact : domain error

goroutine 1 [running]:
main.fact(0xffffffffffffffff, 0x1, 0x1, 0x2)
        C:/Users/takatoh/Documents/w/learning-go/panic.go:7 +0xb8
main.main()
        C:/Users/takatoh/Documents/w/learning-go/panic.go:18 +0x43
exit status 2

for ループの最後で fact-1 を与えているので panic が呼ばれている。

エラー処理

Go には例外機構がない。じゃ、どうやってエラー処理をするかというと、多値を使って関数の返り値としてエラーを返すようにする。
例を見てみよう。

package main

import (
    "fmt"
    "errors"
)

func fact(n int) (int, error) {
    if n < 0 {
        return 0, errors.New("fact : domain error")
    }
    a := 1
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

fact 関数は階乗を求める関数だけど、値を2つ返す。求めたい関数の値とエラーだ。通常はエラーとして nil を返すけど、引数が0より小さかった場合はゼロ値と nil でないエラーを返す。
試してみよう。

^o^ > go run err_fact.go
3628800
362880
40320
5040
720
120
24
6
2
1
1
fact : domain error

最後の行が fact-1 を与えた場合だ。ちゃんとエラーが表示されている。

エラトステネスの篩

前回のエントリで作った makeIntstreamFileter を使って、素数列を生成するプログラムを作ってみよう。「エラトステネスの篩」というアルゴリズムを使う。
まずはコード。

package main

import (
    "fmt"
)

type Stream chan int

func makeInt(n, m int) Stream {
    s := make(Stream)
    go func() {
        for i := n; i <= m; i++ {
            s <- i
        }
        close(s)
    }()
    return s
}

func streamFilter(f func(int) bool, in Stream) Stream {
    s := make(Stream)
    go func() {
        for {
            x, ok := <- in
            if !ok {
                break
            }
            if f(x) {
                s <- x
            }
        }
        close(s)
    }()
    return s
}

func filter(n int, in Stream) Stream {
    return streamFilter(func(x int) bool {
        return x % n != 0
    }, in)
}

func seive(n int) Stream {
    s := make(Stream)
    go func() {
        in := makeInt(2, n)
        for {
            x, ok := <- in
            if !ok {
                break
            }
            s <- x
            if x * x <= n {
                in = streamFilter(func(y int) bool {
                    return y % x != 0
                }, in)
            }
        }
        close(s)
    }()
    return s
}

func main() {
    for x := range seive(500) {
        fmt.Print(x, " ")
    }
    fmt.Println("")
}

makeIntstreamFilter は前回と同じだ。篩にあたる seive が素数列を生成する。
まずはストリームを用意しておく(変数 s)。で、ゴルーチンを起動して、中で 2 から始まる整数列を生成するストリームを in に代入する。ここからは繰り返しで、in の先頭 x は素数なのでそのまま s に送信する。そして inx で割り切れる数を取り除くフィルタをかけたストリームに置き換える。これで inx で割り切れない数列を生成するストリームになった。
結果として素数以外はストリームから取り除かれて、素数だけが残るっていうわけだ。
実行結果。

^o^ > go run eratosthenes.go
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499

ストリーム

チャネルを使って、数列を生成することを考える。数列のような一連のデータの流れを、ストリームとよぶ。
まずは単純な例から。

package main

import (
    "fmt"
)

type Stream chan int

func makeInt(n, m int) Stream {
    s := make(Stream)
    go func() {
        for i := n; i <= m; i++ {
            s <- i
        }
        close(s)
    }()
    return s
}

func main() {
    s := makeInt(1, 20)
    for x := range s {
        fmt.Print(x, " ")
    }
    fmt.Println("")
}

type を使って、整数をやり取りするチャネルに Stream という別名をつけている。
makeInt 関数は、整数2つ(nm)を引数にとって Stream を返す。中では、Stream を作っておき、ゴルーチンで匿名関数を実行する。このゴルーチンは for ループの中で n から m までを Stream に書き込み、最後に閉じる。
こうすることによって、数列が生成できるわけだ。
実行例:

^o^ > go run stream.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

次に、Stream を高階関数に渡してみよう。マッピングを行う streamMap とフィルタリングを行う streamFilter を作ってみる。どちらも関数と Stream を受け取って、新しい Stream を返す。

package main

import (
    "fmt"
)

type Stream chan int

func makeInt(n, m int) Stream {
    s := make(Stream)
    go func() {
        for i := n; i <= m; i++ {
            s <- i
        }
        close(s)
    }()
    return s
}

func streamMap(f func(int) int, in Stream) Stream {
    s := make(Stream)
    go func() {
        for {
            x, ok := <- in
            if !ok {
                break
            }
            s <- f(x)
        }
        close(s)
    }()
    return s
}

func streamFilter(f func(int) bool, in Stream) Stream {
    s := make(Stream)
    go func() {
        for {
            x, ok := <- in
            if !ok {
                break
            }
            if f(x) {
                s <- x
            }
        }
        close(s)
    }()
    return s
}

func main() {
    s0 := makeInt(1, 20)
    for x := range s0 {
        fmt.Print(x, " ")
    }
    fmt.Println("")
    square := func(x int) int {
        return x * x
    }
    s1 := streamMap(square, makeInt(1, 20))
    for x := range s1 {
        fmt.Print(x, " ")
    }
    fmt.Println("")
    isOdd := func(x int) bool {
        return x % 2 != 0
    }
    s2 := streamFilter(isOdd, makeInt(1, 20))
    for x := range s2 {
        fmt.Print(x, " ")
    }
    fmt.Println("")
} 

実行例:

^o^ > go run stream2.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400
1 3 5 7 9 11 13 15 17 19

タイムアウト

非常に時間のかかる処理をゴルーチンにするといつまでも終わらなくなる可能性がある。selecttime パッケージの After を組み合わせると、タイムアウトの処理ができる。
After は一定時間が経過したらデータを送信するチャネルを返す。なので、select と併用することでタイムアウトの処理ができるわけだ。

package main

import (
    "fmt"
    "time"
)

func fibo(n int) int {
    if n < 2 {
        return 1
    } else {
        return fibo(n - 2) + fibo(n - 1)
    }
}

func main() {
    ch := make(chan int, 5)
    for _, n := range []int{ 41, 40, 38, 36, 34 } {
        go func(x int) {
            ch <- fibo(x)
        }(n)
    }
    for i := 5; i > 0; {
        select {
            case n := <- ch: fmt.Println(n)
            i--
            case <- time.After(500 * time.Millisecond): fmt.Println("Timeout")
            i = 0
        }
    }
}

ここでは、時間のかかる処理(関数)として、フィボナッチ数を求める関数を用意した。これで5つのゴルーチンを起動しておき、その終了を待つと同時に time.After で500ミリ秒でタイムアウトするようにしている。つまり time.After の返り値であるチャネルからデータが送られてくる前に処理が終わったゴルーチンだけが結果を表示するわけだ。

^o^ > go run channel_after.go
9227465
24157817
63245986
Timeout

このとおり、500ミリ秒では3つしか終わらなかった。