UNPKG

@mvp-factory/holy-upload

Version:

File upload processing system extracted from Holy Habit project with security validation and image optimization

411 lines 15.5 kB
"use strict"; /** * Image Optimizer * * Optimizes images using Sharp library * Extracted from Holy Habit image processing logic */ 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.ImageOptimizer = void 0; const sharp_1 = __importDefault(require("sharp")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); class ImageOptimizer { /** * Optimize image file * * @param inputPath - Input file path * @param outputPath - Output file path (optional, defaults to input path) * @param options - Optimization options * @returns Optimization result */ static async optimize(inputPath, outputPath, options = {}) { const opts = { ...this.DEFAULT_OPTIONS, ...options }; const output = outputPath || inputPath; try { const originalStats = await fs.promises.stat(inputPath); const originalSize = originalStats.size; const image = (0, sharp_1.default)(inputPath); const metadata = await image.metadata(); // Check if resizing is needed const needsResize = this.needsResize(metadata, opts); let pipeline = image; if (needsResize) { const { width, height } = this.calculateDimensions(metadata, opts); pipeline = pipeline.resize(width, height, { fit: 'inside', withoutEnlargement: true }); } // Apply format-specific optimizations const format = metadata.format; switch (format) { case 'jpeg': pipeline = pipeline.jpeg({ quality: opts.jpegQuality, progressive: true, mozjpeg: true }); break; case 'png': pipeline = pipeline.png({ compressionLevel: opts.pngCompression, progressive: false, palette: true }); break; case 'webp': pipeline = pipeline.webp({ quality: opts.webpQuality, effort: 6 }); break; case 'gif': // Keep GIF as-is, just resize if needed break; default: // Convert unknown formats to JPEG pipeline = pipeline.jpeg({ quality: opts.jpegQuality, progressive: true }); } // Save optimized image await pipeline.toFile(output); const optimizedStats = await fs.promises.stat(output); const optimizedSize = optimizedStats.size; const savings = ((originalSize - optimizedSize) / originalSize) * 100; return { success: true, originalSize, optimizedSize, savings: Math.max(0, savings) }; } catch (error) { throw new Error(`Image optimization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Optimize image buffer * * @param buffer - Input image buffer * @param options - Optimization options * @returns Optimized buffer and metadata */ static async optimizeBuffer(buffer, options = {}) { const opts = { ...this.DEFAULT_OPTIONS, ...options }; const originalSize = buffer.length; try { const image = (0, sharp_1.default)(buffer); const metadata = await image.metadata(); // Check if resizing is needed const needsResize = this.needsResize(metadata, opts); let pipeline = image; if (needsResize) { const { width, height } = this.calculateDimensions(metadata, opts); pipeline = pipeline.resize(width, height, { fit: 'inside', withoutEnlargement: true }); } // Apply format-specific optimizations const format = metadata.format; switch (format) { case 'jpeg': pipeline = pipeline.jpeg({ quality: opts.jpegQuality, progressive: true, mozjpeg: true }); break; case 'png': pipeline = pipeline.png({ compressionLevel: opts.pngCompression, progressive: false, palette: true }); break; case 'webp': pipeline = pipeline.webp({ quality: opts.webpQuality, effort: 6 }); break; case 'gif': // Keep GIF as-is, just resize if needed break; default: // Convert unknown formats to JPEG pipeline = pipeline.jpeg({ quality: opts.jpegQuality, progressive: true }); } const { data, info } = await pipeline.toBuffer({ resolveWithObject: true }); const optimizedSize = data.length; const savings = ((originalSize - optimizedSize) / originalSize) * 100; return { buffer: data, metadata: info, savings: Math.max(0, savings) }; } catch (error) { throw new Error(`Image optimization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Convert image to WebP format * * @param inputPath - Input file path * @param outputPath - Output file path * @param quality - WebP quality (1-100) * @returns Conversion result */ static async convertToWebP(inputPath, outputPath, quality = 85) { try { const originalStats = await fs.promises.stat(inputPath); const originalSize = originalStats.size; await (0, sharp_1.default)(inputPath) .webp({ quality, effort: 6 }) .toFile(outputPath); const webpStats = await fs.promises.stat(outputPath); const webpSize = webpStats.size; const savings = ((originalSize - webpSize) / originalSize) * 100; return { success: true, originalSize, webpSize, savings: Math.max(0, savings) }; } catch (error) { throw new Error(`WebP conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Generate image thumbnails * * @param inputPath - Input file path * @param sizes - Array of thumbnail sizes * @param outputDir - Output directory * @returns Generated thumbnails */ static async generateThumbnails(inputPath, sizes, outputDir) { const baseName = path.basename(inputPath, path.extname(inputPath)); const extension = path.extname(inputPath); const thumbnails = []; try { // Ensure output directory exists await fs.promises.mkdir(outputDir, { recursive: true }); const image = (0, sharp_1.default)(inputPath); for (const size of sizes) { const outputPath = path.join(outputDir, `${baseName}_${size.suffix}${extension}`); await image .clone() .resize(size.width, size.height, { fit: 'cover', position: 'center' }) .toFile(outputPath); const stats = await fs.promises.stat(outputPath); thumbnails.push({ path: outputPath, width: size.width, height: size.height, size: stats.size }); } return thumbnails; } catch (error) { throw new Error(`Thumbnail generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get image metadata * * @param inputPath - Input file path * @returns Image metadata */ static async getMetadata(inputPath) { try { return await (0, sharp_1.default)(inputPath).metadata(); } catch (error) { throw new Error(`Failed to read image metadata: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Check if image needs resizing * * @param metadata - Image metadata * @param options - Optimization options * @returns True if resizing is needed */ static needsResize(metadata, options) { if (!metadata.width || !metadata.height) { return false; } const maxWidth = options.maxWidth || this.DEFAULT_OPTIONS.maxWidth; const maxHeight = options.maxHeight || this.DEFAULT_OPTIONS.maxHeight; return metadata.width > maxWidth || metadata.height > maxHeight; } /** * Calculate optimal dimensions * * @param metadata - Image metadata * @param options - Optimization options * @returns Calculated dimensions */ static calculateDimensions(metadata, options) { if (!metadata.width || !metadata.height) { throw new Error('Cannot calculate dimensions: image width or height is unknown'); } const maxWidth = options.maxWidth || this.DEFAULT_OPTIONS.maxWidth; const maxHeight = options.maxHeight || this.DEFAULT_OPTIONS.maxHeight; if (!options.maintainAspectRatio) { return { width: maxWidth, height: maxHeight }; } const aspectRatio = metadata.width / metadata.height; let width = maxWidth; let height = Math.round(width / aspectRatio); if (height > maxHeight) { height = maxHeight; width = Math.round(height * aspectRatio); } return { width, height }; } /** * Create progressive JPEG * * @param inputPath - Input file path * @param outputPath - Output file path * @param quality - JPEG quality * @returns Processing result */ static async createProgressiveJPEG(inputPath, outputPath, quality = 85) { try { await (0, sharp_1.default)(inputPath) .jpeg({ quality, progressive: true, mozjpeg: true }) .toFile(outputPath); const stats = await fs.promises.stat(outputPath); return { success: true, size: stats.size }; } catch (error) { throw new Error(`Progressive JPEG creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Remove EXIF data from image * * @param inputPath - Input file path * @param outputPath - Output file path * @returns Processing result */ static async removeExifData(inputPath, outputPath) { try { await (0, sharp_1.default)(inputPath) .rotate() // Auto-rotate based on EXIF orientation .withMetadata({}) // Remove all metadata .toFile(outputPath); return { success: true }; } catch (error) { throw new Error(`EXIF removal failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Batch optimize images in directory * * @param inputDir - Input directory * @param outputDir - Output directory * @param options - Optimization options * @returns Batch optimization results */ static async batchOptimize(inputDir, outputDir, options = {}) { const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; let processed = 0; let failed = 0; let totalSavings = 0; try { const files = await fs.promises.readdir(inputDir); // Ensure output directory exists await fs.promises.mkdir(outputDir, { recursive: true }); for (const file of files) { const ext = path.extname(file).toLowerCase(); if (imageExtensions.includes(ext)) { const inputPath = path.join(inputDir, file); const outputPath = path.join(outputDir, file); try { const result = await this.optimize(inputPath, outputPath, options); totalSavings += result.savings; processed++; } catch (error) { console.error(`Failed to optimize ${file}:`, error); failed++; } } } return { processed, failed, totalSavings: totalSavings / processed || 0 }; } catch (error) { throw new Error(`Batch optimization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } exports.ImageOptimizer = ImageOptimizer; ImageOptimizer.DEFAULT_OPTIONS = { maxWidth: 1920, maxHeight: 1080, jpegQuality: 85, pngCompression: 8, webpQuality: 85, maintainAspectRatio: true }; //# sourceMappingURL=ImageOptimizer.js.map