feat(PDF):Add PDF viewer (#2730)
Display DICOM encapsulated PDF documents in a PDF viewer. Addressed the remaining PR comments, and am merging this.
This commit is contained in:
parent
f3822f98a3
commit
82df3eac03
@ -25,7 +25,6 @@ import StaticWadoClient from './utils/StaticWadoClient.js';
|
|||||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||||
|
|
||||||
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
|
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
|
||||||
const { urlUtil } = utils;
|
|
||||||
|
|
||||||
const ImplementationClassUID =
|
const ImplementationClassUID =
|
||||||
'2.25.270695996825855179949881587723571202391.2.0.0';
|
'2.25.270695996825855179949881587723571202391.2.0.0';
|
||||||
@ -43,6 +42,7 @@ const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
|||||||
* @param {string} thumbnailRendering - wadors | ? (unsure of where/how this is used)
|
* @param {string} thumbnailRendering - wadors | ? (unsure of where/how this is used)
|
||||||
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
|
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
|
||||||
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
||||||
|
* @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true
|
||||||
*/
|
*/
|
||||||
function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||||
const {
|
const {
|
||||||
@ -53,17 +53,20 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
supportsWildcard,
|
supportsWildcard,
|
||||||
supportsReject,
|
supportsReject,
|
||||||
staticWado,
|
staticWado,
|
||||||
|
singlepart,
|
||||||
} = dicomWebConfig;
|
} = dicomWebConfig;
|
||||||
|
|
||||||
const qidoConfig = {
|
const qidoConfig = {
|
||||||
url: qidoRoot,
|
url: qidoRoot,
|
||||||
staticWado,
|
staticWado,
|
||||||
|
singlepart,
|
||||||
headers: UserAuthenticationService.getAuthorizationHeader(),
|
headers: UserAuthenticationService.getAuthorizationHeader(),
|
||||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const wadoConfig = {
|
const wadoConfig = {
|
||||||
url: wadoRoot,
|
url: wadoRoot,
|
||||||
|
singlepart,
|
||||||
headers: UserAuthenticationService.getAuthorizationHeader(),
|
headers: UserAuthenticationService.getAuthorizationHeader(),
|
||||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
};
|
};
|
||||||
@ -91,7 +94,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
query: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: mapParams.bind(),
|
mapParams: mapParams.bind(),
|
||||||
search: async function(origParams) {
|
search: async function (origParams) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
qidoDicomWebClient.headers = headers;
|
qidoDicomWebClient.headers = headers;
|
||||||
@ -116,7 +119,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
// mapParams: mapParams.bind(),
|
||||||
search: async function(studyInstanceUid) {
|
search: async function (studyInstanceUid) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
qidoDicomWebClient.headers = headers;
|
qidoDicomWebClient.headers = headers;
|
||||||
@ -149,13 +152,46 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
/* Generates a URL that can be used for direct retrieve of the bulkdata */
|
/**
|
||||||
|
* Generates a URL that can be used for direct retrieve of the bulkdata
|
||||||
|
*
|
||||||
|
* @param {object} params
|
||||||
|
* @param {string} params.tag is the tag name of the URL to retrieve
|
||||||
|
* @param {object} params.instance is the instance object that the tag is in
|
||||||
|
* @param {string} params.defaultType is the mime type of the response
|
||||||
|
* @param {string} params.singlepart is the type of the part to retrieve
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
directURL: (params) => {
|
directURL: (params) => {
|
||||||
const { instance, tag = "PixelData", defaultPath = "/pixeldata", defaultType = "video/mp4" } = params;
|
const {
|
||||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
instance,
|
||||||
// If the BulkDataURI isn't present, then assume it uses the pixeldata endpoint
|
tag = "PixelData",
|
||||||
// The standard isn't quite clear on that, but appears to be what is expected
|
defaultPath = "/pixeldata",
|
||||||
|
defaultType = "video/mp4",
|
||||||
|
singlepart: fetchPart = "video",
|
||||||
|
} = params;
|
||||||
const value = instance[tag];
|
const value = instance[tag];
|
||||||
|
if (!value) return undefined;
|
||||||
|
|
||||||
|
if (value.DirectRetrieveURL) return value.DirectRetrieveURL;
|
||||||
|
if (value.InlineBinary) {
|
||||||
|
const blob = utils.b64toBlob(value.InlineBinary, defaultType);
|
||||||
|
value.DirectRetrieveURL = URL.createObjectURL(blob);
|
||||||
|
return value.DirectRetrieveURL;
|
||||||
|
}
|
||||||
|
if (!singlepart || singlepart !== true && singlepart.indexOf(fetchPart) === -1) {
|
||||||
|
if (value.retrieveBulkData) {
|
||||||
|
return value.retrieveBulkData().then(arr => {
|
||||||
|
value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType }));
|
||||||
|
return value.DirectRetrieveURL;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.warn('Unable to retrieve', tag, 'from', instance);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||||
const BulkDataURI =
|
const BulkDataURI =
|
||||||
(value && value.BulkDataURI) ||
|
(value && value.BulkDataURI) ||
|
||||||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
|
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
|
||||||
@ -164,8 +200,8 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
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) return acceptUri;
|
||||||
if (BulkDataURI.indexOf('/') == 0) {
|
if (BulkDataURI.indexOf('/') === 0) {
|
||||||
return wadoRoot + acceptUri;
|
return wadoRoot + acceptUri;
|
||||||
}
|
}
|
||||||
if (BulkDataURI.indexOf('series/') == 0) {
|
if (BulkDataURI.indexOf('series/') == 0) {
|
||||||
@ -174,6 +210,9 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
if (BulkDataURI.indexOf('instances/') === 0) {
|
if (BulkDataURI.indexOf('instances/') === 0) {
|
||||||
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
|
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
|
||||||
}
|
}
|
||||||
|
if (BulkDataURI.indexOf('bulkdata/') === 0) {
|
||||||
|
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
|
||||||
|
}
|
||||||
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
|
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
|
||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
if (actual.length === 0) return true;
|
if (actual.length === 0) return true;
|
||||||
if (desired.length === 0 || desired === '*') return true;
|
if (desired.length === 0 || desired === '*') return true;
|
||||||
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
|
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
|
||||||
console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
|
// console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
|
||||||
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
|
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
|
||||||
} else if (desired[desired.length - 1] === '*') {
|
} else if (desired[desired.length - 1] === '*') {
|
||||||
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
|
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
|
||||||
|
|||||||
21
extensions/dicom-pdf/LICENSE
Normal file
21
extensions/dicom-pdf/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Open Health Imaging Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
5
extensions/dicom-pdf/README.md
Normal file
5
extensions/dicom-pdf/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# DICOM Encapsulated PDF
|
||||||
|
This extension adds support for displaying DICOM encapsulated PDF documents.
|
||||||
|
|
||||||
|
The extension is a "standard" extension in that it is installed and available
|
||||||
|
by default.
|
||||||
49
extensions/dicom-pdf/package.json
Normal file
49
extensions/dicom-pdf/package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-dicom-pdf",
|
||||||
|
"version": "3.0.1",
|
||||||
|
"description": "OHIF extension for PDF display",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"main": "dist/index.umd.js",
|
||||||
|
"module": "src/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||||
|
"dev:cornerstone": "yarn run dev",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package": "yarn run build",
|
||||||
|
"start": "yarn run dev",
|
||||||
|
"test:unit": "jest --watchAll",
|
||||||
|
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "^0.50.0",
|
||||||
|
"@ohif/ui": "^0.50.0",
|
||||||
|
"cornerstone-core": "^2.6.0",
|
||||||
|
"cornerstone-math": "^0.1.9",
|
||||||
|
"cornerstone-tools": "6.0.2",
|
||||||
|
"cornerstone-wado-image-loader": "4.0.4",
|
||||||
|
"dcmjs": "0.16.1",
|
||||||
|
"dicom-parser": "^1.8.9",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-cornerstone-viewport": "4.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "7.7.6",
|
||||||
|
"classnames": "^2.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
70
extensions/dicom-pdf/src/getSopClassHandlerModule.js
Normal file
70
extensions/dicom-pdf/src/getSopClassHandlerModule.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Name, SOPClassHandlerId } from './id';
|
||||||
|
import { utils, classes } from '@ohif/core';
|
||||||
|
|
||||||
|
const { ImageSet } = classes;
|
||||||
|
|
||||||
|
const SOP_CLASS_UIDS = {
|
||||||
|
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sopClassUids = Object.values(SOP_CLASS_UIDS);
|
||||||
|
|
||||||
|
|
||||||
|
const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => {
|
||||||
|
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||||
|
return instances
|
||||||
|
.map(instance => {
|
||||||
|
const { Modality, SOPInstanceUID, EncapsulatedDocument } = instance;
|
||||||
|
const { SeriesDescription = "PDF", MIMETypeOfEncapsulatedDocument, } = instance;
|
||||||
|
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, } = instance;
|
||||||
|
const pdfUrl = dataSource.retrieve.directURL({
|
||||||
|
instance,
|
||||||
|
tag: 'EncapsulatedDocument',
|
||||||
|
defaultType: MIMETypeOfEncapsulatedDocument || "application/pdf",
|
||||||
|
singlepart: "pdf",
|
||||||
|
});
|
||||||
|
|
||||||
|
const displaySet = {
|
||||||
|
//plugin: id,
|
||||||
|
Modality,
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPClassHandlerId,
|
||||||
|
referencedImages: null,
|
||||||
|
measurements: null,
|
||||||
|
pdfUrl,
|
||||||
|
others: [instance],
|
||||||
|
thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }),
|
||||||
|
isDerivedDisplaySet: true,
|
||||||
|
isLoaded: false,
|
||||||
|
sopClassUids,
|
||||||
|
numImageFrames: 0,
|
||||||
|
numInstances: 1,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
return displaySet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||||
|
const getDisplaySetsFromSeries = instances => {
|
||||||
|
return _getDisplaySetsFromSeries(
|
||||||
|
instances,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: Name,
|
||||||
|
sopClassUids,
|
||||||
|
getDisplaySetsFromSeries,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
8
extensions/dicom-pdf/src/id.js
Normal file
8
extensions/dicom-pdf/src/id.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const Name = 'dicom-pdf';
|
||||||
|
const id = `org.ohif.${Name}`;
|
||||||
|
|
||||||
|
export default id;
|
||||||
|
|
||||||
|
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${Name}`;
|
||||||
|
|
||||||
|
export { Name, SOPClassHandlerId, };
|
||||||
76
extensions/dicom-pdf/src/index.js
Normal file
76
extensions/dicom-pdf/src/index.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
|
import id from './id.js';
|
||||||
|
|
||||||
|
const Component = React.lazy(() => {
|
||||||
|
return import(
|
||||||
|
/* webpackPrefetch: true */ './viewports/OHIFCornerstonePdfViewport'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const OHIFCornerstonePdfViewport = props => {
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Component {...props} />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
dependencies: [
|
||||||
|
// TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future.
|
||||||
|
// OHIF-229
|
||||||
|
{
|
||||||
|
id: 'org.ohif.cornerstone',
|
||||||
|
version: '3.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'org.ohif.measurement-tracking',
|
||||||
|
version: '^0.0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
|
// No-op for now
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
getViewportModule({ servicesManager, extensionManager }) {
|
||||||
|
const ExtendedOHIFCornerstonePdfViewport = props => {
|
||||||
|
return (
|
||||||
|
<OHIFCornerstonePdfViewport
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }];
|
||||||
|
},
|
||||||
|
getCommandsModule({ servicesManager }) {
|
||||||
|
return {
|
||||||
|
definitions: {
|
||||||
|
setToolActive: {
|
||||||
|
commandFn: () => null,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultContext: 'ACTIVE_VIEWPORT::PDF',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getSopClassHandlerModule,
|
||||||
|
};
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function OHIFCornerstonePdfViewport({
|
||||||
|
displaySet,
|
||||||
|
}) {
|
||||||
|
const [url, setUrl] = useState(null);
|
||||||
|
const { pdfUrl } = displaySet;
|
||||||
|
useEffect(async () => {
|
||||||
|
setUrl(await pdfUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-primary-black w-full h-full">
|
||||||
|
<object data={url} type="application/pdf" className="w-full h-full">
|
||||||
|
<div>No online PDF viewer installed</div>
|
||||||
|
</object>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OHIFCornerstonePdfViewport.propTypes = {
|
||||||
|
displaySet: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OHIFCornerstonePdfViewport;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-video",
|
"name": "@ohif/extension-dicom-video",
|
||||||
"version": "0.0.1",
|
"version": "3.0.1",
|
||||||
"description": "OHIF extension for video display",
|
"description": "OHIF extension for video display",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
import { Name, SOPClassHandlerId } from './id';
|
||||||
import { utils, classes } from '@ohif/core';
|
import { utils, } from '@ohif/core';
|
||||||
|
|
||||||
const { ImageSet } = classes;
|
|
||||||
|
|
||||||
const SOP_CLASS_UIDS = {
|
const SOP_CLASS_UIDS = {
|
||||||
VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1',
|
VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1',
|
||||||
@ -36,8 +34,7 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
|||||||
return supportedTransferSyntaxUIDs.includes(tsuid);
|
return supportedTransferSyntaxUIDs.includes(tsuid);
|
||||||
})
|
})
|
||||||
.map(instance => {
|
.map(instance => {
|
||||||
const { Modality, FrameOfReferenceUID, SOPInstanceUID } = instance;
|
const { Modality, SOPInstanceUID, SeriesDescription = "VIDEO" } = instance;
|
||||||
const { SeriesDescription, ContentDate, ContentTime } = instance;
|
|
||||||
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames } = instance;
|
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames } = instance;
|
||||||
const displaySet = {
|
const displaySet = {
|
||||||
//plugin: id,
|
//plugin: id,
|
||||||
@ -52,7 +49,7 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
|||||||
SOPClassHandlerId,
|
SOPClassHandlerId,
|
||||||
referencedImages: null,
|
referencedImages: null,
|
||||||
measurements: null,
|
measurements: null,
|
||||||
videoUrl: dataSource.retrieve.directURL({ instance }),
|
videoUrl: dataSource.retrieve.directURL({ instance, singlepart: "video", tag: "PixelData", }),
|
||||||
others: [instance],
|
others: [instance],
|
||||||
thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }),
|
thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }),
|
||||||
isDerivedDisplaySet: true,
|
isDerivedDisplaySet: true,
|
||||||
@ -76,7 +73,7 @@ export default function getSopClassHandlerModule({ servicesManager, extensionMan
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: SOPClassHandlerName,
|
name: Name,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
getDisplaySetsFromSeries,
|
getDisplaySetsFromSeries,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
const id = 'org.ohif.dicom-video';
|
const Name = 'dicom-video';
|
||||||
|
const id = `org.ohif.${Name}`;
|
||||||
|
|
||||||
export default id;
|
export default id;
|
||||||
|
|
||||||
const SOPClassHandlerName = 'dicom-video';
|
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${Name}`;
|
||||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
|
||||||
export { SOPClassHandlerName, SOPClassHandlerId };
|
export { Name, SOPClassHandlerId, };
|
||||||
|
|||||||
@ -1,51 +1,36 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
|
||||||
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
|
||||||
import {
|
|
||||||
Notification,
|
|
||||||
ViewportActionBar,
|
|
||||||
useViewportGrid,
|
|
||||||
useViewportDialog,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
import { adapters } from 'dcmjs';
|
|
||||||
import id from '../id';
|
|
||||||
|
|
||||||
|
|
||||||
function OHIFCornerstoneVideoViewport({
|
function OHIFCornerstoneVideoViewport({
|
||||||
children,
|
|
||||||
dataSource,
|
|
||||||
displaySet,
|
displaySet,
|
||||||
viewportIndex,
|
|
||||||
servicesManager,
|
|
||||||
extensionManager,
|
|
||||||
}) {
|
}) {
|
||||||
const {
|
|
||||||
DisplaySetService,
|
|
||||||
MeasurementService,
|
|
||||||
ToolBarService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
const { videoUrl } = displaySet;
|
const { videoUrl } = displaySet;
|
||||||
const mimeType = "video/mp4";
|
const mimeType = "video/mp4";
|
||||||
|
const [url, setUrl] = useState(null);
|
||||||
|
useEffect(async () => {
|
||||||
|
setUrl(await videoUrl);
|
||||||
|
});
|
||||||
|
|
||||||
// Need to copies of the source to fix a firefox bug
|
// Need to copies of the source to fix a firefox bug
|
||||||
return (
|
return (
|
||||||
<div className="bg-primary-black w-full h-full">
|
<div className="bg-primary-black w-full h-full">
|
||||||
<video
|
<video
|
||||||
src={videoUrl}
|
src={url}
|
||||||
controls
|
controls
|
||||||
controlsList="nodownload"
|
controlsList="nodownload"
|
||||||
preload="auto"
|
preload="auto"
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
>
|
>
|
||||||
<source src={videoUrl} type={mimeType} />
|
<source src={url} type={mimeType} />
|
||||||
<source src={videoUrl} type={mimeType} />
|
<source src={url} type={mimeType} />
|
||||||
Video src/type not supported: <a href={videoUrl}>{videoUrl} of type {mimeType}</a>
|
Video src/type not supported: <a href={url}>{url} of type {mimeType}</a>
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OHIFCornerstoneVideoViewport.propTypes = {
|
||||||
|
displaySet: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default OHIFCornerstoneVideoViewport;
|
export default OHIFCornerstoneVideoViewport;
|
||||||
|
|||||||
@ -23,6 +23,11 @@ const dicomvideo = {
|
|||||||
viewport: 'org.ohif.dicom-video.viewportModule.dicom-video',
|
viewport: 'org.ohif.dicom-video.viewportModule.dicom-video',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dicompdf = {
|
||||||
|
sopClassHandler: 'org.ohif.dicom-pdf.sopClassHandlerModule.dicom-pdf',
|
||||||
|
viewport: 'org.ohif.dicom-pdf.viewportModule.dicom-pdf',
|
||||||
|
}
|
||||||
|
|
||||||
export default function mode({ modeConfiguration }) {
|
export default function mode({ modeConfiguration }) {
|
||||||
return {
|
return {
|
||||||
// TODO: We're using this as a route segment
|
// TODO: We're using this as a route segment
|
||||||
@ -96,6 +101,10 @@ export default function mode({ modeConfiguration }) {
|
|||||||
namespace: dicomvideo.viewport,
|
namespace: dicomvideo.viewport,
|
||||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
namespace: dicompdf.viewport,
|
||||||
|
displaySetsToDisplay: [dicompdf.sopClassHandler],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -108,9 +117,18 @@ export default function mode({ modeConfiguration }) {
|
|||||||
'org.ohif.measurement-tracking',
|
'org.ohif.measurement-tracking',
|
||||||
'org.ohif.dicom-sr',
|
'org.ohif.dicom-sr',
|
||||||
'org.ohif.dicom-video',
|
'org.ohif.dicom-video',
|
||||||
|
'org.ohif.dicom-pdf',
|
||||||
],
|
],
|
||||||
hangingProtocols: [ohif.hangingProtocols],
|
hangingProtocols: [ohif.hangingProtocols],
|
||||||
sopClassHandlers: [dicomvideo.sopClassHandler, ohif.sopClassHandler, dicomsr.sopClassHandler,],
|
// Order is important in sop class handlers when two handlers both use
|
||||||
|
// the same sop class under different situations. In that case, the more
|
||||||
|
// general handler needs to come last. For this case, the dicomvideo msut
|
||||||
|
// come first to remove video transfer syntax before ohif uses images
|
||||||
|
sopClassHandlers: [
|
||||||
|
dicomvideo.sopClassHandler,
|
||||||
|
ohif.sopClassHandler,
|
||||||
|
dicompdf.sopClassHandler,
|
||||||
|
dicomsr.sopClassHandler,],
|
||||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,6 @@
|
|||||||
"link-list": "npm ls --depth=0 --link=true"
|
"link-list": "npm ls --depth=0 --link=true"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"config-point": "^0.3.4",
|
|
||||||
"@babel/runtime": "7.16.3",
|
"@babel/runtime": "7.16.3",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
"cornerstone-core": "2.6.0",
|
"cornerstone-core": "2.6.0",
|
||||||
|
|||||||
@ -24,12 +24,16 @@ const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
|||||||
const Dotenv = require('dotenv-webpack');
|
const Dotenv = require('dotenv-webpack');
|
||||||
|
|
||||||
const setHeaders = (res, path) => {
|
const setHeaders = (res, path) => {
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
|
||||||
if (path.indexOf('.gz') !== -1) {
|
if (path.indexOf('.gz') !== -1) {
|
||||||
res.setHeader('Content-Encoding', 'gzip')
|
res.setHeader('Content-Encoding', 'gzip')
|
||||||
} else if (path.indexOf('.br') !== -1) {
|
} else if (path.indexOf('.br') !== -1) {
|
||||||
res.setHeader('Content-Encoding', 'br')
|
res.setHeader('Content-Encoding', 'br')
|
||||||
}
|
}
|
||||||
|
if (path.indexOf('.pdf') !== -1) {
|
||||||
|
res.setHeader('Content-Type', 'application/pdf');
|
||||||
|
} else {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (env, argv) => {
|
module.exports = (env, argv) => {
|
||||||
|
|||||||
18
platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js
Normal file
18
platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
describe('OHIF PDF Display', function () {
|
||||||
|
before(() => {
|
||||||
|
cy.openStudyInViewer(
|
||||||
|
'2.25.317377619501274872606137091638706705333'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
cy.resetViewport().wait(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks if series thumbnails are being displayed', function () {
|
||||||
|
cy.get('[data-cy="study-browser-thumbnail"]')
|
||||||
|
.its('length')
|
||||||
|
.should('be.gt', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -57,7 +57,6 @@
|
|||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"@types/react": "^16.0.0",
|
"@types/react": "^16.0.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"config-point": "^0.3.4",
|
|
||||||
"core-js": "^3.16.1",
|
"core-js": "^3.16.1",
|
||||||
"cornerstone-math": "^0.1.9",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "6.0.2",
|
"cornerstone-tools": "6.0.2",
|
||||||
|
|||||||
@ -23,6 +23,7 @@ window.config = {
|
|||||||
supportsFuzzyMatching: false,
|
supportsFuzzyMatching: false,
|
||||||
supportsWildcard: true,
|
supportsWildcard: true,
|
||||||
staticWado: true,
|
staticWado: true,
|
||||||
|
singlepart: "video,thumbnail,pdf",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ window.config = {
|
|||||||
supportsFuzzyMatching: false,
|
supportsFuzzyMatching: false,
|
||||||
supportsWildcard: true,
|
supportsWildcard: true,
|
||||||
staticWado: true,
|
staticWado: true,
|
||||||
|
singlepart: "bulkdata,video,pdf",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
errorHandler
|
errorHandler
|
||||||
// utils,
|
// utils,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
import ConfigPoint from 'config-point';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
||||||
@ -50,10 +49,6 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
appConfig,
|
appConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load the default theme settings
|
|
||||||
const defaultTheme = config && config.defaultTheme || 'theme';
|
|
||||||
ConfigPoint.load(defaultTheme, '/theme', 'theme');
|
|
||||||
|
|
||||||
servicesManager.registerServices([
|
servicesManager.registerServices([
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
|||||||
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
|
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
|
||||||
import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr';
|
import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr';
|
||||||
import OHIFDICOMVIDEOExtension from '@ohif/extension-dicom-video';
|
import OHIFDICOMVIDEOExtension from '@ohif/extension-dicom-video';
|
||||||
|
import OHIFDICOMPDFExtension from '@ohif/extension-dicom-pdf';
|
||||||
|
|
||||||
/** Combine our appConfiguration and "baked-in" extensions */
|
/** Combine our appConfiguration and "baked-in" extensions */
|
||||||
const appProps = {
|
const appProps = {
|
||||||
@ -34,11 +35,11 @@ const appProps = {
|
|||||||
OHIFMeasurementTrackingExtension,
|
OHIFMeasurementTrackingExtension,
|
||||||
OHIFDICOMSRExtension,
|
OHIFDICOMSRExtension,
|
||||||
OHIFDICOMVIDEOExtension,
|
OHIFDICOMVIDEOExtension,
|
||||||
|
OHIFDICOMPDFExtension,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Create App */
|
/** Create App */
|
||||||
const app = React.createElement(App, appProps, null);
|
const app = React.createElement(App, appProps, null);
|
||||||
|
|
||||||
/** Render */
|
/** Render */
|
||||||
ReactDOM.render(app, document.getElementById('root'));
|
ReactDOM.render(app, document.getElementById('root'));
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@ -1236,7 +1236,7 @@
|
|||||||
core-js-pure "^3.20.2"
|
core-js-pure "^3.20.2"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@7.1.2", "@babel/runtime@7.16.3", "@babel/runtime@7.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
"@babel/runtime@7.1.2", "@babel/runtime@7.16.3", "@babel/runtime@7.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||||
version "7.16.3"
|
version "7.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||||
@ -7943,14 +7943,6 @@ config-chain@^1.1.11:
|
|||||||
ini "^1.3.4"
|
ini "^1.3.4"
|
||||||
proto-list "~1.2.1"
|
proto-list "~1.2.1"
|
||||||
|
|
||||||
config-point@^0.3.4:
|
|
||||||
version "0.3.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/config-point/-/config-point-0.3.4.tgz#ac4ec8f39432400b6f31b0fe5d6edcc18fe8650a"
|
|
||||||
integrity sha512-/5heLx9DqfZKduBF09w64FPxLzPBD+yYepNvGS6rexUVfIBCKiE0hQkALvriqEzX6MvEA5U2gxdrd+mQLlTCtw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.14.6"
|
|
||||||
json5 "^2.2.0"
|
|
||||||
|
|
||||||
configstore@^5.0.1:
|
configstore@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user