73 lines
2.5 KiB
JavaScript
73 lines
2.5 KiB
JavaScript
|
|
import sharp from 'sharp';
|
||
|
|
import { readdir, stat, rename } from 'node:fs/promises';
|
||
|
|
import { join, extname, basename } from 'node:path';
|
||
|
|
|
||
|
|
const MAX_WIDTH = 1600;
|
||
|
|
const MIN_BYTES_TO_OPTIMISE = 250 * 1024;
|
||
|
|
|
||
|
|
const dirs = ['src/lib/images', 'static/images'];
|
||
|
|
|
||
|
|
async function optimiseFile(file) {
|
||
|
|
const ext = extname(file).toLowerCase();
|
||
|
|
const input = await sharp(file, { failOn: 'none' }).rotate();
|
||
|
|
const meta = await input.metadata();
|
||
|
|
const width = meta.width ?? 0;
|
||
|
|
const targetWidth = width > MAX_WIDTH ? MAX_WIDTH : width;
|
||
|
|
const pipeline = sharp(file, { failOn: 'none' })
|
||
|
|
.rotate()
|
||
|
|
.resize({ width: targetWidth, withoutEnlargement: true });
|
||
|
|
|
||
|
|
let buf;
|
||
|
|
if (ext === '.png') {
|
||
|
|
if (meta.hasAlpha) {
|
||
|
|
buf = await pipeline
|
||
|
|
.png({ palette: true, quality: 88, compressionLevel: 9, effort: 10 })
|
||
|
|
.toBuffer();
|
||
|
|
} else {
|
||
|
|
buf = await pipeline
|
||
|
|
.png({ palette: true, quality: 82, compressionLevel: 9, effort: 10 })
|
||
|
|
.toBuffer();
|
||
|
|
}
|
||
|
|
} else if (ext === '.jpg' || ext === '.jpeg') {
|
||
|
|
buf = await pipeline.jpeg({ quality: 82, mozjpeg: true }).toBuffer();
|
||
|
|
} else if (ext === '.webp') {
|
||
|
|
buf = await pipeline.webp({ quality: 80, effort: 6 }).toBuffer();
|
||
|
|
} else {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const original = (await stat(file)).size;
|
||
|
|
if (buf.length >= original) return { file, original, optimised: original, skipped: true };
|
||
|
|
|
||
|
|
const tmp = file + '.opt.tmp';
|
||
|
|
await sharp(buf).toFile(tmp);
|
||
|
|
await rename(tmp, file);
|
||
|
|
return { file, original, optimised: buf.length, skipped: false };
|
||
|
|
}
|
||
|
|
|
||
|
|
let totalOrig = 0;
|
||
|
|
let totalNew = 0;
|
||
|
|
for (const dir of dirs) {
|
||
|
|
const entries = await readdir(dir);
|
||
|
|
for (const name of entries) {
|
||
|
|
if (!/\.(png|jpe?g|webp)$/i.test(name)) continue;
|
||
|
|
const file = join(dir, name);
|
||
|
|
const s = await stat(file);
|
||
|
|
if (s.size < MIN_BYTES_TO_OPTIMISE) continue;
|
||
|
|
try {
|
||
|
|
const res = await optimiseFile(file);
|
||
|
|
if (!res) continue;
|
||
|
|
totalOrig += res.original;
|
||
|
|
totalNew += res.optimised;
|
||
|
|
const pct = ((1 - res.optimised / res.original) * 100).toFixed(0);
|
||
|
|
const flag = res.skipped ? ' (skipped: no gain)' : '';
|
||
|
|
console.log(
|
||
|
|
`${basename(file).padEnd(58)} ${(res.original / 1024).toFixed(0).padStart(5)}KB → ${(res.optimised / 1024).toFixed(0).padStart(5)}KB (-${pct}%)${flag}`
|
||
|
|
);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('FAILED', file, err.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log(`\nTotal: ${(totalOrig / 1024 / 1024).toFixed(2)} MB → ${(totalNew / 1024 / 1024).toFixed(2)} MB`);
|