UNPKG

sharp-pic

Version:

A tool for image compression

189 lines (162 loc) 5.63 kB
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;