From 66bec45ca1767287d48da7d145353f833b88eec2 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 2 May 2022 16:28:43 -0400 Subject: [PATCH 1/5] feat: delegating dicom web proxy datasource --- .../src/DicomWebProxyDataSource/index.js | 74 +++++++++++++++++++ .../default/src/getDataSourcesModule.js | 6 ++ platform/viewer/public/config/default.js | 8 ++ 3 files changed, 88 insertions(+) create mode 100644 extensions/default/src/DicomWebProxyDataSource/index.js diff --git a/extensions/default/src/DicomWebProxyDataSource/index.js b/extensions/default/src/DicomWebProxyDataSource/index.js new file mode 100644 index 0000000000..5a9a9d34b7 --- /dev/null +++ b/extensions/default/src/DicomWebProxyDataSource/index.js @@ -0,0 +1,74 @@ +import { IWebApiDataSource } from '@ohif/core'; +import { createDicomWebApi } from '../DicomWebDataSource/index'; + +function createDicomWebProxyApi( + dicomWebProxyConfig, + UserAuthenticationService +) { + const { name } = dicomWebProxyConfig; + let dicomWebDelegate = undefined; + + const implementation = { + initialize: async ({ params, query, url }) => { + let studyInstanceUIDs = []; + + if (!url) url = query.get('url'); + + if (!url) { + console.error(`No url for '${name}'`); + } else { + const response = await fetch(url); + let data = await response.json(); + + // there seem to be a couple of variations of the case for this parameter + const queryStudyInstanceUIDs = + query.get('studyInstanceUIDs') || query.get('studyInstanceUids'); + if (!queryStudyInstanceUIDs) { + console.error(`No studyInstanceUids in request for '${name}'`); + } + if (data.servers && queryStudyInstanceUIDs) { + dicomWebDelegate = createDicomWebApi( + data.servers.dicomWeb[0], + UserAuthenticationService + ); + studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); + } + } + return studyInstanceUIDs; + }, + query: { + studies: { + search: params => dicomWebDelegate.query.studies.search(params), + }, + series: { + search: (...args) => dicomWebDelegate.query.series.search(...args), + }, + instances: { + search: (studyInstanceUid, queryParameters) => + dicomWebDelegate.query.instances.search( + studyInstanceUid, + queryParameters + ), + }, + }, + retrieve: { + directURL: (...args) => dicomWebDelegate.retrieve.directURL(...args), + series: { + metadata: (...args) => + dicomWebDelegate.retrieve.series.metadata(...args), + }, + }, + store: { + dicom: (...args) => dicomWebDelegate.store(...args), + }, + deleteStudyMetadataPromise: (...args) => + dicomWebDelegate.deleteStudyMetadataPromise(...args), + getImageIdsForDisplaySet: (...args) => + dicomWebDelegate.getImageIdsForDisplaySet(...args), + getImageIdsForInstance: (...args) => + dicomWebDelegate.getImageIdsForInstance(...args), + }; + return IWebApiDataSource.create(implementation); +} + +export { createDicomWebProxyApi }; diff --git a/extensions/default/src/getDataSourcesModule.js b/extensions/default/src/getDataSourcesModule.js index dfccc6a18e..6eab56a2fa 100644 --- a/extensions/default/src/getDataSourcesModule.js +++ b/extensions/default/src/getDataSourcesModule.js @@ -5,6 +5,7 @@ import { createDicomWebApi } from './DicomWebDataSource/index.js'; import { createDicomJSONApi } from './DicomJSONDataSource/index.js'; import { createDicomLocalApi } from './DicomLocalDataSource/index.js'; +import { createDicomWebProxyApi } from './DicomWebProxyDataSource/index.js'; /** * @@ -16,6 +17,11 @@ function getDataSourcesModule() { type: 'webApi', createDataSource: createDicomWebApi, }, + { + name: 'dicomwebproxy', + type: 'webApi', + createDataSource: createDicomWebProxyApi, + }, { name: 'dicomjson', type: 'jsonApi', diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index ccad643070..a962f53c76 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -24,6 +24,14 @@ window.config = { supportsWildcard: true, }, }, + { + friendlyName: 'dicomweb delegating proxy', + namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy', + sourceName: 'dicomwebproxy', + configuration: { + name: 'dicomwebproxy', + }, + }, { friendlyName: 'dicom json', namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', From b02e76cc0c259477566af509bbfb6e5fbbdfd175 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 2 May 2022 16:30:16 -0400 Subject: [PATCH 2/5] fix: configurably allow WADO image loader to issue preflight OPTIONS request --- extensions/cornerstone/src/index.js | 9 +++++++-- extensions/cornerstone/src/init.js | 4 +++- extensions/cornerstone/src/initWADOImageLoader.js | 9 ++++++--- platform/viewer/public/config/default.js | 2 ++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js index 58b0e181dd..0f5ecf1127 100644 --- a/extensions/cornerstone/src/index.js +++ b/extensions/cornerstone/src/index.js @@ -30,8 +30,13 @@ export default { * @param {object} [configuration={}] * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` */ - preRegistration({ servicesManager, commandsManager, configuration = {} }) { - init({ servicesManager, commandsManager, configuration }); + preRegistration({ + servicesManager, + commandsManager, + appConfig, + configuration = {}, + }) { + init({ servicesManager, commandsManager, appConfig, configuration }); }, getViewportModule({ servicesManager, commandsManager }) { const ExtendedOHIFCornerstoneViewport = props => { diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 01171e71cd..ecaa5e5d1e 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -62,6 +62,7 @@ const _createInternalToolsConfig = UIDialogService => { export default function init({ servicesManager, commandsManager, + appConfig, configuration, }) { const { @@ -276,7 +277,8 @@ export default function init({ cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999); - initWADOImageLoader(UserAuthenticationService); + const { imageLoaderAcceptHeader } = appConfig; + initWADOImageLoader(UserAuthenticationService, imageLoaderAcceptHeader); // ~~ const defaultCsToolsConfig = csToolsConfig || { diff --git a/extensions/cornerstone/src/initWADOImageLoader.js b/extensions/cornerstone/src/initWADOImageLoader.js index dadf7f26f3..ff346531ab 100644 --- a/extensions/cornerstone/src/initWADOImageLoader.js +++ b/extensions/cornerstone/src/initWADOImageLoader.js @@ -24,7 +24,10 @@ function initWebWorkers() { } } -export default function initWADOImageLoader(UserAuthenticationService) { +export default function initWADOImageLoader( + UserAuthenticationService, + imageLoaderAcceptHeader = 'multipart/related; type=application/octet-stream' +) { cornerstoneWADOImageLoader.external.cornerstone = cornerstone; cornerstoneWADOImageLoader.external.dicomParser = dicomParser; @@ -38,8 +41,8 @@ export default function initWADOImageLoader(UserAuthenticationService) { // For now we use image/jls and image/x-jls because some servers still use the old type // http://dicom.nema.org/medical/dicom/current/output/html/part18.html const xhrRequestHeaders = { - // To prevent Preflight requests: - accept: 'multipart/related; type=application/octet-stream', + // Preflight requests disabled by default: + accept: imageLoaderAcceptHeader, // //accept: 'multipart/related; type="image/x-jls"', // 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*', diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index a962f53c76..f1a86899b0 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -5,6 +5,8 @@ window.config = { modes: [], showStudyList: true, // filterQueryParam: false, + // enable imageLoaderAcceptHeader configuration to force a reconfiguration of WADOImageLoader to support preflight request + // imageLoaderAcceptHeader: 'multipart/related; type="application/octet-stream"', dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', From d02dbd7a2b659b9ed615ac9de1a1f6e43f7cb3c3 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 3 May 2022 00:13:36 -0400 Subject: [PATCH 3/5] Added documentation and fixed up some error handling from PR review --- .../src/DicomWebProxyDataSource/index.js | 38 ++++++++----- .../dataSources/dicom-web-proxy.md | 54 +++++++++++++++++++ 2 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 platform/docs/docs/configuration/dataSources/dicom-web-proxy.md diff --git a/extensions/default/src/DicomWebProxyDataSource/index.js b/extensions/default/src/DicomWebProxyDataSource/index.js index 5a9a9d34b7..55d9e586cb 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.js +++ b/extensions/default/src/DicomWebProxyDataSource/index.js @@ -1,6 +1,14 @@ import { IWebApiDataSource } from '@ohif/core'; import { createDicomWebApi } from '../DicomWebDataSource/index'; +/** + * This datasource is initialized with a url that returns a JSON object with a + * dicomWeb datasource configuration array present in a "servers" object. + * + * Only the first array item is parsed, if there are multiple items in the + * dicomWeb configuration array + * + */ function createDicomWebProxyApi( dicomWebProxyConfig, UserAuthenticationService @@ -12,27 +20,29 @@ function createDicomWebProxyApi( initialize: async ({ params, query, url }) => { let studyInstanceUIDs = []; + // there seem to be a couple of variations of the case for this parameter + const queryStudyInstanceUIDs = + query.get('studyInstanceUIDs') || query.get('studyInstanceUids'); + if (!queryStudyInstanceUIDs) { + throw new Error(`No studyInstanceUids in request for '${name}'`); + } + if (!url) url = query.get('url'); if (!url) { - console.error(`No url for '${name}'`); + throw new Error(`No url for '${name}'`); } else { const response = await fetch(url); let data = await response.json(); - - // there seem to be a couple of variations of the case for this parameter - const queryStudyInstanceUIDs = - query.get('studyInstanceUIDs') || query.get('studyInstanceUids'); - if (!queryStudyInstanceUIDs) { - console.error(`No studyInstanceUids in request for '${name}'`); - } - if (data.servers && queryStudyInstanceUIDs) { - dicomWebDelegate = createDicomWebApi( - data.servers.dicomWeb[0], - UserAuthenticationService - ); - studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); + if (!data.servers?.dicomWeb?.[0]) { + throw new Error('Invalid configuration returned by url'); } + + dicomWebDelegate = createDicomWebApi( + data.servers.dicomWeb[0], + UserAuthenticationService + ); + studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); } return studyInstanceUIDs; }, diff --git a/platform/docs/docs/configuration/dataSources/dicom-web-proxy.md b/platform/docs/docs/configuration/dataSources/dicom-web-proxy.md new file mode 100644 index 0000000000..6f98b36a45 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/dicom-web-proxy.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 4 +sidebar_label: DICOMweb Proxy +--- + +# DICOMweb Proxy + +You can launch the OHIF Viewer with a url that returns a JSON file which +contains a DICOMWeb configuration. The DICOMweb Proxy constructs a DICOMweb +datasource and delegates subsequent requests for metadata and images to that. + +Usage is similar to that of the [DICOM JSON](./dicom-json.md) datasource and +might look like + +`https://v3-demo.ohif.org/viewer/dicomwebproxy?url=https://ohif-dicom-json-example.s3.amazonaws.com/dicomweb.json` + +The url to the location of the JSON file is passed in the query +after the `dicomwebproxy` string, which is +`https://ohif-dicom-json-example.s3.amazonaws.com/dicomweb.json` (this json file +does not exist at the moment of this writing). + +## DICOMweb JSON configuration sample + +The json returned by the url in this example contains a dicomweb configuration +(see [DICOMweb](dicom-web.md)), in a "servers" object, which is then used to +construct a dynamic DICOMweb datasource to delegate requests to. Here is an +example configuration that might be returned using the url parameter. + +```json +{ + "servers": { + "dicomWeb": [ + { + "name": "DCM4CHEE", + "wadoUriRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado", + "qidoRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs", + "wadoRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs", + "qidoSupportsIncludeField": true, + "supportsReject": true, + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "enableStudyLazyLoad": true, + "supportsFuzzyMatching": true, + "supportsWildcard": true + } + ] + } +} +``` + +The DICOMweb Proxy expects the json returned by the url parameter it is invoked +with to include a servers object which contains a "dicomWeb" configuration array +as above. It will only consider the first array item in the dicomWeb +configuration. From 3637d4f0a56d663e83596fdbedb09df88f64a8e1 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 1 May 2023 09:43:55 -0400 Subject: [PATCH 4/5] review comments changes --- extensions/default/src/DicomWebProxyDataSource/index.js | 4 ++-- platform/viewer/public/config/default.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/default/src/DicomWebProxyDataSource/index.js b/extensions/default/src/DicomWebProxyDataSource/index.js index 55d9e586cb..cf016c825b 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.js +++ b/extensions/default/src/DicomWebProxyDataSource/index.js @@ -17,7 +17,7 @@ function createDicomWebProxyApi( let dicomWebDelegate = undefined; const implementation = { - initialize: async ({ params, query, url }) => { + initialize: async ({ params, query }) => { let studyInstanceUIDs = []; // there seem to be a couple of variations of the case for this parameter @@ -27,7 +27,7 @@ function createDicomWebProxyApi( throw new Error(`No studyInstanceUids in request for '${name}'`); } - if (!url) url = query.get('url'); + let url = query.get('url'); if (!url) { throw new Error(`No url for '${name}'`); diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index bb5a651790..05a4089692 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -24,8 +24,6 @@ window.config = { prefetch: 25, }, // filterQueryParam: false, - // enable imageLoaderAcceptHeader configuration to force a reconfiguration of WADOImageLoader to support preflight request - // imageLoaderAcceptHeader: 'multipart/related; type="application/octet-stream"', dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', From 4612d87004879ac162b423ce828bcab1b281107d Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 1 May 2023 09:59:32 -0400 Subject: [PATCH 5/5] another small code review fix --- extensions/default/src/DicomWebProxyDataSource/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/default/src/DicomWebProxyDataSource/index.js b/extensions/default/src/DicomWebProxyDataSource/index.js index cf016c825b..e873237dde 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.js +++ b/extensions/default/src/DicomWebProxyDataSource/index.js @@ -27,7 +27,7 @@ function createDicomWebProxyApi( throw new Error(`No studyInstanceUids in request for '${name}'`); } - let url = query.get('url'); + const url = query.get('url'); if (!url) { throw new Error(`No url for '${name}'`);