diff --git a/frontend/src/capture/decodeOrientedBitmap.ts b/frontend/src/capture/decodeOrientedBitmap.ts index 73dc3c7..60cb38c 100644 --- a/frontend/src/capture/decodeOrientedBitmap.ts +++ b/frontend/src/capture/decodeOrientedBitmap.ts @@ -4,24 +4,54 @@ * * 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 { - try { - return await createImageBitmap(blob, ORIENTED_BITMAP_OPTIONS); - } catch (first) { + if (typeof createImageBitmap === 'function') { try { - return await createImageBitmap(blob); - } catch { - throw first; + return await createImageBitmap(blob, ORIENTED_BITMAP_OPTIONS); + } catch (first) { + try { + return await createImageBitmap(blob); + } catch { + throw first; + } } } + return decodeViaCanvas(blob); +} + +/** Canvas + Image decode for engines without createImageBitmap. */ +async function decodeViaCanvas(blob: Blob): Promise { + 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 { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('image decode failed')); + img.src = url; + }); }