| |
| |
| |
| |
|
|
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| const DEFAULT_RESIZE_OPTIONS: ResizeOptions = { |
| maxWidth: 1900, |
| maxHeight: 1900, |
| quality: 0.92, |
| format: 'jpeg', |
| }; |
|
|
| |
| |
| |
| export function supportsClientResize(): boolean { |
| try { |
| |
| if (typeof HTMLCanvasElement === 'undefined') return false; |
| if (typeof FileReader === 'undefined') return false; |
| if (typeof Image === 'undefined') return false; |
|
|
| |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d'); |
|
|
| return !!(ctx && ctx.drawImage && canvas.toBlob); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| function calculateDimensions( |
| originalWidth: number, |
| originalHeight: number, |
| maxWidth: number, |
| maxHeight: number, |
| ): { width: number; height: number } { |
| const { width, height } = { width: originalWidth, height: originalHeight }; |
|
|
| |
| if (width <= maxWidth && height <= maxHeight) { |
| return { width, height }; |
| } |
|
|
| |
| 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), |
| }; |
| } |
|
|
| |
| |
| |
| export function resizeImage( |
| file: File, |
| options: Partial<ResizeOptions> = {}, |
| ): Promise<ResizeResult> { |
| return new Promise((resolve, reject) => { |
| |
| if (!supportsClientResize()) { |
| reject(new Error('Browser does not support client-side image resizing')); |
| return; |
| } |
|
|
| |
| 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 ( |
| newDimensions.width === originalDimensions.width && |
| newDimensions.height === originalDimensions.height |
| ) { |
| resolve({ |
| file, |
| originalSize: file.size, |
| newSize: file.size, |
| originalDimensions, |
| newDimensions, |
| compressionRatio: 1, |
| }); |
| return; |
| } |
|
|
| |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d')!; |
|
|
| canvas.width = newDimensions.width; |
| canvas.height = newDimensions.height; |
|
|
| |
| ctx.imageSmoothingEnabled = true; |
| ctx.imageSmoothingQuality = 'high'; |
|
|
| |
| ctx.drawImage(img, 0, 0, newDimensions.width, newDimensions.height); |
|
|
| |
| canvas.toBlob( |
| (blob) => { |
| if (!blob) { |
| reject(new Error('Failed to create blob from canvas')); |
| return; |
| } |
|
|
| |
| 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); |
| }); |
| } |
|
|
| |
| |
| |
| export function shouldResizeImage( |
| file: File, |
| fileSizeLimit: number = 512 * 1024 * 1024, |
| ): boolean { |
| |
| if (file.size < fileSizeLimit * 0.1) { |
| |
| return false; |
| } |
|
|
| |
| if (!file.type.startsWith('image/')) { |
| return false; |
| } |
|
|
| |
| if (file.type === 'image/gif') { |
| return false; |
| } |
|
|
| return true; |
| } |
|
|