From 38c0e8c343f45a15039fd71e1723f96aea2d93e1 Mon Sep 17 00:00:00 2001 From: James Manners Date: Tue, 8 Apr 2025 17:19:49 +1000 Subject: [PATCH 01/12] feat(dicom-image-loader): Add ability to customise path to web worker and codec wasm files This is helpul for using Cornerstone in an angular environment where the bundler/build tools don't automatically extract the wasm files --- .../src/decodeImageFrameWorker.js | 97 +++++++++++++++++-- .../src/imageLoader/decodeImageFrame.ts | 16 +-- packages/dicomImageLoader/src/init.ts | 23 ++++- .../src/shared/decoders/decodeHTJ2K.ts | 38 ++++++-- .../src/shared/decoders/decodeJPEG2000.ts | 38 +++++--- .../shared/decoders/decodeJPEGBaseline8Bit.ts | 35 +++++-- .../src/shared/decoders/decodeJPEGLS.ts | 37 +++++-- .../src/types/LoaderDecodeOptions.ts | 52 ++++++++++ .../src/types/LoaderOptions.ts | 10 ++ 9 files changed, 287 insertions(+), 59 deletions(-) diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.js b/packages/dicomImageLoader/src/decodeImageFrameWorker.js index 99a6add366..840ea858e7 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.js +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.js @@ -1,3 +1,4 @@ +// @ts-check /* eslint-disable complexity */ import bilinear from './shared/scaling/bilinear'; import replicate from './shared/scaling/replicate'; @@ -33,6 +34,14 @@ const typedArrayConstructors = { Float32Array, }; +/** + * + * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame + * @param {*} options + * @param {number} start + * @param {import('./types').LoaderDecodeOptions} decodeConfig + * @returns + */ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { const shouldShift = imageFrame.pixelRepresentation !== undefined && @@ -166,6 +175,14 @@ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { return imageFrame; } +/** + * + * @param {*} options + * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame + * @param {*} typedArrayConstructors + * @param {import("@cornerstonejs/core").Types.PixelDataTypedArray} pixelDataArray + * @returns + */ function _handleTargetBuffer( options, imageFrame, @@ -218,6 +235,14 @@ function _handleTargetBuffer( return pixelDataArray; } +/** + * + * @param {*} options + * @param {number} minBeforeScale + * @param {number} maxBeforeScale + * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame + * @returns + */ function _handlePreScaleSetup( options, minBeforeScale, @@ -242,6 +267,13 @@ function _handlePreScaleSetup( return _getDefaultPixelDataArray(scaledMin, scaledMax, imageFrame); } +/** + * + * @param {number} min + * @param {number} max + * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame + * @returns + */ function _getDefaultPixelDataArray(min, max, imageFrame) { const TypedArrayConstructor = getPixelDataTypeFromMinMax(min, max); // @ts-ignore @@ -259,6 +291,13 @@ function _validateScalingParameters(scalingParameters) { } } +/** + * + * @param {*} imageFrame - The type probably should be import("@cornerstonejs/core").Types.IImageFrame but this causes build errors + * @param {*} targetBuffer + * @param {*} TypedArrayConstructor + * @returns + */ function createDestinationImage( imageFrame, targetBuffer, @@ -287,8 +326,14 @@ function createDestinationImage( }; } -/** Scales the image frame, updating the frame in place with a new scaled - * version of it (in place modification) +/** + * + * @description Scales the image frame, updating the frame in place with a new + * scaled version of it (in place modification) + * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame + * @param {*} targetBuffer + * @param {*} TypedArrayConstructor + * @returns */ function scaleImageFrame(imageFrame, targetBuffer, TypedArrayConstructor) { const dest = createDestinationImage( @@ -308,6 +353,17 @@ function scaleImageFrame(imageFrame, targetBuffer, TypedArrayConstructor) { * This is an async function return the result, or you can provide an optional * callbackFn that is called with the results. */ +/** + * + * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame + * @param {string} transferSyntax + * @param {import('dicom-parser').ByteArray} pixelData + * @param {import('./types').LoaderDecodeOptions} decodeConfig + * @param {import("./types").DICOMLoaderImageOptions} options + * @param {(image: import("@cornerstonejs/core").Types.IImageFrame) => void} callbackFn - (Deprecated) Optional callback function for handling + * the decoded frame. + * @returns {Promise} - The decoded image frame. + */ export async function decodeImageFrame( imageFrame, transferSyntax, @@ -318,6 +374,7 @@ export async function decodeImageFrame( ) { const start = new Date().getTime(); + /** @type {Promise | null} */ let decodePromise = null; let opts; @@ -346,7 +403,11 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeJPEGBaseline8Bit(pixelData, opts); + decodePromise = decodeJPEGBaseline8Bit( + pixelData, + opts, + decodeConfig?.wasmUrlCodecLibJpegTurbo8bit + ); break; case '1.2.840.10008.1.2.4.51': // JPEG Baseline lossy process 2 & 4 (12 bit) @@ -374,7 +435,11 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeJPEGLS(pixelData, opts); + decodePromise = decodeJPEGLS( + pixelData, + opts, + decodeConfig?.wasmUrlCodecCharls + ); break; case '1.2.840.10008.1.2.4.81': // JPEG-LS Lossy (Near-Lossless) Image Compression @@ -385,7 +450,11 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeJPEGLS(pixelData, opts); + decodePromise = decodeJPEGLS( + pixelData, + opts, + decodeConfig?.wasmUrlCodecCharls + ); break; case '1.2.840.10008.1.2.4.90': opts = { @@ -394,7 +463,11 @@ export async function decodeImageFrame( // JPEG 2000 Lossless // imageFrame, pixelData, decodeConfig, options - decodePromise = decodeJPEG2000(pixelData, opts); + decodePromise = decodeJPEG2000( + pixelData, + opts, + decodeConfig?.wasmUrlCodecOpenJpeg + ); break; case '1.2.840.10008.1.2.4.91': // JPEG 2000 Lossy @@ -404,7 +477,11 @@ export async function decodeImageFrame( // JPEG 2000 Lossy // imageFrame, pixelData, decodeConfig, options - decodePromise = decodeJPEG2000(pixelData, opts); + decodePromise = decodeJPEG2000( + pixelData, + opts, + decodeConfig?.wasmUrlCodecOpenJpeg + ); break; case '3.2.840.10008.1.2.4.96': case '1.2.840.10008.1.2.4.201': @@ -415,7 +492,11 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeHTJ2K(pixelData, opts); + decodePromise = decodeHTJ2K( + pixelData, + opts, + decodeConfig?.wasmUrlCodecOpenJph + ); break; default: throw new Error(`no decoder for transfer syntax ${transferSyntax}`); diff --git a/packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts b/packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts index 6b4388a229..cff977149f 100644 --- a/packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts +++ b/packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts @@ -3,10 +3,10 @@ import decodeJPEGBaseline8BitColor from './decodeJPEGBaseline8BitColor'; // dicomParser requires pako for browser-side decoding of deflate transfer syntax // We only need one function though, so lets import that so we don't make our bundle // too large. -import type { ByteArray } from 'dicom-parser'; import type { Types } from '@cornerstonejs/core'; import { getWebWorkerManager } from '@cornerstonejs/core'; -import type { LoaderDecodeOptions } from '../types'; +import type { ByteArray } from 'dicom-parser'; +import type { DICOMLoaderImageOptions, LoaderDecodeOptions } from '../types'; function processDecodeTask( imageFrame: Types.IImageFrame, @@ -47,12 +47,12 @@ function processDecodeTask( } function decodeImageFrame( - imageFrame, - transferSyntax, - pixelData, - canvas, - options = {}, - decodeConfig + imageFrame: Types.IImageFrame, + transferSyntax: string, + pixelData: ByteArray, + canvas: HTMLCanvasElement, + options: DICOMLoaderImageOptions = {}, + decodeConfig: LoaderDecodeOptions ) { switch (transferSyntax) { case '1.2.840.10008.1.2': diff --git a/packages/dicomImageLoader/src/init.ts b/packages/dicomImageLoader/src/init.ts index 41d1bb0b77..fd9e851f2f 100644 --- a/packages/dicomImageLoader/src/init.ts +++ b/packages/dicomImageLoader/src/init.ts @@ -18,11 +18,17 @@ function init(options: LoaderOptions = {}): void { setOptions(options); registerLoaders(); + const isValidWorkerFactory = validateWorkerFactoryOption(options); + const workerManager = getWebWorkerManager(); const maxWorkers = options?.maxWebWorkers || getReasonableWorkerCount(); - workerManager.registerWorker('dicomImageLoader', workerFn, { - maxWorkerInstances: maxWorkers, - }); + workerManager.registerWorker( + 'dicomImageLoader', + isValidWorkerFactory ? options.webWorkerFactory : workerFn, + { + maxWorkerInstances: maxWorkers, + } + ); } function getReasonableWorkerCount(): number { @@ -35,3 +41,14 @@ function getReasonableWorkerCount(): number { } export default init; + +function validateWorkerFactoryOption(options: LoaderOptions) { + const isValidWorkerFactory = + options.webWorkerFactory && typeof options.webWorkerFactory === 'function'; + if (!!options.webWorkerFactory && !isValidWorkerFactory) { + console.warn( + 'webWorkerFactory should be a function that returns a Worker instance.' + ); + } + return isValidWorkerFactory; +} diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts index e46d5f83da..3ef13f5d42 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts @@ -1,15 +1,18 @@ +import openJphFactory from '@cornerstonejs/codec-openjph/wasmjs'; // @ts-ignore import type { ByteArray } from 'dicom-parser'; -// @ts-ignore -import openJphFactory from '@cornerstonejs/codec-openjph/wasmjs'; -// @ts-ignore -// import openjphWasm from '@cornerstonejs/codec-openjph/wasm'; +import type { LoaderDecodeOptions } from '../../types'; + +/** + * Default URL to load the OpenJPH codec from. + * + * In order for this to be loaded correctly, you will need to configure your + * bundler to treat `.wasm` files as an asset/resource. + */ const openjphWasm = new URL( '@cornerstonejs/codec-openjph/wasm', import.meta.url ); -import type { LoaderDecodeOptions } from '../../types'; - const local: { codec: unknown; decoder: unknown; @@ -34,7 +37,17 @@ function calculateSizeAtDecompositionLevel( return result; } -export function initialize(decodeConfig?: LoaderDecodeOptions): Promise { +/** + * + * @param decodeConfig + * @param wasmUrlCodecOpenJph Optional path to load the OpenJPH WASM codec from. + * If not given, it will default to using the default `openjphWasm` + * @returns + */ +export function initialize( + decodeConfig?: LoaderDecodeOptions, + wasmUrlCodecOpenJph?: string +): Promise { local.decodeConfig = decodeConfig; if (local.codec) { @@ -44,6 +57,9 @@ export function initialize(decodeConfig?: LoaderDecodeOptions): Promise { const openJphModule = openJphFactory({ locateFile: (f) => { if (f.endsWith('.wasm')) { + if (wasmUrlCodecOpenJph) { + return wasmUrlCodecOpenJph; + } return openjphWasm.toString(); } @@ -61,8 +77,12 @@ export function initialize(decodeConfig?: LoaderDecodeOptions): Promise { } // https://github.com/chafey/openjpegjs/blob/master/test/browser/index.html -async function decodeAsync(compressedImageFrame: ByteArray, imageInfo) { - await initialize(); +async function decodeAsync( + compressedImageFrame: ByteArray, + imageInfo, + wasmUrlCodecOpenJph?: string +) { + await initialize(undefined, wasmUrlCodecOpenJph); // const decoder = local.decoder; // This is much slower for some reason // @ts-expect-error const decoder = new local.codec.HTJ2KDecoder(); diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts index b3a98da743..9aea8d5050 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts @@ -1,26 +1,26 @@ // https://emscripten.org/docs/api_reference/module.html +// @ts-ignore +import openJpegFactory from '@cornerstonejs/codec-openjpeg/decodewasmjs'; import type { J2KDecoder, OpenJpegModule, } from '@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode'; -// @ts-ignore -import openJpegFactory from '@cornerstonejs/codec-openjpeg/decodewasmjs'; +import type { Types } from '@cornerstonejs/core'; +import type { WebWorkerDecodeConfig } from '../../types'; // Webpack asset/resource copies this to our output folder -// TODO: At some point maybe we can use this instead. -// This is closer to what Webpack 5 wants but it doesn't seem to work now -// const wasm = new URL('./blah.wasm', import.meta.url) -// @ts-ignore -// import openjpegWasm from '@cornerstonejs/codec-openjpeg/decodewasm'; +/** + * Default URL to load the OpenJPH codec from. + * + * In order for this to be loaded correctly, you will need to configure your + * bundler to treat `.wasm` files as an asset/resource. + */ const openjpegWasm = new URL( '@cornerstonejs/codec-openjpeg/decodewasm', import.meta.url ); -import type { Types } from '@cornerstonejs/core'; -import type { WebWorkerDecodeConfig } from '../../types'; - const local: { codec: OpenJpegModule; decoder: J2KDecoder; @@ -31,8 +31,16 @@ const local: { decodeConfig: {} as WebWorkerDecodeConfig, }; +/** + * + * @param decodeConfig + * @param wasmUrlCodecOpenJpeg - Optional path to load the OpenJpeg WASM codec from. + * If not given, it will default to using the default `openjpegWasm` URL. + * @returns + */ export function initialize( - decodeConfig?: WebWorkerDecodeConfig + decodeConfig?: WebWorkerDecodeConfig, + wasmUrlCodecOpenJpeg?: string ): Promise { local.decodeConfig = decodeConfig; @@ -43,6 +51,9 @@ export function initialize( const openJpegModule = openJpegFactory({ locateFile: (f) => { if (f.endsWith('.wasm')) { + if (wasmUrlCodecOpenJpeg) { + return wasmUrlCodecOpenJpeg; + } return openjpegWasm.toString(); } @@ -62,9 +73,10 @@ export function initialize( // https://github.com/chafey/openjpegjs/blob/master/test/browser/index.html async function decodeAsync( compressedImageFrame, - imageInfo + imageInfo, + wasmUrlCodecOpenJpeg?: string ): Promise { - await initialize(); + await initialize(undefined, wasmUrlCodecOpenJpeg); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts index 45798da4bc..ec1c6b9762 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts @@ -1,18 +1,22 @@ +// @ts-ignore +import libjpegTurboFactory from '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs'; import type { LibJpegTurbo8Bit, OpenJpegModule, } from '@cornerstonejs/codec-libjpeg-turbo-8bit/dist/libjpegturbowasm_decode'; +import type { Types } from '@cornerstonejs/core'; import type { ByteArray } from 'dicom-parser'; -// @ts-ignore -import libjpegTurboFactory from '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs'; -// @ts-ignore -// import libjpegTurboWasm from '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm'; +/** + * Default URL to load the LibJpeg Turbo 8bit codec from. + * + * In order for this to be loaded correctly, you will need to configure your + * bundler to treat `.wasm` files as an asset/resource. + */ const libjpegTurboWasm = new URL( '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', import.meta.url ); -import type { Types } from '@cornerstonejs/core'; const local: { codec: OpenJpegModule; @@ -22,14 +26,28 @@ const local: { decoder: undefined, }; -function initLibjpegTurbo(): Promise { +/** + * + * @param [wasmUrlCodecLibjpegTurbo8bit] - Optional URL for the codec WASM file. + * If not provided, it will default to the `libjpegTurboWasm` URL. + * @returns + */ +function initLibjpegTurbo( + wasmUrlCodecLibjpegTurbo8bit?: string +): Promise { if (local.codec) { return Promise.resolve(); } const libjpegTurboModule = libjpegTurboFactory({ - locateFile: (f) => { + locateFile: (f: string) => { if (f.endsWith('.wasm')) { + /** + * If a custom URL is provided, use that instead of the default one. + */ + if (wasmUrlCodecLibjpegTurbo8bit) { + return wasmUrlCodecLibjpegTurbo8bit; + } return libjpegTurboWasm.toString(); } @@ -55,7 +73,8 @@ function initLibjpegTurbo(): Promise { */ async function decodeAsync( compressedImageFrame, - imageInfo + imageInfo, + wasmUrlCodecLibjpegTurbo8bit?: string ): Promise { await initLibjpegTurbo(); const decoder = local.decoder; diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts index 60970a8f2c..9d7c4729ff 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts @@ -1,18 +1,23 @@ +// @ts-ignore +import charlsFactory from '@cornerstonejs/codec-charls/decodewasmjs'; import type { CharlsModule, JpegLSDecoder, } from '@cornerstonejs/codec-charls/dist/charlswasm_decode'; -// @ts-ignore -import charlsFactory from '@cornerstonejs/codec-charls/decodewasmjs'; -// @ts-ignore -// import charlsWasm from '@cornerstonejs/codec-charls/decodewasm'; +import type { Types } from '@cornerstonejs/core'; +import type { ByteArray } from 'dicom-parser'; +import type { WebWorkerDecodeConfig } from '../../types'; + +/** + * Default URL to load the Charls codec from. + * + * In order for this to be loaded correctly, you will need to configure your + * bundler to treat `.wasm` files as an asset/resource. + */ const charlsWasm = new URL( '@cornerstonejs/codec-charls/decodewasm', import.meta.url ); -import type { ByteArray } from 'dicom-parser'; -import type { WebWorkerDecodeConfig } from '../../types'; -import type { Types } from '@cornerstonejs/core'; const local: { codec: CharlsModule; @@ -30,8 +35,16 @@ function getExceptionMessage(exception) { : exception; } +/** + * + * @param decodeConfig + * @param wasmUrlCodecCharls Optional - a path/url where to load the charls wasm + * file from. If not given, it will default to using the default `charlsWasm` URL. + * @returns + */ export function initialize( - decodeConfig?: WebWorkerDecodeConfig + decodeConfig?: WebWorkerDecodeConfig, + wasmUrlCodecCharls?: string ): Promise { local.decodeConfig = decodeConfig; @@ -42,6 +55,9 @@ export function initialize( const charlsModule = charlsFactory({ locateFile: (f) => { if (f.endsWith('.wasm')) { + if (wasmUrlCodecCharls) { + return wasmUrlCodecCharls; + } return charlsWasm.toString(); } @@ -66,10 +82,11 @@ export function initialize( */ async function decodeAsync( compressedImageFrame, - imageInfo + imageInfo, + wasmUrlCodecCharls?: string ): Promise { try { - await initialize(); + await initialize(undefined, wasmUrlCodecCharls); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory diff --git a/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts b/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts index e1eaf6464c..7ab5123484 100644 --- a/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts +++ b/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts @@ -1,3 +1,55 @@ export interface LoaderDecodeOptions { // whatever + /** + * Specifies the url/path to the codec wasm files. If specified, the wasm + * blobs will be fetched from these paths rather than the default. + * + * By default, the codec wasm files are included in the end build by your + * bundler (webpack, vite etc). + * + * Rather than using a bundler to identify the assets, it is possible to pass + * in paths to where the wasm files should be loaded from. For instance, in + * your build step extract the following wasm files and include them in your + * static assets directory. Then set the the correct paths to where + * cornerstone should load the files. + * + * This should point to the export `@cornerstonejs/codec-charls/decodewasm` + * that resolves to `@cornerstonejs/codec-charls/dist/charlswasm_decode.wasm`. + * Copy this file to your static assets directory and set the path to it. + * + * @example `wasmUrlCodecCharls: '/static/charlswasm_decode.wasm'` + */ + wasmUrlCodecCharls?: string; + /** + * Manually set the path/url to load `openjphjs.wasm`. See the notes above + * for detailed notes. + * + * This should point to the export `@cornerstonejs/codec-openjph/wasm` which + * resolves to + * `node_modules/@cornerstonejs/codec-openjph/dist/openjphjs.wasm`. + * + * @example `wasmUrlCodecOpenJph: '/static/openjphjs.wasm'` + */ + wasmUrlCodecOpenJph?: string; + /** + * Manually set the path/url to load `openjpegwasm_decode.wasm`. See the + * notes above for detailed notes. + * + * This should point to the `@cornerstonejs/codec-openjpeg/decodewasm` which + * resolves to `@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode.wasm` + * + * @example `wasmUrlCodecOpenJph: '/static/openjpegwasm_decode.wasm'` + */ + wasmUrlCodecOpenJpeg?: string; + /** + * Manually set the path/url to load `libjpegturbowasm_decode.wasm`. See the + * notes above for detailed notes. + * + * This should point to the export + * `@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm` which resolves to + * `@cornerstonejs/codec-libjpeg-turbo-8bit/dist/libjpegturbowasm_decode.wasm` + * + * @example `wasmUrlCodecOpenJph: '/static/libjpegturbowasm_decode.wasm'` + */ + wasmUrlCodecLibJpegTurbo8bit?: string; } diff --git a/packages/dicomImageLoader/src/types/LoaderOptions.ts b/packages/dicomImageLoader/src/types/LoaderOptions.ts index 5a79b899c2..c779af2ae0 100644 --- a/packages/dicomImageLoader/src/types/LoaderOptions.ts +++ b/packages/dicomImageLoader/src/types/LoaderOptions.ts @@ -31,4 +31,14 @@ export interface LoaderOptions { errorInterceptor?: (error: LoaderXhrRequestError) => void; strict?: boolean; decodeConfig?: LoaderDecodeOptions; + /** + * Specifies the url/path to the web worker. + * + * By default, the web worker is included in the final build by your bundler + * (webpack, vite etc). However, for in some cases, it may be required to + * manually specify the deployment path. Specifically this is useful for + * Angular projects, allowing the `decodeImageFrameWorker.js` to be included + * in the standard Angular build process. + */ + webWorkerFactory?: () => Worker; } From 076bdf1225cf9aa963a0ef531c5a5f1e4ee1e7be Mon Sep 17 00:00:00 2001 From: James Manners Date: Tue, 8 Apr 2025 17:31:46 +1000 Subject: [PATCH 02/12] docs(LoaderOptions): Update comments --- .../src/types/LoaderOptions.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/dicomImageLoader/src/types/LoaderOptions.ts b/packages/dicomImageLoader/src/types/LoaderOptions.ts index c779af2ae0..0a8a159615 100644 --- a/packages/dicomImageLoader/src/types/LoaderOptions.ts +++ b/packages/dicomImageLoader/src/types/LoaderOptions.ts @@ -32,13 +32,28 @@ export interface LoaderOptions { strict?: boolean; decodeConfig?: LoaderDecodeOptions; /** - * Specifies the url/path to the web worker. + * Pass a custom web worker factory function to create the web worker. * - * By default, the web worker is included in the final build by your bundler - * (webpack, vite etc). However, for in some cases, it may be required to - * manually specify the deployment path. Specifically this is useful for - * Angular projects, allowing the `decodeImageFrameWorker.js` to be included - * in the standard Angular build process. + * By default, cornerstone creates the path to the web worker, relying on a + * bundler to resolve the path. This is not always possible, especially when + * using a custom bundler or when the web worker is not in the same directory + * as the main script. This is particularly helpful when including + * `@cornerstonejs/dicom-image-loader` in an Angular project. + * + * This option allows you to provide a custom function that returns a new web + * worker instance. + * + * @example + * ```typescript + * const customWebWorkerFactory = () => { + * const worker = new Worker('path/to/your/customWorker.js'); + * return worker; + * } + * + * const loaderOptions: LoaderOptions = { + * webWorkerFactory: customWebWorkerFactory, + * } + * ``` */ webWorkerFactory?: () => Worker; } From b01cb71eee4b5c0a542edd627e9c2f38bcdc75d4 Mon Sep 17 00:00:00 2001 From: James Manners Date: Fri, 11 Apr 2025 16:34:18 +1000 Subject: [PATCH 03/12] fix(dicomImageLoader): fix loading libJpegTurbo wasm from custom url --- .../src/shared/decoders/decodeJPEGBaseline8Bit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts index ec1c6b9762..518d6d12fd 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts @@ -76,7 +76,7 @@ async function decodeAsync( imageInfo, wasmUrlCodecLibjpegTurbo8bit?: string ): Promise { - await initLibjpegTurbo(); + await initLibjpegTurbo(wasmUrlCodecLibjpegTurbo8bit); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory From a7d2a89b21d5615d22e08b71d4ae68f3ee4529ad Mon Sep 17 00:00:00 2001 From: James Manners Date: Mon, 30 Jun 2025 11:48:15 +1000 Subject: [PATCH 04/12] refactor(dicom-image-loader): add additional types --- .../src/decodeImageFrameWorker.js | 130 +++++++++++++----- 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.js b/packages/dicomImageLoader/src/decodeImageFrameWorker.js index 840ea858e7..dde97ed78e 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.js +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.js @@ -32,15 +32,16 @@ const typedArrayConstructors = { Uint16Array, Int16Array, Float32Array, + Uint32Array, }; /** * * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @param {*} options + * @param {import("./types").DICOMLoaderImageOptions} options * @param {number} start * @param {import('./types').LoaderDecodeOptions} decodeConfig - * @returns + * @returns {import("@cornerstonejs/core").Types.IImageFrame} */ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { const shouldShift = @@ -94,13 +95,15 @@ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { // are actually within the range of the type. If not, we need to convert the // pixel data to the correct type. if (type && options.preScale.enabled && !disableScale) { - const { rescaleSlope, rescaleIntercept } = - options.preScale.scalingParameters; - const minAfterScale = rescaleSlope * minBeforeScale + rescaleIntercept; - const maxAfterScale = rescaleSlope * maxBeforeScale + rescaleIntercept; + const scalingParameters = options.preScale.scalingParameters; + const scaledValues = _calculateScaledMinMax( + minBeforeScale, + maxBeforeScale, + scalingParameters + ); invalidType = !validatePixelDataType( - minAfterScale, - maxAfterScale, + scaledValues.min, + scaledValues.max, typedArrayConstructors[type] ); } @@ -134,26 +137,22 @@ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { const scalingParameters = options.preScale.scalingParameters; _validateScalingParameters(scalingParameters); - const { rescaleSlope, rescaleIntercept } = scalingParameters; - const isSlopeAndInterceptNumbers = - typeof rescaleSlope === 'number' && typeof rescaleIntercept === 'number'; + const isRequiredScaling = _isRequiredScaling(scalingParameters); - if (isSlopeAndInterceptNumbers) { + if (isRequiredScaling) { applyModalityLUT(pixelDataArray, scalingParameters); imageFrame.preScale = { ...options.preScale, scaled: true, }; - // calculate the min and max after scaling - const { rescaleIntercept, rescaleSlope, suvbw } = scalingParameters; - minAfterScale = rescaleSlope * minBeforeScale + rescaleIntercept; - maxAfterScale = rescaleSlope * maxBeforeScale + rescaleIntercept; - - if (suvbw) { - minAfterScale = minAfterScale * suvbw; - maxAfterScale = maxAfterScale * suvbw; - } + const scaledValues = _calculateScaledMinMax( + minBeforeScale, + maxBeforeScale, + scalingParameters + ); + minAfterScale = scaledValues.min; + maxAfterScale = scaledValues.max; } } else if (disableScale) { imageFrame.preScale = { @@ -175,13 +174,32 @@ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { return imageFrame; } +/** + * + * @param {import("@cornerstonejs/core").Types.ScalingParameters} scalingParameters + * @returns {boolean} + */ +function _isRequiredScaling(scalingParameters) { + // @ts-expect-error ScalingParameters type does not include `doseGridScaling` + const { rescaleSlope, rescaleIntercept, modality, doseGridScaling, suvbw } = + scalingParameters; + + const hasRescaleValues = + typeof rescaleSlope === 'number' && typeof rescaleIntercept === 'number'; + const isRTDOSEWithScaling = + modality === 'RTDOSE' && typeof doseGridScaling === 'number'; + const isPTWithSUV = modality === 'PT' && typeof suvbw === 'number'; + + return hasRescaleValues || isRTDOSEWithScaling || isPTWithSUV; +} + /** * * @param {*} options * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame * @param {*} typedArrayConstructors * @param {import("@cornerstonejs/core").Types.PixelDataTypedArray} pixelDataArray - * @returns + * @returns {import("@cornerstonejs/core").Types.PixelDataTypedArray} */ function _handleTargetBuffer( options, @@ -237,7 +255,7 @@ function _handleTargetBuffer( /** * - * @param {*} options + * @param {import("./types").DICOMLoaderImageOptions} options * @param {number} minBeforeScale * @param {number} maxBeforeScale * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame @@ -252,19 +270,17 @@ function _handlePreScaleSetup( const scalingParameters = options.preScale.scalingParameters; _validateScalingParameters(scalingParameters); - const { rescaleSlope, rescaleIntercept } = scalingParameters; - const areSlopeAndInterceptNumbers = - typeof rescaleSlope === 'number' && typeof rescaleIntercept === 'number'; - - let scaledMin = minBeforeScale; - let scaledMax = maxBeforeScale; - - if (areSlopeAndInterceptNumbers) { - scaledMin = rescaleSlope * minBeforeScale + rescaleIntercept; - scaledMax = rescaleSlope * maxBeforeScale + rescaleIntercept; - } + const scaledValues = _calculateScaledMinMax( + minBeforeScale, + maxBeforeScale, + scalingParameters + ); - return _getDefaultPixelDataArray(scaledMin, scaledMax, imageFrame); + return _getDefaultPixelDataArray( + scaledValues.min, + scaledValues.max, + imageFrame + ); } /** @@ -272,7 +288,7 @@ function _handlePreScaleSetup( * @param {number} min * @param {number} max * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @returns + * @returns {import("@cornerstonejs/core").Types.PixelDataTypedArray} */ function _getDefaultPixelDataArray(min, max, imageFrame) { const TypedArrayConstructor = getPixelDataTypeFromMinMax(min, max); @@ -283,6 +299,48 @@ function _getDefaultPixelDataArray(min, max, imageFrame) { return typedArray; } +/** + * + * @param {number} minValue + * @param {number} maxValue + * @param {import("@cornerstonejs/core").Types.ScalingParameters} scalingParameters + * @returns {{ min: number, max: number }} + */ +function _calculateScaledMinMax(minValue, maxValue, scalingParameters) { + // @ts-expect-error ScalingParameters type does not include `doseGridScaling` + const { rescaleSlope, rescaleIntercept, modality, doseGridScaling, suvbw } = + scalingParameters; + + if (modality === 'PT' && typeof suvbw === 'number' && !isNaN(suvbw)) { + return { + min: suvbw * (minValue * rescaleSlope + rescaleIntercept), + max: suvbw * (maxValue * rescaleSlope + rescaleIntercept), + }; + } else if ( + modality === 'RTDOSE' && + typeof doseGridScaling === 'number' && + !isNaN(doseGridScaling) + ) { + return { + min: minValue * doseGridScaling, + max: maxValue * doseGridScaling, + }; + } else if ( + typeof rescaleSlope === 'number' && + typeof rescaleIntercept === 'number' + ) { + return { + min: rescaleSlope * minValue + rescaleIntercept, + max: rescaleSlope * maxValue + rescaleIntercept, + }; + } else { + return { + min: minValue, + max: maxValue, + }; + } +} + function _validateScalingParameters(scalingParameters) { if (!scalingParameters) { throw new Error( From 66a184865e728851fe5581d7db323ffe310b99df Mon Sep 17 00:00:00 2001 From: James Manners Date: Mon, 30 Jun 2025 12:45:57 +1000 Subject: [PATCH 05/12] refactor(dicom-image-loader): Refactor decodeImageFrameWorker into typescript --- ...ameWorker.js => decodeImageFrameWorker.ts} | 166 +++++++----------- .../src/types/DICOMLoaderImageOptions.ts | 1 + 2 files changed, 60 insertions(+), 107 deletions(-) rename packages/dicomImageLoader/src/{decodeImageFrameWorker.js => decodeImageFrameWorker.ts} (82%) diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.js b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts similarity index 82% rename from packages/dicomImageLoader/src/decodeImageFrameWorker.js rename to packages/dicomImageLoader/src/decodeImageFrameWorker.ts index dde97ed78e..111f848ef3 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.js +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts @@ -1,26 +1,27 @@ -// @ts-check /* eslint-disable complexity */ -import bilinear from './shared/scaling/bilinear'; -import replicate from './shared/scaling/replicate'; import { expose } from 'comlink'; - -import decodeLittleEndian from './shared/decoders/decodeLittleEndian'; import decodeBigEndian from './shared/decoders/decodeBigEndian'; -import decodeRLE from './shared/decoders/decodeRLE'; import decodeJPEGBaseline8Bit from './shared/decoders/decodeJPEGBaseline8Bit'; +import decodeLittleEndian from './shared/decoders/decodeLittleEndian'; +import decodeRLE from './shared/decoders/decodeRLE'; +import bilinear from './shared/scaling/bilinear'; +import replicate from './shared/scaling/replicate'; // import decodeJPEGBaseline12Bit from './shared/decoders/decodeJPEGBaseline12Bit'; +import decodeHTJ2K from './shared/decoders/decodeHTJ2K'; +import decodeJPEG2000 from './shared/decoders/decodeJPEG2000'; import decodeJPEGBaseline12Bit from './shared/decoders/decodeJPEGBaseline12Bit-js'; import decodeJPEGLossless from './shared/decoders/decodeJPEGLossless'; import decodeJPEGLS from './shared/decoders/decodeJPEGLS'; -import decodeJPEG2000 from './shared/decoders/decodeJPEG2000'; -import decodeHTJ2K from './shared/decoders/decodeHTJ2K'; // Note that the scaling is pixel value scaling, which is applying a modality LUT -import applyModalityLUT from './shared/scaling/scaleArray'; +import type { Types as CoreTypes } from '@cornerstonejs/core'; +import type { ByteArray } from 'dicom-parser'; import getMinMax from './shared/getMinMax'; import getPixelDataTypeFromMinMax, { validatePixelDataType, } from './shared/getPixelDataTypeFromMinMax'; import isColorImage from './shared/isColorImage'; +import applyModalityLUT from './shared/scaling/scaleArray'; +import type { DICOMLoaderImageOptions, LoaderDecodeOptions } from './types'; const imageUtils = { bilinear, @@ -34,16 +35,14 @@ const typedArrayConstructors = { Float32Array, Uint32Array, }; - -/** - * - * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @param {import("./types").DICOMLoaderImageOptions} options - * @param {number} start - * @param {import('./types').LoaderDecodeOptions} decodeConfig - * @returns {import("@cornerstonejs/core").Types.IImageFrame} - */ -function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { +type TypedArrayConstructorsMap = typeof typedArrayConstructors; + +function postProcessDecodedPixels( + imageFrame: CoreTypes.IImageFrame, + options: DICOMLoaderImageOptions, + start: number, + decodeConfig: LoaderDecodeOptions +): CoreTypes.IImageFrame { const shouldShift = imageFrame.pixelRepresentation !== undefined && imageFrame.pixelRepresentation === 1; @@ -174,12 +173,9 @@ function postProcessDecodedPixels(imageFrame, options, start, decodeConfig) { return imageFrame; } -/** - * - * @param {import("@cornerstonejs/core").Types.ScalingParameters} scalingParameters - * @returns {boolean} - */ -function _isRequiredScaling(scalingParameters) { +function _isRequiredScaling( + scalingParameters: CoreTypes.ScalingParameters +): boolean { // @ts-expect-error ScalingParameters type does not include `doseGridScaling` const { rescaleSlope, rescaleIntercept, modality, doseGridScaling, suvbw } = scalingParameters; @@ -193,20 +189,12 @@ function _isRequiredScaling(scalingParameters) { return hasRescaleValues || isRTDOSEWithScaling || isPTWithSUV; } -/** - * - * @param {*} options - * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @param {*} typedArrayConstructors - * @param {import("@cornerstonejs/core").Types.PixelDataTypedArray} pixelDataArray - * @returns {import("@cornerstonejs/core").Types.PixelDataTypedArray} - */ function _handleTargetBuffer( - options, - imageFrame, - typedArrayConstructors, - pixelDataArray -) { + options: DICOMLoaderImageOptions, + imageFrame: CoreTypes.IImageFrame, + typedArrayConstructors: TypedArrayConstructorsMap, + pixelDataArray: CoreTypes.PixelDataTypedArray +): CoreTypes.PixelDataTypedArray { const { arrayBuffer, type, @@ -253,19 +241,11 @@ function _handleTargetBuffer( return pixelDataArray; } -/** - * - * @param {import("./types").DICOMLoaderImageOptions} options - * @param {number} minBeforeScale - * @param {number} maxBeforeScale - * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @returns - */ function _handlePreScaleSetup( - options, - minBeforeScale, - maxBeforeScale, - imageFrame + options: DICOMLoaderImageOptions, + minBeforeScale: number, + maxBeforeScale: number, + imageFrame: CoreTypes.IImageFrame ) { const scalingParameters = options.preScale.scalingParameters; _validateScalingParameters(scalingParameters); @@ -283,14 +263,11 @@ function _handlePreScaleSetup( ); } -/** - * - * @param {number} min - * @param {number} max - * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @returns {import("@cornerstonejs/core").Types.PixelDataTypedArray} - */ -function _getDefaultPixelDataArray(min, max, imageFrame) { +function _getDefaultPixelDataArray( + min: number, + max: number, + imageFrame: CoreTypes.IImageFrame +): CoreTypes.PixelDataTypedArray { const TypedArrayConstructor = getPixelDataTypeFromMinMax(min, max); // @ts-ignore const typedArray = new TypedArrayConstructor(imageFrame.pixelData.length); @@ -299,14 +276,11 @@ function _getDefaultPixelDataArray(min, max, imageFrame) { return typedArray; } -/** - * - * @param {number} minValue - * @param {number} maxValue - * @param {import("@cornerstonejs/core").Types.ScalingParameters} scalingParameters - * @returns {{ min: number, max: number }} - */ -function _calculateScaledMinMax(minValue, maxValue, scalingParameters) { +function _calculateScaledMinMax( + minValue: number, + maxValue: number, + scalingParameters: CoreTypes.ScalingParameters +): { min: number; max: number } { // @ts-expect-error ScalingParameters type does not include `doseGridScaling` const { rescaleSlope, rescaleIntercept, modality, doseGridScaling, suvbw } = scalingParameters; @@ -341,7 +315,9 @@ function _calculateScaledMinMax(minValue, maxValue, scalingParameters) { } } -function _validateScalingParameters(scalingParameters) { +function _validateScalingParameters( + scalingParameters: CoreTypes.ScalingParameters +) { if (!scalingParameters) { throw new Error( 'options.preScale.scalingParameters must be defined if preScale.enabled is true, and scalingParameters cannot be derived from the metadata providers.' @@ -349,17 +325,10 @@ function _validateScalingParameters(scalingParameters) { } } -/** - * - * @param {*} imageFrame - The type probably should be import("@cornerstonejs/core").Types.IImageFrame but this causes build errors - * @param {*} targetBuffer - * @param {*} TypedArrayConstructor - * @returns - */ function createDestinationImage( - imageFrame, - targetBuffer, - TypedArrayConstructor + imageFrame: any, + targetBuffer: DICOMLoaderImageOptions['targetBuffer'], + TypedArrayConstructor: new (size: number) => CoreTypes.PixelDataTypedArray ) { const { samplesPerPixel } = imageFrame; const { rows, columns } = targetBuffer; @@ -384,16 +353,11 @@ function createDestinationImage( }; } -/** - * - * @description Scales the image frame, updating the frame in place with a new - * scaled version of it (in place modification) - * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @param {*} targetBuffer - * @param {*} TypedArrayConstructor - * @returns - */ -function scaleImageFrame(imageFrame, targetBuffer, TypedArrayConstructor) { +function scaleImageFrame( + imageFrame: CoreTypes.IImageFrame, + targetBuffer: DICOMLoaderImageOptions['targetBuffer'], + TypedArrayConstructor: new (size: number) => CoreTypes.PixelDataTypedArray +) { const dest = createDestinationImage( imageFrame, targetBuffer, @@ -411,29 +375,17 @@ function scaleImageFrame(imageFrame, targetBuffer, TypedArrayConstructor) { * This is an async function return the result, or you can provide an optional * callbackFn that is called with the results. */ -/** - * - * @param {import("@cornerstonejs/core").Types.IImageFrame} imageFrame - * @param {string} transferSyntax - * @param {import('dicom-parser').ByteArray} pixelData - * @param {import('./types').LoaderDecodeOptions} decodeConfig - * @param {import("./types").DICOMLoaderImageOptions} options - * @param {(image: import("@cornerstonejs/core").Types.IImageFrame) => void} callbackFn - (Deprecated) Optional callback function for handling - * the decoded frame. - * @returns {Promise} - The decoded image frame. - */ export async function decodeImageFrame( - imageFrame, - transferSyntax, - pixelData, - decodeConfig, - options, - callbackFn -) { + imageFrame: CoreTypes.IImageFrame, + transferSyntax: string, + pixelData: ByteArray, + decodeConfig: LoaderDecodeOptions, + options: DICOMLoaderImageOptions, + callbackFn?: (image: CoreTypes.IImageFrame) => void +): Promise { const start = new Date().getTime(); - /** @type {Promise | null} */ - let decodePromise = null; + let decodePromise: Promise | null = null; let opts; diff --git a/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts b/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts index 71e008b4e5..dda287c1b8 100644 --- a/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts +++ b/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts @@ -16,6 +16,7 @@ export interface DICOMLoaderImageOptions { offset: number; rows?: number; columns?: number; + scalingType?: 'replicate' | string; }; loader?: LoadRequestFunction; decodeLevel?: number; From fc66e0c059fb8903c32a6e8968ea4afd83ceaa0d Mon Sep 17 00:00:00 2001 From: James Manners Date: Mon, 30 Jun 2025 12:47:01 +1000 Subject: [PATCH 06/12] docs(dicom-image-loader): fix comment --- packages/dicomImageLoader/src/decodeImageFrameWorker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts index 111f848ef3..48c7b42225 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts @@ -12,7 +12,6 @@ import decodeJPEG2000 from './shared/decoders/decodeJPEG2000'; import decodeJPEGBaseline12Bit from './shared/decoders/decodeJPEGBaseline12Bit-js'; import decodeJPEGLossless from './shared/decoders/decodeJPEGLossless'; import decodeJPEGLS from './shared/decoders/decodeJPEGLS'; -// Note that the scaling is pixel value scaling, which is applying a modality LUT import type { Types as CoreTypes } from '@cornerstonejs/core'; import type { ByteArray } from 'dicom-parser'; import getMinMax from './shared/getMinMax'; @@ -20,6 +19,7 @@ import getPixelDataTypeFromMinMax, { validatePixelDataType, } from './shared/getPixelDataTypeFromMinMax'; import isColorImage from './shared/isColorImage'; +// Note that the scaling is pixel value scaling, which is applying a modality LUT import applyModalityLUT from './shared/scaling/scaleArray'; import type { DICOMLoaderImageOptions, LoaderDecodeOptions } from './types'; From 568464767de9aea7ccd0f09bd1e26544bfc63650 Mon Sep 17 00:00:00 2001 From: James Manners Date: Mon, 30 Jun 2025 13:00:55 +1000 Subject: [PATCH 07/12] fix(dicom-image-loader): Fix typescript compliation --- packages/dicomImageLoader/src/decodeImageFrameWorker.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts index 48c7b42225..285a21edc5 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts @@ -326,7 +326,7 @@ function _validateScalingParameters( } function createDestinationImage( - imageFrame: any, + imageFrame: CoreTypes.IImageFrame, targetBuffer: DICOMLoaderImageOptions['targetBuffer'], TypedArrayConstructor: new (size: number) => CoreTypes.PixelDataTypedArray ) { @@ -340,11 +340,13 @@ function createDestinationImage( rows, columns, frameInfo: { + // @ts-expect-error frameInfo is not defined in IImageFrame ...imageFrame.frameInfo, rows, columns, }, imageInfo: { + // @ts-expect-error imageInfo is not defined in IImageFrame ...imageFrame.imageInfo, rows, columns, From d1c615c52c91b756527b3e6b8d74eb6e38556325 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 16 Oct 2025 13:37:15 -0400 Subject: [PATCH 08/12] fix decode image frame worker load --- packages/dicomImageLoader/src/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dicomImageLoader/src/init.ts b/packages/dicomImageLoader/src/init.ts index fd9e851f2f..28b0be153e 100644 --- a/packages/dicomImageLoader/src/init.ts +++ b/packages/dicomImageLoader/src/init.ts @@ -5,7 +5,7 @@ import { getWebWorkerManager } from '@cornerstonejs/core'; const workerFn = () => { const instance = new Worker( - new URL('./decodeImageFrameWorker.js', import.meta.url), + new URL('./decodeImageFrameWorker.ts', import.meta.url), { type: 'module' } ); return instance; From 0123335237f2c45b705714119547ca28063220f2 Mon Sep 17 00:00:00 2001 From: James Manners Date: Fri, 5 Dec 2025 17:19:18 +1100 Subject: [PATCH 09/12] refactor(dicom-image-loader): --- packages/dicomImageLoader/src/decodeImageFrameWorker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts index 285a21edc5..aaf2ee4e7b 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts @@ -37,7 +37,7 @@ const typedArrayConstructors = { }; type TypedArrayConstructorsMap = typeof typedArrayConstructors; -function postProcessDecodedPixels( +export function postProcessDecodedPixels( imageFrame: CoreTypes.IImageFrame, options: DICOMLoaderImageOptions, start: number, From 31b7908ab9e4c9e258d8ea9fc6ef454b7ea17cc1 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 9 Feb 2026 18:00:48 -0500 Subject: [PATCH 10/12] Fix imports --- packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts index da07055c44..c64f391388 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts @@ -1,11 +1,10 @@ -import openJphFactory from '@cornerstonejs/codec-openjph/wasmjs'; // @ts-ignore import type { ByteArray } from 'dicom-parser'; import openJphFactory, { type HTJ2KDecoder, type HTJ2KModule, } from '@cornerstonejs/codec-openjph/wasmjs'; -// @ts-ignore -// import openjphWasm from '@cornerstonejs/codec-openjph/wasm'; +import LoaderDecodeOptions from '../../types/LoaderDecodeOptions'; + const openjphWasm = new URL( '@cornerstonejs/codec-openjph/wasm', import.meta.url From a48c6bd2d9585f7bdecce5e47d95c15098e49ab1 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 10 Feb 2026 20:26:19 -0500 Subject: [PATCH 11/12] Use peerImport to load wasm modules --- .../core/src/RenderingEngine/WSIViewport.ts | 4 +- packages/core/src/init.ts | 6 +- .../core/src/types/Cornerstone3DConfig.ts | 10 ++- .../dicomImageLoader/docs/Configuration.md | 65 ++++++++++++++ .../examples/htj2kStackBasic/index.ts | 2 +- .../src/decodeImageFrameWorker.ts | 44 ++-------- .../src/shared/createInitializeDecoder.ts | 77 +++++++++++++++++ .../src/shared/decoders/decodeHTJ2K.ts | 86 +++++-------------- .../src/shared/decoders/decodeJPEG2000.ts | 81 ++++------------- .../shared/decoders/decodeJPEGBaseline8Bit.ts | 76 ++++------------ .../src/shared/decoders/decodeJPEGLS.ts | 81 ++++------------- .../src/shared/decoders/decodeJPEGLossless.ts | 19 ++-- .../src/types/LoaderDecodeOptions.ts | 54 +----------- .../src/workers/polySegConverters.js | 17 +--- utils/demo/helpers/initDemo.ts | 11 ++- 15 files changed, 263 insertions(+), 370 deletions(-) create mode 100644 packages/dicomImageLoader/src/shared/createInitializeDecoder.ts diff --git a/packages/core/src/RenderingEngine/WSIViewport.ts b/packages/core/src/RenderingEngine/WSIViewport.ts index 13a875f105..616017d9e1 100644 --- a/packages/core/src/RenderingEngine/WSIViewport.ts +++ b/packages/core/src/RenderingEngine/WSIViewport.ts @@ -507,7 +507,9 @@ class WSIViewport extends Viewport { * as a globalThis.browserImportFunction taking the package name, and a set * of options defining how to get the value out of the package. */ - public static getDicomMicroscopyViewer = async () => { + // dicom-microscopy-viewer is a peer dependency with no types; use any for its API + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static getDicomMicroscopyViewer = async (): Promise => { return peerImport('dicom-microscopy-viewer'); }; diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 866ec8a8be..cfbe449c48 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -50,7 +50,7 @@ const defaultConfig: Cornerstone3DConfig = { * This may just fallback to the default import, but many packaging * systems don't deal with peer imports properly. */ - peerImport: (moduleId) => null, + peerImport: (_moduleId, fallback) => fallback?.() ?? null, }; let config: Cornerstone3DConfig = { @@ -270,8 +270,8 @@ function getWebWorkerManager() { return webWorkerManager; } -async function peerImport(moduleId: string) { - return config.peerImport(moduleId); +function peerImport(moduleId: string, fallback?) { + return config.peerImport(moduleId, fallback); } export { diff --git a/packages/core/src/types/Cornerstone3DConfig.ts b/packages/core/src/types/Cornerstone3DConfig.ts index 8ee0892b95..00d7af0e39 100644 --- a/packages/core/src/types/Cornerstone3DConfig.ts +++ b/packages/core/src/types/Cornerstone3DConfig.ts @@ -71,9 +71,15 @@ interface Cornerstone3DConfig { * This function returns an imported module for the given module id. * It allows replacing broken packing system imports with external importers * that perform lazy imports. + * When the config does not handle the moduleId, it may call the optional + * fallback (when provided) and return its result. */ - // eslint-disable-next-line - peerImport?: (moduleId: string) => Promise; + /* eslint-disable @typescript-eslint/no-explicit-any -- peerImport is dynamic; module/return type varies by caller */ + peerImport?: ( + moduleId: string, + fallback?: () => Promise + ) => Promise; + /* eslint-enable @typescript-eslint/no-explicit-any */ } export type { Cornerstone3DConfig as default }; diff --git a/packages/dicomImageLoader/docs/Configuration.md b/packages/dicomImageLoader/docs/Configuration.md index 992947859f..a9bfda5121 100644 --- a/packages/dicomImageLoader/docs/Configuration.md +++ b/packages/dicomImageLoader/docs/Configuration.md @@ -24,3 +24,68 @@ Where options can have the following properties: - imageCreated - Callback allowing modification of newly created image objects. - decodeConfig - The configuration for the decoder - strict - Whether strict mode for image decoding is on. + +## Loading codecs and WASM via peerImport + +WASM-based decoders (JPEG-LS, JPEG 2000, HTJ2K, JPEG Baseline 8-bit) load two things at runtime: + +1. **The codec JS module** – the Emscripten-generated glue that compiles and instantiates the WASM (e.g. `@cornerstonejs/codec-charls/decodewasmjs`). +2. **The WASM binary** – the actual `.wasm` file (e.g. `@cornerstonejs/codec-charls/decodewasm`). + +Both are resolved through **peerImport**, which is provided by the host application when it initializes Cornerstone (e.g. `cornerstone.init({ peerImport })`). The DICOM Image Loader calls `peerImport(moduleId, fallback)`: + +- **Codec module:** `peerImport(libraryId, libraryFallback)` – the loader expects the returned promise to resolve to an object with a `default` function (the Emscripten factory). If the host does not handle that `libraryId`, the `libraryFallback` is used (typically a static `import(...)` so the bundler can resolve the default path). +- **WASM URL:** `peerImport(wasmId, () => ({ default: wasmDefaultUrl }))` – the loader expects the returned promise to resolve to an object with a `default` string: the URL of the `.wasm` file. That URL is passed to the codec’s `locateFile` so the Emscripten loader can fetch the binary. If the host does not handle that `wasmId`, the fallback returns the default URL (derived from the package at build time). + +This design allows the host to: + +- Use different bundling or loading (e.g. load the codec from a CDN or a different path). +- Serve the WASM from a different origin or path (e.g. same CDN, or a custom asset server) without changing the DICOM Image Loader or codec packages. + +### Registering a custom peerImport + +`peerImport` is configured on **Cornerstone (core)**, not on the DICOM Image Loader. Pass it when you call `init()`: + +```js +import { init } from '@cornerstonejs/core'; + +async function myPeerImport(moduleId, fallback) { + // Override WASM URLs for a specific environment (e.g. CDN) + if (moduleId === '@cornerstonejs/codec-charls/decodewasm') { + return { + default: 'https://my-cdn.example.com/assets/codec-charls-decodewasm.wasm', + }; + } + if (moduleId === '@cornerstonejs/codec-openjph/wasm') { + return { + default: 'https://my-cdn.example.com/assets/codec-openjph-wasm.wasm', + }; + } + // Optional: override the JS module as well + // if (moduleId === '@cornerstonejs/codec-charls/decodewasmjs') { + // return import('https://my-cdn.example.com/codec-charls-decodewasmjs.js'); + // } + // Let the default (bundled) resolution take over + if (fallback) return fallback(); + return null; +} + +await init({ + peerImport: myPeerImport, + // ...other cornerstone config +}); +``` + +### Module IDs used by the DICOM Image Loader + +| Codec | Library (JS) module ID | WASM module ID | +| ------------------- | ------------------------------------------------------ | ---------------------------------------------------- | +| JPEG-LS | `@cornerstonejs/codec-charls/decodewasmjs` | `@cornerstonejs/codec-charls/decodewasm` | +| JPEG 2000 | `@cornerstonejs/codec-openjpeg/decodewasmjs` | `@cornerstonejs/codec-openjpeg/decodewasm` | +| HTJ2K | `@cornerstonejs/codec-openjph/wasmjs` | `@cornerstonejs/codec-openjph/wasm` | +| JPEG Baseline 8-bit | `@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs` | `@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm` | + +Return values: + +- For **library** IDs: resolve with the module object (e.g. `{ default: factoryFunction }`). +- For **WASM** IDs: resolve with `{ default: string }` where the string is the absolute URL of the `.wasm` file. diff --git a/packages/dicomImageLoader/examples/htj2kStackBasic/index.ts b/packages/dicomImageLoader/examples/htj2kStackBasic/index.ts index e0388817b6..67e7f11ad2 100644 --- a/packages/dicomImageLoader/examples/htj2kStackBasic/index.ts +++ b/packages/dicomImageLoader/examples/htj2kStackBasic/index.ts @@ -202,7 +202,7 @@ async function run() { SeriesInstanceUID: '1.3.6.1.4.1.9590.100.1.2.160160590111755920740089886004263812825', wadoRsRoot: - getLocalUrl() || 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', }); // Instantiate a rendering engine diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts index aaf2ee4e7b..08a52c5e9c 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.ts +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts @@ -415,19 +415,10 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeJPEGBaseline8Bit( - pixelData, - opts, - decodeConfig?.wasmUrlCodecLibJpegTurbo8bit - ); + decodePromise = decodeJPEGBaseline8Bit(pixelData, opts); break; case '1.2.840.10008.1.2.4.51': // JPEG Baseline lossy process 2 & 4 (12 bit) - // opts = { - // ...imageFrame, - // }; - // decodePromise = decodeJPEGBaseline12Bit(pixelData, opts); - //throw new Error('Currently unsupported: 1.2.840.10008.1.2.4.51'); decodePromise = decodeJPEGBaseline12Bit(imageFrame, pixelData); break; case '1.2.840.10008.1.2.4.57': @@ -447,11 +438,7 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeJPEGLS( - pixelData, - opts, - decodeConfig?.wasmUrlCodecCharls - ); + decodePromise = decodeJPEGLS(pixelData, opts); break; case '1.2.840.10008.1.2.4.81': // JPEG-LS Lossy (Near-Lossless) Image Compression @@ -462,11 +449,7 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeJPEGLS( - pixelData, - opts, - decodeConfig?.wasmUrlCodecCharls - ); + decodePromise = decodeJPEGLS(pixelData, opts); break; case '1.2.840.10008.1.2.4.90': opts = { @@ -474,12 +457,7 @@ export async function decodeImageFrame( }; // JPEG 2000 Lossless - // imageFrame, pixelData, decodeConfig, options - decodePromise = decodeJPEG2000( - pixelData, - opts, - decodeConfig?.wasmUrlCodecOpenJpeg - ); + decodePromise = decodeJPEG2000(pixelData, opts); break; case '1.2.840.10008.1.2.4.91': // JPEG 2000 Lossy @@ -487,13 +465,7 @@ export async function decodeImageFrame( ...imageFrame, }; - // JPEG 2000 Lossy - // imageFrame, pixelData, decodeConfig, options - decodePromise = decodeJPEG2000( - pixelData, - opts, - decodeConfig?.wasmUrlCodecOpenJpeg - ); + decodePromise = decodeJPEG2000(pixelData, opts); break; case '3.2.840.10008.1.2.4.96': case '1.2.840.10008.1.2.4.201': @@ -504,11 +476,7 @@ export async function decodeImageFrame( ...imageFrame, }; - decodePromise = decodeHTJ2K( - pixelData, - opts, - decodeConfig?.wasmUrlCodecOpenJph - ); + decodePromise = decodeHTJ2K(pixelData, opts); break; default: throw new Error(`no decoder for transfer syntax ${transferSyntax}`); diff --git a/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts b/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts new file mode 100644 index 0000000000..791b5f693c --- /dev/null +++ b/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts @@ -0,0 +1,77 @@ +import { peerImport } from '@cornerstonejs/core'; +import type { WebWorkerDecodeConfig } from '../types'; + +export interface CreateInitializeDecoderOptions { + /** Peer import id for the WASM JS loader (e.g. '@cornerstonejs/codec-charls/decodewasmjs'). */ + library: string; + /** Fallback when no peer provides the library; typically () => import('...decodewasmjs'). */ + libraryFallback: () => Promise<{ + default: (opts?: object) => Promise; + }>; + /** Peer import id for the WASM binary (e.g. '@cornerstonejs/codec-charls/decodewasm'). */ + wasm: string; + /** Default WASM URL for the bundler; pass e.g. new URL('...decodewasm', import.meta.url).toString(). */ + wasmDefaultUrl: string; + /** Name of the decoder class on the loaded module (e.g. 'JpegLSDecoder'). */ + constructor: string; +} + +export interface InitializeDecoderState { + codec: TModule; + decoder: TDecoder; + decodeConfig: WebWorkerDecodeConfig; +} + +export type InitializeDecoderFn = ( + decodeConfig?: WebWorkerDecodeConfig +) => Promise; + +/** + * Creates an initialize function and shared state for a WASM decoder that uses + * locateFile (Emscripten-style). The returned initialize loads the JS module via + * peerImport(library), resolves the WASM URL via peerImport(wasm) with wasmDefaultUrl + * as fallback, and instantiates the decoder via `new codec[constructor]()`. + * + * Use the returned `state` as the module's local ref for codec, decoder, and + * decodeConfig (e.g. `const local = state`). + */ +export function createInitializeDecoder( + opts: CreateInitializeDecoderOptions +): { + initialize: InitializeDecoderFn; + state: InitializeDecoderState; +} { + const state: InitializeDecoderState = { + codec: undefined as TModule, + decoder: undefined as TDecoder, + decodeConfig: {} as WebWorkerDecodeConfig, + }; + + const initialize: InitializeDecoderFn = async (decodeConfig) => { + state.decodeConfig = decodeConfig ?? state.decodeConfig; + + if (state.codec) { + return; + } + + const mod = await peerImport(opts.library, opts.libraryFallback); + const wasmModule = await peerImport(opts.wasm, () => ({ + default: opts.wasmDefaultUrl, + })); + const wasmUrl = wasmModule?.default; + const locateFileOpts = { + locateFile: (file: string) => + file.endsWith('.wasm') ? wasmUrl : undefined, + }; + const codec = (await ( + mod as { default: (initOpts?: object) => Promise } + ).default(locateFileOpts)) as TModule; + + state.codec = codec; + state.decoder = new (codec as Record unknown>)[ + opts.constructor + ]() as TDecoder; + }; + + return { initialize, state }; +} diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts index c64f391388..b8d0726424 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts @@ -1,25 +1,28 @@ import type { ByteArray } from 'dicom-parser'; -import openJphFactory, { - type HTJ2KDecoder, - type HTJ2KModule, +import type { + HTJ2KDecoder, + HTJ2KModule, } from '@cornerstonejs/codec-openjph/wasmjs'; -import LoaderDecodeOptions from '../../types/LoaderDecodeOptions'; - -const openjphWasm = new URL( - '@cornerstonejs/codec-openjph/wasm', - import.meta.url -); - -const local: { - codec: HTJ2KModule | undefined; - decoder: HTJ2KDecoder | undefined; - decodeConfig: LoaderDecodeOptions; -} = { - codec: undefined, - decoder: undefined, - decodeConfig: {}, +import { createInitializeDecoder } from '../createInitializeDecoder'; + +const { initialize, state } = createInitializeDecoder({ + library: '@cornerstonejs/codec-openjph/wasmjs', + libraryFallback: () => import('@cornerstonejs/codec-openjph/wasmjs'), + wasm: '@cornerstonejs/codec-openjph/wasm', + wasmDefaultUrl: new URL( + '@cornerstonejs/codec-openjph/wasm', + import.meta.url + ).toString(), + constructor: 'HTJ2KDecoder', +}); +const local = state as { + codec: HTJ2KModule; + decoder: HTJ2KDecoder; + decodeConfig: typeof state.decodeConfig; }; +export { initialize }; + function calculateSizeAtDecompositionLevel( decompositionLevel: number, frameWidth: number, @@ -34,52 +37,9 @@ function calculateSizeAtDecompositionLevel( return result; } -/** - * - * @param decodeConfig - * @param wasmUrlCodecOpenJph Optional path to load the OpenJPH WASM codec from. - * If not given, it will default to using the default `openjphWasm` - * @returns - */ -export function initialize( - decodeConfig?: LoaderDecodeOptions, - wasmUrlCodecOpenJph?: string -): Promise { - local.decodeConfig = decodeConfig; - - if (local.codec) { - return Promise.resolve(); - } - - const openJphModule = openJphFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - if (wasmUrlCodecOpenJph) { - return wasmUrlCodecOpenJph; - } - return openjphWasm.toString(); - } - - return f; - }, - }); - - return new Promise((resolve, reject) => { - openJphModule.then((instance) => { - local.codec = instance; - local.decoder = new instance.HTJ2KDecoder(); - resolve(); - }, reject); - }); -} - // https://github.com/chafey/openjpegjs/blob/master/test/browser/index.html -async function decodeAsync( - compressedImageFrame: ByteArray, - imageInfo, - wasmUrlCodecOpenJph?: string -) { - await initialize(undefined, wasmUrlCodecOpenJph); +async function decodeAsync(compressedImageFrame: ByteArray, imageInfo) { + await initialize(); // const decoder = local.decoder; // This is much slower for some reason const decoder = new local.codec.HTJ2KDecoder(); diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts index 9aea8d5050..6a70b1687b 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts @@ -1,82 +1,35 @@ // https://emscripten.org/docs/api_reference/module.html -// @ts-ignore -import openJpegFactory from '@cornerstonejs/codec-openjpeg/decodewasmjs'; import type { J2KDecoder, OpenJpegModule, } from '@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode'; import type { Types } from '@cornerstonejs/core'; -import type { WebWorkerDecodeConfig } from '../../types'; - -// Webpack asset/resource copies this to our output folder - -/** - * Default URL to load the OpenJPH codec from. - * - * In order for this to be loaded correctly, you will need to configure your - * bundler to treat `.wasm` files as an asset/resource. - */ -const openjpegWasm = new URL( - '@cornerstonejs/codec-openjpeg/decodewasm', - import.meta.url -); - -const local: { +import { createInitializeDecoder } from '../createInitializeDecoder'; + +const { initialize, state } = createInitializeDecoder({ + library: '@cornerstonejs/codec-openjpeg/decodewasmjs', + libraryFallback: () => import('@cornerstonejs/codec-openjpeg/decodewasmjs'), + wasm: '@cornerstonejs/codec-openjpeg/decodewasm', + wasmDefaultUrl: new URL( + '@cornerstonejs/codec-openjpeg/decodewasm', + import.meta.url + ).toString(), + constructor: 'J2KDecoder', +}); +const local = state as { codec: OpenJpegModule; decoder: J2KDecoder; - decodeConfig: WebWorkerDecodeConfig; -} = { - codec: undefined, - decoder: undefined, - decodeConfig: {} as WebWorkerDecodeConfig, + decodeConfig: typeof state.decodeConfig; }; -/** - * - * @param decodeConfig - * @param wasmUrlCodecOpenJpeg - Optional path to load the OpenJpeg WASM codec from. - * If not given, it will default to using the default `openjpegWasm` URL. - * @returns - */ -export function initialize( - decodeConfig?: WebWorkerDecodeConfig, - wasmUrlCodecOpenJpeg?: string -): Promise { - local.decodeConfig = decodeConfig; - - if (local.codec) { - return Promise.resolve(); - } - - const openJpegModule = openJpegFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - if (wasmUrlCodecOpenJpeg) { - return wasmUrlCodecOpenJpeg; - } - return openjpegWasm.toString(); - } - - return f; - }, - }); - - return new Promise((resolve, reject) => { - openJpegModule.then((instance) => { - local.codec = instance; - local.decoder = new instance.J2KDecoder(); - resolve(); - }, reject); - }); -} +export { initialize }; // https://github.com/chafey/openjpegjs/blob/master/test/browser/index.html async function decodeAsync( compressedImageFrame, - imageInfo, - wasmUrlCodecOpenJpeg?: string + imageInfo ): Promise { - await initialize(undefined, wasmUrlCodecOpenJpeg); + await initialize(); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts index a0eb4c0531..613f58e332 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts @@ -1,69 +1,28 @@ -// @ts-ignore -import libjpegTurboFactory from '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs'; import type { LibJpegTurbo8Bit, OpenJpegModule, } from '@cornerstonejs/codec-libjpeg-turbo-8bit/dist/libjpegturbowasm_decode'; import type { Types } from '@cornerstonejs/core'; +import { createInitializeDecoder } from '../createInitializeDecoder'; import type { ByteArray } from 'dicom-parser'; -/** - * Default URL to load the LibJpeg Turbo 8bit codec from. - * - * In order for this to be loaded correctly, you will need to configure your - * bundler to treat `.wasm` files as an asset/resource. - */ -const libjpegTurboWasm = new URL( - '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', - import.meta.url -); - -const local: { +const { initialize, state } = createInitializeDecoder({ + library: '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs', + libraryFallback: () => + import('@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs'), + wasm: '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', + wasmDefaultUrl: new URL( + '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', + import.meta.url + ).toString(), + constructor: 'JPEGDecoder', +}); +const local = state as { codec: OpenJpegModule; decoder: LibJpegTurbo8Bit; -} = { - codec: undefined, - decoder: undefined, + decodeConfig: typeof state.decodeConfig; }; -/** - * - * @param [wasmUrlCodecLibjpegTurbo8bit] - Optional URL for the codec WASM file. - * If not provided, it will default to the `libjpegTurboWasm` URL. - * @returns - */ -function initLibjpegTurbo( - wasmUrlCodecLibjpegTurbo8bit?: string -): Promise { - if (local.codec) { - return Promise.resolve(); - } - - const libjpegTurboModule = libjpegTurboFactory({ - locateFile: (f: string) => { - if (f.endsWith('.wasm')) { - /** - * If a custom URL is provided, use that instead of the default one. - */ - if (wasmUrlCodecLibjpegTurbo8bit) { - return wasmUrlCodecLibjpegTurbo8bit; - } - return libjpegTurboWasm.toString(); - } - - return f; - }, - }); - - return new Promise((resolve, reject) => { - libjpegTurboModule.then((instance) => { - local.codec = instance; - local.decoder = new instance.JPEGDecoder(); - resolve(); - }, reject); - }); -} - // imageFrame.pixelRepresentation === 1 <-- Signed /** * @@ -73,10 +32,9 @@ function initLibjpegTurbo( */ async function decodeAsync( compressedImageFrame, - imageInfo, - wasmUrlCodecLibjpegTurbo8bit?: string + imageInfo ): Promise { - await initLibjpegTurbo(wasmUrlCodecLibjpegTurbo8bit); + await initialize(); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory @@ -138,8 +96,6 @@ function getPixelData(frameInfo, decodedBuffer: ByteArray) { ); } -const initialize = initLibjpegTurbo; - export { initialize }; export default decodeAsync; diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts index 9d7c4729ff..05550937da 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts @@ -1,79 +1,35 @@ -// @ts-ignore -import charlsFactory from '@cornerstonejs/codec-charls/decodewasmjs'; import type { CharlsModule, JpegLSDecoder, } from '@cornerstonejs/codec-charls/dist/charlswasm_decode'; import type { Types } from '@cornerstonejs/core'; import type { ByteArray } from 'dicom-parser'; -import type { WebWorkerDecodeConfig } from '../../types'; - -/** - * Default URL to load the Charls codec from. - * - * In order for this to be loaded correctly, you will need to configure your - * bundler to treat `.wasm` files as an asset/resource. - */ -const charlsWasm = new URL( - '@cornerstonejs/codec-charls/decodewasm', - import.meta.url -); - -const local: { +import { createInitializeDecoder } from '../createInitializeDecoder'; + +const { initialize, state } = createInitializeDecoder({ + library: '@cornerstonejs/codec-charls/decodewasmjs', + libraryFallback: () => import('@cornerstonejs/codec-charls/decodewasmjs'), + wasm: '@cornerstonejs/codec-charls/decodewasm', + wasmDefaultUrl: new URL( + '@cornerstonejs/codec-charls/decodewasm', + import.meta.url + ).toString(), + constructor: 'JpegLSDecoder', +}); +const local = state as { codec: CharlsModule; decoder: JpegLSDecoder; - decodeConfig: WebWorkerDecodeConfig; -} = { - codec: undefined, - decoder: undefined, - decodeConfig: {} as WebWorkerDecodeConfig, + decodeConfig: typeof state.decodeConfig; }; +export { initialize }; + function getExceptionMessage(exception) { return typeof exception === 'number' ? local.codec.getExceptionMessage(exception) : exception; } -/** - * - * @param decodeConfig - * @param wasmUrlCodecCharls Optional - a path/url where to load the charls wasm - * file from. If not given, it will default to using the default `charlsWasm` URL. - * @returns - */ -export function initialize( - decodeConfig?: WebWorkerDecodeConfig, - wasmUrlCodecCharls?: string -): Promise { - local.decodeConfig = decodeConfig; - - if (local.codec) { - return Promise.resolve(); - } - - const charlsModule = charlsFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - if (wasmUrlCodecCharls) { - return wasmUrlCodecCharls; - } - return charlsWasm.toString(); - } - - return f; - }, - }); - - return new Promise((resolve, reject) => { - charlsModule.then((instance) => { - local.codec = instance; - local.decoder = new instance.JpegLSDecoder(); - resolve(); - }, reject); - }); -} - /** * * @param {*} compressedImageFrame @@ -82,11 +38,10 @@ export function initialize( */ async function decodeAsync( compressedImageFrame, - imageInfo, - wasmUrlCodecCharls?: string + imageInfo ): Promise { try { - await initialize(undefined, wasmUrlCodecCharls); + await initialize(); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts index 69fdc88a68..d516a01710 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLossless.ts @@ -1,5 +1,6 @@ import type { ByteArray } from 'dicom-parser'; import type { Types } from '@cornerstonejs/core'; +import { peerImport } from '@cornerstonejs/core'; import type { WebWorkerDecodeConfig } from '../../types'; const local = { @@ -7,21 +8,23 @@ const local = { decodeConfig: {} as WebWorkerDecodeConfig, }; -export function initialize( +export async function initialize( decodeConfig?: WebWorkerDecodeConfig ): Promise { local.decodeConfig = decodeConfig; if (local.DecoderClass) { - return Promise.resolve(); + return; } - return new Promise((resolve, reject) => { - import('jpeg-lossless-decoder-js').then(({ Decoder }) => { - local.DecoderClass = Decoder; - resolve(); - }, reject); - }); + const mod = await peerImport( + 'jpeg-lossless-decoder-js', + () => import('jpeg-lossless-decoder-js') + ); + if (!mod?.Decoder) { + throw new Error('Failed to load jpeg-lossless-decoder-js'); + } + local.DecoderClass = mod.Decoder; } async function decodeJPEGLossless( diff --git a/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts b/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts index 7ab5123484..4c5523e470 100644 --- a/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts +++ b/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts @@ -1,55 +1,3 @@ export interface LoaderDecodeOptions { - // whatever - /** - * Specifies the url/path to the codec wasm files. If specified, the wasm - * blobs will be fetched from these paths rather than the default. - * - * By default, the codec wasm files are included in the end build by your - * bundler (webpack, vite etc). - * - * Rather than using a bundler to identify the assets, it is possible to pass - * in paths to where the wasm files should be loaded from. For instance, in - * your build step extract the following wasm files and include them in your - * static assets directory. Then set the the correct paths to where - * cornerstone should load the files. - * - * This should point to the export `@cornerstonejs/codec-charls/decodewasm` - * that resolves to `@cornerstonejs/codec-charls/dist/charlswasm_decode.wasm`. - * Copy this file to your static assets directory and set the path to it. - * - * @example `wasmUrlCodecCharls: '/static/charlswasm_decode.wasm'` - */ - wasmUrlCodecCharls?: string; - /** - * Manually set the path/url to load `openjphjs.wasm`. See the notes above - * for detailed notes. - * - * This should point to the export `@cornerstonejs/codec-openjph/wasm` which - * resolves to - * `node_modules/@cornerstonejs/codec-openjph/dist/openjphjs.wasm`. - * - * @example `wasmUrlCodecOpenJph: '/static/openjphjs.wasm'` - */ - wasmUrlCodecOpenJph?: string; - /** - * Manually set the path/url to load `openjpegwasm_decode.wasm`. See the - * notes above for detailed notes. - * - * This should point to the `@cornerstonejs/codec-openjpeg/decodewasm` which - * resolves to `@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode.wasm` - * - * @example `wasmUrlCodecOpenJph: '/static/openjpegwasm_decode.wasm'` - */ - wasmUrlCodecOpenJpeg?: string; - /** - * Manually set the path/url to load `libjpegturbowasm_decode.wasm`. See the - * notes above for detailed notes. - * - * This should point to the export - * `@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm` which resolves to - * `@cornerstonejs/codec-libjpeg-turbo-8bit/dist/libjpegturbowasm_decode.wasm` - * - * @example `wasmUrlCodecOpenJph: '/static/libjpegturbowasm_decode.wasm'` - */ - wasmUrlCodecLibJpegTurbo8bit?: string; + // WASM codecs are loaded via peerImport with fallback; no path options. } diff --git a/packages/polymorphic-segmentation/src/workers/polySegConverters.js b/packages/polymorphic-segmentation/src/workers/polySegConverters.js index f8558051d7..d178acca84 100644 --- a/packages/polymorphic-segmentation/src/workers/polySegConverters.js +++ b/packages/polymorphic-segmentation/src/workers/polySegConverters.js @@ -1,5 +1,5 @@ import { expose } from 'comlink'; -import { utilities } from '@cornerstonejs/core'; +import { utilities, peerImport } from '@cornerstonejs/core'; import { utilities as ToolsUtilities } from '@cornerstonejs/tools'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; @@ -17,17 +17,6 @@ const { planar: { isPlaneIntersectingAABB }, } = ToolsUtilities; -async function peerImport(moduleId) { - try { - if (moduleId === '@icr/polyseg-wasm') { - return import('@icr/polyseg-wasm'); - } - } catch (error) { - console.warn('Error importing module:', error); - return null; - } -} - /** * Object containing methods for converting between different representations of * segmentations (e.g., contour, labelmap, surface, etc.) These logics @@ -55,7 +44,9 @@ const polySegConverters = { async initializePolySeg(progressCallback) { let ICRPolySeg; try { - ICRPolySeg = (await peerImport('@icr/polyseg-wasm')).default; + ICRPolySeg = ( + await peerImport('@icr/polyseg-wasm', () => import('@icr/polyseg-wasm')) + ).default; } catch (error) { console.error(error); console.debug( diff --git a/utils/demo/helpers/initDemo.ts b/utils/demo/helpers/initDemo.ts index 2e6f123764..d4d6acadd5 100644 --- a/utils/demo/helpers/initDemo.ts +++ b/utils/demo/helpers/initDemo.ts @@ -59,8 +59,13 @@ export default async function initDemo(config: any = {}) { * This is one example of how to import peer modules that works with webpack * It in fact just uses the default import from the browser, so it should work * on any standards compliant ecmascript environment. + * When this handler does not support the moduleId, it may call the optional + * fallback (second argument) and return its result. */ -export async function peerImport(moduleId) { +export async function peerImport( + moduleId: string, + fallback?: () => Promise +): Promise { if (moduleId === 'dicom-microscopy-viewer') { // The microscopy viewer loads relative to the public URL window.PUBLIC_URL ||= '/'; @@ -71,6 +76,10 @@ export async function peerImport(moduleId) { 'dicomMicroscopyViewer' ); } + if (fallback) { + return fallback(); + } + return null; } async function importGlobal(path, globalName) { From 45d2758c52f899a597bc12a69bf12a7e93d454c6 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 2 Mar 2026 20:56:50 -0500 Subject: [PATCH 12/12] fix: Wasm load path --- .../src/shared/createInitializeDecoder.ts | 68 +++++++++++++++---- .../src/shared/decoders/decodeHTJ2K.ts | 4 +- .../src/shared/decoders/decodeJPEG2000.ts | 4 +- .../shared/decoders/decodeJPEGBaseline8Bit.ts | 5 +- .../src/shared/decoders/decodeJPEGLS.ts | 4 +- 5 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts b/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts index 791b5f693c..ad9931af3c 100644 --- a/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts +++ b/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts @@ -1,13 +1,25 @@ import { peerImport } from '@cornerstonejs/core'; import type { WebWorkerDecodeConfig } from '../types'; -export interface CreateInitializeDecoderOptions { - /** Peer import id for the WASM JS loader (e.g. '@cornerstonejs/codec-charls/decodewasmjs'). */ - library: string; - /** Fallback when no peer provides the library; typically () => import('...decodewasmjs'). */ - libraryFallback: () => Promise<{ - default: (opts?: object) => Promise; - }>; +export interface CreateInitializeDecoderOptions { + /** + * The already-imported codec library. + * + * This can be either: + * - A WASM JS loader module that exposes `default(initOpts) => Promise` (Emscripten-style), or + * - The initialized codec module itself (TModule). + */ + library?: unknown; + /** + * Peer import id for the WASM JS loader, used only when `library` is not provided + * (e.g. '@cornerstonejs/codec-charls/decodewasmjs'). + */ + libraryName?: string; + /** + * Fallback when no peer provides the library; typically `() => import('...decodewasmjs')`. + * Only used when `library` is not provided. + */ + libraryFallback?: () => Promise; /** Peer import id for the WASM binary (e.g. '@cornerstonejs/codec-charls/decodewasm'). */ wasm: string; /** Default WASM URL for the bundler; pass e.g. new URL('...decodewasm', import.meta.url).toString(). */ @@ -29,14 +41,15 @@ export type InitializeDecoderFn = ( /** * Creates an initialize function and shared state for a WASM decoder that uses * locateFile (Emscripten-style). The returned initialize loads the JS module via - * peerImport(library), resolves the WASM URL via peerImport(wasm) with wasmDefaultUrl - * as fallback, and instantiates the decoder via `new codec[constructor]()`. + * either a provided `library` module or via peerImport(libraryName, libraryFallback), + * resolves the WASM URL via peerImport(wasm) with wasmDefaultUrl as fallback, and + * instantiates the decoder via `new codec[constructor]()`. * * Use the returned `state` as the module's local ref for codec, decoder, and * decodeConfig (e.g. `const local = state`). */ export function createInitializeDecoder( - opts: CreateInitializeDecoderOptions + opts: CreateInitializeDecoderOptions ): { initialize: InitializeDecoderFn; state: InitializeDecoderState; @@ -54,7 +67,16 @@ export function createInitializeDecoder( return; } - const mod = await peerImport(opts.library, opts.libraryFallback); + const lib = + opts.library ?? + (opts.libraryName + ? await peerImport(opts.libraryName, opts.libraryFallback) + : await opts.libraryFallback?.()); + if (!lib) { + throw new Error( + 'createInitializeDecoder: no codec library provided. Pass `library`, or provide `libraryName` and `libraryFallback`.' + ); + } const wasmModule = await peerImport(opts.wasm, () => ({ default: opts.wasmDefaultUrl, })); @@ -63,9 +85,27 @@ export function createInitializeDecoder( locateFile: (file: string) => file.endsWith('.wasm') ? wasmUrl : undefined, }; - const codec = (await ( - mod as { default: (initOpts?: object) => Promise } - ).default(locateFileOpts)) as TModule; + const codec = await (async () => { + // Accept either an init-wrapper module (module.default(initOpts)) or an already-initialized codec module. + if ( + typeof lib === 'object' && + lib !== null && + 'default' in lib && + typeof (lib as { default?: unknown }).default === 'function' + ) { + return (await ( + lib as { default: (initOpts?: object) => Promise } + ).default(locateFileOpts)) as TModule; + } + + if (typeof lib === 'function') { + return (await (lib as (initOpts?: object) => Promise)( + locateFileOpts + )) as TModule; + } + + return lib as TModule; + })(); state.codec = codec; state.decoder = new (codec as Record unknown>)[ diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts index b8d0726424..58f1268cbc 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts @@ -3,11 +3,11 @@ import type { HTJ2KDecoder, HTJ2KModule, } from '@cornerstonejs/codec-openjph/wasmjs'; +import * as openJphWasmJs from '@cornerstonejs/codec-openjph/wasmjs'; import { createInitializeDecoder } from '../createInitializeDecoder'; const { initialize, state } = createInitializeDecoder({ - library: '@cornerstonejs/codec-openjph/wasmjs', - libraryFallback: () => import('@cornerstonejs/codec-openjph/wasmjs'), + library: openJphWasmJs, wasm: '@cornerstonejs/codec-openjph/wasm', wasmDefaultUrl: new URL( '@cornerstonejs/codec-openjph/wasm', diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts index 6a70b1687b..99d973c19c 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts @@ -3,12 +3,12 @@ import type { J2KDecoder, OpenJpegModule, } from '@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode'; +import * as openJpegWasmJs from '@cornerstonejs/codec-openjpeg/decodewasmjs'; import type { Types } from '@cornerstonejs/core'; import { createInitializeDecoder } from '../createInitializeDecoder'; const { initialize, state } = createInitializeDecoder({ - library: '@cornerstonejs/codec-openjpeg/decodewasmjs', - libraryFallback: () => import('@cornerstonejs/codec-openjpeg/decodewasmjs'), + library: openJpegWasmJs, wasm: '@cornerstonejs/codec-openjpeg/decodewasm', wasmDefaultUrl: new URL( '@cornerstonejs/codec-openjpeg/decodewasm', diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts index 613f58e332..8ff983535f 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts @@ -2,14 +2,13 @@ import type { LibJpegTurbo8Bit, OpenJpegModule, } from '@cornerstonejs/codec-libjpeg-turbo-8bit/dist/libjpegturbowasm_decode'; +import * as libJpegTurboWasmJs from '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs'; import type { Types } from '@cornerstonejs/core'; import { createInitializeDecoder } from '../createInitializeDecoder'; import type { ByteArray } from 'dicom-parser'; const { initialize, state } = createInitializeDecoder({ - library: '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs', - libraryFallback: () => - import('@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasmjs'), + library: libJpegTurboWasmJs, wasm: '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', wasmDefaultUrl: new URL( '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts index 05550937da..105f39837f 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts @@ -2,13 +2,13 @@ import type { CharlsModule, JpegLSDecoder, } from '@cornerstonejs/codec-charls/dist/charlswasm_decode'; +import * as charlsWasmJs from '@cornerstonejs/codec-charls/decodewasmjs'; import type { Types } from '@cornerstonejs/core'; import type { ByteArray } from 'dicom-parser'; import { createInitializeDecoder } from '../createInitializeDecoder'; const { initialize, state } = createInitializeDecoder({ - library: '@cornerstonejs/codec-charls/decodewasmjs', - libraryFallback: () => import('@cornerstonejs/codec-charls/decodewasmjs'), + library: charlsWasmJs, wasm: '@cornerstonejs/codec-charls/decodewasm', wasmDefaultUrl: new URL( '@cornerstonejs/codec-charls/decodewasm',