fix(fe): canvas fallback for decodeOrientedBitmap when createImageBitmap is absent

The LAN dev URL is a non-secure, sometimes restricted browser where
createImageBitmap may be undefined; the pick-calibration -> mask decode
would then reject. Fall back to a canvas + Image decode returning an
ImageBitmap. Contract unchanged (still returns ImageBitmap).
This commit is contained in:
2026-09-25 08:03:10 -07:00
parent 3321a7cfdf
commit b03939cf84
+34 -4
View File
@@ -4,17 +4,19 @@
*
* Matched-set / DJI JPEGs often carry EXIF Orientation 8 (stored landscape,
* display portrait). `new Image()` and bare `createImageBitmap(blob)` can
* disagree across browsers → axis arrow space ≠ mask bitset space.
* disagree across browsers -> axis arrow space != mask bitset space.
*
* Contract: always request `imageOrientation: 'from-image'`. If the runtime
* rejects that option (older engines), fall back to bare `createImageBitmap`;
* engines that already apply EXIF by default still match Image/canvas size.
* Contract: always request `imageOrientation: 'from-image'` when supported.
* The dev environment is LAN-HTTP (a non-secure, sometimes restricted
* browser) where `createImageBitmap` may be absent, so fall back to a
* canvas + Image decode that returns an ImageBitmap.
*/
export const ORIENTED_BITMAP_OPTIONS: ImageBitmapOptions = {
imageOrientation: 'from-image',
};
export async function decodeOrientedBitmap(blob: Blob): Promise<ImageBitmap> {
if (typeof createImageBitmap === 'function') {
try {
return await createImageBitmap(blob, ORIENTED_BITMAP_OPTIONS);
} catch (first) {
@@ -25,3 +27,31 @@ export async function decodeOrientedBitmap(blob: Blob): Promise<ImageBitmap> {
}
}
}
return decodeViaCanvas(blob);
}
/** Canvas + Image decode for engines without createImageBitmap. */
async function decodeViaCanvas(blob: Blob): Promise<ImageBitmap> {
const url = URL.createObjectURL(blob);
try {
const img = await loadImage(url);
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new Error('2d context unavailable');
ctx.drawImage(img, 0, 0);
return await createImageBitmap(canvas);
} finally {
URL.revokeObjectURL(url);
}
}
function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('image decode failed'));
img.src = url;
});
}