Add DICOM PDF plugin

This commit is contained in:
Erik Ziegler 2019-01-25 16:54:10 +01:00
parent fe68719eca
commit 4d0822e47e
19 changed files with 1900 additions and 125 deletions

3
.gitmodules vendored
View File

@ -7,3 +7,6 @@
[submodule "Packages-react/react-cornerstone-viewport"]
path = Packages-react/react-cornerstone-viewport
url = git@github.com:cornerstonejs/react-cornerstone-viewport.git
[submodule "Packages-react/plugins/VTKPlugin"]
path = Packages-react/plugins/VTKPlugin
url = git@github.com:OHIF/VTKPlugin.git

View File

@ -10,6 +10,7 @@
"cornerstone-tools": "3.0.0-b.1512",
"cornerstone-wado-image-loader": "2.2.3",
"dicom-parser": "1.8.3",
"dicomweb-client": "^0.4.1",
"hammerjs": "^2.0.8",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
@ -27,6 +28,7 @@
"react-router-dom": "^4.3.1",
"react-scripts": "^2.1.3",
"react-viewerbase": "^0.2.1",
"react-vtkjs-viewport": "^0.0.1",
"redux": "^4.0.1",
"redux-oidc": "^3.1.0"
},

View File

@ -44,6 +44,12 @@ class App extends Component {
const userNotLoggedIn = userManager && (!user || user.expired);
if (userNotLoggedIn) {
const pathname = this.props.location.pathname;
if (pathname !== '/callback') {
sessionStorage.setItem('ohif-redirect-to', pathname);
}
return (
<Switch>
<Route exact path="/silent-refresh.html" onEnter={reload} />

View File

@ -14,7 +14,9 @@ class CallbackPage extends Component {
<CallbackComponent
userManager={this.props.userManager}
successCallback={() => {
this.props.history.push("/");
const pathname = sessionStorage.getItem('ohif-redirect-to');
this.props.history.push(pathname);
}}
errorCallback={error => {
//this.props.history.push("/");

View File

@ -35,7 +35,13 @@ class FlexboxLayout extends Component {
const thumbnails = study.displaySets.map((displaySet) => {
const { displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames } = displaySet;
const imageId = displaySet.images[0].getImageId();
let imageId;
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
}
return {
imageId,

View File

@ -9,7 +9,7 @@ OHIFComponentPlugin.propTypes = {
id: PropTypes.string.isRequired,
init: PropTypes.func.isRequired,
destroy: PropTypes.func.isRequired,
children: PropTypes.node.isRequired
children: PropTypes.node
};
export default OHIFComponentPlugin;

View File

@ -2,7 +2,7 @@ import React, { Component } from "react";
import PropTypes from "prop-types";
import OHIF from 'ohif-core';
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
import OHIFComponentPlugin from '../OHIFComponentPlugin.js';
const { StackManager } = OHIF.utils;

View File

@ -0,0 +1,80 @@
import React, { Component } from "react";
import dicomParser from 'dicom-parser';
import TypedArrayProp from './TypedArrayProp';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1'
};
class DicomPDFViewport extends Component {
state = {
fileURL: null,
error: null
};
static propTypes = {
byteArray: TypedArrayProp.uint8,
};
renderPDF = (dataSet) => {
debugger;
const sopClassUid = dataSet.string('x00080016');
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
throw new Error('This is not a DICOM-encapsulated PDF');
}
const fileTag = dataSet.elements.x00420011;
const offset = fileTag.dataOffset;
const remainder = offset + fileTag.length;
const pdfByteArray = dataSet.byteArray.slice(offset, remainder);
const PDF = new Blob([pdfByteArray], {type: 'application/pdf'});
const fileURL = URL.createObjectURL(PDF);
this.setState({
fileURL
});
}
parseByteArray = (byteArray) => {
const options = {
untilTag: ''
};
let dataSet;
try {
dataSet = dicomParser.parseDicom(byteArray, options);
} catch(error) {
this.setState({
error
});
}
return dataSet
}
componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
this.renderPDF(dataSet);
}
render() {
return (
<div className={'DicomPDFViewport'} style={{width: '100%', height: '100%'}}>
{this.state.fileURL &&
<object data={this.state.fileURL} type="application/pdf" width="100%" height="100%">
</object>
}
{this.state.error &&
<h2>{JSON.stringify(this.state.error)}</h2>
}
</div>
);
}
}
export default DicomPDFViewport;

View File

@ -0,0 +1,32 @@
import OHIF from "ohif-core";
const { plugins, utils } = OHIF;
const { PLUGIN_TYPES } = plugins;
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1'
};
const OHIFDicomPDFSopClassHandlerPlugin = {
id: 'OHIFDicomPDFSopClassHandlerPlugin',
type: PLUGIN_TYPES.SOP_CLASS_HANDLER,
sopClassUids: [
SOP_CLASS_UIDS.ENCAPSULATED_PDF
],
getDisplaySetFromSeries(series, study) {
const instance = series.getFirstInstance();
return {
plugin: 'pdf',
displaySetInstanceUid: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID()
};
}
}
export default OHIFDicomPDFSopClassHandlerPlugin;

View File

@ -0,0 +1,77 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import OHIF from 'ohif-core';
import OHIFComponentPlugin from '../OHIFComponentPlugin.js';
import DicomPDFViewport from './DicomPDFViewport';
const { DICOMWeb } = OHIF;
class OHIFDicomPDFViewportPlugin extends Component {
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
};
state = {
byteArray: null,
error: null
}
static id = 'DicomPDFViewportPlugin';
static init() {
console.log('DicomPDFViewportPlugin init()');
}
static destroy() {
console.log('DicomPDFViewportPlugin destroy()');
}
componentDidMount() {
const { displaySet } = this.props.viewportData;
const { studyInstanceUid, seriesInstanceUid, sopInstanceUid, wadoRoot, wadoUri } = displaySet;
this.retrieveDicomData(studyInstanceUid, seriesInstanceUid, sopInstanceUid, wadoRoot, wadoUri).then(byteArray => {
this.setState({
byteArray
});
}, error => {
this.setState({
error
});
throw new Error(error);
});
}
retrieveDicomData(studyInstanceUid, seriesInstanceUid, sopInstanceUid, wadoRoot, wadoUri) {
// TODO: Passing in a lot of data we aren't using
// TODO: Authorization header depends on the server. If we ever have multiple servers
// we will need to figure out how / when to pass this information in.
return fetch(wadoUri, {
headers: DICOMWeb.getAuthorizationHeader()
}).then(response => response.arrayBuffer()).then(arraybuffer => {
debugger;
return new Uint8Array(arraybuffer);
});
}
render() {
const { id, init, destroy } = OHIFDicomPDFViewportPlugin;
const pluginProps = { id, init, destroy };
return (
<OHIFComponentPlugin {...pluginProps}>
{this.state.byteArray && <DicomPDFViewport byteArray={this.state.byteArray}/>}
{this.state.error &&
<h2>{JSON.stringify(this.state.error)}</h2>
}
</OHIFComponentPlugin>
);
}
}
export default OHIFDicomPDFViewportPlugin;

View File

@ -0,0 +1,137 @@
// https://github.com/facebook/prop-types/issues/69
export const TypedArrayProp = {
any: (props, propName, componentName) => {
let obj = props[propName];
if (!(obj instanceof Float64Array ||
obj instanceof Float32Array ||
obj instanceof Int32Array ||
obj instanceof Int16Array ||
obj instanceof Int8Array ||
obj instanceof Uint32Array ||
obj instanceof Uint16Array ||
obj instanceof Uint8Array ||
obj instanceof Uint8ClampedArray)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a typed array.'
);
}
},
float64: (props, propName, componentName) => {
if (!(props[propName] instanceof Float64Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Float64Array.'
);
}
},
float32: (props, propName, componentName) => {
if (!(props[propName] instanceof Float32Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Float32Array.'
);
}
},
float: (props, propName, componentName) => {
if (!(props[propName] instanceof Float64Array ||
props[propName] instanceof Float32Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Float32Array or Float64Array.'
);
}
},
int32: (props, propName, componentName) => {
if (!(props[propName] instanceof Int32Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected an Int32Array.'
);
}
},
int16: (props, propName, componentName) => {
if (!(props[propName] instanceof Int16Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected an In16Array.'
);
}
},
int8: (props, propName, componentName) => {
if (!(props[propName] instanceof Int8Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected an Int8Array.'
);
}
},
int: (props, propName, componentName) => {
if (!(props[propName] instanceof Int32Array ||
props[propName] instanceof Int16Array ||
props[propName] instanceof Int8Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected an Int32Array, In16Array, or Int8Array.'
);
}
},
uint32: (props, propName, componentName) => {
if (!(props[propName] instanceof Uint32Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Uint32Array.'
);
}
},
uint16: (props, propName, componentName) => {
if (!(props[propName] instanceof Uint16Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Uint16Array.'
);
}
},
uint8: (props, propName, componentName) => {
if (!(props[propName] instanceof Uint8Array)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Uint8Array.'
);
}
},
uint8clamped: (props, propName, componentName) => {
if (!(props[propName] instanceof Uint8ClampedArray)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Uint8ClampedArray.'
);
}
},
uint: (props, propName, componentName) => {
if (!(props[propName] instanceof Uint32Array ||
props[propName] instanceof Uint16Array ||
props[propName] instanceof Uint8Array ||
props[propName] instanceof Uint8ClampedArray)) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Expected a Uint32Array, Uint16Array, Uint8Array, or Uint8ClampedArray.'
);
}
},
};
export { TypedArrayProp as default };

View File

@ -21,13 +21,11 @@ class ViewerMain extends Component {
studies: PropTypes.array.isRequired
};
getDisplaySetsWithImages(studies) {
getDisplaySets(studies) {
const displaySets = [];
studies.forEach((study) => {
study.displaySets.forEach(dSet => {
if (dSet.images.length) {
displaySets.push(dSet);
}
displaySets.push(dSet);
});
});
@ -53,7 +51,7 @@ class ViewerMain extends Component {
//window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
// Get all the display sets for the viewer studies
const displaySets = this.getDisplaySetsWithImages(this.props.studies);
const displaySets = this.getDisplaySets(this.props.studies);
this.setState({
viewportData: displaySets
@ -66,14 +64,10 @@ class ViewerMain extends Component {
// Note: Use Slice because React does a shallow equality check. Mutating the array
// would not trigger a re-render. We have to create a copy.
debugger;
console.warn(item);
const updatedViewportData = this.state.viewportData.slice(0);
const displaySet = this.findDisplaySet(this.props.studies, item.studyInstanceUid, item.displaySetInstanceUid);
console.warn(displaySet);
updatedViewportData[viewportIndex] = Object.assign({}, displaySet);
this.setState({
@ -101,9 +95,6 @@ class ViewerMain extends Component {
OHIF.viewer.updateImageSynchronizer.destroy();
// TODO: Instruct all plugins to clean up themselves
//
// Clear references to all stacks in the StackManager
//StackManager.clearStacks();
// @TypeSafeStudies
// Clears OHIF.viewer.Studies collection

View File

@ -11,6 +11,9 @@ import App from './App.js';
import OHIFCornerstoneViewportPlugin from "./connectedComponents/OHIFCornerstoneViewportPlugin/OHIFCornerstoneViewportPlugin.js";
//import ConnectedExampleViewportPlugin from './components/ConnectedExampleViewportPlugin.js';
//import OHIFVTKViewportPlugin from './connectedComponents/OHIFVTKViewportPlugin/OHIFVTKViewportPlugin.js';
import OHIFDicomPDFViewportPlugin from './connectedComponents/OHIFDicomPDFViewportPlugin/OHIFDicomPDFViewportPlugin.js';
import OHIFDicomPDFSopClassHandlerPlugin from './connectedComponents/OHIFDicomPDFViewportPlugin/OHIFDicomPDFSopClassHandlerPlugin.js';
const reducers = OHIF.redux.reducers;
reducers.ui = ui;
@ -127,21 +130,44 @@ const buttonsAction = OHIF.redux.actions.setAvailableButtons(defaultButtons);
store.dispatch(buttonsAction);
const { plugins } = OHIF;
const { PLUGIN_TYPES } = plugins;
// Uncomment this and comment the Cornerstone version to see how the
// example plugin works
/*const pluginAction = OHIF.redux.actions.addPlugin({
id: 'example',
type: 'viewport',
type: PLUGIN_TYPES.VIEWPORT,
component: ConnectedExampleViewportPlugin
});*/
const pluginAction = OHIF.redux.actions.addPlugin({
const cornerstonePluginAction = OHIF.redux.actions.addPlugin({
id: 'cornerstone',
type: 'viewport',
type: PLUGIN_TYPES.VIEWPORT,
component: OHIFCornerstoneViewportPlugin
});
store.dispatch(pluginAction);
/*const pluginAction = OHIF.redux.actions.addPlugin({
id: 'vtk',
type: PLUGIN_TYPES.VIEWPORT,
component: OHIFVTKViewportPlugin
});*/
const pdfPluginAction = OHIF.redux.actions.addPlugin({
id: 'pdf',
type: PLUGIN_TYPES.VIEWPORT,
component: OHIFDicomPDFViewportPlugin
});
const pdfPluginActionSopClass = OHIF.redux.actions.addPlugin({
id: 'pdf_sopClassHandler',
type: PLUGIN_TYPES.SOP_CLASS_HANDLER,
component: OHIFDicomPDFSopClassHandlerPlugin
});
store.dispatch(cornerstonePluginAction);
store.dispatch(pdfPluginAction);
store.dispatch(pdfPluginActionSopClass);
// TODO[react] Use a provider when the whole tree is React
window.store = store;

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit 305643a1c9d355888643163e8080f06701cb59ed
Subproject commit 7ab55fe8f5778def698282d9e6381835897c392d

View File

@ -15,7 +15,6 @@ class CallbackPage extends Component {
userManager={this.props.userManager}
successCallback={() => {
const pathname = sessionStorage.getItem('ohif-redirect-to');
debugger;
this.props.history.push(pathname);
}}

View File

@ -38,7 +38,6 @@ class OHIFStandaloneViewer extends Component {
const userNotLoggedIn = userManager && (!user || user.expired);
if (userNotLoggedIn) {
debugger;
const pathname = this.props.location.pathname;
if (pathname !== '/callback') {

@ -0,0 +1 @@
Subproject commit dd8b3a7edddc5bc97af49bd4ceeb89c5f7d56dfb

@ -1 +1 @@
Subproject commit 34b0df571866dcd374d6189944c16a863b79e0bd
Subproject commit 6baa2cc97f224cc2baef62436014385b9cc6ac4d