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.js b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts similarity index 86% rename from packages/dicomImageLoader/src/decodeImageFrameWorker.js rename to packages/dicomImageLoader/src/decodeImageFrameWorker.ts index ea41912fff..08a52c5e9c 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.js +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.ts @@ -1,25 +1,27 @@ /* 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'; +// 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'; const imageUtils = { bilinear, @@ -33,13 +35,14 @@ const typedArrayConstructors = { Float32Array, Uint32Array, }; +type TypedArrayConstructorsMap = typeof typedArrayConstructors; export function postProcessDecodedPixels( - imageFrame, - options, - start, - decodeConfig -) { + imageFrame: CoreTypes.IImageFrame, + options: DICOMLoaderImageOptions, + start: number, + decodeConfig: LoaderDecodeOptions +): CoreTypes.IImageFrame { const shouldShift = imageFrame.pixelRepresentation !== undefined && imageFrame.pixelRepresentation === 1; @@ -170,7 +173,10 @@ export function postProcessDecodedPixels( return imageFrame; } -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; @@ -184,11 +190,11 @@ function _isRequiredScaling(scalingParameters) { } function _handleTargetBuffer( - options, - imageFrame, - typedArrayConstructors, - pixelDataArray -) { + options: DICOMLoaderImageOptions, + imageFrame: CoreTypes.IImageFrame, + typedArrayConstructors: TypedArrayConstructorsMap, + pixelDataArray: CoreTypes.PixelDataTypedArray +): CoreTypes.PixelDataTypedArray { const { arrayBuffer, type, @@ -236,10 +242,10 @@ function _handleTargetBuffer( } function _handlePreScaleSetup( - options, - minBeforeScale, - maxBeforeScale, - imageFrame + options: DICOMLoaderImageOptions, + minBeforeScale: number, + maxBeforeScale: number, + imageFrame: CoreTypes.IImageFrame ) { const scalingParameters = options.preScale.scalingParameters; _validateScalingParameters(scalingParameters); @@ -257,7 +263,11 @@ function _handlePreScaleSetup( ); } -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); @@ -266,7 +276,12 @@ function _getDefaultPixelDataArray(min, max, imageFrame) { return typedArray; } -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; @@ -300,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.' @@ -309,9 +326,9 @@ function _validateScalingParameters(scalingParameters) { } function createDestinationImage( - imageFrame, - targetBuffer, - TypedArrayConstructor + imageFrame: CoreTypes.IImageFrame, + targetBuffer: DICOMLoaderImageOptions['targetBuffer'], + TypedArrayConstructor: new (size: number) => CoreTypes.PixelDataTypedArray ) { const { samplesPerPixel } = imageFrame; const { rows, columns } = targetBuffer; @@ -323,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, @@ -336,10 +355,11 @@ function createDestinationImage( }; } -/** Scales the image frame, updating the frame in place with a new scaled - * version of it (in place modification) - */ -function scaleImageFrame(imageFrame, targetBuffer, TypedArrayConstructor) { +function scaleImageFrame( + imageFrame: CoreTypes.IImageFrame, + targetBuffer: DICOMLoaderImageOptions['targetBuffer'], + TypedArrayConstructor: new (size: number) => CoreTypes.PixelDataTypedArray +) { const dest = createDestinationImage( imageFrame, targetBuffer, @@ -358,16 +378,16 @@ function scaleImageFrame(imageFrame, targetBuffer, TypedArrayConstructor) { * callbackFn that is called with the results. */ 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(); - let decodePromise = null; + let decodePromise: Promise | null = null; let opts; @@ -399,11 +419,6 @@ export async function decodeImageFrame( 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': @@ -442,7 +457,6 @@ export async function decodeImageFrame( }; // JPEG 2000 Lossless - // imageFrame, pixelData, decodeConfig, options decodePromise = decodeJPEG2000(pixelData, opts); break; case '1.2.840.10008.1.2.4.91': @@ -451,8 +465,6 @@ export async function decodeImageFrame( ...imageFrame, }; - // JPEG 2000 Lossy - // imageFrame, pixelData, decodeConfig, options decodePromise = decodeJPEG2000(pixelData, opts); break; case '3.2.840.10008.1.2.4.96': 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..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; @@ -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/createInitializeDecoder.ts b/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts new file mode 100644 index 0000000000..ad9931af3c --- /dev/null +++ b/packages/dicomImageLoader/src/shared/createInitializeDecoder.ts @@ -0,0 +1,117 @@ +import { peerImport } from '@cornerstonejs/core'; +import type { WebWorkerDecodeConfig } from '../types'; + +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(). */ + 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 + * 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 +): { + 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 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, + })); + const wasmUrl = wasmModule?.default; + const locateFileOpts = { + locateFile: (file: string) => + file.endsWith('.wasm') ? wasmUrl : undefined, + }; + 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>)[ + 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 8c43f79a14..58f1268cbc 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeHTJ2K.ts @@ -1,27 +1,28 @@ import type { ByteArray } from 'dicom-parser'; -import openJphFactory, { - type HTJ2KDecoder, - type HTJ2KModule, +import type { + HTJ2KDecoder, + HTJ2KModule, } from '@cornerstonejs/codec-openjph/wasmjs'; -// @ts-ignore -// import openjphWasm from '@cornerstonejs/codec-openjph/wasm'; -const openjphWasm = new URL( - '@cornerstonejs/codec-openjph/wasm', - import.meta.url -); - -import type { LoaderDecodeOptions } from '../../types'; - -const local: { - codec: HTJ2KModule | undefined; - decoder: HTJ2KDecoder | undefined; - decodeConfig: LoaderDecodeOptions; -} = { - codec: undefined, - decoder: undefined, - decodeConfig: {}, +import * as openJphWasmJs from '@cornerstonejs/codec-openjph/wasmjs'; +import { createInitializeDecoder } from '../createInitializeDecoder'; + +const { initialize, state } = createInitializeDecoder({ + library: openJphWasmJs, + 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, @@ -36,32 +37,6 @@ function calculateSizeAtDecompositionLevel( return result; } -export function initialize(decodeConfig?: LoaderDecodeOptions): Promise { - local.decodeConfig = decodeConfig; - - if (local.codec) { - return Promise.resolve(); - } - - const openJphModule = openJphFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - 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) { await initialize(); diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts index b3a98da743..99d973c19c 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEG2000.ts @@ -3,61 +3,26 @@ import type { J2KDecoder, OpenJpegModule, } from '@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode'; -// @ts-ignore -import openJpegFactory from '@cornerstonejs/codec-openjpeg/decodewasmjs'; - -// 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'; -const openjpegWasm = new URL( - '@cornerstonejs/codec-openjpeg/decodewasm', - import.meta.url -); - +import * as openJpegWasmJs from '@cornerstonejs/codec-openjpeg/decodewasmjs'; import type { Types } from '@cornerstonejs/core'; -import type { WebWorkerDecodeConfig } from '../../types'; - -const local: { +import { createInitializeDecoder } from '../createInitializeDecoder'; + +const { initialize, state } = createInitializeDecoder({ + library: openJpegWasmJs, + 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; }; -export function initialize( - decodeConfig?: WebWorkerDecodeConfig -): Promise { - local.decodeConfig = decodeConfig; - - if (local.codec) { - return Promise.resolve(); - } - - const openJpegModule = openJpegFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - 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( diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts index 1ff49d7f83..8ff983535f 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGBaseline8Bit.ts @@ -2,50 +2,26 @@ import type { LibJpegTurbo8Bit, OpenJpegModule, } from '@cornerstonejs/codec-libjpeg-turbo-8bit/dist/libjpegturbowasm_decode'; -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'; -const libjpegTurboWasm = new URL( - '@cornerstonejs/codec-libjpeg-turbo-8bit/decodewasm', - import.meta.url -); +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 local: { +const { initialize, state } = createInitializeDecoder({ + library: libJpegTurboWasmJs, + 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; }; -function initLibjpegTurbo(): Promise { - if (local.codec) { - return Promise.resolve(); - } - - const libjpegTurboModule = libjpegTurboFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - 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 /** * @@ -57,7 +33,7 @@ async function decodeAsync( compressedImageFrame, imageInfo ): Promise { - await initLibjpegTurbo(); + await initialize(); const decoder = local.decoder; // get pointer to the source/encoded bit stream buffer in WASM memory @@ -119,8 +95,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 60970a8f2c..105f39837f 100644 --- a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGLS.ts @@ -2,62 +2,34 @@ 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'; -const charlsWasm = new URL( - '@cornerstonejs/codec-charls/decodewasm', - import.meta.url -); -import type { ByteArray } from 'dicom-parser'; -import type { WebWorkerDecodeConfig } from '../../types'; +import * as charlsWasmJs from '@cornerstonejs/codec-charls/decodewasmjs'; import type { Types } from '@cornerstonejs/core'; - -const local: { +import type { ByteArray } from 'dicom-parser'; +import { createInitializeDecoder } from '../createInitializeDecoder'; + +const { initialize, state } = createInitializeDecoder({ + library: charlsWasmJs, + 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; } -export function initialize( - decodeConfig?: WebWorkerDecodeConfig -): Promise { - local.decodeConfig = decodeConfig; - - if (local.codec) { - return Promise.resolve(); - } - - const charlsModule = charlsFactory({ - locateFile: (f) => { - if (f.endsWith('.wasm')) { - 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 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/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; diff --git a/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts b/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts index e1eaf6464c..4c5523e470 100644 --- a/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts +++ b/packages/dicomImageLoader/src/types/LoaderDecodeOptions.ts @@ -1,3 +1,3 @@ export interface LoaderDecodeOptions { - // whatever + // WASM codecs are loaded via peerImport with fallback; no path options. } diff --git a/packages/dicomImageLoader/src/types/LoaderOptions.ts b/packages/dicomImageLoader/src/types/LoaderOptions.ts index d34885d167..f364399bde 100644 --- a/packages/dicomImageLoader/src/types/LoaderOptions.ts +++ b/packages/dicomImageLoader/src/types/LoaderOptions.ts @@ -31,4 +31,29 @@ export interface LoaderOptions { errorInterceptor?: (error: LoaderXhrRequestError) => void; strict?: boolean; decodeConfig?: LoaderDecodeOptions; + /** + * Pass a custom web worker factory function to create the web worker. + * + * 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; } 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) {