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:
parent
0eda151418
commit
8eaa1877fd
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -14,8 +14,7 @@ function OHIFCornerstoneVideoViewport({ displaySets }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
await videoUrl;
|
setUrl(await videoUrl);
|
||||||
setUrl(videoUrl);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|||||||
@ -549,6 +549,8 @@ const thumbnailNoImageModalities = [
|
|||||||
'RTSTRUCT',
|
'RTSTRUCT',
|
||||||
'RTPLAN',
|
'RTPLAN',
|
||||||
'RTDOSE',
|
'RTDOSE',
|
||||||
|
'DOC',
|
||||||
|
'OT',
|
||||||
];
|
];
|
||||||
|
|
||||||
function _getComponentType(Modality) {
|
function _getComponentType(Modality) {
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -32,6 +32,7 @@ window.config = {
|
|||||||
auth: 'admin:admin',
|
auth: 'admin:admin',
|
||||||
},
|
},
|
||||||
dicomUploadEnabled: true,
|
dicomUploadEnabled: true,
|
||||||
|
singlepart: 'pdf,video',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user