diff --git a/.eslintignore b/.eslintignore index f859685105..6d3fd24453 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,6 @@ *.md *.json packages/adapters/config/webpack/*.js +packages/adapters/jest.config.js packages/docs/*.js packages/tools/src/utilities/segmentation/growCut/runGrowCut.ts diff --git a/bun.lock b/bun.lock index 16c8629e4b..81704920ad 100644 --- a/bun.lock +++ b/bun.lock @@ -136,7 +136,7 @@ }, "packages/adapters": { "name": "@cornerstonejs/adapters", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", @@ -151,7 +151,7 @@ }, "packages/ai": { "name": "@cornerstonejs/ai", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", @@ -169,7 +169,7 @@ }, "packages/core": { "name": "@cornerstonejs/core", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "^4.4.1", @@ -179,10 +179,11 @@ }, "packages/dicomImageLoader": { "name": "@cornerstonejs/dicom-image-loader", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-libjxl": "file:../../cornerstonejs-codec-libjxl-0.0.1.tgz", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", @@ -192,13 +193,13 @@ "uuid": "^9.0.0", }, "peerDependencies": { - "@cornerstonejs/core": "^3.7.6", + "@cornerstonejs/core": "^3.10.24", "dicom-parser": "^1.8.9", }, }, "packages/labelmap-interpolation": { "name": "@cornerstonejs/labelmap-interpolation", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165", @@ -211,7 +212,7 @@ }, "packages/nifti-volume-loader": { "name": "@cornerstonejs/nifti-volume-loader", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "nifti-reader-js": "^0.6.8", }, @@ -221,7 +222,7 @@ }, "packages/polymorphic-segmentation": { "name": "@cornerstonejs/polymorphic-segmentation", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@icr/polyseg-wasm": "0.4.0", }, @@ -233,7 +234,7 @@ }, "packages/tools": { "name": "@cornerstonejs/tools", - "version": "3.10.5", + "version": "3.10.24", "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", @@ -521,6 +522,8 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": ["@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.2", "", {}, "sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg=="], + "@cornerstonejs/codec-libjxl": ["@cornerstonejs/codec-libjxl@../../cornerstonejs-codec-libjxl-0.0.1.tgz", {}], + "@cornerstonejs/codec-openjpeg": ["@cornerstonejs/codec-openjpeg@1.2.4", "", {}, "sha512-UT2su6xZZnCPSuWf2ldzKa/2+guQ7BGgfBSKqxanggwJHh48gZqIAzekmsLyJHMMK5YDK+ti+fzvVJhBS3Xi/g=="], "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], diff --git a/cornerstonejs-codec-libjxl-0.0.1.tgz b/cornerstonejs-codec-libjxl-0.0.1.tgz new file mode 100644 index 0000000000..b82ff6334b Binary files /dev/null and b/cornerstonejs-codec-libjxl-0.0.1.tgz differ diff --git a/packages/core/examples/stackBasic/index.ts b/packages/core/examples/stackBasic/index.ts index c34595a60d..23a3bdf7ed 100644 --- a/packages/core/examples/stackBasic/index.ts +++ b/packages/core/examples/stackBasic/index.ts @@ -5,6 +5,7 @@ import { createImageIdsAndCacheMetaData, setTitleAndDescription, ctVoiRange, + getLocalUrl, } from '../../../../utils/demo/helpers'; const { examplesLog } = utilities.logger; @@ -44,7 +45,8 @@ async function run() { '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', SeriesInstanceUID: '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', - wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRsRoot: + getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', }); // Instantiate a rendering engine diff --git a/packages/dicomImageLoader/package.json b/packages/dicomImageLoader/package.json index 761c557a52..a518bca689 100644 --- a/packages/dicomImageLoader/package.json +++ b/packages/dicomImageLoader/package.json @@ -109,6 +109,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/codec-libjxl": "file:../../cornerstonejs-codec-libjxl-0.0.1.tgz", "comlink": "^4.4.1", "dicom-parser": "^1.8.9", "jpeg-lossless-decoder-js": "^2.1.0", diff --git a/packages/dicomImageLoader/src/decodeImageFrameWorker.js b/packages/dicomImageLoader/src/decodeImageFrameWorker.js index 99a6add366..c45938f2b1 100644 --- a/packages/dicomImageLoader/src/decodeImageFrameWorker.js +++ b/packages/dicomImageLoader/src/decodeImageFrameWorker.js @@ -11,6 +11,7 @@ import decodeJPEGBaseline8Bit from './shared/decoders/decodeJPEGBaseline8Bit'; import decodeJPEGBaseline12Bit from './shared/decoders/decodeJPEGBaseline12Bit-js'; import decodeJPEGLossless from './shared/decoders/decodeJPEGLossless'; import decodeJPEGLS from './shared/decoders/decodeJPEGLS'; +import decodeJPEGXL from './shared/decoders/decodeJPEGXL'; 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 @@ -417,6 +418,16 @@ export async function decodeImageFrame( decodePromise = decodeHTJ2K(pixelData, opts); break; + case '1.2.840.10008.1.2.4.110': + case '1.2.840.10008.1.2.4.111': + case '1.2.840.10008.1.2.4.112': + // JPEG XL + opts = { + ...imageFrame, + }; + + decodePromise = decodeJPEGXL(pixelData, opts); + 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..430994c513 100644 --- a/packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts +++ b/packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts @@ -183,6 +183,17 @@ function decodeImageFrame( decodeConfig ); + case '1.2.840.10008.1.2.4.110': + case '1.2.840.10008.1.2.4.111': + case '1.2.840.10008.1.2.4.112': + // JPEGXL + return processDecodeTask( + imageFrame, + transferSyntax, + pixelData, + options, + decodeConfig + ); case '3.2.840.10008.1.2.4.96': case '1.2.840.10008.1.2.4.201': case '1.2.840.10008.1.2.4.202': diff --git a/packages/dicomImageLoader/src/shared/decoders/decodeJPEGXL.ts b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGXL.ts new file mode 100644 index 0000000000..86717fa0c3 --- /dev/null +++ b/packages/dicomImageLoader/src/shared/decoders/decodeJPEGXL.ts @@ -0,0 +1,150 @@ +import type { ByteArray } from 'dicom-parser'; +// @ts-ignore +import libjxlFactory from '@cornerstonejs/codec-libjxl'; +// @ts-ignore +// import libjxlWasm from '@cornerstonejs/codec-libjxl/wasm'; +const libjxlWasm = new URL('@cornerstonejs/codec-libjxl/wasm', import.meta.url); + +import type { LoaderDecodeOptions } from '../../types'; + +const local: { + codec: unknown; + decoder: unknown; + decodeConfig: LoaderDecodeOptions; +} = { + codec: undefined, + decoder: undefined, + decodeConfig: {}, +}; + +function calculateSizeAtDecompositionLevel( + decompositionLevel: number, + frameWidth: number, + frameHeight: number +) { + const result = { width: frameWidth, height: frameHeight }; + while (decompositionLevel > 0) { + result.width = Math.ceil(result.width / 2); + result.height = Math.ceil(result.height / 2); + decompositionLevel--; + } + return result; +} + +export function initialize(decodeConfig?: LoaderDecodeOptions): Promise { + local.decodeConfig = decodeConfig; + + if (local.codec) { + return Promise.resolve(); + } + + const libjxlModule = libjxlFactory({ + locateFile: (f) => { + if (f.endsWith('.wasm')) { + return libjxlWasm.toString(); + } + + return f; + }, + }); + + return new Promise((resolve, reject) => { + libjxlModule.then((instance) => { + local.codec = instance; + local.decoder = new instance.JpegXLDecoder(); + resolve(); + }, reject); + }); +} + +// https://github.com/chafey/openjpegjs/blob/master/test/browser/index.html +async function decodeAsync(compressedImageFrame: ByteArray, imageInfo) { + await initialize(); + debugger; + // const decoder = local.decoder; + const decoder = new local.codec.JpegXLDecoder(); + + // get pointer to the source/encoded bit stream buffer in WASM memory + // that can hold the encoded bitstream + const encodedBufferInWASM = decoder.getEncodedBuffer( + compressedImageFrame.length + ); + + // copy the encoded bitstream into WASM memory buffer + encodedBufferInWASM.set(compressedImageFrame); + + // decode it + decoder.decode(); + + // get information about the decoded image + const frameInfo = decoder.getFrameInfo(); + console.log('frameInfo=', frameInfo); + + // get the decoded pixels + const decodedPixelsInWASM = decoder.getDecodedBuffer(); + + const encodedImageInfo = { + columns: frameInfo.width, + rows: frameInfo.height, + bitsPerPixel: frameInfo.bitsPerSample, + signed: imageInfo.signed, + bytesPerPixel: imageInfo.bytesPerPixel, + componentsPerPixel: frameInfo.componentCount, + }; + + console.log('decodedPixelsInWASM', decodedPixelsInWASM); + const pixelData = getPixelData( + frameInfo, + decodedPixelsInWASM, + imageInfo.signed + ); + + const encodeOptions = { + frameInfo, + }; + + // local.codec.doLeakCheck(); + + return { + ...imageInfo, + pixelData, + imageInfo: encodedImageInfo, + encodeOptions, + ...encodeOptions, + ...encodedImageInfo, + }; +} + +function getPixelData(frameInfo, decodedBuffer) { + if (frameInfo.bitsPerSample > 8) { + if (frameInfo.isSigned) { + return new Int16Array( + decodedBuffer.buffer, + decodedBuffer.byteOffset, + decodedBuffer.byteLength / 2 + ); + } + + return new Uint16Array( + decodedBuffer.buffer, + decodedBuffer.byteOffset, + decodedBuffer.byteLength / 2 + ); + } + + if (frameInfo.isSigned) { + return new Int8Array( + decodedBuffer.buffer, + decodedBuffer.byteOffset, + decodedBuffer.byteLength + ); + } + + return new Uint8Array( + decodedBuffer.buffer, + decodedBuffer.byteOffset, + decodedBuffer.byteLength + ); +} + +export default decodeAsync; diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index 5a3e4a2380..5652b29827 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -94,6 +94,7 @@ module.exports = { modules, extensions: ['.ts', '.tsx', '.js', '.jsx'], fallback: { + "child_process": false, fs: false, path: require.resolve('path-browserify'), events: false