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"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ setViewportElement(ref)}
+ >
+
+
+
+
+
{t('Image 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;