const fs = require('fs-extra');
const path = require('path');
const sharp = require('sharp');
const SKETCH_DIR = path.join(__dirname, '../static/sketch');
const BACKUP_DIR = path.join(__dirname, '../static/sketch-backup');
const MAX_WIDTH = 2560;
const MAX_HEIGHT = 1440;
const WEBP_QUALITY = 92;
const WEBP_EFFORT = 6;
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.avif'];
function getFileSize(filePath) {
const stats = fs.statSync(filePath);
return stats.size;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + 'B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
return (bytes / (1024 * 1024)).toFixed(2) + 'MB';
}
async function processImage(inputPath, backupPath) {
if (!fs.existsSync(backupPath)) {
fs.ensureDirSync(path.dirname(backupPath));
fs.copyFileSync(inputPath, backupPath);
console.log(`📦 已备份: ${path.relative(process.cwd(), inputPath)}`);
}
const originalSize = getFileSize(inputPath);
const metadata = await sharp(inputPath).metadata();
const ext = path.extname(inputPath).toLowerCase();
const fileName = path.basename(inputPath, ext);
const dir = path.dirname(inputPath);
const webpPath = path.join(dir, fileName + '.webp');
let pipeline = sharp(inputPath);
if (metadata.width > MAX_WIDTH || metadata.height > MAX_HEIGHT) {
pipeline = pipeline.resize({
width: MAX_WIDTH,
height: MAX_HEIGHT,
fit: 'inside',
kernel: 'lanczos3',
withoutEnlargement: true,
});
}
pipeline = pipeline.webp({
quality: WEBP_QUALITY,
alphaQuality: WEBP_QUALITY,
lossless: false,
effort: WEBP_EFFORT,
nearLossless: false,
smartSubsample: true,
});
await pipeline.toFile(webpPath);
if (ext !== '.webp') {
fs.unlinkSync(inputPath);
} else {
}
const newSize = getFileSize(webpPath);
const saved = ((originalSize - newSize) / originalSize * 100).toFixed(1);
const ratio = (newSize / originalSize * 100).toFixed(1);
const fromExt = ext !== '.webp' ? ext.toUpperCase().replace('.', '') : 'WebP';
console.log(`🔄 ${fromExt} → WebP: ${path.basename(inputPath)}`);
console.log(` 📊 ${formatSize(originalSize)} → ${formatSize(newSize)} (${ratio}%,节省 ${saved}%)`);
return { webpPath, originalPath: inputPath };
}
async function scanDirectory(dir, backupBaseDir) {
const items = fs.readdirSync(dir);
let processed = 0;
let totalFiles = 0;
let skipped = 0;
let totalSaved = 0;
let totalOriginal = 0;
let converted = 0;
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
if (fullPath === BACKUP_DIR) continue;
const subBackupDir = path.join(backupBaseDir, item);
const result = await scanDirectory(fullPath, subBackupDir);
processed += result.processed;
totalFiles += result.totalFiles;
skipped += result.skipped;
totalSaved += result.totalSaved || 0;
totalOriginal += result.totalOriginal || 0;
converted += result.converted || 0;
} else {
const ext = path.extname(item).toLowerCase();
if (IMAGE_EXTS.includes(ext)) {
totalFiles++;
try {
const relativePath = path.relative(SKETCH_DIR, fullPath);
const backupPath = path.join(BACKUP_DIR, relativePath);
if (ext === '.gif') {
console.log(`⏭️ 跳过 GIF(保留动画): ${path.basename(fullPath)}`);
skipped++;
continue;
}
const originalSize = getFileSize(fullPath);
await processImage(fullPath, backupPath);
const fileName = path.basename(fullPath, ext);
const dir = path.dirname(fullPath);
const webpPath = path.join(dir, fileName + '.webp');
const newSize = getFileSize(webpPath);
totalSaved += (originalSize - newSize);
totalOriginal += originalSize;
converted++;
processed++;
} catch (err) {
console.error(`❌ 处理失败 ${item}:`, err.message);
}
} else {
skipped++;
}
}
}
return { processed, totalFiles, skipped, totalSaved, totalOriginal, converted };
}
async function main() {
console.log('🚀 开始批量转换 WebP...\n');
console.log(`📊 最大尺寸: ${MAX_WIDTH}×${MAX_HEIGHT}px`);
console.log(`📊 WebP 质量: ${WEBP_QUALITY}%`);
console.log(`📊 压缩努力度: ${WEBP_EFFORT}/6\n`);
if (!fs.existsSync(SKETCH_DIR)) {
console.error('❌ 找不到 static/sketch 目录');
process.exit(1);
}
console.log('📊 正在扫描...');
const result = await scanDirectory(SKETCH_DIR, BACKUP_DIR);
const totalMB = (result.totalOriginal / (1024 * 1024)).toFixed(1);
const savedMB = (result.totalSaved / (1024 * 1024)).toFixed(1);
const finalMB = ((result.totalOriginal - result.totalSaved) / (1024 * 1024)).toFixed(1);
const savedPercent = result.totalOriginal > 0 ? (result.totalSaved / result.totalOriginal * 100).toFixed(1) : 0;
console.log('\n' + '='.repeat(50));
console.log(`✅ 全部完成!`);
console.log(` 📁 转换了 ${result.converted} 张图片为 WebP`);
console.log(` 📁 处理了 ${result.processed} 张图片`);
console.log(` 📁 共 ${result.totalFiles} 张图片(跳过 ${result.skipped} 张)`);
console.log(` 📊 总大小: ${totalMB}MB → ${finalMB}MB`);
console.log(` 📊 节省: ${savedMB}MB (${savedPercent}%)`);
console.log(` 📁 原图备份在: ${BACKUP_DIR}`);
console.log(`\n💡 所有图片已转换为 .webp 格式!`);
console.log(`💡 如果效果不满意,可以从备份恢复:`);
console.log(` cp -r ${BACKUP_DIR}/* ${SKETCH_DIR}/`);
}
main().catch((err) => {
console.error('❌ 程序出错:', err.message);
process.exit(1);
});