From 4aa7f30a23bcd7e0021de973759857a16bc8cea6 Mon Sep 17 00:00:00 2001 From: Denny Spiegelberg Date: Mon, 20 Apr 2026 08:38:58 +0200 Subject: [PATCH 1/3] fix(nifti): pass through DICOM metadata for NIfTI-backed display sets --- extensions/default/package.json | 1 + .../default/src/getSopClassHandlerModule.js | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/extensions/default/package.json b/extensions/default/package.json index f06e0efe13e..3835767e658 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -49,6 +49,7 @@ "dependencies": { "@babel/runtime": "7.28.2", "@cornerstonejs/calculate-suv": "1.1.0", + "@cornerstonejs/nifti-volume-loader": "4.21.7", "lodash.get": "4.4.2", "lodash.uniqby": "4.7.0", "react-color": "2.19.3" diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index 7c312d7ef4b..1bf6dd70712 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -1,4 +1,5 @@ import { utils, classes } from '@ohif/core'; +import { createNiftiImageIdsAndCacheMetadata } from '@cornerstonejs/nifti-volume-loader'; import i18n from '@ohif/i18n'; import { id } from './id'; import getDisplaySetMessages from './getDisplaySetMessages'; @@ -14,6 +15,23 @@ const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; const sopClassHandlerName = 'stack'; let appContext = {}; +const isAbsolutePathOrUrl = value => + typeof value === 'string' && + (/^[a-z]+:\/\//i.test(value) || value.startsWith('//') || value.startsWith('/')); + +const joinUrl = (baseUrl, path) => { + if (!baseUrl) { + return path; + } + + try { + return new URL(path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`).toString(); + } catch (error) { + const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + return `${normalizedBaseUrl}${path.replace(/^\/+/, '')}`; + } +}; + const getDynamicVolumeInfo = instances => { const { extensionManager } = appContext; @@ -96,6 +114,14 @@ const makeDisplaySet = (instances, index) => { // set appropriate attributes to image set... const messages = getDisplaySetMessages(instances, isReconstructable, isDynamicVolume); + const { niftiPrivateTagName, niftiBaseUrl } = dataSource.getConfig?.() || {}; + const niftiPath = niftiPrivateTagName ? instance[niftiPrivateTagName] : undefined; + let niftiURL; + + if (typeof niftiPath === 'string' && niftiPath.trim().length > 0) { + niftiURL = isAbsolutePathOrUrl(niftiPath) ? niftiPath : joinUrl(niftiBaseUrl, niftiPath); + } + imageSet.setAttributes({ volumeLoaderSchema, displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID @@ -124,6 +150,43 @@ const makeDisplaySet = (instances, index) => { FrameOfReferenceUID: instance.FrameOfReferenceUID, }); + if (niftiURL) { + // If a downstream integration already exposes a NIfTI URL on the series, + // pass through the DICOM-derived metadata that a paired loader-side + // implementation can consume for VOI and series registration. + const dicomMetadata = { + Modality: instance.Modality, + SeriesInstanceUID: instance.SeriesInstanceUID, + SeriesNumber: instance.SeriesNumber, + SeriesDescription: instance.SeriesDescription, + WindowCenter: instance.WindowCenter, + WindowWidth: instance.WindowWidth, + VOILUTFunction: instance.VOILUTFunction, + RescaleSlope: instance.RescaleSlope, + RescaleIntercept: instance.RescaleIntercept, + }; + + imageSet.setAttributes({ + niftiURL, + loadImageIds: async () => { + try { + const loadedImageIds = await createNiftiImageIdsAndCacheMetadata({ + url: niftiURL, + dicomMetadata, + }); + + imageSet.setAttributes({ + imageIds: loadedImageIds, + numImageFrames: Array.isArray(loadedImageIds) ? loadedImageIds.length : 0, + isReconstructable: true, + }); + } catch (error) { + console.error('Error loading niftiURL:', error); + } + }, + }); + } + const imageIds = dataSource.getImageIdsForDisplaySet(imageSet); let imageId = imageIds[Math.floor(imageIds.length / 2)]; let thumbnailInstance = instances[Math.floor(instances.length / 2)]; From 8a0710971f08686027956e7be00620701259f2cc Mon Sep 17 00:00:00 2001 From: Denny Spiegelberg Date: Mon, 20 Apr 2026 08:59:44 +0200 Subject: [PATCH 2/3] fix(nifti): lazy load loader and surface failures --- extensions/default/src/getSopClassHandlerModule.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index 1bf6dd70712..45f026be929 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -1,5 +1,4 @@ import { utils, classes } from '@ohif/core'; -import { createNiftiImageIdsAndCacheMetadata } from '@cornerstonejs/nifti-volume-loader'; import i18n from '@ohif/i18n'; import { id } from './id'; import getDisplaySetMessages from './getDisplaySetMessages'; @@ -14,6 +13,7 @@ const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; const sopClassHandlerName = 'stack'; let appContext = {}; +let niftiIntegrationConfig = {}; const isAbsolutePathOrUrl = value => typeof value === 'string' && @@ -114,7 +114,7 @@ const makeDisplaySet = (instances, index) => { // set appropriate attributes to image set... const messages = getDisplaySetMessages(instances, isReconstructable, isDynamicVolume); - const { niftiPrivateTagName, niftiBaseUrl } = dataSource.getConfig?.() || {}; + const { niftiPrivateTagName, niftiBaseUrl } = niftiIntegrationConfig; const niftiPath = niftiPrivateTagName ? instance[niftiPrivateTagName] : undefined; let niftiURL; @@ -170,6 +170,9 @@ const makeDisplaySet = (instances, index) => { niftiURL, loadImageIds: async () => { try { + const { createNiftiImageIdsAndCacheMetadata } = await import( + '@cornerstonejs/nifti-volume-loader' + ); const loadedImageIds = await createNiftiImageIdsAndCacheMetadata({ url: niftiURL, dicomMetadata, @@ -182,6 +185,7 @@ const makeDisplaySet = (instances, index) => { }); } catch (error) { console.error('Error loading niftiURL:', error); + throw error; } }, }); @@ -360,6 +364,9 @@ const sopClassUids = [ function getSopClassHandlerModule(appContextParam) { appContext = appContextParam; + const { extensionManager } = appContext; + const activeDataSource = extensionManager?.getActiveDataSource?.()?.[0]; + niftiIntegrationConfig = activeDataSource?.getConfig?.() || {}; return [ { From 08902ec81dcf6653c017878bdbbeda6391887857 Mon Sep 17 00:00:00 2001 From: Denny Spiegelberg Date: Mon, 20 Apr 2026 10:54:38 +0200 Subject: [PATCH 3/3] fix(nifti): resolve datasource config at display-set creation --- extensions/default/src/getSopClassHandlerModule.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index 45f026be929..d79297b3d39 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -13,7 +13,6 @@ const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; const sopClassHandlerName = 'stack'; let appContext = {}; -let niftiIntegrationConfig = {}; const isAbsolutePathOrUrl = value => typeof value === 'string' && @@ -114,7 +113,7 @@ const makeDisplaySet = (instances, index) => { // set appropriate attributes to image set... const messages = getDisplaySetMessages(instances, isReconstructable, isDynamicVolume); - const { niftiPrivateTagName, niftiBaseUrl } = niftiIntegrationConfig; + const { niftiPrivateTagName, niftiBaseUrl } = dataSource.getConfig?.() || {}; const niftiPath = niftiPrivateTagName ? instance[niftiPrivateTagName] : undefined; let niftiURL; @@ -364,9 +363,6 @@ const sopClassUids = [ function getSopClassHandlerModule(appContextParam) { appContext = appContextParam; - const { extensionManager } = appContext; - const activeDataSource = extensionManager?.getActiveDataSource?.()?.[0]; - niftiIntegrationConfig = activeDataSource?.getConfig?.() || {}; return [ {