developer-roadmap/scripts/compress-images.ts
Arik Chakma 240c55cc6a
feat: implement image compressor (#5551)
* wip: image compressor

* fix: ignore increase file

* Compress images

* Compress images

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-04-26 17:05:54 +01:00

155 lines
4.1 KiB
TypeScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
// ERROR: `__dirname` is not defined in ES module scope
// https://iamwebwiz.medium.com/how-to-fix-dirname-is-not-defined-in-es-module-scope-34d94a86694d
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const allowedFileExtensions = [
'.avif',
'.gif',
'.heif',
'.jpeg',
'.png',
'.raw',
'.tiff',
'.webp',
] as const;
type AllowedFileExtension = (typeof allowedFileExtensions)[number];
const publicDir = path.join(__dirname, '../public');
const cacheFile = path.join(__dirname, '/compressed-images.json');
const KB_IN_BYTES = 1024;
const COMPRESS_CONFIG = {
avif: {
chromaSubsampling: '4:4:4',
effort: 9.0,
},
gif: {
effort: 10.0,
},
jpeg: {
chromaSubsampling: '4:4:4',
mozjpeg: true,
trellisQuantisation: true,
overshootDeringing: true,
optimiseScans: true,
},
png: {
compressionLevel: 9.0,
palette: true,
},
raw: {},
tiff: {
compression: 'lzw',
},
webp: {
effort: 6.0,
},
};
(async () => {
let cache: string[] = [];
const isCacheFileExists = await fs
.access(cacheFile)
.then(() => true)
.catch(() => false);
if (isCacheFileExists) {
const cacheFileContent = await fs.readFile(cacheFile, 'utf8');
cache = JSON.parse(cacheFileContent);
}
const images = await recursiveGetImages(publicDir);
for (const image of images) {
const extname = path.extname(image).toLowerCase() as AllowedFileExtension;
if (
!allowedFileExtensions.includes(extname) ||
image.includes('node_modules') ||
image.includes('.astro') ||
image.includes('.vscode') ||
image.includes('.git')
) {
continue;
}
const stats = await fs.stat(image);
const relativeImagePath = path.relative(path.join(__dirname, '..'), image);
if (cache.includes(relativeImagePath)) {
continue;
}
const prevSize = stats.size / KB_IN_BYTES;
let imageBuffer: Buffer | undefined;
switch (extname) {
case '.avif':
imageBuffer = await sharp(image).avif(COMPRESS_CONFIG.avif).toBuffer();
break;
case '.gif':
imageBuffer = await sharp(image).gif(COMPRESS_CONFIG.gif).toBuffer();
break;
case '.heif':
imageBuffer = await sharp(image).heif().toBuffer();
break;
case '.jpeg':
imageBuffer = await sharp(image).jpeg(COMPRESS_CONFIG.jpeg).toBuffer();
break;
case '.png':
imageBuffer = await sharp(image).png(COMPRESS_CONFIG.png).toBuffer();
break;
case '.raw':
imageBuffer = await sharp(image).raw().toBuffer();
break;
case '.tiff':
imageBuffer = await sharp(image).tiff(COMPRESS_CONFIG.tiff).toBuffer();
break;
case '.webp':
imageBuffer = await sharp(image).webp(COMPRESS_CONFIG.webp).toBuffer();
break;
}
if (!imageBuffer) {
console.error(`${image} Compressing failed!`);
continue;
}
const newSize = imageBuffer.length / KB_IN_BYTES;
const diff = prevSize - newSize;
if (diff <= 0) {
console.log(`📦 Skipped ${relativeImagePath}`);
continue;
}
const diffPercent = ((diff / prevSize) * 100).toFixed(2);
console.log(
`📦 Reduced ${prevSize.toFixed(2)}KB → ${newSize.toFixed(2)}KB (${diff.toFixed(2)}KB, ${diffPercent}%) for ${relativeImagePath}`,
);
await fs.writeFile(image, imageBuffer);
cache.push(relativeImagePath);
// So that we don't lose the cache if the script crashes
await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf8');
}
await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf8');
})();
async function recursiveGetImages(dir: string): Promise<string[]> {
const subdirs = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
subdirs.map((dirent) => {
const res = path.resolve(dir, dirent.name);
return dirent.isDirectory() ? recursiveGetImages(res) : res;
}),
);
return Array.prototype.concat(...files);
}