画像を一括で圧縮するCLIプログラム
最近画像の多い記事を書くことが増えてきたので 画像を一括でリサイズと圧縮をするCLIプログラムを書きました。 とりあえずpngとアニメーションgifとjpgを圧縮するようにしています。
実行にはNode.jsの環境が必要です。 次のコマンドでインストールします。
npm install -g git://github.com/MatchaChoco010/pgjmin.git
次のようにして使います。
pgjmin -o <outdir> <image-path ...>
拡張子.png``.jpg``.jpeg``.gif
以外は無視されます。
入力の画像のパスにはglobパターンが利用可能です。
pgjmin -o out "*.png"
pgjmin -o out "*.{png,gif}"
pgjmin -o out "*.png" "*.jpg"
リサイズの大きさを指定できます。
pgjmin --width 600 --height 400 -o out *.gif
width
、height
より大きい画像だけリサイズされます。
width
、height
より小さい画像はそのままです。
省略するとリサイズされません。
圧縮のパラメータを渡せます。
pgjmin -o out --pngquant-quality 65-80 *.png
pgjmin -o out --pngquant-posterize 4 *.png
pgjmin -o out --mozjpeg-quality 80 *.jpg
pgjmin -o out --gifsicle-optimize 3 *.gif
pgjmin -o out --gifsicle-colors 128 *.gif
pgjmin -o out --mozjpeg-quality 0 --pngquant-posterize 4 --gifsicle--colors 64 --gifsicle-optimize 3 *.png *.gif *.jpg
- --pngquant-quality
min-max
の書式で指定します。
0(最低)から100(最高)までの間で指定します。
デフォルトでは65-80
です。
- --pngquant-posterize
0から4までの値で指定します。
- --mozjpeg-quality
0から100の値を指定します。 デフォルトでは80を指定しています。
- --gifsicle-optimize
1から3の値を設定します。
- --gifsicle-colors
2から256の値を設定します。
やっていることはjimp
でのリサイズとimagemin
による圧縮をしているだけです。
圧縮にはimagemin-pngquant
とgifsicle
と
imagemin-mozjpeg
を利用しています。
次のようなファイル構成になっています。
<root>
├── index.js
├── pgjmin.js
├── pgjminPng.js
├── pgjminGif.js
├── pgjminJpg.js
└── package.json
index.js
ではコマンドラインオプションをパースしてpgjmin
を呼び出しています。
#!/usr/bin/env node
const program = require('commander')
const fs = require('fs')
const path = require('path')
const pkg = require('./package.json')
const pgjmin = require('./pgjmin.js')
program
.version(pkg.version)
.usage('[options] <file...>')
.option('-o, --out-dir <dir>', 'Output directory')
.option('-w, --width <width>', 'Resize width')
.option('-h, --height <height>', 'Resize height')
.option('--pngquant-quality <min-max>', 'pngquant quality')
.option('--pngquant-posterize <0..4>', 'pngquant posterize')
.option('--mozjpeg-quality <0..100>', 'mozjpeg quality')
.option('--gifsicle-optimize <1..3>', 'gifsicle optimize')
.option('--gifsicle-colors <2..256>', 'gifsicle colors')
program.on('--help', () => {
console.log('')
console.log('Examples:')
console.log('')
console.log(' $ pgjmin -o out/ "*.jpg"')
console.log(' $ pgjmin -o out/ "*.{png,jpg}"')
console.log(' $ pgjmin -o out/ -w 300 "*.gif"')
console.log(' $ pgjmin -o out/ -h 300 "*.jpeg"')
console.log(' $ pgjmin -o out/ -w 300 -h 300 "*.gif" "*.png"')
console.log(
' $ pgjmin -o out/ --pngquant-quality 65-80 --pngquant-posterize 4 "*.png"'
)
console.log(
' $ pgjmin -o out --mozjpeg-quality 0 --pngquant-posterize 4 --gifsicle--colors 64 --gifsicle-optimize 3 *.png *.gif *.jpg'
)
})
program.parse(process.argv)
const patterns = program.args
if (patterns.length < 1) {
console.error('file path is required')
process.exit(1)
}
const outdir = path.join(__dirname, program.outDir)
if (outdir === undefined) {
console.error('output directory is required')
process.exit(1)
}
if (!fs.existsSync(outdir)) {
console.error("output directory doesn't exist")
process.exit(1)
}
if (!fs.statSync(outdir).isDirectory()) {
console.error('given output directory path is not a directory')
process.exit(1)
}
let width
if (program.width) {
width = parseInt(program.width)
if (isNaN(width)) {
console.error('width must be integer')
process.exit(1)
}
}
let height
if (program.height) {
height = parseInt(program.height)
if (isNaN(height)) {
console.error('height must be integer')
process.exit(1)
}
}
const options = {}
options.quantpngQuality = program.pngquantQuality
options.pngquantPosterize = program.pngquantPosterize
options.mozjpegQuality = program.mozjpegQuality
options.gifsicleOptimize = program.gifsicleOptimize
options.gifsicleColors = program.gitsicleColors
pgjmin(patterns, outdir, width, height, options)
pgjmin
はpgjmin.js
に定義されています。
pgjmin
はglobパターンを展開して拡張子のファイルごとに別の関数に渡しています。
すべてのPromiseが完了したら終了します。
const path = require('path')
const util = require('util')
const glob = util.promisify(require('glob'))
const flat = require('array.prototype.flat')
const png = require('./pgjminPng.js')
const gif = require('./pgjminGif.js')
const jpg = require('./pgjminJpg.js')
module.exports = async function(patterns, outdir, width, height, options) {
const paths = flat(await Promise.all(patterns.map(pattern => glob(pattern))))
const pngFiles = paths.filter(p => path.extname(p) === '.png').map(file =>
png(file, outdir, width, height, {
quality: options.pngquantQuality,
posterize: options.pngquantPosterize
})
)
const gifFiles = paths.filter(p => path.extname(p) === '.gif').map(file =>
gif(file, outdir, width, height, {
optimize: options.gifsicleOptimize,
colors: options.gifsicleColors
})
)
const jpgFiles = paths
.filter(p => /\.(jpeg)|(jpg)/.test(path.extname(p)))
.map(file =>
jpg(file, outdir, width, height, {
quality: options.mozjpegQuality
})
)
await Promise.all([...pngFiles, ...gifFiles, ...jpgFiles])
console.log('Complete!')
}
拡張子ごとの関数ではjimp
でのリサイズとimagemin
による圧縮を行っています。
pngファイル用の関数はpgjminPng.js
に記述されています。
const fs = require('fs').promises
const path = require('path')
const Jimp = require('jimp')
const imagemin = require('imagemin')
const imageminPngquant = require('imagemin-pngquant')
module.exports = async function(file, outdir, width, height, options = {}) {
const { quality = '65-80', posterize = 0 } = options
const outpath = path.join(outdir, path.basename(file))
const img = await Jimp.read(file)
if (
width !== undefined &&
width < img.bitmap.width &&
height !== undefined &&
height < img.bitmap.height
) {
img.scaleToFit(width, height)
} else if (width !== undefined && width < img.bitmap.width) {
img.resize(width, Jimp.AUTO)
} else if (height !== undefined && height < img.bitmap.height) {
img.resize(Jimp.AUTO, height)
}
const resizedBuffer = await img.getBufferAsync(Jimp.MIME_PNG)
const optimizedBuffer = await imagemin.buffer(resizedBuffer, {
plugins: [imageminPngquant({ quality, posterize })]
})
await fs.writeFile(outpath, optimizedBuffer)
}
jpgファイル用の関数はpgjminJpg.js
に記述されています。
const fs = require('fs').promises
const path = require('path')
const Jimp = require('jimp')
const imagemin = require('imagemin')
const imageminMozjpeg = require('imagemin-mozjpeg')
module.exports = async function(file, outdir, width, height, options = {}) {
const { quality } = options
const outpath = path.join(outdir, path.basename(file))
const img = await Jimp.read(file)
if (
width !== undefined &&
width < img.bitmap.width &&
height !== undefined &&
height < img.bitmap.height
) {
img.scaleToFit(width, height)
} else if (width !== undefined && width < img.bitmap.width) {
img.resize(width, Jimp.AUTO)
} else if (height !== undefined && height < img.bitmap.height) {
img.resize(Jimp.AUTO, height)
}
const resizedBuffer = await img.getBufferAsync(Jimp.MIME_JPEG)
const optimizedBuffer = await imagemin.buffer(resizedBuffer, {
plugins: [imageminMozjpeg({ quality })]
})
await fs.writeFile(outpath, optimizedBuffer)
}
gifファイル用の関数はpgjminJpg.js
に記述されています。
jimpがgifファイルに対応していないためimagemin-gifsicle
を使わずに
gifsicle
を直接使って圧縮とリサイズを行っています。
const path = require('path')
const util = require('util')
const execFile = util.promisify(require('child_process').execFile)
const gifsicle = require('gifsicle')
module.exports = async function(file, outdir, width, height, options = {}) {
const { optimize = 3, colors } = options
const outpath = path.join(outdir, path.basename(file))
const args = []
args.push('--output', outpath)
args.push(`--optimize=${optimize}`)
if (colors) {
args.push(`--colors=${colors}`)
}
if (width !== undefined && height !== undefined) {
args.push(`--resize-fit=${width}x${height}`)
} else if (width !== undefined) {
args.push(`--resize-fit-width=${width}`)
} else if (height !== undefined) {
args.push(`--resize-fit-height=${height}`)
}
args.push(file)
await execFile(gifsicle, args)
}