Files
MyBlog/scripts/optimize-images.mjs
T
2026-05-11 11:12:43 +08:00

146 lines
5.1 KiB
JavaScript

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();
// For standard images, if webp is larger, we'll keep it if it's the specific target like bgImage
// but for general article images we might skip.
const isTargeted = inputPath.includes(SOURCE_DIR);
if (!isTargeted && buffer.length >= originalSize && outputExt === '.webp') {
console.log(`Skipping ${path.basename(inputPath)}: Optimized size (${formatBytes(buffer.length)}) >= original (${formatBytes(originalSize)})`);
return null;
}
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, buffer);
const optimizedSize = buffer.length;
return {
inputPath,
outputPath,
name: path.basename(inputPath),
outputName: path.basename(outputPath),
originalSize,
optimizedSize,
reduction: ((originalSize - optimizedSize) / originalSize * 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 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);