99 lines
3.0 KiB
JavaScript
99 lines
3.0 KiB
JavaScript
const sharp = require('sharp')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const IMG_DIR = path.join(__dirname, 'src', 'img')
|
|
const QUALITY = 60 // JPG 质量
|
|
const MAX_WIDTH = 800 // 最长边限制
|
|
const MAX_FILE_KB = 80 // 单张上限 KB —— 强制压到 80KB 以下
|
|
|
|
/**
|
|
* 压缩单个文件(强制覆盖)
|
|
*/
|
|
async function compressFile(inputPath) {
|
|
const ext = path.extname(inputPath).toLowerCase()
|
|
const dir = path.dirname(inputPath)
|
|
const base = path.basename(inputPath, ext)
|
|
const finalPath = path.join(dir, base + '.jpg')
|
|
const isAlreadyJpg = ext === '.jpg' || ext === '.jpeg'
|
|
|
|
const stat = fs.statSync(inputPath)
|
|
const currentKb = stat.size / 1024
|
|
|
|
// 如果已经 .jpg、体积已小于上限、且最长边不超,跳过
|
|
if (isAlreadyJpg && currentKb <= MAX_FILE_KB) {
|
|
const meta = await sharp(inputPath).metadata()
|
|
const longEdge = Math.max(meta.width || 0, meta.height || 0)
|
|
if (longEdge <= MAX_WIDTH) {
|
|
console.log(`skip ${inputPath} (${currentKb.toFixed(1)}KB, ${longEdge}px)`)
|
|
return { from: inputPath, to: inputPath, size: stat.size }
|
|
}
|
|
}
|
|
|
|
// 重建 pipeline
|
|
const meta = await sharp(inputPath).metadata()
|
|
const longEdge = Math.max(meta.width || 0, meta.height || 0)
|
|
const resizeOpts = longEdge > MAX_WIDTH ? { width: MAX_WIDTH, height: MAX_WIDTH, fit: 'inside' } : undefined
|
|
|
|
let pipeline = sharp(inputPath).jpeg({ quality: QUALITY, progressive: true, force: true })
|
|
if (resizeOpts) pipeline = pipeline.resize(resizeOpts)
|
|
|
|
const tmpPath = path.join(dir, base + '_tmp.jpg')
|
|
await pipeline.toFile(tmpPath)
|
|
|
|
fs.unlinkSync(inputPath)
|
|
fs.renameSync(tmpPath, finalPath)
|
|
|
|
const newSize = fs.statSync(finalPath).size
|
|
console.log(
|
|
`${isAlreadyJpg ? 'compress' : 'convert'} ${base}${ext} (${currentKb.toFixed(1)}KB, ${longEdge}px → ${(newSize / 1024).toFixed(1)}KB)`
|
|
)
|
|
|
|
return { from: inputPath, to: finalPath, size: newSize }
|
|
}
|
|
|
|
/**
|
|
* 递归收集所有图片
|
|
*/
|
|
function collectImages(dir) {
|
|
let results = []
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
for (const entry of entries) {
|
|
const full = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
results = results.concat(collectImages(full))
|
|
} else if (/\.(png|jpe?g)$/i.test(entry.name)) {
|
|
results.push(full)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* 主函数
|
|
*/
|
|
async function main() {
|
|
console.log(`scanning ${IMG_DIR}...\n`)
|
|
const images = collectImages(IMG_DIR)
|
|
console.log(`found ${images.length} images\n`)
|
|
|
|
let totalBefore = 0
|
|
let totalAfter = 0
|
|
|
|
for (const img of images) {
|
|
totalBefore += fs.statSync(img).size
|
|
const result = await compressFile(img)
|
|
totalAfter += result.size
|
|
}
|
|
|
|
console.log(`\n✅ done`)
|
|
console.log(` before: ${(totalBefore / 1024 / 1024).toFixed(2)} MB`)
|
|
console.log(` after: ${(totalAfter / 1024 / 1024).toFixed(2)} MB`)
|
|
console.log(` saved: ${((totalBefore - totalAfter) / 1024 / 1024).toFixed(2)} MB`)
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|