GolangでアニメーションGifをリサイズする

#はじめに
前回の記事の「[Slackのemojiをドラッグ＆ドロップで追加できるアプリをGolangで作った](http://qiita.com/from_Unknown/items/c6d0f16fe208d382404b "Slackのemojiをドラッグ＆ドロップで追加できるアプリをGolangで作った")」で、ようやくアニメーションGif（以下アニメGif）のリサイズが出来たのでメモがてら記事を書きます。
上記のアプリを作った当初はgifのリサイズを行うと色がおかしくなってしまう問題があって、gifの対応を見送りました。
落ち着いて調べ直したら色々と分かったことがあったので、お役に立てば幸いです。

#サンプル
ネットで探してきた素材の縮小例です。
http://droid-chan.net/archives/338
![droid_girl.gif](https://qiita-image-store.s3.amazonaws.com/0/139405/95361051-687d-b61b-30fd-7b8354a9a534.gif)
![droid_girl_resized.gif](https://qiita-image-store.s3.amazonaws.com/0/139405/d3075555-eaec-2145-48ca-5b26f25ce8b4.gif)

小さくなった分荒くはなりますが、リサイズ出来てちゃんと動いています。

#概要
今回は以下のライブラリを使用してリサイズします。
[nfnt/resize](https://github.com/nfnt/resize "nfnt/resize")

処理の流れは以下の順序で行います。

1. アニメGifを読み込む
2. 現在のgifのサイズを取得
3. リサイズする比を計算
4. アニメGif内のgif画像を1枚ずつ縮小
5. アニメGifをファイルに出力

こう書くと単純ですが、4の工程が中々難しかったです。

#コード
リサイズに使用した関数を転記します。
一部説明の都合上、処理を入れ替えている箇所があります。
実際に使用したソースなので、jpgやpngのリサイズの処理も入っています。

```go
func resizeImage(filePath string, maxSize float64) error {
	const postFix string = "_resized"
	base := filepath.Base(filePath)
	ext := filepath.Ext(filePath)
	ext = strings.ToLower(ext)

	imageFile, err := os.Open(filePath)
	if err != nil {
		log.Fatal(err)
	}
	defer imageFile.Close()

	var decImage image.Image
	var gifImage *gif.GIF
	var imageConfig image.Config
	if ext == ".jpg" || ext == ".jpeg" {
		decImage, err = jpeg.Decode(imageFile)
		if err != nil {
			log.Println(err)
		}
		_, err = imageFile.Seek(io.SeekStart, 0)
		if err != nil {
			return err
		}
		imageConfig, err = jpeg.DecodeConfig(imageFile)
		if err != nil {
			return err
		}
	} else if ext == ".png" {
		decImage, err = png.Decode(imageFile)
		if err != nil {
			return err
		}
		_, err = imageFile.Seek(io.SeekStart, 0)
		if err != nil {
			return err
		}
		imageConfig, err = png.DecodeConfig(imageFile)
		if err != nil {
			return err
		}
	} else if ext == ".gif" {
		gifImage, err = gif.DecodeAll(imageFile)
		if err != nil {
			return err
		}
		imageConfig = gifImage.Config
		if err != nil {
			return err
		}
	} else {
		return nil
	}

	width := float64(imageConfig.Width)
	height := float64(imageConfig.Height)

	var ratio float64
	if width > height && width > maxSize {
		ratio = maxSize / width
	} else if height > maxSize {
		ratio = maxSize / height
	} else {
		ratio = 1
	}

	tmpFileName := base[0:len(base)-len(ext)] + postFix + ext
	tmpFile, err := os.Create(tmpFileName)
	if err != nil {
		return err
	}
	defer tmpFile.Close()

	if ratio == 1 {
		_, err = imageFile.Seek(io.SeekStart, 0)
		if err != nil {
			return err
		}
		_, err := io.Copy(tmpFile, imageFile)
		if err != nil {
			log.Fatal(err)
			return err
		}
	} else {
		if ext == ".jpg" || ext == ".jpeg" {
			resized := resize.Resize(uint(math.Floor(width*ratio)), uint(math.Floor(height*ratio)),
				decImage, resize.Lanczos3)
			jpeg.Encode(tmpFile, resized, nil)
		} else if ext == ".png" {
			resized := resize.Resize(uint(math.Floor(width*ratio)), uint(math.Floor(height*ratio)),
				decImage, resize.Lanczos3)
			png.Encode(tmpFile, resized)
		} else if ext == ".gif" {
			for index, frame := range gifImage.Image {
				rect := frame.Bounds()
				tmpImage := frame.SubImage(rect)
				resizedImage := resize.Resize(uint(math.Floor(float64(rect.Dx())*ratio)),
					uint(math.Floor(float64(rect.Dy())*ratio)),
					tmpImage, resize.Lanczos3)
				// Add colors from original gif image
				var tmpPalette color.Palette
				for x := 1; x <= rect.Dx(); x++ {
					for y := 1; y <= rect.Dy(); y++ {
						if !contains(tmpPalette, gifImage.Image[index].At(x, y)) {
							tmpPalette = append(tmpPalette, gifImage.Image[index].At(x, y))
						}
					}
				}
				// After first image, image may contains only difference
				// bounds may not start from at (0,0)
				resizedBounds := resizedImage.Bounds()
				if index >= 1 {
					marginX := int(math.Floor(float64(rect.Min.X) * ratio))
					marginY := int(math.Floor(float64(rect.Min.Y) * ratio))
					resizedBounds = image.Rect(marginX, marginY, resizedBounds.Dx()+marginX,
						resizedBounds.Dy()+marginY)
				}
				resizedPalette := image.NewPaletted(resizedBounds, tmpPalette)
				draw.Draw(resizedPalette, resizedBounds, resizedImage, image.ZP, draw.Src)
				gifImage.Image[index] = resizedPalette
			}
			// Set size to resized size
			gifImage.Config.Width = int(math.Floor(width * ratio))
			gifImage.Config.Height = int(math.Floor(height * ratio))
			gif.EncodeAll(tmpFile, gifImage)
		}
	}
	return nil
}

// Check if color is already in the Palette
func contains(colorPalette color.Palette, c color.Color) bool {
	for _, tmpColor := range colorPalette {
		if tmpColor == c {
			return true
		}
	}
	return false
}
```

#ポイント
アニメGifをリサイズするにあたってポイントを幾つか解説します。

##アニメGifを読み込む時
[gif.DecodeAll](https://golang.org/pkg/image/gif/#DecodeAll)を使用します。

```go
var gifImage *gif.GIF
gifImage, err = gif.DecodeAll(imageFile)
```

他のjpgやpngと違い難しいのは、`DecodeAll`から返却される値が[*GIF](https://golang.org/pkg/image/gif/#GIF)というところです。
`Decode`から返却されるのは`Image`なのでそのまま`resize.Resize`に渡せるのですが、`GIF`には`Image`がありません。
参考：

```go
type GIF struct {
        Image     []*image.Paletted // The successive images.
        Delay     []int             // The successive delay times, one per frame, in 100ths of a second.
        LoopCount int               // The loop count.
        Disposal []byte
        BackgroundIndex byte
}
```

そのため、ここから`Image`を作る必要があります。

##各画像を縮小する

先程取得した`GIF`はアニメGifの画像全てが格納されているため、`range`を使用してループで回します。
`for index, frame := range gifImage.Image {`
`GIF`内の`Image`を`range`で回すことで、`index`と[Paletted](https://golang.org/pkg/image/#Paletted)を取得できます。
`Paletted`は[SubImage](https://golang.org/pkg/image/#Paletted.SubImage)を使って、`Image`に変換できます。
`Paletted`内の`Rectangle`を使用して、原寸大の`Image`を作ります。

```go
rect := frame.Bounds()
tmpImage := frame.SubImage(rect)
```

あとはこの`Image`を`Resize`に渡して、リサイズすればいいだけ・・・と思っていたらそう簡単ではありませんでした。

##ImageをPalettedに変換する
`GIF`は`Image`ではなく、`Paletted`で画像を保持しています。
そのため、リサイズした`Image`を`Paletted`に戻さなければいけません。
[NewPaletted](https://golang.org/pkg/image/#NewPaletted)という関数を使用すれば出来そうです。
`func NewPaletted(r Rectangle, p color.Palette) *Paletted`
そのためには`Rectangle`と`Palette`を作る必要があります。
まずは`Palette`を作ります。

###Paletteを作成
Gifは256色しか使用できませんが、リサイズ直後の画像は256色を超えている可能性があります。
（このことに中々気付けませんでした・・・。）
当初別のソースを参考にして`palette.Plan9`を`Palette`として使用しましたが、色がおかしくなってしまいました。
元画像が持っていた色と違うから当たり前ですね・・・。
試行錯誤した結果、元画像から色情報を取得するという結論に至りました。

```go
// Add colors from original gif image
var tmpPalette color.Palette
for x := 1; x <= rect.Dx(); x++ {
    for y := 1; y <= rect.Dy(); y++ {
        if !contains(tmpPalette, gifImage.Image[index].At(x, y)) {
            tmpPalette = append(tmpPalette, gifImage.Image[index].At(x, y))
        }
    }
}
```

読み込んだ元画像をループで回し、まだ追加していない`Color`があったら`Palette`に追加するという処理です。
`At`関数で`Paletted`の特定のポイントの`Color`情報を取得できます。
これで元画像の色情報を持った`Palette`が出来ました。

###Rectangleを作成
ここも一筋縄ではいきませんでした・・・。
アニメGifの1枚目以降はフレームの再利用が可能なため、差分しか情報がない場合があります。
つまり、座標が(0,0)から始まらない場合があります。
しかし、`Resize`はこのことを考慮してくれず、描画開始位置が(0,0)に戻ってしまうため、描画開始位置を適切な箇所にずらしてあげなければいけません。

```go
// After first image, image may contains only difference
// bounds may not start from at (0,0)
resizedBounds := resizedImage.Bounds()
if index >= 1 {
    marginX := int(math.Floor(float64(rect.Min.X) * ratio))
    marginY := int(math.Floor(float64(rect.Min.Y) * ratio))
    resizedBounds = image.Rect(marginX, marginY, resizedBounds.Dx()+marginX,
                        resizedBounds.Dy()+marginY)
}
```

元画像の`Min.X`と`Min.Y`の位置を取得して縮小し、リサイズ後の`Rectangle`に反映します。
これで`Rectangle`と`Palette`が出来ました。

##リサイズした画像を描画
`NewPaletted`を使用して`Paletted`を作成後、リサイズした画像を`Draw`を使用して描画します。
描画した`Paletted`を元画像と差し替えます。

```go
resizedPalette := image.NewPaletted(resizedBounds, tmpPalette)
draw.Draw(resizedPalette, resizedBounds, resizedImage, image.ZP, draw.Src)
gifImage.Image[index] = resizedPalette
```

##アニメGifを出力
アニメGifを出力する前に、`Config`のサイズをリサイズ後のサイズに変更します。
その後、[gif.EncodeAll](https://golang.org/pkg/image/gif/#EncodeAll)でファイルを出力します。

```go
// Set size to resized size
gifImage.Config.Width = int(math.Floor(width * ratio))
gifImage.Config.Height = int(math.Floor(height * ratio))
gif.EncodeAll(tmpFile, gifImage)
```

これでリサイズされたアニメGifが出力されます。

#作ってみて
最初はどうしたものかと思いましたが、どんなデータが入っているのかとにかく出力してみたらなんとなく解決の糸口が見つかりました。
もし簡単に出来る方法がありましたら、ぜひ教えて下さい。

今回の経験を応用して、アニメGifで色々やってみたいと思います。