AI / src /utils /imageOptimizer.ts
Harshavardhan
Upload 25 files
4dc44dc
Raw History Blame Contribute Delete
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);
});
}