Add rudimentary DICOM-SR to HTML extension

This commit is contained in:
Erik Ziegler 2019-02-08 19:13:12 +01:00
parent 4d08af5beb
commit af20f5f3e3
23 changed files with 11631 additions and 4 deletions

View File

@ -0,0 +1,14 @@
{
"presets": [
["@babel/preset-env", {
"targets": {
"ie": "11"
}
}],
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}

View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -0,0 +1,16 @@
{
"extends": [
"react-app",
"eslint:recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint",
"env": {
"jest": true
},
"settings": {
"react": {
"version": "detect",
},
},
}

View File

@ -0,0 +1,23 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
node_modules
# builds
build
dist
.rpt2_cache
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea

View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@ -0,0 +1,4 @@
language: node_js
node_js:
- 9
- 8

View 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.

View File

@ -0,0 +1,86 @@
{
"name": "ohif-dicom-html-extension",
"version": "0.0.1",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "dist/index.es.js",
"jsnext:main": "dist/index.es.js",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"scripts": {
"build": "rollup -c",
"prepublishOnly": "npm build",
"start": "rollup -c -w",
"lint": "eslint -c .eslintrc --fix src && prettier --single-quote --write src/**/*.{js,jsx,json,css}"
},
"peerDependencies": {
"dcmjs": "^0.3.3",
"ohif-core": "^0.2.6",
"prop-types": "^15.6.2",
"react": "^15.0.0 || ^16.0.0",
"react-dom": "^15.0.0 || ^16.0.0"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-external-helpers": "^7.2.0",
"@babel/plugin-proposal-class-properties": "^7.2.3",
"@babel/plugin-transform-runtime": "^7.2.0",
"@babel/preset-env": "^7.2.3",
"@babel/preset-react": "^7.0.0",
"@svgr/rollup": "^4.1.0",
"babel-eslint": "^9.0.0",
"cross-env": "^5.2.0",
"eslint": "5.6.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
"gh-pages": "^2.0.1",
"husky": "^1.3.1",
"lint-staged": "^8.1.0",
"lodash.clonedeep": "^4.5.0",
"prettier": "^1.15.3",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-scripts": "^2.1.2",
"rollup": "^0.68.2",
"rollup-plugin-babel": "^4.2.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-peer-deps-external": "^2.2.0",
"rollup-plugin-postcss": "^1.6.3",
"rollup-plugin-url": "^2.1.0",
"stylelint": "^9.9.0",
"stylelint-config-recommended": "^2.1.0",
"stylus": "^0.54.5"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,json,css}": [
"prettier --single-quote --write",
"git add"
]
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"files": [
"dist"
],
"dependencies": {
"@babel/runtime": "^7.2.0"
}
}

View File

@ -0,0 +1,55 @@
import babel from 'rollup-plugin-babel'
import commonjs from 'rollup-plugin-commonjs'
import external from 'rollup-plugin-peer-deps-external'
import postcss from 'rollup-plugin-postcss'
import resolve from 'rollup-plugin-node-resolve'
import url from 'rollup-plugin-url'
import svgr from '@svgr/rollup'
import pkg from './package.json'
// Deal with https://github.com/rollup/rollup-plugin-commonjs/issues/297
import builtins from 'rollup-plugin-node-builtins';
const globals = {
'react': 'React',
'react-dom': 'ReactDOM',
'prop-types': 'PropTypes',
'ohif-core': 'OHIF',
'dcmjs': 'dcmjs'
};
export default {
input: 'src/index.js',
output: [
{
file: pkg.main,
format: 'umd',
name: 'ohif-dicom-pdf-extension',
sourcemap: true,
globals
},
{
file: pkg.module,
format: 'es',
sourcemap: true,
globals
}
],
plugins: [
builtins(),
external(),
postcss({
modules: false
}),
url(),
svgr(),
babel({
exclude: 'node_modules/**',
plugins: [ '@babel/external-helpers' ],
externalHelpers: true,
runtimeHelpers: true
}),
resolve(),
commonjs()
],
external: Object.keys(pkg.peerDependencies || {}),
}

View File

@ -0,0 +1,131 @@
import React, { Component } from 'react';
import * as dcmjs from 'dcmjs';
import TypedArrayProp from './TypedArrayProp';
const { DicomMetaDictionary, DicomMessage } = dcmjs.data;
function getMainData(dataset) {
const root = [];
const {
CompletionFlag,
VerificationFlag,
Manufacturer,
ContentDateTime
} = dataset;
if (CompletionFlag) {
root.push(getMainDataItem('Completion flag', CompletionFlag));
}
if (VerificationFlag) {
root.push(getMainDataItem('Verification flag', VerificationFlag));
}
if (Manufacturer) {
root.push(getMainDataItem('Manufacturer', Manufacturer));
}
if (ContentDateTime) {
root.push(getMainDataItem('Content Date/Time', ContentDateTime));
}
return <div>{root}</div>;
}
const getContentSequence = (data, level = 1) => {
let header;
if (data.ConceptNameCodeSequence) {
const {
CodeMeaning,
CodeValue,
CodingSchemeDesignator
} = data.ConceptNameCodeSequence;
header = `${CodeMeaning} (${CodeValue} - ${CodingSchemeDesignator})`;
}
const root = [];
if (header) {
const HeaderDynamicLevel = `h${level}`;
root.push(<HeaderDynamicLevel key={header}>{header}</HeaderDynamicLevel>);
}
Object.keys(data).forEach(key => {
const value = data[key];
let content;
if (value instanceof Object) {
content = getContentSequence(value, level + 1);
} else {
content = (
<div key={key}>
{key} - {data[key]}
</div>
);
}
root.push(content);
});
return <div>{root}</div>;
};
function getMainDataItem(key, value) {
return (
<div key={key}>
<b>{key}</b>: {value}
</div>
);
}
class DicomHtmlViewport extends Component {
state = {
content: null,
error: null
};
static propTypes = {
byteArray: TypedArrayProp.uint8
};
componentDidMount() {
const dataSet = this.setContentFromByteArray(this.props.byteArray);
}
setContentFromByteArray(byteArray) {
const arrayBuffer = byteArray.buffer;
const dicomData = DicomMessage.readFile(arrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
// TODO: not sure why this stuff was separated from the rest...
const mainData = getMainData(dataset);
const contentSequence = getContentSequence(dataset.ContentSequence);
debugger;
const content = (
<>
{mainData}
{contentSequence}
</>
);
this.setState({
content
});
}
render() {
const style = { width: '100%', height: '100%', 'overflow-y': 'scroll' };
return (
<div className={'DicomHtmlViewport'} style={style}>
{this.state.content}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
</div>
);
}
}
export default DicomHtmlViewport;

View File

@ -0,0 +1,27 @@
import OHIFDicomHtmlViewport from './OHIFDicomHtmlViewport.js';
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
export default class OHIFDicomHtmlExtension {
/**
* Extension ID is a unique id, might be used for namespacing extension specific redux actions/reducers (?)
*/
getExtensionId() {
return 'html';
}
getViewportModule() {
return OHIFDicomHtmlViewport;
}
getSopClassHandler() {
return OHIFDicomHtmlSopClassHandler;
}
getPanelModuleDefinition() {
return null;
}
getToolbarModuleDefinition() {
return null;
}
}

View File

@ -0,0 +1,41 @@
import OHIF from 'ohif-core';
const { plugins, utils } = OHIF;
const { PLUGIN_TYPES } = plugins;
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67'
};
const sopClassUids = Object.values(SOP_CLASS_UIDS);
// TODO: Handle the case where there is more than one SOP Class Handler for the
// same SOP Class
const OHIFDicomHtmlSopClassHandler = {
id: 'OHIFDicomHtmlSopClassHandler',
type: PLUGIN_TYPES.SOP_CLASS_HANDLER,
sopClassUids,
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return {
plugin: 'html',
displaySetInstanceUid: utils.guid(),
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders
};
}
};
export default OHIFDicomHtmlSopClassHandler;

View File

@ -0,0 +1,84 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DicomHtmlViewport from './DicomHtmlViewport';
class OHIFDicomHtmlViewport extends Component {
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number
};
state = {
byteArray: null,
error: null
};
componentDidMount() {
const { displaySet } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
} = displaySet;
this.retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
).then(
byteArray => {
this.setState({
byteArray
});
},
error => {
this.setState({
error
});
throw new Error(error);
}
);
}
retrieveDicomData(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders
) {
// 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: authorizationHeaders
})
.then(response => response.arrayBuffer())
.then(arraybuffer => {
return new Uint8Array(arraybuffer);
});
}
render() {
return (
<>
{this.state.byteArray && (
<DicomHtmlViewport byteArray={this.state.byteArray} />
)}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
</>
);
}
}
export default OHIFDicomHtmlViewport;

View File

@ -0,0 +1,205 @@
// 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

@ -0,0 +1,3 @@
import OHIFDicomHtmlExtension from './OHIFDicomHtmlExtension.js';
export default OHIFDicomHtmlExtension;

View File

@ -0,0 +1,4 @@
{
"version": "v1",
"packages": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,6 @@ const OHIFDicomPDFSopClassHandler = {
SOP_CLASS_UIDS.ENCAPSULATED_PDF
],
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
debugger;
const instance = series.getFirstInstance();
return {

View File

@ -105,6 +105,7 @@
"lodash.isequal": "^4.5.0",
"ohif-core": "^0.2.7",
"ohif-cornerstone-extension": "^0.0.6",
"ohif-dicom-html-extension": "^0.0.1",
"ohif-dicom-microscopy-extension": "^0.0.5",
"ohif-dicom-pdf-extension": "^0.0.5",
"oidc-client": "^1.6.1",

View File

@ -10,6 +10,7 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer';
import WhiteLabellingContext from './WhiteLabellingContext';
import OHIFCornerstoneExtension from 'ohif-cornerstone-extension';
import OHIFDicomPDFExtension from 'ohif-dicom-pdf-extension';
import OHIFDicomHtmlExtension from 'ohif-dicom-html-extension';
import OHIFDicomMicroscopyExtension from 'ohif-dicom-microscopy-extension';
import {
loadUser,
@ -116,9 +117,10 @@ const buttonsAction = OHIF.redux.actions.setAvailableButtons(defaultButtons);
store.dispatch(buttonsAction);
/** TODO: extensions should be passed in as prop as soon as we have the extensions as separate packages and then registered by ExtensionsManager */
let extensions = [
const extensions = [
new OHIFCornerstoneExtension(),
new OHIFDicomPDFExtension(),
new OHIFDicomHtmlExtension(),
new OHIFDicomMicroscopyExtension()
];
ExtensionManager.registerExtensions(store, extensions);

View File

@ -1 +1 @@
export default '3d1eff07addbc53ced948c42cac8bd351ff083df';
export default '4d08af5bebf68e149ab539d02a1006fb390f7926';

View File

@ -1 +1 @@
export default '0.0.6';
export default '0.0.8';

View File

@ -8060,6 +8060,13 @@ ohif-cornerstone-extension@^0.0.6:
hammerjs "^2.0.8"
react-cornerstone-viewport "^0.1.9"
ohif-dicom-html-extension@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/ohif-dicom-html-extension/-/ohif-dicom-html-extension-0.0.1.tgz#da2015e8f2aa8574efc1931e08d0664cf38c57ca"
integrity sha512-n00nf2omQROB/6a+Du/eMEpeR/zpM0Ya1fdXD44jGjX6xwK3ofH+vqnGlPWQLD9S3yjvKcpcgUkwMpnz77xzww==
dependencies:
"@babel/runtime" "^7.2.0"
ohif-dicom-microscopy-extension@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/ohif-dicom-microscopy-extension/-/ohif-dicom-microscopy-extension-0.0.5.tgz#c61ca7ae9bfae25de8c964a436ea95187ec98de8"