import sharp from 'sharp'; import { glob } from 'glob'; import path from 'path'; import fs from 'fs/promises'; const SOURCE_DIR = 'image-sources'; const PUBLIC_DIR = 'src/.vuepress/public'; async function getFileSize(filePath) { try { const stats = await fs.stat(filePath); return stats.size; } catch { return 0; } } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } async function optimizeImage(inputPath, outputPath, options = {}) { const { width, quality = 80, outputExt = '.webp' } = options; const originalSize = await getFileSize(inputPath); let pipeline = sharp(inputPath); if (width) { pipeline = pipeline.resize({ width, withoutEnlargement: true }); } if (outputExt === '.webp') { pipeline = pipeline.webp({ quality, lossless: false, force: true }); } else if (outputExt === '.png') { pipeline = pipeline.png({ quality, compressionLevel: 9, force: true }); } const buffer = await pipeline.toBuffer(); const optimizedSize = buffer.length; // Threshold: webp must be at least 10% smaller than original const isTargeted = inputPath.includes(SOURCE_DIR); const reductionRatio = (originalSize - optimizedSize) / originalSize; if (!isTargeted && reductionRatio < 0.1 && outputExt === '.webp') { console.log(`Skipping ${path.basename(inputPath)}: Reduction ${ (reductionRatio * 100).toFixed(2) }% < 10% threshold`); return null; } await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, buffer); return { inputPath, outputPath, name: path.basename(inputPath), outputName: path.basename(outputPath), originalSize, optimizedSize, reduction: (reductionRatio * 100).toFixed(2) + '%' }; } async function main() { console.log('Starting reorganized image optimization...'); const results = []; // 1. Optimize Background from Source const bgSource = path.join(SOURCE_DIR, 'bg/bgImage.jpg'); if (await fs.access(bgSource).then(() => true).catch(() => false)) { const res = await optimizeImage(bgSource, path.join(PUBLIC_DIR, 'bg/bgImage.webp'), { width: 1920, quality: 78 }); if (res) results.push(res); } // 2. Optimize Logo from Source const logoSource = path.join(SOURCE_DIR, 'logo/transparentLogo.png'); if (await fs.access(logoSource).then(() => true).catch(() => false)) { const res = await optimizeImage(logoSource, path.join(PUBLIC_DIR, 'logo/transparentLogo.webp'), { width: 256, quality: 82 }); if (res) results.push(res); } // 3. Optimize Favicon from Source (output to public/favicon.png) const favSource = path.join(SOURCE_DIR, 'favicon-source.ico'); if (await fs.access(favSource).then(() => true).catch(() => false)) { const res = await optimizeImage(favSource, path.join(PUBLIC_DIR, 'favicon.png'), { width: 32, outputExt: '.png' }); if (res) results.push(res); } // 4. Optimize Cover Images in Public Assets const coverImages = await glob('src/.vuepress/public/assets/images/cover*.{jpg,jpeg,png}'); console.log(`Found ${coverImages.length} cover images to process...`); for (const img of coverImages) { const outputWebp = img.replace(path.extname(img), '.webp'); const res = await optimizeImage(img, outputWebp, { quality: 80 }); if (res) results.push(res); } // 5. Optimize Article Images (In-place WebP generation) const articleImages = await glob('src/**/*.{jpg,jpeg,png}', { ignore: ['src/.vuepress/dist/**', 'src/.vuepress/public/**'] }); console.log(`Found ${articleImages.length} article images to process...`); for (const img of articleImages) { const metadata = await sharp(img).metadata(); const outputWebp = img.replace(path.extname(img), '.webp'); const options = { quality: 80 }; if (metadata.width > 1600) { options.width = 1600; } const res = await optimizeImage(img, outputWebp, options); if (res) results.push(res); } // Print Report console.log('\nOptimization Report:'); console.table(results.map(r => ({ File: r.name, Source: r.inputPath.startsWith(SOURCE_DIR) ? 'Source Folder' : 'Article Folder', Original: formatBytes(r.originalSize), Optimized: formatBytes(r.optimizedSize), Reduction: r.reduction }))); // 5. Update Markdown references console.log('\nUpdating Markdown references...'); const mdFiles = await glob('src/**/*.md'); for (const res of results) { // Only update for article images (not the core theme images which are already updated) if (!res.inputPath.includes(SOURCE_DIR)) { const oldName = res.name; const newName = res.outputName; for (const mdFile of mdFiles) { let content = await fs.readFile(mdFile, 'utf-8'); if (content.includes(oldName)) { const newContent = content.replaceAll(oldName, newName); if (newContent !== content) { await fs.writeFile(mdFile, newContent); console.log(`Updated ${oldName} -> ${newName} in ${mdFile}`); } } } } } const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0); const totalOptimized = results.reduce((sum, r) => sum + r.optimizedSize, 0); console.log(`\nTotal Reduction: ${formatBytes(totalOriginal - totalOptimized)} (${((totalOriginal - totalOptimized) / totalOriginal * 100).toFixed(2)}%)`); } main().catch(console.error);