From 8eaa1877fd8f3a3b33a91f9c805bd973c74d722f Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Wed, 10 May 2023 12:42:48 -0400 Subject: [PATCH] fix(DICOM PDF and Video): fixes for local DICOM load and remote data sources (#3374) * fix(DICOM PDF and Video) - added retrieve.directURL for local DICOM load data source; it returns a data URL - fixed various checks for video using transfer syntaxes and SOP class UID with number of frames - added call to 'rendered' endpoint for those data sources that support it; others get BulkDataURI * Added DOC (PDF) and OT (for video in particular) to list of non-image thumbnail modalities. * Fixed broken e2e tests. --- .../default/src/DicomLocalDataSource/index.js | 12 ++++++++++++ .../default/src/DicomWebDataSource/index.js | 10 ++++++++-- extensions/default/src/utils/getDirectURL.js | 18 ++++++++++-------- .../viewports/OHIFCornerstonePdfViewport.tsx | 5 ++--- .../src/getSopClassHandlerModule.js | 13 ++++++++++++- .../viewports/OHIFCornerstoneVideoViewport.tsx | 3 +-- .../PanelStudyBrowserTracking.tsx | 2 ++ .../ThumbnailNoImage/ThumbnailNoImage.tsx | 3 ++- .../cypress/integration/OHIFPdfDisplay.spec.js | 2 +- .../integration/OHIFVideoDisplay.spec.js | 6 ++++-- .../viewer/public/config/local_dcm4chee.js | 1 + .../viewer/src/routes/Local/dicomFileLoader.js | 4 ++++ 12 files changed, 59 insertions(+), 20 deletions(-) diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index f10d427ba..d7b3d7106 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -115,6 +115,18 @@ function createDicomLocalApi(dicomLocalConfig) { }, }, retrieve: { + directURL: params => { + const { instance, tag, defaultType } = params; + + const value = instance[tag]; + if (value instanceof Array && value[0] instanceof ArrayBuffer) { + return URL.createObjectURL( + new Blob([value[0]], { + type: defaultType, + }) + ); + } + }, series: { metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => { if (!StudyInstanceUID) { diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index c5782375c..01943417e 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -178,7 +178,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { * or is already retrieved, or a promise to a URL for such use if a BulkDataURI */ directURL: params => { - return getDirectURL(wadoRoot, params); + return getDirectURL({ wadoRoot, singlepart }, params); }, bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => { const options = { @@ -394,7 +394,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }; // Todo: this needs to be from wado dicom web client return qidoDicomWebClient.retrieveBulkData(options).then(val => { - const ret = (val && val[0]) || undefined; + // There are DICOM PDF cases where the first ArrayBuffer in the array is + // the bulk data and DICOM video cases where the second ArrayBuffer is + // the bulk data. Here we play it safe and do a find. + const ret = + (val instanceof Array && + val.find(arrayBuffer => arrayBuffer?.byteLength)) || + undefined; value.Value = ret; return ret; }); diff --git a/extensions/default/src/utils/getDirectURL.js b/extensions/default/src/utils/getDirectURL.js index e90ff17af..4580b8d30 100644 --- a/extensions/default/src/utils/getDirectURL.js +++ b/extensions/default/src/utils/getDirectURL.js @@ -19,13 +19,13 @@ import { * @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart, * or is already retrieved, or a promise to a URL for such use if a BulkDataURI */ -const getDirectURL = (wadoRoot, params) => { +const getDirectURL = (config, params) => { + const { wadoRoot, singlepart } = config; const { instance, tag = 'PixelData', defaultPath = '/pixeldata', defaultType = 'video/mp4', - singlepart = null, singlepart: fetchPart = 'video', } = params; const value = instance[tag]; @@ -53,11 +53,7 @@ const getDirectURL = (wadoRoot, params) => { return undefined; } - const { - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID, - } = instance; + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; const BulkDataURI = (value && value.BulkDataURI) || `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; @@ -66,7 +62,13 @@ const getDirectURL = (wadoRoot, params) => { const acceptUri = BulkDataURI + (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); - if (BulkDataURI.indexOf('http') === 0) return acceptUri; + if (BulkDataURI.indexOf('http') === 0) { + if (tag === 'PixelData' || tag === 'EncapsulatedDocument') { + return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`; + } else { + return acceptUri; + } + } if (BulkDataURI.indexOf('/') === 0) { return wadoRoot + acceptUri; } diff --git a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx index 99488a21f..9da1904c1 100644 --- a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx +++ b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx @@ -14,15 +14,14 @@ function OHIFCornerstonePdfViewport({ displaySets }) { useEffect(() => { const load = async () => { - await pdfUrl; - setUrl(pdfUrl); + setUrl(await pdfUrl); }; load(); }, [pdfUrl]); return ( -
+
No online PDF viewer installed
diff --git a/extensions/dicom-video/src/getSopClassHandlerModule.js b/extensions/dicom-video/src/getSopClassHandlerModule.js index 4be185e74..72fc00884 100644 --- a/extensions/dicom-video/src/getSopClassHandlerModule.js +++ b/extensions/dicom-video/src/getSopClassHandlerModule.js @@ -37,7 +37,18 @@ const _getDisplaySetsFromSeries = ( metadata.AvailableTransferSyntaxUID || metadata.TransferSyntaxUID || metadata['00083002']; - return supportedTransferSyntaxUIDs.includes(tsuid); + + if (supportedTransferSyntaxUIDs.includes(tsuid)) { + return true; + } + + // Assume that an instance with certain SOPClassUID and + // with at least 90 frames (i.e. typically 3 seconds of video) is indeed a video. + return ( + metadata.SOPClassUID === + SOP_CLASS_UIDS.MULTIFRAME_TRUE_COLOR_SECONDARY_CAPTURE_IMAGE_STORAGE && + metadata.NumberOfFrames >= 90 + ); }) .map(instance => { const { diff --git a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx index 56545b2a0..c6aa80b2f 100644 --- a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx +++ b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx @@ -14,8 +14,7 @@ function OHIFCornerstoneVideoViewport({ displaySets }) { useEffect(() => { const load = async () => { - await videoUrl; - setUrl(videoUrl); + setUrl(await videoUrl); }; load(); diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index e81309ce2..ca1f383e2 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -549,6 +549,8 @@ const thumbnailNoImageModalities = [ 'RTSTRUCT', 'RTPLAN', 'RTDOSE', + 'DOC', + 'OT', ]; function _getComponentType(Modality) { diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx index 1a6e24952..83eb44536 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx @@ -18,7 +18,7 @@ const ThumbnailNoImage = ({ isActive, }) => { const [collectedProps, drag, dragPreview] = useDrag({ - type: "displayset", + type: 'displayset', item: { ...dragData }, canDrag: function(monitor) { return Object.keys(dragData).length !== 0; @@ -39,6 +39,7 @@ const ThumbnailNoImage = ({ onDoubleClick={onDoubleClick} role="button" tabIndex="0" + data-cy={`study-browser-thumbnail-no-image`} >
diff --git a/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js b/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js index 954dca093..8027380f6 100644 --- a/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js +++ b/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js @@ -6,7 +6,7 @@ describe('OHIF PDF Display', function() { }); it('checks if series thumbnails are being displayed', function() { - cy.get('[data-cy="study-browser-thumbnail"]') + cy.get('[data-cy="study-browser-thumbnail-no-image"]') .its('length') .should('be.gt', 0); }); diff --git a/platform/viewer/cypress/integration/OHIFVideoDisplay.spec.js b/platform/viewer/cypress/integration/OHIFVideoDisplay.spec.js index b03ec0a42..e312144ee 100644 --- a/platform/viewer/cypress/integration/OHIFVideoDisplay.spec.js +++ b/platform/viewer/cypress/integration/OHIFVideoDisplay.spec.js @@ -5,13 +5,15 @@ describe('OHIF Video Display', function() { }); it('checks if series thumbnails are being displayed', function() { - cy.get('[data-cy="study-browser-thumbnail"]') + cy.get('[data-cy="study-browser-thumbnail-no-image"]') .its('length') .should('be.gt', 1); }); it('performs double-click to load thumbnail in active viewport', () => { - cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick(); + cy.get( + '[data-cy="study-browser-thumbnail-no-image"]:nth-child(2)' + ).dblclick(); //const expectedText = 'Ser: 3'; //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); diff --git a/platform/viewer/public/config/local_dcm4chee.js b/platform/viewer/public/config/local_dcm4chee.js index d47c4c34a..682501fff 100644 --- a/platform/viewer/public/config/local_dcm4chee.js +++ b/platform/viewer/public/config/local_dcm4chee.js @@ -32,6 +32,7 @@ window.config = { auth: 'admin:admin', }, dicomUploadEnabled: true, + singlepart: 'pdf,video', }, }, { diff --git a/platform/viewer/src/routes/Local/dicomFileLoader.js b/platform/viewer/src/routes/Local/dicomFileLoader.js index 1188e3169..d9e2ea92c 100644 --- a/platform/viewer/src/routes/Local/dicomFileLoader.js +++ b/platform/viewer/src/routes/Local/dicomFileLoader.js @@ -21,6 +21,10 @@ const DICOMFileLoader = new (class extends FileLoader { dicomData.meta ); + dataset.AvailableTransferSyntaxUID = + dataset.AvailableTransferSyntaxUID || + dataset._meta.TransferSyntaxUID?.Value?.[0]; + return dataset; } })();