diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index 3ebffa748..1fe534e2a 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/package.json @@ -37,7 +37,8 @@ "dependencies": { "@babel/runtime": "^7.5.5", "classnames": "^2.2.6", - "lodash.isequal": "^4.5.0" + "lodash.isequal": "^4.5.0", + "pdfjs-dist": "^2.2.228" }, "gitHead": "a5baa9228c0eda0df880136bde4420d78e6f8706" } diff --git a/extensions/dicom-pdf/src/ConnectedOHIFDicomPDFViewer.js b/extensions/dicom-pdf/src/ConnectedOHIFDicomPDFViewer.js new file mode 100644 index 000000000..4f9a8fc96 --- /dev/null +++ b/extensions/dicom-pdf/src/ConnectedOHIFDicomPDFViewer.js @@ -0,0 +1,27 @@ +import OHIF from '@ohif/core'; +import { connect } from 'react-redux'; +import OHIFDicomPDFViewport from './OHIFDicomPDFViewport'; + +const { setViewportActive } = OHIF.redux.actions; + +const mapStateToProps = (state, ownProps) => { + const { activeViewportIndex } = state.viewports; + return { activeViewportIndex }; +}; + +const mapDispatchToProps = (dispatch, ownProps) => { + const { viewportIndex } = ownProps; + + return { + setViewportActive: () => { + dispatch(setViewportActive(viewportIndex)); + }, + }; +}; + +const ConnectedOHIFDicomPDFViewer = connect( + mapStateToProps, + mapDispatchToProps +)(OHIFDicomPDFViewport); + +export default ConnectedOHIFDicomPDFViewer; diff --git a/extensions/dicom-pdf/src/DicomPDFViewport.css b/extensions/dicom-pdf/src/DicomPDFViewport.css new file mode 100644 index 000000000..72595fdc8 --- /dev/null +++ b/extensions/dicom-pdf/src/DicomPDFViewport.css @@ -0,0 +1,62 @@ +.DicomPDFViewport { + --header-height: 50px; +} + +.DicomPDFViewport #toolbar { + display: flex; + align-items: center; + color: #fff; + padding: 0.5em; + border-bottom: var(--ui-border-thickness) solid var(--ui-border-color); + height: var(--header-height); +} + +.DicomPDFViewport #canvas { + height: calc(100% - var(--header-height)); + overflow-y: scroll; +} + +.DicomPDFViewport #text-layer { + height: 100%; + margin-top: var(--header-height); +} + +.DicomPDFViewport #pdf-canvas-container { + position: relative; +} + +.DicomPDFViewport canvas, #text-layer { + display: block; + margin: 0 auto; + overflow: hidden; +} + +.DicomPDFViewport #toolbar button { + color: currentColor; + background-color: transparent; + font: inherit; + border: var(--ui-border-thickness) solid var(--ui-border-color); + border-radius: 3px; + padding: 0.25em 0.5em; + margin-right: 0.5em; + cursor: pointer; +} + +.DicomPDFViewport #text-layer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + opacity: 0.2; + line-height: 1; +} + +.DicomPDFViewport #text-layer>span { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0% 0%; +} diff --git a/extensions/dicom-pdf/src/DicomPDFViewport.js b/extensions/dicom-pdf/src/DicomPDFViewport.js index 55bf1a2c9..0fdcde3d4 100644 --- a/extensions/dicom-pdf/src/DicomPDFViewport.js +++ b/extensions/dicom-pdf/src/DicomPDFViewport.js @@ -1,6 +1,14 @@ -import React, { Component } from 'react'; +import React, { Component, createRef } from 'react'; import dicomParser from 'dicom-parser'; +import PDFJS from 'pdfjs-dist'; +import PropTypes from 'prop-types'; + import TypedArrayProp from './TypedArrayProp'; +import './DicomPDFViewport.css'; + +import pdfjsBuild from 'pdfjs-dist/build/pdf'; +import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'; +pdfjsBuild.GlobalWorkerOptions.workerSrc = pdfjsWorker; // TODO: Should probably use dcmjs for this const SOP_CLASS_UIDS = { @@ -8,16 +16,88 @@ const SOP_CLASS_UIDS = { }; class DicomPDFViewport extends Component { - state = { - fileURL: null, - error: null, - }; + constructor(props) { + super(props); + + this.state = { + fileURL: null, + error: null, + currentPageIndex: 1, + pdf: null, + scale: 1, + }; + + this.canvas = createRef(); + this.textLayer = createRef(); + } static propTypes = { byteArray: TypedArrayProp.uint8, + useNative: PropTypes.bool, + viewportData: PropTypes.object, + activeViewportIndex: PropTypes.number, + setViewportActive: PropTypes.func, + viewportIndex: PropTypes.number, }; - renderPDF = (dataSet, byteArray) => { + static defaultProps = { + useNative: false, + }; + + async componentDidMount() { + const dataSet = this.parseByteArray(this.props.byteArray); + const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray); + + this.setState(state => ({ ...state, fileURL })); + + if (!this.props.useNative) { + const pdf = await PDFJS.getDocument(fileURL).promise; + this.setState(state => ({ ...state, pdf }), () => this.updatePDFCanvas()); + } + } + + updatePDFCanvas = async () => { + const { pdf, scale, currentPageIndex } = this.state; + const context = this.canvas.getContext('2d'); + + const page = await pdf.getPage(currentPageIndex); + let viewport = page.getViewport({ scale }); + + this.canvas.height = viewport.height; + this.canvas.width = viewport.width; + + const renderContext = { + canvasContext: context, + viewport: viewport, + }; + + await page.render(renderContext); + const textContent = await page.getTextContent(); + + this.textLayer.innerHTML = ''; + this.textLayer.style.height = viewport.height + 'px'; + this.textLayer.style.width = viewport.width + 'px'; + + PDFJS.renderTextLayer({ + textContent, + container: this.textLayer, + viewport, + textDivs: [], + }); + }; + + componentDidUpdate(prevProps, prevState) { + const { currentPageIndex, scale } = this.state; + const newValidScale = prevState.scale !== scale && scale > 0; + const newValidPageNumber = + prevState.currentPageIndex !== currentPageIndex && currentPageIndex > 0; + + if (newValidScale || newValidPageNumber) { + this.updatePDFCanvas(); + } + } + + getPDFFileUrl = (dataSet, byteArray) => { let pdfByteArray = byteArray; if (dataSet) { @@ -36,50 +116,144 @@ class DicomPDFViewport extends Component { const PDF = new Blob([pdfByteArray], { type: 'application/pdf' }); const fileURL = URL.createObjectURL(PDF); - this.setState({ - fileURL, - }); + return fileURL; + }; + + onPageChange = async event => { + const { currentPageIndex, pdf } = this.state; + let newPageIndex = currentPageIndex; + + const action = event.target.getAttribute('data-pager'); + if (action === 'prev') { + if (currentPageIndex === 1) { + return; + } + newPageIndex -= 1; + if (currentPageIndex < 0) { + newPageIndex = 0; + } + } + + if (action === 'next') { + if (currentPageIndex === pdf.numPages - 1) { + return; + } + newPageIndex += 1; + if (currentPageIndex > pdf.numPages - 1) { + newPageIndex = pdf.numPages - 1; + } + } + + this.setState(state => ({ ...state, currentPageIndex: newPageIndex })); + }; + + onZoomChange = () => { + let newZoomValue = this.state.scale; + + const action = event.target.getAttribute('data-pager'); + + if (action === '+') { + newZoomValue += 0.25; + } + + if (action === '-') { + newZoomValue -= 0.25; + } + + this.setState(state => ({ ...state, scale: newZoomValue })); }; parseByteArray = byteArray => { - const options = { - untilTag: '', - }; + const options = { untilTag: '' }; let dataSet; - try { dataSet = dicomParser.parseDicom(byteArray, options); } catch (error) { - this.setState({ - error, - }); + this.setState(state => ({ ...state, error })); } return dataSet; }; - componentDidMount() { - const dataSet = this.parseByteArray(this.props.byteArray); + setViewportActiveHandler = () => { + const { + setViewportActive, + viewportIndex, + activeViewportIndex, + } = this.props; - this.renderPDF(dataSet, this.props.byteArray); - } + if (viewportIndex !== activeViewportIndex) { + setViewportActive(viewportIndex); + } + }; + + downloadPDFCanvas = () => { + const { fileURL } = this.state; + const a = document.createElement('a'); + a.href = fileURL; + a.download = fileURL.substr(fileURL.lastIndexOf('/') + 1); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }; render() { + const { fileURL, pdf, error } = this.state; + return (
- {this.state.fileURL && ( + {!this.props.useNative ? ( + <> +
+
+ {pdf && pdf.numPages > 1 && ( + <> + + + + )} + + + +
+
+
+
+ (this.canvas = canvas)} + /> +
(this.textLayer = textLayer)} + >
+
+
+ + ) : ( )} - {this.state.error &&

{JSON.stringify(this.state.error)}

} + {error &&

{JSON.stringify(error)}

} ); } diff --git a/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js b/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js index 302a8ee39..88cca1ed2 100644 --- a/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js +++ b/extensions/dicom-pdf/src/OHIFDicomPDFViewport.js @@ -11,6 +11,9 @@ class OHIFDicomPDFViewport extends Component { studies: PropTypes.object, displaySet: PropTypes.object, viewportIndex: PropTypes.number, + viewportData: PropTypes.object, + activeViewportIndex: PropTypes.number, + setViewportActive: PropTypes.func, }; state = { @@ -30,42 +33,36 @@ class OHIFDicomPDFViewport extends Component { componentDidMount() { const { displaySet, studies } = this.props.viewportData; - const { - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid, - wadoRoot, - wadoUri, - authorizationHeaders, - } = displaySet; - DicomLoaderService.findDicomDataPromise(displaySet, studies).then( - data => { - const byteArray = new Uint8Array(data); - this.setState({ - byteArray: byteArray, - }); - }, + data => this.setState({ byteArray: new Uint8Array(data) }), error => { - this.setState({ - error, - }); - + this.setState({ error }); throw new Error(error); } ); } render() { + const { + setViewportActive, + viewportIndex, + activeViewportIndex, + } = this.props; + const { byteArray, error } = this.state; const { id, init, destroy } = OHIFDicomPDFViewport; const pluginProps = { id, init, destroy }; return ( - {this.state.byteArray && ( - + {byteArray && ( + )} - {this.state.error &&

{JSON.stringify(this.state.error)}

} + {error &&

{JSON.stringify(error)}

}
); } diff --git a/extensions/dicom-pdf/src/asyncComponent.js b/extensions/dicom-pdf/src/asyncComponent.js new file mode 100644 index 000000000..e8f344a35 --- /dev/null +++ b/extensions/dicom-pdf/src/asyncComponent.js @@ -0,0 +1,37 @@ +/** + * We use this component to leverage "Code Splitting" + * + * Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html + */ + +import React, { Component } from 'react'; + +export default function asyncComponent(importComponent) { + class AsyncComponent extends Component { + constructor(props) { + super(props); + + this.state = { + component: null, + }; + } + + async componentDidMount() { + // Add dynamically loaded component to state + const { default: component } = await importComponent(); + + this.setState({ + component: component, + }); + } + + render() { + const C = this.state.component; + + // Render the loaded component, or null + return C ? : null; + } + } + + return AsyncComponent; +} diff --git a/extensions/dicom-pdf/src/index.js b/extensions/dicom-pdf/src/index.js index b74560ec9..3e4c55a85 100644 --- a/extensions/dicom-pdf/src/index.js +++ b/extensions/dicom-pdf/src/index.js @@ -1,16 +1,21 @@ +import asyncComponent from './asyncComponent.js'; import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js'; -import OHIFDicomPDFViewport from './OHIFDicomPDFViewport.js'; + +const ConnectedOHIFDicomPDFViewer = asyncComponent(() => + import( + /* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer' + ) +); export default { /** * Only required property. Should be a unique value across all extensions. */ id: 'pdf', - getViewportModule() { - return OHIFDicomPDFViewport; + return ConnectedOHIFDicomPDFViewer; }, getSopClassHandlerModule() { return OHIFDicomPDFSopClassHandler; - } + }, }; diff --git a/yarn.lock b/yarn.lock index c46619380..bd16e2563 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13296,6 +13296,11 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" +node-ensure@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" + integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc= + node-fetch-npm@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7" @@ -14423,6 +14428,14 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pdfjs-dist@^2.2.228: + version "2.2.228" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.2.228.tgz#777b068a0a16c96418433303807c183058b47aaa" + integrity sha512-W5LhYPMS2UKX0ELIa4u+CFCMoox5qQNQElt0bAK2mwz1V8jZL0rvLao+0tBujce84PK6PvWG36Nwr7agCCWFGQ== + dependencies: + node-ensure "^0.0.0" + worker-loader "^2.0.0" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"