Download frontend/src/lib/prepareImage.ts from Duke410/PawTrace: direct link, hf CLI and curl.
- Browser
- Download file 3.2 kB
-
https://huggingface.co/spaces/Duke410/PawTrace/resolve/main/frontend/src/lib/prepareImage.ts
- Command line
-
hf download hf://spaces/Duke410/PawTrace/frontend/src/lib/prepareImage.ts
-
curl -L -o prepareImage.ts https://huggingface.co/spaces/Duke410/PawTrace/resolve/main/frontend/src/lib/prepareImage.ts
3.2 kB
| // Normalise a picked image in the browser BEFORE uploading it. | |
| // | |
| // Phones are the hard case and this fixes several failure modes at once: | |
| // * HEIC β iPhones shoot HEIC by default and the server can't decode it. Safari *can*, so | |
| // decoding here and re-encoding as JPEG converts it transparently. | |
| // * Size β a 4 MB phone photo x6 over mobile data is what produces Safari's "Load failed" | |
| // mid-upload. The server downscales to 1024px anyway, so sending more is wasted bytes. | |
| // * Stale file handles β an iCloud photo that isn't downloaded locally can yield a File that | |
| // fails to read later. Decoding immediately turns it into a real in-memory blob. | |
| // * Orientation β canvas drops EXIF, so rotation is baked in here instead. | |
| // | |
| // Anything undecodable throws, so the caller can say which file failed instead of letting the | |
| // server return an opaque 400. | |
| const MAX_SIDE = 1024; // matches the server's max_image_longest_side | |
| const QUALITY = 0.85; | |
| export class UnreadableImageError extends Error { | |
| constructor(name: string) { | |
| super(`Couldn't read "${name}"`); | |
| this.name = "UnreadableImageError"; | |
| } | |
| } | |
| /** Decode to a bitmap, honouring EXIF rotation, with a fallback for browsers lacking the option. */ | |
| async function decode(file: File): Promise<ImageBitmap | HTMLImageElement> { | |
| if (typeof createImageBitmap === "function") { | |
| try { | |
| return await createImageBitmap(file, { imageOrientation: "from-image" }); | |
| } catch { | |
| /* Safari < 17 rejects the option, and some codecs aren't supported here β fall through. */ | |
| } | |
| } | |
| const url = URL.createObjectURL(file); | |
| try { | |
| return await new Promise<HTMLImageElement>((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(img); | |
| img.onerror = () => reject(new UnreadableImageError(file.name)); | |
| img.src = url; | |
| }); | |
| } finally { | |
| URL.revokeObjectURL(url); | |
| } | |
| } | |
| /** | |
| * Decode, downscale to at most 1024px on the long side, and re-encode as JPEG. | |
| * @throws UnreadableImageError if the browser can't decode the file at all. | |
| */ | |
| export async function prepareImage(file: File): Promise<File> { | |
| let source: ImageBitmap | HTMLImageElement; | |
| try { | |
| source = await decode(file); | |
| } catch { | |
| throw new UnreadableImageError(file.name); | |
| } | |
| const w = "width" in source ? source.width : 0; | |
| const h = "height" in source ? source.height : 0; | |
| if (!w || !h) throw new UnreadableImageError(file.name); | |
| const scale = Math.min(1, MAX_SIDE / Math.max(w, h)); | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = Math.round(w * scale); | |
| canvas.height = Math.round(h * scale); | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) throw new UnreadableImageError(file.name); | |
| ctx.drawImage(source as CanvasImageSource, 0, 0, canvas.width, canvas.height); | |
| if ("close" in source) source.close(); | |
| const blob = await new Promise<Blob | null>((resolve) => | |
| canvas.toBlob(resolve, "image/jpeg", QUALITY) | |
| ); | |
| if (!blob || blob.size === 0) throw new UnreadableImageError(file.name); | |
| const name = file.name.replace(/\.[^.]+$/, "") || "photo"; | |
| return new File([blob], `${name}.jpg`, { type: "image/jpeg" }); | |
| } | |