GAE/Go で静的画像リサイズ・変換をやった話


GAE/Goを動的画像リサイズ・変換サーバとして動かす情報はあるが, 静的画像リサイズ・変換をGAE/Goでやる情報が見つけられなかったのでまとめておく。

実現したい内容

一覧で表示する場合は小さいサイズの画像, 詳細画面で見る場合は大きいサイズの画像で表示したい。
実際には動的画像変換サーバを置くのが一番良いのだが, 実運用しているプロダクトだったり様々な事情があったので今回は静的に画像変換をしCloud Storageにアップロードすることにした。

注意点

GAEだとローカルにファイルを保存することができない。
一時ファイルを使ってそのファイルに変換した画像を保存するような処理ができない。
全てオンメモリで処理するしかない

実装内容

実際の処理は大きくこんな感じ。

  1. Cloud Storageに保存している画像へのurlにGetリクエストを送りレスポンスを取得する。
  2. レスポンスを image にDecodeする。
  3. image をリサイズ・変換する。
  4. imagebyte[] に変換する。
  5. bucketにアップロードする

importしたパッケージ

import (
    "bytes"
    "context"
    "fmt"
    "image"
    "image/gif"
    "image/jpeg"
    "image/png"
    "io"
    "io/ioutil"
    "time"

    "cloud.google.com/go/storage"
    "github.com/nfnt/resize"
    "github.com/pkg/errors"
    "google.golang.org/appengine"
    "google.golang.org/appengine/log"
    "google.golang.org/appengine/urlfetch"
)

1. Cloud Storageに保存している画像へのurlにGetリクエストを送りレスポンスを取得する。

    client := urlfetch.Client(c)
    resp, err := client.Get(originalImageUrl)

GAE/Go httpでGetリクエストを送りたい場合は urlfetch パッケージを使用する必要があります。
func Client(ctx context.Context) *http.Client 帰ってきた *http.Clientを使ってGetリクエストを送ります。

2. レスポンスをimageにDecodeする。

    originalImage, format, err := image.Decode(resp.Body)

func Decode(r io.Reader) (Image, string, error) デコードとformatを返してくれます。
このformatをcontentTypeの指定に使います。

3. imageをリサイズ・変換する。

    smallImage := resize.Thumbnail(150, 150, original, resize.Lanczos3)

func Thumbnail(maxWidth, maxHeight uint, img image.Image, interp InterpolationFunction) image.Image

widthとheightとベースとなるimageとリサイズする際のポリシーを指定すると, アスペクト比を保ったままリサイズしてくれる。

4. imagebyte[] に変換する。

storageClientで書き込む場合に引数がbyte[]なので imageを変換する必要がある。

    buf := new(bytes.Buffer)
    err := encodeImage(buf, image, format)
    if err != nil {
        return nil, errors.Wrap(err, "Failed - Encode Error")
    }
    return buf.Bytes(), nil
func encodeImage(target io.Writer, imageData image.Image, imageFormat string) error {
    switch imageFormat {
    case "jpeg", "jpg":
        jpeg.Encode(target, imageData, nil)
    case "png":
        png.Encode(target, imageData)
    case "gif":
        gif.Encode(target, imageData, nil)
    default:
        return errors.New("invalid format")
    }
    return nil
}
  1. 新しいバッファーを作成する。
  2. formatによって使うPackageのEncodeを分ける。
  3. Encodeした結果をバッファーに保存する。
  4. バッファの内容をbyte[] で返す。

5. bucketにアップロードする

    storageClient, _ := storage.NewClient(c)
    blobWriter := storageClient.Bucket(bucketName).Object(fileName).NewWriter(c)
    blobWriter.ContentType = contentType
    _, err := blobWriter.Write(bytes)
    if err != nil {
        return "", errors.Wrap(err, "Failed - Write Error")
    }
    if err := blobWriter.Close(); err != nil {
        return "", errors.Wrap(err, "Failed - WriterClose Error")
    }

こんな感じでアップロードできる。
ちなみにアップロードができたかの判断はblobWriter.Close()でやることが推奨されている。

Close completes the write operation and flushes any buffered data. If Close doesn't return an error, metadata about the written object can be retrieved by calling Attrs.

アップロードができた場合は func(w * Writer)Attrs()* ObjectAttrs でメタデータの確認もできる。

最後に

GAE/Go で一時的なtmpファイルを作らなくてもリサイズ・アップロードはできたが動的画像変換サーバを使うのがベストや(笑)
複数サービスに展開したりするのも大変だし, オリジナルの画像をいじって複数枚保存するのは要領が悪いように見える。