From 82df3eac03cc2afa52672895ce159860112df6ff Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 4 Apr 2022 09:48:14 -0400 Subject: [PATCH] feat(PDF):Add PDF viewer (#2730) Display DICOM encapsulated PDF documents in a PDF viewer. Addressed the remaining PR comments, and am merging this. --- .../default/src/DicomWebDataSource/index.js | 59 +++++++++++--- .../utils/StaticWadoClient.js | 2 +- extensions/dicom-pdf/LICENSE | 21 +++++ extensions/dicom-pdf/README.md | 5 ++ extensions/dicom-pdf/package.json | 49 ++++++++++++ .../dicom-pdf/src/getSopClassHandlerModule.js | 70 +++++++++++++++++ extensions/dicom-pdf/src/id.js | 8 ++ extensions/dicom-pdf/src/index.js | 76 +++++++++++++++++++ .../viewports/OHIFCornerstonePdfViewport.js | 26 +++++++ extensions/dicom-video/package.json | 2 +- .../src/getSopClassHandlerModule.js | 13 ++-- extensions/dicom-video/src/id.js | 9 ++- .../viewports/OHIFCornerstoneVideoViewport.js | 41 ++++------ modes/longitudinal/src/index.js | 20 ++++- package.json | 1 - platform/viewer/.webpack/webpack.pwa.js | 6 +- .../integration/OHIFPdfDisplay.spec.js | 18 +++++ platform/viewer/package.json | 1 - platform/viewer/public/config/e2e.js | 1 + platform/viewer/public/config/local_static.js | 1 + platform/viewer/src/appInit.js | 5 -- platform/viewer/src/index.js | 3 +- yarn.lock | 10 +-- 23 files changed, 376 insertions(+), 71 deletions(-) create mode 100644 extensions/dicom-pdf/LICENSE create mode 100644 extensions/dicom-pdf/README.md create mode 100644 extensions/dicom-pdf/package.json create mode 100644 extensions/dicom-pdf/src/getSopClassHandlerModule.js create mode 100644 extensions/dicom-pdf/src/id.js create mode 100644 extensions/dicom-pdf/src/index.js create mode 100644 extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.js create mode 100644 platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index c83fabfdfb4..c1df31e461d 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -25,7 +25,6 @@ import StaticWadoClient from './utils/StaticWadoClient.js'; const { DicomMetaDictionary, DicomDict } = dcmjs.data; const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; -const { urlUtil } = utils; const ImplementationClassUID = '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 {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE) * @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) { const { @@ -53,17 +53,20 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { supportsWildcard, supportsReject, staticWado, + singlepart, } = dicomWebConfig; const qidoConfig = { url: qidoRoot, staticWado, + singlepart, headers: UserAuthenticationService.getAuthorizationHeader(), errorInterceptor: errorHandler.getHTTPErrorHandler(), }; const wadoConfig = { url: wadoRoot, + singlepart, headers: UserAuthenticationService.getAuthorizationHeader(), errorInterceptor: errorHandler.getHTTPErrorHandler(), }; @@ -91,7 +94,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { query: { studies: { mapParams: mapParams.bind(), - search: async function(origParams) { + search: async function (origParams) { const headers = UserAuthenticationService.getAuthorizationHeader(); if (headers) { qidoDicomWebClient.headers = headers; @@ -116,7 +119,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { }, series: { // mapParams: mapParams.bind(), - search: async function(studyInstanceUid) { + search: async function (studyInstanceUid) { const headers = UserAuthenticationService.getAuthorizationHeader(); if (headers) { qidoDicomWebClient.headers = headers; @@ -149,13 +152,46 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { }, }, 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) => { - const { instance, tag = "PixelData", defaultPath = "/pixeldata", defaultType = "video/mp4" } = params; - const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; - // If the BulkDataURI isn't present, then assume it uses the pixeldata endpoint - // The standard isn't quite clear on that, but appears to be what is expected + const { + instance, + tag = "PixelData", + defaultPath = "/pixeldata", + defaultType = "video/mp4", + singlepart: fetchPart = "video", + } = params; 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 = (value && value.BulkDataURI) || `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; @@ -164,8 +200,8 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { const acceptUri = BulkDataURI + (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); - if (BulkDataURI.indexOf('http') == 0) return acceptUri; - if (BulkDataURI.indexOf('/') == 0) { + if (BulkDataURI.indexOf('http') === 0) return acceptUri; + if (BulkDataURI.indexOf('/') === 0) { return wadoRoot + acceptUri; } if (BulkDataURI.indexOf('series/') == 0) { @@ -174,6 +210,9 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { if (BulkDataURI.indexOf('instances/') === 0) { 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); }, series: { diff --git a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js index 92f40f66b0e..d250ecacc74 100644 --- a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js +++ b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js @@ -70,7 +70,7 @@ export default class StaticWadoClient extends api.DICOMwebClient { if (actual.length === 0) return true; if (desired.length === 0 || desired === '*') return true; 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; } else if (desired[desired.length - 1] === '*') { return actual.indexOf(desired.substring(0, desired.length - 1)) != -1; diff --git a/extensions/dicom-pdf/LICENSE b/extensions/dicom-pdf/LICENSE new file mode 100644 index 00000000000..19e20dd35ca --- /dev/null +++ b/extensions/dicom-pdf/LICENSE @@ -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. diff --git a/extensions/dicom-pdf/README.md b/extensions/dicom-pdf/README.md new file mode 100644 index 00000000000..1e2c6085d2e --- /dev/null +++ b/extensions/dicom-pdf/README.md @@ -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. diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json new file mode 100644 index 00000000000..17ea43990d1 --- /dev/null +++ b/extensions/dicom-pdf/package.json @@ -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" + } +} diff --git a/extensions/dicom-pdf/src/getSopClassHandlerModule.js b/extensions/dicom-pdf/src/getSopClassHandlerModule.js new file mode 100644 index 00000000000..fc34fcf8385 --- /dev/null +++ b/extensions/dicom-pdf/src/getSopClassHandlerModule.js @@ -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, + }, + ]; +} diff --git a/extensions/dicom-pdf/src/id.js b/extensions/dicom-pdf/src/id.js new file mode 100644 index 00000000000..6f5978f4059 --- /dev/null +++ b/extensions/dicom-pdf/src/id.js @@ -0,0 +1,8 @@ +const Name = 'dicom-pdf'; +const id = `org.ohif.${Name}`; + +export default id; + +const SOPClassHandlerId = `${id}.sopClassHandlerModule.${Name}`; + +export { Name, SOPClassHandlerId, }; diff --git a/extensions/dicom-pdf/src/index.js b/extensions/dicom-pdf/src/index.js new file mode 100644 index 00000000000..b40fc2de8aa --- /dev/null +++ b/extensions/dicom-pdf/src/index.js @@ -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 ( + Loading...}> + + + ); +}; + +/** + * + */ +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 ( + + ); + }; + + return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }]; + }, + getCommandsModule({ servicesManager }) { + return { + definitions: { + setToolActive: { + commandFn: () => null, + storeContexts: [], + options: {}, + }, + }, + defaultContext: 'ACTIVE_VIEWPORT::PDF', + }; + }, + getSopClassHandlerModule, +}; diff --git a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.js b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.js new file mode 100644 index 00000000000..590a33be226 --- /dev/null +++ b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.js @@ -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 ( +
+ +
No online PDF viewer installed
+
+
+ ) +} + +OHIFCornerstonePdfViewport.propTypes = { + displaySet: PropTypes.object.isRequired, +}; + +export default OHIFCornerstonePdfViewport; diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index bbd0ce7ff14..471c91678a5 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-dicom-video", - "version": "0.0.1", + "version": "3.0.1", "description": "OHIF extension for video display", "author": "OHIF", "license": "MIT", diff --git a/extensions/dicom-video/src/getSopClassHandlerModule.js b/extensions/dicom-video/src/getSopClassHandlerModule.js index 5e6e8dce11a..f1d93a2ef7b 100644 --- a/extensions/dicom-video/src/getSopClassHandlerModule.js +++ b/extensions/dicom-video/src/getSopClassHandlerModule.js @@ -1,7 +1,5 @@ -import { SOPClassHandlerName, SOPClassHandlerId } from './id'; -import { utils, classes } from '@ohif/core'; - -const { ImageSet } = classes; +import { Name, SOPClassHandlerId } from './id'; +import { utils, } from '@ohif/core'; const SOP_CLASS_UIDS = { 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); }) .map(instance => { - const { Modality, FrameOfReferenceUID, SOPInstanceUID } = instance; - const { SeriesDescription, ContentDate, ContentTime } = instance; + const { Modality, SOPInstanceUID, SeriesDescription = "VIDEO" } = instance; const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames } = instance; const displaySet = { //plugin: id, @@ -52,7 +49,7 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) SOPClassHandlerId, referencedImages: null, measurements: null, - videoUrl: dataSource.retrieve.directURL({ instance }), + videoUrl: dataSource.retrieve.directURL({ instance, singlepart: "video", tag: "PixelData", }), others: [instance], thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }), isDerivedDisplaySet: true, @@ -76,7 +73,7 @@ export default function getSopClassHandlerModule({ servicesManager, extensionMan return [ { - name: SOPClassHandlerName, + name: Name, sopClassUids, getDisplaySetsFromSeries, }, diff --git a/extensions/dicom-video/src/id.js b/extensions/dicom-video/src/id.js index 6aa6633167f..6a36a5e4f95 100644 --- a/extensions/dicom-video/src/id.js +++ b/extensions/dicom-video/src/id.js @@ -1,7 +1,8 @@ -const id = 'org.ohif.dicom-video'; +const Name = 'dicom-video'; +const id = `org.ohif.${Name}`; export default id; -const SOPClassHandlerName = 'dicom-video'; -const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; -export { SOPClassHandlerName, SOPClassHandlerId }; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.${Name}`; + +export { Name, SOPClassHandlerId, }; diff --git a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.js b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.js index 6943ef645d5..0edbde32140 100644 --- a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.js +++ b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.js @@ -1,51 +1,36 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; 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({ - children, - dataSource, displaySet, - viewportIndex, - servicesManager, - extensionManager, }) { - const { - DisplaySetService, - MeasurementService, - ToolBarService, - } = servicesManager.services; const { videoUrl } = displaySet; 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 return (
) } +OHIFCornerstoneVideoViewport.propTypes = { + displaySet: PropTypes.object.isRequired, +}; + export default OHIFCornerstoneVideoViewport; diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 37a17e8314b..3936db7f626 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -23,6 +23,11 @@ const dicomvideo = { 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 }) { return { // TODO: We're using this as a route segment @@ -96,6 +101,10 @@ export default function mode({ modeConfiguration }) { namespace: dicomvideo.viewport, displaySetsToDisplay: [dicomvideo.sopClassHandler], }, + { + namespace: dicompdf.viewport, + displaySetsToDisplay: [dicompdf.sopClassHandler], + }, ], }, }; @@ -108,9 +117,18 @@ export default function mode({ modeConfiguration }) { 'org.ohif.measurement-tracking', 'org.ohif.dicom-sr', 'org.ohif.dicom-video', + 'org.ohif.dicom-pdf', ], 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], }; } diff --git a/package.json b/package.json index 8a6267e278d..66f267736ec 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "link-list": "npm ls --depth=0 --link=true" }, "dependencies": { - "config-point": "^0.3.4", "@babel/runtime": "7.16.3", "core-js": "^3.2.1", "cornerstone-core": "2.6.0", diff --git a/platform/viewer/.webpack/webpack.pwa.js b/platform/viewer/.webpack/webpack.pwa.js index 2a419043a69..409d8d767af 100644 --- a/platform/viewer/.webpack/webpack.pwa.js +++ b/platform/viewer/.webpack/webpack.pwa.js @@ -24,12 +24,16 @@ const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`; const Dotenv = require('dotenv-webpack'); const setHeaders = (res, path) => { - res.setHeader('Content-Type', 'text/plain') if (path.indexOf('.gz') !== -1) { res.setHeader('Content-Encoding', 'gzip') } else if (path.indexOf('.br') !== -1) { 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) => { diff --git a/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js b/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js new file mode 100644 index 00000000000..3701e83bc71 --- /dev/null +++ b/platform/viewer/cypress/integration/OHIFPdfDisplay.spec.js @@ -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); + }); + +}); diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 04854bd6057..188ae2c3386 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -57,7 +57,6 @@ "@ohif/ui": "^2.0.0", "@types/react": "^16.0.0", "classnames": "^2.2.6", - "config-point": "^0.3.4", "core-js": "^3.16.1", "cornerstone-math": "^0.1.9", "cornerstone-tools": "6.0.2", diff --git a/platform/viewer/public/config/e2e.js b/platform/viewer/public/config/e2e.js index 5dab42f6669..d6fc7e9dd4f 100644 --- a/platform/viewer/public/config/e2e.js +++ b/platform/viewer/public/config/e2e.js @@ -23,6 +23,7 @@ window.config = { supportsFuzzyMatching: false, supportsWildcard: true, staticWado: true, + singlepart: "video,thumbnail,pdf", }, }, // { diff --git a/platform/viewer/public/config/local_static.js b/platform/viewer/public/config/local_static.js index f98dfc88819..b31eca387c7 100644 --- a/platform/viewer/public/config/local_static.js +++ b/platform/viewer/public/config/local_static.js @@ -23,6 +23,7 @@ window.config = { supportsFuzzyMatching: false, supportsWildcard: true, staticWado: true, + singlepart: "bulkdata,video,pdf", }, }, { diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index f702857c34b..8015ca3eb84 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -17,7 +17,6 @@ import { errorHandler // utils, } from '@ohif/core'; -import ConfigPoint from 'config-point'; /** * @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration @@ -50,10 +49,6 @@ function appInit(appConfigOrFunc, defaultExtensions) { appConfig, }); - // Load the default theme settings - const defaultTheme = config && config.defaultTheme || 'theme'; - ConfigPoint.load(defaultTheme, '/theme', 'theme'); - servicesManager.registerServices([ UINotificationService, UIModalService, diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js index 33f2d7d1319..74d480d2bb3 100644 --- a/platform/viewer/src/index.js +++ b/platform/viewer/src/index.js @@ -24,6 +24,7 @@ import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking'; import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr'; import OHIFDICOMVIDEOExtension from '@ohif/extension-dicom-video'; +import OHIFDICOMPDFExtension from '@ohif/extension-dicom-pdf'; /** Combine our appConfiguration and "baked-in" extensions */ const appProps = { @@ -34,11 +35,11 @@ const appProps = { OHIFMeasurementTrackingExtension, OHIFDICOMSRExtension, OHIFDICOMVIDEOExtension, + OHIFDICOMPDFExtension, ], }; /** Create App */ const app = React.createElement(App, appProps, null); - /** Render */ ReactDOM.render(app, document.getElementById('root')); diff --git a/yarn.lock b/yarn.lock index ab03aa93b84..b4f9dd4c3e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1236,7 +1236,7 @@ core-js-pure "^3.20.2" 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" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== @@ -7943,14 +7943,6 @@ config-chain@^1.1.11: ini "^1.3.4" 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: version "5.0.1" resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"