soyoo-cocos/cocos压缩脚本.js

205 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

const sharp = require('sharp');
const fs = require('fs').promises;
const path = require('path');
const ffmpeg = require('fluent-ffmpeg');
// 支持的图片格式
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp'];
const AUDIO_EXTENSIONS = ['.mp3'];
// 压缩配置
const COMPRESSION_OPTIONS = {
jpeg: {
quality: 60, // 降低质量以获得更高压缩率
mozjpeg: true, // 使用 mozjpeg 压缩
chromaSubsampling: '4:2:0' // 更激进的色度采样
},
png: {
quality: 60, // 降低质量
compressionLevel: 9, // 最高压缩级别
palette: true, // 使用调色板模式
effort: 10, // 最大压缩效果
colors: 128 // 减少调色板颜色数量以增加压缩率
},
webp: {
quality: 60, // 降低质量
effort: 6, // 最高压缩效果
lossless: false, // 有损压缩
nearLossless: false // 关闭近无损压缩
}
};
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
async function processDirectory(inputDir) {
try {
const items = await fs.readdir(inputDir);
let totalOriginalSize = 0;
let totalCompressedSize = 0;
for (const item of items) {
const filePath = path.join(inputDir, item);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await processDirectory(filePath);
} else if (stats.isFile()) {
const ext = path.extname(item).toLowerCase();
const originalSize = stats.size;
if (IMAGE_EXTENSIONS.includes(ext)) {
await compressImage(filePath);
} else if (AUDIO_EXTENSIONS.includes(ext)) {
await compressAudio(filePath);
} else {
continue;
}
const compressedStats = await fs.stat(filePath);
const compressedSize = compressedStats.size;
totalOriginalSize += originalSize;
totalCompressedSize += compressedSize;
const savedSize = originalSize - compressedSize;
const savingPercentage = ((savedSize / originalSize) * 100).toFixed(2);
console.log(`已压缩: ${filePath}`);
console.log(` 原始大小: ${formatFileSize(originalSize)}`);
console.log(` 压缩后大小: ${formatFileSize(compressedSize)}`);
console.log(` 节省: ${formatFileSize(savedSize)} (${savingPercentage}%)`);
console.log('----------------------------------------');
}
}
if (totalOriginalSize > 0) {
const totalSaved = totalOriginalSize - totalCompressedSize;
const totalSavingPercentage = ((totalSaved / totalOriginalSize) * 100).toFixed(2);
console.log(`\n当前目录 ${inputDir} 总计:`);
console.log(` 原始总大小: ${formatFileSize(totalOriginalSize)}`);
console.log(` 压缩后总大小: ${formatFileSize(totalCompressedSize)}`);
console.log(` 总共节省: ${formatFileSize(totalSaved)} (${totalSavingPercentage}%)\n`);
}
} catch (error) {
console.error('处理出错:', error);
}
}
async function compressImage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const tempPath = filePath + '.temp';
const image = sharp(filePath);
try {
// 获取图片信息
const metadata = await image.metadata();
// 根据图片大小和类型选择压缩策略
switch (ext) {
case '.jpg':
case '.jpeg':
await image
.jpeg(COMPRESSION_OPTIONS.jpeg)
.toFile(tempPath);
break;
case '.png':
// 如果PNG是透明的保持PNG格式
if (metadata.hasAlpha) {
await image
.png(COMPRESSION_OPTIONS.png)
.toFile(tempPath);
} else {
// 如果PNG不是透明的转换为JPEG可能会得到更好的压缩效果
await image
.jpeg(COMPRESSION_OPTIONS.jpeg)
.toFile(tempPath);
}
break;
case '.webp':
await image
.webp(COMPRESSION_OPTIONS.webp)
.toFile(tempPath);
break;
}
// 检查压缩后的文件大小
const originalStats = await fs.stat(filePath);
const compressedStats = await fs.stat(tempPath);
// 只有当压缩后的文件更小时才替换
if (compressedStats.size < originalStats.size) {
await fs.unlink(filePath);
await fs.rename(tempPath, filePath);
console.log(` 压缩成功: ${formatFileSize(originalStats.size)} -> ${formatFileSize(compressedStats.size)}`);
} else {
// 如果压缩后反而更大,则删除临时文件保留原文件
await fs.unlink(tempPath);
console.log(` 跳过压缩: 原始大小 ${formatFileSize(originalStats.size)}, 压缩后 ${formatFileSize(compressedStats.size)}`);
console.log(` 原因: 压缩后文件更大,已保留原文件`);
}
} catch (error) {
try {
await fs.unlink(tempPath);
} catch (e) {
// 忽略临时文件删除失败的错误
}
throw error;
}
}
async function compressAudio(filePath) {
const tempPath = filePath + '.temp';
return new Promise((resolve, reject) => {
ffmpeg(filePath)
.toFormat('mp3')
.audioBitrate(64) // 降低到64kbps以获得最大压缩
.audioChannels(1) // 转换为单声道
.audioFrequency(22050) // 降低采样率
.audioCodec('libmp3lame') // 使用LAME编码器
.addOptions([
'-q:a 9', // 最高压缩质量
'-compression_level 9' // 最高压缩级别
])
.on('end', async () => {
try {
// 检查压缩后的文件大小
const originalStats = await fs.stat(filePath);
const compressedStats = await fs.stat(tempPath);
// 只有当压缩后的文件更小时才替换
if (compressedStats.size < originalStats.size) {
await fs.unlink(filePath);
await fs.rename(tempPath, filePath);
console.log(` 压缩成功: ${formatFileSize(originalStats.size)} -> ${formatFileSize(compressedStats.size)}`);
} else {
await fs.unlink(tempPath);
console.log(` 跳过压缩: 原始大小 ${formatFileSize(originalStats.size)}, 压缩后 ${formatFileSize(compressedStats.size)}`);
console.log(` 原因: 压缩后文件更大,已保留原文件`);
}
resolve();
} catch (error) {
reject(error);
}
})
.on('error', (err) => {
reject(err);
})
.save(tempPath);
});
}
// 设置输入目录
const INPUT_DIR = './assets';
// 运行脚本
(async () => {
console.log('开始处理图片压缩...');
await processDirectory(INPUT_DIR);
console.log('图片压缩完成!');
})();