Squoosh開発終了? globが通らない?
いままで上記のスクリプトでlibsqooshを使って画像を変換していたが、
- Node18以降に対応していない
- 正直遅い(特にPNGの変換)
- 開発が終了してしまった
ことを受け、話題のsharpに乗り換えてみた。
また、最近はSVGファイルを使うことが多くなったのでこちらにも対応することにした
さらに、globを最新にしたところ、Windowsでエラーが出るようになってしまった。
どうやら、ディレクトリの区切り文字がOS依存になり、スラッシュからバックスラッシュ(円マーク)になってしまった模様。
これも合わせて修正したい。
要件
- 前述のスクリプトをベースにsharpに書き換える
- svgファイルはSVGOで最適化する
- svgzも出力可能にする
- globの仕様が9.xで変わってしまったためこちらにも対応する
利用パッケージ
# 前述のスクリプトから以降する場合
npm remove @squoosh/lib
npm install --save-dev sharp svgo zlib
# 新規に作成する場合
npm install --save-dev sharp glob path fs-extra svgo zlib commander
スクリプト(convertImage.mjs)
/**
* 画像一括変換スクリプト
* 参考: https://sharp.pixelplumbing.com/
*
* インストール
* npm install --save-dev sharp glob path fs-extra svgo zlib commander
*
* 実行(例)
* node convertImage.mjs -i ./dev/_assets/images -o ./public_html/static/images -w -t
*
* ヘルプ
* node convertImage.mjs -h
*/
import sharp from "sharp";
import {globSync} from "glob";
import path from "path";
import fse from "fs-extra";
import { Command } from "commander";
import { optimize, loadConfig } from "svgo";
import zlib from "zlib";
// 引数設定
const program = new Command();
program
.requiredOption("-i, --input <string>", "ソースディレクトリ(必須)")
.requiredOption("-o, --out <string>", "出力先ディレクトリ(必須)")
.option("-m, --minify", "画像の最適化を行う(同一拡張子での変換)", false)
.option("-w, --webp", "webp化を行う", false)
.option("-a, --webp-suffix-add", "webp化の際、拡張子を書き換え(false)するか追加(true)するか", false)
.option("-v, --svg", "svgの最適化を行う", false)
.option("-z, --svgz", "svgzを出力する", false)
.option("-n, --nosvg", "svgzを出力した場合、svgは出力しない", false)
.option("-t, --truncate", "出力先のディレクトリを空にする", false)
.parse();
/**
* 設定項目ここから
*/
// 変換対象拡張子とエンコーダーの設定
const GET_ENCODER_FROM_EXTENSION = {
jpg: "jpg",
jpeg: "jpg",
png: "png"
};
// 変換オプション(参考: https://sharp.pixelplumbing.com/api-output)
const ENCODER_OPTIONS = {
png: {
compressionLevel: 9,
adaptiveFiltering: true,
progressive: true
},
jpg: {
quality: 90
},
webp: {
png: {
lossless: true
},
jpg: {
quality: 90
}
}
};
// SVGを認識する拡張子
const SVG_EXTENSION = "svg";
/**
* 設定項目ここまで
*/
// オプション項目読み取り
const Options = program.opts();
const IMAGE_DIR = Options.input;
const OUTPUT_DIR = Options.out;
const DO_OPTIMIZE = Options.minify;
const DO_OPTIMIZE_SVG = Options.svg;
const ENCODE_WEBP = Options.webp;
const WEBP_SUFFIX_ADD = Options.webpSuffixAdd;
const ENCODE_SVGZ = Options.svgz;
const NO_SVG = ENCODE_SVGZ && Options.nosvg;
const TRUNCATE_BEFORE = Options.truncate;
const svgoConfig = await loadConfig(); // svgo.config.jsから設定を取得
// ソースディレクトリからファイル一覧を取得
let imageFileList = [];
globSync(IMAGE_DIR + "/**/*.*").map(function(file) {
// windows対応
file = "./" + file.replace(/\\/g, "/");
imageFileList.push(file.replace(IMAGE_DIR, "."));
});
// 出力先ディレクトリを空にする
if (TRUNCATE_BEFORE) {
fse.emptyDirSync(OUTPUT_DIR);
}
// 変数初期化
const ts_start = Date.now();
let ts_worker_start = Date.now();
let ts_worker_end;
let targetFileNum = imageFileList.length;
let encodedFileNum = 1;
await Promise.all(
imageFileList.map(async imagePath => {
// ファイルの拡張子を取得
const fileExtension = path.extname(imagePath).substring(1).toLowerCase();
// ソースパスと出力パスを取得
const sourcePath = path.join(IMAGE_DIR, imagePath);
const destinationPath = path.join(OUTPUT_DIR, imagePath);
// destinationPathのディレクトリがなければ作成
await fse.ensureDir(path.dirname(destinationPath))
// 拡張子からエンコーダーを取得
const encoder = GET_ENCODER_FROM_EXTENSION[fileExtension];
// SVGかどうか
const isSvg = fileExtension === "svg";
// 変数の初期化
let action = "";
let isCopy = !encoder && !isSvg;
let encodeOptions = {};
let binaryData = "";
if (encoder !== "") {
// エンコーダーの設定
if (DO_OPTIMIZE) {
encodeOptions[encoder] = ENCODER_OPTIONS[encoder];
}
if (ENCODE_WEBP) {
encodeOptions["webp"] = ENCODER_OPTIONS["webp"];
}
if (Object.keys(encodeOptions).length === 0) {
isCopy = true;
}
}
if (isCopy) {
// エンコード対象外
await fse.copy(sourcePath, destinationPath);
action = "copied";
} else if (isSvg) {
// SVGの処理
binaryData = fse.readFileSync(sourcePath);
if (DO_OPTIMIZE_SVG) {
binaryData = optimize(binaryData, svgoConfig);
binaryData = binaryData.data;
}
if (!NO_SVG) {
await fse.outputFile(destinationPath, binaryData);
action += "optimized";
}
if (ENCODE_SVGZ) {
await zlib.gzip(binaryData, async (__, svgzData) => {
await fse.outputFile(destinationPath + "z", svgzData);
});
if (action !== "") {
action += " and ";
}
action += "encoded to svgz";
}
} else {
// 最適化を行う
if (DO_OPTIMIZE) {
// encoder と encodeOptions を指定して最適化
await sharp(sourcePath)
.toFormat(encoder, ENCODER_OPTIONS[encoder])
.toFile(destinationPath);
action += "optimized";
}
if (ENCODE_WEBP) {
// webp と encodeOptions を指定して最適化
const destinationPathWebp = WEBP_SUFFIX_ADD
? destinationPath + ".webp"
: destinationPath.slice(0, fileExtension.length * -1) + "webp";
await sharp(sourcePath)
.webp(ENCODER_OPTIONS["webp"][encoder])
.toFile(destinationPathWebp);
if (action !== "") {
action += " and ";
}
action += "encoded to webp";
}
}
// 変換結果表示
ts_worker_end = Date.now();
console.info(
"[",
encodedFileNum++,
"/",
targetFileNum,
"]",
imagePath,
"is",
action,
"(",
ts_worker_end - ts_worker_start,
"ms",
")"
);
ts_worker_start = ts_worker_end;
})
);
// 結果表示
console.info("done!", "(", "total:", ts_worker_end - ts_start, "ms", ")");
ポイント
- 引数設定やOPTIONの変更
- CPU数に関する設定を削除
- OPTIONSの設定がSquooshとsharpで微妙に違うので注意。なお、設定値はお好みで変更すること。
- SVGに関する設定を追加
- SVGのOPTIONは長くなるので
svgo.config.js
に記載することにした。
- globのwindows対策
- Windowsでglob9.x以降を使うとディレクトリ記号がスラッシュ
/
から\
マークになってしまうため、file = "./" + file.replace(/\\/g, "/");
を追加している。
- Windowsでglob9.x以降を使うとディレクトリ記号がスラッシュ
- ディレクトリの作成
- Squooshはディレクトリを勝手に作成してくれたが、sharpは出来ないようなので、予めディレクトリを作成するコードを追記している
- 変換部分は大分簡略化出来た。
- バッファとかいらなくなったのでパス指定で変換できるようになった。
利用方法
設定項目(SVG以外)
ここをいじると変換対象の拡張子とlibsquooshのオプション設定を変更できる
GET_ENCODER_FROM_EXTENSION
に含まれていない(svg以外の)拡張子は出力ディレクトリへのコピーのみ行われる。
正直 SVG_EXTENSION はいらない気がするが、念のため書いておいた。
webpはjpegから変換する場合とpngから変換する場合で分けられるようにした。
// convertImage.mjs
// line44付近から
/**
* 設定項目ここから
*/
// 変換対象拡張子とエンコーダーの設定(参考: https://squoosh.app/)
const GET_ENCODER_FROM_EXTENSION = {
jpg: "jpg",
jpeg: "jpg",
png: "png"
};
// 変換オプション(参考: https://sharp.pixelplumbing.com/api-output)
const ENCODER_OPTIONS = {
png: {
compressionLevel: 9,
adaptiveFiltering: true,
progressive: true
},
jpg: {
quality: 90
},
webp: {
png: {
lossless: true
},
jpg: {
quality: 90
}
}
};
// SVGを認識する拡張子
const SVG_EXTENSION = "svg";
/**
* 設定項目ここまで
*/
設定項目(SVG)
svgo.config.js が存在すればそれを利用、なければデフォルトで動作する。
詳しくは以下のConfigurationとBuilt-in pluginsを参照
ヘルプを表示(by commander)
node convertImage.mjs -h
Options:
-i, --input <string> ソースディレクトリ(必須)
-o, --out <string> 出力先ディレクトリ(必須)
-m, --minify 画像の最適化を行う(同一拡張子での変換) (default: false)
-w, --webp webp化を行う (default: false)
-a, --webp-suffix-add webp化の際、拡張子を書き換え(false)するか追加(true)するか (default: false)
-v, --svg svgの最適化を行う (default: false)
-z, --svgz svgzを出力する (default: false)
-n, --nosvg svgzを出力した場合、svgは出力しない (default: false)
-t, --truncate 出力先のディレクトリを空にする (default: false)
-h, --help display help for command
実行
変換元データ
> ls -al ./src/assets/images
-rwxrwxrwx 1 user user 49375 4月 10 12:27 sample01.jpg
-rwxrwxrwx 1 user user 14739 10月 27 16:56 sample02.png
-rwxrwxrwx 1 user user 1792 4月 10 12:27 sample03.svg
-rwxrwxrwx 1 user user 70627 4月 11 15:51 sample04.html
> ls -al ./src/assets/images/sub
-rwxrwxrwx 1 user user 255303 3月 7 2022 sample11.PNG
-rwxrwxrwx 1 user user 1198601 3月 7 2022 sample12.jpeg
-rwxrwxrwx 1 user user 346045 3月 16 2021 sample13.PNG
1. jpg,pngをwebp変換し、SVGを最適化する
IE非対応ならこれでOK。
ちなみに、同名のjpgとpng画像を作ってしまうとあとから変換したもので上書きされてしまうので注意
> node convertImage.mjs -i ./src/images -o ./public/assets/images -w -t -v
// 出力結果
[ 1 / 7 ] ./sample01.jpg is encoded to webp ( 45 ms )
[ 2 / 7 ] ./sample02.png is encoded to webp ( 59 ms )
[ 3 / 7 ] ./sample03.svg is optimized ( 4 ms )
[ 4 / 7 ] ./sample04.html is copied ( 6 ms )
[ 5 / 7 ] ./sub/sample13.PNG is encoded to webp ( 176 ms )
[ 6 / 7 ] ./sub/sample11.PNG is encoded to webp ( 46 ms )
[ 7 / 7 ] ./sub/sample12.jpeg is encoded to webp ( 24 ms )
done! ( total: 360 ms )
> ls -al ./public/assets/images
-rwxrwxrwx 1 user user 12146 4月 11 16:51 sample01.webp
-rwxrwxrwx 1 user user 4724 4月 11 16:51 sample02.webp
-rwxrwxrwx 1 user user 1448 4月 11 16:51 sample03.svg
-rwxrwxrwx 1 user user 70627 4月 11 15:51 sample04.html
ls -al ./public/assets/images/sub
-rwxrwxrwx 1 user user 145486 4月 11 16:51 sample11.webp
-rwxrwxrwx 1 user user 315570 4月 11 16:51 sample12.webp
-rwxrwxrwx 1 user user 206952 4月 11 16:51 sample13.webp
2. jpg,pngの最適化を行い、かつ、元拡張子.webp の形でwebp変換、svgはsvgzファイルも出力する
IEや旧デバイス対応なら
> node convertImage.mjs -i ./src/images -o ./public/assets/images -m -w -t -v -z
// 出力結果
[ 1 / 7 ] ./sample01.jpg is optimized and encoded to webp ( 80 ms )
[ 2 / 7 ] ./sample03.svg is optimized and encoded to svgz ( 63 ms )
[ 3 / 7 ] ./sample02.png is optimized and encoded to webp ( 5 ms )
[ 4 / 7 ] ./sample04.html is copied ( 15 ms )
[ 5 / 7 ] ./sub/sample13.PNG is optimized and encoded to webp ( 334 ms )
[ 6 / 7 ] ./sub/sample12.jpeg is optimized and encoded to webp ( 16 ms )
[ 7 / 7 ] ./sub/sample11.PNG is optimized and encoded to webp ( 63 ms )
> ls -al ./public/assets/images
-rwxrwxrwx 1 user user 16964 4月 11 16:59 sample01.jpg
-rwxrwxrwx 1 user user 12146 4月 11 16:59 sample01.webp
-rwxrwxrwx 1 user user 15487 4月 11 16:59 sample02.png
-rwxrwxrwx 1 user user 4724 4月 11 16:59 sample02.webp
-rwxrwxrwx 1 user user 1448 4月 11 16:59 sample03.svg
-rwxrwxrwx 1 user user 794 4月 11 16:59 sample03.svgz
-rwxrwxrwx 1 user user 70627 4月 11 15:51 sample04.html
> ls -al ./public/assets/images/sub
-rwxrwxrwx 1 user user 257940 4月 11 16:59 sample11.PNG
-rwxrwxrwx 1 user user 145486 4月 11 16:59 sample11.webp
-rwxrwxrwx 1 user user 423161 4月 11 16:59 sample12.jpeg
-rwxrwxrwx 1 user user 315570 4月 11 16:59 sample12.webp
-rwxrwxrwx 1 user user 365373 4月 11 16:59 sample13.PNG
-rwxrwxrwx 1 user user 206952 4月 11 16:59 sample13.webp
利用シーン
package.json に image とか convertimage とか登録しておけばいちいちパスなどを打たなくても良い
{
...,
"scripts": {
...,
"image": "node convertImage.mjs -i ./src/assets/images -o ./public/assets/images -w -v -z -t",
},
...
}
これでnpm run image
とすれば楽に変換可能。
感想
変換がめちゃくちゃ早い
Squooshやimageminでは特にPNGの変換がとても遅いのだが、sharpはなにか設定を間違えてるんじゃないかと思うくらい早い。
具体的にはいままで160秒くらいかかっていた変換処理が20秒足らずで終わるようになった。
いままでデプロイのボトルネックが画像だったのでこれは嬉しい。
astro-compressで実感していたけれど、実際に比較したことはなかったので感動した。