import sharp from "sharp"; import type { FileProcessingResult, ImageProcessingOptions } from "../data"; /** * Process images with compression, resizing, format conversion, and thumbnail generation */ export async function processImage( buffer: Buffer | Uint8Array, options: ImageProcessingOptions = {}, ): Promise { try { const inputBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); let processedBuffer = inputBuffer; let thumbnailBuffer: Buffer | undefined; const metadata: Record = {}; // Initialize Sharp instance const image = sharp(inputBuffer); const originalMetadata = await image.metadata(); // Store original metadata metadata.original = { width: originalMetadata.width, height: originalMetadata.height, format: originalMetadata.format, size: inputBuffer.length, colorSpace: originalMetadata.space, channels: originalMetadata.channels, density: originalMetadata.density, hasAlpha: originalMetadata.hasAlpha, }; // Apply transformations let transformedImage = image; // Resize if requested if (options.resize) { const { width, height, fit = "cover" } = options.resize; transformedImage = transformedImage.resize(width, height, { fit: fit as keyof sharp.FitEnum, withoutEnlargement: true, // Don't enlarge smaller images }); metadata.processed = { ...metadata.processed, resized: true, targetWidth: width, targetHeight: height, fit, }; } // Apply format conversion and quality settings const outputFormat = options.format || "webp"; const quality = options.quality || 85; switch (outputFormat) { case "jpeg": transformedImage = transformedImage.jpeg({ quality, progressive: true, mozjpeg: true, // Use mozjpeg encoder for better compression }); break; case "png": transformedImage = transformedImage.png({ quality, compressionLevel: 9, progressive: true, }); break; case "webp": transformedImage = transformedImage.webp({ quality, effort: 6, // Max compression effort }); break; case "avif": transformedImage = transformedImage.avif({ quality, effort: 6, }); break; default: // Keep original format but apply quality if possible if (originalMetadata.format === "jpeg") { transformedImage = transformedImage.jpeg({ quality }); } else if (originalMetadata.format === "png") { transformedImage = transformedImage.png({ quality }); } } // Generate processed image processedBuffer = await transformedImage.toBuffer(); // Get final metadata const finalMetadata = await sharp(processedBuffer).metadata(); metadata.processed = { ...metadata.processed, width: finalMetadata.width, height: finalMetadata.height, format: outputFormat, size: processedBuffer.length, quality, compressionRatio: inputBuffer.length / processedBuffer.length, }; // Generate thumbnail if requested if (options.generateThumbnail) { const thumbSize = options.thumbnailSize || { width: 300, height: 300, }; thumbnailBuffer = await sharp(inputBuffer) .resize(thumbSize.width, thumbSize.height, { fit: "cover", position: "center", }) .webp({ quality: 80 }) .toBuffer(); const thumbMetadata = await sharp(thumbnailBuffer).metadata(); metadata.thumbnail = { width: thumbMetadata.width, height: thumbMetadata.height, format: "webp", size: thumbnailBuffer.length, }; } // Add processing stats metadata.processing = { processedAt: new Date().toISOString(), sizeSaving: inputBuffer.length - processedBuffer.length, sizeSavingPercentage: ((inputBuffer.length - processedBuffer.length) / inputBuffer.length) * 100, processingTime: Date.now(), // You'd measure this properly in production }; return { processed: true, originalFile: inputBuffer, processedFile: processedBuffer, thumbnail: thumbnailBuffer, metadata, }; } catch (error) { return { processed: false, error: `Image processing failed: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Extract image metadata without processing */ export async function extractImageMetadata( buffer: Buffer | Uint8Array, ): Promise> { try { const inputBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); const image = sharp(inputBuffer); const metadata = await image.metadata(); return { width: metadata.width, height: metadata.height, format: metadata.format, size: inputBuffer.length, colorSpace: metadata.space, channels: metadata.channels, density: metadata.density, hasAlpha: metadata.hasAlpha, isAnimated: metadata.pages && metadata.pages > 1, orientation: metadata.orientation, }; } catch (error) { throw new Error( `Failed to extract image metadata: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Generate multiple sizes for responsive images */ export async function generateResponsiveSizes( buffer: Buffer | Uint8Array, sizes: Array<{ name: string; width: number; height?: number }> = [ { name: "small", width: 400 }, { name: "medium", width: 800 }, { name: "large", width: 1200 }, { name: "xlarge", width: 1920 }, ], ): Promise> { const results: Record = {}; const inputBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); try { for (const size of sizes) { const resized = await sharp(inputBuffer) .resize(size.width, size.height, { fit: "inside", withoutEnlargement: true, }) .webp({ quality: 85 }) .toBuffer(); results[size.name] = resized; } return results; } catch (error) { throw new Error( `Failed to generate responsive sizes: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Create an optimized avatar image */ export async function processAvatar( buffer: Buffer | Uint8Array, size: number = 200, ): Promise { try { const inputBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); return await sharp(inputBuffer) .resize(size, size, { fit: "cover", position: "center" }) .webp({ quality: 90 }) .toBuffer(); } catch (error) { throw new Error( `Avatar processing failed: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Remove EXIF data from images for privacy */ export async function stripExifData( buffer: Buffer | Uint8Array, ): Promise { try { const inputBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); return await sharp(inputBuffer) .rotate() // Auto-rotate based on EXIF, then removes EXIF .toBuffer(); } catch (error) { throw new Error( `EXIF stripping failed: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Validate if buffer contains a valid image */ export async function validateImage( buffer: Buffer | Uint8Array, ): Promise { try { const inputBuffer = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); const metadata = await sharp(inputBuffer).metadata(); return !!(metadata.width && metadata.height && metadata.format); } catch { return false; } }