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.
This commit is contained in:
Joe Boccanfuso 2023-05-10 12:42:48 -04:00 committed by GitHub
parent 0eda151418
commit 8eaa1877fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 59 additions and 20 deletions

View File

@ -115,6 +115,18 @@ function createDicomLocalApi(dicomLocalConfig) {
}, },
}, },
retrieve: { 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: { series: {
metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => { metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => {
if (!StudyInstanceUID) { if (!StudyInstanceUID) {

View File

@ -178,7 +178,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI * or is already retrieved, or a promise to a URL for such use if a BulkDataURI
*/ */
directURL: params => { directURL: params => {
return getDirectURL(wadoRoot, params); return getDirectURL({ wadoRoot, singlepart }, params);
}, },
bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => { bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
const options = { const options = {
@ -394,7 +394,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
}; };
// Todo: this needs to be from wado dicom web client // Todo: this needs to be from wado dicom web client
return qidoDicomWebClient.retrieveBulkData(options).then(val => { 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; value.Value = ret;
return ret; return ret;
}); });

View File

@ -19,13 +19,13 @@ import {
* @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart, * @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 * 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 { const {
instance, instance,
tag = 'PixelData', tag = 'PixelData',
defaultPath = '/pixeldata', defaultPath = '/pixeldata',
defaultType = 'video/mp4', defaultType = 'video/mp4',
singlepart = null,
singlepart: fetchPart = 'video', singlepart: fetchPart = 'video',
} = params; } = params;
const value = instance[tag]; const value = instance[tag];
@ -53,11 +53,7 @@ const getDirectURL = (wadoRoot, params) => {
return undefined; return undefined;
} }
const { const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
} = instance;
const BulkDataURI = const BulkDataURI =
(value && value.BulkDataURI) || (value && value.BulkDataURI) ||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
@ -66,7 +62,13 @@ const getDirectURL = (wadoRoot, params) => {
const acceptUri = const acceptUri =
BulkDataURI + BulkDataURI +
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); (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) { if (BulkDataURI.indexOf('/') === 0) {
return wadoRoot + acceptUri; return wadoRoot + acceptUri;
} }

View File

@ -14,15 +14,14 @@ function OHIFCornerstonePdfViewport({ displaySets }) {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
await pdfUrl; setUrl(await pdfUrl);
setUrl(pdfUrl);
}; };
load(); load();
}, [pdfUrl]); }, [pdfUrl]);
return ( return (
<div className="bg-primary-black w-full h-full"> <div className="bg-primary-black w-full h-full text-white">
<object data={url} type="application/pdf" className="w-full h-full"> <object data={url} type="application/pdf" className="w-full h-full">
<div>No online PDF viewer installed</div> <div>No online PDF viewer installed</div>
</object> </object>

View File

@ -37,7 +37,18 @@ const _getDisplaySetsFromSeries = (
metadata.AvailableTransferSyntaxUID || metadata.AvailableTransferSyntaxUID ||
metadata.TransferSyntaxUID || metadata.TransferSyntaxUID ||
metadata['00083002']; 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 => { .map(instance => {
const { const {

View File

@ -14,8 +14,7 @@ function OHIFCornerstoneVideoViewport({ displaySets }) {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
await videoUrl; setUrl(await videoUrl);
setUrl(videoUrl);
}; };
load(); load();

View File

@ -549,6 +549,8 @@ const thumbnailNoImageModalities = [
'RTSTRUCT', 'RTSTRUCT',
'RTPLAN', 'RTPLAN',
'RTDOSE', 'RTDOSE',
'DOC',
'OT',
]; ];
function _getComponentType(Modality) { function _getComponentType(Modality) {

View File

@ -18,7 +18,7 @@ const ThumbnailNoImage = ({
isActive, isActive,
}) => { }) => {
const [collectedProps, drag, dragPreview] = useDrag({ const [collectedProps, drag, dragPreview] = useDrag({
type: "displayset", type: 'displayset',
item: { ...dragData }, item: { ...dragData },
canDrag: function(monitor) { canDrag: function(monitor) {
return Object.keys(dragData).length !== 0; return Object.keys(dragData).length !== 0;
@ -39,6 +39,7 @@ const ThumbnailNoImage = ({
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
role="button" role="button"
tabIndex="0" tabIndex="0"
data-cy={`study-browser-thumbnail-no-image`}
> >
<div ref={drag}> <div ref={drag}>
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">

View File

@ -6,7 +6,7 @@ describe('OHIF PDF Display', function() {
}); });
it('checks if series thumbnails are being displayed', 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') .its('length')
.should('be.gt', 0); .should('be.gt', 0);
}); });

View File

@ -5,13 +5,15 @@ describe('OHIF Video Display', function() {
}); });
it('checks if series thumbnails are being displayed', 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') .its('length')
.should('be.gt', 1); .should('be.gt', 1);
}); });
it('performs double-click to load thumbnail in active viewport', () => { 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'; //const expectedText = 'Ser: 3';
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);

View File

@ -32,6 +32,7 @@ window.config = {
auth: 'admin:admin', auth: 'admin:admin',
}, },
dicomUploadEnabled: true, dicomUploadEnabled: true,
singlepart: 'pdf,video',
}, },
}, },
{ {

View File

@ -21,6 +21,10 @@ const DICOMFileLoader = new (class extends FileLoader {
dicomData.meta dicomData.meta
); );
dataset.AvailableTransferSyntaxUID =
dataset.AvailableTransferSyntaxUID ||
dataset._meta.TransferSyntaxUID?.Value?.[0];
return dataset; return dataset;
} }
})(); })();