UNPKG

vite-plugin-animated-webp-optimizer

Version:

Vite plugin for optimizing animated WebP files with Sharp and webpmux

258 lines 9.45 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isValidWebP = isValidWebP; exports.formatBytes = formatBytes; exports.detectAnimatedWebP = detectAnimatedWebP; exports.optimizeAnimatedWebP = optimizeAnimatedWebP; exports.optimizeStaticWebP = optimizeStaticWebP; exports.copyFileToDist = copyFileToDist; exports.processDirectory = processDirectory; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const sharp_1 = __importDefault(require("sharp")); /** * WebP 파일의 유효성을 검사합니다. */ function isValidWebP(buffer) { if (!buffer || !Buffer.isBuffer(buffer)) { return false; } return (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP"); } /** * 바이트를 사람이 읽기 쉬운 형태로 변환합니다. */ function formatBytes(bytes) { if (bytes === 0) return "0 Bytes"; if (bytes < 0) return `-${formatBytes(Math.abs(bytes))}`; if (!isFinite(bytes)) return `${bytes} Bytes`; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); if (i >= sizes.length) { return `${bytes} Bytes`; } return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } /** * 애니메이션 WebP인지 감지합니다. */ async function detectAnimatedWebP(filePath) { try { const sharpImage = (0, sharp_1.default)(filePath, { animated: true, pages: -1 }); const metadata = await sharpImage.metadata(); return typeof metadata?.pages === "number" && metadata.pages > 1; } catch (error) { return false; } } /** * 애니메이션 WebP를 최적화합니다. */ async function optimizeAnimatedWebP(inputPath, outputPath, options) { const { quality, effort, lossless = false, maxWidth, maxHeight, verbose = false, } = options; try { if (verbose) { console.log(`🎬 Optimizing animated WebP: ${path.basename(inputPath)}`); } const sharpImage = (0, sharp_1.default)(inputPath, { animated: true, pages: -1 }); const imageMeta = await sharpImage.metadata(); const { width, height: heightAllPages, size, loop, pages, pageHeight, delay, } = imageMeta; const height = pageHeight || (heightAllPages && pages ? heightAllPages / pages : heightAllPages); let processedImage = sharpImage; if (maxWidth > 0 || maxHeight > 0) { const targetWidth = maxWidth > 0 ? maxWidth : width || 0; const targetHeight = maxHeight > 0 ? maxHeight : height || 0; const adjustedHeight = pages && pages > 1 ? targetHeight * pages : targetHeight; processedImage = sharpImage.resize({ width: targetWidth, height: adjustedHeight, fit: sharp_1.default.fit.inside, }); } const webpOptions = { quality, effort, smartSubsample: true, lossless, loop: typeof loop === "number" && loop >= 0 ? loop : 0, delay: delay || undefined, force: true, }; await processedImage.webp(webpOptions).toFile(outputPath); if (verbose) { const originalSize = fs.statSync(inputPath).size; const optimizedSize = fs.statSync(outputPath).size; const savings = originalSize - optimizedSize; const savingsPercent = ((savings / originalSize) * 100).toFixed(1); console.log(`✅ Optimized: ${formatBytes(originalSize)}${formatBytes(optimizedSize)} (${savingsPercent}% saved)`); } } catch (error) { if (verbose) { console.error(`❌ Optimization failed:`, error); } throw error; } } /** * 정적 WebP를 최적화합니다. */ async function optimizeStaticWebP(inputPath, outputPath, options) { const { quality, effort, lossless = false, verbose = false } = options; try { if (verbose) { console.log(`🖼️ Optimizing static WebP: ${path.basename(inputPath)}`); } await (0, sharp_1.default)(inputPath) .webp({ quality, effort, smartSubsample: true, lossless, }) .toFile(outputPath); if (verbose) { const originalSize = fs.statSync(inputPath).size; const optimizedSize = fs.statSync(outputPath).size; const savings = originalSize - optimizedSize; const savingsPercent = ((savings / originalSize) * 100).toFixed(1); console.log(`✅ Optimized: ${formatBytes(originalSize)}${formatBytes(optimizedSize)} (${savingsPercent}% saved)`); } } catch (error) { if (verbose) { console.error(`❌ Optimization failed:`, error); } throw error; } } /** * 파일을 dist 디렉토리로 복사합니다. */ function copyFileToDist(inputPath, distDir) { try { const outputPath = path.join(distDir, path.basename(inputPath)); fs.copyFileSync(inputPath, outputPath); } catch (error) { throw new Error(`Failed to copy file: ${error}`); } } /** * 디렉토리를 재귀적으로 처리합니다. */ async function processDirectory(dirPath, distDir, options) { const files = fs.readdirSync(dirPath); for (const file of files) { const filePath = path.join(dirPath, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { await processDirectory(filePath, distDir, options); } else if (file.toLowerCase().endsWith(".webp")) { await processWebpFile(filePath, distDir, options); } } } /** * WebP 파일을 처리합니다. */ async function processWebpFile(filePath, distDir, options) { const { verbose = false, maxFileSize, skipIfSmaller, quality, effort, animationQuality, animationCompression, optimizeAnimation, maxWidth, maxHeight, } = options; try { const fileBuffer = fs.readFileSync(filePath); const fileSize = fileBuffer.length; if (verbose) { console.log(`🔍 Processing: ${path.basename(filePath)} (${formatBytes(fileSize)})`); } if (!isValidWebP(fileBuffer)) { if (verbose) { console.log(`❌ Invalid WebP file, skipping`); } return; } if (skipIfSmaller > 0 && fileSize < skipIfSmaller) { if (verbose) { console.log(`⏭️ File too small, skipping`); } return; } const isAnimated = await detectAnimatedWebP(filePath); const outputPath = path.join(distDir, path.basename(filePath)); if (isAnimated && optimizeAnimation) { await optimizeAnimatedWebP(filePath, outputPath, { quality: animationQuality, effort: animationCompression, maxWidth, maxHeight, verbose, }); } else { await optimizeStaticWebP(filePath, outputPath, { quality, effort, verbose, }); } if (maxFileSize > 0) { const optimizedSize = fs.statSync(outputPath).size; if (optimizedSize > maxFileSize) { if (verbose) { console.warn(`⚠️ File still too large: ${formatBytes(optimizedSize)} > ${formatBytes(maxFileSize)}`); } } } } catch (error) { if (verbose) { console.error(`❌ Error processing file:`, error); } copyFileToDist(filePath, distDir); } } //# sourceMappingURL=utils.js.map