#!/usr/bin/env node
/**
* 使用 TinyPNG API 压缩大图片并替换原文件
* 用法: node compress_images.js <API_KEY> [最小文件大小KB,默认500]
*
* 获取免费 API Key 要加上http : tinypng.com/developers
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const API_KEY = "p31kx4TbNJmtM5NKKKyyMGcPl84cbW6g";
const MIN_SIZE_KB = parseInt(process.argv[3] || '50');
if (!API_KEY) {
console.error('用法: node compress_images.js <API_KEY> [最小文件大小KB]');
process.exit(1);
}
const RES_ROOT = path.resolve(__dirname, '2.0');
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
function findLargeImages(minSizeBytes) {
const results = [];
function walk(dir) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir)) {
const full = path.join(dir, entry);
const stat = fs.statSync(full);
if (stat.isDirectory()) walk(full);
else if (IMAGE_EXTS.has(path.extname(full).toLowerCase()) && stat.size >= minSizeBytes) {
results.push({ filePath: full, size: stat.size });
}
}
}
walk(RES_ROOT);
return results.sort((a, b) => b.size - a.size);
}
function tinypngCompress(filePath) {
return new Promise((resolve, reject) => {
const fileData = fs.readFileSync(filePath);
const auth = Buffer.from(`api:${API_KEY}`).toString('base64');
// 第一步:上传图片
const uploadReq = https.request({
hostname: 'api.tinify.com',
path: '/shrink',
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/octet-stream',
'Content-Length': fileData.length,
},
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode !== 201) {
try {
const err = JSON.parse(body);
reject(new Error(`上传失败: ${err.message}`));
} catch {
reject(new Error(`上传失败: HTTP ${res.statusCode}`));
}
return;
}
const location = res.headers['location'];
if (!location) { reject(new Error('未获取到压缩结果地址')); return; }
// 第二步:下载压缩后的图片
https.get(location, {
headers: { 'Authorization': `Basic ${auth}` }
}, (dlRes) => {
const chunks = [];
dlRes.on('data', chunk => chunks.push(chunk));
dlRes.on('end', () => {
const compressed = Buffer.concat(chunks);
resolve({ compressed, output: JSON.parse(body).output });
});
dlRes.on('error', reject);
}).on('error', reject);
});
});
uploadReq.on('error', reject);
uploadReq.write(fileData);
uploadReq.end();
});
}
async function main() {
const minBytes = MIN_SIZE_KB * 1024;
console.log(`扫描大于 ${MIN_SIZE_KB}KB 的图片...\n`);
const images = findLargeImages(minBytes);
if (images.length === 0) {
console.log('没有找到符合条件的图片');
return;
}
console.log(`找到 ${images.length} 张图片需要压缩:\n`);
for (const { filePath, size } of images) {
console.log(` ${(size / 1024 / 1024).toFixed(2)}MB ${path.relative(__dirname, filePath)}`);
}
console.log('');
let totalSaved = 0;
let successCount = 0;
let failCount = 0;
for (let i = 0; i < images.length; i++) {
const { filePath, size } = images[i];
const relPath = path.relative(__dirname, filePath);
process.stdout.write(`[${i + 1}/${images.length}] 压缩 ${relPath} ... `);
try {
const { compressed, output } = await tinypngCompress(filePath);
const saved = size - compressed.length;
const ratio = ((saved / size) * 100).toFixed(1);
fs.writeFileSync(filePath, compressed);
totalSaved += saved;
successCount++;
console.log(`✓ ${(size/1024).toFixed(0)}KB → ${(compressed.length/1024).toFixed(0)}KB (节省 ${ratio}%)`);
} catch (err) {
failCount++;
console.log(`✗ 失败: ${err.message}`);
}
// 避免请求过快
if (i < images.length - 1) await new Promise(r => setTimeout(r, 200));
}
console.log(`\n========== 完成 ==========`);
console.log(`成功: ${successCount} 张,失败: ${failCount} 张`);
console.log(`共节省: ${(totalSaved / 1024 / 1024).toFixed(2)}MB`);
}
main().catch(err => {
console.error('错误:', err.message);
process.exit(1);
});
其中
const MIN_SIZE_KB = parseInt(process.argv[3] || '50');
50代表压缩50kb以上大小的图片
const RES_ROOT = path.resolve(__dirname, '2.0');
2.0代表文件目录,记得把这个node放在需要压缩图片的根目录