/** * Photo Compression Utility * Client-side image compression before upload per specs/photo-management.md */ import imageCompression from 'browser-image-compression'; export interface CompressedPhoto { file: File; preview: string; originalSize: number; compressedSize: number; savingsPercent: number; } export interface CompressionOptions { maxSizeMB?: number; maxWidthOrHeight?: number; quality?: number; useWebWorker?: boolean; } const DEFAULT_OPTIONS: CompressionOptions = { maxSizeMB: 1, // Max 1MB after compression maxWidthOrHeight: 1920, useWebWorker: true, }; /** * Compress a single photo before upload */ export async function compressPhoto( file: File, options: CompressionOptions = {} ): Promise { const opts = { ...DEFAULT_OPTIONS, ...options }; const originalSize = file.size; try { const compressedFile = await imageCompression(file, { maxSizeMB: opts.maxSizeMB!, maxWidthOrHeight: opts.maxWidthOrHeight!, useWebWorker: opts.useWebWorker!, fileType: 'image/webp', }); const preview = await createPreview(compressedFile); const compressedSize = compressedFile.size; const savingsPercent = Math.round((1 - compressedSize / originalSize) * 100); return { file: compressedFile, preview, originalSize, compressedSize, savingsPercent, }; } catch (error) { console.error('Photo compression failed:', error); // Return original if compression fails const preview = await createPreview(file); return { file, preview, originalSize, compressedSize: originalSize, savingsPercent: 0, }; } } /** * Compress multiple photos */ export async function compressPhotos( files: File[], options: CompressionOptions = {}, onProgress?: (completed: number, total: number) => void ): Promise { const results: CompressedPhoto[] = []; for (let i = 0; i < files.length; i++) { const result = await compressPhoto(files[i], options); results.push(result); onProgress?.(i + 1, files.length); } return results; } /** * Create a preview URL for an image file */ async function createPreview(file: File): Promise { return new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.readAsDataURL(file); }); } /** * Format file size for display */ export function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * Check if file is a valid image type */ export function isValidImageType(file: File): boolean { const validTypes = [ 'image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif', ]; return validTypes.includes(file.type); } /** * Get image dimensions */ export async function getImageDimensions(file: File): Promise<{ width: number; height: number }> { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve({ width: img.width, height: img.height }); URL.revokeObjectURL(img.src); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); }