diff --git a/apps/web/src/components/files/imageDecode.worker.ts b/apps/web/src/components/files/imageDecode.worker.ts index cf3c9c121f9be..b7f09f43951bc 100644 --- a/apps/web/src/components/files/imageDecode.worker.ts +++ b/apps/web/src/components/files/imageDecode.worker.ts @@ -1,3 +1,5 @@ +import { loadHeicDecoder } from "../../lib/heicDecoder"; + const MAX_PIXELS = 32_000_000; function validateTiffMetadata(bytes: ArrayBuffer) { @@ -152,8 +154,17 @@ async function decode(bytes: ArrayBuffer, heif: boolean) { if (bytes.byteLength > 50 * 1024 * 1024) throw new Error("Preview limited to files under 50 MB."); if (heif) { validateHeif(bytes); - const { heicTo } = await import("heic-to/next"); - return await heicTo({ blob: new Blob([bytes]), type: "image/png" }); + const { heicTo } = await loadHeicDecoder(); + const bitmap = await heicTo({ blob: new Blob([bytes]), type: "bitmap" }); + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + try { + const context = canvas.getContext("2d"); + if (!context) throw new Error("Image decoding is unavailable."); + context.drawImage(bitmap, 0, 0); + } finally { + bitmap.close(); + } + return await canvas.convertToBlob({ type: "image/png" }); } const { decode: decodeTiff } = await import("tiff"); validateTiffMetadata(bytes); diff --git a/apps/web/src/lib/heicDecoder.ts b/apps/web/src/lib/heicDecoder.ts new file mode 100644 index 0000000000000..5dc6744529fac --- /dev/null +++ b/apps/web/src/lib/heicDecoder.ts @@ -0,0 +1,6 @@ +import decoderUrl from "heic-to/csp?url"; + +// Emit one unchanged module so the main and worker builds share the same decoder asset. +export function loadHeicDecoder(): Promise { + return import(/* @vite-ignore */ decoderUrl); +} diff --git a/apps/web/src/lib/imageCompression.test.ts b/apps/web/src/lib/imageCompression.test.ts index e40c3968bc850..cc0a069214ff4 100644 --- a/apps/web/src/lib/imageCompression.test.ts +++ b/apps/web/src/lib/imageCompression.test.ts @@ -19,8 +19,8 @@ const mocks = vi.hoisted(() => ({ heicTo: vi.fn(), })); -vi.mock("heic-to/csp", () => ({ - heicTo: mocks.heicTo, +vi.mock("./heicDecoder", () => ({ + loadHeicDecoder: async () => ({ heicTo: mocks.heicTo }), })); const originalCreateImageBitmap = globalThis.createImageBitmap; diff --git a/apps/web/src/lib/imageCompression.ts b/apps/web/src/lib/imageCompression.ts index 53e948e535b75..a7038b375258d 100644 --- a/apps/web/src/lib/imageCompression.ts +++ b/apps/web/src/lib/imageCompression.ts @@ -1,3 +1,4 @@ +import { loadHeicDecoder } from "./heicDecoder"; import { downscalableImageMimeType, fitImageAttachmentSize, @@ -510,7 +511,7 @@ async function convertHeicImage( ? await createImageBitmap(file).catch(() => undefined) : undefined; if (!bitmap) { - const { heicTo } = await import("heic-to/csp"); + const { heicTo } = await loadHeicDecoder(); bitmap = await heicTo({ blob: file, type: "bitmap" }); } const imageSize = fitImageAttachmentSize(bitmap) ?? { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 83847db8c00a0..14f58d7e0c603 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -123,7 +123,6 @@ const browserTestProject = { "fast-xml-parser", "entities", "dompurify", - "heic-to/next", "jszip", "pdfjs-dist", "sql.js",