Spaces:
Running
Running
Download src/utils/imageOptimizer.ts from Aura-Studio/AI: direct link, hf CLI and curl.
- Browser
- Download file 4.67 kB
-
https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/utils/imageOptimizer.ts
- Command line
-
hf download hf://spaces/Aura-Studio/AI/src/utils/imageOptimizer.ts
-
curl -L -o imageOptimizer.ts https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/utils/imageOptimizer.ts
4.67 kB
| /** | |
| * Lightweight, memory-safe client-side image optimizer for low-RAM devices (e.g., 4 GB RAM) | |
| * Prevents tab OOM (Out Of Memory) crashes, white blank screens, and UI freezes. | |
| */ | |
| export interface OptimizedImageResult { | |
| dataUrl: string; | |
| blob: Blob; | |
| fileName: string; | |
| mimeType: string; | |
| originalSize: number; | |
| compressedSize: number; | |
| width: number; | |
| height: number; | |
| } | |
| /** | |
| * Compresses and downscales high-resolution images before storing in state or sending to server. | |
| * A 15MB 4000x3000 photo is scaled down to max 1600px and converted to optimized JPEG/WebP (~200KB). | |
| */ | |
| export async function optimizeImageForUpload( | |
| file: File, | |
| options: { | |
| maxDimension?: number; | |
| quality?: number; | |
| maxFileSizeMb?: number; | |
| } = {} | |
| ): Promise<OptimizedImageResult> { | |
| const maxDimension = options.maxDimension || 1600; | |
| const quality = options.quality || 0.82; | |
| // If not an image or SVG/GIF (preserve animation/vector), return direct data URL safely | |
| if (!file.type.startsWith("image/") || file.type === "image/svg+xml" || file.type === "image/gif") { | |
| const dataUrl = await readFileAsDataUrl(file); | |
| return { | |
| dataUrl, | |
| blob: file, | |
| fileName: file.name, | |
| mimeType: file.type || "image/png", | |
| originalSize: file.size, | |
| compressedSize: file.size, | |
| width: 0, | |
| height: 0 | |
| }; | |
| } | |
| return new Promise((resolve, reject) => { | |
| const objectUrl = URL.createObjectURL(file); | |
| const img = new Image(); | |
| img.onload = () => { | |
| // Release initial object URL to free memory immediately | |
| URL.revokeObjectURL(objectUrl); | |
| let { width, height } = img; | |
| // Calculate constrained dimensions | |
| if (width > maxDimension || height > maxDimension) { | |
| if (width > height) { | |
| height = Math.round((height * maxDimension) / width); | |
| width = maxDimension; | |
| } else { | |
| width = Math.round((width * maxDimension) / height); | |
| height = maxDimension; | |
| } | |
| } | |
| // Render onto canvas with memory cleanup | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = width; | |
| canvas.height = height; | |
| const ctx = canvas.getContext("2d", { alpha: false, desynchronized: true }); | |
| if (!ctx) { | |
| // Fallback to direct read if canvas context fails | |
| readFileAsDataUrl(file).then(dataUrl => { | |
| resolve({ | |
| dataUrl, | |
| blob: file, | |
| fileName: file.name, | |
| mimeType: file.type, | |
| originalSize: file.size, | |
| compressedSize: file.size, | |
| width, | |
| height | |
| }); | |
| }).catch(reject); | |
| return; | |
| } | |
| // Draw with high smoothing | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.imageSmoothingQuality = "high"; | |
| ctx.drawImage(img, 0, 0, width, height); | |
| // Export as optimized JPEG | |
| const outputMime = file.type === "image/png" && file.size < 800 * 1024 ? "image/png" : "image/jpeg"; | |
| const dataUrl = canvas.toDataURL(outputMime, quality); | |
| canvas.toBlob( | |
| (blob) => { | |
| // Clean up canvas | |
| canvas.width = 0; | |
| canvas.height = 0; | |
| if (blob) { | |
| resolve({ | |
| dataUrl, | |
| blob, | |
| fileName: file.name.replace(/\.[^/.]+$/, outputMime === "image/jpeg" ? ".jpg" : ".png"), | |
| mimeType: outputMime, | |
| originalSize: file.size, | |
| compressedSize: blob.size, | |
| width, | |
| height | |
| }); | |
| } else { | |
| resolve({ | |
| dataUrl, | |
| blob: file, | |
| fileName: file.name, | |
| mimeType: outputMime, | |
| originalSize: file.size, | |
| compressedSize: Math.round(dataUrl.length * 0.75), | |
| width, | |
| height | |
| }); | |
| } | |
| }, | |
| outputMime, | |
| quality | |
| ); | |
| }; | |
| img.onerror = (err) => { | |
| URL.revokeObjectURL(objectUrl); | |
| // Fallback | |
| readFileAsDataUrl(file).then(dataUrl => { | |
| resolve({ | |
| dataUrl, | |
| blob: file, | |
| fileName: file.name, | |
| mimeType: file.type || "image/jpeg", | |
| originalSize: file.size, | |
| compressedSize: file.size, | |
| width: 0, | |
| height: 0 | |
| }); | |
| }).catch(reject); | |
| }; | |
| img.src = objectUrl; | |
| }); | |
| } | |
| function readFileAsDataUrl(file: File): Promise<string> { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result as string); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |