/** * Client-side image resizing utility for LibreChat * Resizes images to prevent backend upload errors while maintaining quality */ export interface ResizeOptions { maxWidth?: number; maxHeight?: number; quality?: number; format?: 'jpeg' | 'png' | 'webp'; } export interface ResizeResult { file: File; originalSize: number; newSize: number; originalDimensions: { width: number; height: number }; newDimensions: { width: number; height: number }; compressionRatio: number; } /** * Default resize options based on backend 'high' resolution settings * Backend 'high' uses maxShortSide=768, maxLongSide=2000 * We use slightly smaller values to ensure no backend resizing is triggered */ const DEFAULT_RESIZE_OPTIONS: ResizeOptions = { maxWidth: 1900, // Slightly less than backend maxLongSide=2000 maxHeight: 1900, // Slightly less than backend maxLongSide=2000 quality: 0.92, // High quality while reducing file size format: 'jpeg', // Most compatible format }; /** * Checks if the browser supports canvas-based image resizing */ export function supportsClientResize(): boolean { try { // Check for required APIs if (typeof HTMLCanvasElement === 'undefined') return false; if (typeof FileReader === 'undefined') return false; if (typeof Image === 'undefined') return false; // Test canvas creation const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); return !!(ctx && ctx.drawImage && canvas.toBlob); } catch { return false; } } /** * Calculates new dimensions while maintaining aspect ratio */ function calculateDimensions( originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number, ): { width: number; height: number } { const { width, height } = { width: originalWidth, height: originalHeight }; // If image is smaller than max dimensions, don't upscale if (width <= maxWidth && height <= maxHeight) { return { width, height }; } // Calculate scaling factor const widthRatio = maxWidth / width; const heightRatio = maxHeight / height; const scalingFactor = Math.min(widthRatio, heightRatio); return { width: Math.round(width * scalingFactor), height: Math.round(height * scalingFactor), }; } /** * Resizes an image file using canvas */ export function resizeImage( file: File, options: Partial = {}, ): Promise { return new Promise((resolve, reject) => { // Check browser support if (!supportsClientResize()) { reject(new Error('Browser does not support client-side image resizing')); return; } // Only process image files if (!file.type.startsWith('image/')) { reject(new Error('File is not an image')); return; } const opts = { ...DEFAULT_RESIZE_OPTIONS, ...options }; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { try { const originalDimensions = { width: img.width, height: img.height }; const newDimensions = calculateDimensions( img.width, img.height, opts.maxWidth!, opts.maxHeight!, ); // If no resizing needed, return original file if ( newDimensions.width === originalDimensions.width && newDimensions.height === originalDimensions.height ) { resolve({ file, originalSize: file.size, newSize: file.size, originalDimensions, newDimensions, compressionRatio: 1, }); return; } // Create canvas and resize const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d')!; canvas.width = newDimensions.width; canvas.height = newDimensions.height; // Use high-quality image smoothing ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; // Draw resized image ctx.drawImage(img, 0, 0, newDimensions.width, newDimensions.height); // Convert to blob canvas.toBlob( (blob) => { if (!blob) { reject(new Error('Failed to create blob from canvas')); return; } // Create new file with same name but potentially different extension const extension = opts.format === 'jpeg' ? '.jpg' : `.${opts.format}`; const baseName = file.name.replace(/\.[^/.]+$/, ''); const newFileName = `${baseName}${extension}`; const resizedFile = new File([blob], newFileName, { type: `image/${opts.format}`, lastModified: Date.now(), }); resolve({ file: resizedFile, originalSize: file.size, newSize: resizedFile.size, originalDimensions, newDimensions, compressionRatio: resizedFile.size / file.size, }); }, `image/${opts.format}`, opts.quality, ); } catch (error) { reject(error); } }; img.onerror = () => reject(new Error('Failed to load image')); img.src = event.target?.result as string; }; reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsDataURL(file); }); } /** * Determines if an image should be resized based on size and dimensions */ export function shouldResizeImage( file: File, fileSizeLimit: number = 512 * 1024 * 1024, // 512MB default ): boolean { // Don't resize if file is already small if (file.size < fileSizeLimit * 0.1) { // Less than 10% of limit return false; } // Don't process non-images if (!file.type.startsWith('image/')) { return false; } // Don't process GIFs (they might be animated) if (file.type === 'image/gif') { return false; } return true; }