129 lines
4.2 KiB
JavaScript
129 lines
4.2 KiB
JavaScript
const sharp = require('sharp');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const STATIC_DIR = path.join(__dirname, '../frontend/static');
|
|
|
|
// Compression quality settings
|
|
const QUALITY = {
|
|
jpeg: 80,
|
|
png: 80,
|
|
webp: 80,
|
|
};
|
|
|
|
// File size threshold (in MB) - only compress files larger than this
|
|
const MIN_SIZE_MB = 0.5;
|
|
|
|
function getFileSizeMB(filePath) {
|
|
const stats = fs.statSync(filePath);
|
|
return stats.size / (1024 * 1024);
|
|
}
|
|
|
|
function getOutputPath(filePath, ext) {
|
|
const dir = path.dirname(filePath);
|
|
const name = path.basename(filePath, ext);
|
|
return path.join(dir, `${name}.compressed${ext}`);
|
|
}
|
|
|
|
async function compressImage(filePath) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const sizeMB = getFileSizeMB(filePath);
|
|
|
|
if (sizeMB < MIN_SIZE_MB) {
|
|
console.log(` Skipping (${sizeMB.toFixed(2)}MB < ${MIN_SIZE_MB}MB): ${path.relative(STATIC_DIR, filePath)}`);
|
|
return { skipped: true };
|
|
}
|
|
|
|
const outputPath = getOutputPath(filePath, ext);
|
|
const originalSize = fs.statSync(filePath).size;
|
|
|
|
try {
|
|
let pipeline = sharp(filePath);
|
|
|
|
if (ext === '.png') {
|
|
pipeline = pipeline.png({ quality: QUALITY.png, compressionLevel: 9 });
|
|
} else if (ext === '.jpg' || ext === '.jpeg') {
|
|
pipeline = pipeline.jpeg({ quality: QUALITY.jpeg, mozjpeg: true });
|
|
} else if (ext === '.webp') {
|
|
pipeline = pipeline.webp({ quality: QUALITY.webp });
|
|
} else {
|
|
return { skipped: true };
|
|
}
|
|
|
|
await pipeline.toFile(outputPath);
|
|
|
|
const compressedSize = fs.statSync(outputPath).size;
|
|
const originalSizeMB = originalSize / (1024 * 1024);
|
|
const compressedSizeMB = compressedSize / (1024 * 1024);
|
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
|
|
|
|
if (compressedSize < originalSize) {
|
|
// Replace original with compressed
|
|
fs.unlinkSync(filePath);
|
|
fs.renameSync(outputPath, filePath);
|
|
console.log(` Compressed (${ratio}%↓, ${originalSizeMB.toFixed(2)}MB → ${compressedSizeMB.toFixed(2)}MB): ${path.relative(STATIC_DIR, filePath)}`);
|
|
return { compressed: true, saved: originalSize - compressedSize };
|
|
} else {
|
|
// Compression didn't help, discard compressed file
|
|
fs.unlinkSync(outputPath);
|
|
console.log(` No benefit (${originalSizeMB.toFixed(2)}MB → ${compressedSizeMB.toFixed(2)}MB): ${path.relative(STATIC_DIR, filePath)}`);
|
|
return { noBenefit: true };
|
|
}
|
|
} catch (err) {
|
|
console.error(` Error: ${filePath} - ${err.message}`);
|
|
return { error: true };
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const dryRun = args.includes('--dry-run');
|
|
const specificExt = args.find(arg => arg.startsWith('--ext='))?.replace('--ext=', '');
|
|
|
|
console.log('Image Compression Script');
|
|
console.log('=======================');
|
|
if (dryRun) console.log('Mode: DRY RUN (no files will be modified)\n');
|
|
else console.log('Mode: LIVE (files will be compressed in place)\n');
|
|
|
|
const extensions = specificExt
|
|
? [`.${specificExt}`]
|
|
: ['.png', '.jpg', '.jpeg', '.webp'];
|
|
|
|
// Find all image files
|
|
const imageFiles = [];
|
|
for (const ext of extensions) {
|
|
const pattern = path.join(STATIC_DIR, '**', `*${ext}`);
|
|
const { globSync } = require('glob');
|
|
const files = globSync(pattern, { nodir: true });
|
|
imageFiles.push(...files);
|
|
}
|
|
|
|
console.log(`Found ${imageFiles.length} ${extensions.join('/')} images in static/\n`);
|
|
console.log('Compressing files > 0.5MB (use --dry-run to preview)...\n');
|
|
|
|
let totalSaved = 0;
|
|
let processed = 0;
|
|
let skipped = 0;
|
|
let noBenefit = 0;
|
|
let errors = 0;
|
|
|
|
for (const file of imageFiles) {
|
|
processed++;
|
|
const result = await compressImage(file);
|
|
if (result.saved) totalSaved += result.saved;
|
|
if (result.skipped) skipped++;
|
|
if (result.noBenefit) noBenefit++;
|
|
if (result.error) errors++;
|
|
}
|
|
|
|
console.log('\n--- Summary ---');
|
|
console.log(`Processed: ${processed}`);
|
|
console.log(`Skipped (small): ${skipped}`);
|
|
console.log(`No benefit: ${noBenefit}`);
|
|
console.log(`Errors: ${errors}`);
|
|
console.log(`Total saved: ${(totalSaved / (1024 * 1024)).toFixed(2)} MB`);
|
|
if (dryRun) console.log('\n(Run without --dry-run to apply changes)');
|
|
}
|
|
|
|
main().catch(console.error);
|