Help us understand the problem. What is going on with this article?

Goで画像を扱う話(webp編)

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

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


今回は前回書きました Goで画像を扱う話 でも予告したとおりWebp変換のお話になります。

Webpとは?

Googleさんが丁寧な 解説 を書いておりますので、詳しい話はそちらで。
簡単に書くならば

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

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

GoはWebp対応してんの?

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

というところです。

公式パッケージとしては webp単独のものは無く、Decode用の パッケージ が用意されているのみです。

有志の外部パッケージ

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

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

今回は

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

Webp変換

前提

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

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

そーすこーど

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"))
}
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\
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する

    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設置用の場所を確保する

    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 では 出力時io.Writerが必要なので bufio.NewWriter(os.File) でWriterを作っておきます。 Writerって Flush() でClose相当の処理やってくれるんですねぇ。知らなかった…(1敗)

Webp変換

    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 を扱う為に…

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

????

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

ファイル劣化

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

  • 色境界のエッジがちょっときついかも
  • 色劣化?(色Profileが異なるので判別が付きづらいかも)

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

まとめ

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

次回予告

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away