diff --git a/extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js b/extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js
new file mode 100644
index 000000000..6aea859ca
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js
@@ -0,0 +1,12 @@
+const path = require('path');
+const webpackCommon = require('./../../../.webpack/webpack.base.js');
+const SRC_DIR = path.join(__dirname, '../src');
+const DIST_DIR = path.join(__dirname, '../dist');
+
+const ENTRY = {
+ app: `${SRC_DIR}/index.tsx`,
+};
+
+module.exports = (env, argv) => {
+ return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
+};
diff --git a/extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js b/extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js
new file mode 100644
index 000000000..0ae7d6c2d
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js
@@ -0,0 +1,54 @@
+const webpack = require('webpack');
+const { merge } = require('webpack-merge');
+const path = require('path');
+const webpackCommon = require('./../../../.webpack/webpack.base.js');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+
+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');
+const ENTRY = {
+ app: `${SRC_DIR}/index.tsx`,
+};
+
+const outputName = `ohif-${pkg.name.split('/').pop()}`;
+
+module.exports = (env, argv) => {
+ const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
+
+ return merge(commonConfig, {
+ 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: 'ohif-extension-cornerstone-dicom-pmap',
+ libraryTarget: 'umd',
+ filename: pkg.main,
+ },
+ externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
+ plugins: [
+ new webpack.optimize.LimitChunkCountPlugin({
+ maxChunks: 1,
+ }),
+ new MiniCssExtractPlugin({
+ filename: `./dist/${outputName}.css`,
+ chunkFilename: `./dist/${outputName}.css`,
+ }),
+ ],
+ });
+};
diff --git a/extensions/cornerstone-dicom-pmap/CHANGELOG.md b/extensions/cornerstone-dicom-pmap/CHANGELOG.md
new file mode 100644
index 000000000..e4d87c4d4
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
diff --git a/extensions/cornerstone-dicom-pmap/LICENSE b/extensions/cornerstone-dicom-pmap/LICENSE
new file mode 100644
index 000000000..983c5ef34
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/LICENSE
@@ -0,0 +1,20 @@
+MIT License
+
+Copyright (c) 2023 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/cornerstone-dicom-pmap/README.md b/extensions/cornerstone-dicom-pmap/README.md
new file mode 100644
index 000000000..af903360c
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/README.md
@@ -0,0 +1,12 @@
+# dicom-pmap
+## Description
+
+DICOM PMAP read workflow. This extension will allow you to load a DICOM Parametric
+Map image and display it on OHIF.
+
+## Author
+
+OHIF
+
+## License
+MIT
diff --git a/extensions/cornerstone-dicom-pmap/babel.config.js b/extensions/cornerstone-dicom-pmap/babel.config.js
new file mode 100644
index 000000000..e51442551
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/babel.config.js
@@ -0,0 +1,44 @@
+module.exports = {
+ plugins: ['@babel/plugin-proposal-class-properties'],
+ env: {
+ test: {
+ presets: [
+ [
+ // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
+ '@babel/preset-env',
+ {
+ modules: 'commonjs',
+ debug: false,
+ },
+ '@babel/preset-typescript',
+ ],
+ '@babel/preset-react',
+ ],
+ plugins: [
+ '@babel/plugin-proposal-object-rest-spread',
+ '@babel/plugin-syntax-dynamic-import',
+ '@babel/plugin-transform-regenerator',
+ '@babel/plugin-transform-runtime',
+ ],
+ },
+ production: {
+ presets: [
+ // WebPack handles ES6 --> Target Syntax
+ ['@babel/preset-env', { modules: false }],
+ '@babel/preset-react',
+ '@babel/preset-typescript',
+ ],
+ ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
+ },
+ development: {
+ presets: [
+ // WebPack handles ES6 --> Target Syntax
+ ['@babel/preset-env', { modules: false }],
+ '@babel/preset-react',
+ '@babel/preset-typescript',
+ ],
+ plugins: ['react-hot-loader/babel'],
+ ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
+ },
+ },
+};
diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json
new file mode 100644
index 000000000..427cb7874
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@ohif/extension-cornerstone-dicom-pmap",
+ "version": "3.9.0-beta.64",
+ "description": "DICOM Parametric Map read workflow",
+ "author": "OHIF",
+ "license": "MIT",
+ "main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
+ "module": "src/index.tsx",
+ "files": [
+ "dist/**",
+ "public/**",
+ "README.md"
+ ],
+ "repository": "OHIF/Viewers",
+ "keywords": [
+ "ohif-extension"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1.18.0"
+ },
+ "scripts": {
+ "clean": "shx rm -rf dist",
+ "clean:deep": "yarn run clean && shx rm -rf node_modules",
+ "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
+ "dev:dicom-pmap": "yarn run dev",
+ "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
+ "build:package-1": "yarn run build",
+ "start": "yarn run dev"
+ },
+ "peerDependencies": {
+ "@ohif/core": "3.9.0-beta.64",
+ "@ohif/extension-cornerstone": "3.9.0-beta.64",
+ "@ohif/extension-default": "3.9.0-beta.64",
+ "@ohif/i18n": "3.9.0-beta.64",
+ "prop-types": "^15.6.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-i18next": "^12.2.2",
+ "react-router": "^6.8.1",
+ "react-router-dom": "^6.8.1"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@cornerstonejs/adapters": "^1.77.6",
+ "@cornerstonejs/core": "^1.77.6",
+ "@kitware/vtk.js": "30.4.1",
+ "react-color": "^2.19.3"
+ }
+}
diff --git a/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts
new file mode 100644
index 000000000..6f446b0d0
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts
@@ -0,0 +1,217 @@
+import { utils } from '@ohif/core';
+import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
+import { adaptersPMAP } from '@cornerstonejs/adapters';
+import { SOPClassHandlerId } from './id';
+import { dicomLoaderService } from '@ohif/extension-cornerstone';
+
+const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
+const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
+
+function _getDisplaySetsFromSeries(
+ instances,
+ servicesManager: AppTypes.ServicesManager,
+ extensionManager
+) {
+ const instance = instances[0];
+
+ const {
+ StudyInstanceUID,
+ SeriesInstanceUID,
+ SOPInstanceUID,
+ SeriesDescription,
+ SeriesNumber,
+ SeriesDate,
+ SOPClassUID,
+ wadoRoot,
+ wadoUri,
+ wadoUriRoot,
+ } = instance;
+
+ const displaySet = {
+ // Parametric map use to have the same modality as its referenced volume but
+ // "PMAP" is used in the viewer even though this is not a valid DICOM modality
+ Modality: 'PMAP',
+ isReconstructable: true, // by default for now
+ displaySetInstanceUID: `pmap.${utils.guid()}`,
+ SeriesDescription,
+ SeriesNumber,
+ SeriesDate,
+ SOPInstanceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
+ SOPClassHandlerId,
+ SOPClassUID,
+ referencedImages: null,
+ referencedSeriesInstanceUID: null,
+ referencedDisplaySetInstanceUID: null,
+ referencedVolumeURI: null,
+ referencedVolumeId: null,
+ isDerivedDisplaySet: true,
+ loadStatus: {
+ loading: false,
+ loaded: false,
+ },
+ sopClassUids,
+ instance,
+ instances: [instance],
+ wadoRoot,
+ wadoUriRoot,
+ wadoUri,
+ isOverlayDisplaySet: true,
+ };
+
+ const referencedSeriesSequence = instance.ReferencedSeriesSequence;
+
+ if (!referencedSeriesSequence) {
+ console.error('ReferencedSeriesSequence is missing for the parametric map');
+ return;
+ }
+
+ const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
+
+ displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
+ displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
+
+ // Does not get the referenced displaySet during parametric displaySet creation
+ // because it is still not available (getDisplaySetByUID returns `undefined`).
+ displaySet.getReferenceDisplaySet = () => {
+ const { displaySetService } = servicesManager.services;
+
+ if (displaySet.referencedDisplaySetInstanceUID) {
+ return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID);
+ }
+
+ const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
+ displaySet.referencedSeriesInstanceUID
+ );
+
+ if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
+ throw new Error('Referenced displaySet is missing for the parametric map');
+ }
+
+ const referencedDisplaySet = referencedDisplaySets[0];
+
+ displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
+
+ return referencedDisplaySet;
+ };
+
+ // Does not get the referenced volumeId during parametric displaySet creation because the
+ // referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`).
+ displaySet.getReferencedVolumeId = () => {
+ if (displaySet.referencedVolumeId) {
+ return displaySet.referencedVolumeId;
+ }
+
+ const referencedDisplaySet = displaySet.getReferenceDisplaySet();
+ const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
+ const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`;
+
+ displaySet.referencedVolumeURI = referencedVolumeURI;
+ displaySet.referencedVolumeId = referencedVolumeId;
+
+ return referencedVolumeId;
+ };
+
+ displaySet.load = async ({ headers }) =>
+ await _load(displaySet, servicesManager, extensionManager, headers);
+
+ return [displaySet];
+}
+
+async function _load(
+ displaySet,
+ servicesManager: AppTypes.ServicesManager,
+ extensionManager,
+ headers
+) {
+ const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`;
+ const volumeLoadObject = cache.getVolumeLoadObject(volumeId);
+
+ if (volumeLoadObject) {
+ return volumeLoadObject.promise;
+ }
+
+ displaySet.loadStatus.loading = true;
+
+ // We don't want to fire multiple loads, so we'll wait for the first to finish
+ // and also return the same promise to any other callers.
+ // loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
+ const promise = _loadParametricMap({
+ extensionManager,
+ displaySet,
+ headers,
+ });
+
+ cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
+ throw err;
+ });
+
+ promise
+ .then(() => {
+ displaySet.loadStatus.loading = false;
+ displaySet.loadStatus.loaded = true;
+ })
+ .catch(err => {
+ displaySet.loadStatus.loading = false;
+ throw err;
+ });
+
+ return promise;
+}
+
+async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
+ const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
+ const referencedVolumeId = displaySet.getReferencedVolumeId();
+ const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
+
+ // Parametric map can be loaded only if its referenced volume exists otherwise it will fail
+ if (!cachedReferencedVolume) {
+ throw new Error(
+ 'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet'
+ );
+ }
+
+ const { imageIds } = cachedReferencedVolume;
+ const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState(
+ imageIds,
+ arrayBuffer,
+ metaData
+ );
+ const { pixelData } = results;
+ const TypedArrayConstructor = pixelData.constructor;
+ const paramMapId = displaySet.displaySetInstanceUID;
+
+ const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, {
+ volumeId: paramMapId,
+ targetBuffer: {
+ type: TypedArrayConstructor.name,
+ },
+ });
+
+ derivedVolume.getScalarData().set(pixelData);
+
+ const range = derivedVolume.imageData.getPointData().getScalars().getRange();
+ const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
+
+ derivedVolume.metadata.voiLut = [windowLevel];
+ derivedVolume.loadStatus = { loaded: true };
+
+ return derivedVolume;
+}
+
+function getSopClassHandlerModule({ servicesManager, extensionManager }) {
+ const getDisplaySetsFromSeries = instances => {
+ return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
+ };
+
+ return [
+ {
+ name: 'dicom-pmap',
+ sopClassUids,
+ getDisplaySetsFromSeries,
+ },
+ ];
+}
+
+export default getSopClassHandlerModule;
diff --git a/extensions/cornerstone-dicom-pmap/src/id.js b/extensions/cornerstone-dicom-pmap/src/id.js
new file mode 100644
index 000000000..4ec5d5996
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/src/id.js
@@ -0,0 +1,7 @@
+import packageJson from '../package.json';
+
+const id = packageJson.name;
+const SOPClassHandlerName = 'dicom-pmap';
+const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
+
+export { id, SOPClassHandlerId, SOPClassHandlerName };
diff --git a/extensions/cornerstone-dicom-pmap/src/index.tsx b/extensions/cornerstone-dicom-pmap/src/index.tsx
new file mode 100644
index 000000000..916f8a07f
--- /dev/null
+++ b/extensions/cornerstone-dicom-pmap/src/index.tsx
@@ -0,0 +1,39 @@
+import { id } from './id';
+import React from 'react';
+import getSopClassHandlerModule from './getSopClassHandlerModule';
+
+const Component = React.lazy(() => {
+ return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport');
+});
+
+const OHIFCornerstonePMAPViewport = props => {
+ return (
+