Goで画像を扱う話（webp編）

2回目の登場になります @sys_cat です。気づいたらVtuberが好きになってました、推しが良すぎて胸が苦しい…ﾊｯ、これが恋…。

はい。15日目になります。
昨日は @kyam_ さんの [[Swift] UITextViewに画像を添付する方法](https://qiita.com/kyam_/items/f08ba6d47f233ef04860) でした。
普段、アプリ開発者の方々とお話する機会が無いのですが社内LTとかで凄く面白い話していただくのでとっても悔しい思いをしてます。そういうの、もっと頂戴っていう。

---

今回は前回書きました [Goで画像を扱う話](https://qiita.com/sys_cat/items/15322618884b1a6d0871) でも予告したとおりWebp変換のお話になります。

## Webpとは？

Googleさんが丁寧な [解説](https://developers.google.com/speed/webp) を書いておりますので、詳しい話はそちらで。
簡単に書くならば

- Googleが開発している画像形式
- PNGやJPEGよりちょっと（25%〜30%位）軽くなるよ（非可逆圧縮モードの時の話）
- 軽くて画像劣化が少ない（無いわけではない）のでレスポンスが向上しやすい
- Web開発ではWebp化対応はたまーに聞く（定石かは知らないです…）

はい、「**なんかいい感じの画像**」って思ってくれたら良さそうな感じですね。

## GoはWebp対応してんの？

- 読むことは出来る
- 作成する事は出来ない

というところです。

公式パッケージとしては webp単独のものは無く、Decode用の [パッケージ](https://godoc.org/golang.org/x/image/webp) が用意されているのみです。

### 有志の外部パッケージ

公式は無くともそれはGo言語、つよつよなエンジニアさんが界隈に沢山いらっしゃいますので調べてみますと、外部パッケージで作成されている作者さんが以下の様にいらっしゃいます。

- harukasanさん / https://github.com/harukasan/go-libwebp
- chai2010さん / https://github.com/chai2010/webp
- nickalieさん / https://github.com/nickalie/go-webpbin

以前Webp対応した時は chai2010さんのパッケージを使いましたが既存のimageパッケージの枠にはまらない使い方でとても驚いたのを覚えてます
（しっかりbytesを使ってたしあの頃は1日ずっとDocument片手にソースコードを読む毎日でとても為になりました！）

### 今回は

個人的に尊敬している harukasan さんの `go-libwebp` を使って変換をしていきます。

## Webp変換

### 前提

個人的な趣味として以下の前提の上に実装をしてみました

- Webサーバとして提供したい
    - GETで受けてファイルをResponseする
- Docker上に諸々置く
    - 最近 `docker-compose.yml` 書くのが癖みたいになってるんですよ
    - 今回、インフラ面に少し記述があるので一応 Dockerfile も晒します
- FwはEcho
    - 最高にEchoすこ、時点でGinとかGoa

### そーすこーど

```go:main.go
package main

import (
	"bufio"
	"github.com/labstack/echo"
	"github.com/harukasan/go-libwebp/webp"
	"golang.org/x/image/draw"
	"image"
	"image/jpeg"
	"net/http"
	"os"
)

type (
	Error struct {
		Message string
	}
)

func initServe() *echo.Echo {
	e := echo.New()
	e.Static("tmp", "tmp")
	e.GET("/", handler)
	return e
}

func handler(c echo.Context) error {
	f, err := os.Open("./tmp/Go-Logo_LightBlue.jpg")
	if err != nil {
		return c.JSON(http.StatusNotFound, &Error{
			Message:"file not found",
		})
	}
	defer f.Close()

	img, err := jpeg.Decode(f)
	if err != nil {
		return c.JSON(http.StatusGone, &Error{Message:err.Error()})
	}
	bou := img.Bounds()
	dst := image.NewRGBA(image.Rect(0, 0, bou.Dx(), bou.Dy()))
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, bou, draw.Over, nil)

	o, err := os.Create("tmp/New.webp")
	if err != nil {
		return c.JSON(http.StatusInternalServerError, &Error{Message:"create new image is failed"})
	}
	w := bufio.NewWriter(o)
	defer func() {
		w.Flush()
		o.Close()
	}()

	con, _ := webp.ConfigPreset(webp.PresetDefault, 80)
	err = webp.EncodeRGBA(w, dst, con)

	if err != nil {
		return c.JSON(http.StatusInternalServerError, &Error{Message:"encode error"})
	}

	return c.File("tmp/New.webp")
}

func main() {
	serve := initServe()

	serve.Logger.Fatal(serve.Start(":8080"))
}
```

```dockerfile
FROM golang:1.13-alpine3.10

RUN apk update && apk add --no-cache make\
        gcc\
        g++\
        git\
        libwebp\
        binutils-gold \
        curl \
        gnupg \
        libgcc \
        linux-headers \
        make \
        python\
        libwebp-tools\
        libwebp-dev\
        tiff-dev\
        vips-dev\
        libzip libzip-dev\
```

```yaml:docker-compose.yml
version: '3'
services:
  go:
    build:
      context: ./
      dockerfile: ./Dockerfile
    volumes:
      - ./:/go/src/github.com/sys-cat/imageapi_for_advent2019
    working_dir: /go/src/github.com/sys-cat/imageapi_for_advent2019
    environment:
      - GO111MODULE=on
    ports:
      - "8080:8080"
    command: "go run main.go"
```

ちょっと長いですね。1つずつ説明していきます。

### せつめい！( Go )

#### 画像をDecodeする

```go
	f, err := os.Open("./tmp/Go-Logo_LightBlue.jpg")
	if err != nil {
		return c.JSON(http.StatusNotFound, &Error{
			Message:"file not found",
		})
	}
	defer f.Close()

	img, err := jpeg.Decode(f)
	if err != nil {
		return c.JSON(http.StatusGone, &Error{Message:err.Error()})
	}
	bou := img.Bounds()
	dst := image.NewRGBA(image.Rect(0, 0, bou.Dx(), bou.Dy()))
	draw.CatmullRom.Scale(dst, dst.Bounds(), img, bou, draw.Over, nil)
```

前回まででやったところですね。
`go-libwebp` のドキュメントを見た場合分かるのですが画像が `RGBA` 形式であれば `dst` あたりの記述は不要になります。今回は対象の画像が `image.YCbCr` 形式なので新しくRGBA形式の画像を作成しています。

#### Webp設置用の場所を確保する

```go
	o, err := os.Create("tmp/New.webp")
	if err != nil {
		return c.JSON(http.StatusInternalServerError, &Error{Message:"create new image is failed"})
	}
	w := bufio.NewWriter(o)
	defer func() {
		w.Flush()
		o.Close()
	}()
```

`tmp` に適当な画像を作っています。 `go-libwebp` では [出力時](https://github.com/harukasan/go-libwebp/blob/master/webp/encode.go#L460) に `io.Writer`が必要なので `bufio.NewWriter(os.File)` でWriterを作っておきます。 Writerって `Flush()` でClose相当の処理やってくれるんですねぇ。知らなかった…（1敗）

#### Webp変換

```go
	con, _ := webp.ConfigPreset(webp.PresetDefault, 80)
	err = webp.EncodeRGBA(w, dst, con)

	if err != nil {
		return c.JSON(http.StatusInternalServerError, &Error{Message:"encode error"})
	}

	return c.File("tmp/New.webp")
```

README.md を読んでると `webp.EncodeRGBA(w, dst, webp.ConfigPreset(webp.PresetDefault, 80))` とかしたくなるけど実は `webp.ConfigPreset` は `*webp.Config, error` が返却値になるので注意です（1敗）。
Configは非可逆モードだったり多岐に設定出来るので読めばもっと詰める事が出来るかも（ https://godoc.org/github.com/harukasan/go-libwebp/webp#Config )
`c.File` でファイルを返せるの、いいよね（申し訳程度のEcho要素）。
 

### せつめい！（ Docker )

#### Webp を扱う為に…

```dockerfile
FROM golang:1.13-alpine3.10

RUN apk update && apk add --no-cache make\
        gcc\
        g++\
        git\
        libwebp\
        binutils-gold \
        curl \
        gnupg \
        libgcc \
        linux-headers \
        make \
        python\
        libwebp-tools\
        libwebp-dev\
        tiff-dev\
        vips-dev\
        libzip libzip-dev\
```

もうこれに尽きるというか、Webpってjpegなどと違って簡単に扱える様になってないので alpine に色々と食わせてやる必要があります。ここに上げてるのは今回対応していて必要そうだったファイル群です。
これで大体100パッケージ位インストールされます。（g++とかはビルドに必要になる感じ）
Go内部で扱えれば良いのですが今回利用している `go-libwebp` はCGOを使っているのでどうしても `webp.h` がないといけなかった。という訳ですね。

## 出来たものを比べてみよう！

### ファイルサイズ

![Screenshot from 2019-12-15 04-29-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4411/b35cc0c5-19a2-cd26-c5c3-53c8e08cc21c.png)

？？？？

いや、ちょっとこれは差が多すぎる… `image.YCbCr` を `image.RGBA` 化したりQuarityが80だったりしているので劣化込みでこのサイズ減かなと思います。

### ファイル劣化

変換画像をそのまま上げるのはちょっと憚れるのでそれぞれの環境で試していただきたいのですが個人環境で確認してみると以下の劣化を確認出来ました。

- 色境界のエッジがちょっときついかも
- 色劣化？（色Profileが異なるので判別が付きづらいかも）

設定詰めたり色Profileを社内で規定すれば劣化がほとんど無い変換が可能だと思います。

## まとめ

- Webp変換はあんまり手間でも無いので画像サイズで困ってる人は使ってみると良さそう
- 脱ImageMagic！が現実的になる
    - 昔のImageMagicを使い続ける必要がなくなる（OSバージョンが固定化されなくなる）
    - PHPのバージンアップがしやすくなる
    - しあわせなせかい
- 実はPNGからの変換もいい感じに出来るのでいい感じに変換出来る
- Gif to Webp はちょっと難易度高めです
- みんなもGoで画像操作しよう！！

## 次回予告

21日も書きます。
ちゃんと弊社にも自キー好きがいるよって話をします。