Web production note

sharpで画像を一括圧縮、WebP・AVIF変換する

Category:
開発環境

画像変換ライブラリsharpを用いて、画像をまとめて圧縮や変換(Webp・AVIF・JPG・PNG)できる方法をまとめました。

ベースコード

本記事で紹介するサンプルコードは、以下の記事で紹介されているメインのコードをベースとして利用させていただいています。

この記事のsharpでできること

  • 画像(JPG、PNG、GIF)の圧縮
  • WebP変換
  • AVIF変換
  • JPG変換
  • PNG変換
  • 圧縮と変換処理を同時に実行
    • 例)ベース画像(jpg)を圧縮しつつWebPやAVIFに変換するなど

ディレクトリ構成

変換前の画像ファイル一式を images/ に格納する構成です。

■プロジェクトディレクトリ
 ┣ dist (処理後の画像が出力されるディレクトリ) ※コマンド実行時に自動追加
 ┣ images (処理する前の画像を格納) ※手動作成してください
 ┃
 ┣ config.mjs (各処理の設定を記載するファイル) ※手動作成してください(詳細後述)
 ┣ convert-avif.mjs (AVIF変換処理ファイル) ※手動作成してください(詳細後述)
 ┣ convert-jpg.mjs (JPG変換処理ファイル) ※手動作成してください(詳細後述)
 ┣ convert-png.mjs (PNG変換処理ファイル) ※手動作成してください(詳細後述)
 ┣ convert-webp.mjs (WebP変換処理ファイル) ※手動作成してください(詳細後述)
 ┣ optimise.mjs (画像圧縮処理ファイル) ※手動作成してください(詳細後述)
 ┃
 ┣ package.json (プロジェクトのjsonファイル) ※初期設定(sharpインストール)時に自動追加
 ┃
 ┗ node_modules (編集不要:自動生成されるコアファイル格納場所) ※初期設定(sharpインストール)時に自動追加 

初期設定(ファイルのインストール)

構築したいディレクトリへ移動

作業を始める前にsharpを構築したいディレクトリまで移動してください。

Windowsでの移動

cd C:¥Users¥.....¥sharpを構築したいプロジェクトディレクトリ

Macでの移動

cd /Users/....../sharpを構築したいプロジェクトディレクトリ

macなら該当フォルダを右クリック→「フォルダに新規ターミナルタブ」を選択してターミナルを立ち上げるとスムーズです。

sharpをインストール

以下のコマンドを実行してsharpをインストールします。

npm install -D sharp

sharpの設定用JSファイルを作成

開発ディレクトリに各処理の設定を記載するJSファイル(config.mjs)と、5つの処理をそれぞれ記載するJSファイル(optimise.mjs、convert-webp.mjs、convert-avif.mjs、convert-jpg.mjs、convert-png.mjs)計6つのファイルを作成します。

config.mjs (各処理の設定を記載するファイル)

config.mjs

/*
  sharpライブラリを使用して画像を圧縮する
  https://sharp.pixelplumbing.com/

  参考
  https://zenn.dev/spicato_blog/articles/6afdf43d0f0a97
*/
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';


//対象ファイルの格納先
const dirName = './images';

//変換後ファイルの格納先
const outPutDir = `./dist`;


//jpgオプション  詳細: https://sharp.pixelplumbing.com/api-output#jpeg
const jpgOptions = {
  quality: 70,
  mozjpeg: true
}

//pngオプション  詳細: https://sharp.pixelplumbing.com/api-output#png
const pngOptions = {
  effort: 10,
  quality: 70,
  compressionLevel: 9
}

//gifオプション  詳細: https://sharp.pixelplumbing.com/api-output#gif
const gifOptions = {
  quality: 70
}

//webpオプション  詳細: https://sharp.pixelplumbing.com/api-output#webp
const webpOptions = {
  quality: 70,
  alpha_quality: 0
}

//avifオプション  詳細: https://sharp.pixelplumbing.com/api-output#avif
const avifOptions = {
  quality: 50
}

async function imageConversion (options) {
  options = Object.assign({
    isOptimize: true,
    isWebp: true,
    isAvif: true,
    isJpg: true,
    isPng: true
  }, options);
  const { isOptimize, isWebp, isAvif, isJpg, isPng } = options;


  // 拡張子を確認
  function getExtension(file) {
    const ext = path.extname(file || '').split('.');
    return ext[ext.length - 1];
  }

  const readSubDir = (folderPath, finishFunc) => {
    // フォルダ内の全ての画像の配列
    let result = [];
    let execCounter = 0;

    const readTopDir = (folderPath) => {
      execCounter += 1;
      fs.readdir(folderPath, (err, items) => {
        //.から始まる隠しファイルを除外
        items = items.filter((item) => {
          return item.indexOf('.') !== 0;
        });

        if (err) {
          console.log(err);
        }

        items = items.map((itemName) => {
          return path.join(folderPath, itemName);
        });

        items.forEach((itemPath) => {
          if (fs.statSync(itemPath).isFile()) {
            result.push(itemPath);
          }
          if (fs.statSync(itemPath).isDirectory()) {
            //フォルダなら再帰呼び出し
            readTopDir(itemPath);
          }
        });

        execCounter -= 1;

        if (execCounter === 0) {
          if (finishFunc) {
            finishFunc(result);
          }
        }
      });
    };

    readTopDir(folderPath);
  };

  //サブディレクトリの列挙 非同期
  readSubDir(dirName, (items) => {
    items.forEach((item) => {
      const pathName = path.dirname(item);
      const fileName = path.basename(item);
      const fileFormat = getExtension(fileName);

      // もしディレクトリがなければ作成
      if (!fs.existsSync(outPutDir)) {
        fs.mkdirSync(outPutDir);
      }

      //非対応ファイルの簡易チェック
      if (fileFormat === '') {
        //拡張子なし
        console.log(
          `\u001b[1;31m 対応していないファイルです。-> ${fileName}`
        );
        return;

      } else if (fileFormat === 'svg') {
        // svgは複製のみ
        fs.copyFile(item, `${outPutDir}/${fileName}`, (err) => {
          if (err) {
            return;
          }
          console.log(
            `\u001b[1;32m ${fileName}を${outPutDir}に複製しました。`
          );
        });
        return;
      }

      //JPG、PNG、GIFファイルを圧縮
      if(isOptimize) {
        let sh = sharp(`${pathName}/${path.basename(item)}`);
        if (fileFormat === 'JPG' || fileFormat === 'JPEG' || fileFormat === 'jpg' || fileFormat === 'jpeg') {
          sh = sh.jpeg(jpgOptions);
        } else if (fileFormat === 'PNG' || fileFormat === 'png') {
          sh = sh.png(pngOptions);
        } else if (fileFormat === 'GIF' || fileFormat === 'gif') {
          sh = sh.gif(gifOptions);
        } else {
          console.log(
            `\u001b[1;31m 対応していないファイルです。-> ${fileName}`
          );
          return;
        }
        sh.toFile(`${outPutDir}/${fileName}`, (err, info) => {
          if (err) {
            console.error(err);
            return;
          }
          console.log(
            `\u001b[1;32m ${fileName}を圧縮しました。 ${info.size / 1000}KB`
          );
        });
      }

      //webp変換
      if (isWebp) {
        sharp(`${pathName}/${path.basename(item)}`).webp(webpOptions).toFile(
          `${outPutDir}/${fileName.replace(
            /\.[^/.]+$/,
            '.webp'
          )}`,
          (err, info) => {
            if (err) {
              console.error(err);
              return;
            }

            console.log(
              `\u001b[1;32m ${fileName}をwebpに変換しました。 ${
                info.size / 1000
              }KB`
            );
          }
        );
      }

      //avif変換
      if (isAvif) {
        sharp(`${pathName}/${path.basename(item)}`).avif(avifOptions).toFile(
          `${outPutDir}/${fileName.replace(
            /\.[^/.]+$/,
            '.avif'
          )}`,
          (err, info) => {
            if (err) {
              console.error(err);
              return;
            }

            console.log(
              `\u001b[1;32m ${fileName}をavifに変換しました。 ${
                info.size / 1000
              }KB`
            );
          }
        );
      }

      //jpg変換
      if (isJpg) {
        sharp(`${pathName}/${path.basename(item)}`).jpeg(jpgOptions).toFile(
          `${outPutDir}/${fileName.replace(
            /\.[^/.]+$/,
            '.jpg'
          )}`,
          (err, info) => {
            if (err) {
              console.error(err);
              return;
            }

            console.log(
              `\u001b[1;32m ${fileName}をjpgに変換しました。 ${
                info.size / 1000
              }KB`
            );
          }
        );
      }

      //png変換
      if (isPng) {
        sharp(`${pathName}/${path.basename(item)}`).png(pngOptions).toFile(
          `${outPutDir}/${fileName.replace(
            /\.[^/.]+$/,
            '.png'
          )}`,
          (err, info) => {
            if (err) {
              console.error(err);
              return;
            }

            console.log(
              `\u001b[1;32m ${fileName}をpngに変換しました。 ${
                info.size / 1000
              }KB`
            );
          }
        );
      }

    });
  });
}
export { imageConversion };

optimise.mjs (画像圧縮処理ファイル)

optimise.mjs

import { imageConversion } from "./config.mjs";
imageConversion({
  isOptimize: true,
  isWebp: false,
  isAvif: false,
  isJpg: false,
  isPng: false
});

convert-webp.mjs (WebP変換処理ファイル)

convert-webp.mjs

import { imageConversion } from "./config.mjs";
imageConversion({
  isOptimize: false,
  isWebp: true,
  isAvif: false,
  isJpg: false,
  isPng: false
});

convert-avif.mjs (AVIF変換処理ファイル)

convert-avif.mjs

import { imageConversion } from "./config.mjs";
imageConversion({
  isOptimize: false,
  isWebp: false,
  isAvif: true,
  isJpg: false,
  isPng: false
});

convert-jpg.mjs (JPG変換処理ファイル)

convert-jpg.mjs

import { imageConversion } from "./config.mjs";
imageConversion({
  isOptimize: false,
  isWebp: false,
  isAvif: false,
  isJpg: true,
  isPng: false
});

convert-png.mjs (JPG変換処理ファイル)

convert-png.mjs

import { imageConversion } from "./config.mjs";
imageConversion({
  isOptimize: false,
  isWebp: false,
  isAvif: false,
  isJpg: true,
  isPng: false
});

package.jsonへコマンドを追加

サンプルの各コマンドは入力時の簡素化目的で短縮記法にしています。

全てのパターンは網羅していないため、一括実行したい組み合わせがない場合は必要に応じて追記してください。

package.json

{
  〜省略〜
  "scripts": {
    "o": "node optimize.mjs",
    "w": "node convert-webp.mjs",
    "a": "node convert-avif.mjs",
    "j": "node convert-jpg.mjs",
    "p": "node convert-png.mjs",
    "ow": "node optimize.mjs && node convert-webp.mjs",
    "oa": "node optimize.mjs && node convert-avif.mjs",
    "aw": "node convert-avif.mjs && node convert-webp.mjs",
    "all": "node optimize.mjs && node convert-avif.mjs && node convert-webp.mjs"
  },
  〜省略〜
}

圧縮コマンド

npm run oもしくは node optimise.mjs で画像圧縮を実施します。

npm run o
node optimise.mjs

WebP変換コマンド

npm run wもしくは node convert-webp.mjs でWebP変換を実施します。

npm run w
node convert-webp.mjs

AVIF変換コマンド

npm run aもしくは node convert-avif.mjs でAVIF変換を実施します。

npm run a
node convert-avif.mjs

JPG変換コマンド

npm run jもしくは node convert-jpg.mjs でJPG変換を実施します。

npm run j
node convert-jpg.mjs

PNG変換コマンド

npm run pもしくは node convert-png.mjs でPNG変換を実施します。

npm run convert
node convert-png.mjs

一括実行コマンド

AVIF変換・WebP変換コマンド

npm run awもしくは node convert-avif.mjs && node convert-webp.mjs でWebP変換とAVIF変換を実施します。

npm run aw
node convert-avif.mjs && node convert-webp.mjs

圧縮・WebP変換コマンド

npm run owもしくは node optimize.mjs && node convert-webp.mjs で元の画像を圧縮しつつWebP変換も実施します。

npm run ow
node optimize.mjs && node convert-webp.mjs

圧縮・AVIF変換コマンド

npm run oaもしくは node optimize.mjs && node convert-avif.mjs で元の画像を圧縮しつつAVIF変換も実施します。

npm run oa
node optimize.mjs && node convert-avif.mjs

圧縮・AVIF変換・WebP変換コマンド

npm run allもしくは node optimize.mjs && node convert-avif.mjs && node convert-webp.mjs で元の画像を圧縮しつつWebP変換とAVIF変換も実施します。

npm run all
node optimize.mjs && node convert-avif.mjs && node convert-webp.mjs

圧縮・変換のオプション変更

公式ドキュメントの Output options の項目に画像の拡張子ごとに渡せるオプション値がまとめられています。より細かく設定を変更したい場合は参照してください。

各処理の設定場所は、config.mjsの上部に変数でまとめて記載しているので、用途に合わせて適宜調整してください。

WebP変換で透過部分の品質が気になった場合は alpha_quality(0〜100)の値を変更してみてください。

AVIF変換で画質が気になる場合はquality(1〜100)の値を調整してください。
各オプション値については以下記事の「AVIFのオプション」の項目が参考になるかもしれません。

config.mjs (ファイル上部の各画像オプション部分のみ抜粋)

〜省略〜

//jpgオプション  詳細: https://sharp.pixelplumbing.com/api-output#jpeg
const jpgOptions = {
  quality: 70,
  mozjpeg: true
}

//pngオプション  詳細: https://sharp.pixelplumbing.com/api-output#png
const pngOptions = {
  effort: 10,
  quality: 70,
  compressionLevel: 9
}

//gifオプション  詳細: https://sharp.pixelplumbing.com/api-output#gif
const gifOptions = {
  quality: 70
}

//webpオプション  詳細: https://sharp.pixelplumbing.com/api-output#webp
const webpOptions = {
  quality: 70,
  alpha_quality: 0
}

//avifオプション  詳細: https://sharp.pixelplumbing.com/api-output#avif
const avifOptions = {
  quality: 50
}

〜省略〜

参考リンク