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 (