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を利用したラッパーは、いくつか存在しています。
- cshum/vipsgen (★81 / 最終リリース: 2025-06-06)
- h2non/bimg (★2.9k / 最終更新: 2020-11-21)
- davidbyttow/govips (★1.5k / 最終更新: 2025-01-16)
最後の 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 /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