/** * Image processing utilities for resizing and encoding images */ export interface ProcessedImage { dataUrl: string; blob: Blob; width: number; height: number; originalSize: number; compressedSize: number; } /** * Resize an image to a specific size and convert to base64 * @param file - The image file to process * @param maxWidth - Maximum width (default: 128) * @param maxHeight - Maximum height (default: 128) * @param quality - JPEG quality (0-1, default: 0.8) * @returns Promise with processed image data */ export const processImage = async ( file: File, maxWidth: number = 128, maxHeight: number = 128, quality: number = 0.8 ): Promise => { return new Promise((resolve, reject) => { // Validate file type if (!file.type.startsWith('image/')) { reject(new Error('File must be an image')); return; } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { // Calculate dimensions to maintain aspect ratio let { width, height } = calculateDimensions( img.width, img.height, maxWidth, maxHeight ); canvas.width = width; canvas.height = height; // Draw and resize image ctx!.imageSmoothingEnabled = true; ctx!.imageSmoothingQuality = 'high'; ctx!.drawImage(img, 0, 0, width, height); // Convert to blob and data URL canvas.toBlob( (blob) => { if (!blob) { reject(new Error('Failed to process image')); return; } const dataUrl = canvas.toDataURL('image/jpeg', quality); resolve({ dataUrl, blob, width, height, originalSize: file.size, compressedSize: blob.size, }); }, 'image/jpeg', quality ); }; img.onerror = () => { reject(new Error('Failed to load image')); }; // Load the image img.src = URL.createObjectURL(file); }); }; /** * Calculate dimensions to fit within max bounds while maintaining aspect ratio */ const calculateDimensions = ( originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number ): { width: number; height: number } => { const aspectRatio = originalWidth / originalHeight; let width = maxWidth; let height = maxHeight; if (originalWidth > originalHeight) { // Landscape height = width / aspectRatio; if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; } } else { // Portrait or square width = height * aspectRatio; if (width > maxWidth) { width = maxWidth; height = width / aspectRatio; } } return { width: Math.round(width), height: Math.round(height), }; }; /** * Validate image file */ export const validateImageFile = (file: File): { valid: boolean; error?: string } => { // Check file type if (!file.type.startsWith('image/')) { return { valid: false, error: 'File must be an image' }; } // Check file size (max 10MB) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { return { valid: false, error: 'Image must be smaller than 10MB' }; } // Check supported formats const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!supportedTypes.includes(file.type)) { return { valid: false, error: 'Supported formats: JPEG, PNG, GIF, WebP' }; } return { valid: true }; }; /** * Generate a unique filename */ export const generateUniqueFileName = (originalName: string, prefix: string = 'img'): string => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); const extension = originalName.split('.').pop() || 'jpg'; return `${prefix}_${timestamp}_${random}.${extension}`; };