export interface DecodedLocalImage { readonly source: CanvasImageSource; readonly width: number; readonly height: number; dispose: () => void; } const abortError = (): DOMException => new DOMException("Image decoding was cancelled.", "AbortError"); const decodeWithImageElement = ( file: File, signal?: AbortSignal, ): Promise => new Promise((resolve, reject) => { if (signal?.aborted) { reject(abortError()); return; } const objectUrl = URL.createObjectURL(file); const image = new Image(); image.decoding = "async"; let settled = false; const cleanup = (): void => { image.onload = null; image.onerror = null; signal?.removeEventListener("abort", onAbort); URL.revokeObjectURL(objectUrl); }; const onAbort = (): void => { if (settled) return; settled = true; image.src = ""; cleanup(); reject(abortError()); }; image.onload = () => { if (settled) return; settled = true; cleanup(); resolve({ source: image, width: image.naturalWidth, height: image.naturalHeight, dispose: () => { image.src = ""; }, }); }; image.onerror = () => { if (settled) return; settled = true; cleanup(); reject(new Error("The browser could not decode this image.")); }; signal?.addEventListener("abort", onAbort, { once: true }); image.src = objectUrl; }); export const decodeLocalImage = async ( file: File, signal?: AbortSignal, ): Promise => { if (signal?.aborted) throw abortError(); if (typeof createImageBitmap === "function") { try { const bitmap = await createImageBitmap(file, { imageOrientation: "from-image", }); if (signal?.aborted) { bitmap.close(); throw abortError(); } return { source: bitmap, width: bitmap.width, height: bitmap.height, dispose: () => bitmap.close(), }; } catch (error) { if ( signal?.aborted || (error instanceof DOMException && error.name === "AbortError") ) throw error; // Older browsers sometimes expose createImageBitmap without supporting every format. } } return decodeWithImageElement(file, signal); };