vite-plugin-animated-webp-optimizer
Version:
Vite plugin for optimizing animated WebP files with Sharp and webpmux
258 lines • 9.45 kB
JavaScript
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
;