🎃

Goのlibvipsラッパー「vips-gen」で画像サイズを取得する

に公開

はじめに

今回は、Goを使って画像のサイズを取得する方法について紹介します。今回の前提として、

  • なるべく多くの画像フォーマットに対応する
  • 任意の画像が送られてくるものとし、画像のフォーマットは不明(拡張子がない場合もある)
  • ただし安全な画像のみが送られてくるものとし、悪意のある攻撃の可能性はないものとする

標準ライブラリでの実装

Goには標準ライブラリに image パッケージがあり、画像を読み込んでファイルの情報を取得することができます。

package main

import (
	"bytes"
	"fmt"
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"log"
	"os"

	_ "golang.org/x/image/bmp"
	_ "golang.org/x/image/tiff"
	_ "golang.org/x/image/webp"
)

type ImageInfo struct {
	Width  int `json:"width"`
	Height int `json:"height"`
}

func AnalyzeImageBinary(binary []byte) (*ImageInfo, error) {
	if len(binary) == 0 {
		return nil, fmt.Errorf("empty image data")
	}

	reader := bytes.NewReader(binary)
	config, _, err := image.DecodeConfig(reader)
	if err != nil {
		return nil, fmt.Errorf("failed to decode image: %w", err)
	}

	return &ImageInfo{
		Width:  config.Width,
		Height: config.Height,
	}, nil
}

func main() {
	binary, err := os.ReadFile("image.jpg")
	if err != nil {
		log.Fatal(err)
	}
	info, err := AnalyzeImageBinary(binary)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Image info: %+v\n", info)
}

_ "image/gif" などのように、import する画像タイプを増やせば、対応する画像フォーマットを増やすことができます(参考: Golangのimage Packageを掘り下げる)。

標準ライブラリの問題点

基本的には大きな問題はないです。標準ライブラリでは対応していない画像があったら困る、ぐらいかと思います。

例えば、エッジケースとして、次のようなエラーが発生する場合があります。

failed to decode image: unsupported JPEG feature: luma/chroma subsampling ratio

実際にIssueも起票されていますが、解消はしていないようです。

外部ライブラリの利用

今回は、libvipsというC言語で実装されたオープンソースな画像処理ライブラリを利用してみます。「同様のライブラリと比べ、高速で、メモリ使用量が少ない」といったことが謳われています(今回のユースケースでも同様か、検証はしていませんのであくまで公式情報として...)。

そして、Goでlibvipsを利用したラッパーは、いくつか存在しています。

最後の govips については、スターも多く今年1月までリリースされていましたが、vipsgenを「better solution」と紹介しており、積極的にメンテナンスをする方向ではなさそうに見えます。そのため、今回はvipsgenを選定してみました。

vipsgen への移行

※ vipsgen を使う場合は、事前に libvips や pkg-config をインストールする必要があります。

package main

import (
	"fmt"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"log"
	"os"

	"github.com/cshum/vipsgen/vips"
	_ "golang.org/x/image/bmp"
	_ "golang.org/x/image/tiff"
	_ "golang.org/x/image/webp"
)

type ImageInfo struct {
	Width  int `json:"width"`
	Height int `json:"height"`
}

func AnalyzeImageBinary(binary []byte) (*ImageInfo, error) {
	if len(binary) == 0 {
		return nil, fmt.Errorf("empty image data")
	}

	image, err := vips.NewImageFromBuffer(binary, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to decode image: %w", err)
	}
	defer image.Close()

	width := image.Width()
	height := image.Height()

	return &ImageInfo{
		Width:  width,
		Height: height,
	}, nil
}

func main() {
	binary, err := os.ReadFile("image.jpg")
	if err != nil {
		log.Fatal(err)
	}
	info, err := AnalyzeImageBinary(binary)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Image info: %+v\n", info)
}

vipsgenは "github.com/cshum/vipsgen/vips" を import するだけで使うことができますが、 vipsgen -out ./vips を実行することで、 libvips 用のバインディングを自前で生成することもできます。

通常、vipsgen を使う場合は、libvips のバージョンが 8.17 以降である必要がありますが、古い libvips のバージョンを使っている場合は、vipsgen を実行することで、古いバージョンのバインディングを自前で生成することもできる、ということです。

Docker(Alpine)でのハマりどころと解決策

Alpine のデフォルトの libvips は 8.16 です。そのため、通常の vipsgen の使い方 (importするだけ)をすると、バージョン互換性がなくてエラーが発生します。次のいずれかの対策が必要です。

  • 古い libvips をインストールして、vipsgen -out ./vips を実行してバインディングを自前で生成する
  • 最新の libvips をインストールする

今回は、edge/community リポジトリを使うことで、最新の libvips をインストールしてみます。

FROM golang:1.24-alpine AS builder
WORKDIR /app

RUN apk add --no-cache \
    gcc \
    pkgconfig \
    musl-dev \
    imagemagick \
    && apk add --no-cache \
    --repository http://dl-cdn.alpinelinux.org/alpine/edge/community \
    vips-dev

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -o main .

FROM alpine AS production

RUN apk add --no-cache \
    --repository http://dl-cdn.alpinelinux.org/alpine/edge/community \
    vips \
    imagemagick \
    ca-certificates \
    && addgroup -g 65532 nonroot \
    && adduser -D -u 65532 -G nonroot nonroot

COPY --from=builder /app/main /main

RUN chown -R nonroot:nonroot /main
USER nonroot

ENTRYPOINT ["/main"]

細かい点ですが、Bitmapをサポートするため、ImageMagickもインストールしています。ただし、信頼できない画像がある場合は、セキュリティリスクの考慮が必要なため、Bitmapが不要な場合は入れないほうが良いのではと思います。

まとめ

Goの標準 image パッケージが対応していない画像フォーマットの問題を、libvips とそのGoラッパーである vipsgen を導入することで解決した事例を紹介しました。

ただし、いくつかデメリットがあります。

  • Pure Go でなくなってしまう
  • 最終的なDockerイメージの肥大化
  • セットアップが大変

今回のようなユースケースであれば、基本的には標準パッケージで十分ですが、対応フォーマットを増やしたい場合の選択肢として、libvips を使うことも検討してみてはいかがでしょうか。

Discussion