// 把 public/werewolf 下的牌图压到每张 <= 200KB(原地覆盖)。 // 策略:最长边限 900px,mozjpeg 质量从高到低递减,直到达标。 // 已经 <= 目标的文件跳过,避免重复编码反复掉质量。 // 运行: npm run compress:images import sharp from 'sharp' import { readdir, stat, writeFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { dirname, resolve, join } from 'node:path' const here = dirname(fileURLToPath(import.meta.url)) const root = resolve(here, '../public/werewolf') const TARGET = 200 * 1024 const MAX_EDGE = 900 const QUALITIES = [82, 76, 70, 64, 58, 52, 46] async function listJpgs(dir) { const out = [] for (const name of await readdir(dir, { withFileTypes: true })) { const p = join(dir, name.name) if (name.isDirectory()) out.push(...(await listJpgs(p))) else if (/\.jpe?g$/i.test(name.name)) out.push(p) } return out } async function compress(file) { const before = (await stat(file)).size if (before <= TARGET) return { file, skipped: true, before } let chosen = null for (const quality of QUALITIES) { const buf = await sharp(file) .rotate() // 按 EXIF 摆正 .resize({ width: MAX_EDGE, height: MAX_EDGE, fit: 'inside', withoutEnlargement: true }) .jpeg({ quality, mozjpeg: true }) .toBuffer() chosen = { buf, quality } if (buf.length <= TARGET) break } await writeFile(file, chosen.buf) return { file, before, after: chosen.buf.length, quality: chosen.quality } } const files = await listJpgs(root) let totalBefore = 0 let totalAfter = 0 for (const f of files) { const r = await compress(f) const rel = f.slice(root.length + 1) if (r.skipped) { totalBefore += r.before totalAfter += r.before console.log(`skip ${rel} (${(r.before / 1024) | 0}K)`) } else { totalBefore += r.before totalAfter += r.after console.log(`ok ${rel} ${(r.before / 1024) | 0}K -> ${(r.after / 1024) | 0}K q${r.quality}`) } } console.log(`\n${files.length} files: ${(totalBefore / 1024 / 1024).toFixed(1)}MB -> ${(totalAfter / 1024 / 1024).toFixed(1)}MB`)