piclets / src /lib /utils /image.ts
Fraser's picture
Phase 1: thin-client scan path + docs
aa1a88c
Raw History Blame Contribute Delete
1.72 kB
/**
* Downscale an image File to a max dimension and re-encode as JPEG.
* Phone photos routinely exceed the backend's upload limit; this keeps
* uploads well under it while preserving enough detail for captioning.
*/
export async function downscaleImage(
file: File,
maxDim = 1536,
quality = 0.85,
): Promise<File> {
const bitmap = await loadImageBitmap(file);
const { width, height } = scaleDimensions(bitmap.width, bitmap.height, maxDim);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas 2D context unavailable');
ctx.drawImage(bitmap, 0, 0, width, height);
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error('canvas.toBlob returned null'))),
'image/jpeg',
quality,
);
});
const name = file.name.replace(/\.[^.]+$/, '') + '.jpg';
return new File([blob], name, { type: 'image/jpeg' });
}
function scaleDimensions(w: number, h: number, maxDim: number): { width: number; height: number } {
if (w <= maxDim && h <= maxDim) return { width: w, height: h };
const ratio = w > h ? maxDim / w : maxDim / h;
return { width: Math.round(w * ratio), height: Math.round(h * ratio) };
}
async function loadImageBitmap(file: File): Promise<ImageBitmap | HTMLImageElement> {
if ('createImageBitmap' in window) {
return createImageBitmap(file);
}
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Failed to load image'));
img.src = URL.createObjectURL(file);
});
}