topfans/scripts/compress-images.js
2026-04-07 23:08:49 +08:00

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