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);