diff --git a/extensions/dicom-sr/.webpack/webpack.dev.js b/extensions/dicom-sr/.webpack/webpack.dev.js
new file mode 100644
index 000000000..1ae308448
--- /dev/null
+++ b/extensions/dicom-sr/.webpack/webpack.dev.js
@@ -0,0 +1,8 @@
+const path = require('path');
+const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
+const SRC_DIR = path.join(__dirname, '../src');
+const DIST_DIR = path.join(__dirname, '../dist');
+
+module.exports = (env, argv) => {
+ return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
+};
diff --git a/extensions/dicom-sr/.webpack/webpack.prod.js b/extensions/dicom-sr/.webpack/webpack.prod.js
new file mode 100644
index 000000000..ca612cd37
--- /dev/null
+++ b/extensions/dicom-sr/.webpack/webpack.prod.js
@@ -0,0 +1,44 @@
+const webpack = require('webpack');
+const merge = require('webpack-merge');
+const path = require('path');
+const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
+const pkg = require('./../package.json');
+
+const ROOT_DIR = path.join(__dirname, './..');
+const SRC_DIR = path.join(__dirname, '../src');
+const DIST_DIR = path.join(__dirname, '../dist');
+
+module.exports = (env, argv) => {
+ const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
+
+ return merge(commonConfig, {
+ devtool: 'source-map',
+ stats: {
+ colors: true,
+ hash: true,
+ timings: true,
+ assets: true,
+ chunks: false,
+ chunkModules: false,
+ modules: false,
+ children: false,
+ warnings: true,
+ },
+ optimization: {
+ minimize: true,
+ sideEffects: true,
+ },
+ output: {
+ path: ROOT_DIR,
+ library: 'OHIFExtCornerstone',
+ libraryTarget: 'umd',
+ libraryExport: 'default',
+ filename: pkg.main,
+ },
+ plugins: [
+ new webpack.optimize.LimitChunkCountPlugin({
+ maxChunks: 1,
+ }),
+ ],
+ });
+};
diff --git a/extensions/dicom-sr/LICENSE b/extensions/dicom-sr/LICENSE
new file mode 100644
index 000000000..19e20dd35
--- /dev/null
+++ b/extensions/dicom-sr/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Open Health Imaging Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/extensions/dicom-sr/README.md b/extensions/dicom-sr/README.md
new file mode 100644
index 000000000..9c6e86015
--- /dev/null
+++ b/extensions/dicom-sr/README.md
@@ -0,0 +1,121 @@
+# @ohif/extension-cornerstone
+
+
+
+This extension adds support for viewing and manipulating 2D medical images via a
+viewport. The underlying implementation wraps the
+`cornerstonejs/react-cornerstone-viewport`, and provides basic commands and
+toolbar buttons for common actions.
+
+
+
+#### Index
+
+Extension Id: `cornerstone`
+
+- [Commands Module](#commands-module)
+- [Toolbar Module](#toolbar-module)
+- [Viewport Module](#viewport-module)
+
+## Commands Module
+
+This extensions includes the following `Commands` and `Command Definitions`.
+These can be registered with `@ohif/core`'s `CommandManager`. After registering
+the commands, they can be bound to `hotkeys` using the `HotkeysManager` and
+listed in the `UserPreferences` modal.
+
+You can read more about [`Commands`][docs-commands], [`Hotkeys`][docs-hotkeys],
+and the [`UserPreferences` Modal][docs-userprefs] in their respective locations
+in the OHIF Viewer's documentation.
+
+| Command Name | Description | Store Contexts |
+| ---------------------------- | --------------------------------------- | -------------- |
+| `rotateViewportCW` | | viewports |
+| `rotateViewportCCW` | | viewports |
+| `invertViewport` | | viewports |
+| `flipViewportVertical` | | viewports |
+| `flipViewportHorizontal` | | viewports |
+| `scaleUpViewport` | | viewports |
+| `scaleDownViewport` | | viewports |
+| `fitViewportToWindow` | | viewports |
+| `resetViewport` | | viewports |
+| clearAnnotations | TODO | |
+| next/previous Image | TODO | |
+| first/last Image | TODO | |
+| `nextViewportDisplaySet` | | |
+| `previousViewportDisplaySet` | | |
+| `setToolActive` | Activates tool for primary button/touch | |
+
+## Toolbar Module
+
+Our toolbar module contains definitions for:
+
+- `StackScroll`
+- `Zoom`
+- `Wwwc`
+- `Pan`
+- `Length`
+- `Angle`
+- `Reset`
+- `Cine`
+
+All use the `ACTIVE_VIEWPORT::CORNERSTONE` context.
+
+## Viewport Module
+
+Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
+and is connected the redux store. This module is the most prone to change as we
+hammer out our Viewport interface.
+
+## Tool Configuration
+
+Tools can be configured through extension configuration using the tools key:
+
+```js
+ ...
+ cornerstoneExtensionConfig: {
+ tools: {
+ ArrowAnnotate: {
+ configuration: {
+ getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
+ },
+ },
+ },
+ },
+ ...
+```
+
+## Annotate Tools Configuration
+
+*We currently support one property for annotation tools.*
+
+### Hide handles
+This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
+
+```js
+ ...
+ cornerstoneExtensionConfig: {
+ hideHandles: true,
+ },
+ ...
+
+## Resources
+
+### Repositories
+
+- [cornerstonejs/react-cornerstone-viewport][react-viewport]
+- [cornerstonejs/cornerstoneTools][cornerstone-tools]
+- [cornerstonejs/cornerstone][cornerstone]
+
+
+
+
+[docs-commands]: https://www.com
+[docs-hotkeys]: https://www.com
+[docs-userprefs]: htt
+[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport
+[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
+[cornerstone]: https://github.com/cornerstonejs/cornerstone
+
diff --git a/extensions/dicom-sr/babel.config.js b/extensions/dicom-sr/babel.config.js
new file mode 100644
index 000000000..325ca2a8e
--- /dev/null
+++ b/extensions/dicom-sr/babel.config.js
@@ -0,0 +1 @@
+module.exports = require('../../babel.config.js');
diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json
new file mode 100644
index 000000000..6b14984c9
--- /dev/null
+++ b/extensions/dicom-sr/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@ohif/extension-dicom-sr",
+ "version": "0.0.1",
+ "description": "OHIF extension for an SR Cornerstone Viewport",
+ "author": "OHIF",
+ "license": "MIT",
+ "repository": "OHIF/Viewers",
+ "main": "dist/index.umd.js",
+ "module": "src/index.js",
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6",
+ "yarn": ">=1.16.0"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
+ "dev:cornerstone": "yarn run dev",
+ "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
+ "build:package": "yarn run build",
+ "start": "yarn run dev",
+ "test:unit": "jest --watchAll",
+ "test:unit:ci": "jest --ci --runInBand --collectCoverage"
+ },
+ "peerDependencies": {
+ "@ohif/core": "^0.50.0",
+ "@ohif/ui": "^0.50.0",
+ "cornerstone-core": "^2.3.0",
+ "cornerstone-math": "^0.1.8",
+ "cornerstone-tools": "4.15.1",
+ "dcmjs": "^0.12.3",
+ "cornerstone-wado-image-loader": "^3.1.2",
+ "dicom-parser": "^1.8.3",
+ "hammerjs": "^2.0.8",
+ "prop-types": "^15.6.2",
+ "react": "^16.11.0",
+ "react-dom": "^16.11.0",
+ "react-redux": "^6.0.0",
+ "react-resize-detector": "^3.4.0",
+ "redux": "^4.0.1"
+ },
+ "dependencies": {
+ "@babel/runtime": "7.7.6",
+ "classnames": "^2.2.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.throttle": "^4.1.1",
+ "react-cornerstone-viewport": "2.3.8"
+ }
+}
diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js
new file mode 100644
index 000000000..ad1cf72f9
--- /dev/null
+++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js
@@ -0,0 +1,232 @@
+import React, { Component } from 'react';
+import CornerstoneViewport from 'react-cornerstone-viewport';
+//import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
+import OHIF from '@ohif/core';
+import PropTypes from 'prop-types';
+import cornerstone from 'cornerstone-core';
+import debounce from 'lodash.debounce';
+import throttle from 'lodash.throttle';
+
+// const {
+// onAdded,
+// onRemoved,
+// onModified,
+// } = OHIF.measurements.MeasurementHandlers;
+
+// // TODO: Transition to enums for the action names so that we can ensure they stay up to date
+// // everywhere they're used.
+// const MEASUREMENT_ACTION_MAP = {
+// added: onAdded,
+// removed: onRemoved,
+// modified: throttle(event => {
+// return onModified(event);
+// }, 300),
+// };
+
+// const cine = viewportSpecificData.cine;
+
+// isPlaying = cine.isPlaying === true;
+// frameRate = cine.cineFrameRate || frameRate;
+
+const { StackManager } = OHIF.utils;
+
+class OHIFCornerstoneViewport extends Component {
+ state = {
+ viewportData: null,
+ };
+
+ static defaultProps = {
+ customProps: {},
+ };
+
+ static propTypes = {
+ displaySet: PropTypes.object,
+ viewportIndex: PropTypes.number,
+ dataSource: PropTypes.object,
+ children: PropTypes.node,
+ customProps: PropTypes.object,
+ };
+
+ static name = 'OHIFCornerstoneViewport';
+
+ static init() {
+ console.log('OHIFCornerstoneViewport init()');
+ }
+
+ static destroy() {
+ console.log('OHIFCornerstoneViewport destroy()');
+ StackManager.clearStacks();
+ }
+
+ /**
+ * Obtain the CornerstoneTools Stack for the specified display set.
+ *
+ * @param {Object} displaySet
+ * @param {Object} dataSource
+ * @return {Object} CornerstoneTools Stack
+ */
+ static getCornerstoneStack(displaySet, dataSource) {
+ const { frameIndex } = displaySet;
+
+ // Get stack from Stack Manager
+ const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
+
+ // Clone the stack here so we don't mutate it
+ const stack = Object.assign({}, storedStack);
+
+ stack.currentImageIdIndex = frameIndex;
+
+ // TODO -> Do we ever use this like this?
+ // if (SOPInstanceUID) {
+ // const index = stack.imageIds.findIndex(imageId => {
+ // const imageIdSOPInstanceUID = cornerstone.metaData.get(
+ // 'SOPInstanceUID',
+ // imageId
+ // );
+
+ // return imageIdSOPInstanceUID === SOPInstanceUID;
+ // });
+
+ // if (index > -1) {
+ // stack.currentImageIdIndex = index;
+ // } else {
+ // console.warn(
+ // 'SOPInstanceUID provided was not found in specified DisplaySet'
+ // );
+ // }
+ // }
+
+ return stack;
+ }
+
+ getViewportData = async displaySet => {
+ let viewportData;
+
+ const { dataSource } = this.props;
+
+ const stack = OHIFCornerstoneViewport.getCornerstoneStack(
+ displaySet,
+ dataSource
+ );
+
+ viewportData = {
+ StudyInstanceUID: displaySet.StudyInstanceUID,
+ displaySetInstanceUID: displaySet.displaySetInstanceUID,
+ stack,
+ };
+
+ return viewportData;
+ };
+
+ setStateFromProps() {
+ const { displaySet } = this.props;
+ const {
+ StudyInstanceUID,
+ displaySetInstanceUID,
+ sopClassUids,
+ } = displaySet;
+
+ if (!StudyInstanceUID || !displaySetInstanceUID) {
+ return;
+ }
+
+ if (sopClassUids && sopClassUids.length > 1) {
+ console.warn(
+ 'More than one SOPClassUID in the same series is not yet supported.'
+ );
+ }
+
+ this.getViewportData(displaySet).then(viewportData => {
+ this.setState({
+ viewportData,
+ });
+ });
+ }
+
+ componentDidMount() {
+ this.setStateFromProps();
+ }
+
+ componentDidUpdate(prevProps) {
+ const { displaySet } = this.props;
+ const prevDisplaySet = prevProps.displaySet;
+
+ if (
+ displaySet.displaySetInstanceUID !==
+ prevDisplaySet.displaySetInstanceUID ||
+ displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
+ displaySet.frameIndex !== prevDisplaySet.frameIndex
+ ) {
+ this.setStateFromProps();
+ }
+ }
+
+ render() {
+ let childrenWithProps = null;
+
+ if (!this.state.viewportData) {
+ return null;
+ }
+ const { viewportIndex } = this.props;
+ const {
+ imageIds,
+ currentImageIdIndex,
+ // If this comes from the instance, would be a better default
+ // `FrameTime` in the instance
+ // frameRate = 0,
+ } = this.state.viewportData.stack;
+
+ // TODO: Does it make more sense to use Context?
+ if (this.props.children && this.props.children.length) {
+ childrenWithProps = this.props.children.map((child, index) => {
+ return (
+ child &&
+ React.cloneElement(child, {
+ viewportIndex: this.props.viewportIndex,
+ key: index,
+ })
+ );
+ });
+ }
+
+ const debouncedNewImageHandler = debounce(
+ ({ currentImageIdIndex, sopInstanceUid }) => {
+ const { displaySet } = this.props;
+ const { StudyInstanceUID } = displaySet;
+ if (currentImageIdIndex > 0) {
+ this.props.onNewImage({
+ StudyInstanceUID,
+ SOPInstanceUID: sopInstanceUid,
+ frameIndex: currentImageIdIndex,
+ activeViewportIndex: viewportIndex,
+ });
+ }
+ },
+ 700
+ );
+
+ // TODO -> We may still want a wrapped component to define all the measurement api stuff.
+
+ return (
+ <>
+