UNPKG

sharp-pic

Version:

A tool for image compression

183 lines (159 loc) 5.41 kB
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;