Goで画像を扱う話

気圧が変動が大きいとへばってしまう体をなんとかしたいと思ってます [@sys_cat](https://twitter.com/sys_cat) です。
普段はGameWithで設計書を書いてみたりサーバサイドのコーディングを行ったりしています。最近の推しゲームはアッシュアームズです。細かい動きが可愛いのですよね。
昨日は @fukiworks さんの [「Adobe MAX Japan 2019」で発表された激アツ最新トピック5選 【デザイナー独断】](https://tech.gamewith.co.jp/entry/2019/12/06/130533) でした。
デザイナさんとお仕事してると面白いですよね。考え方の方向性が違うのに似た結論に行くこともあるので楽しんで仕事出来ますね。

---

さて、今回はGoで画像を扱う方法についてです。
弊社はPHPの会社ですがツール周りはGoが多くなってきたんですね。画像を扱う時はよくImageMagicを使うと思うんですがよくある画像の表示、操作は割とGoでも出来ます。

## JPEGとPNGを読み込んでみよう

Webサイトで表示する鉄板のJPEGとPNGの画像ファイルを読み込んでサイズなどを取得してみます。

### 利用する画像

利用する画像は [go.dev](https://go.dev/) で紹介されている [Brand Guidline](https://blog.golang.org/go-brand) からロゴファイルをダウンロードして見ていきます。

### 縦横の幅を取得する

```go:main.go
package main

import (
	"fmt"
	"image/jpeg"
	"image/png"
	"os"
)

func main() {
	// JPEG
	j, err := os.Open("../tmp/Go-Logo_LightBlue.jpg")
	if err != nil {
		panic(err)
	}
	defer j.Close()
	img, err := jpeg.Decode(j)
	if err != nil {
		panic(err)
	}
	bound := img.Bounds()
	fmt.Printf("Go-Logo_LightBlue.jpg\tX : %d px, Y : %d px\n", bound.Dx(), bound.Dy())

	// PNG
	p, err := os.Open("../tmp/Go-Logo_Yellow.png")
	if err != nil {
		panic(err)
	}
	defer p.Close()
	img, err = png.Decode(p)
	if err != nil {
		panic(err)
	}
	bound = img.Bounds()
	fmt.Printf("Go-Logo_Yellow.png\tX : %d px, Y : %d px\n", bound.Dx(), bound.Dy())
}
```

結果

```
$ go run main.go 
Go-Logo_LightBlue.jpg	X : 1061 px, Y : 938 px
Go-Logo_Yellow.png	X : 1062 px, Y : 938 px
```

基本的に `os.Open()` で画像ファイルを開いて `<image.Type>.Decode(*os.File)` でファイルをDecodeしてファイルのデータを取得することが出来ます。

それぞれのファイルのサイズがあってるか確認しましょう。

**JPEG**
![Screenshot from 2019-12-07 18-35-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4411/5ecd9da1-970b-b422-05c6-bbb53337fd80.png)

**PNG**
![Screenshot from 2019-12-07 18-36-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4411/7d7c06a7-e79f-e618-0d3e-ceed9ed58874.png)

うん、正しく取れてますね。

## リサイズしてみよう

Webで画像を扱う時にはリサイズする必要が多かったりしますのでその辺りも行ってみましょう。

### 画像を取得してリサイズ画像を生成する（ちょっとコードをが冗長です）

```go:main.go
package main

import (
	"fmt"
	"image"
	"golang.org/x/image/draw"
	"image/jpeg"
	"image/png"
	"os"
)

func main() {
	// JPEG
	j, err := os.Open("../tmp/Go-Logo_LightBlue.jpg")
	if err != nil {
		panic(err)
	}
	defer j.Close()
	img, err := jpeg.Decode(j)
	if err != nil {
		panic(err)
	}
	bound := img.Bounds()
	fmt.Printf("Before\tGo-Logo_LightBlue.jpg\tX : %d px, Y : %d px\n", bound.Dx(), bound.Dy())

	dst := image.NewRGBA(image.Rect(0, 0, bound.Dx()/2, bound.Dy()/2))
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, bound, draw.Over, nil)

	jOutput, err := os.Create("./new.jpg")
	if err != nil {
		panic(err)
	}
	defer jOutput.Close()

	err = jpeg.Encode(jOutput, dst, &jpeg.Options{Quality: 100})
	if err != nil {
		panic(err)
	}

	reopen, err := os.Open("./new.jpg")
	if err != nil {
		panic(err)
	}
	defer reopen.Close()
	img, _ = jpeg.Decode(reopen)
	bound = img.Bounds()
	fmt.Printf("After\tGo-Logo_LightBlue.jpg\tX : %d px, Y : %d px\n", bound.Dx(), bound.Dy())

	// PNG
	p, err := os.Open("../tmp/Go-Logo_Yellow.png")
	if err != nil {
		panic(err)
	}
	defer p.Close()
	img, err = png.Decode(p)
	if err != nil {
		panic(err)
	}
	bound = img.Bounds()
	fmt.Printf("Before\tGo-Logo_Yellow.png\tX : %d px, Y : %d px\n", bound.Dx(), bound.Dy())

	dst = image.NewRGBA(image.Rect(0, 0, bound.Dx()/2, bound.Dy()/2))
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, bound, draw.Over, nil)

	pOutput, err := os.Create("./new.png")
	if err != nil {
		panic(err)
	}
	defer pOutput.Close()

	err = png.Encode(pOutput, dst)
	if err != nil {
		panic(err)
	}

	reopen, err = os.Open("./new.png")
	if err != nil {
		panic(err)
	}
	defer reopen.Close()
	img, _ = png.Decode(reopen)
	bound = img.Bounds()
	fmt.Printf("After\tGo-Logo_Yellow.png\tX : %d px, Y : %d px\n", bound.Dx(), bound.Dy())
}
```

結果

```
$ go run main.go 
Before	Go-Logo_LightBlue.jpg	X : 1061 px, Y : 938 px
After	Go-Logo_LightBlue.jpg	X : 530 px, Y : 469 px
Before	Go-Logo_Yellow.png	X : 1062 px, Y : 938 px
After	Go-Logo_Yellow.png	X : 531 px, Y : 469 px
```

画像も正しく作られてますね。PNGだと色の量とか透過とかありますのでサイズだけでも無いみたいなのでサイズについてはあんまり変わりませんね。
JPEGは顕著ですね。ちゃんと半分以下になっています。

![Screenshot from 2019-12-07 19-09-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4411/e341bf33-302c-8aff-9815-c673a6a49a11.png)

## こぼれ話

`Decode` は `image.Decode(*os.File)` で代用することが出来ます。FileTypeを返すのでFileType毎にEncodeすることも出来ますので汎用化できそうですね。

## まとめ

普段ImageMagicなどでやっていた変換系の対応をコードで実現出来るのはとても夢があることですね。やろうと思えばBase64の文字列をjpegとして変換することも出来るとのことで画像処理は夢が多いなぁって感じですね。

Goでの画像変換系のネタは割とあるあるネタです。そんな画像ネタを使うのも抵抗がありましたが外部ライブラリを使わずに書けたので一先ずまんぞくです。
今月はもう1本Goについて書く予定なのでそこでWebp周りの読み込みだったりjpeg to webp あたりをやっていこうかなと思ってます。