sharp-pic
Version:
A tool for image compression
149 lines (127 loc) • 4.37 kB
JavaScript
const fs = require('fs');
const path = require('path');
const jsonfile = require('jsonfile');
const pLimit = require('p-limit');
const sharp = require('sharp');
// 配置项
const exts = ['.jpg', '.png', '.jpeg'];
const maxSize = 10 * 1024 * 1024; // 5MB
const compressRecord = {}; // 压缩记录缓存
const limit = pLimit(3); // 并发限制
/**
* 本地压缩函数
*/
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)
const relativePath = path.relative(compressRecordPath, filePath)
.replace(/\\/g, '/'); // 统一路径格式
// 检查是否已压缩过
if (compressRecord[compressRecordPath][relativePath]) {
// console.log(`⏩ 跳过已处理文件: ${relativePath}`);
return 'skip';
}
let retries = 3;
while (retries > 0) {
try {
const tempPath = `${filePath}.tmp${Date.now()}`;
const data = await compressFile(filePath, tempPath);
// 替换原文件
fs.unlinkSync(filePath);
fs.renameSync(tempPath, filePath);
// 更新记录(使用相对路径)
compressRecord[compressRecordPath][relativePath] = {
ratio: data.output.ratio,
date: new Date().toISOString()
};
console.log(`✅ [${relativePath}] 压缩成功,节省 ${data.output.ratio * 100}%`);
return 'success';
} catch (error) {
retries--;
if (retries === 0) {
console.log(`❌ 失败: ${relativePath}`, 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 {
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}`);
console.log(`❌ 失败: ${fail}`);
console.log(`💾 记录文件: ${recordFile}`);
}
module.exports = startCompressImage;