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言語、つよつよなエンジニアさんが界隈に沢山いらっしゃいますので調べてみますと、外部パッケージで作成されている作者さんが以下の様にいらっしゃいます。
- 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
そーすこーど
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\
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
がないといけなかった。という訳ですね。
出来たものを比べてみよう!
ファイルサイズ
????
いや、ちょっとこれは差が多すぎる… image.YCbCr
を image.RGBA
化したりQuarityが80だったりしているので劣化込みでこのサイズ減かなと思います。
ファイル劣化
変換画像をそのまま上げるのはちょっと憚れるのでそれぞれの環境で試していただきたいのですが個人環境で確認してみると以下の劣化を確認出来ました。
- 色境界のエッジがちょっときついかも
- 色劣化?(色Profileが異なるので判別が付きづらいかも)
設定詰めたり色Profileを社内で規定すれば劣化がほとんど無い変換が可能だと思います。
まとめ
- Webp変換はあんまり手間でも無いので画像サイズで困ってる人は使ってみると良さそう
- 脱ImageMagic!が現実的になる
- 昔のImageMagicを使い続ける必要がなくなる(OSバージョンが固定化されなくなる)
- PHPのバージンアップがしやすくなる
- しあわせなせかい
- 実はPNGからの変換もいい感じに出来るのでいい感じに変換出来る
- Gif to Webp はちょっと難易度高めです
- みんなもGoで画像操作しよう!!
次回予告
21日も書きます。
ちゃんと弊社にも自キー好きがいるよって話をします。