diff --git a/package.json b/package.json index 42cc729..7e92b93 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,8 @@ "@cornerstonejs/codec-openjph": "^2.4.5", "dicomweb-client": "^0.10.3", "colormap": "^2.3", + "dicomicc": "^0.2", "dcmjs": "^0.41.0", - "dicomicc": "^0.1", "image-type": "^4.1", "mathjs": "^11.2", "ol": "^10.6.0", diff --git a/src/decode.js b/src/decode.js index 0b0fb47..6bba1b0 100644 --- a/src/decode.js +++ b/src/decode.js @@ -10,7 +10,8 @@ function _processDecodeAndTransformTask ( samplesPerPixel, sopInstanceUID, metadata, - iccProfiles + iccProfiles, + iccOutputType = "srgb" // "srgb" or "display-p3" ) { const priority = undefined const transferList = undefined @@ -26,7 +27,8 @@ function _processDecodeAndTransformTask ( samplesPerPixel, sopInstanceUID, metadata, - iccProfiles + iccProfiles, + iccOutputType }, priority, transferList @@ -42,7 +44,8 @@ async function _decodeAndTransformFrame ({ samplesPerPixel, sopInstanceUID, metadata, // metadata of all images (different resolution levels) - iccProfiles // ICC profiles for all images + iccProfiles, // ICC profiles for all images + iccOutputType = "srgb" // "srgb" or "display-p3" }) { const result = await _processDecodeAndTransformTask( frame, @@ -53,7 +56,8 @@ async function _decodeAndTransformFrame ({ samplesPerPixel, sopInstanceUID, metadata, - iccProfiles + iccProfiles, + iccOutputType ) const signed = pixelRepresentation === 1 diff --git a/src/pyramid.js b/src/pyramid.js index 44d9252..0f331cf 100644 --- a/src/pyramid.js +++ b/src/pyramid.js @@ -376,6 +376,7 @@ function _createTileLoadFunction ({ client, channel, iccProfiles, + iccOutputType, targetElement }) { return async (z, y, x) => { @@ -521,7 +522,8 @@ function _createTileLoadFunction ({ samplesPerPixel, sopInstanceUID, metadata: pyramid.metadata, - iccProfiles + iccProfiles, + iccOutputType }).then(pixelArray => { if (pixelArray.constructor === Float64Array) { // TODO: handle Float64Array using LUT diff --git a/src/viewer.js b/src/viewer.js index e28a2cb..e99a62f 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -781,6 +781,7 @@ const _tileGrid = Symbol('tileGrid') const _updateOverviewMapSize = Symbol('updateOverviewMapSize') const _annotationOptions = Symbol('annotationOptions') const _isICCProfilesEnabled = Symbol('isICCProfilesEnabled') +const _iccOutputType = Symbol('_iccOutputType') const _iccProfiles = Symbol('iccProfiles') const _container = Symbol('container') const _highResSources = Symbol('highResSources') @@ -841,6 +842,7 @@ class VolumeImageViewer { this[_clients] = {} this[_errorInterceptor] = options.errorInterceptor || (error => error) this[_isICCProfilesEnabled] = true + this[_iccOutputType] = "srgb" this[_container] = null this[_clients] = {} this[_iccProfiles] = [] @@ -1202,6 +1204,26 @@ class VolumeImageViewer { extent: this[_pyramid].extent }) + /** + * Detect the display color space. + * Note: The WebGLRenderingContext only supports sRGB and Display-P3 + * color spaces, Adobe RGB (1998) and ROMM RGB are not supported. + * @returns {string} 'display-p3' or 'srgb' + */ + function detectDisplayColorSpace() { + if (typeof window !== 'undefined' && window.matchMedia) { + if (window.matchMedia("(color-gamut: p3)").matches) { + return 'display-p3'; + } else if (window.matchMedia("(color-gamut: srgb)").matches) { + return 'srgb'; + } + } + return 'srgb'; + } + + this[_iccOutputType] = detectDisplayColorSpace(); + console.log(`Detected display color space: "${this[_iccOutputType]}"`); + const layers = [] const overviewLayers = [] this[_opticalPaths] = {} @@ -1355,7 +1377,11 @@ class VolumeImageViewer { }) opticalPath.layer.helper = helper opticalPath.layer.on('precompose', (event) => { - const gl = event.context + const gl = event.context; + if ('drawingBufferColorSpace' in gl) { + gl.drawingBufferColorSpace = this[_iccOutputType] + console.debug("Using color space - layer:", gl.drawingBufferColorSpace) + } gl.enable(gl.BLEND) gl.blendEquation(gl.FUNC_ADD) gl.blendFunc(gl.SRC_COLOR, gl.ONE) @@ -1382,6 +1408,10 @@ class VolumeImageViewer { opticalPath.overviewLayer.helper = overviewHelper opticalPath.overviewLayer.on('precompose', (event) => { const gl = event.context + if ('drawingBufferColorSpace' in gl) { + gl.drawingBufferColorSpace = this[_iccOutputType] + console.debug("Using color space - overviewLayer:", gl.drawingBufferColorSpace) + } gl.enable(gl.BLEND) gl.blendEquation(gl.FUNC_ADD) gl.blendFunc(gl.SRC_COLOR, gl.ONE) @@ -1451,6 +1481,14 @@ class VolumeImageViewer { useInterimTilesOnError: false, cacheSize: this[_options].tilesCacheSize }) + opticalPath.layer.on('precompose', (event) => { + const gl = event.context; + if ('drawingBufferColorSpace' in gl) { + gl.drawingBufferColorSpace = this[_iccOutputType] + console.debug("Using color space - layer:", gl.drawingBufferColorSpace) + } + }) + opticalPath.layer.on('error', (event) => { console.error( `error rendering optical path "${opticalPathIdentifier}"`, @@ -1468,6 +1506,13 @@ class VolumeImageViewer { preload: 0, useInterimTilesOnError: false }) + opticalPath.overviewLayer.on('precompose', (event) => { + const gl = event.context; + if ('drawingBufferColorSpace' in gl) { + gl.drawingBufferColorSpace = this[_iccOutputType] + console.debug("Using color space - overviewLayer:", gl.drawingBufferColorSpace) + } + }) layers.push(opticalPath.layer) overviewLayers.push(opticalPath.overviewLayer) @@ -2325,6 +2370,7 @@ class VolumeImageViewer { const loaderWithICCProfiles = _createTileLoadFunction({ targetElement: this[_container], iccProfiles: profiles, + iccOutputType: this[_iccOutputType], ...item.loaderParams }) const loaderWithoutICCProfiles = _createTileLoadFunction({ @@ -2414,6 +2460,7 @@ class VolumeImageViewer { const loader = _createTileLoadFunction({ targetElement: container, iccProfiles: profiles, + iccOutputType: this[_iccOutputType], ...opticalPath.loaderParams }) const source = opticalPath.layer.getSource() @@ -2585,6 +2632,7 @@ class VolumeImageViewer { const loader = _createTileLoadFunction({ targetElement: container, iccProfiles: this[_isICCProfilesEnabled] && profiles.length > 0 ? profiles : null, + iccOutputType: this[_isICCProfilesEnabled] && profiles.length > 0 ? this[_iccOutputType] : null, ...item.loaderParams }) source.setLoader(loader) diff --git a/src/webWorker/decodeAndTransformTask.js b/src/webWorker/decodeAndTransformTask.js index bef15d0..35cc63c 100644 --- a/src/webWorker/decodeAndTransformTask.js +++ b/src/webWorker/decodeAndTransformTask.js @@ -27,7 +27,8 @@ function _handler (data, doneCallback) { frame, sopInstanceUID, metadata, - iccProfiles + iccProfiles, + iccOutputType = "srgb" // "srgb" or "display-p3" } = data.data _checkImageTypeAndDecode( @@ -43,7 +44,7 @@ function _handler (data, doneCallback) { if (iccProfiles?.length) { // Only instantiate the transformer once and cache it for reuse. if (transformerColor === undefined) { - transformerColor = new ColorTransformer(metadata, iccProfiles) + transformerColor = new ColorTransformer(metadata, iccProfiles, iccOutputType) } // Apply ICC color transform transformerColor.transform( diff --git a/src/webWorker/transformers/transformerICC.js b/src/webWorker/transformers/transformerICC.js index b9bd192..2682587 100644 --- a/src/webWorker/transformers/transformerICC.js +++ b/src/webWorker/transformers/transformerICC.js @@ -10,8 +10,10 @@ export default class ColorTransformer extends Transformer { * @param {Array} - Metadata of each * image * @param {Array} - ICC profiles of each image + * @param {number} [iccOutputType="srgb"] - ICC output type + * ("srgb": sRGB (default), "display-p3": Display-P3, "adobe-rgb": Adobe RGB (1998), "romm-rgb": ROMM RGB). */ - constructor (metadata, iccProfiles) { + constructor (metadata, iccProfiles, iccOutputType = "srgb") { super() if (metadata.length !== iccProfiles.length) { throw new Error( @@ -23,6 +25,7 @@ export default class ColorTransformer extends Transformer { this.iccProfiles = iccProfiles this.codec = null this.transformers = {} + this.iccOutputTypeString = iccOutputType; } _initialize () { @@ -50,11 +53,31 @@ export default class ColorTransformer extends Transformer { const samplesPerPixel = this.metadata[index].SamplesPerPixel const planarConfiguration = this.metadata[index].PlanarConfiguration const sopInstanceUID = this.metadata[index].SOPInstanceUID + const profile = inlineBinaryToUint8Array(this.iccProfiles[index]) if (!profile) { console.warn('Unable to convert icc profile: ', this.iccProfiles[index]) return } + + // Determine ICC output type using the exposed enum + let iccOutputType + switch (this.iccOutputTypeString) { + case "display-p3": + iccOutputType = this.codec.DcmIccOutputType.DISPLAY_P3 + break + case "adobe-rgb": + iccOutputType = this.codec.DcmIccOutputType.ADOBE_RGB + break + case "romm-rgb": + iccOutputType = this.codec.DcmIccOutputType.ROMM_RGB + break + case "srgb": + default: + iccOutputType = this.codec.DcmIccOutputType.SRGB + break + } + this.transformers[sopInstanceUID] = new this.codec.ColorManager( { columns, @@ -63,7 +86,8 @@ export default class ColorTransformer extends Transformer { samplesPerPixel, planarConfiguration }, - profile + profile, + iccOutputType ) } resolve(this.transformers)