sharp-pic
Version:
A tool for image compression
189 lines (162 loc) • 5.63 kB
JavaScript
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const jsonfile = require('jsonfile');
const pLimit = require('p-limit');
const sharp = require('sharp');
// 配置项
const exts = ['.jpg', '.png', '.jpeg'];
const maxSize = 10 * 1024 * 1024; // 10MB
const compressRecord = {}; // { MD5: 相对路径 }
const limit = pLimit(5); // 并发限制
const jpegOptions = { quality: 75, mozjpeg: true };
const pngOptions = { palette: true, quality: 85, compressionLevel: 9 };
/**
* 计算文件MD5
*/
function computeMD5(filePath) {
try {
const buffer = fs.readFileSync(filePath);
return crypto.createHash('md5').update(buffer).digest('hex');
} catch (err) {
console.error(`❌ MD5计算失败: ${filePath}`, err.message);
return null;
}
}
/**
* 本地压缩函数
*/
async function compressFile(inputPath, outputPath) {
try {
const meta = await sharp(inputPath).metadata();
let processor = sharp(inputPath);
// 根据格式设置参数
if (meta.format === 'jpeg') {
processor = processor.jpeg(jpegOptions);
} else if (meta.format === 'png') {
processor = processor.png(pngOptions);
}
// 执行压缩
await processor.toFile(outputPath);
// 返回压缩信息
const origSize = fs.statSync(inputPath).size;
const compressedSize = fs.statSync(outputPath).size;
return {
input: { size: origSize },
output: {
size: compressedSize,
ratio: (1 - compressedSize / origSize).toFixed(2)
}
};
} catch (err) {
throw new Error(`压缩失败: ${err.message}`);
}
}
/**
* 处理单个文件
*/
async function processFile(filePath, rootDir) {
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/');
const record = compressRecord[rootDir];
// 计算原始文件MD5
const originalMD5 = computeMD5(filePath);
if (!originalMD5) return 'fail';
// 检查是否已处理过
if (record[originalMD5] === relativePath) {
// console.log(`⏩ 跳过已处理文件: ${relativePath}`);
return 'skip';
}
let retries = 3;
while (retries > 0) {
const tempPath = `${filePath}.tmp${Date.now()}`;
try {
// 执行压缩
const data = await compressFile(filePath, tempPath);
const newMD5 = computeMD5(tempPath);
// 检查重复内容
if (record[newMD5]) {
console.log(`♻️ 内容重复: ${relativePath} → ${record[newMD5]}`);
fs.unlinkSync(tempPath);
} else {
// 替换原文件
fs.unlinkSync(filePath);
fs.renameSync(tempPath, filePath);
// 清理旧记录
Object.keys(record).forEach(md5 => {
if (record[md5] === relativePath) delete record[md5];
});
// 添加新记录
record[newMD5] = relativePath;
}
const origSize = `${(data.input.size / 1024).toFixed(2)}KB`;
const compressedSize = `${(data.output.size / 1024).toFixed(2)}KB`;
console.log(`🎉 [${relativePath}] 压缩成功,原始大小:${origSize},压缩大小:${compressedSize},优化比例:${data.output.ratio * 100}%`);
return 'success';
} catch (error) {
retries--;
fs.existsSync(tempPath) && fs.unlinkSync(tempPath);
if (retries === 0) {
console.log(`❌ 失败: ${relativePath}`, error.message);
return 'fail';
}
// await new Promise(resolve => setTimeout(resolve, 300));
}
}
}
/**
* 递归处理目录
*/
async function processDirectory(folder, rootDir) {
const files = fs.readdirSync(folder);
let total = 0, success = 0, fail = 0;
await Promise.all(files.map(file => {
const filePath = path.join(folder, file);
const stat = fs.statSync(filePath);
if (stat.isFile() && exts.includes(path.extname(filePath).toLowerCase())) {
// 文件处理:包裹在并发限制中
return limit(async () => {
if (stat.size > maxSize) {
console.log(`⏭️ 跳过大文件: ${file} (${(stat.size / 1024 / 1024).toFixed(2)}MB)`);
return;
}
total++;
const result = await processFile(filePath, rootDir);
if (result === 'success') success++;
else if (result === 'fail') fail++;
});
} else if (stat.isDirectory()) {
// 目录处理:立即执行不占用并发名额
return processDirectory(filePath, rootDir).then(subResult => {
total += subResult.total;
success += subResult.success;
fail += subResult.fail;
});
}
}));
return { total, success, fail };
}
/**
* 入口函数
*/
async function startCompressImage(imagePath) {
console.log('🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀 准备开始压缩图片,请耐心等待 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀');
const recordFile = path.join(imagePath, 'tiny.json');
// 初始化记录
try {
compressRecord[imagePath] = jsonfile.readFileSync(recordFile);
// console.log('🔍 加载历史压缩记录');
} catch (e) {
compressRecord[imagePath] = {};
}
// 执行压缩
const { total, success, fail } = await processDirectory(imagePath, imagePath);
// 保存记录
jsonfile.writeFileSync(recordFile, compressRecord[imagePath], { spaces: 2 });
console.log('\n📊 统计结果 📊');
console.log(`📂 总文件数: ${total}`);
console.log(`📂 本次压缩: ${success + fail}`);
console.log(`✅ 成功: ${success}`);
console.log(`❌ 失败: ${fail}`);
// console.log(`💾 记录文件: ${recordFile}`);
}
module.exports = startCompressImage;