156 lines
5.5 KiB
JavaScript
156 lines
5.5 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();
|
|
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);
|