LoginSignup
5
4

【Node.js】sharp でサクッと「AVIF」「WebP」生成

Last updated at Posted at 2023-10-02

次世代画像形式といわれる「AVIF」と「WebP」。
WEBサイトに組み込むことで、表示速度を大幅に向上させることができます。

とっても魅力的な「AVIF」と「WebP」ですが、画像を生成するのが課題となります。
対応しているグラフィックソフトがほとんどないんです…

利用する場合は、プロジェクトごとに自動生成の仕組みを導入することになるかと思います。
でも、自動生成の仕組みが導入されていないプロジェクトでも使いたい…:tired_face:

というわけで、Node.js の画像処理モジュール sharp を使って、サクッと「AVIF」と「WebP」を生成できるようにしてみました!

仕様

事前に「JPEG」または「PNG」で書き出した画像を、「AVIF」と「WebP」に変換します。

  • 変換前画像
    • src ディレクトリに格納(デフォルト)
      • ディレクトリ名は変更可能(オプション)
    • 配下にディレクトリを含む場合でも再帰的に変換
  • 変換後画像
    • dest ディレクトリに生成(デフォルト)
      • ディレクトリ名は変更可能(オプション)
      • ディレクトリが存在しない場合は自動で生成
    • 変換元画像と同一ディレクトリ( src )に生成可能(オプション)
    • ファイル名
      • 変換前画像の拡張子を含むことが可能(オプション)
    • 画像形式と品質
      • AVIF: 80 と WebP: 80(デフォルト)
      • 画像形式をどちらか一方だけにすることが可能(オプション)
      • 書き出し品質を変更可能
    • すでに同一ファイル名の画像が存在する場合は上書き

ファイル構造

デフォルト設定では以下のような構造になります。

image-format-converter
├── dest/
│   ├── hoge.avif
│   ├── hoge.webp
│   ├── fuga.avif
│   ├── fuga.webp
│   ├── piyo.avif
│   └── piyo.webp
├── node_modules
├── src/
│   ├── hoge.jpg
│   ├── fuga.jpg
│   └── piyo.jpg
├── _image-format-converter.bat
├── image-format-converter.js
├── package.json
└── package-lock.json

準備

package.json の作成

package.json
{
  "name": "image-format-converter",
  "version": "1.0.0",
  "license": "UNLICENSED",
  "private": true,
  "scripts": {
    "image-format-converter": "node ./image-format-converter.js"
  },
  "type": "module"
}

node_module のインストール

必要な node_module は以下となります。

モジュール名 役割
ansi-colors ログメッセージに色をつける
fancy-log 時刻付きのログを表示
globule ファイルのパスを取得
sharp 画像を処理する
npm i ansi-colors fancy-log globule sharp -D

npm-scripts の作成

image-format-converter.js
import c from 'ansi-colors'
import log from 'fancy-log'
import fs from 'fs'
import globule from 'globule'
import sharp from 'sharp'

class ImageFormatConverter {
  constructor(options = {}) {
    this.srcBase = options.srcBase || 'src'
    this.destBase = options.destBase || 'dest'
    this.includeExtensionName = options.includeExtensionName || false
    this.formats = options.formats || [
      {
        type: 'avif',
        quality: 50
      },
      {
        type: 'webp',
        quality: 80
      }
    ]
    this.srcImages = `${this.srcBase}/**/*.{jpg,jpeg,png}`
    this.init()
  }

  init = async () => {
    const imagePathList = this.findImagePaths()
    await this.convertImages(imagePathList)
  }

  /**
   * globパターンで指定した画像パスを配列化して返す
   * @return { array } 画像パスの配列
   */
  findImagePaths = () => {
    return globule.find({
      src: [this.srcImages]
    })
  }

  /**
   * 画像を変換する
   * @param { string } imagePath 画像パス
   * @param { object } format 画像形式と圧縮品質
   */
  convertImageFormat = async (imagePath, format) => {
    const reg = /\/(.*)\.(jpe?g|png)$/i
    const [, imageName, imageExtension] = imagePath.match(reg)
    const imageFileName = this.includeExtensionName
      ? `${imageName}.${imageExtension}`
      : imageName
    const destPath = `${this.destBase}/${imageFileName}.${format.type}`
    await sharp(imagePath)
      .toFormat(format.type, { quality: format.quality })
      .toFile(destPath)
      .then((info) => {
        log(
          `Converted ${c.blue(imagePath)} to ${c.yellow(
            format.type.toUpperCase()
          )} ${c.green(destPath)}`
        )
      })
      .catch((err) => {
        log(
          c.red(
            `Error converting image to ${c.yellow(
              format.type.toUpperCase()
            )}\n${err}`
          )
        )
      })
  }

  /**
   * 配列内の画像パスのファイルを変換する
   * @param { array } imagePathList 画像パスの配列
   */
  convertImages = async (imagePathList) => {
    if (imagePathList.length === 0) {
      log(c.red('No images found to convert'))
      return
    }
    for (const imagePath of imagePathList) {
      const reg = new RegExp(`^${this.srcBase}/(.*/)?`)
      const path = imagePath.match(reg)[1] || ''
      const destDir = `${this.destBase}/${path}`
      if (!fs.existsSync(destDir)) {
        try {
          fs.mkdirSync(destDir, { recursive: true })
          log(`Created directory ${c.green(destDir)}`)
        } catch (err) {
          log(`Failed to create directory ${c.green(destDir)}\n${err}`)
        }
      }
      const conversionPromises = this.formats.map((format) =>
        this.convertImageFormat(imagePath, format)
      )
      await Promise.all(conversionPromises)
    }
  }
}
const imageFormatConverter = new ImageFormatConverter()

オプション

インスタンス作成時、以下のオプション設定が可能です。

オプション 内容 デフォルト
srcBase 変換前画像の格納ディレクトリ名 string 'src'
destBase 変換後画像の格納ディレクトリ名 string 'dest'
includeExtensionName ファイル名に変換前画像の拡張子を含む boolean false
formats 変換する画像形式と品質 object AVIF: 50
WebP: 80
image-format-converter.js

const imageFormatConverter = new ImageFormatConverter({
  // 変換前画像の格納ディレクトリ変更
  srcBase: 'hoge',
  // 変換後画像の格納ディレクトリ変更
  destBase: 'hoge',
  // ファイル名に変換前画像の拡張子を含む
  includeExtensionName: true,
  // AVIFだけ生成する
  formats: [
    {
      type: 'avif',
      quality: 60 // 品質を変更
    },
  ]
})

バッチファイル の作成(Windows)

Windows のみとなります。
npm-scripts の実行を簡単にするためにバッチファイルを作成します。

_image-format-converter.bat
@echo off
PowerShell -command npm run image-format-converter
pause

npm-scripts の実行

Windows

バッチファイル( _image-format-converter.bat )をダブルクリックでOK:ok_hand:

Mac

ターミナルで image-format-converter.js ファイルのあるディレクトリ移動し、以下コマンドを実行します。

npm run image-format-converter
5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4