[Go] JPEG/PNG 画像の幅・高さを変えずに容量を圧縮する
はじめに
業務で携わっているプロジェクトで、ユーザがアップロードした画像の容量をサーバサイドで小さくしたい要件がありました。あまり経験のないことで、調べるなかでいろいろ学びがあったので記事に残します。
画像の圧縮処理をするコードサンプル
圧縮処理のサンプルコードは以下のとおりです。今回圧縮対象としたのは jpeg と png のみです。実装には Go 標準の image
パッケージとそのサブパッケージである image/jpeg
と image/png
を使用しました。Go のバージョンは v1.20.3 です。
nfnt/resize
を紹介している記事がいくつか見つかりましたが、2023/04/09 現在メンテナンスが行われておらず、リポジトリもアーカイブされているようなので採用を見送りました。
package main
import (
"fmt"
"image"
"image/jpeg"
"image/png"
"log"
"net/http"
"os"
)
func main() {
if len(os.Args) < 3 || os.Args[2] == "" {
log.Println("引数にファイルパスを指定してください")
return
}
inputFileName := os.Args[1]
outputFileName := os.Args[2]
b, err := os.ReadFile(inputFileName)
if err != nil {
panic(err)
}
// MIME Sniffing: https://triple-underscore.github.io/mimesniff-ja.html
mimeType := http.DetectContentType(b)
if mimeType != "image/jpeg" && mimeType != "image/png" {
log.Println("処理対象のファイル種別ではありません")
return
}
r, err := os.Open(inputFileName)
if err != nil {
panic(err)
}
defer r.Close()
srcImg, format, err := image.Decode(r)
if err != nil {
panic(err)
}
if err := r.Close(); err != nil {
log.Println(err)
}
dstImgWriter, err := os.Create(outputFileName)
if err != nil {
panic(err)
}
defer dstImgWriter.Close()
switch format {
case "jpeg":
if err := jpeg.Encode(dstImgWriter, srcImg, &jpeg.Options{
Quality: 85,
}); err != nil {
panic(err)
}
case "png":
encoder := png.Encoder{
CompressionLevel: png.DefaultCompression,
}
if err := encoder.Encode(dstImgWriter, srcImg); err != nil {
panic(err)
}
default:
panic(fmt.Errorf("不明なエンコード方式: %s", format))
}
log.Println(format, "画像の圧縮が完了しました")
}
実際プロジェクトでは、画像圧縮処理を GCP の Cloud Functions 上で動かしています。そのため、ファイルの読み書き等実務のコードと若干異なる部分があります。
jpeg、png の各エンコード方式ごとに圧縮方法の指定のしかたが異なっているため、それぞれ解説します。
jpeg の圧縮
srcImg, encode, err := image.Decode(r)
if err != nil {
panic(err)
}
dstImgWriter, err := os.Create(outputFileName)
if err != nil {
panic(err)
}
if err := jpeg.Encode(dstImgWriter, srcImg, &jpeg.Options{
Quality: 85,
}); err != nil {
panic(err)
}
jpeg 画像を圧縮するには、画像を image.Image
interface に変換し、image/jpeg
パッケージの Encode
のオプションに Quality
というパラメータを指定します。このパラメータは「圧縮率」「品質」など、さまざまな呼び方がされているようですが、本記事では Quality
という表記で統一します。Quality
の値は 1 ~ 100 の範囲で指定が可能です。値が高ければ高いほど元の画質が維持されますが、エンコード後のファイルサイズは大きくなります。逆に低すぎると画像の劣化が生じます。余談ですが、こういった現象を「圧縮アーティファクト」と呼ぶらしいです (ChatGPT に教えてもらった)。
jpeg の圧縮は非可逆圧縮方式であるため、元の画像に戻すことはできません。したがって、Quality
の値を決定する際には、元の画像の品質を維持しつつ、ファイルサイズを小さくする必要があります。Google Developers によると Quality
の値を 85 に設定することが適切であるとされています。
また、こちらの記事での検証結果によると「通常 85 でよい、それ以上だと容量が急増する。画質を優先したい場合は 90」とされていました。容量がどのくらい小さくなるかは元の画像によるようです。(85 を指定したからといって画像の容量が 85% になるとは限らない)
ちなみに image/jpeg
パッケージではデフォルト値が 75 になっていました。なぜ 75 なのかは軽く調べてみましたが、手がかりがつかめずわかりませんでした。
最終的には無難な 85 を指定することに落ち着きました。
png の圧縮
srcImg, encode, err := image.Decode(r)
if err != nil {
panic(err)
}
dstImgWriter, err := os.Create(outputFileName)
if err != nil {
panic(err)
}
encoder := png.Encoder{
CompressionLevel: png.DefaultCompression,
}
if err := encoder.Encode(dstImgWriter, srcImg); err != nil {
panic(err)
}
png の場合は jpeg と同様にまずは画像を読みこんで image.Image
interface に変換します。png.Encoder
構造体の CompressionLevel
フィールドに圧縮レベルを指定します。ここで指定が可能な値は DefaultCompression
NoCompression
BestSpeed
BestCompression
の 4 つの値です。image/png
のエンコード処理内部では compress/zlib
パッケージが使われています。これは zlib の機能を Go でも使えるように実装したパッケージです。
本家の zlib では圧縮レベルが 0 ~ 9 の間で指定可能で、Z_NO_COMPRESSION
に 0、Z_NO_COMPRESSION
に 1、Z_DEFAULT_COMPRESSION
に 6、Z_BEST_COMPRESSION
に 9 を割り当てられていました。数字が大きいほど圧縮率が高いが、圧縮に時間がかかる仕様のようです (どれくらい差が出るのかちゃんと検証してないです)。
(C が読めないので抜粋箇所が正しいかどうかが不安...)
zlib は可逆圧縮なので圧縮しても画質の変化はありません。画像のサイズを小さくすることにこだわるのであれば BestCompression
を指定するのが良いのでしょうが、処理速度が膨らんでしまうおそれがあります。
Z_DEFAULT_COMPRESSION は、速度と圧縮の間のデフォルトの妥協点 (現在、レベル 6 に対応します) を表します。
こちらは python 標準の zlib ライブラリからの引用ですが、デフォルトの圧縮レベルが速度と圧縮のバランスが良いそうです。ということで、png での圧縮レベルは DefaultCompression
としています。
ちなみに圧縮レベルを DefaultCompression
にしてエンコードを行う場合は、png.Encoder
構造体を作らずとも以下のように image/png.Encode
を使うことで簡単に同じことができます。わざわざ明示的に CompressionLevel
の指定をする例を挙げていたのは圧縮レベルの説明をしやすくする意図がありました。
srcImg, encode, err := image.Decode(r)
if err != nil {
panic(err)
}
dstImgWriter, err := os.Create(outputFileName)
if err != nil {
panic(err)
}
if err := png.Encode(dstImgWriter, srcImg); err != nil {
panic(err)
}
まとめ
圧縮処理は標準パッケージのみで簡単に実装することができました。コード自体はとてもシンプルでしたが、指定するオプションの意味を調べていくと、今まであまり意識したことのない領域に首を突っ込むことができ勉強になりました。本記事では趣旨と逸れるため取り上げませんでしたが、そもそも zlib って何者なのかとか、圧縮関連の各種技術について調べていくと面白かったので、参考リンクを載せておきます。
参考
Discussion