From e0f42d4f1d9a28b6c6f7fa8546d1086dae9a1630 Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 6 Jul 2026 09:35:26 -0400 Subject: [PATCH] fix(dicom-pdf): enhance PDF loading with authentication support - move authenticated rendered media loading into the datasource\n- route DICOM video display sets through Cornerstone video viewports\n- add a 3.12 to 3.13 migration note for the removed DICOM-video viewport namespace --- .../default/src/DicomWebDataSource/index.ts | 10 +- .../retrieveRendered.test.ts | 147 ++++++++++++++++++ .../DicomWebDataSource/retrieveRendered.ts | 141 +++++++++++++++++ .../src/DicomWebProxyDataSource/index.ts | 4 +- .../default/src/MergeDataSource/index.ts | 18 +++ .../dicom-pdf/src/getSopClassHandlerModule.js | 10 +- extensions/dicom-pdf/src/index.tsx | 10 +- .../viewports/OHIFCornerstonePdfViewport.tsx | 33 +++- .../src/getSopClassHandlerModule.js | 10 +- extensions/dicom-video/src/index.tsx | 44 ------ .../OHIFCornerstoneVideoViewport.tsx | 55 ------- modes/basic-dev-mode/src/index.ts | 7 +- modes/basic-test-mode/src/index.ts | 5 - modes/basic/src/index.tsx | 1 - modes/microscopy/src/index.tsx | 3 +- modes/usAnnotation/src/index.ts | 1 - .../3p12-to-3p13/dicom-video.md | 74 +++++++++ .../migration-guide/3p12-to-3p13/index.md | 3 + 18 files changed, 446 insertions(+), 130 deletions(-) create mode 100644 extensions/default/src/DicomWebDataSource/retrieveRendered.test.ts create mode 100644 extensions/default/src/DicomWebDataSource/retrieveRendered.ts delete mode 100644 extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx create mode 100644 platform/docs/docs/migration-guide/3p12-to-3p13/dicom-video.md diff --git a/extensions/default/src/DicomWebDataSource/index.ts b/extensions/default/src/DicomWebDataSource/index.ts index 209cd406c..9a4d31d95 100644 --- a/extensions/default/src/DicomWebDataSource/index.ts +++ b/extensions/default/src/DicomWebDataSource/index.ts @@ -18,6 +18,7 @@ import getDirectURL from '../utils/getDirectURL'; import { fixBulkDataURI } from './utils/fixBulkDataURI'; import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders'; import { getGetThumbnailSrc, ThumbnailContext } from './retrieveThumbnail'; +import { getRenderedURL } from './retrieveRendered'; const { DicomMetaDictionary, DicomDict } = dcmjs.data; @@ -108,7 +109,6 @@ export type BulkDataURIConfig = { relativeResolution?: 'studies' | 'series'; }; - /** * The header options are the options passed into the generateWadoHeader * command. This takes an extensible set of attributes to allow future enhancements. @@ -291,6 +291,14 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { params ); }, + renderedURL: (params, options) => { + return getRenderedURL({ + config: dicomWebConfig, + getAuthorizationHeader, + retrieve: implementation.retrieve, + userAuthenticationService, + })(params, options); + }, /** * Provide direct access to the dicom web client for certain use cases * where the dicom web client is used by an external library such as the diff --git a/extensions/default/src/DicomWebDataSource/retrieveRendered.test.ts b/extensions/default/src/DicomWebDataSource/retrieveRendered.test.ts new file mode 100644 index 000000000..b0c82873d --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/retrieveRendered.test.ts @@ -0,0 +1,147 @@ +import { fetchRenderedURL, getRenderedURL, isTrustedWadoURL } from './retrieveRendered'; + +describe('retrieveRendered', () => { + const originalFetch = global.fetch; + const originalCreateObjectURL = URL.createObjectURL; + const originalRevokeObjectURL = URL.revokeObjectURL; + + beforeEach(() => { + global.fetch = jest.fn(); + URL.createObjectURL = jest.fn(() => 'blob:rendered') as jest.Mock; + URL.revokeObjectURL = jest.fn() as jest.Mock; + jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + global.fetch = originalFetch; + URL.createObjectURL = originalCreateObjectURL; + URL.revokeObjectURL = originalRevokeObjectURL; + }); + + it('returns null when there is no resolved URL', async () => { + const result = await fetchRenderedURL({ + url: undefined, + wadoRoot: 'https://example.com/dicomweb', + headers: { Authorization: 'Bearer token' }, + }); + + expect(result).toEqual({ url: null }); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('returns the direct URL without fetching when there is no authorization header', async () => { + const result = await fetchRenderedURL({ + url: 'https://example.com/dicomweb/rendered', + wadoRoot: 'https://example.com/dicomweb', + headers: {}, + }); + + expect(result).toEqual({ url: 'https://example.com/dicomweb/rendered' }); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('does not attach auth headers to untrusted absolute URLs', async () => { + const result = await fetchRenderedURL({ + url: 'https://untrusted.example.com/rendered', + wadoRoot: 'https://example.com/dicomweb', + headers: { Authorization: 'Bearer token' }, + }); + + expect(result).toEqual({ url: 'https://untrusted.example.com/rendered' }); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('fetches trusted URLs with auth and returns a revocable object URL', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(new Blob(['pdf'], { type: 'application/pdf' })), + }); + + const result = await fetchRenderedURL({ + url: 'https://example.com/dicomweb/studies/1/rendered', + wadoRoot: 'https://example.com/dicomweb', + headers: { Authorization: 'Bearer token' }, + }); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/dicomweb/studies/1/rendered', + expect.objectContaining({ + method: 'GET', + headers: { Authorization: 'Bearer token' }, + }) + ); + expect(result.url).toBe('blob:rendered'); + + result.revoke?.(); + result.revoke?.(); + + expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:rendered'); + }); + + it('returns null and triggers unauthenticated handling on 401/403 responses', async () => { + const handleUnauthenticated = jest.fn(); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 401, + }); + + const result = await fetchRenderedURL({ + url: 'https://example.com/dicomweb/studies/1/rendered', + wadoRoot: 'https://example.com/dicomweb', + headers: { Authorization: 'Bearer token' }, + userAuthenticationService: { handleUnauthenticated }, + }); + + expect(result).toEqual({ url: null }); + expect(handleUnauthenticated).toHaveBeenCalledTimes(1); + }); + + it('revokes an object URL created after the request was aborted', async () => { + const abortController = new AbortController(); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockImplementationOnce(async () => { + abortController.abort(); + return new Blob(['pdf'], { type: 'application/pdf' }); + }), + }); + + const result = await fetchRenderedURL({ + url: 'https://example.com/dicomweb/studies/1/rendered', + wadoRoot: 'https://example.com/dicomweb', + headers: { Authorization: 'Bearer token' }, + signal: abortController.signal, + }); + + expect(result).toEqual({ url: null }); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:rendered'); + }); + + it('resolves directURL through the datasource retrieve API', async () => { + const renderedURL = getRenderedURL({ + config: { wadoRoot: 'https://example.com/dicomweb' }, + getAuthorizationHeader: () => ({}), + retrieve: { + directURL: jest.fn().mockResolvedValueOnce('https://example.com/dicomweb/rendered'), + }, + }); + + await expect(renderedURL({ instance: {} })).resolves.toEqual({ + url: 'https://example.com/dicomweb/rendered', + }); + }); + + it('trusts URLs that share the configured WADO origin', () => { + expect( + isTrustedWadoURL('/dicomweb/studies/1/rendered', `${window.location.origin}/dicomweb`) + ).toBe(true); + expect( + isTrustedWadoURL( + 'https://other.example.com/dicomweb/studies/1/rendered', + 'https://example.com/dicomweb' + ) + ).toBe(false); + }); +}); diff --git a/extensions/default/src/DicomWebDataSource/retrieveRendered.ts b/extensions/default/src/DicomWebDataSource/retrieveRendered.ts new file mode 100644 index 000000000..0f53b84b2 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/retrieveRendered.ts @@ -0,0 +1,141 @@ +import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders'; + +type RetrieveApi = { + directURL: (params: unknown) => unknown; +}; + +type RenderedURLConfig = { + wadoRoot?: string; +}; + +type UserAuthenticationService = { + handleUnauthenticated?: () => unknown; +}; + +type GetRenderedURLDeps = { + config: RenderedURLConfig; + getAuthorizationHeader: () => HeadersInterface; + retrieve: RetrieveApi; + userAuthenticationService?: UserAuthenticationService; +}; + +type RenderedURLOptions = { + signal?: AbortSignal; +}; + +type FetchRenderedURLOptions = RenderedURLOptions & { + url?: string | null; + wadoRoot?: string; + headers?: HeadersInterface; + userAuthenticationService?: UserAuthenticationService; +}; + +export type RenderedURLResult = { + url: string | null; + revoke?: () => void; +}; + +export function getRenderedURL({ + config, + getAuthorizationHeader, + retrieve, + userAuthenticationService, +}: GetRenderedURLDeps) { + return async function renderedURL( + params: unknown, + options: RenderedURLOptions = {} + ): Promise { + const resolvedUrl = (await retrieve.directURL(params)) as string | undefined | null; + + return fetchRenderedURL({ + url: resolvedUrl, + wadoRoot: config.wadoRoot, + headers: getAuthorizationHeader(), + signal: options.signal, + userAuthenticationService, + }); + }; +} + +export async function fetchRenderedURL({ + url, + wadoRoot, + headers, + signal, + userAuthenticationService, +}: FetchRenderedURLOptions): Promise { + if (!url || signal?.aborted) { + return { url: null }; + } + + if (!headers?.Authorization || !isTrustedWadoURL(url, wadoRoot)) { + return { url }; + } + + try { + const response = await fetch(url, { + method: 'GET', + headers: headers as Record, + signal, + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + userAuthenticationService?.handleUnauthenticated?.(); + } + console.warn(`rendered media fetch failed with status ${response.status}`); + return { url: null }; + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + + if (signal?.aborted) { + URL.revokeObjectURL(objectUrl); + return { url: null }; + } + + return { + url: objectUrl, + revoke: createRevokeOnce(objectUrl), + }; + } catch (error) { + if ((error as { name?: string })?.name === 'AbortError') { + return { url: null }; + } + + console.warn('rendered media fetch failed', error); + return { url: null }; + } +} + +export function isTrustedWadoURL(url: string, wadoRoot?: string): boolean { + if (!wadoRoot) { + return false; + } + + try { + const parsedUrl = new URL(url, window.location.href); + const parsedWadoRoot = new URL(wadoRoot, window.location.href); + const isHttpUrl = parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; + const isHttpWadoRoot = + parsedWadoRoot.protocol === 'http:' || parsedWadoRoot.protocol === 'https:'; + + return isHttpUrl && isHttpWadoRoot && parsedUrl.origin === parsedWadoRoot.origin; + } catch { + return false; + } +} + +function createRevokeOnce(objectUrl: string) { + let revoked = false; + + return () => { + if (revoked) { + return; + } + + URL.revokeObjectURL(objectUrl); + revoked = true; + }; +} diff --git a/extensions/default/src/DicomWebProxyDataSource/index.ts b/extensions/default/src/DicomWebProxyDataSource/index.ts index 453afe499..8445ffcea 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.ts +++ b/extensions/default/src/DicomWebProxyDataSource/index.ts @@ -52,6 +52,7 @@ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.S retrieve: { getGetThumbnailSrc: (...args) => dicomWebDelegate.retrieve.getGetThumbnailSrc(...args), directURL: (...args) => dicomWebDelegate.retrieve.directURL(...args), + renderedURL: (...args) => dicomWebDelegate.retrieve.renderedURL(...args), series: { metadata: async (...args) => dicomWebDelegate.retrieve.series.metadata(...args), }, @@ -62,7 +63,8 @@ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.S reject: { series: (...args) => dicomWebDelegate?.reject?.series?.(...args), }, - deleteStudyMetadataPromise: (...args) => dicomWebDelegate?.deleteStudyMetadataPromise?.(...args), + deleteStudyMetadataPromise: (...args) => + dicomWebDelegate?.deleteStudyMetadataPromise?.(...args), getImageIdsForDisplaySet: (...args) => dicomWebDelegate?.getImageIdsForDisplaySet?.(...args), getImageIdsForInstance: (...args) => dicomWebDelegate?.getImageIdsForInstance?.(...args), getConfig: (...args) => diff --git a/extensions/default/src/MergeDataSource/index.ts b/extensions/default/src/MergeDataSource/index.ts index 69b89c565..282605e82 100644 --- a/extensions/default/src/MergeDataSource/index.ts +++ b/extensions/default/src/MergeDataSource/index.ts @@ -242,6 +242,24 @@ function createMergeDataSourceApi( defaultDataSourceName, extensionManager, }), + renderedURL: (...args: unknown[]) => { + const [dataSource] = extensionManager.getDataSources(defaultDataSourceName); + const renderedURL = get(dataSource, 'retrieve.renderedURL'); + + if (renderedURL) { + return renderedURL.apply(dataSource, args); + } + + const directURL = get(dataSource, 'retrieve.directURL'); + + if (!directURL) { + return Promise.resolve({ url: null }); + } + + return Promise.resolve(directURL.apply(dataSource, [args[0]])).then(url => ({ + url: (url as string | undefined | null) || null, + })); + }, series: { metadata: (...args: unknown[]) => callForAllDataSourcesAsync({ diff --git a/extensions/dicom-pdf/src/getSopClassHandlerModule.js b/extensions/dicom-pdf/src/getSopClassHandlerModule.js index d720ccb30..83c3890a9 100644 --- a/extensions/dicom-pdf/src/getSopClassHandlerModule.js +++ b/extensions/dicom-pdf/src/getSopClassHandlerModule.js @@ -14,12 +14,17 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) const { Modality, SOPInstanceUID } = instance; const { SeriesDescription = 'PDF', MIMETypeOfEncapsulatedDocument } = instance; const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, SOPClassUID } = instance; - const renderedUrl = dataSource.retrieve.directURL({ + const renderedUrlParams = { instance, tag: 'EncapsulatedDocument', defaultType: MIMETypeOfEncapsulatedDocument || 'application/pdf', singlepart: 'pdf', - }); + }; + const renderedUrl = dataSource.retrieve.directURL(renderedUrlParams); + const getRenderedUrl = dataSource.retrieve.renderedURL + ? options => + dataSource.retrieve.renderedURL({ ...renderedUrlParams, url: renderedUrl }, options) + : undefined; const displaySet = { //plugin: id, @@ -36,6 +41,7 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) referencedImages: null, measurements: null, renderedUrl: renderedUrl, + getRenderedUrl, instances: [instance], thumbnailSrc: null, isDerivedDisplaySet: true, diff --git a/extensions/dicom-pdf/src/index.tsx b/extensions/dicom-pdf/src/index.tsx index a655cbc50..aa4739d66 100644 --- a/extensions/dicom-pdf/src/index.tsx +++ b/extensions/dicom-pdf/src/index.tsx @@ -28,15 +28,9 @@ const dicomPDFExtension = { * @param {object} [configuration={}] * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` */ - getViewportModule({ servicesManager, extensionManager }) { + getViewportModule() { const ExtendedOHIFCornerstonePdfViewport = props => { - return ( - - ); + return ; }; return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }]; diff --git a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx index 6c1d1d878..b99992752 100644 --- a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx +++ b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx @@ -33,14 +33,43 @@ function OHIFCornerstonePdfViewport({ displaySets, viewportId = 'pdf-viewport' } } const { renderedUrl } = displaySets[0]; + const { getRenderedUrl } = displaySets[0]; useEffect(() => { + let isCancelled = false; + let revokeUrl; + const abortController = new AbortController(); + const load = async () => { - setUrl(await renderedUrl); + try { + const result = getRenderedUrl + ? await getRenderedUrl({ signal: abortController.signal }) + : { url: await renderedUrl }; + + if (isCancelled) { + result?.revoke?.(); + return; + } + + revokeUrl = result?.revoke; + setUrl(result?.url || null); + } catch (error) { + console.warn('Failed to load PDF', error); + if (!isCancelled) { + setUrl(null); + } + return; + } }; load(); - }, [renderedUrl]); + + return () => { + isCancelled = true; + abortController.abort(); + revokeUrl?.(); + }; + }, [renderedUrl, getRenderedUrl]); return (
dataSource.retrieve.renderedURL({ ...videoUrlParams, url: videoUrl }, options) + : undefined; const displaySet = { //plugin: id, Modality, @@ -76,6 +80,8 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) SOPClassHandlerId, referencedImages: null, measurements: null, + videoUrl, + getVideoUrl, viewportType: csEnums.ViewportType.VIDEO, instances: [instance], getThumbnailSrc: dataSource.retrieve.getGetThumbnailSrc?.(instance), diff --git a/extensions/dicom-video/src/index.tsx b/extensions/dicom-video/src/index.tsx index 0fde8ca82..202a9eb44 100644 --- a/extensions/dicom-video/src/index.tsx +++ b/extensions/dicom-video/src/index.tsx @@ -1,19 +1,6 @@ -import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; import { id } from './id'; -const Component = React.lazy(() => { - return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneVideoViewport'); -}); - -const OHIFCornerstoneVideoViewport = props => { - return ( - Loading...
}> - - - ); -}; - /** * */ @@ -23,38 +10,7 @@ const dicomVideoExtension = { */ id, - /** - * - * - * @param {object} [configuration={}] - * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` - */ - getViewportModule({ servicesManager, extensionManager }) { - const ExtendedOHIFCornerstoneVideoViewport = props => { - return ( - - ); - }; - - return [{ name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }]; - }, getSopClassHandlerModule, }; -function _getToolAlias(toolName) { - let toolAlias = toolName; - - switch (toolName) { - case 'EllipticalRoi': - toolAlias = 'SREllipticalRoi'; - break; - } - - return toolAlias; -} - export default dicomVideoExtension; diff --git a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx deleted file mode 100644 index 8f7e965a8..000000000 --- a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; - -function OHIFCornerstoneVideoViewport({ displaySets }) { - if (displaySets && displaySets.length > 1) { - throw new Error( - 'OHIFCornerstoneVideoViewport: only one display set is supported for dicom video right now' - ); - } - - const { videoUrl } = displaySets[0]; - const mimeType = 'video/mp4'; - const [url, setUrl] = useState(null); - - useEffect(() => { - const load = async () => { - setUrl(await videoUrl); - }; - - load(); - }, [videoUrl]); - - // Need to copies of the source to fix a firefox bug - return ( -
- -
- ); -} - -OHIFCornerstoneVideoViewport.propTypes = { - displaySets: PropTypes.arrayOf(PropTypes.object).isRequired, -}; - -export default OHIFCornerstoneVideoViewport; diff --git a/modes/basic-dev-mode/src/index.ts b/modes/basic-dev-mode/src/index.ts index c1663580e..af531c896 100644 --- a/modes/basic-dev-mode/src/index.ts +++ b/modes/basic-dev-mode/src/index.ts @@ -26,7 +26,6 @@ const dicomsr = { const dicomvideo = { sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', - viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', }; const dicompdf = { @@ -141,11 +140,7 @@ function modeFactory({ modeConfiguration }) { viewports: [ { namespace: cs3d.viewport, - displaySetsToDisplay: [ohif.sopClassHandler], - }, - { - namespace: dicomvideo.viewport, - displaySetsToDisplay: [dicomvideo.sopClassHandler], + displaySetsToDisplay: [ohif.sopClassHandler, dicomvideo.sopClassHandler], }, { namespace: dicompdf.viewport, diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts index b8a983757..540b0fff9 100644 --- a/modes/basic-test-mode/src/index.ts +++ b/modes/basic-test-mode/src/index.ts @@ -33,7 +33,6 @@ const dicomsr = { const dicomvideo = { sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', - viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', }; const dicompdf = { @@ -254,10 +253,6 @@ function modeFactory() { namespace: dicomsr.viewport, displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D], }, - { - namespace: dicomvideo.viewport, - displaySetsToDisplay: [dicomvideo.sopClassHandler], - }, { namespace: dicompdf.viewport, displaySetsToDisplay: [dicompdf.sopClassHandler], diff --git a/modes/basic/src/index.tsx b/modes/basic/src/index.tsx index f9de0101a..2b8f3c9d4 100644 --- a/modes/basic/src/index.tsx +++ b/modes/basic/src/index.tsx @@ -44,7 +44,6 @@ export const dicomsr = { export const dicomvideo = { sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', - viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', }; export const dicomecg = { diff --git a/modes/microscopy/src/index.tsx b/modes/microscopy/src/index.tsx index ec789fa7a..3acad37c7 100644 --- a/modes/microscopy/src/index.tsx +++ b/modes/microscopy/src/index.tsx @@ -17,7 +17,6 @@ export const cornerstone = { const dicomvideo = { sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', - viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', }; const dicompdf = { @@ -107,7 +106,7 @@ function modeFactory({ modeConfiguration }) { ], }, { - namespace: dicomvideo.viewport, + namespace: cornerstone.viewport, displaySetsToDisplay: [dicomvideo.sopClassHandler], }, { diff --git a/modes/usAnnotation/src/index.ts b/modes/usAnnotation/src/index.ts index 5535373ab..0aa18797b 100644 --- a/modes/usAnnotation/src/index.ts +++ b/modes/usAnnotation/src/index.ts @@ -32,7 +32,6 @@ const dicomsr = { const dicomvideo = { sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', - viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', }; const dicompdf = { diff --git a/platform/docs/docs/migration-guide/3p12-to-3p13/dicom-video.md b/platform/docs/docs/migration-guide/3p12-to-3p13/dicom-video.md new file mode 100644 index 000000000..4e662e3b3 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p12-to-3p13/dicom-video.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 8 +sidebar_label: DICOM video viewport +title: DICOM video viewport migration +--- + +# DICOM video viewport migration + +The `@ohif/extension-dicom-video` extension no longer provides its own viewport module in 3.13. DICOM video rendering is handled by the Cornerstone viewport, which supports Cornerstone3D video viewports directly. + +The DICOM video SOP class handler remains available and should still be used to identify and build video display sets: + +```ts +'@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video' +``` + +## What changed + +The following viewport namespace has been removed: + +```ts +'@ohif/extension-dicom-video.viewportModule.dicom-video' +``` + +Use the Cornerstone viewport namespace instead: + +```ts +'@ohif/extension-cornerstone.viewportModule.cornerstone' +``` + +## Migration + +If your mode has a dedicated DICOM video viewport entry, replace it with the Cornerstone viewport or add the video SOP class handler to an existing Cornerstone viewport entry. + +**Before (3.12):** + +```ts +const dicomvideo = { + sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', +}; + +viewports: [ + { + namespace: '@ohif/extension-cornerstone.viewportModule.cornerstone', + displaySetsToDisplay: [ohif.sopClassHandler], + }, + { + namespace: dicomvideo.viewport, + displaySetsToDisplay: [dicomvideo.sopClassHandler], + }, +]; +``` + +**After (3.13):** + +```ts +const dicomvideo = { + sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', +}; + +viewports: [ + { + namespace: '@ohif/extension-cornerstone.viewportModule.cornerstone', + displaySetsToDisplay: [ohif.sopClassHandler, dicomvideo.sopClassHandler], + }, +]; +``` + +If your mode uses a custom viewport that wraps `OHIFCornerstoneViewport` (for example, a tracked Cornerstone viewport), route DICOM video display sets through that wrapper instead of the removed DICOM video viewport namespace. + +## Why + +Keeping video rendering in the Cornerstone viewport avoids a separate custom React video viewport path and keeps video behavior aligned with Cornerstone3D viewport services, tools, annotations, and presentation state handling. diff --git a/platform/docs/docs/migration-guide/3p12-to-3p13/index.md b/platform/docs/docs/migration-guide/3p12-to-3p13/index.md index 3bf5b80cd..5e862d75c 100644 --- a/platform/docs/docs/migration-guide/3p12-to-3p13/index.md +++ b/platform/docs/docs/migration-guide/3p12-to-3p13/index.md @@ -23,5 +23,8 @@ The largest changes in 3.13 are infrastructure-level: is now **24**. - **[SegmentationService](./segmentation-service.md)** — the `removeSegmentationRepresentations` method was renamed. +- **[DICOM video viewport](./dicom-video.md)** — the + `@ohif/extension-dicom-video.viewportModule.dicom-video` namespace was + removed; route DICOM video display sets through the Cornerstone viewport.