From 450e0981a5ba054fcfcb85eeaeb18371af9088f8 Mon Sep 17 00:00:00 2001 From: Romulo Bordezani Date: Fri, 25 Oct 2019 23:46:32 -0300 Subject: [PATCH] feat: Snapshot Download Tool (#840) * feat(Download ScreenShot Dialog) - UI part of it * feat(Download ScreenShot Dialog) - UI part of it * feat(Download ScreenShot Dialog) - Missing Annotations showing controls * feat(Download ScreenShot Dialog) - Missing Annotations showing controls * feat(Download ScreenShot Dialog) - Partial Pull Request - Not ready to Prod yet. * feat(Download ScreenShot Dialog) - UI - v2 * feat(Download ScreenShot Dialog) - UI - v3 * feat(Download ScreenShot Dialog) - Resizing tool * feat(Download ScreenShot Dialog) - Resizing tool * feat(Download ScreenShot Dialog) - Removing hardcoded CSS left * feat(Download ScreenShot Dialog) - Bugfixes * feat(Download ScreenShot Dialog) - Bugfixes - Escape closes the modal * feat(Download ScreenShot Dialog) - Bugfixes - Responsive issues * WIP: Refactoring DownloadDialog * WIP: Refactoring DownloadDialog * Align label and hide background canvas * Add constantes and remove code not used * Reset state on close modal * Extract cornerstone to connect * Fix modal zindex and add custom canvas classname * CR Update: Add proptypes and fix useeffect warning * CR Update: Refactor hooks, styles and imports * CR Update: Remove important from css * CR Update: Update core utils test of index --- .vscode/settings.json | 19 +- extensions/cornerstone/src/toolbarModule.js | 10 + platform/core/src/utils/b64toBlob.js | 22 ++ platform/core/src/utils/index.js | 3 + platform/core/src/utils/index.test.js | 3 +- .../downloadDialog/DownloadDialog.js | 335 ++++++++++++++++++ .../downloadDialog/DownloadDialog.styl | 107 ++++++ .../__docs__/downloadDialog.mdx | 39 ++ .../ui/src/components/downloadDialog/index.js | 2 + platform/ui/src/components/index.js | 2 + platform/ui/src/elements/form/Select.css | 48 ++- platform/ui/src/elements/form/Select.js | 55 +-- platform/ui/src/elements/form/TextInput.css | 37 +- platform/ui/src/elements/form/TextInput.js | 47 ++- platform/ui/src/elements/index.js | 5 +- platform/ui/src/index.js | 22 +- .../ConnectedDownloadDialog.js | 121 +++++++ .../src/connectedComponents/ToolbarRow.js | 41 ++- platform/viewer/src/theme-tide.css | 2 + 19 files changed, 824 insertions(+), 96 deletions(-) create mode 100644 platform/core/src/utils/b64toBlob.js create mode 100644 platform/ui/src/components/downloadDialog/DownloadDialog.js create mode 100644 platform/ui/src/components/downloadDialog/DownloadDialog.styl create mode 100644 platform/ui/src/components/downloadDialog/__docs__/downloadDialog.mdx create mode 100644 platform/ui/src/components/downloadDialog/index.js create mode 100644 platform/viewer/src/connectedComponents/ConnectedDownloadDialog.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 50eed8343..c19209219 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,11 @@ { - "editor.rulers": [80, 120], - + "editor.rulers": [ + 80, + 120 + ], // === // Spacing // === - "editor.insertSpaces": true, "editor.tabSize": 2, "editor.trimAutoWhitespace": true, @@ -12,17 +13,21 @@ "files.eol": "\n", "files.insertFinalNewline": true, "files.trimFinalNewlines": true, - // === // Event Triggers // === - "editor.formatOnSave": true, "eslint.autoFixOnSave": true, "eslint.run": "onSave", "eslint.validate": [ - { "language": "javascript", "autoFix": true }, - { "language": "javascriptreact", "autoFix": true } + { + "language": "javascript", + "autoFix": true + }, + { + "language": "javascriptreact", + "autoFix": true + } ], "prettier.disableLanguages": [], "prettier.endOfLine": "lf", diff --git a/extensions/cornerstone/src/toolbarModule.js b/extensions/cornerstone/src/toolbarModule.js index e89352300..18104ee86 100644 --- a/extensions/cornerstone/src/toolbarModule.js +++ b/extensions/cornerstone/src/toolbarModule.js @@ -213,6 +213,16 @@ const definitions = [ commandName: 'setToolActive', commandOptions: { toolName: 'Eraser' }, }, + { + id: 'Download', + label: 'Download', + icon: 'create-screen-capture', + // + type: TOOLBAR_BUTTON_TYPES.BUILT_IN, + options: { + behavior: 'DOWNLOAD_SCREEN_SHOT', + }, + }, ], }, ]; diff --git a/platform/core/src/utils/b64toBlob.js b/platform/core/src/utils/b64toBlob.js new file mode 100644 index 000000000..0928ea7aa --- /dev/null +++ b/platform/core/src/utils/b64toBlob.js @@ -0,0 +1,22 @@ +/* Enabled JPEG images downloading on IE11. */ +const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, { type: contentType }); + return blob; +}; + +export default b64toBlob; diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 1f3ff98a5..074d1a8b4 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -9,6 +9,7 @@ import studyMetadataManager from './studyMetadataManager'; import updateMetaDataManager from './updateMetaDataManager.js'; import writeScript from './writeScript.js'; import DicomLoaderService from './dicomLoaderService.js'; +import b64toBlob from './b64toBlob.js'; import * as urlUtil from './urlUtil'; const utils = { @@ -18,6 +19,7 @@ const utils = { addServers, sortBy, writeScript, + b64toBlob, StackManager, studyMetadataManager, // Updates WADO-RS metaDataManager @@ -34,6 +36,7 @@ export { addServers, sortBy, writeScript, + b64toBlob, StackManager, studyMetadataManager, // Updates WADO-RS metaDataManager diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index a80bcf130..c1788b5be 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -9,13 +9,14 @@ describe('Top level exports', () => { 'addServers', 'sortBy', 'writeScript', + 'b64toBlob', 'StackManager', 'studyMetadataManager', // Updates WADO-RS metaDataManager 'updateMetaDataManager', 'DICOMTagDescriptions', 'DicomLoaderService', - 'urlUtil' + 'urlUtil', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/ui/src/components/downloadDialog/DownloadDialog.js b/platform/ui/src/components/downloadDialog/DownloadDialog.js new file mode 100644 index 000000000..67f77ab5e --- /dev/null +++ b/platform/ui/src/components/downloadDialog/DownloadDialog.js @@ -0,0 +1,335 @@ +import React, { useEffect, useState, createRef } from 'react'; +import Modal from 'react-bootstrap-modal'; +import PropTypes from 'prop-types'; + +import './DownloadDialog.styl'; +import { TextInput, Select } from '@ohif/ui'; +import { withTranslation } from '../../utils/LanguageProvider'; + +const FILE_TYPE_OPTIONS = [ + { + key: 'jpg', + value: 'jpg', + }, + { + key: 'png', + value: 'png', + }, +]; + +const DEFAULT_FILENAME = 'image'; + +const DownloadDialog = ({ + t, + isOpen, + activeViewport, + onClose, + updateViewportPreview, + enableViewport, + disableViewport, + toggleAnnotations, + loadImage, + downloadBlob, + defaultSize, + minimumSize, + canvasClass, +}) => { + const [filename, setFilename] = useState(DEFAULT_FILENAME); + const [fileType, setFileType] = useState('jpg'); + + const [height, setHeight] = useState(defaultSize); + const [width, setWidth] = useState(defaultSize); + + const [showAnnotations, setShowAnnotations] = useState(true); + + const [keepAspect, setKeepAspect] = useState(true); + const [lastImage, setLastImage] = useState(); + + const [viewportElement, setViewportElement] = useState(); + const [viewportElementHeight, setViewportElementHeight] = useState( + minimumSize + ); + const [viewportElementWidth, setViewportElementWidth] = useState(minimumSize); + + const [downloadCanvas, setDownloadCanvas] = useState({ + ref: createRef(), + width: minimumSize, + height: minimumSize, + }); + + const [viewportPreview, setViewportPreview] = useState({ + src: null, + width: minimumSize, + height: minimumSize, + }); + + useEffect(() => { + enableViewport(viewportElement); + + return () => { + disableViewport(viewportElement); + + setHeight(defaultSize); + setWidth(defaultSize); + }; + }, [defaultSize, disableViewport, enableViewport, viewportElement]); + + useEffect(() => { + const loadAndUpdateViewports = async () => { + const { + image, + width: scaledWidth, + height: scaledHeight, + } = await loadImage(activeViewport, viewportElement, width, height); + + setLastImage(image); + + toggleAnnotations(showAnnotations, viewportElement); + + setViewportElementHeight(scaledHeight); + setViewportElementWidth(scaledWidth); + + setDownloadCanvas(state => ({ + ...state, + height: scaledHeight, + width: scaledWidth, + })); + + const { + dataUrl, + width: viewportElementWidth, + height: viewportElementHeight, + } = await updateViewportPreview( + viewportElement, + downloadCanvas.ref.current, + fileType + ); + + setViewportPreview(state => ({ + ...state, + src: dataUrl, + width: viewportElementWidth, + height: viewportElementHeight, + })); + }; + + loadAndUpdateViewports(); + }, [ + activeViewport, + viewportElement, + showAnnotations, + height, + width, + loadImage, + toggleAnnotations, + updateViewportPreview, + fileType, + downloadCanvas.ref, + ]); + + const onHeightChange = () => { + const newHeight = event.target.value; + setHeight(newHeight); + + setViewportElementHeight(newHeight); + + setDownloadCanvas(state => ({ + ...state, + height: newHeight, + })); + + if (keepAspect) { + const multiplier = newHeight / lastImage.height; + const newWidth = Math.round(lastImage.width * multiplier); + + setWidth(newWidth); + setViewportElementWidth(newWidth); + + setDownloadCanvas(state => ({ + ...state, + width: newWidth, + })); + } + }; + + const onWidthChange = event => { + const newWidth = event.target.value; + setWidth(newWidth); + + setViewportElementWidth(newWidth); + + setDownloadCanvas(state => ({ + ...state, + width: newWidth, + })); + + if (keepAspect) { + const multiplier = newWidth / lastImage.width; + const newHeight = Math.round(lastImage.height * multiplier); + + setHeight(newHeight); + setViewportElementHeight(newHeight); + + setDownloadCanvas(state => ({ + ...state, + height: newHeight, + })); + } + }; + + const downloadImage = () => { + downloadBlob( + filename || DEFAULT_FILENAME, + fileType, + viewportElement, + downloadCanvas.ref.current + ); + }; + + return ( + + + {t('Download High Quality Image')} + + +
+ {t( + 'Please specify the dimensions, filename, and desired type for the output image.' + )} +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ setFilename(event.target.value)} + label={t('File name')} + id="file-name" + /> +
+
+ setShowAnnotations(event.target.checked)} + /> + {t('Show Annotations')} + +
+
+
+ +
setViewportElement(ref)} + > + +
+ +
+

{t('Image Preview')}

+ Viewport Preview +
+ +
+
+ +
+
+ +
+
+
+
+ ); +}; + +DownloadDialog.propTypes = { + t: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + activeViewport: PropTypes.object, + onClose: PropTypes.func.isRequired, + updateViewportPreview: PropTypes.func.isRequired, + enableViewport: PropTypes.func.isRequired, + disableViewport: PropTypes.func.isRequired, + toggleAnnotations: PropTypes.func.isRequired, + loadImage: PropTypes.func.isRequired, + downloadBlob: PropTypes.func.isRequired, + defaultSize: PropTypes.number.isRequired, + minimumSize: PropTypes.number.isRequired, + canvasClass: PropTypes.string.isRequired, +}; + +export default withTranslation('DownloadDialog')(DownloadDialog); diff --git a/platform/ui/src/components/downloadDialog/DownloadDialog.styl b/platform/ui/src/components/downloadDialog/DownloadDialog.styl new file mode 100644 index 000000000..b7ea177a3 --- /dev/null +++ b/platform/ui/src/components/downloadDialog/DownloadDialog.styl @@ -0,0 +1,107 @@ +@import './../../design/styles/common/global.styl' +@import './../../design/styles/common/form.styl' +@import './../../design/styles/common/button.styl' + +.DownloadDialog + color: var(--text-secondary-color); + filter: drop-shadow(0 0 3px var(--ui-gray-darkest)); + border: none; + border-radius: 8px; + width: inherit; + padding: 15px; + background: transparent; + z-index: 1080 !important; + + .title + margin: 0; + font-weight: bold; + + .file-info-container + display: flex; + flex-direction: row; + margin: 20px 0; + border-radius: 5px; + padding: 20px 10px 0; + background-color: #16202b; + + .form-control.input-ohif + padding: 6px 12px; + + @media screen and (max-width: 1023px) + flex-direction: column; + + .col + flex-grow: 1; + + .input-ohif + margin-left: 15px; + @media screen and (max-width: 1023px) + margin-left: 0; + margin-top: 5px; + width: 100%; + + .file-type + .select-ohif + margin-left: 17px; + @media screen and (max-width: 1023px) + margin-left: 0; + width: 100%; + + .show-annotations + font-weight: bold; + line-height: 30px; + input + margin-right: 7px; + vertical-align: middle; + label + display: flex; + justify-content: center; + align-items: center; + + .preview + display: flex; + flex-direction column; + height: fit-content; + background-color: #16202b; + width: fit-content; + padding: 10px; + border-radius: 5px; + align-self: center; + @media screen and (max-width: 1023px) + width: 100%; + justify-content: center; + align-items: center; + + .viewport-preview + max-height: 512px; + max-width: 512px; + + h4 + width: 100%; + text-align center; + font-size: 1.3em; + margin: 0 0 10px; + + .preview-container + width: auto; + height: 100%; + max-height: 400px; + object-fit contain; + + .actions + display: flex; + height: 60px; + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; + .action-cancel + margin: 0 20px; + .actions-save + margin: 0 0 0 10px; + +.modal-dialog + height: 100%; + + .modal-body + display: flex; + flex-direction: column; diff --git a/platform/ui/src/components/downloadDialog/__docs__/downloadDialog.mdx b/platform/ui/src/components/downloadDialog/__docs__/downloadDialog.mdx new file mode 100644 index 000000000..11d442a7a --- /dev/null +++ b/platform/ui/src/components/downloadDialog/__docs__/downloadDialog.mdx @@ -0,0 +1,39 @@ +--- +name: Download Dialog +menu: Components +route: /components/download-dialog +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { DownloadDialog } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +# Download Dialog + +## Basic usage + + + + {({ state, setState }) => ( + +
+
{JSON.stringify(state, null, 2)}
+
+
+ +
+
+ )} +
+
+ +## API + + + +## Translation Namespace + + diff --git a/platform/ui/src/components/downloadDialog/index.js b/platform/ui/src/components/downloadDialog/index.js new file mode 100644 index 000000000..6cec626bf --- /dev/null +++ b/platform/ui/src/components/downloadDialog/index.js @@ -0,0 +1,2 @@ +import DownloadDialog from './DownloadDialog'; +export { DownloadDialog }; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 609026ba5..92748e4d5 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -11,6 +11,7 @@ import { import { Checkbox } from './checkbox'; import { CineDialog } from './cineDialog'; +import { DownloadDialog } from './downloadDialog'; import { QuickSwitch } from './quickSwitch'; import { RoundedButtonGroup } from './roundedButtonGroup'; import { SelectTree } from './selectTree'; @@ -22,6 +23,7 @@ import { Tooltip } from './tooltip'; export { Checkbox, CineDialog, + DownloadDialog, LayoutButton, LayoutChooser, MeasurementTable, diff --git a/platform/ui/src/elements/form/Select.css b/platform/ui/src/elements/form/Select.css index 3f420fd39..13741eb1e 100644 --- a/platform/ui/src/elements/form/Select.css +++ b/platform/ui/src/elements/form/Select.css @@ -1,42 +1,34 @@ -.select-ohif { - display: block; - font-family: Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', - sans-serif; - font-size: 0.8em; - font-weight: 600; - color: #444; - line-height: 1.3; - padding: 0.6em 1.4em 0.5em 0.8em; - width: 100%; - max-width: 100%; +.select-ohif-container .select-ohif { + display: inline-block; + font-size: 10pt; box-sizing: border-box; - margin: 0; - border: 1px solid #aaa; - box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.04); - border-radius: 0.5em; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; - background-color: #fff; - background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), - linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%); - background-repeat: no-repeat, repeat; - background-position: right 0.7em top 50%, 0 0; - background-size: 0.65em auto, 100%; + width: auto; + transition: all 0.15s ease; + background-color: var(--input-background-color); + line-height: 16px; + color: var(--input-placeholder-color); + height: 40px; + margin: 0 5px 20px 5px; + padding: 0 20px; + placeholder-color: var(--input-placeholder-color); + cursor: pointer; + border: none; + font-weight: normal; + border-radius: 4px; } -.select-ohif::-ms-expand { +.select-ohif-container .select-ohif::-ms-expand { display: none; } -.select-ohif:hover { - border-color: #888; +.select-ohif-container .select-ohif-label { + padding: 5px; } .select-ohif:focus { outline: none; } -.select-ohif option { +.select-ohif-container .select-ohif option { font-weight: normal; } diff --git a/platform/ui/src/elements/form/Select.js b/platform/ui/src/elements/form/Select.js index fe8a79dbb..8a997ebbb 100644 --- a/platform/ui/src/elements/form/Select.js +++ b/platform/ui/src/elements/form/Select.js @@ -7,9 +7,19 @@ import PropTypes from 'prop-types'; class Select extends Component { constructor(props) { super(props); - this.state = { value: this.props.value }; } + static propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }) + ), + value: PropTypes.string, + onChange: PropTypes.func, + }; + handleChange = event => { const value = event.target.value; this.setState({ value }); @@ -18,32 +28,27 @@ class Select extends Component { render() { return ( - +
+ + +
+ ); } } -Select.propTypes = { - options: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - }) - ), - value: PropTypes.string, - onChange: PropTypes.func, -}; - export { Select }; diff --git a/platform/ui/src/elements/form/TextInput.css b/platform/ui/src/elements/form/TextInput.css index 42ea9d163..e05510529 100644 --- a/platform/ui/src/elements/form/TextInput.css +++ b/platform/ui/src/elements/form/TextInput.css @@ -1,7 +1,34 @@ -.input-ohif { - background-color: #b6b6b6; - border-color: #b6b6b6; - font-family: Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', - sans-serif; +.input-ohif-container { font-size: 1em; + color: var(--text-primary-color); + font-weight: 400; +} + +.input-ohif-container .input-ohif { + display: inline-block; + height: 40px; + margin: 0 10px 20px; + padding: 0 20px; + cursor: pointer; + border: none; + background-color: var(--input-background-color); + color: var(--input-placeholder-color); + font-size: 10pt; + font-weight: normal; + width: auto; + border-radius: 4px; + transition: all 0.15s ease; +} + +.input-ohif-container .input-ohif-label { + padding: 5px; +} + +.input-ohif-container .input-ohif:active, +.input-ohif-container .input-ohif:focus { + background-color: var(--input-background-color); +} + +.input-ohif-container .input-ohif.invisible { + visibility: hidden; } diff --git a/platform/ui/src/elements/form/TextInput.js b/platform/ui/src/elements/form/TextInput.js index 637b34a34..13de72c65 100644 --- a/platform/ui/src/elements/form/TextInput.js +++ b/platform/ui/src/elements/form/TextInput.js @@ -1,36 +1,45 @@ -import './TextInput.css'; - import React from 'react'; import PropTypes from 'prop-types'; +import './TextInput.css'; + class TextInput extends React.Component { constructor(props) { super(props); - this.state = { value: props.value }; } - handleChange = event => { - this.setState({ value: event.target.value }); - if (this.props.onChange) this.props.onChange(); + static propTypes = { + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]), + id: PropTypes.string, + label:PropTypes.string, + type: PropTypes.string, + }; + + static defaultProps = { + value: '', + id: `TextInput-${new Date().toTimeString()}`, + label: undefined, + type: 'text', }; render() { return ( - +
+ +
); } } -TextInput.propTypes = { - value: PropTypes.string, - id: PropTypes.string, - onChange: PropTypes.func, -}; - export { TextInput }; diff --git a/platform/ui/src/elements/index.js b/platform/ui/src/elements/index.js index 537107443..6fc29cefd 100644 --- a/platform/ui/src/elements/index.js +++ b/platform/ui/src/elements/index.js @@ -1,3 +1,2 @@ -import { ICONS, Icon } from './Icon'; - -export { Icon, ICONS }; +export * from './Icon'; +export * from './form'; \ No newline at end of file diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 82a221070..d11e5fa6f 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -1,6 +1,7 @@ import { Checkbox, CineDialog, + DownloadDialog, LayoutButton, LayoutChooser, MeasurementTable, @@ -22,10 +23,20 @@ import { UserPreferences, UserPreferencesModal, } from './components'; -import { ICONS, Icon } from './elements'; + +// Elements +import { + ICONS, + Icon, + DropdownMenu as Dropdown, + Select, + Label, + Range, + TextArea, + TextInput, +} from './elements'; // Alias this for now as not all dependents are using strict versioning -import { DropdownMenu as Dropdown, Range, Select } from './elements/form'; import ExpandableToolMenu from './viewer/ExpandableToolMenu.js'; import PlayClipButton from './viewer/PlayClipButton.js'; import { ScrollableArea } from './ScrollableArea/ScrollableArea.js'; @@ -38,11 +49,16 @@ import SnackbarProvider, { } from './utils/SnackbarProvider'; export { + // Elements ICONS, // Checkbox, - CineDialog, Dropdown, + Label, + TextArea, + TextInput, + CineDialog, + DownloadDialog, ExpandableToolMenu, Icon, LayoutButton, diff --git a/platform/viewer/src/connectedComponents/ConnectedDownloadDialog.js b/platform/viewer/src/connectedComponents/ConnectedDownloadDialog.js new file mode 100644 index 000000000..106c4bf40 --- /dev/null +++ b/platform/viewer/src/connectedComponents/ConnectedDownloadDialog.js @@ -0,0 +1,121 @@ +import { connect } from 'react-redux'; +import { DownloadDialog } from '@ohif/ui'; +import { utils } from '@ohif/core'; +import cornerstone from 'cornerstone-core'; +import cornerstoneTools from 'cornerstone-tools'; + +const MINIMUM_SIZE = 100; +const DEFAULT_SIZE = 512; + +const mapStateToProps = (state, ownProps) => { + const { viewportSpecificData, activeViewportIndex } = state.viewports; + const { dom: activeEnabledElement } = + viewportSpecificData[activeViewportIndex] || {}; + + return { + minimumSize: MINIMUM_SIZE, + defaultSize: DEFAULT_SIZE, + canvasClass: 'cornerstone-canvas', + onClose: ownProps.toggleDownloadDialog, + activeViewport: activeEnabledElement, + enableViewport: viewportElement => { + if (viewportElement) { + cornerstone.enable(viewportElement); + } + }, + disableViewport: viewportElement => { + if (viewportElement) { + cornerstone.disable(viewportElement); + } + }, + updateViewportPreview: (viewportElement, downloadCanvas, fileType) => + new Promise(resolve => { + cornerstone.fitToWindow(viewportElement); + + viewportElement.addEventListener( + 'cornerstoneimagerendered', + function updateViewport(event) { + const enabledElement = cornerstone.getEnabledElement(event.target) + .element; + const type = 'image/' + fileType; + const dataUrl = downloadCanvas.toDataURL(type, 1); + + let newWidth = enabledElement.offsetHeight; + let newHeight = enabledElement.offsetWidth; + + if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) { + const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight); + newHeight *= multiplier; + newWidth *= multiplier; + } + + resolve({ dataUrl, width: newWidth, height: newHeight }); + + viewportElement.removeEventListener( + 'cornerstoneimagerendered', + updateViewport + ); + } + ); + }), + loadImage: (activeViewport, viewportElement, width, height) => + new Promise(resolve => { + if (activeViewport && viewportElement) { + const enabledElement = cornerstone.getEnabledElement(activeViewport); + const viewport = Object.assign({}, enabledElement.viewport); + delete viewport.scale; + viewport.translation = { + x: 0, + y: 0, + }; + + cornerstone.loadImage(enabledElement.image.imageId).then(image => { + cornerstone.displayImage(viewportElement, image); + cornerstone.setViewport(viewportElement, viewport); + cornerstone.resize(viewportElement, true); + + const MAX_TEXTURE_SIZE = 16384; + const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE); + const newHeight = Math.min( + height || image.height, + MAX_TEXTURE_SIZE + ); + + resolve({ image, width: newWidth, height: newHeight }); + }); + } + }), + toggleAnnotations: (toggle, viewportElement) => { + cornerstoneTools.store.state.tools.forEach(({ name }) => { + if (toggle) { + cornerstoneTools.setToolEnabledForElement(viewportElement, name); + } else { + cornerstoneTools.setToolDisabledForElement(viewportElement, name); + } + }); + }, + downloadBlob: (filename, fileType, viewportElement, downloadCanvas) => { + const file = `${filename}.${fileType}`; + const mimetype = `image/${fileType}`; + + /* Handles JPEG images for IE11 */ + if (downloadCanvas.msToBlob && fileType === 'jpeg') { + const image = downloadCanvas.toDataURL(mimetype, 1); + const blob = utils.b64toBlob( + image.replace('data:image/jpeg;base64,', ''), + mimetype + ); + return window.navigator.msSaveBlob(blob, file); + } + + return cornerstoneTools.SaveAs(viewportElement, file, mimetype); + }, + }; +}; + +const ConnectedDownloadDialog = connect( + mapStateToProps, + null +)(DownloadDialog); + +export default ConnectedDownloadDialog; diff --git a/platform/viewer/src/connectedComponents/ToolbarRow.js b/platform/viewer/src/connectedComponents/ToolbarRow.js index c4fd2667c..faa38c6a2 100644 --- a/platform/viewer/src/connectedComponents/ToolbarRow.js +++ b/platform/viewer/src/connectedComponents/ToolbarRow.js @@ -1,19 +1,21 @@ -import './ToolbarRow.css'; - import React, { Component } from 'react'; +import { MODULE_TYPES } from '@ohif/core'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; import { ExpandableToolMenu, RoundedButtonGroup, ToolbarButton, } from '@ohif/ui'; + +import './ToolbarRow.css'; import { commandsManager, extensionManager } from './../App.js'; import ConnectedCineDialog from './ConnectedCineDialog'; +import ConnectedDownloadDialog from './ConnectedDownloadDialog'; import ConnectedLayoutButton from './ConnectedLayoutButton'; import ConnectedPluginSwitch from './ConnectedPluginSwitch.js'; -import { MODULE_TYPES } from '@ohif/core'; -import PropTypes from 'prop-types'; -import { withTranslation } from 'react-i18next'; + class ToolbarRow extends Component { // TODO: Simplify these? isOpen can be computed if we say "any" value for selected, @@ -44,10 +46,13 @@ class ToolbarRow extends Component { toolbarButtons: toolbarButtonDefinitions, activeButtons: [], isCineDialogOpen: false, + isDownloadScreenShotDialogOpen: false, }; this._handleBuiltIn = _handleBuiltIn.bind(this); + this.toggleDownloadDialog = toggleDownloadDialog.bind(this); + const panelModules = extensionManager.modules[MODULE_TYPES.PANEL]; this.buttonGroups = { left: [ @@ -111,6 +116,13 @@ class ToolbarRow extends Component { zIndex: 999, }; + const downloadScreenShotContainerStyle = { + display: this.state.isDownloadScreenShotDialogOpen ? 'block' : 'none', + position: 'absolute', + top: '82px', + zIndex: 1001, + }; + const onPress = (side, value) => { this.props.handleSidePanelChange(side, value); }; @@ -146,6 +158,12 @@ class ToolbarRow extends Component {
+
+ +
); } @@ -277,12 +295,25 @@ function _getVisibleToolbarButtons() { return toolbarButtonDefinitions; } +/** + * Toggles the Download Dialog Modal + */ +function toggleDownloadDialog() { + this.setState({ + isDownloadScreenShotDialogOpen: !this.state.isDownloadScreenShotDialogOpen, + }); +} + function _handleBuiltIn({ behavior } = {}) { if (behavior === 'CINE') { this.setState({ isCineDialogOpen: !this.state.isCineDialogOpen, }); } + + if (behavior === 'DOWNLOAD_SCREEN_SHOT') { + this.toggleDownloadDialog(); + } } export default withTranslation('Common')(ToolbarRow); diff --git a/platform/viewer/src/theme-tide.css b/platform/viewer/src/theme-tide.css index 7e10d1dce..f2bcae5d6 100644 --- a/platform/viewer/src/theme-tide.css +++ b/platform/viewer/src/theme-tide.css @@ -38,8 +38,10 @@ --text-disabled-color: #878787; --input-background-color: #2c363f; + --input-placeholder-color--hover: #4d5a63; --input-placeholder-color: #d3d3d3; + --table-hover-color: #2c363f; --table-text-primary-color: #ffffff; --table-text-secondary-color: #91b9cd;