mirror of
https://github.com/Dvorinka/ClubLogos.git
synced 2026-06-03 19:42:58 +00:00
fff
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { optimize as optimizeSvg } from 'svgo';
|
||||
import sharp from 'sharp';
|
||||
import chalk from 'chalk';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
// File size reporting thresholds (in KB)
|
||||
sizeThresholds: {
|
||||
small: 10,
|
||||
medium: 100,
|
||||
large: 500,
|
||||
},
|
||||
// Compression settings
|
||||
compression: {
|
||||
png: {
|
||||
quality: 80, // 1-100
|
||||
effort: 6, // 1-9 (higher is slower but better compression)
|
||||
},
|
||||
jpg: {
|
||||
quality: 80, // 1-100
|
||||
mozjpeg: true,
|
||||
},
|
||||
pdf: {
|
||||
quality: 'printer', // 'screen', 'ebook', 'printer', 'prepress', or 'default'
|
||||
},
|
||||
},
|
||||
// Directories to scan (relative to project root)
|
||||
scanDirs: [
|
||||
'data/logos',
|
||||
'frontend/dist',
|
||||
],
|
||||
// File extensions to process
|
||||
fileExtensions: ['svg', 'png', 'jpg', 'jpeg', 'pdf'],
|
||||
};
|
||||
|
||||
// Statistics
|
||||
const stats = {
|
||||
totalFiles: 0,
|
||||
processedFiles: 0,
|
||||
skippedFiles: 0,
|
||||
totalSaved: 0,
|
||||
byType: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Format file size in human-readable format
|
||||
*/
|
||||
function formatSize(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]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size in bytes
|
||||
*/
|
||||
async function getFileSize(filePath) {
|
||||
const stats = await fs.stat(filePath);
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize SVG file
|
||||
*/
|
||||
async function optimizeSvgFile(filePath) {
|
||||
const svg = await fs.readFile(filePath, 'utf8');
|
||||
const result = optimize(svg, {
|
||||
path: filePath,
|
||||
multipass: true,
|
||||
plugins: [
|
||||
'preset-default',
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
name: 'addAttributesToSVGElement',
|
||||
params: {
|
||||
attributes: [{ 'data-optimized': 'true' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, result.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize PNG/JPG file
|
||||
*/
|
||||
async function optimizeImageFile(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const isJpeg = ['.jpg', '.jpeg'].includes(ext);
|
||||
|
||||
const pipeline = sharp(filePath);
|
||||
|
||||
// Get image metadata
|
||||
const metadata = await pipeline.metadata();
|
||||
|
||||
// Skip if already optimized
|
||||
if (metadata.optimizationLevel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optimize based on file type
|
||||
if (isJpeg) {
|
||||
await pipeline.jpeg({
|
||||
quality: CONFIG.compression.jpg.quality,
|
||||
mozjpeg: CONFIG.compression.jpg.mozjpeg,
|
||||
});
|
||||
} else {
|
||||
await pipeline.png({
|
||||
quality: CONFIG.compression.png.quality,
|
||||
effort: CONFIG.compression.png.effort,
|
||||
});
|
||||
}
|
||||
|
||||
// Write optimized file
|
||||
await pipeline.toFile(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize PDF file using Ghostscript
|
||||
*/
|
||||
async function optimizePdfFile(filePath) {
|
||||
try {
|
||||
// Create a temporary file
|
||||
const tempPath = `${filePath}.optimized`;
|
||||
|
||||
// Use Ghostscript to optimize the PDF
|
||||
const { stderr } = await execPromise(
|
||||
`gswin64c -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 ` +
|
||||
`-dPDFSETTINGS=/${CONFIG.compression.pdf.quality} ` +
|
||||
`-dNOPAUSE -dQUIET -dBATCH ` +
|
||||
`-sOutputFile="${tempPath}" "${filePath}"`
|
||||
);
|
||||
|
||||
if (stderr && !stderr.includes('Error')) {
|
||||
// Replace original with optimized version
|
||||
await fs.rename(tempPath, filePath);
|
||||
return true;
|
||||
} else {
|
||||
// Clean up temp file if there was an error
|
||||
await fs.unlink(tempPath).catch(() => {});
|
||||
throw new Error(stderr || 'Unknown error optimizing PDF');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.warn(chalk.yellow(' ⚠️ Ghostscript not found. Install it from https://ghostscript.com/ for PDF optimization.'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single file
|
||||
*/
|
||||
async function processFile(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase().slice(1);
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// Skip unsupported file types
|
||||
if (!CONFIG.fileExtensions.includes(ext)) {
|
||||
return { status: 'skipped', message: 'Unsupported file type' };
|
||||
}
|
||||
|
||||
// Get original size
|
||||
const originalSize = await getFileSize(filePath);
|
||||
const originalSizeFormatted = formatSize(originalSize);
|
||||
|
||||
// Skip empty files
|
||||
if (originalSize === 0) {
|
||||
return { status: 'skipped', message: 'Empty file' };
|
||||
}
|
||||
|
||||
try {
|
||||
let optimized = false;
|
||||
|
||||
// Optimize based on file type
|
||||
switch (ext) {
|
||||
case 'svg':
|
||||
optimized = await optimizeSvgFile(filePath);
|
||||
break;
|
||||
case 'png':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
optimized = await optimizeImageFile(filePath);
|
||||
break;
|
||||
case 'pdf':
|
||||
optimized = await optimizePdfFile(filePath);
|
||||
break;
|
||||
}
|
||||
|
||||
// Get new size
|
||||
const newSize = await getFileSize(filePath);
|
||||
const newSizeFormatted = formatSize(newSize);
|
||||
const saved = originalSize - newSize;
|
||||
const savedPercent = ((saved / originalSize) * 100).toFixed(2);
|
||||
|
||||
// Update statistics
|
||||
stats.processedFiles++;
|
||||
stats.totalSaved += saved;
|
||||
|
||||
if (!stats.byType[ext]) {
|
||||
stats.byType[ext] = { count: 0, saved: 0 };
|
||||
}
|
||||
stats.byType[ext].count++;
|
||||
stats.byType[ext].saved += saved;
|
||||
|
||||
return {
|
||||
status: 'optimized',
|
||||
originalSize,
|
||||
newSize,
|
||||
saved,
|
||||
savedPercent,
|
||||
message: `${originalSizeFormatted} → ${newSizeFormatted} (saved ${savedPercent}%)`,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(` ✗ Error optimizing ${fileName}: ${error.message}`));
|
||||
stats.skippedFiles++;
|
||||
return { status: 'error', message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a directory recursively
|
||||
*/
|
||||
async function processDirectory(directory) {
|
||||
try {
|
||||
const entries = await fs.readdir(directory, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await processDirectory(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase().slice(1);
|
||||
|
||||
if (CONFIG.fileExtensions.includes(ext)) {
|
||||
stats.totalFiles++;
|
||||
|
||||
console.log(chalk.blue(`\n🔍 Processing: ${fullPath}`));
|
||||
|
||||
const result = await processFile(fullPath);
|
||||
|
||||
switch (result.status) {
|
||||
case 'optimized':
|
||||
console.log(chalk.green(` ✓ Optimized: ${result.message}`));
|
||||
break;
|
||||
case 'skipped':
|
||||
console.log(chalk.yellow(` ⏭️ Skipped: ${result.message}`));
|
||||
break;
|
||||
case 'error':
|
||||
console.error(chalk.red(` ✗ Error: ${result.message}`));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error reading directory ${directory}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
console.log(chalk.cyan('🚀 Starting asset optimization...\n'));
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Process each directory
|
||||
for (const dir of CONFIG.scanDirs) {
|
||||
const fullPath = path.resolve(__dirname, '..', dir);
|
||||
console.log(chalk.cyan(`📂 Processing directory: ${fullPath}`));
|
||||
await processDirectory(fullPath);
|
||||
}
|
||||
|
||||
// Calculate total time
|
||||
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
|
||||
// Print summary
|
||||
console.log('\n' + chalk.cyan('📊 Optimization Summary:'));
|
||||
console.log(chalk.cyan('='.repeat(50)));
|
||||
console.log(chalk.cyan(`Total files processed: ${stats.totalFiles}`));
|
||||
console.log(chalk.green(`✓ Successfully optimized: ${stats.processedFiles}`));
|
||||
|
||||
if (stats.skippedFiles > 0) {
|
||||
console.log(chalk.yellow(`⏭️ Skipped: ${stats.skippedFiles}`));
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`💾 Total space saved: ${formatSize(stats.totalSaved)}`));
|
||||
|
||||
// Print stats by file type
|
||||
console.log('\n' + chalk.cyan('📂 By file type:'));
|
||||
for (const [type, data] of Object.entries(stats.byType)) {
|
||||
console.log(` ${type.toUpperCase()}: ${data.count} files, saved ${formatSize(data.saved)}`);
|
||||
}
|
||||
|
||||
console.log('\n' + chalk.green(`✨ Optimization completed in ${totalTime} seconds!`));
|
||||
}
|
||||
|
||||
// Run the script
|
||||
main().catch(error => {
|
||||
console.error(chalk.red('❌ Fatal error:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user