├── .babelrc
├── .circleci
└── config.yml
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── .webpack
├── webpack.config.js
└── webpack.dev.js
├── LICENSE
├── README.md
├── babel.config.js
├── examples
├── App.js
├── ExamplePageBasic.js
├── ExamplePageCustomOverlay.js
├── ExamplePageEscapeHatch.js
├── ExamplePageGrid.js
├── index.css
├── index.html
├── index.js
└── initCornerstone.js
├── netlify.toml
├── package-lock.json
├── package.json
├── public
├── index.html
├── initCornerstone.js
├── manifest.json
└── script-tag-index.html
├── rollup.config.js
└── src
├── CornerstoneViewport
├── CornerstoneViewport.css
└── CornerstoneViewport.js
├── ImageScrollbar
├── ImageScrollbar.css
└── ImageScrollbar.js
├── LoadingIndicator
├── LoadingIndicator.css
└── LoadingIndicator.js
├── ViewportOrientationMarkers
├── ViewportOrientationMarkers.css
└── ViewportOrientationMarkers.js
├── ViewportOverlay
├── ViewportOverlay.css
└── ViewportOverlay.js
├── helpers
├── areStringArraysEqual.js
├── formatDA.js
├── formatNumberPrecision.js
├── formatPN.js
├── formatTM.js
├── index.js
└── isValidNumber.js
├── index.js
├── metadataProvider.js
└── test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "targets": {
5 | "ie": "11"
6 | },
7 | }],
8 | "@babel/preset-react"
9 | ],
10 | "plugins": [
11 | "@babel/plugin-proposal-class-properties"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Use the latest 2.1 version of CircleCI pipeline processing engine, see https://circleci.com/docs/2.0/configuration-reference/
2 | version: 2.1
3 |
4 | defaults: &defaults
5 | working_directory: ~/repo
6 | # https://circleci.com/docs/2.0/circleci-images/#language-image-variants
7 | docker:
8 | - image: cimg/node:16.8.0
9 | environment:
10 | TERM: xterm # Enable colors in term
11 |
12 | jobs:
13 | CHECKOUT:
14 | <<: *defaults
15 | steps:
16 | - checkout
17 | - restore_cache:
18 | name: Restore Package Cache
19 | keys:
20 | - packages-v1-{{ .Branch }}-{{ checksum "package.json" }}
21 | - packages-v1-{{ .Branch }}-
22 | - packages-v1-
23 | - run: npm ci
24 | - save_cache:
25 | name: Save Package Cache
26 | paths:
27 | - ~/repo/node_modules
28 | key: packages-v1-{{ .Branch }}-{{ checksum "package.json" }}
29 | - persist_to_workspace:
30 | root: ~/repo
31 | paths: .
32 |
33 | BUILD_AND_TEST:
34 | <<: *defaults
35 | steps:
36 | - attach_workspace:
37 | at: ~/repo
38 | - run: npm run build
39 | # No tests yet :(
40 | # https://circleci.com/docs/2.0/collect-test-data/#karma
41 | # - store_test_results:
42 | # path: reports/junit
43 | # - store_artifacts:
44 | # path: reports/junit
45 | - persist_to_workspace:
46 | root: ~/repo
47 | paths: .
48 |
49 | NPM_PUBLISH:
50 | <<: *defaults
51 | steps:
52 | - attach_workspace:
53 | at: ~/repo
54 | - run:
55 | name: Avoid hosts unknown for github
56 | command:
57 | mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
58 | no\n" > ~/.ssh/config
59 | - run:
60 | name: Publish using Semantic Release
61 | command: npx semantic-release
62 |
63 | workflows:
64 | version: 2
65 |
66 | # PULL REQUEST
67 | PULL_REQUEST:
68 | jobs:
69 | - CHECKOUT:
70 | filters:
71 | branches:
72 | ignore:
73 | - master
74 | - feature/*
75 | - hotfix/*
76 | - BUILD_AND_TEST:
77 | requires:
78 | - CHECKOUT
79 |
80 | # MERGE TO MASTER
81 | TEST_AND_RELEASE:
82 | jobs:
83 | - CHECKOUT:
84 | filters:
85 | branches:
86 | only: master
87 | - BUILD_AND_TEST:
88 | requires:
89 | - CHECKOUT
90 | - NPM_PUBLISH:
91 | requires:
92 | - BUILD_AND_TEST
93 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.css
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "react-app",
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:prettier/recommended"
7 | ],
8 | "parser": "babel-eslint",
9 | "env": {
10 | "jest": true
11 | },
12 | "settings": {
13 | "react": {
14 | "version": "detect"
15 | }
16 | },
17 | "globals": {
18 | "context": true,
19 | "assert": true
20 | },
21 | "rules": {
22 | "no-console": "warn",
23 | "no-undef": "warn",
24 | "no-unused-vars": "warn",
25 | "prettier/prettier": [
26 | "error",
27 | {
28 | "endOfLine": "auto"
29 | }
30 | ],
31 | // React
32 | // https://github.com/yannickcr/eslint-plugin-react#recommended
33 | "react/sort-comp": "warn"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | .idea
24 | .yalc
25 | yalc.lock
26 | yarn.lock
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "printWidth": 80,
4 | "proseWrap": "always",
5 | "tabWidth": 2,
6 | "semi": true,
7 | "singleQuote": true
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.rulers": [80, 120],
3 |
4 | // ===
5 | // Spacing
6 | // ===
7 |
8 | "editor.insertSpaces": true,
9 | "editor.tabSize": 2,
10 | "editor.trimAutoWhitespace": true,
11 | "files.trimTrailingWhitespace": true,
12 | "files.insertFinalNewline": true,
13 | "files.trimFinalNewlines": true,
14 |
15 | // ===
16 | // Event Triggers
17 | // ===
18 |
19 | "editor.formatOnSave": true,
20 | "eslint.autoFixOnSave": true,
21 | "eslint.run": "onSave",
22 | "eslint.validate": [
23 | { "language": "javascript", "autoFix": true },
24 | { "language": "javascriptreact", "autoFix": true }
25 | ],
26 | "prettier.disableLanguages": [],
27 | "workbench.colorCustomizations": {},
28 | "editor.codeActionsOnSave": {
29 | "source.fixAll.eslint": true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const autoprefixer = require('autoprefixer');
4 | // Plugins
5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
6 | .BundleAnalyzerPlugin;
7 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
8 | const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
9 | .rules;
10 |
11 | const ENTRY_VTK_EXT = path.join(__dirname, './../src/index.js');
12 | const SRC_PATH = path.join(__dirname, './../src');
13 | const OUT_PATH = path.join(__dirname, './../dist');
14 |
15 | /**
16 | * `argv` are options from the CLI. They will override our config here if set.
17 | * `-d` - Development shorthand, sets `debug`, `devtool`, and `mode`
18 | * `-p` - Production shorthand, sets `minimize`, `NODE_ENV`, and `mode`
19 | */
20 | module.exports = (env, argv) => {
21 | const isProdBuild = argv.mode !== 'development';
22 | const outputFilename = isProdBuild ? '[name].umd.min.js' : '[name].umd.js';
23 |
24 | return {
25 | entry: {
26 | vtkViewport: ENTRY_VTK_EXT,
27 | },
28 | devtool: 'source-map',
29 | output: {
30 | path: OUT_PATH,
31 | filename: outputFilename,
32 | library: 'VTKViewport',
33 | libraryTarget: 'umd',
34 | globalObject: 'this',
35 | },
36 | module: {
37 | rules: [
38 | {
39 | test: /\.(js|jsx)$/,
40 | exclude: /node_modules/,
41 | use: ['babel-loader'],
42 | },
43 | {
44 | test: /\.css$/,
45 | exclude: /\.module\.css$/,
46 | use: [
47 | 'style-loader',
48 | 'css-loader',
49 | {
50 | loader: 'postcss-loader',
51 | options: {
52 | plugins: () => [autoprefixer('last 2 version', 'ie >= 10')],
53 | },
54 | },
55 | ],
56 | },
57 | ].concat(vtkRules),
58 | },
59 | resolve: {
60 | modules: [path.resolve(__dirname, './../node_modules'), SRC_PATH],
61 | },
62 | externals: [
63 | // Used to build/load metadata
64 | {
65 | 'cornerstone-core': {
66 | commonjs: 'cornerstone-core',
67 | commonjs2: 'cornerstone-core',
68 | amd: 'cornerstone-core',
69 | root: 'cornerstone',
70 | },
71 | // Vector 3 use
72 | 'cornerstone-math': {
73 | commonjs: 'cornerstone-math',
74 | commonjs2: 'cornerstone-math',
75 | amd: 'cornerstone-math',
76 | root: 'cornerstoneMath',
77 | },
78 | //
79 | react: 'react',
80 | // https://webpack.js.org/guides/author-libraries/#external-limitations
81 | 'vtk.js/Sources': {
82 | commonjs: 'vtk.js',
83 | commonjs2: 'vtk.js',
84 | amd: 'vtk.js',
85 | root: 'vtk.js',
86 | },
87 | },
88 | ],
89 | node: {
90 | // https://github.com/webpack-contrib/style-loader/issues/200
91 | Buffer: false,
92 | },
93 | plugins: [
94 | // Uncomment to generate bundle analyzer
95 | new BundleAnalyzerPlugin(),
96 | // Show build progress
97 | new webpack.ProgressPlugin(),
98 | // Clear dist between builds
99 | // new CleanWebpackPlugin(),
100 | ],
101 | };
102 | };
103 |
--------------------------------------------------------------------------------
/.webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | // Plugins
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const PUBLIC_URL = process.env.PUBLIC_URL || '/';
6 | const ENTRY_VIEWPORT = path.join(__dirname, './../src/index.js');
7 | const ENTRY_EXAMPLES = path.join(__dirname, './../examples/index.js');
8 | const SRC_PATH = path.join(__dirname, './../src');
9 | const OUT_PATH = path.join(__dirname, './../dist');
10 |
11 | module.exports = {
12 | entry: {
13 | examples: ENTRY_EXAMPLES,
14 | },
15 | mode: 'development',
16 | devtool: 'eval',
17 | output: {
18 | path: OUT_PATH,
19 | filename: '[name].bundle.[hash].js',
20 | library: 'cornerstoneViewport',
21 | libraryTarget: 'umd',
22 | globalObject: 'this',
23 | clean: true
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.(js|jsx)$/,
29 | exclude: /node_modules/,
30 | use: ['babel-loader'],
31 | },
32 | {
33 | test: /\.css$/,
34 | exclude: /\.module\.css$/,
35 | use: ['style-loader', 'css-loader'],
36 | },
37 | ],
38 | },
39 | resolve: {
40 | modules: [path.resolve(__dirname, './../node_modules'), SRC_PATH],
41 | alias: {
42 | '@cornerstone-viewport': ENTRY_VIEWPORT,
43 | },
44 | fallback: { fs: false, path: false },
45 | },
46 | plugins: [
47 | // Show build progress
48 | new webpack.ProgressPlugin(),
49 | new webpack.DefinePlugin({
50 | 'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
51 | }),
52 | // Uncomment to generate bundle analyzer
53 | // new BundleAnalyzerPlugin(),
54 | // Generate `index.html` with injected build assets
55 | new HtmlWebpackPlugin({
56 | filename: 'index.html',
57 | template: path.resolve(__dirname, '..', 'examples', 'index.html'),
58 | templateParameters: {
59 | PUBLIC_URL: PUBLIC_URL,
60 | },
61 | }),
62 | ],
63 | // Fix for `cornerstone-wado-image-loader` fs dep
64 | devServer: {
65 | hot: true,
66 | open: true,
67 | port: 3000,
68 | historyApiFallback: {
69 | disableDotRule: true,
70 | },
71 | },
72 | };
73 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Open Health Imaging Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Deprecated] Use Cornerstone3D Instead https://cornerstonejs.org/
2 |
3 | # react-cornerstone-viewport
4 |
5 | > Cornerstone medical image viewport component for React
6 |
7 | [](https://www.npmjs.com/package/react-cornerstone-viewport)
8 |
9 | Documentation and Examples: [https://react.cornerstonejs.org/](https://react.cornerstonejs.org/)
10 |
11 | ## Install
12 |
13 | ```bash
14 | ## NPM
15 | npm install --save react-cornerstone-viewport
16 |
17 | ## Yarn
18 | yarn add react-cornerstone-viewport
19 | ```
20 |
21 | ## Usage
22 |
23 | ```jsx
24 | import React, { Component } from 'react'
25 |
26 | import CornerstoneViewport from 'react-cornerstone-viewport'
27 |
28 | class Example extends Component {
29 | render () {
30 | return (
31 |
32 | )
33 | }
34 | }
35 | ```
36 |
37 | ## License
38 |
39 | MIT © [OHIF](https://github.com/OHIF)
40 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | ie: '11',
8 | },
9 | },
10 | ],
11 | '@babel/preset-react',
12 | ],
13 | plugins: [
14 | '@babel/plugin-proposal-class-properties',
15 | '@babel/plugin-transform-runtime',
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/examples/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
5 | // Routes
6 | import ExamplePageBasic from './ExamplePageBasic.js';
7 | import ExamplePageGrid from './ExamplePageGrid.js';
8 | import ExamplePageCustomOverlay from './ExamplePageCustomOverlay.js';
9 | import ExamplePageEscapeHatch from './ExamplePageEscapeHatch.js';
10 |
11 | /**
12 | *
13 | *
14 | * @param {*} { href, text }
15 | * @returns
16 | */
17 | function LinkOut({ href, text }) {
18 | return (
19 |
20 | {text}
21 |
22 | );
23 | }
24 |
25 | /**
26 | *
27 | *
28 | * @param {*} { title, url, text, screenshotUrl }
29 | * @returns
30 | */
31 | function ExampleEntry({ title, url, text, screenshotUrl }) {
32 | return (
33 |
34 |
35 | {title}
36 |
37 |
{text}
38 |
39 |
40 | );
41 | }
42 |
43 | function Index() {
44 | const style = {
45 | minHeight: '512px',
46 | };
47 |
48 | const examples = [
49 | {
50 | title: 'Props Documentation',
51 | url: '/props',
52 | text: 'COMING SOON',
53 | },
54 | {
55 | title: 'Basic Usage',
56 | url: '/basic',
57 | text:
58 | 'How to render an array of DICOM images and setup common built-in tools.',
59 | },
60 | {
61 | title: 'Grid Layout',
62 | url: '/grid',
63 | text: 'How to render multiple viewports and track the "active viewport".',
64 | },
65 | {
66 | title: 'Custom Overlay and Loader Component',
67 | url: '/custom-overlay',
68 | text:
69 | 'Provide an alternative React Component to use in place of the built in overlay-text and loading indicator components.',
70 | },
71 | {
72 | title: 'Escape Hatch',
73 | url: '/escape-hatch',
74 | text:
75 | 'How to access the created enabledElement so you can leverage cornerstone and cornerstone-tools APIs directly.',
76 | },
77 | // MOST COMPLEX: (mini viewer)
78 | // - (mini viewer) Dynamic Grid + Global Tool Sync + Changing Tools
79 | // Misc. Other Props: (just list them all, prop-types, basic comments for docs)
80 | // - onElementEnabled (escape hatch)
81 | // - eventListeners
82 | // - isStackPrefetchEnabled
83 | // - react-resize-observer
84 | ];
85 |
86 | const exampleComponents = examples.map(e => {
87 | return ;
88 | });
89 |
90 | return (
91 |
92 |
93 |
Cornerstone Viewport
94 |
95 |
96 |
97 |
98 | This is a set of re-usable components for displaying data with{' '}
99 |
103 | .
104 |
105 |
106 |
107 |
108 |
Examples
109 | {exampleComponents}
110 |
111 |
112 |
113 |
Configuring Cornerstone
114 |
115 | All of these examples assume that the cornerstone family of
116 | libraries have been imported and configured prior to use. Here is
117 | brief example of what that may look like in ES6:
118 |
119 |
124 | {`import dicomParser from 'dicom-parser';
125 | import cornerstone from 'cornerstone-core';
126 | import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
127 | import cornerstoneMath from 'cornerstone-math';
128 | import cornerstoneTools from 'cornerstone-tools';
129 | import Hammer from 'hammerjs';
130 |
131 | export default function initCornerstone() {
132 |
133 | // Cornerstone Tools
134 | cornerstoneTools.external.cornerstone = cornerstone;
135 | cornerstoneTools.external.Hammer = Hammer;
136 | cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
137 | cornerstoneTools.init();
138 |
139 | // Image Loader
140 | cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
141 | cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
142 | cornerstoneWADOImageLoader.webWorkerManager.initialize({
143 | maxWebWorkers: navigator.hardwareConcurrency || 1,
144 | startWebWorkersOnDemand: true,
145 | taskConfiguration: {
146 | decodeTask: {
147 | initializeCodecsOnStartup: false,
148 | usePDFJS: false,
149 | strict: false,
150 | },
151 | },
152 | });
153 | }`}
154 |
155 |
156 |
157 |
158 | );
159 | }
160 |
161 | /**
162 | *
163 | *
164 | * @param {*} props
165 | * @returns
166 | */
167 | function Example(props) {
168 | return (
169 |
170 |
171 | Back to Examples
172 |
173 | {props.children}
174 |
175 | );
176 | }
177 |
178 | function AppRouter() {
179 | const basic = () => Example({ children: });
180 | const grid = () => Example({ children: });
181 | const customOverlay = () =>
182 | Example({ children: });
183 | const escapeHatch = () => Example({ children: });
184 |
185 | return (
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | );
197 | }
198 |
199 | export default class App extends Component {
200 | render() {
201 | return ;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/examples/ExamplePageBasic.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CornerstoneViewport from '@cornerstone-viewport';
3 |
4 | // https://github.com/conorhastings/react-syntax-highlighter
5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
7 |
8 | class ExamplePageBasic extends Component {
9 | state = {
10 | tools: [
11 | // Mouse
12 | {
13 | name: 'Wwwc',
14 | mode: 'active',
15 | modeOptions: { mouseButtonMask: 1 },
16 | },
17 | {
18 | name: 'Zoom',
19 | mode: 'active',
20 | modeOptions: { mouseButtonMask: 2 },
21 | },
22 | {
23 | name: 'Pan',
24 | mode: 'active',
25 | modeOptions: { mouseButtonMask: 4 },
26 | },
27 | // Scroll
28 | { name: 'StackScrollMouseWheel', mode: 'active' },
29 | // Touch
30 | { name: 'PanMultiTouch', mode: 'active' },
31 | { name: 'ZoomTouchPinch', mode: 'active' },
32 | { name: 'StackScrollMultiTouch', mode: 'active' },
33 | ],
34 | imageIds: [
35 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.11.dcm',
36 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.12.dcm',
37 | ],
38 | };
39 |
40 | render() {
41 | return (
42 |
43 |
Basic Demo
44 |
45 |
50 |
51 |
52 |
Source / Usage
53 |
54 |
59 | {`state = {
60 | tools: [
61 | // Mouse
62 | {
63 | name: 'Wwwc',
64 | mode: 'active',
65 | modeOptions: { mouseButtonMask: 1 },
66 | },
67 | {
68 | name: 'Zoom',
69 | mode: 'active',
70 | modeOptions: { mouseButtonMask: 2 },
71 | },
72 | {
73 | name: 'Pan',
74 | mode: 'active',
75 | modeOptions: { mouseButtonMask: 4 },
76 | },
77 | // Scroll
78 | { name: 'StackScrollMouseWheel', mode: 'active' },
79 | // Touch
80 | { name: 'PanMultiTouch', mode: 'active' },
81 | { name: 'ZoomTouchPinch', mode: 'active' },
82 | { name: 'StackScrollMultiTouch', mode: 'active' },
83 | ],
84 | imageIds: [
85 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.11.dcm',
86 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.12.dcm',
87 | ],
88 | };
89 |
90 | {/* RENDER */}
91 | `}
96 |
97 |
98 |
99 | );
100 | }
101 | }
102 |
103 | export default ExamplePageBasic;
104 |
--------------------------------------------------------------------------------
/examples/ExamplePageCustomOverlay.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CornerstoneViewport from '@cornerstone-viewport';
3 | import PropTypes from 'prop-types';
4 |
5 | // https://github.com/conorhastings/react-syntax-highlighter
6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
7 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
8 |
9 | class CustomOverlay extends Component {
10 | static propTypes = {
11 | scale: PropTypes.number.isRequired,
12 | windowWidth: PropTypes.number.isRequired,
13 | windowCenter: PropTypes.number.isRequired,
14 | imageId: PropTypes.string.isRequired,
15 | imageIndex: PropTypes.number.isRequired,
16 | stackSize: PropTypes.number.isRequired,
17 | };
18 |
19 | render() {
20 | return (
21 |
31 | 🎉🎉🎉
32 | {Object.keys(this.props).map(key => {
33 | const val = this.props[key];
34 | return (
35 |
36 | {key} : {val}
37 |
38 | );
39 | })}
40 | 🎉🎉🎉
41 |
42 | );
43 | }
44 | }
45 |
46 | class CustomLoader extends Component {
47 | render() {
48 | return (
49 |
63 | );
64 | }
65 | }
66 | class ExamplePageCustomOverlay extends Component {
67 | render() {
68 | return (
69 |
70 |
Custom Overlay
71 |
72 | The most important thing to note here are the props received by the
73 | Custom Overlay component.
74 |
75 |
76 |
92 |
93 |
94 |
Source / Usage
95 |
96 |
101 | {`class CustomOverlay extends Component {
102 | static propTypes = {
103 | scale: PropTypes.number.isRequired,
104 | windowWidth: PropTypes.number.isRequired,
105 | windowCenter: PropTypes.number.isRequired,
106 | imageId: PropTypes.string.isRequired,
107 | imageIndex: PropTypes.number.isRequired,
108 | stackSize: PropTypes.number.isRequired,
109 | };
110 |
111 | render() {
112 | return (
113 |
123 | 🎉🎉🎉
124 | {Object.keys(this.props).map(key => {
125 | const val = this.props[key];
126 | return (
127 |
128 | {key} : {val}
129 |
130 | );
131 | })}
132 | 🎉🎉🎉
133 |
134 | );
135 | }
136 | }
137 |
138 | class CustomLoader extends Component {
139 | render() {
140 | return (
141 |
155 | );
156 | }
157 | }
158 |
159 |
160 | {/* RENDER */}
161 | `}
177 |
178 |
179 |
180 | );
181 | }
182 | }
183 |
184 | export default ExamplePageCustomOverlay;
185 |
--------------------------------------------------------------------------------
/examples/ExamplePageEscapeHatch.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CornerstoneViewport from '@cornerstone-viewport';
3 | import cornerstone from 'cornerstone-core';
4 |
5 | // https://github.com/conorhastings/react-syntax-highlighter
6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
7 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
8 |
9 | class ExamplePageEscapeHatch extends Component {
10 | state = {
11 | cornerstoneElement: undefined,
12 | };
13 |
14 | render() {
15 | return (
16 |
17 |
Escape Hatch
18 |
19 | {
32 | const cornerstoneElement = elementEnabledEvt.detail.element;
33 |
34 | // Save this for later
35 | this.setState({
36 | cornerstoneElement,
37 | });
38 |
39 | // Wait for image to render, then invert it
40 | cornerstoneElement.addEventListener(
41 | 'cornerstoneimagerendered',
42 | imageRenderedEvent => {
43 | const viewport = imageRenderedEvent.detail.viewport;
44 | const invertedViewport = Object.assign({}, viewport, {
45 | invert: true,
46 | });
47 |
48 | cornerstone.setViewport(cornerstoneElement, invertedViewport);
49 | }
50 | );
51 | }}
52 | style={{ minWidth: '100%', height: '512px', flex: '1' }}
53 | />
54 |
55 |
56 |
Source / Usage
57 |
58 | The onElementEnabled event allows us to capture the point in time our
59 | element is enabled, and a reference to the element that was enabled.
60 | The bulk of the Cornerstone and CornerstoneTools APIs use the element
61 | as an identifier in API calls -- having access to it opens the door
62 | for more advanced/custom usage.
63 |
64 |
65 | Most notably, you can forego using the "tools" and "activeTool" props
66 | and instead manage things by hand. This can be particularly useful if
67 | you are leveraging Cornerstone Tool's{' '}
68 |
69 | globalToolSyncEnabled
70 | {' '}
71 | configuration property to manage and synchronize viewport tool
72 | modes/bindings.
73 |
74 |
75 |
76 |
81 | {` {
94 | const cornerstoneElement = elementEnabledEvt.detail.element;
95 |
96 | // Save this for later
97 | this.setState({
98 | cornerstoneElement,
99 | });
100 |
101 | // Wait for image to render, then invert it
102 | cornerstoneElement.addEventListener(
103 | 'cornerstoneimagerendered',
104 | imageRenderedEvent => {
105 | const viewport = imageRenderedEvent.detail.viewport;
106 | const invertedViewport = Object.assign({}, viewport, {
107 | invert: true,
108 | });
109 |
110 | cornerstone.setViewport(cornerstoneElement, invertedViewport);
111 | }
112 | );
113 | }}
114 | style={{ minWidth: '100%', height: '512px', flex: '1' }}
115 | />`}
116 |
117 |
118 |
119 | );
120 | }
121 | }
122 |
123 | export default ExamplePageEscapeHatch;
124 |
--------------------------------------------------------------------------------
/examples/ExamplePageGrid.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CornerstoneViewport from '@cornerstone-viewport';
3 |
4 | // https://github.com/conorhastings/react-syntax-highlighter
5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
7 |
8 | const stack1 = [
9 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.7.dcm',
10 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.8.dcm',
11 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.9.dcm',
12 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.10.dcm',
13 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.11.dcm',
14 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.12.dcm',
15 | ];
16 |
17 | const stack2 = [
18 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.9.dcm',
19 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.10.dcm',
20 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.11.dcm',
21 | ];
22 |
23 | class ExamplePageGrid extends Component {
24 | state = {
25 | activeViewportIndex: 0,
26 | viewports: [0, 1, 2, 3],
27 | tools: [
28 | // Mouse
29 | {
30 | name: 'Wwwc',
31 | mode: 'active',
32 | modeOptions: { mouseButtonMask: 1 },
33 | },
34 | {
35 | name: 'Zoom',
36 | mode: 'active',
37 | modeOptions: { mouseButtonMask: 2 },
38 | },
39 | {
40 | name: 'Pan',
41 | mode: 'active',
42 | modeOptions: { mouseButtonMask: 4 },
43 | },
44 | 'Length',
45 | 'Angle',
46 | 'Bidirectional',
47 | 'FreehandRoi',
48 | 'Eraser',
49 | // Scroll
50 | { name: 'StackScrollMouseWheel', mode: 'active' },
51 | // Touch
52 | { name: 'PanMultiTouch', mode: 'active' },
53 | { name: 'ZoomTouchPinch', mode: 'active' },
54 | { name: 'StackScrollMultiTouch', mode: 'active' },
55 | ],
56 | imageIds: stack1,
57 | // FORM
58 | activeTool: 'Wwwc',
59 | imageIdIndex: 0,
60 | isPlaying: false,
61 | frameRate: 22,
62 | };
63 |
64 | componentDidMount() {}
65 |
66 | render() {
67 | return (
68 |
69 |
Grid Demo
70 |
71 | {this.state.viewports.map(vp => (
72 | {
83 | this.setState({
84 | activeViewportIndex: vp,
85 | });
86 | }}
87 | />
88 | ))}
89 |
90 |
91 | {/* FORM */}
92 |
Misc. Props
93 |
94 | Note, when we change the active stack, we also need to update the
95 | imageIdIndex prop to a value that falls within the new stack's range
96 | of possible indexes.
97 |
98 |
200 |
201 | {/* CODE SNIPPET */}
202 |
Source / Usage
203 |
204 |
209 | {`state = {
210 | activeViewportIndex: 0,
211 | viewports: [0, 1, 2, 3],
212 | tools: [
213 | // Mouse
214 | {
215 | name: 'Wwwc',
216 | mode: 'active',
217 | modeOptions: { mouseButtonMask: 1 },
218 | },
219 | {
220 | name: 'Zoom',
221 | mode: 'active',
222 | modeOptions: { mouseButtonMask: 2 },
223 | },
224 | {
225 | name: 'Pan',
226 | mode: 'active',
227 | modeOptions: { mouseButtonMask: 4 },
228 | },
229 | 'Length',
230 | 'Angle',
231 | 'Bidirectional',
232 | 'FreehandRoi',
233 | 'Eraser',
234 | // Scroll
235 | { name: 'StackScrollMouseWheel', mode: 'active' },
236 | // Touch
237 | { name: 'PanMultiTouch', mode: 'active' },
238 | { name: 'ZoomTouchPinch', mode: 'active' },
239 | { name: 'StackScrollMultiTouch', mode: 'active' },
240 | ],
241 | imageIds: [
242 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.9.dcm',
243 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.10.dcm',
244 | 'dicomweb://s3.amazonaws.com/lury/PTCTStudy/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.11.dcm',
245 | ],
246 | // FORM
247 | activeTool: 'Wwwc',
248 | imageIdIndex: 0,
249 | isPlaying: false,
250 | frameRate: 22,
251 | };
252 |
253 | {/* RENDER */}
254 |
255 | {this.state.viewports.map(viewportIndex => (
256 | {
267 | this.setState({
268 | activeViewportIndex: viewportIndex,
269 | });
270 | }}
271 | />
272 | ))}
273 |
`}
274 |
275 |
276 |
277 | );
278 | }
279 | }
280 |
281 | export default ExamplePageGrid;
282 |
--------------------------------------------------------------------------------
/examples/index.css:
--------------------------------------------------------------------------------
1 | /**
2 | * DEMO STYLES
3 | * - Not exported as part of library
4 | */
5 | html {
6 | box-sizing: border-box;
7 | }
8 |
9 | *,
10 | *:before,
11 | *:after {
12 | box-sizing: inherit;
13 | }
14 |
15 | body {
16 | margin : 0;
17 | padding : 0;
18 | font-family: sans-serif;
19 | }
20 |
21 | .viewport-wrapper {
22 | border: 2px solid black;
23 | }
24 |
25 | .viewport-wrapper.active {
26 | border: 2px solid dodgerblue;
27 | }
28 |
29 | /* Custom Loader with animation */
30 | .lds-ripple {
31 | display : inline-block;
32 | position: relative;
33 | width : 64px;
34 | height : 64px;
35 | }
36 |
37 | .lds-ripple div {
38 | position : absolute;
39 | border : 4px solid blue;
40 | opacity : 1;
41 | border-radius: 50%;
42 | animation : lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
43 | }
44 |
45 | .lds-ripple div:nth-child(2) {
46 | animation-delay: -0.5s;
47 | }
48 |
49 | @keyframes lds-ripple {
50 | 0% {
51 | top : 28px;
52 | left : 28px;
53 | width : 0;
54 | height : 0;
55 | opacity: 1;
56 | }
57 |
58 | 100% {
59 | top : -1px;
60 | left : -1px;
61 | width : 58px;
62 | height : 58px;
63 | opacity: 0;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 | Cornerstone Viewport
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import './index.css';
5 | import App from './App';
6 | import initCornerstone from './initCornerstone.js';
7 |
8 | //
9 | initCornerstone();
10 | ReactDOM.render( , document.getElementById('root'));
11 |
--------------------------------------------------------------------------------
/examples/initCornerstone.js:
--------------------------------------------------------------------------------
1 | import dicomParser from 'dicom-parser';
2 | import cornerstone from 'cornerstone-core';
3 | import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
4 | import cornerstoneMath from 'cornerstone-math';
5 | import cornerstoneTools from 'cornerstone-tools';
6 | import Hammer from 'hammerjs';
7 |
8 | export default function initCornerstone() {
9 | // Cornertone Tools
10 | cornerstoneTools.external.cornerstone = cornerstone;
11 | cornerstoneTools.external.Hammer = Hammer;
12 | cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
13 |
14 | //
15 | cornerstoneTools.init();
16 |
17 | // Preferences
18 | const fontFamily =
19 | 'Work Sans, Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
20 | cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
21 | cornerstoneTools.toolStyle.setToolWidth(2);
22 | cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)');
23 | cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
24 |
25 | cornerstoneTools.store.state.touchProximity = 40;
26 |
27 | // IMAGE LOADER
28 | cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
29 | cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
30 | cornerstoneWADOImageLoader.webWorkerManager.initialize({
31 | maxWebWorkers: navigator.hardwareConcurrency || 1,
32 | startWebWorkersOnDemand: true,
33 | taskConfiguration: {
34 | decodeTask: {
35 | initializeCodecsOnStartup: false,
36 | usePDFJS: false,
37 | strict: false,
38 | },
39 | },
40 | });
41 |
42 | // Debug
43 | window.cornerstone = cornerstone;
44 | window.cornerstoneTools = cornerstoneTools;
45 | }
46 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | # https://www.netlify.com/docs/netlify-toml-reference/
2 |
3 | [build]
4 | # Directory (relative to root of your repo) that contains the deploy-ready
5 | # HTML files and assets generated by the build. If a base directory has
6 | # been specified, include it in the publish directory path.
7 | command = "npm run build:examples"
8 | publish = "dist"
9 |
10 | # COMMENT: NODE_VERSION in root `.nvmrc` takes priority
11 | [build.environment]
12 | NODE_VERSION = "16.8.0"
13 |
14 | # COMMENT: This a rule for Single Page Applications
15 | [[redirects]]
16 | from = "/*"
17 | to = "/index.html"
18 | status = 200
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-cornerstone-viewport",
3 | "version": "0.2.2",
4 | "description": "Cornerstone medical image viewport component for React",
5 | "author": "Cornerstone Contributors",
6 | "license": "MIT",
7 | "repository": "cornerstonejs/react-cornerstone-viewport",
8 | "main": "dist/index.js",
9 | "browser": "dist/index.umd.js",
10 | "module": "dist/index.es.js",
11 | "jsnext:main": "dist/index.es.js",
12 | "engines": {
13 | "node": ">=8",
14 | "npm": ">=5"
15 | },
16 | "scripts": {
17 | "build": "rollup -c",
18 | "build:release": "rollup -c",
19 | "build:examples": "webpack --progress --config ./.webpack/webpack.dev.js",
20 | "cm": "npx git-cz",
21 | "dev": "webpack serve --config ./.webpack/webpack.dev.js",
22 | "start": "npm run dev",
23 | "prepublishOnly": "npm run build && npm run build:release"
24 | },
25 | "peerDependencies": {
26 | "cornerstone-core": "^2.6.0",
27 | "cornerstone-math": "^0.1.9",
28 | "cornerstone-tools": "^6.0.1",
29 | "cornerstone-wado-image-loader": "^4.0.4",
30 | "dicom-parser": "^1.8.8",
31 | "hammerjs": "^2.0.8",
32 | "react": "^17.0.2",
33 | "react-dom": "^17.0.2"
34 | },
35 | "dependencies": {
36 | "classnames": "^2.3.1",
37 | "date-fns": "^2.23.0",
38 | "lodash.debounce": "^4.0.8",
39 | "prop-types": "^15.7.2",
40 | "react-resize-detector": "^6.7.6"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.15.5",
44 | "@babel/plugin-proposal-class-properties": "^7.14.5",
45 | "@babel/plugin-transform-runtime": "^7.15.0",
46 | "@babel/preset-env": "^7.15.6",
47 | "@babel/preset-react": "^7.14.5",
48 | "@svgr/rollup": "^5.5.0",
49 | "babel-eslint": "10.1.0",
50 | "babel-loader": "^8.2.2",
51 | "commitizen": "4.2.x",
52 | "cornerstone-core": "^2.6.0",
53 | "cornerstone-math": "^0.1.9",
54 | "cornerstone-tools": "^6.0.1",
55 | "cornerstone-wado-image-loader": "^4.0.4",
56 | "cross-env": "^7.0.3",
57 | "css-loader": "^6.2.0",
58 | "dicom-parser": "^1.8.8",
59 | "eslint": "7.32.0",
60 | "eslint-config-prettier": "^8.3.0",
61 | "eslint-config-react-app": "^6.0.0",
62 | "eslint-plugin-flowtype": "^6.0.1",
63 | "eslint-plugin-import": "^2.24.2",
64 | "eslint-plugin-jsx-a11y": "^6.4.1",
65 | "eslint-plugin-node": "^11.1.0",
66 | "eslint-plugin-prettier": "^4.0.0",
67 | "eslint-plugin-promise": "^5.1.0",
68 | "eslint-plugin-react": "^7.25.1",
69 | "eslint-plugin-react-hooks": "^4.2.0",
70 | "hammerjs": "^2.0.8",
71 | "html-webpack-plugin": "^5.3.2",
72 | "husky": "^7.0.2",
73 | "lint-staged": "^11.1.2",
74 | "prettier": "^2.4.0",
75 | "react": "^16.6.3",
76 | "react-dom": "^16.6.3",
77 | "react-router-dom": "^5.3.0",
78 | "react-syntax-highlighter": "^15.4.4",
79 | "rollup": "^2.56.3",
80 | "rollup-plugin-babel": "^4.4.0",
81 | "rollup-plugin-commonjs": "^10.1.0",
82 | "rollup-plugin-node-resolve": "^5.2.0",
83 | "rollup-plugin-peer-deps-external": "^2.2.4",
84 | "rollup-plugin-postcss": "^4.0.1",
85 | "rollup-plugin-url": "^3.0.1",
86 | "semantic-release": "^17.4.7",
87 | "style-loader": "^3.2.1",
88 | "webpack": "5.52.1",
89 | "webpack-cli": "^4.8.0",
90 | "webpack-dev-server": "^4.2.1",
91 | "webpack-merge": "^5.8.0"
92 | },
93 | "husky": {
94 | "hooks": {
95 | "pre-commit": "lint-staged"
96 | }
97 | },
98 | "lint-staged": {
99 | "src/**/*.{js,jsx,json,css}": [
100 | "eslint --fix",
101 | "prettier --write",
102 | "git add"
103 | ]
104 | },
105 | "browserslist": [
106 | ">0.2%",
107 | "not dead",
108 | "not ie < 11",
109 | "not op_mini all"
110 | ],
111 | "files": [
112 | "dist"
113 | ],
114 | "config": {
115 | "commitizen": {
116 | "path": "./node_modules/cz-conventional-changelog"
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 | react-cornerstone-viewport
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/initCornerstone.js:
--------------------------------------------------------------------------------
1 | cornerstoneTools.external.cornerstone = cornerstone;
2 | cornerstoneTools.external.Hammer = Hammer;
3 | cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
4 |
5 | cornerstoneTools.init();
6 |
7 | const config = {
8 | maxWebWorkers: navigator.hardwareConcurrency || 1,
9 | startWebWorkersOnDemand: true,
10 | webWorkerTaskPaths: [],
11 | taskConfiguration: {
12 | decodeTask: {
13 | loadCodecsOnStartup: true,
14 | initializeCodecsOnStartup: false,
15 | strict: false
16 | }
17 | }
18 | };
19 |
20 | cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
21 |
22 | cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
23 | cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
24 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-cornerstone-viewport",
3 | "name": "react-cornerstone-viewport",
4 | "start_url": "./index.html",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/public/script-tag-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | react-viewerbase
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | You need to enable JavaScript to run this app.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
48 |
49 |
50 |
51 |
52 |
53 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import external from 'rollup-plugin-peer-deps-external';
4 | import postcss from 'rollup-plugin-postcss';
5 | import resolve from 'rollup-plugin-node-resolve';
6 | import url from 'rollup-plugin-url';
7 | import svgr from '@svgr/rollup';
8 |
9 | import pkg from './package.json';
10 |
11 | const globals = {
12 | react: 'React',
13 | 'react-dom': 'ReactDOM',
14 | 'cornerstone-core': 'cornerstone',
15 | 'cornerstone-math': 'cornerstoneMath',
16 | 'cornerstone-tools': 'cornerstoneTools',
17 | 'cornerstone-wado-image-loader': 'cornerstoneWADOImageLoader',
18 | 'dicom-parser': 'dicomParser',
19 | hammerjs: 'Hammer',
20 | };
21 |
22 | export default {
23 | input: 'src/index.js',
24 | output: [
25 | {
26 | file: pkg.main,
27 | format: 'cjs',
28 | sourcemap: true,
29 | globals,
30 | },
31 | {
32 | file: pkg.browser,
33 | format: 'umd',
34 | name: 'react-cornerstone-viewport',
35 | sourcemap: true,
36 | globals,
37 | },
38 | {
39 | file: pkg.module,
40 | format: 'es',
41 | sourcemap: true,
42 | globals,
43 | },
44 | ],
45 | plugins: [
46 | external(),
47 | postcss({
48 | modules: false,
49 | }),
50 | url(),
51 | svgr(),
52 | babel({
53 | exclude: 'node_modules/**',
54 | plugins: ['@babel/transform-runtime'],
55 | runtimeHelpers: true,
56 | }),
57 | resolve(),
58 | commonjs(),
59 | ],
60 | };
61 |
--------------------------------------------------------------------------------
/src/CornerstoneViewport/CornerstoneViewport.css:
--------------------------------------------------------------------------------
1 | .viewport-wrapper {
2 | width: 100%;
3 | height: 100%; /* MUST have `height` to prevent resize infinite loop */
4 | position: relative;
5 | }
6 |
7 | .viewport-element {
8 | width: 100%;
9 | height: 100%;
10 | position: relative;
11 | background-color: black;
12 |
13 | /* Prevent the blue outline in Chrome when a viewport is selected */
14 | outline: 0 !important;
15 |
16 | /* Prevents the entire page from getting larger
17 | when the magnify tool is near the sides/corners of the page */
18 | overflow: hidden;
19 | }
20 |
--------------------------------------------------------------------------------
/src/CornerstoneViewport/CornerstoneViewport.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 | import ImageScrollbar from '../ImageScrollbar/ImageScrollbar.js';
5 | import ViewportOverlay from '../ViewportOverlay/ViewportOverlay.js';
6 | import LoadingIndicator from '../LoadingIndicator/LoadingIndicator.js';
7 | import ViewportOrientationMarkers from '../ViewportOrientationMarkers/ViewportOrientationMarkers.js';
8 | import cornerstone from 'cornerstone-core';
9 | import cornerstoneTools from 'cornerstone-tools';
10 | import ReactResizeDetector from 'react-resize-detector';
11 | import debounce from 'lodash.debounce';
12 |
13 | // Util
14 | import areStringArraysEqual from './../helpers/areStringArraysEqual.js';
15 |
16 | import './CornerstoneViewport.css';
17 |
18 | const addToBeginning = true;
19 | const priority = -5;
20 | const requestType = 'interaction';
21 |
22 | const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
23 | const { loadHandlerManager } = cornerstoneTools;
24 |
25 | class CornerstoneViewport extends Component {
26 | static propTypes = {
27 | imageIds: PropTypes.arrayOf(PropTypes.string).isRequired,
28 | imageIdIndex: PropTypes.number,
29 | // Controlled
30 | activeTool: PropTypes.string,
31 | tools: PropTypes.arrayOf(
32 | PropTypes.oneOfType([
33 | // String
34 | PropTypes.string,
35 | // Object
36 | PropTypes.shape({
37 | name: PropTypes.string, // Tool Name
38 | toolClass: PropTypes.func, // Custom (ToolClass)
39 | props: PropTypes.Object, // Props to Pass to `addTool`
40 | mode: PropTypes.string, // Initial mode, if one other than default
41 | modeOptions: PropTypes.Object, // { mouseButtonMask: [int] }
42 | }),
43 | ])
44 | ),
45 | // Optional
46 | // isActive ?? classname -> active
47 | children: PropTypes.node,
48 | cornerstoneOptions: PropTypes.object, // cornerstone.enable options
49 | isStackPrefetchEnabled: PropTypes.bool, // should prefetch?
50 | // CINE
51 | isPlaying: PropTypes.bool,
52 | frameRate: PropTypes.number, // Between 1 and ?
53 | //
54 | initialViewport: PropTypes.object,
55 | setViewportActive: PropTypes.func, // Called when viewport should be set to active?
56 | onNewImage: PropTypes.func,
57 | onNewImageDebounced: PropTypes.func,
58 | onNewImageDebounceTime: PropTypes.number,
59 | viewportOverlayComponent: PropTypes.oneOfType([
60 | PropTypes.string,
61 | PropTypes.func,
62 | ]),
63 | // Cornerstone Events
64 | onElementEnabled: PropTypes.func, // Escape hatch
65 | eventListeners: PropTypes.arrayOf(
66 | PropTypes.shape({
67 | target: PropTypes.oneOf(['element', 'cornerstone']).isRequired,
68 | eventName: PropTypes.string.isRequired,
69 | handler: PropTypes.func.isRequired,
70 | })
71 | ),
72 | startLoadHandler: PropTypes.func,
73 | endLoadHandler: PropTypes.func,
74 | loadIndicatorDelay: PropTypes.number,
75 | loadingIndicatorComponent: PropTypes.oneOfType([
76 | PropTypes.element,
77 | PropTypes.func,
78 | ]),
79 | /** false to enable automatic viewport resizing */
80 | enableResizeDetector: PropTypes.bool,
81 | /** rate at witch to apply resize mode's logic */
82 | resizeRefreshRateMs: PropTypes.number,
83 | /** whether resize refresh behavior is exhibited as throttle or debounce */
84 | resizeRefreshMode: PropTypes.oneOf(['throttle', 'debounce']),
85 | //
86 | style: PropTypes.object,
87 | className: PropTypes.string,
88 | isOverlayVisible: PropTypes.bool,
89 | orientationMarkers: PropTypes.arrayOf(PropTypes.string),
90 | };
91 |
92 | static defaultProps = {
93 | // Watch
94 | imageIdIndex: 0,
95 | isPlaying: false,
96 | cineFrameRate: 24,
97 | viewportOverlayComponent: ViewportOverlay,
98 | imageIds: ['no-id://'],
99 | initialViewport: {},
100 | // Init
101 | cornerstoneOptions: {},
102 | isStackPrefetchEnabled: false,
103 | isOverlayVisible: true,
104 | loadIndicatorDelay: 45,
105 | loadingIndicatorComponent: LoadingIndicator,
106 | enableResizeDetector: true,
107 | resizeRefreshRateMs: 200,
108 | resizeRefreshMode: 'debounce',
109 | tools: [],
110 | onNewImageDebounceTime: 0,
111 | orientationMarkers: ['top', 'left'],
112 | };
113 |
114 | constructor(props) {
115 | super(props);
116 |
117 | const imageIdIndex = props.imageIdIndex;
118 | const imageId = props.imageIds[imageIdIndex];
119 | const isOverlayVisible = props.isOverlayVisible;
120 |
121 | this.state = {
122 | // Used for metadata lookup (imagePlane, orientation markers)
123 | // We can probs grab this once and hold on to? (updated on newImage)
124 | imageId,
125 | imageIdIndex, // Maybe
126 | imageProgress: 0,
127 | isLoading: true,
128 | error: null,
129 | // Overlay
130 | scale: undefined,
131 | windowWidth: undefined,
132 | windowCenter: undefined,
133 | isOverlayVisible,
134 | // Orientation Markers
135 | rotationDegrees: undefined,
136 | isFlippedVertically: undefined,
137 | isFlippedHorizontally: undefined,
138 | };
139 |
140 | this._validateExternalEventsListeners();
141 |
142 | // TODO: Deep Copy? How does that work w/ handlers?
143 | // Save a copy. Props could change before `willUnmount`
144 | this.startLoadHandler = this.props.startLoadHandler;
145 | this.endLoadHandler = this.props.endLoadHandler;
146 | this.loadHandlerTimeout = undefined; // "Loading..." timer
147 |
148 | this.numImagesLoaded = 0;
149 | }
150 |
151 | // ~~ LIFECYCLE
152 | async componentDidMount() {
153 | const {
154 | tools,
155 | isStackPrefetchEnabled,
156 | cornerstoneOptions,
157 | imageIds,
158 | isPlaying,
159 | frameRate,
160 | initialViewport,
161 | } = this.props;
162 | const { imageIdIndex } = this.state;
163 | const imageId = imageIds[imageIdIndex];
164 |
165 | // ~~ EVENTS: CORNERSTONE
166 | this._handleOnElementEnabledEvent();
167 | this._bindInternalCornerstoneEventListeners();
168 | this._bindExternalEventListeners('cornerstone');
169 |
170 | cornerstone.enable(this.element, cornerstoneOptions);
171 |
172 | // ~~ EVENTS: ELEMENT
173 | this._bindInternalElementEventListeners();
174 | this._bindExternalEventListeners('element');
175 |
176 | // Only after `uuid` is set for enabledElement
177 | this._setupLoadHandlers();
178 |
179 | try {
180 | // Setup "Stack State"
181 | cornerstoneTools.clearToolState(this.element, 'stack');
182 | cornerstoneTools.addStackStateManager(this.element, [
183 | 'stack',
184 | 'playClip',
185 | 'referenceLines',
186 | ]);
187 | cornerstoneTools.addToolState(this.element, 'stack', {
188 | imageIds: [...imageIds],
189 | currentImageIdIndex: imageIdIndex,
190 | });
191 |
192 | // Load first image in stack
193 | const options = {
194 | addToBeginning,
195 | priority,
196 | };
197 |
198 | const requestFn = (imageId, options) => {
199 | return cornerstone.loadAndCacheImage(imageId, options).then((image) => {
200 | cornerstone.displayImage(this.element, image, initialViewport);
201 | });
202 | };
203 |
204 | // 1. Load the image using the ImageLoadingPool
205 | cornerstone.imageLoadPoolManager.addRequest(
206 | requestFn.bind(this, imageId, options),
207 | requestType,
208 | {
209 | imageId,
210 | },
211 | priority,
212 | addToBeginning
213 | );
214 |
215 | if (isStackPrefetchEnabled) {
216 | cornerstoneTools.stackPrefetch.enable(this.element);
217 | }
218 |
219 | if (isPlaying) {
220 | const validFrameRate = Math.max(frameRate, 1);
221 | cornerstoneTools.playClip(this.element, validFrameRate);
222 | }
223 |
224 | _addAndConfigureInitialToolsForElement(tools, this.element);
225 | _trySetActiveTool(this.element, this.props.activeTool);
226 | this.setState({ isLoading: false });
227 | } catch (error) {
228 | this.setState({ error, isLoading: false });
229 | }
230 | }
231 |
232 | async componentDidUpdate(prevProps, prevState) {
233 | // ~~ STACK/IMAGE
234 | const {
235 | imageIds: stack,
236 | imageIdIndex: imageIndex,
237 | isStackPrefetchEnabled,
238 | initialViewport,
239 | } = this.props;
240 | const {
241 | imageIds: prevStack,
242 | imageIdIndex: prevImageIndex,
243 | isStackPrefetchEnabled: prevIsStackPrefetchEnabled,
244 | } = prevProps;
245 | const hasStackChanged = !areStringArraysEqual(prevStack, stack);
246 | const hasImageIndexChanged =
247 | imageIndex != null && imageIndex !== prevImageIndex;
248 | let updatedState = {};
249 |
250 | if (hasStackChanged) {
251 | // update stack toolstate
252 | cornerstoneTools.clearToolState(this.element, 'stack');
253 | cornerstoneTools.addToolState(this.element, 'stack', {
254 | imageIds: [...stack],
255 | currentImageIdIndex: imageIndex || 0,
256 | });
257 |
258 | // New stack; reset counter
259 | updatedState['numImagesLoaded'] = 0;
260 | updatedState['error'] = null; // Reset error on new stack
261 |
262 | try {
263 | // load + display image
264 | const imageId = stack[imageIndex || 0];
265 | cornerstoneTools.stopClip(this.element);
266 | const requestFn = (imageId, options) => {
267 | return cornerstone
268 | .loadAndCacheImage(imageId, options)
269 | .then((image) => {
270 | cornerstone.displayImage(this.element, image, initialViewport);
271 | cornerstone.reset(this.element);
272 | });
273 | };
274 |
275 | cornerstone.imageLoadPoolManager.addRequest(
276 | requestFn.bind(this, imageId, { addToBeginning, priority }),
277 | requestType,
278 | {
279 | imageId,
280 | },
281 | priority,
282 | addToBeginning
283 | );
284 | } catch (err) {
285 | // :wave:
286 | // What if user kills component before `displayImage`?
287 | }
288 | } else if (!hasStackChanged && hasImageIndexChanged) {
289 | scrollToIndex(this.element, imageIndex);
290 | }
291 |
292 | const shouldStopStartStackPrefetch =
293 | (isStackPrefetchEnabled && hasStackChanged) ||
294 | (!prevIsStackPrefetchEnabled && isStackPrefetchEnabled === true);
295 |
296 | // Need to stop/start to pickup stack changes in prefetcher
297 | if (shouldStopStartStackPrefetch) {
298 | cornerstoneTools.stackPrefetch.enable(this.element);
299 | }
300 |
301 | // ~~ ACTIVE TOOL
302 | const { activeTool } = this.props;
303 | const { activeTool: prevActiveTool } = prevProps;
304 | const hasActiveToolChanges = activeTool !== prevActiveTool;
305 |
306 | if (hasActiveToolChanges) {
307 | _trySetActiveTool(this.element, activeTool);
308 | }
309 |
310 | // ~~ CINE
311 | const { frameRate, isPlaying, isOverlayVisible } = this.props;
312 | const {
313 | frameRate: prevFrameRate,
314 | isPlaying: prevIsPlaying,
315 | isOverlayVisible: prevIsOverlayVisible,
316 | } = prevProps;
317 | const validFrameRate = Math.max(frameRate, 1);
318 | const shouldStart =
319 | (isPlaying !== prevIsPlaying && isPlaying) ||
320 | (isPlaying && hasStackChanged);
321 | const shouldPause = isPlaying !== prevIsPlaying && !isPlaying;
322 | const hasFrameRateChanged = isPlaying && frameRate !== prevFrameRate;
323 |
324 | if (shouldStart || hasFrameRateChanged) {
325 | cornerstoneTools.playClip(this.element, validFrameRate);
326 | } else if (shouldPause) {
327 | cornerstoneTools.stopClip(this.element);
328 | }
329 |
330 | // ~~ OVERLAY
331 | if (isOverlayVisible !== prevIsOverlayVisible)
332 | updatedState.isOverlayVisible = isOverlayVisible;
333 |
334 | // ~~ STATE: Update aggregated state changes
335 | if (Object.keys(updatedState).length > 0) {
336 | this.setState(updatedState);
337 | }
338 |
339 | this._validateExternalEventsListeners();
340 | }
341 |
342 | /**
343 | * Tear down any listeners/handlers, and stop any asynchronous/queued operations
344 | * that could fire after Unmount and cause errors.
345 | *
346 | * @memberof CornerstoneViewport
347 | * @returns {undefined}
348 | */
349 | componentWillUnmount() {
350 | const clear = true;
351 |
352 | this._handleOnElementEnabledEvent(clear);
353 | this._bindInternalCornerstoneEventListeners(clear);
354 | this._bindInternalElementEventListeners(clear);
355 | this._bindExternalEventListeners('cornerstone', clear);
356 | this._bindExternalEventListeners('element', clear);
357 | this._setupLoadHandlers(clear);
358 |
359 | if (this.props.isStackPrefetchEnabled) {
360 | cornerstoneTools.stackPrefetch.disable(this.element);
361 | }
362 |
363 | cornerstoneTools.clearToolState(this.element, 'stackPrefetch');
364 | cornerstoneTools.stopClip(this.element);
365 | cornerstone.disable(this.element);
366 | }
367 |
368 | /**
369 | * @returns Component
370 | * @memberof CornerstoneViewport
371 | */
372 | getLoadingIndicator() {
373 | const { loadingIndicatorComponent: Component } = this.props;
374 | const { error, imageProgress } = this.state;
375 |
376 | return ;
377 | }
378 |
379 | /**
380 | *
381 | *
382 | * @returns
383 | * @memberof CornerstoneViewport
384 | */
385 | getOverlay() {
386 | const { viewportOverlayComponent: Component, imageIds } = this.props;
387 | const { imageIdIndex, scale, windowWidth, windowCenter, isOverlayVisible } =
388 | this.state;
389 | const imageId = imageIds[imageIdIndex];
390 | return (
391 | imageId &&
392 | windowWidth &&
393 | isOverlayVisible && (
394 |
402 | )
403 | );
404 | }
405 |
406 | /**
407 | *
408 | *
409 | * @returns
410 | * @memberof CornerstoneViewport
411 | */
412 | getOrientationMarkersOverlay() {
413 | const { imageIds, orientationMarkers } = this.props;
414 | const {
415 | imageIdIndex,
416 | rotationDegrees,
417 | isFlippedVertically,
418 | isFlippedHorizontally,
419 | } = this.state;
420 | const imageId = imageIds[imageIdIndex];
421 |
422 | // Workaround for below TODO stub
423 | if (!imageId) {
424 | return false;
425 | }
426 | // TODO: This is throwing an error with an undefined `imageId`, and it shouldn't be
427 | const { rowCosines, columnCosines } =
428 | cornerstone.metaData.get('imagePlaneModule', imageId) || {};
429 |
430 | if (!rowCosines || !columnCosines || rotationDegrees === undefined) {
431 | return false;
432 | }
433 |
434 | return (
435 |
443 | );
444 | }
445 |
446 | /**
447 | *
448 | *
449 | * @param {boolean} [clear=false] - True to clear event listeners
450 | * @memberof CornerstoneViewport
451 | * @returns {undefined}
452 | */
453 | _bindInternalCornerstoneEventListeners(clear = false) {
454 | const addOrRemoveEventListener = clear
455 | ? 'removeEventListener'
456 | : 'addEventListener';
457 |
458 | // Update image load progress
459 | cornerstone.events[addOrRemoveEventListener](
460 | 'cornerstoneimageloadprogress',
461 | this.onImageProgress
462 | );
463 |
464 | // Update number of images loaded
465 | cornerstone.events[addOrRemoveEventListener](
466 | cornerstone.EVENTS.IMAGE_LOADED,
467 | this.onImageLoaded
468 | );
469 | }
470 |
471 | /**
472 | *
473 | *
474 | * @param {boolean} [clear=false] - True to clear event listeners
475 | * @memberof CornerstoneViewport
476 | * @returns {undefined}
477 | */
478 | _bindInternalElementEventListeners(clear = false) {
479 | const addOrRemoveEventListener = clear
480 | ? 'removeEventListener'
481 | : 'addEventListener';
482 |
483 | // Updates state's imageId, and imageIndex
484 | this.element[addOrRemoveEventListener](
485 | cornerstone.EVENTS.NEW_IMAGE,
486 | this.onNewImage
487 | );
488 |
489 | // Updates state's imageId, and imageIndex
490 | this.element[addOrRemoveEventListener](
491 | cornerstone.EVENTS.NEW_IMAGE,
492 | this.onNewImageDebounced
493 | );
494 |
495 | // Updates state's viewport
496 | this.element[addOrRemoveEventListener](
497 | cornerstone.EVENTS.IMAGE_RENDERED,
498 | this.onImageRendered
499 | );
500 |
501 | // Set Viewport Active
502 | this.element[addOrRemoveEventListener](
503 | cornerstoneTools.EVENTS.MOUSE_CLICK,
504 | this.setViewportActive
505 | );
506 | this.element[addOrRemoveEventListener](
507 | cornerstoneTools.EVENTS.MOUSE_DOWN,
508 | this.setViewportActive
509 | );
510 | this.element[addOrRemoveEventListener](
511 | cornerstoneTools.EVENTS.TOUCH_PRESS,
512 | this.setViewportActive
513 | );
514 | this.element[addOrRemoveEventListener](
515 | cornerstoneTools.EVENTS.TOUCH_START,
516 | this.setViewportActive
517 | );
518 | this.element[addOrRemoveEventListener](
519 | cornerstoneTools.EVENTS.STACK_SCROLL,
520 | this.setViewportActive
521 | );
522 | }
523 |
524 | /**
525 | * TODO: The ordering here will cause ELEMENT_ENABLED and ELEMENT_DISABLED
526 | * events to never fire. We should have explicit callbacks for these,
527 | * and warn appropriately if user attempts to use them with this prop.
528 | *
529 | *
530 | * Listens out for all events and then defers handling to a single listener to
531 | * act on them
532 | *
533 | * @param {string} target - "cornerstone" || "element"
534 | * @param {boolean} [clear=false] - True to clear event listeners
535 | * @returns {undefined}
536 | */
537 | _bindExternalEventListeners(targetType, clear = false) {
538 | const addOrRemoveEventListener = clear
539 | ? 'removeEventListener'
540 | : 'addEventListener';
541 |
542 | // Unique list of event names
543 | const cornerstoneEvents = Object.values(cornerstone.EVENTS);
544 | const cornerstoneToolsEvents = Object.values(cornerstoneTools.EVENTS);
545 | const csEventNames = cornerstoneEvents.concat(cornerstoneToolsEvents);
546 |
547 | const targetElementOrCornerstone =
548 | targetType === 'element' ? this.element : cornerstone.events;
549 | const boundMethod = this._handleExternalEventListeners.bind(this);
550 |
551 | // Bind our single handler to every cornerstone event
552 | for (let i = 0; i < csEventNames.length; i++) {
553 | targetElementOrCornerstone[addOrRemoveEventListener](
554 | csEventNames[i],
555 | boundMethod
556 | );
557 | }
558 | }
559 |
560 | /**
561 | * Called to validate that events passed into the event listeners prop are valid
562 | *
563 | * @returns {undefined}
564 | */
565 | _validateExternalEventsListeners() {
566 | if (!this.props.eventListeners) return;
567 |
568 | const cornerstoneEvents = Object.values(cornerstone.EVENTS);
569 | const cornerstoneToolsEvents = Object.values(cornerstoneTools.EVENTS);
570 |
571 | for (let i = 0; i < this.props.eventListeners.length; i++) {
572 | const {
573 | target: targetType,
574 | eventName,
575 | handler,
576 | } = this.props.eventListeners[i];
577 | if (
578 | !cornerstoneEvents.includes(eventName) &&
579 | !cornerstoneToolsEvents.includes(eventName)
580 | ) {
581 | console.warn(
582 | `No cornerstone or cornerstone-tools event exists for event name: ${eventName}`
583 | );
584 | continue;
585 | }
586 | }
587 | }
588 | /**
589 | * Handles delegating of events from cornerstone back to the defined
590 | * external events handlers
591 | *
592 | * @param {event}
593 | * @returns {undefined}
594 | */
595 | _handleExternalEventListeners(event) {
596 | if (!this.props.eventListeners) {
597 | return;
598 | }
599 |
600 | for (let i = 0; i < this.props.eventListeners.length; i++) {
601 | const { eventName, handler } = this.props.eventListeners[i];
602 |
603 | if (event.type === eventName) {
604 | handler(event);
605 | }
606 | }
607 | }
608 |
609 | /**
610 | * Convenience handler to pass the "Element Enabled" event back up to the
611 | * parent via a callback. Can be used as an escape hatch for more advanced
612 | * cornerstone fucntionality.
613 | *
614 | * @memberof CornerstoneViewport
615 | * @returns {undefined}
616 | */
617 | _handleOnElementEnabledEvent = (clear = false) => {
618 | const handler = (evt) => {
619 | const elementThatWasEnabled = evt.detail.element;
620 | if (elementThatWasEnabled === this.element) {
621 | // Pass Event
622 | this.props.onElementEnabled(evt);
623 | }
624 | };
625 |
626 | // Start Listening
627 | if (this.props.onElementEnabled && !clear) {
628 | cornerstone.events.addEventListener(
629 | cornerstone.EVENTS.ELEMENT_ENABLED,
630 | handler
631 | );
632 | }
633 |
634 | // Stop Listening
635 | if (clear) {
636 | cornerstone.events.removeEventListener(
637 | cornerstone.EVENTS.ELEMENT_ENABLED,
638 | handler
639 | );
640 | }
641 | };
642 |
643 | /**
644 | * There is a "GLOBAL/DEFAULT" load handler for start/end/error,
645 | * and one that can be defined per element. We use start/end handlers in this
646 | * component to show the "Loading..." indicator if a loading request is taking
647 | * longer than expected.
648 | *
649 | * Because we're using the "per element" handler, we need to call the user's
650 | * handler within our own (if it's set). Load Handlers are not well documented,
651 | * but you can find [their source here]{@link https://github.com/cornerstonejs/cornerstoneTools/blob/master/src/stateManagement/loadHandlerManager.js}
652 | *
653 | * @param {boolean} [clear=false] - true to remove previously set load handlers
654 | * @memberof CornerstoneViewport
655 | * @returns {undefined}
656 | */
657 | _setupLoadHandlers(clear = false) {
658 | if (clear) {
659 | loadHandlerManager.removeHandlers(this.element);
660 | return;
661 | }
662 |
663 | // We use this to "flip" `isLoading` to true, if our startLoading request
664 | // takes longer than our "loadIndicatorDelay"
665 | const startLoadHandler = (element) => {
666 | clearTimeout(this.loadHandlerTimeout);
667 |
668 | // Call user defined loadHandler
669 | if (this.startLoadHandler) {
670 | this.startLoadHandler(element);
671 | }
672 |
673 | // We're taking too long. Indicate that we're "Loading".
674 | this.loadHandlerTimeout = setTimeout(() => {
675 | this.setState({
676 | isLoading: true,
677 | });
678 | }, this.props.loadIndicatorDelay);
679 | };
680 |
681 | const endLoadHandler = (element, image) => {
682 | clearTimeout(this.loadHandlerTimeout);
683 |
684 | // Call user defined loadHandler
685 | if (this.endLoadHandler) {
686 | this.endLoadHandler(element, image);
687 | }
688 |
689 | if (this.state.isLoading) {
690 | this.setState({
691 | isLoading: false,
692 | });
693 | }
694 | };
695 |
696 | loadHandlerManager.setStartLoadHandler(startLoadHandler, this.element);
697 | loadHandlerManager.setEndLoadHandler(endLoadHandler, this.element);
698 | }
699 |
700 | // TODO: May need to throttle?
701 | onImageRendered = (event) => {
702 | const viewport = event.detail.viewport;
703 |
704 | this.setState({
705 | scale: viewport.scale,
706 | windowCenter: viewport.voi.windowCenter,
707 | windowWidth: viewport.voi.windowWidth,
708 | rotationDegrees: viewport.rotation,
709 | isFlippedVertically: viewport.vflip,
710 | isFlippedHorizontally: viewport.hflip,
711 | });
712 | };
713 |
714 | onNewImageHandler = (event, callback) => {
715 | const { imageId } = event.detail.image;
716 | const { sopInstanceUid } =
717 | cornerstone.metaData.get('generalImageModule', imageId) || {};
718 | const currentImageIdIndex = this.props.imageIds.indexOf(imageId);
719 |
720 | // TODO: Should we grab and set some imageId specific metadata here?
721 | // Could prevent cornerstone dependencies in child components.
722 | this.setState({ imageIdIndex: currentImageIdIndex });
723 |
724 | if (callback) {
725 | callback({ currentImageIdIndex, sopInstanceUid });
726 | }
727 | };
728 |
729 | onNewImage = (event) => this.onNewImageHandler(event, this.props.onNewImage);
730 |
731 | onNewImageDebounced = debounce((event) => {
732 | this.onNewImageHandler(event, this.props.onNewImageDebounced);
733 | }, this.props.onNewImageDebounceTime);
734 |
735 | onImageLoaded = () => {
736 | // TODO: This is not necessarily true :thinking:
737 | // We need better cache reporting a layer up
738 | this.numImagesLoaded++;
739 | };
740 |
741 | onImageProgress = (e) => {
742 | this.setState({
743 | imageProgress: e.detail.percentComplete,
744 | });
745 | };
746 |
747 | imageSliderOnInputCallback = (value) => {
748 | this.setViewportActive();
749 |
750 | scrollToIndex(this.element, value);
751 | };
752 |
753 | setViewportActive = () => {
754 | if (this.props.setViewportActive) {
755 | this.props.setViewportActive(); // TODO: should take viewport index/ident?
756 | }
757 | };
758 |
759 | onResize = () => {
760 | cornerstone.resize(this.element);
761 | };
762 |
763 | render() {
764 | const isLoading = this.state.isLoading;
765 | const displayLoadingIndicator = isLoading || this.state.error;
766 | const scrollbarMax = this.props.imageIds.length - 1;
767 | const scrollbarHeight = this.element
768 | ? `${this.element.clientHeight - 20}px`
769 | : '100px';
770 |
771 | return (
772 |
776 | {this.props.enableResizeDetector && this.element != null && (
777 |
786 | )}
787 |
e.preventDefault()}
790 | onMouseDown={(e) => e.preventDefault()}
791 | ref={(input) => {
792 | this.element = input;
793 | }}
794 | >
795 | {displayLoadingIndicator && this.getLoadingIndicator()}
796 | {/* This classname is important in that it tells `cornerstone` to not
797 | * create a new canvas element when we "enable" the `viewport-element`
798 | */}
799 |
800 | {this.getOverlay()}
801 | {this.getOrientationMarkersOverlay()}
802 |
803 |
809 | {this.props.children}
810 |
811 | );
812 | }
813 | }
814 |
815 | /**
816 | *
817 | *
818 | * @param {HTMLElement} element
819 | * @param {string} activeToolName
820 | * @returns
821 | */
822 | function _trySetActiveTool(element, activeToolName) {
823 | if (!element || !activeToolName) {
824 | return;
825 | }
826 |
827 | const validTools = cornerstoneTools.store.state.tools.filter(
828 | (tool) => tool.element === element
829 | );
830 | const validToolNames = validTools.map((tool) => tool.name);
831 |
832 | if (!validToolNames.includes(activeToolName)) {
833 | console.warn(
834 | `Trying to set a tool active that is not "added". Available tools include: ${validToolNames.join(
835 | ', '
836 | )}`
837 | );
838 | }
839 |
840 | cornerstoneTools.setToolActiveForElement(element, activeToolName, {
841 | mouseButtonMask: 1,
842 | });
843 | }
844 |
845 | /**
846 | * Iterate over the provided tools; Add each tool to the target element
847 | *
848 | * @param {string[]|object[]} tools
849 | * @param {HTMLElement} element
850 | */
851 | function _addAndConfigureInitialToolsForElement(tools, element) {
852 | for (let i = 0; i < tools.length; i++) {
853 | const tool =
854 | typeof tools[i] === 'string'
855 | ? { name: tools[i] }
856 | : Object.assign({}, tools[i]);
857 | const toolName = `${tool.name}Tool`; // Top level CornerstoneTools follow this pattern
858 |
859 | tool.toolClass = tool.toolClass || cornerstoneTools[toolName];
860 |
861 | if (!tool.toolClass) {
862 | console.warn(`Unable to add tool with name '${tool.name}'.`);
863 | continue;
864 | }
865 |
866 | cornerstoneTools.addToolForElement(
867 | element,
868 | tool.toolClass,
869 | tool.props || {}
870 | );
871 |
872 | const hasInitialMode =
873 | tool.mode && AVAILABLE_TOOL_MODES.includes(tool.mode);
874 |
875 | if (hasInitialMode) {
876 | // TODO: We may need to check `tool.props` and the tool class's prototype
877 | // to determine the name it registered with cornerstone. `tool.name` is not
878 | // reliable.
879 | const setToolModeFn = TOOL_MODE_FUNCTIONS[tool.mode];
880 | setToolModeFn(element, tool.name, tool.modeOptions || {});
881 | }
882 | }
883 | }
884 |
885 | const AVAILABLE_TOOL_MODES = ['active', 'passive', 'enabled', 'disabled'];
886 |
887 | const TOOL_MODE_FUNCTIONS = {
888 | active: cornerstoneTools.setToolActiveForElement,
889 | passive: cornerstoneTools.setToolPassiveForElement,
890 | enabled: cornerstoneTools.setToolEnabledForElement,
891 | disabled: cornerstoneTools.setToolDisabledForElement,
892 | };
893 |
894 | export default CornerstoneViewport;
895 |
--------------------------------------------------------------------------------
/src/ImageScrollbar/ImageScrollbar.css:
--------------------------------------------------------------------------------
1 | .scroll {
2 | height: 100%;
3 | padding: 5px;
4 | position: absolute;
5 | right: 0;
6 | top: 0;
7 | }
8 | .scroll .scroll-holder {
9 | height: calc(100% - 20px);
10 | margin-top: 5px;
11 | position: relative;
12 | width: 12px;
13 | }
14 | .scroll .scroll-holder .imageSlider {
15 | height: 12px;
16 | left: 12px;
17 | padding: 0;
18 | position: absolute;
19 | top: 0;
20 | transform: rotate(90deg);
21 | transform-origin: top left;
22 | -webkit-appearance: none;
23 | background-color: rgba(0, 0, 0, 0);
24 | }
25 | .scroll .scroll-holder .imageSlider:focus {
26 | outline: none;
27 | }
28 | .scroll .scroll-holder .imageSlider::-moz-focus-outer {
29 | border: none;
30 | }
31 | .scroll .scroll-holder .imageSlider::-webkit-slider-runnable-track {
32 | background-color: rgba(0, 0, 0, 0);
33 | border: none;
34 | cursor: pointer;
35 | height: 5px;
36 | z-index: 6;
37 | }
38 | .scroll .scroll-holder .imageSlider::-moz-range-track {
39 | background-color: rgba(0, 0, 0, 0);
40 | border: none;
41 | cursor: pointer;
42 | height: 2px;
43 | z-index: 6;
44 | }
45 | .scroll .scroll-holder .imageSlider::-ms-track {
46 | animate: 0.2s;
47 | background: transparent;
48 | border: none;
49 | border-width: 15px 0;
50 | color: rgba(0, 0, 0, 0);
51 | cursor: pointer;
52 | height: 12px;
53 | width: 100%;
54 | }
55 | .scroll .scroll-holder .imageSlider::-ms-fill-lower {
56 | background: rgba(0, 0, 0, 0);
57 | }
58 | .scroll .scroll-holder .imageSlider::-ms-fill-upper {
59 | background: rgba(0, 0, 0, 0);
60 | }
61 | .scroll .scroll-holder .imageSlider::-webkit-slider-thumb {
62 | -webkit-appearance: none !important;
63 | background-color: #163239;
64 | border: none;
65 | border-radius: 57px;
66 | cursor: -webkit-grab;
67 | height: 12px;
68 | margin-top: -4px;
69 | width: 39px;
70 | }
71 | .scroll .scroll-holder .imageSlider::-webkit-slider-thumb:active {
72 | background-color: #20a5d6;
73 | cursor: -webkit-grabbing;
74 | }
75 | .scroll .scroll-holder .imageSlider::-moz-range-thumb {
76 | background-color: #163239;
77 | border: none;
78 | border-radius: 57px;
79 | cursor: -moz-grab;
80 | height: 12px;
81 | width: 39px;
82 | z-index: 7;
83 | }
84 | .scroll .scroll-holder .imageSlider::-moz-range-thumb:active {
85 | background-color: #20a5d6;
86 | cursor: -moz-grabbing;
87 | }
88 | .scroll .scroll-holder .imageSlider::-ms-thumb {
89 | background-color: #163239;
90 | border: none;
91 | border-radius: 57px;
92 | cursor: ns-resize;
93 | height: 12px;
94 | width: 39px;
95 | }
96 | .scroll .scroll-holder .imageSlider::-ms-thumb:active {
97 | background-color: #20a5d6;
98 | }
99 | .scroll .scroll-holder .imageSlider::-ms-tooltip {
100 | display: none;
101 | }
102 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
103 | .imageSlider {
104 | left: 50px;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/ImageScrollbar/ImageScrollbar.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import './ImageScrollbar.css';
4 |
5 | class ImageScrollbar extends PureComponent {
6 | static propTypes = {
7 | value: PropTypes.number.isRequired,
8 | max: PropTypes.number.isRequired,
9 | height: PropTypes.string.isRequired,
10 | onInputCallback: PropTypes.func.isRequired,
11 | };
12 |
13 | render() {
14 | if (this.props.max === 0) {
15 | return null;
16 | }
17 |
18 | this.style = {
19 | width: `${this.props.height}`,
20 | };
21 |
22 | return (
23 |
38 | );
39 | }
40 |
41 | onChange = event => {
42 | const intValue = parseInt(event.target.value, 10);
43 | this.props.onInputCallback(intValue);
44 | };
45 |
46 | onKeyDown = event => {
47 | // We don't allow direct keyboard up/down input on the
48 | // image sliders since the natural direction is reversed (0 is at the top)
49 |
50 | // Store the KeyCodes in an object for readability
51 | const keys = {
52 | DOWN: 40,
53 | UP: 38,
54 | };
55 |
56 | // TODO: Enable scroll down / scroll up without depending on ohif-core
57 | if (event.which === keys.DOWN) {
58 | //OHIF.commands.run('scrollDown');
59 | event.preventDefault();
60 | } else if (event.which === keys.UP) {
61 | //OHIF.commands.run('scrollUp');
62 | event.preventDefault();
63 | }
64 | };
65 | }
66 |
67 | export default ImageScrollbar;
68 |
--------------------------------------------------------------------------------
/src/LoadingIndicator/LoadingIndicator.css:
--------------------------------------------------------------------------------
1 | .imageViewerLoadingIndicator {
2 | color: #91b9cd;
3 | }
4 |
5 | .faded {
6 | opacity: 0.5;
7 | }
8 |
9 | .imageViewerErrorLoadingIndicator {
10 | color: #e29e4a;
11 | }
12 |
13 | .imageViewerErrorLoadingIndicator p,
14 | .imageViewerErrorLoadingIndicator h4 {
15 | padding: 4px 0;
16 | text-align: center;
17 | word-wrap: break-word;
18 | }
19 |
20 | .imageViewerErrorLoadingIndicator p {
21 | font-size: 11pt;
22 | }
23 |
24 | .loadingIndicator {
25 | background-color: rgba(0, 0, 0, 0.75);
26 | font-size: 18px;
27 | height: 100%;
28 | overflow: hidden;
29 | pointer-events: none;
30 | position: absolute;
31 | top: 0;
32 | width: 100%;
33 | z-index: 1;
34 | }
35 |
36 | .loadingIndicator .indicatorContents {
37 | font-weight: 300;
38 | position: absolute;
39 | text-align: center;
40 | top: 50%;
41 | transform: translateY(-50%);
42 | width: 100%;
43 | }
44 |
--------------------------------------------------------------------------------
/src/LoadingIndicator/LoadingIndicator.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import './LoadingIndicator.css';
5 |
6 | class LoadingIndicator extends PureComponent {
7 | static propTypes = {
8 | percentComplete: PropTypes.number.isRequired,
9 | error: PropTypes.object,
10 | };
11 |
12 | static defaultProps = {
13 | percentComplete: 0,
14 | error: null,
15 | };
16 |
17 | render() {
18 | const pc = this.props.percentComplete;
19 | const percComplete = `${pc}%`;
20 |
21 | return (
22 |
23 | {this.props.error ? (
24 |
25 |
26 |
Error Loading Image
27 |
An error has occurred.
28 |
{this.props.error.message}
29 |
30 |
31 | ) : (
32 |
33 |
34 |
35 | {pc < 100 ? 'Loading...' : 'Loaded -'}
36 | {' '}
37 |
38 | {pc === 100 &&
Processing...
}
39 |
40 |
41 | )}
42 |
43 | );
44 | }
45 | }
46 |
47 | export default LoadingIndicator;
48 |
--------------------------------------------------------------------------------
/src/ViewportOrientationMarkers/ViewportOrientationMarkers.css:
--------------------------------------------------------------------------------
1 | .ViewportOrientationMarkers {
2 | --marker-width: 100px;
3 | --marker-height: 100px;
4 | --scrollbar-width: 20px;
5 | pointer-events: none;
6 | font-size: 15px;
7 | color: #ccc;
8 | line-height: 18px;
9 | }
10 | .ViewportOrientationMarkers .orientation-marker {
11 | position: absolute;
12 | }
13 | .ViewportOrientationMarkers .top-mid {
14 | top: 5px;
15 | left: 50%;
16 | }
17 | .ViewportOrientationMarkers .left-mid {
18 | top: 47%;
19 | left: 5px;
20 | }
21 | .ViewportOrientationMarkers .right-mid {
22 | top: 47%;
23 | left: calc(100% - var(--marker-width) - var(--scrollbar-width));
24 | }
25 | .ViewportOrientationMarkers .bottom-mid {
26 | top: calc(100% - var(--marker-height) - 5px);
27 | left: 47%;
28 | }
29 | .ViewportOrientationMarkers .right-mid .orientation-marker-value {
30 | display: flex;
31 | justify-content: flex-end;
32 | min-width: var(--marker-width);
33 | }
34 | .ViewportOrientationMarkers .bottom-mid .orientation-marker-value {
35 | display: flex;
36 | justify-content: flex-start;
37 | min-height: var(--marker-height);
38 | flex-direction: column-reverse;
39 | }
40 |
--------------------------------------------------------------------------------
/src/ViewportOrientationMarkers/ViewportOrientationMarkers.js:
--------------------------------------------------------------------------------
1 | import { PureComponent } from 'react';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import cornerstoneTools from 'cornerstone-tools';
5 | import './ViewportOrientationMarkers.css';
6 |
7 | /**
8 | *
9 | * Computes the orientation labels on a Cornerstone-enabled Viewport element
10 | * when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
11 | *
12 | * @param {*} rowCosines
13 | * @param {*} columnCosines
14 | * @param {*} rotationDegrees
15 | * @param {*} isFlippedVertically
16 | * @param {*} isFlippedHorizontally
17 | * @returns
18 | */
19 | function getOrientationMarkers(
20 | rowCosines,
21 | columnCosines,
22 | rotationDegrees,
23 | isFlippedVertically,
24 | isFlippedHorizontally
25 | ) {
26 | const {
27 | getOrientationString,
28 | invertOrientationString,
29 | } = cornerstoneTools.orientation;
30 | const rowString = getOrientationString(rowCosines);
31 | const columnString = getOrientationString(columnCosines);
32 | const oppositeRowString = invertOrientationString(rowString);
33 | const oppositeColumnString = invertOrientationString(columnString);
34 |
35 | const markers = {
36 | top: oppositeColumnString,
37 | left: oppositeRowString,
38 | right: rowString,
39 | bottom: columnString,
40 | };
41 |
42 | // If any vertical or horizontal flips are applied, change the orientation strings ahead of
43 | // the rotation applications
44 | if (isFlippedVertically) {
45 | markers.top = invertOrientationString(markers.top);
46 | markers.bottom = invertOrientationString(markers.bottom);
47 | }
48 |
49 | if (isFlippedHorizontally) {
50 | markers.left = invertOrientationString(markers.left);
51 | markers.right = invertOrientationString(markers.right);
52 | }
53 |
54 | // Swap the labels accordingly if the viewport has been rotated
55 | // This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
56 | if (rotationDegrees === 90 || rotationDegrees === -270) {
57 | return {
58 | top: markers.left,
59 | left: invertOrientationString(markers.top),
60 | right: invertOrientationString(markers.bottom),
61 | bottom: markers.right, // left
62 | };
63 | } else if (rotationDegrees === -90 || rotationDegrees === 270) {
64 | return {
65 | top: invertOrientationString(markers.left),
66 | left: markers.top,
67 | bottom: markers.left,
68 | right: markers.bottom,
69 | };
70 | } else if (rotationDegrees === 180 || rotationDegrees === -180) {
71 | return {
72 | top: invertOrientationString(markers.top),
73 | left: invertOrientationString(markers.left),
74 | bottom: invertOrientationString(markers.bottom),
75 | right: invertOrientationString(markers.right),
76 | };
77 | }
78 |
79 | return markers;
80 | }
81 |
82 | class ViewportOrientationMarkers extends PureComponent {
83 | static propTypes = {
84 | rowCosines: PropTypes.array.isRequired,
85 | columnCosines: PropTypes.array.isRequired,
86 | rotationDegrees: PropTypes.number.isRequired,
87 | isFlippedVertically: PropTypes.bool.isRequired,
88 | isFlippedHorizontally: PropTypes.bool.isRequired,
89 | orientationMarkers: PropTypes.arrayOf(PropTypes.string),
90 | };
91 |
92 | static defaultProps = {
93 | orientationMarkers: ['top', 'left'],
94 | };
95 |
96 | render() {
97 | const {
98 | rowCosines,
99 | columnCosines,
100 | rotationDegrees,
101 | isFlippedVertically,
102 | isFlippedHorizontally,
103 | orientationMarkers,
104 | } = this.props;
105 |
106 | if (!rowCosines || !columnCosines) {
107 | return '';
108 | }
109 |
110 | const markers = getOrientationMarkers(
111 | rowCosines,
112 | columnCosines,
113 | rotationDegrees,
114 | isFlippedVertically,
115 | isFlippedHorizontally
116 | );
117 |
118 | const getMarkers = orientationMarkers =>
119 | orientationMarkers.map((m, index) => (
120 |
126 | ));
127 |
128 | return (
129 |
130 | {getMarkers(orientationMarkers)}
131 |
132 | );
133 | }
134 | }
135 |
136 | export default ViewportOrientationMarkers;
137 |
--------------------------------------------------------------------------------
/src/ViewportOverlay/ViewportOverlay.css:
--------------------------------------------------------------------------------
1 | .imageViewerViewport.empty ~ .ViewportOverlay {
2 | display: none;
3 | }
4 | .ViewportOverlay {
5 | color: #9ccef9;
6 | }
7 | .ViewportOverlay .overlay-element {
8 | position: absolute;
9 | font-weight: 400;
10 | text-shadow: 1px 1px #000;
11 | pointer-events: none;
12 | }
13 | .ViewportOverlay .top-left {
14 | top: 20px;
15 | left: 20px;
16 | }
17 | .ViewportOverlay .top-center {
18 | top: 20px;
19 | padding-top: 20px;
20 | width: 100%;
21 | text-align: center;
22 | }
23 | .ViewportOverlay .top-right {
24 | top: 20px;
25 | right: 20px;
26 | text-align: right;
27 | }
28 | .ViewportOverlay .bottom-left {
29 | bottom: 20px;
30 | left: 20px;
31 | }
32 | .ViewportOverlay .bottom-right {
33 | bottom: 20px;
34 | right: 20px;
35 | text-align: right;
36 | }
37 | .ViewportOverlay.controlsVisible .topright,
38 | .ViewportOverlay.controlsVisible .bottomright {
39 | right: calc(20px + 19px);
40 | }
41 | .ViewportOverlay svg {
42 | color: #9ccef9;
43 | fill: #9ccef9;
44 | stroke: #9ccef9;
45 | background-color: transparent;
46 | margin: 2px;
47 | width: 18px;
48 | height: 18px;
49 | }
50 |
--------------------------------------------------------------------------------
/src/ViewportOverlay/ViewportOverlay.js:
--------------------------------------------------------------------------------
1 | import { PureComponent } from 'react';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import cornerstone from 'cornerstone-core';
5 | import dicomParser from 'dicom-parser';
6 | import { helpers } from '../helpers/index.js';
7 | import './ViewportOverlay.css';
8 |
9 | const {
10 | formatPN,
11 | formatDA,
12 | formatNumberPrecision,
13 | formatTM,
14 | isValidNumber,
15 | } = helpers;
16 |
17 | function getCompression(imageId) {
18 | const generalImageModule =
19 | cornerstone.metaData.get('generalImageModule', imageId) || {};
20 | const {
21 | lossyImageCompression,
22 | lossyImageCompressionRatio,
23 | lossyImageCompressionMethod,
24 | } = generalImageModule;
25 |
26 | if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
27 | const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
28 | const compressionRatio = formatNumberPrecision(
29 | lossyImageCompressionRatio,
30 | 2
31 | );
32 | return compressionMethod + compressionRatio + ' : 1';
33 | }
34 |
35 | return 'Lossless / Uncompressed';
36 | }
37 |
38 | class ViewportOverlay extends PureComponent {
39 | static propTypes = {
40 | scale: PropTypes.number.isRequired,
41 | windowWidth: PropTypes.oneOfType([
42 | PropTypes.number.isRequired,
43 | PropTypes.string.isRequired,
44 | ]),
45 | windowCenter: PropTypes.oneOfType([
46 | PropTypes.number.isRequired,
47 | PropTypes.string.isRequired,
48 | ]),
49 | imageId: PropTypes.string.isRequired,
50 | imageIndex: PropTypes.number.isRequired,
51 | stackSize: PropTypes.number.isRequired,
52 | };
53 |
54 | render() {
55 | const { imageId, scale, windowWidth, windowCenter } = this.props;
56 |
57 | if (!imageId) {
58 | return null;
59 | }
60 |
61 | const zoomPercentage = formatNumberPrecision(scale * 100, 0);
62 | const seriesMetadata =
63 | cornerstone.metaData.get('generalSeriesModule', imageId) || {};
64 | const imagePlaneModule =
65 | cornerstone.metaData.get('imagePlaneModule', imageId) || {};
66 | const { rows, columns, sliceThickness, sliceLocation } = imagePlaneModule;
67 | const { seriesNumber, seriesDescription } = seriesMetadata;
68 |
69 | const generalStudyModule =
70 | cornerstone.metaData.get('generalStudyModule', imageId) || {};
71 | const { studyDate, studyTime, studyDescription } = generalStudyModule;
72 |
73 | const patientModule =
74 | cornerstone.metaData.get('patientModule', imageId) || {};
75 | const { patientId, patientName } = patientModule;
76 |
77 | const generalImageModule =
78 | cornerstone.metaData.get('generalImageModule', imageId) || {};
79 | const { instanceNumber } = generalImageModule;
80 |
81 | const cineModule = cornerstone.metaData.get('cineModule', imageId) || {};
82 | const { frameTime } = cineModule;
83 |
84 | const frameRate = formatNumberPrecision(1000 / frameTime, 1);
85 | const compression = getCompression(imageId);
86 | const wwwc = `W: ${
87 | windowWidth.toFixed ? windowWidth.toFixed(0) : windowWidth
88 | } L: ${windowWidth.toFixed ? windowCenter.toFixed(0) : windowCenter}`;
89 | const imageDimensions = `${columns} x ${rows}`;
90 |
91 | const { imageIndex, stackSize } = this.props;
92 |
93 | const normal = (
94 |
95 |
96 |
{formatPN(patientName)}
97 |
{patientId}
98 |
99 |
100 |
{studyDescription}
101 |
102 | {formatDA(studyDate)} {formatTM(studyTime)}
103 |
104 |
105 |
106 |
Zoom: {zoomPercentage}%
107 |
{wwwc}
108 |
{compression}
109 |
110 |
111 |
{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}
112 |
113 | {stackSize > 1
114 | ? `Img: ${instanceNumber} ${imageIndex}/${stackSize}`
115 | : ''}
116 |
117 |
118 | {frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}
119 |
{imageDimensions}
120 |
121 | {isValidNumber(sliceLocation)
122 | ? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm `
123 | : ''}
124 | {sliceThickness
125 | ? `Thick: ${formatNumberPrecision(sliceThickness, 2)} mm`
126 | : ''}
127 |
128 |
{seriesDescription}
129 |
130 |
131 |
132 | );
133 |
134 | return {normal}
;
135 | }
136 | }
137 |
138 | export default ViewportOverlay;
139 |
--------------------------------------------------------------------------------
/src/helpers/areStringArraysEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Compare equality of two string arrays.
3 | *
4 | * @param {string[]} [arr1] - String array #1
5 | * @param {string[]} [arr2] - String array #2
6 | * @returns {boolean}
7 | */
8 | export default function areStringArraysEqual(arr1, arr2) {
9 | if (arr1 === arr2) return true; // Identity
10 | if (!arr1 || !arr2) return false; // One is undef/null
11 | if (arr1.length !== arr2.length) return false; // Diff length
12 |
13 | for (let i = 0; i < arr1.length; i++) {
14 | if (arr1[i] !== arr2[i]) return false;
15 | }
16 |
17 | return true;
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/formatDA.js:
--------------------------------------------------------------------------------
1 | import { parse, format } from 'date-fns';
2 |
3 | export default function formatDA(date, strFormat = 'MMM d, yyyy') {
4 | if (!date) {
5 | return;
6 | }
7 |
8 | // Goal: 'Apr 5, 1999'
9 | try {
10 | const parsedDateTime = parse(date, 'yyyyMMdd', new Date());
11 | const formattedDateTime = format(parsedDateTime, strFormat);
12 |
13 | return formattedDateTime;
14 | } catch (err) {
15 | // swallow?
16 | }
17 |
18 | return;
19 | }
20 |
--------------------------------------------------------------------------------
/src/helpers/formatNumberPrecision.js:
--------------------------------------------------------------------------------
1 | export default function formatNumberPrecision(number, precision) {
2 | if (number !== null) {
3 | return parseFloat(number).toFixed(precision);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/formatPN.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Formats a patient name for display purposes
3 | */
4 | export default function formatPN(name) {
5 | if (!name) {
6 | return;
7 | }
8 |
9 | // Convert the first ^ to a ', '. String.replace() only affects
10 | // the first appearance of the character.
11 | const commaBetweenFirstAndLast = name.replace('^', ', ');
12 |
13 | // Replace any remaining '^' characters with spaces
14 | const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
15 |
16 | // Trim any extraneous whitespace
17 | return cleaned.trim();
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/formatTM.js:
--------------------------------------------------------------------------------
1 | import { parse, format } from 'date-fns';
2 |
3 | export default function formatTM(time, strFormat = 'HH:mm:ss') {
4 | if (!time) {
5 | return;
6 | }
7 |
8 | // DICOM Time is stored as HHmmss.SSS, where:
9 | // HH 24 hour time:
10 | // m mm 0..59 Minutes
11 | // s ss 0..59 Seconds
12 | // S SS SSS 0..999 Fractional seconds
13 | //
14 | // Goal: '24:12:12'
15 | try {
16 | const inputFormat = 'HHmmss.SSS';
17 | const strTime = time.toString().substring(0, inputFormat.length);
18 | const parsedDateTime = parse(strTime, inputFormat.substring(0,strTime.length), new Date(0));
19 | const formattedDateTime = format(parsedDateTime, strFormat);
20 |
21 | return formattedDateTime;
22 | } catch (err) {
23 | // swallow?
24 | }
25 |
26 | return;
27 | }
28 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | import formatPN from './formatPN';
2 | import formatDA from './formatDA';
3 | import formatTM from './formatTM';
4 | import formatNumberPrecision from './formatNumberPrecision';
5 | import isValidNumber from './isValidNumber';
6 |
7 | const helpers = {
8 | formatPN,
9 | formatDA,
10 | formatTM,
11 | formatNumberPrecision,
12 | isValidNumber,
13 | };
14 |
15 | export { helpers };
16 |
--------------------------------------------------------------------------------
/src/helpers/isValidNumber.js:
--------------------------------------------------------------------------------
1 | export default function isValidNumber(value) {
2 | return typeof value === 'number' && !isNaN(value);
3 | }
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './metadataProvider.js';
2 | import CornerstoneViewport from './CornerstoneViewport/CornerstoneViewport.js';
3 | import ViewportOverlay from './ViewportOverlay/ViewportOverlay';
4 |
5 | export { ViewportOverlay };
6 |
7 | export default CornerstoneViewport;
8 |
--------------------------------------------------------------------------------
/src/metadataProvider.js:
--------------------------------------------------------------------------------
1 | import cornerstone from 'cornerstone-core';
2 | import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
3 | import dicomParser from 'dicom-parser';
4 |
5 | const { getNumberValue, getValue } = cornerstoneWADOImageLoader.wadors.metaData;
6 |
7 | function wadoRsMetaDataProvider(type, imageId) {
8 | const metaData = cornerstoneWADOImageLoader.wadors.metaDataManager.get(
9 | imageId
10 | );
11 |
12 | if (!metaData) {
13 | return;
14 | }
15 |
16 | if (
17 | metaData[type] !== undefined &&
18 | metaData[type].Value !== undefined &&
19 | metaData[type].Value.length
20 | ) {
21 | return metaData[type].Value[0];
22 | }
23 |
24 | const typeCleaned = type.replace('x', '');
25 | if (
26 | metaData[typeCleaned] !== undefined &&
27 | metaData[typeCleaned].Value !== undefined &&
28 | metaData[typeCleaned].Value.length
29 | ) {
30 | return metaData[typeCleaned].Value[0];
31 | }
32 |
33 | if (type === 'generalImageModule') {
34 | return {
35 | sopInstanceUid: getValue(metaData['00080018']),
36 | instanceNumber: getNumberValue(metaData['00200013']),
37 | lossyImageCompression: getValue(metaData['00282110']),
38 | lossyImageCompressionRatio: getValue(metaData['00282112']),
39 | lossyImageCompressionMethod: getValue(metaData['00282114']),
40 | };
41 | }
42 |
43 | if (type === 'patientModule') {
44 | return {
45 | patientName: getValue(metaData['00100010']),
46 | patientId: getValue(metaData['00100020']),
47 | patientSex: getValue(metaData['00100040']),
48 | patientBirthDate: getValue(metaData['00100030']),
49 | };
50 | }
51 |
52 | if (type === 'spacingBetweenSlices') {
53 | return getValue(metaData['00180088']);
54 | }
55 |
56 | if (type === 'generalStudyModule') {
57 | return {
58 | studyDescription: getValue(metaData['00081030']),
59 | studyDate: getValue(metaData['00080020']),
60 | studyTime: getValue(metaData['00080030']),
61 | accessionNumber: getValue(metaData['00080050']),
62 | };
63 | }
64 |
65 | if (type === 'cineModule') {
66 | return {
67 | frameTime: getNumberValue(metaData['00181063']),
68 | };
69 | }
70 | }
71 |
72 | cornerstone.metaData.addProvider(wadoRsMetaDataProvider);
73 |
74 | function wadoUriMetaDataProvider(type, imageId) {
75 | const {
76 | parseImageId,
77 | dataSetCacheManager,
78 | } = cornerstoneWADOImageLoader.wadouri;
79 | const parsedImageId = parseImageId(imageId);
80 | const dataSet = dataSetCacheManager.get(parsedImageId.url);
81 |
82 | if (!dataSet) {
83 | return;
84 | }
85 |
86 | if (type === 'generalImageModule') {
87 | return {
88 | sopInstanceUid: dataSet.string('x00080018'),
89 | instanceNumber: dataSet.intString('x00200013'),
90 | lossyImageCompression: dataSet.string('x00282110'),
91 | lossyImageCompressionRatio: dataSet.string('x00282112'),
92 | lossyImageCompressionMethod: dataSet.string('x00282114'),
93 | };
94 | }
95 |
96 | if (type === 'patientModule') {
97 | return {
98 | patientName: dataSet.string('x00100010'),
99 | patientId: dataSet.string('x00100020'),
100 | };
101 | }
102 |
103 | if (type === 'generalStudyModule') {
104 | return {
105 | studyDescription: dataSet.string('x00081030'),
106 | studyDate: dataSet.string('x00080020'),
107 | studyTime: dataSet.string('x00080030'),
108 | };
109 | }
110 |
111 | if (type === 'cineModule') {
112 | return {
113 | frameTime: dataSet.floatString('x00181063'),
114 | };
115 | }
116 |
117 | if (dataSet.elements[type] !== undefined) {
118 | const element = dataSet.elements[type];
119 | if (!element.vr) {
120 | return;
121 | }
122 |
123 | return dicomParser.explicitElementToString(dataSet, element);
124 | }
125 | }
126 |
127 | cornerstone.metaData.addProvider(wadoUriMetaDataProvider);
128 |
--------------------------------------------------------------------------------
/src/test.js:
--------------------------------------------------------------------------------
1 | import CornerstoneViewport from './CornerstoneViewport/CornerstoneViewport.js';
2 |
3 | describe('CornerstoneViewport', () => {
4 | it('is truthy', () => {
5 | expect(CornerstoneViewport).toBeTruthy();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------