mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-25 10:14:20 +02:00
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>
This commit is contained in:
154
scripts/compress-images.ts
Normal file
154
scripts/compress-images.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
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);
|
||||
}
|
Reference in New Issue
Block a user