【更新日 : 】
libSquooshで画像を一括圧縮、リサイズ、WebP変換する
- Category:
- 開発環境
- Tags:
- JavaScript, libSquoosh, 画像, 画像圧縮, 開発
手元に検証したメモがあるので記事として公開しますが、libSquooshのプロジェクトは保守されなくなったそうなので、新規プロジェクトではsharpなど別のライブラリを導入した方が良さそうです。
※ウェブアプリ版はサポートが続いているようです。
2024.07.09 追記 : sharpライブラリを用いた画像圧縮・変換の記事を公開しました。
Project no longer maintained
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn’t the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The squoosh.app web app will continue to be supported and improved.(DeepL翻訳)
https://www.npmjs.com/package/@squoosh/lib
プロジェクトは保守されなくなりました
残念ながら、数名がチームを離れ、また現在の経済情勢からくるスタッフの問題 (ugh) により、このパッケージは積極的にメンテナンスされなくなりました。残念なことですが、これに取り組む時間と人がいないのです。もしコミュニティからフォークしたい人がいれば、私はそれを祝福します。squoosh.appウェブアプリは引き続きサポートされ、改善されていきます。
Googleが開発した画像圧縮ウェブアプリ「Squoosh」のNode.js版である「libSquoosh」を試してみた備忘録です。
先に構築内容をまとめて記載しています。
後半は内容の補足などです。
ベースコード
ベースコードは主に以下の記事を参考にさせていただいています。
この記事のlibSquooshでできること
- 画像の圧縮
- 3倍の画像を2倍へリサイズ
- 3倍の画像を等倍へリサイズ
- WebP変換
- 上記4処理を分割実行する
- 上記4処理を一括実行する
- jpg、png、webpの3種類に対応
初期設定(ファイルのインストール)
構築したいディレクトリへ移動
作業を始める前にlibSquooshを構築したいディレクトリまで移動してください。
Windowsでの移動
cd C:¥Users¥.....¥libSquooshを構築したいプロジェクトディレクトリ
Macでの移動
cd /Users/....../libSquooshを構築したいプロジェクトディレクトリ
macなら該当フォルダを右クリック→「フォルダに新規ターミナルタブ」を選択してターミナルを立ち上げるとスムーズです。
libSquooshをインストール
以下のコマンドを実行してlibSquooshをインストールします。
npm install -D @squoosh/lib
libSquooshの設定用JSファイルを作成
開発ディレクトリに各処理の設定を記載するJSファイル(config.mjs)と、4つの処理をそれぞれ記載するJSファイル(convert.mjs、optimise.mjs、resize@1x.mjs、resize@2x.mjs)計5つのファイルを作成します。
config.mjs (各処理の設定を記載するファイル)
config.mjs
/*
libSquoosh
https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh
参考
https://ics.media/entry/220204/
https://openbase.com/js/@squoosh/lib
*/
import { ImagePool } from "@squoosh/lib";
import { cpus } from "os";
import { existsSync, readdirSync, readFileSync, mkdirSync } from "fs";
import { writeFile } from "fs/promises";
const imagePool = new ImagePool(cpus().length);
/*
除外リスト
*/
const ignore = ['sample1.jpg'];
/*
パス設定
*/
//画像フォルダ
const IMAGE_DIR = "./images";
//出力先フォルダ
const OUTPUT_DIR = "./dist";
/*
画像圧縮オプション
*/
//共通 前処理オプション
const preprocessOptions = {
//減色処理
quant: {
// 色数。最大256
numColors: 256,
// ディザ。0〜1で設定
dither: .9,
},
};
// JPGの圧縮オプション
const jpgEncodeOptions = {
mozjpeg: {
quality: 70,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: 3 /*YCbCr*/,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
}
}
// PNGの圧縮オプション
const pngEncodeOptions = {
oxipng: {
/*
Oxipng
https://github.com/shssoichiro/oxipng
*/
level: 4, //処理速度優先の場合は1〜2を指定
}
}
// WebPの圧縮オプション
const webpEncodeOptions = {
webp: {
quality: 70,
target_size: 0,
target_PSNR: 0,
method: 4,
sns_strength: 50,
filter_strength: 60,
filter_sharpness: 0,
filter_type: 1,
partitions: 0,
segments: 4,
pass: 1,
show_compressed: 0,
preprocessing: 0,
autofilter: 0,
partition_limit: 0,
alpha_compression: 1,
alpha_filtering: 1,
alpha_quality: 0, //透明箇所のクオリティを上げる場合は100
lossless: 0,
exact: 0,
image_hint: 0,
emulate_jpeg_size: 0,
thread_level: 0,
low_memory: 0,
near_lossless: 100,
use_delta_palette: 0,
use_sharp_yuv: 0,
}
}
/*
画像変換処理
*/
async function imageConversion (options) {
options = Object.assign({
type: 'optimise', //圧縮:optimise、WebP変換:convert、リサイズ:resize
resize: {
isWebp: true,//リサイズ処理時にwebpを生成するか
divisor: 1/3,//リサイズ処理で用いる除数
sizeName: '@1x',//リサイズ処理で用いる画像名の末尾に追加する名称
}
}, options);
const { type,resize } = options;
const {divisor, sizeName, isWebp} = resize;
// 画像フォルダ内のJPGとPNGを抽出
const imageFileList = readdirSync(IMAGE_DIR).filter((file) => {
//除外ファイルの場合は抽出しない
const isIgnore = ignore.some(fileName => new RegExp(fileName).test(file));
if(isIgnore) {
return false;
}
const regex = /\.(jpe?g|png|webp)$/i;
return regex.test(file);
});
// 抽出したファイルをimagePool内にセットし、ファイル名とimagePoolの配列を作成
const imagePoolList = imageFileList.map((fileName) => {
const imageFile = readFileSync(`${IMAGE_DIR}/${fileName}`);
const image = imagePool.ingestImage(imageFile);
return { name: fileName, image };
});
const encodeImage = async ({ image, name }) => {
let options;
let checkWebp = /\.(webp)$/i.test(name);
if(type === 'convert' || checkWebp) {
options = webpEncodeOptions;
} else if (/\.(jpe?g)$/i.test(name)) {
options = jpgEncodeOptions;
} else if (/\.(png)$/i.test(name)) {
options = pngEncodeOptions;
}
if(isWebp && !checkWebp && type === 'resize') {
options = Object.assign(options, webpEncodeOptions);
}
await image.encode(options);
}
// 前処理を実行
await Promise.all(
imagePoolList.map(async (item) => {
const { image } = item;
// リサイズなどを処理するためにデコードする
let imageInfo = await image.decoded;
//リサイズ処理
if(type === 'resize') {
const width = imageInfo.bitmap.width;
preprocessOptions.resize = {
width: width * divisor
}
await image.preprocess(preprocessOptions);
//リサイズ後メイン処理を実行する
//(リサイズ後のencodeはリサイズ処理のみで圧縮されないためここで1回実行しておく)
await image.decoded;
delete preprocessOptions.resize;
await image.preprocess(preprocessOptions);
encodeImage(item);
} else {
await image.preprocess(preprocessOptions);
}
})
);
//画像圧縮処理
await Promise.all(
imagePoolList.map(async (item) => {
encodeImage(item);
})
);
// 圧縮したデータを出力する
// 出力先フォルダがなければ作成
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR);
}
for (const item of imagePoolList) {
let {
name,
image: { encodedWith },
} = item;
// 圧縮したデータを格納する変数
let data;
let fileName = name;
let getSizeName = '';
if(type === 'resize') {
getSizeName = sizeName;
}
// JPGならMozJPEGで圧縮したデータを取得
if (encodedWith.mozjpeg) {
data = await encodedWith.mozjpeg;
}
// PNGならOxiPNGで圧縮したデータを取得
if (encodedWith.oxipng) {
data = await encodedWith.oxipng;
}
// WebPのデータを取得
if(encodedWith.webp) {
data = await encodedWith.webp;
//ファイル名を.で分割して末尾の拡張子を削除
let nameSplit = name.split('.');
nameSplit.pop();
fileName = nameSplit.join('') + getSizeName + '.webp';
}
//リサイズ処理時のリネーム
if(type === 'resize') {
//ファイル名を.で分割して拡張子を取得
let nameSplit = name.split('.');
let extType = nameSplit[nameSplit.length-1];
//末尾の拡張子を削除
nameSplit.pop();
//ファイル名をテキストに戻して末尾に名称追加
fileName = nameSplit.join('') + getSizeName + '.';
await writeFile(`${OUTPUT_DIR}/${fileName + extType}`, data.binary);
//webp生成
if(isWebp) {
data = await encodedWith.webp;
await writeFile(`${OUTPUT_DIR}/${fileName}webp`, data.binary);
}
continue;
}
await writeFile(`${OUTPUT_DIR}/${fileName}`, data.binary);
}
// imagePoolを閉じる
await imagePool.close();
}
export { imageConversion };
convert.mjs (WebP変換処理ファイル)
convert.mjs
import { imageConversion } from "./config.mjs";
imageConversion({
type: 'resize', //圧縮:optimise、WebP変換:convert、リサイズ:resize
resize: {
isWebp: true,//リサイズ処理時にwebpを生成するか
divisor: 2/3,//リサイズ処理で用いる除数
sizeName: '@2x',//リサイズ処理で用いる画像名の末尾に追加する名称
}
});
optimise.mjs (画像圧縮処理ファイル)
optimise.mjs
import { imageConversion } from "./config.mjs";
imageConversion({
type: 'convert' //圧縮:optimise、WebP変換:convert、リサイズ:resize
});
resize@1x.mjs (等倍リサイズ処理ファイル)
resize@1x.mjs
import { imageConversion } from "./config.mjs";
imageConversion({
type: 'resize', //圧縮:optimise、WebP変換:convert、リサイズ:resize
resize: {
isWebp: true,//リサイズ処理時にwebpを生成するか
divisor: 1/3,//リサイズ処理で用いる除数
sizeName: '@1x',//リサイズ処理で用いる画像名の末尾に追加する名称
}
});
resize@2x.mjs (2倍リサイズ処理ファイル)
resize@2x.mjs
import { imageConversion } from "./config.mjs";
imageConversion({
type: 'resize', //圧縮:optimise、WebP変換:convert、リサイズ:resize
resize: {
isWebp: true,//リサイズ処理時にwebpを生成するか
divisor: 2/3,//リサイズ処理で用いる除数
sizeName: '@2x',//リサイズ処理で用いる画像名の末尾に追加する名称
}
});
package.jsonへコマンドを追加
package.json
{
〜省略〜
"scripts": {
"o": "node optimise.mjs",
"r": "node resize@1x.mjs && node resize@2x.mjs",
"c": "node convert.mjs",
"cr": "node convert.mjs && node resize@1x.mjs && node resize@2x.mjs",
"all": "node optimise.mjs && node convert.mjs && node resize@1x.mjs && node resize@2x.mjs"
},
〜省略〜
}
圧縮コマンド
npm run o
もしくは node optimise.mjs
で画像圧縮を実施します。
npm run o
node optimise.mjs
リサイズコマンド
npm run r
もしくは node resize@1x.mjs && node resize@2x.mjs
で画像のリサイズを実施します。
npm run r
node resize@1x.mjs && node resize@2x.mjs
WebP変換コマンド
npm run c
もしくは node convert.mjs
でWebP変換を実施します。
npm run convert
node convert.mjs
リサイズとWebP変換コマンド
npm run cr
でリサイズとWebP変換を同時に実施します。
npm run cr
一括実行コマンド
npm run all
で全ての処理を一括で実施します。
npm run all
ディレクトリ構成
変換前の画像ファイル一式を images/ に格納する構成です。
■プロジェクトディレクトリ
┣ dist (処理後の画像が出力されるディレクトリ)
┣ images (処理する前の画像を格納)
┃
┣ config.mjs (各処理の設定を記載するファイル)
┣ convert.mjs (WebP変換処理ファイル)
┣ optimise.mjs (画像圧縮処理ファイル)
┣ resize@1x.mjs(等倍リサイズ処理ファイル)
┣ resize@1x.mjs(2倍リサイズ処理ファイル)
┃
┣ package.json (プロジェクトのjsonファイル)
┃
┗ node_modules (編集不要:自動生成されるコアファイル格納場所)
ファイルの除外オプションについて
config.mjsの上部にあるignore配列に追加したファイル名と一致する画像は対象から除外されます。
config.mjs
〜 省略 〜
/*
除外リスト
*/
const ignore = ['sample1.jpg'];
〜 省略 〜
oxipngのlevelオプションについて
oxipngのGitHubにそれらしい記載がありましたのでそのまま記載します。
Optimization: -o 1 through -o 6, lower is faster, higher is better compression. The default (-o 2) is sufficiently fast on a modern CPU and provides 30-50% compression gains over an unoptimized PNG. -o 4 is 6 times slower than -o 2 but can provide 5-10% extra compression over -o 2. Using any setting higher than -o 4 is unlikely to give any extra compression gains and is not recommended.
DeepL翻訳
最適化:-o 1から-o 6まで、低いほど速く、高いほど圧縮率が高い。デフォルト (-o 2) は最新の CPU で十分に速く、最適化されていない PNG と比較して 30 ~ 50% の圧縮率を得ることができます。-o 4 は -o 2 の 6 倍の速度だが、-o 2 よりも 5-10% 圧縮率が高くなる。
4は圧縮率が良いようですが速度にやや難があるようなので、速度優先の場合は2くらいの設定にするのが良さそうです。
細かい調整をしたい場合
画像によっては細かく調整したい場合があると思いますが、
その場合は同じオプションで視覚的に調整できるウェブアプリ版の利用をオススメします。