sharp-pic
Version:
A tool for image compression
183 lines (159 loc) • 5.41 kB
JavaScript
const fs = require('fs');
const path = require('path');
const jsonfile = require('jsonfile');
const pLimit = require('p-limit');
const sharp = require('sharp');
const crypto = require('crypto');
// 配置项
const exts = ['.jpg', '.png', '.jpeg'];
const maxSize = 10 * 1024 * 1024; // 5MB
const compressRecord = {}; // 压缩记录缓存
const limit = pLimit(3); // 并发限制
function computeMD5(filePath) {
const buffer = fs.readFileSync(filePath);
return crypto.createHash('md5').update(buffer).digest('hex');
}
/**
* 本地压缩函数
*/
async function compressFile(inputPath, outputPath) {
try {
const meta = await sharp(inputPath).metadata();
let processor = sharp(inputPath);
// 根据格式设置参数
if (meta.format === 'jpeg') {
processor = processor.jpeg({ quality: 70, mozjpeg: true });
} else if (meta.format === 'png') {
processor = processor.png({ quality: 75, compressionLevel: 9 });
}
// 执行压缩
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, compressRecordPath) {
// 计算当前文件MD5
let currentMD5;
try {
currentMD5 = computeMD5(filePath);
} catch (err) {
console.log(`❌ MD5计算失败: ${filePath}`, err.message);
return 'fail';
}
// 检查是否已处理过
if (compressRecord[compressRecordPath].existingHashes.has(currentMD5)) {
console.log(`⏩ 跳过已处理文件: ${filePath}`);
return 'skip';
}
// 压缩流程
let retries = 3;
while (retries-- > 0) {
try {
const tempPath = `${filePath}.tmp${Date.now()}`;
const data = await compressFile(filePath, tempPath);
// 计算压缩文件MD5
const newMD5 = computeMD5(tempPath);
// 检查是否已存在相同压缩文件
if (compressRecord[compressRecordPath].existingHashes.has(newMD5)) {
console.log(`⏭️ 发现重复压缩文件,使用缓存: ${filePath}`);
fs.unlinkSync(tempPath);
} else {
// 替换原文件
fs.unlinkSync(filePath);
fs.renameSync(tempPath, filePath);
}
// 更新记录
compressRecord[compressRecordPath].entries[currentMD5] = {
compressedMD5: newMD5,
ratio: data.output.ratio,
date: new Date().toISOString()
};
[currentMD5, newMD5].forEach(md5 =>
compressRecord[compressRecordPath].existingHashes.add(md5)
);
console.log(`✅ [${filePath}] 压缩成功,节省 ${(data.output.ratio * 100).toFixed(1)}%`);
return 'success';
} catch (error) {
if (retries === 0) {
console.log(`❌ 最终失败: ${filePath}`, error.message);
return 'fail';
}
await new Promise(resolve => setTimeout(resolve, 300));
}
}
}
/**
* 递归处理目录(保持不变)
*/
async function processDirectory(folder, recordPath) {
const files = fs.readdirSync(folder);
let total = 0, success = 0, fail = 0;
await Promise.all(files.map(file => limit(async () => {
const filePath = path.join(folder, file);
const stat = fs.statSync(filePath);
if (stat.isFile() && exts.includes(path.extname(filePath).toLowerCase())) {
if (stat.size > maxSize) {
console.log(`⏭️ 跳过大文件: ${file} (${(stat.size / 1024 / 1024).toFixed(2)}MB)`);
return;
}
total++;
const result = await processFile(filePath, recordPath);
if (result === 'success') success++;
else if (result === 'fail') fail++;
} else if (stat.isDirectory()) {
const subResult = await processDirectory(filePath, recordPath);
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 {
const recordData = jsonfile.readFileSync(recordFile);
compressRecord[imagePath] = {
entries: recordData.entries || {},
existingHashes: new Set([
...Object.keys(recordData.entries),
...Object.values(recordData.entries).map(e => e.compressedMD5)
])
};
} catch (e) {
compressRecord[imagePath] = { entries: {}, existingHashes: new Set() };
}
// 执行压缩
const { total, success, fail } = await processDirectory(imagePath, imagePath);
// 保存记录(使用相对路径)
// jsonfile.writeFileSync(recordFile, compressRecord[imagePath], { spaces: 2 });
jsonfile.writeFileSync(recordFile,
{ entries: compressRecord[imagePath].entries },
{ spaces: 2 }
);
console.log('\n📊 统计:');
console.log(`📂 文件总数: ${total}`);
console.log(`✅ 成功: ${success}`);
console.log(`❌ 失败: ${fail}`);
console.log(`💾 记录文件: ${recordFile}`);
}
module.exports = startCompressImage;