Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38c0e8c
feat(dicom-image-loader): Add ability to customise path to web worker…
jmannau Apr 8, 2025
076bdf1
docs(LoaderOptions): Update comments
jmannau Apr 8, 2025
b01cb71
fix(dicomImageLoader): fix loading libJpegTurbo wasm from custom url
jmannau Apr 11, 2025
790b535
Merge remote-tracking branch 'upstream/main' into feature/dicom-image…
jmannau Apr 11, 2025
a7d2a89
refactor(dicom-image-loader): add additional types
jmannau Jun 30, 2025
3585d61
Merge remote-tracking branch 'upstream/main' into feature/dicom-image…
jmannau Jun 30, 2025
66a1848
refactor(dicom-image-loader): Refactor decodeImageFrameWorker into ty…
jmannau Jun 30, 2025
fc66e0c
docs(dicom-image-loader): fix comment
jmannau Jun 30, 2025
5684647
fix(dicom-image-loader): Fix typescript compliation
jmannau Jun 30, 2025
ca74248
Merge remote-tracking branch 'upstream/main' into feature/dicom-image…
jmannau Sep 3, 2025
f5bed65
Merge remote-tracking branch 'origin/main' into feature/dicom-image-l…
wayfarer3130 Oct 15, 2025
d1c615c
fix decode image frame worker load
wayfarer3130 Oct 16, 2025
3cea8df
Merge remote-tracking branch 'upstream/main' into feature/dicom-image…
jmannau Oct 23, 2025
0123335
refactor(dicom-image-loader):
jmannau Dec 5, 2025
bb93154
Merge remote-tracking branch upstream/main into feature/dicom-image-l…
jmannau Dec 5, 2025
4259d14
Merge remote-tracking branch 'origin/main' into feature/dicom-image-l…
wayfarer3130 Feb 9, 2026
31b7908
Fix imports
wayfarer3130 Feb 9, 2026
a48c6bd
Use peerImport to load wasm modules
wayfarer3130 Feb 11, 2026
52b5212
Merge remote-tracking branch 'origin/main' into feature/dicom-image-l…
wayfarer3130 Feb 24, 2026
e761cea
Merge remote-tracking branch 'origin/main' into feature/dicom-image-l…
wayfarer3130 Mar 2, 2026
45d2758
fix: Wasm load path
wayfarer3130 Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/src/RenderingEngine/WSIViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> => {
return peerImport('dicom-microscopy-viewer');
};

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/types/Cornerstone3DConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
/* eslint-disable @typescript-eslint/no-explicit-any -- peerImport is dynamic; module/return type varies by caller */
peerImport?: (
moduleId: string,
fallback?: () => Promise<any>
) => Promise<any>;
/* eslint-enable @typescript-eslint/no-explicit-any */
}

export type { Cornerstone3DConfig as default };
65 changes: 65 additions & 0 deletions packages/dicomImageLoader/docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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;

Expand Down Expand Up @@ -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.'
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can read the TsuidConfigOptions here for the specific transferSyntaxUID, then pass it in generally below, eg
const tsuidConfigOptions = decodeConfig?.[transferSyntax]
Then other options can be passed in the future.

options: DICOMLoaderImageOptions,
callbackFn?: (image: CoreTypes.IImageFrame) => void
): Promise<CoreTypes.IImageFrame> {
const start = new Date().getTime();

let decodePromise = null;
let decodePromise: Promise<CoreTypes.IImageFrame> | null = null;

let opts;

Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand All @@ -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':
Expand Down
16 changes: 8 additions & 8 deletions packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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':
Expand Down
Loading
Loading