From 57205703bb27909930773df7d331f33e55fe0c59 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 21 Nov 2025 13:07:26 -0500 Subject: [PATCH] Fix contour annotations RTSS saving (#5545) * [WIP] Fix contour annotations RTSS saving * fix: Export image sop image reference provider * Fixes for download with filename * Updates to fix metadata issues with CS3D * Updates to save menu * Fixing some additional re-save metadata * Add support for saving with predecessor image sequence * PR comment fixes * fix: Load RTSS after save * fix segmentation save * fix load of mixed seg/rt studies * Use frame module to get information on the frame shown * Fix study browser to use instances.length when numImageFrames not present * fix: Load of display set split from multiple non-FOR referenes * Fix RT and SEG applying to wrong series because of using referenced series sequence without checkout sop instances * Fix save seg * Update to released CS3D versions * test: Add revokeObjectURL to test download blob * Add hard dependency on mode-basic to fix build order issues * fix: build * Try to fix build segmentation * PR comments * PR comment update --- addOns/package.json | 2 +- bun.lock | 402 +++++++++--------- .../cornerstone-dicom-pmap/package.json | 4 +- .../src/getSopClassHandlerModule.ts | 40 +- .../cornerstone-dicom-rt/src/loadRTStruct.js | 7 +- .../viewports/OHIFCornerstoneRTViewport.tsx | 2 +- extensions/cornerstone-dicom-seg/package.json | 4 +- .../src/commandsModule.ts | 101 +++-- .../src/getSopClassHandlerModule.ts | 22 +- .../viewports/OHIFCornerstoneSEGViewport.tsx | 2 +- extensions/cornerstone-dicom-sr/package.json | 8 +- .../src/commandsModule.ts | 11 +- .../src/getSopClassHandlerModule.ts | 6 + .../src/utils/hydrateStructuredReport.ts | 14 +- .../cornerstone-dynamic-volume/package.json | 6 +- .../src/commandsModule.ts | 18 +- extensions/cornerstone/package.json | 16 +- extensions/cornerstone/src/commandsModule.ts | 8 +- .../ExportSegmentationSubMenuItem.tsx | 129 +++--- .../CustomDropdownMenuContent.tsx | 16 +- .../cornerstone/src/initMeasurementService.ts | 3 + .../SegmentationService.ts | 5 + .../CornerstoneViewportService.ts | 2 + .../utils/CornerstoneViewportDownloadForm.tsx | 8 +- .../generateSegmentationCSVReport.test.ts | 1 + .../utils/generateSegmentationCSVReport.ts | 22 +- .../src/utils/segmentationExportUtils.ts | 2 +- extensions/default/package.json | 2 +- .../Panels/StudyBrowser/PanelStudyBrowser.tsx | 17 +- .../src/Panels/createReportDialogPrompt.tsx | 46 +- .../reportDialogCustomization.tsx | 34 +- .../getDisplaySetsFromUnsupportedSeries.js | 3 +- .../default/src/utils/promptSaveReport.tsx | 41 +- .../MicroscopyPanel/MicroscopyPanel.tsx | 11 +- .../src/utils/saveByteArray.ts | 12 - extensions/dicom-pdf/package.json | 2 +- extensions/dicom-video/package.json | 2 +- extensions/measurement-tracking/package.json | 6 +- .../PanelStudyBrowserTracking.tsx | 2 +- extensions/test-extension/package.json | 2 +- extensions/tmtv/package.json | 2 +- .../src/utils/createAndDownloadTMTVReport.js | 15 +- extensions/usAnnotation/package.json | 4 +- .../usAnnotation/src/getCommandsModule.ts | 16 +- modes/basic/package.json | 2 +- modes/basic/src/index.tsx | 10 + modes/longitudinal/package.json | 3 +- modes/segmentation/.webpack/webpack.prod.js | 6 + modes/segmentation/package.json | 1 + modes/segmentation/src/index.tsx | 40 +- modes/segmentation/src/toolbarButtons.ts | 2 +- modes/usAnnotation/package.json | 4 +- package.json | 9 +- platform/app/package.json | 4 +- platform/app/public/config/kheops.js | 23 + platform/core/package.json | 6 +- platform/core/src/classes/MetadataProvider.ts | 111 ++--- .../DicomMetadataStore/DicomMetadataStore.ts | 20 +- .../DisplaySetService/DisplaySetService.ts | 40 +- .../MeasurementService/MeasurementService.ts | 28 +- platform/core/src/types/DisplaySet.ts | 27 ++ platform/core/src/utils/addProxyFields.js | 7 + platform/core/src/utils/downloadBlob.ts | 36 ++ platform/core/src/utils/downloadCSVReport.js | 8 +- platform/core/src/utils/index.ts | 10 + .../migration-guide/3p11-to-3p12/index.md | 42 ++ .../extensions/modules/sop-class-handler.md | 25 ++ yarn.lock | 68 +-- 68 files changed, 962 insertions(+), 648 deletions(-) delete mode 100644 extensions/dicom-microscopy/src/utils/saveByteArray.ts create mode 100644 platform/core/src/utils/downloadBlob.ts diff --git a/addOns/package.json b/addOns/package.json index e2c47c14e..ed186e03a 100644 --- a/addOns/package.json +++ b/addOns/package.json @@ -38,7 +38,7 @@ "resolutions": { "**/@babel/runtime": "7.28.2", "commander": "8.3.0", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicomweb-client": "0.10.4", "nth-check": "2.1.1", "trim-newlines": "5.0.0", diff --git a/bun.lock b/bun.lock index c64495e74..de13978c6 100644 --- a/bun.lock +++ b/bun.lock @@ -52,7 +52,7 @@ }, "addOns/externals/devDependencies": { "name": "@externals/devDependencies", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "@kitware/vtk.js": "32.12.0", @@ -137,22 +137,22 @@ }, "addOns/externals/dicom-microscopy-viewer": { "name": "@externals/dicom-microscopy-viewer", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "dicom-microscopy-viewer": "0.48.6", }, }, "extensions/cornerstone": { "name": "@ohif/extension-cornerstone", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/ai": "4.11.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/labelmap-interpolation": "4.11.2", - "@cornerstonejs/polymorphic-segmentation": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/ai": "4.11.3", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/labelmap-interpolation": "4.11.3", + "@cornerstonejs/polymorphic-segmentation": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "1.4.1", @@ -169,10 +169,10 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/dicom-image-loader": "4.11.2", - "@ohif/core": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@cornerstonejs/dicom-image-loader": "4.11.3", + "@ohif/core": "3.12.0-beta.89", + "@ohif/ui": "3.12.0-beta.89", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -183,19 +183,19 @@ }, "extensions/cornerstone-dicom-pmap": { "name": "@ohif/extension-cornerstone-dicom-pmap", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/core": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/core": "4.11.3", "@kitware/vtk.js": "32.12.0", "react-color": "2.19.3", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/i18n": "3.12.0-beta.87", "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -206,16 +206,16 @@ }, "extensions/cornerstone-dicom-rt": { "name": "@ohif/extension-cornerstone-dicom-rt", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "react-color": "2.19.3", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone": "3.12.0-beta.89", + "@ohif/extension-default": "3.12.0-beta.89", + "@ohif/i18n": "3.12.0-beta.89", "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -226,19 +226,19 @@ }, "extensions/cornerstone-dicom-seg": { "name": "@ohif/extension-cornerstone-dicom-seg", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/core": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/core": "4.11.3", "@kitware/vtk.js": "32.12.0", "react-color": "2.19.3", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone": "3.12.0-beta.89", + "@ohif/extension-default": "3.12.0-beta.89", + "@ohif/i18n": "3.12.0-beta.89", "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -249,20 +249,20 @@ }, "extensions/cornerstone-dicom-sr": { "name": "@ohif/extension-cornerstone-dicom-sr", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "classnames": "2.5.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-measurement-tracking": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone": "3.12.0-beta.89", + "@ohif/extension-measurement-tracking": "3.12.0-beta.89", + "@ohif/ui": "3.12.0-beta.89", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -271,20 +271,20 @@ }, "extensions/cornerstone-dynamic-volume": { "name": "@ohif/extension-cornerstone-dynamic-volume", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "classnames": "2.5.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/i18n": "3.12.0-beta.87", + "@ohif/ui": "3.12.0-beta.87", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -293,7 +293,7 @@ }, "extensions/default": { "name": "@ohif/extension-default", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "@cornerstonejs/calculate-suv": "1.1.0", @@ -301,9 +301,9 @@ "lodash.uniqby": "4.7.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.89", + "@ohif/i18n": "3.12.0-beta.89", + "dcmjs": "0.45.0", "dicomweb-client": "0.10.4", "prop-types": "15.8.1", "react": "18.3.1", @@ -316,7 +316,7 @@ }, "extensions/dicom-microscopy": { "name": "@ohif/extension-dicom-microscopy", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "@cornerstonejs/codec-charls": "1.2.3", @@ -327,10 +327,10 @@ "mathjs": "12.4.3", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/i18n": "3.12.0-beta.87", + "@ohif/ui": "3.12.0-beta.87", "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1", @@ -341,15 +341,15 @@ }, "extensions/dicom-pdf": { "name": "@ohif/extension-dicom-pdf", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "classnames": "2.5.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.89", + "@ohif/ui": "3.12.0-beta.89", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -358,15 +358,15 @@ }, "extensions/dicom-video": { "name": "@ohif/extension-dicom-video", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "classnames": "2.5.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.89", + "@ohif/ui": "3.12.0-beta.89", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -375,22 +375,22 @@ }, "extensions/measurement-tracking": { "name": "@ohif/extension-measurement-tracking", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@ohif/ui": "3.12.0-beta.86", + "@ohif/ui": "3.12.0-beta.89", "@xstate/react": "3.2.2", "xstate": "4.38.3", }, "peerDependencies": { - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.89", + "@ohif/extension-default": "3.12.0-beta.89", + "@ohif/ui": "3.12.0-beta.89", "classnames": "2.5.1", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "lodash.debounce": "4.0.8", "prop-types": "15.8.1", "react": "18.3.1", @@ -401,15 +401,15 @@ }, "extensions/test-extension": { "name": "@ohif/extension-test", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "classnames": "2.5.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.87", + "@ohif/ui": "3.12.0-beta.87", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -418,15 +418,15 @@ }, "extensions/tmtv": { "name": "@ohif/extension-tmtv", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "classnames": "2.5.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.78", - "@ohif/ui": "3.12.0-beta.78", - "dcmjs": "0.43.1", + "@ohif/core": "3.12.0-beta.87", + "@ohif/ui": "3.12.0-beta.87", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -435,16 +435,16 @@ }, "extensions/usAnnotation": { "name": "@ohif/extension-ultrasound-pleura-bline", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", - "@ohif/ui-next": "3.12.0-beta.86", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/i18n": "3.12.0-beta.87", + "@ohif/ui-next": "3.12.0-beta.87", }, "devDependencies": { "@babel/core": "7.28.0", @@ -487,7 +487,7 @@ }, "modes/basic": { "name": "@ohif/mode-basic", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", }, @@ -496,19 +496,19 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.78", - "@ohif/extension-cornerstone": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.78", - "@ohif/extension-default": "3.12.0-beta.78", - "@ohif/extension-dicom-pdf": "3.12.0-beta.78", - "@ohif/extension-dicom-video": "3.12.0-beta.78", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.89", + "@ohif/extension-default": "3.12.0-beta.89", + "@ohif/extension-dicom-pdf": "3.12.0-beta.89", + "@ohif/extension-dicom-video": "3.12.0-beta.89", }, }, "modes/basic-dev-mode": { "name": "@ohif/mode-basic-dev-mode", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next": "17.3.1", @@ -518,17 +518,17 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/extension-dicom-pdf": "3.12.0-beta.86", - "@ohif/extension-dicom-video": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/extension-dicom-pdf": "3.12.0-beta.87", + "@ohif/extension-dicom-video": "3.12.0-beta.87", }, }, "modes/basic-test-mode": { "name": "@ohif/mode-test", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next": "17.3.1", @@ -538,19 +538,19 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/extension-dicom-pdf": "3.12.0-beta.86", - "@ohif/extension-dicom-video": "3.12.0-beta.86", - "@ohif/extension-measurement-tracking": "3.12.0-beta.86", - "@ohif/extension-test": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/extension-dicom-pdf": "3.12.0-beta.87", + "@ohif/extension-dicom-video": "3.12.0-beta.87", + "@ohif/extension-measurement-tracking": "3.12.0-beta.87", + "@ohif/extension-test": "3.12.0-beta.87", }, }, "modes/longitudinal": { "name": "@ohif/mode-longitudinal", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next": "17.3.1", @@ -560,32 +560,33 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/extension-dicom-pdf": "3.12.0-beta.86", - "@ohif/extension-dicom-video": "3.12.0-beta.86", - "@ohif/extension-measurement-tracking": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.89", + "@ohif/extension-default": "3.12.0-beta.89", + "@ohif/extension-dicom-pdf": "3.12.0-beta.89", + "@ohif/extension-dicom-video": "3.12.0-beta.89", + "@ohif/extension-measurement-tracking": "3.12.0-beta.89", + "@ohif/mode-basic": "3.12.0-beta.89", }, }, "modes/microscopy": { "name": "@ohif/mode-microscopy", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next": "17.3.1", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-dicom-microscopy": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-dicom-microscopy": "3.12.0-beta.87", }, }, "modes/preclinical-4d": { "name": "@ohif/mode-preclinical-4d", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", }, @@ -594,17 +595,17 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.78", - "@ohif/extension-cornerstone": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dynamic-volume": "3.12.0-beta.78", - "@ohif/extension-default": "3.12.0-beta.78", - "@ohif/extension-tmtv": "3.12.0-beta.78", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dynamic-volume": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/extension-tmtv": "3.12.0-beta.87", }, }, "modes/segmentation": { "name": "@ohif/mode-segmentation", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next": "17.3.1", @@ -636,19 +637,20 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.78", - "@ohif/extension-cornerstone": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.78", - "@ohif/extension-default": "3.12.0-beta.78", - "@ohif/extension-dicom-pdf": "3.12.0-beta.78", - "@ohif/extension-dicom-video": "3.12.0-beta.78", + "@ohif/core": "3.12.0-beta.89", + "@ohif/extension-cornerstone": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.89", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.89", + "@ohif/extension-default": "3.12.0-beta.89", + "@ohif/extension-dicom-pdf": "3.12.0-beta.89", + "@ohif/extension-dicom-video": "3.12.0-beta.89", + "@ohif/mode-basic": "3.12.0-beta.89", }, }, "modes/tmtv": { "name": "@ohif/mode-tmtv", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next": "17.3.1", @@ -658,25 +660,25 @@ "webpack-merge": "5.10.0", }, "peerDependencies": { - "@ohif/core": "3.12.0-beta.78", - "@ohif/extension-cornerstone": "3.12.0-beta.78", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.78", - "@ohif/extension-default": "3.12.0-beta.78", - "@ohif/extension-dicom-pdf": "3.12.0-beta.78", - "@ohif/extension-dicom-video": "3.12.0-beta.78", - "@ohif/extension-measurement-tracking": "3.12.0-beta.78", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/extension-dicom-pdf": "3.12.0-beta.87", + "@ohif/extension-dicom-video": "3.12.0-beta.87", + "@ohif/extension-measurement-tracking": "3.12.0-beta.87", }, }, "modes/usAnnotation": { "name": "@ohif/mode-ultrasound-pleura-bline", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.86", - "@ohif/extension-ultrasound-pleura-bline": "3.12.0-beta.86", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.87", + "@ohif/extension-ultrasound-pleura-bline": "3.12.0-beta.87", "i18next": "17.3.1", }, "devDependencies": { @@ -708,40 +710,40 @@ }, "platform/app": { "name": "@ohif/app", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "@cornerstonejs/codec-charls": "1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/dicom-image-loader": "4.11.2", + "@cornerstonejs/dicom-image-loader": "4.11.3", "@emotion/serialize": "1.3.3", - "@ohif/core": "3.12.0-beta.86", - "@ohif/extension-cornerstone": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.86", - "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.86", - "@ohif/extension-default": "3.12.0-beta.86", - "@ohif/extension-dicom-microscopy": "3.12.0-beta.86", - "@ohif/extension-dicom-pdf": "3.12.0-beta.86", - "@ohif/extension-dicom-video": "3.12.0-beta.86", - "@ohif/extension-test": "3.12.0-beta.86", - "@ohif/extension-ultrasound-pleura-bline": "3.12.0-beta.86", - "@ohif/i18n": "3.12.0-beta.86", - "@ohif/mode-basic-dev-mode": "3.12.0-beta.86", - "@ohif/mode-longitudinal": "3.12.0-beta.86", - "@ohif/mode-microscopy": "3.12.0-beta.86", - "@ohif/mode-test": "3.12.0-beta.86", - "@ohif/mode-ultrasound-pleura-bline": "3.12.0-beta.86", - "@ohif/ui": "3.12.0-beta.86", - "@ohif/ui-next": "3.12.0-beta.86", + "@ohif/core": "3.12.0-beta.87", + "@ohif/extension-cornerstone": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-seg": "3.12.0-beta.87", + "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.87", + "@ohif/extension-default": "3.12.0-beta.87", + "@ohif/extension-dicom-microscopy": "3.12.0-beta.87", + "@ohif/extension-dicom-pdf": "3.12.0-beta.87", + "@ohif/extension-dicom-video": "3.12.0-beta.87", + "@ohif/extension-test": "3.12.0-beta.87", + "@ohif/extension-ultrasound-pleura-bline": "3.12.0-beta.87", + "@ohif/i18n": "3.12.0-beta.87", + "@ohif/mode-basic-dev-mode": "3.12.0-beta.87", + "@ohif/mode-longitudinal": "3.12.0-beta.87", + "@ohif/mode-microscopy": "3.12.0-beta.87", + "@ohif/mode-test": "3.12.0-beta.87", + "@ohif/mode-ultrasound-pleura-bline": "3.12.0-beta.87", + "@ohif/ui": "3.12.0-beta.87", + "@ohif/ui-next": "3.12.0-beta.87", "@svgr/webpack": "8.1.0", "@types/react": "18.3.23", "classnames": "2.5.1", "core-js": "3.45.1", "cornerstone-math": "0.1.10", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "detect-gpu": "4.0.50", "dicom-parser": "1.8.21", "dotenv-webpack": "1.8.0", @@ -787,7 +789,7 @@ }, "platform/cli": { "name": "@ohif/cli", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "bin": { "ohif-cli": "src/index.js", }, @@ -811,10 +813,10 @@ }, "platform/core": { "name": "@ohif/core", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicomweb-client": "0.10.4", "gl-matrix": "3.4.3", "immutability-helper": "3.1.1", @@ -836,16 +838,16 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/dicom-image-loader": "4.11.2", - "@ohif/ui": "3.12.0-beta.86", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/dicom-image-loader": "4.11.3", + "@ohif/ui": "3.12.0-beta.89", "cornerstone-math": "0.1.9", "dicom-parser": "1.8.21", }, }, "platform/i18n": { "name": "@ohif/i18n", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@babel/runtime": "7.28.2", "i18next-locize-backend": "2.2.2", @@ -870,7 +872,7 @@ }, "platform/ui": { "name": "@ohif/ui", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@testing-library/react": "13.4.0", "browser-detect": "0.2.28", @@ -921,7 +923,7 @@ }, "platform/ui-next": { "name": "@ohif/ui-next", - "version": "3.12.0-beta.86", + "version": "3.12.0-beta.89", "dependencies": { "@radix-ui/react-accordion": "1.2.11", "@radix-ui/react-checkbox": "1.3.2", @@ -973,7 +975,7 @@ "core-js": "3.45.1", "cross-env": "7.0.3", "cross-spawn": "7.0.6", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "glob-parent": "6.0.2", "nth-check": "2.1.1", "package-json": "8.1.1", @@ -1228,9 +1230,9 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], - "@cornerstonejs/adapters": ["@cornerstonejs/adapters@4.11.2", "", { "dependencies": { "@babel/runtime-corejs2": "7.26.10", "buffer": "6.0.3", "dcmjs": "0.43.1", "gl-matrix": "3.4.3", "ndarray": "1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "4.11.2", "@cornerstonejs/tools": "4.11.2" } }, "sha512-yYZd4W+BnM9So0SsPYYjYN+cho5FBlXIOg0o3JX+gQGoguSk9UedRfd5xa7f7TYoN4ect7RdU4ytwajdabRY5Q=="], + "@cornerstonejs/adapters": ["@cornerstonejs/adapters@4.11.3", "", { "dependencies": { "@babel/runtime-corejs2": "7.26.10", "buffer": "6.0.3", "dcmjs": "0.45.0", "gl-matrix": "3.4.3", "ndarray": "1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "4.11.3", "@cornerstonejs/tools": "4.11.3" } }, "sha512-N0nKn7ZKBr4ryvveSy19h8EnyhAUyYkdmn23KibPrF84VjzFfK99xMIFWYHxxhYWfCJwzcGf7RR4LIop3FD60Q=="], - "@cornerstonejs/ai": ["@cornerstonejs/ai@4.11.2", "", { "dependencies": { "@babel/runtime-corejs2": "7.26.10", "buffer": "6.0.3", "dcmjs": "0.43.1", "gl-matrix": "3.4.3", "lodash.clonedeep": "4.5.0", "ndarray": "1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "4.11.2", "@cornerstonejs/tools": "4.11.2" } }, "sha512-aUfcRidr8/48Szw9MPK4MFXZPhoPlABxOXltx6pZHFe7da3KE1WYoSLD/Dw0T72AtqV2w8blXQuNpplTL4ErAg=="], + "@cornerstonejs/ai": ["@cornerstonejs/ai@4.11.3", "", { "dependencies": { "@babel/runtime-corejs2": "7.26.10", "buffer": "6.0.3", "dcmjs": "0.45.0", "gl-matrix": "3.4.3", "lodash.clonedeep": "4.5.0", "ndarray": "1.0.19", "onnxruntime-common": "1.17.1", "onnxruntime-web": "1.17.1" }, "peerDependencies": { "@cornerstonejs/core": "4.11.3", "@cornerstonejs/tools": "4.11.3" } }, "sha512-qHrFOuM8iYtCX683+Nii9fBkBvgPQA2x+/9jbZpV17YPHAxUSy5gi6a+3ISMwVzKtFhIVhVZK+JMKAAOcCtVVQ=="], "@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="], @@ -1242,15 +1244,15 @@ "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], - "@cornerstonejs/core": ["@cornerstonejs/core@4.11.2", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "4.4.2", "gl-matrix": "3.4.3", "loglevel": "1.9.2" } }, "sha512-8Lq3hq+k31C4KbU0sGfUEP/ORn/6nqVYioXvQG4iU+SpLbbQVyhUgEqtQPIKoZVsGaV2mX4HOU3fWXAmMfP4fQ=="], + "@cornerstonejs/core": ["@cornerstonejs/core@4.11.3", "", { "dependencies": { "@kitware/vtk.js": "32.12.1", "comlink": "4.4.2", "gl-matrix": "3.4.3", "loglevel": "1.9.2" } }, "sha512-72yGsvuf5XezQ2nV6JPIOPMlIRtXzuABn1JgpDpoCYbjqbrmApL6G0nLswj0Tuo2dmh4E3iS80KSVVggjllcPg=="], - "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@4.11.2", "", { "dependencies": { "@cornerstonejs/codec-charls": "1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", "comlink": "4.4.2", "dicom-parser": "1.8.21", "jpeg-lossless-decoder-js": "2.1.2", "pako": "2.1.0", "uuid": "9.0.1" }, "peerDependencies": { "@cornerstonejs/core": "4.11.2" } }, "sha512-xIWbAOrkVBgpBpcqlyYIL2fiAZ8KRF453Ao6svDRixoL7Fl2gvhnpJGbG3wWCYhHaPXbYpbhmfzIovT9qZ7ACg=="], + "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@4.11.3", "", { "dependencies": { "@cornerstonejs/codec-charls": "1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", "comlink": "4.4.2", "dicom-parser": "1.8.21", "jpeg-lossless-decoder-js": "2.1.2", "pako": "2.1.0", "uuid": "9.0.1" }, "peerDependencies": { "@cornerstonejs/core": "4.11.3" } }, "sha512-SULpwXEpHF0cINusoTehwUpzQOyxakSTrZ+h4WTLu9GKosxxoIPorw3/eQpsJR9tUBxNnrvgSPFkXXGBdEq4Cw=="], - "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@4.11.2", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "4.11.2", "@cornerstonejs/tools": "4.11.2", "@kitware/vtk.js": "32.12.1" } }, "sha512-gS0Gt07U9hDzjoD75I3fKcXRM47sEWsAI706L23/3Q0nN3DGz5NDDySIDo8E+IfZDdSJeX2yBO38RqszVlTkcA=="], + "@cornerstonejs/labelmap-interpolation": ["@cornerstonejs/labelmap-interpolation@4.11.3", "", { "dependencies": { "@itk-wasm/morphological-contour-interpolation": "1.1.0", "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { "@cornerstonejs/core": "4.11.3", "@cornerstonejs/tools": "4.11.3", "@kitware/vtk.js": "32.12.1" } }, "sha512-yqIvR+7x8HC1l6cMgTBx75LMQf3ZMXdsfiBbOl4mYOlRiiQ5tEcx+8anU+kllwEhsWThu5MjY7s5ZSRybyks0g=="], - "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@4.11.2", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "4.11.2", "@cornerstonejs/tools": "4.11.2", "@kitware/vtk.js": "32.12.1" } }, "sha512-cW1kVQOiwzWnsHdQOi6wi/k7ji3+qC1sqWb7WgFqf7uTuBbab4Oh0KO6ODcg3G0SOWhzQCPT+GcZf7tsgQh2Tw=="], + "@cornerstonejs/polymorphic-segmentation": ["@cornerstonejs/polymorphic-segmentation@4.11.3", "", { "dependencies": { "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { "@cornerstonejs/core": "4.11.3", "@cornerstonejs/tools": "4.11.3", "@kitware/vtk.js": "32.12.1" } }, "sha512-RXKP26mEcQLzpC1xArr/QpMq+minkjO4W02jhhJxmGdTTVypyFyq3Kr9tPJUhosM91yssUHa5mIIHwPwURWxKg=="], - "@cornerstonejs/tools": ["@cornerstonejs/tools@4.11.2", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "4.4.2", "lodash.get": "4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "4.11.2", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "3.2.1", "@types/d3-interpolate": "3.0.4", "d3-array": "3.2.4", "d3-interpolate": "3.0.1", "gl-matrix": "3.4.3" } }, "sha512-7opk5t0l3HDSEEa2/PfoiuWBtK6wMdHUjCzy1LY9dVqTpkoqE7YRhNsCuByvd4xgzfp/gijaqlcxS1gyZz0hlw=="], + "@cornerstonejs/tools": ["@cornerstonejs/tools@4.11.3", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "4.4.2", "lodash.get": "4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "4.11.3", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "3.2.1", "@types/d3-interpolate": "3.0.4", "d3-array": "3.2.4", "d3-interpolate": "3.0.1", "gl-matrix": "3.4.3" } }, "sha512-L1H57kP0/mfMsmEKc9VhI8XDWYfuPJ0QfNKax2hfF1lma2w/zBsvkYJYNUqpUTZkDFSFVUjPZTBaPxP/BeXcSA=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -2678,7 +2680,7 @@ "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - "dcmjs": ["dcmjs@0.43.1", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q=="], + "dcmjs": ["dcmjs@0.45.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-MFtACBLQB6vj/3lzHEf6iGBLFC8f/QysoDO+DVUJeLrKYQi4mXYzzBbHmZCpMXqIpj8qXXLvnP/u2ugd9DVu7Q=="], "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index c95167c47..9134ce115 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/core": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/core": "4.11.3", "@kitware/vtk.js": "32.12.0", "react-color": "2.19.3" } diff --git a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts index 45564d7df..12085633f 100644 --- a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts @@ -17,19 +17,25 @@ function _getDisplaySetsFromSeries( servicesManager: AppTypes.ServicesManager, extensionManager ) { - const instance = instances[0]; + utils.sortStudyInstances(instances); + // Choose the LAST instance in the list as the most recently created one. + const instance = instances[instances.length - 1]; const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, - SeriesDescription, + SeriesDescription = '', SeriesNumber, SeriesDate, + SeriesTime, + StructureSetDate, + StructureSetTime, SOPClassUID, wadoRoot, wadoUri, wadoUriRoot, + imageId: predecessorImageId, } = instance; const displaySet = { @@ -39,7 +45,13 @@ function _getDisplaySetsFromSeries( displaySetInstanceUID: utils.guid(), SeriesDescription, SeriesNumber, - SeriesDate, + /** + * The "SeriesDate" for a display set is really the display set date, which + * should be the date of the instance being used, which will be the structure + * set date in this case. + */ + SeriesDate: StructureSetDate || SeriesDate, + SeriesTime: StructureSetTime || SeriesTime, SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID, @@ -54,6 +66,8 @@ function _getDisplaySetsFromSeries( structureSet: null, sopClassUids, instance, + instances, + predecessorImageId, wadoRoot, wadoUriRoot, wadoUri, @@ -62,7 +76,10 @@ function _getDisplaySetsFromSeries( }; let referencedSeriesSequence = instance.ReferencedSeriesSequence; - if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) { + if ( + instance.ReferencedFrameOfReferenceSequence?.RTReferencedStudySequence && + !instance.ReferencedSeriesSequence + ) { instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence( instance.ReferencedFrameOfReferenceSequence ); @@ -70,7 +87,8 @@ function _getDisplaySetsFromSeries( } if (!referencedSeriesSequence) { - throw new Error('ReferencedSeriesSequence is missing for the RTSTRUCT'); + console.error('ReferencedSeriesSequence is missing for the RTSTRUCT'); + return; } const referencedSeries = referencedSeriesSequence[0]; @@ -79,9 +97,13 @@ function _getDisplaySetsFromSeries( displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; const { displaySetService } = servicesManager.services; - const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( - displaySet.referencedSeriesInstanceUID - ); + const referencedDisplaySets = + displaySetService.getDisplaySetsForReferences(referencedSeriesSequence); + if (referencedDisplaySets?.length > 1) { + console.warn( + 'Reference applies to more than 1 display set for Contours, applying only to first display set' + ); + } if (!referencedDisplaySets || referencedDisplaySets.length === 0) { // Instead of throwing error, subscribe to display sets added @@ -97,7 +119,7 @@ function _getDisplaySetsFromSeries( } ); } else { - const referencedDisplaySet = referencedDisplaySets[0]; + const [referencedDisplaySet] = referencedDisplaySets; displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; displaySet.isReconstructable = referencedDisplaySet.isReconstructable; } diff --git a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js index 1642c2865..480d32f0c 100644 --- a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js +++ b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js @@ -89,9 +89,12 @@ async function checkAndLoadContourData({ ROIContour.ContourSequence.forEach((Contour, index) => { const promise = resolvedPromises[index]; if (promise.status === 'fulfilled') { - if (Array.isArray(promise.value) && promise.value.every(Number.isFinite)) { + if ( + Array.isArray(promise.value) && + promise.value.every(it => Number.isFinite(Number(it))) + ) { // If promise.value is already an array of numbers, use it directly - Contour.ContourData = promise.value; + Contour.ContourData = promise.value.map(Number); } else { // If the resolved promise value is a byte array (Blob), it needs to be decoded const uint8Array = new Uint8Array(promise.value); diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 8d70b2701..f225ab303 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -103,7 +103,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { const { unsubscribe } = segmentationService.subscribe( segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, evt => { - if (evt.rtDisplaySet.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) { + if (evt.rtDisplaySet?.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) { setRtIsLoading(false); } diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index 41d9a6b1b..783cc2f1b 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/core": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/core": "4.11.3", "@kitware/vtk.js": "32.12.0", "react-color": "2.19.3" } diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 443b4ce9a..6f300e10b 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -1,5 +1,5 @@ import dcmjs from 'dcmjs'; -import { classes, Types } from '@ohif/core'; +import { classes, Types, utils } from '@ohif/core'; import { cache, metaData } from '@cornerstonejs/core'; import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools'; import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters'; @@ -8,8 +8,6 @@ import { DicomMetadataStore } from '@ohif/core'; import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES'; -const { datasetToBlob } = dcmjs.data; - const getTargetViewport = ({ viewportId, viewportGridService }) => { const { viewports, activeViewportId } = viewportGridService.getState(); const targetViewportId = viewportId || activeViewportId; @@ -27,7 +25,7 @@ const { const { Cornerstone3D: { - RTSS: { generateRTSSFromSegmentations }, + RTSS: { generateRTSSFromRepresentation }, }, } = adaptersRT; @@ -92,6 +90,7 @@ const commandsModule = ({ */ generateSegmentation: ({ segmentationId, options = {} }) => { const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); + const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId; const { imageIds } = segmentation.representationData.Labelmap; @@ -173,12 +172,10 @@ const commandsModule = ({ labelmap3D.metadata[segmentIndex] = segmentMetadata; }); - const generatedSegmentation = generateSegmentation( - referencedImages, - labelmap3D, - metaData, - options - ); + const generatedSegmentation = generateSegmentation(referencedImages, labelmap3D, metaData, { + predecessorImageId, + ...options, + }); return generatedSegmentation; }, @@ -212,24 +209,28 @@ const commandsModule = ({ * @returns {Object|void} Returns the naturalized report if successfully stored, * otherwise throws an error. */ - storeSegmentation: async ({ segmentationId, dataSource }) => { + storeSegmentation: async ({ segmentationId, dataSource, modality = 'SEG' }) => { const segmentation = segmentationService.getSegmentation(segmentationId); if (!segmentation) { throw new Error('No segmentation found'); } - const { label } = segmentation; + const { label, predecessorImageId } = segmentation; const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource()[0]; const { value: reportName, dataSourceName: selectedDataSource, + series, + priorSeriesNumber, action, } = await createReportDialogPrompt({ servicesManager, extensionManager, + predecessorImageId, title: 'Store Segmentation', + modality, }); if (action === PROMPT_RESPONSES.CREATE_REPORT) { @@ -238,12 +239,18 @@ const commandsModule = ({ ? extensionManager.getDataSources(selectedDataSource)[0] : defaultDataSource; - const generatedData = actions.generateSegmentation({ + const args = { segmentationId, options: { - SeriesDescription: reportName || label || 'Research Derived Series', + SeriesDescription: series ? undefined : reportName || label || 'Contour Series', + SeriesNumber: series ? undefined : 1 + priorSeriesNumber, + predecessorImageId: series, }, - }); + }; + const generatedDataAsync = + (modality === 'SEG' && actions.generateSegmentation(args)) || + (modality === 'RTSTRUCT' && actions.generateContour(args)); + const generatedData = await generatedDataAsync; if (!generatedData || !generatedData.dataset) { throw new Error('Error during segmentation generation'); @@ -270,14 +277,9 @@ const commandsModule = ({ } } }, - /** - * Converts segmentations into RTSS for download. - * This sample function retrieves all segentations and passes to - * cornerstone tool adapter to convert to DICOM RTSS format. It then - * converts dataset to downloadable blob. - * - */ - downloadRTSS: async ({ segmentationId }) => { + + generateContour: async args => { + const { segmentationId, options } = args; const segmentations = segmentationService.getSegmentation(segmentationId); // inject colors to the segmentIndex @@ -287,26 +289,34 @@ const commandsModule = ({ segment.color = segmentationService.getSegmentColor( firstRepresentation.viewportId, segmentationId, - segmentIndex + Number(segmentIndex) ); }); + const predecessorImageId = options?.predecessorImageId ?? segmentations.predecessorImageId; + const dataset = await generateRTSSFromRepresentation(segmentations, { + predecessorImageId, + ...options, + }); + return { dataset }; + }, - const RTSS = await generateRTSSFromSegmentations( - segmentations, - classes.MetadataProvider, - DicomMetadataStore - ); + /** + * Downloads an RTSS instance from a segmentation or contour + * representation. + */ + downloadRTSS: async args => { + const { dataset } = await actions.generateContour(args); + const { InstanceNumber: instanceNumber = 1, SeriesInstanceUID: seriesUID } = dataset; try { - const reportBlob = datasetToBlob(RTSS); - //Create a URL for the binary. - const objectUrl = URL.createObjectURL(reportBlob); - window.location.assign(objectUrl); + const filename = `rtss-${seriesUID}-${instanceNumber}.dcm`; + downloadDICOMData(dataset, filename); } catch (e) { console.warn(e); } }, + toggleActiveSegmentationUtility: ({ itemId: buttonId }) => { const { uiState, setUIState } = useUIStateStore.getState(); const isButtonActive = uiState['activeSegmentationUtility'] === buttonId; @@ -321,25 +331,12 @@ const commandsModule = ({ }; const definitions = { - loadSegmentationsForViewport: { - commandFn: actions.loadSegmentationsForViewport, - }, - - generateSegmentation: { - commandFn: actions.generateSegmentation, - }, - downloadSegmentation: { - commandFn: actions.downloadSegmentation, - }, - storeSegmentation: { - commandFn: actions.storeSegmentation, - }, - downloadRTSS: { - commandFn: actions.downloadRTSS, - }, - toggleActiveSegmentationUtility: { - commandFn: actions.toggleActiveSegmentationUtility, - }, + loadSegmentationsForViewport: actions.loadSegmentationsForViewport, + generateSegmentation: actions.generateSegmentation, + downloadSegmentation: actions.downloadSegmentation, + storeSegmentation: actions.storeSegmentation, + downloadRTSS: actions.downloadRTSS, + toggleActiveSegmentationUtility: actions.toggleActiveSegmentationUtility, }; return { diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts index 705a46f11..903d0216a 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts @@ -16,19 +16,24 @@ function _getDisplaySetsFromSeries( servicesManager: AppTypes.ServicesManager, extensionManager ) { - const instance = instances[0]; + utils.sortStudyInstances(instances); + + // Choose the LAST instance in the list as the most recently created one. + const instance = instances[instances.length - 1]; const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, - SeriesDescription, + SeriesDescription = '', SeriesNumber, SeriesDate, + StructureSetDate, SOPClassUID, wadoRoot, wadoUri, wadoUriRoot, + imageId: predecessorImageId, } = instance; const displaySet = { @@ -38,7 +43,7 @@ function _getDisplaySetsFromSeries( displaySetInstanceUID: utils.guid(), SeriesDescription, SeriesNumber, - SeriesDate, + SeriesDate: SeriesDate || StructureSetDate || '', SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID, @@ -53,6 +58,7 @@ function _getDisplaySetsFromSeries( segments: {}, sopClassUids, instance, + predecessorImageId, instances: [instance], wadoRoot, wadoUriRoot, @@ -73,10 +79,16 @@ function _getDisplaySetsFromSeries( displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; const { displaySetService } = servicesManager.services; - const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( - displaySet.referencedSeriesInstanceUID + const referencedDisplaySets = displaySetService.getDisplaySetsForReferences( + instance.ReferencedSeriesSequence ); + if (referencedDisplaySets?.length > 1) { + console.warn( + 'Segmentation does not currently handle references to multiple series, defaulting to first series' + ); + } + const referencedDisplaySet = referencedDisplaySets[0]; if (!referencedDisplaySet) { diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 81e42d2d4..c492b20fe 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -134,7 +134,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { const { unsubscribe } = segmentationService.subscribe( segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, evt => { - if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) { + if (evt.segDisplaySet?.displaySetInstanceUID === segDisplaySet?.displaySetInstanceUID) { setSegIsLoading(false); } diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 126f6baba..44caadeed 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -38,7 +38,7 @@ "@ohif/extension-cornerstone": "3.12.0-beta.90", "@ohif/extension-measurement-tracking": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "classnames": "2.5.1" } } diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index d00969c3b..8ba532414 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -1,12 +1,14 @@ -import { metaData, utilities } from '@cornerstonejs/core'; +import { metaData } from '@cornerstonejs/core'; -import OHIF, { DicomMetadataStore } from '@ohif/core'; +import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; import dcmjs from 'dcmjs'; import { adaptersSR } from '@cornerstonejs/adapters'; import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState'; import hydrateStructuredReport from './utils/hydrateStructuredReport'; +const { downloadBlob } = utils; + const { MeasurementReport } = adaptersSR.Cornerstone3D; const { log } = OHIF; @@ -42,8 +44,6 @@ const _generateReport = (measurementData, additionalFindingTypes, options: Optio dataset.SpecificCharacterSet = 'ISO_IR 192'; } - dataset.InstanceNumber = options.InstanceNumber ?? 1; - return dataset; }; @@ -85,8 +85,7 @@ const commandsModule = (props: withAppTypes) => { const reportBlob = dcmjs.data.datasetToBlob(srDataset); //Create a URL for the binary. - const objectUrl = URL.createObjectURL(reportBlob); - window.location.assign(objectUrl); + downloadBlob(reportBlob, { filename: 'dicom-sr.dcm' }); }, /** diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index e2440fd19..753ec3616 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -100,6 +100,7 @@ function _getDisplaySetsFromSeries( SeriesTime, ConceptNameCodeSequence, SOPClassUID, + imageId: predecessorImageId, } = instance; validateSameStudyUID(instance.StudyInstanceUID, instances); @@ -128,6 +129,7 @@ function _getDisplaySetsFromSeries( isImagingMeasurementReport, sopClassUids, instance, + predecessorImageId, addInstances, label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`, }; @@ -184,6 +186,10 @@ async function _load( srDisplaySet.referencedImages = []; srDisplaySet.measurements = []; } + const { predecessorImageId } = srDisplaySet; + for (const measurement of srDisplaySet.measurements) { + measurement.predecessorImageId = predecessorImageId; + } const mappings = measurementService.getSourceMappings( CORNERSTONE_3D_TOOLS_SOURCE_NAME, diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts index 211697147..c6c674777 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts @@ -54,6 +54,11 @@ export default function hydrateStructuredReport( const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing'); const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + const { + StudyInstanceUID: studyUID, + SeriesInstanceUID: seriesUID, + instance: { SOPInstanceUID: sopUID }, + } = displaySet; // TODO -> We should define a strict versioning somewhere. const mappings = measurementService.getSourceMappings( @@ -67,16 +72,12 @@ export default function hydrateStructuredReport( ); } - const instance = DicomMetadataStore.getInstance( - displaySet.StudyInstanceUID, - displaySet.SeriesInstanceUID, - displaySet.SOPInstanceUID - ); + const instance = DicomMetadataStore.getInstance(studyUID, seriesUID, sopUID); const sopInstanceUIDToImageId = {}; displaySet.measurements.forEach(measurement => { - const { ReferencedSOPInstanceUID, imageId, frameNumber=1 } = measurement; + const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement; const key = `${ReferencedSOPInstanceUID}:${frameNumber}`; if (!sopInstanceUIDToImageId[key]) { @@ -200,6 +201,7 @@ export default function hydrateStructuredReport( const annotation = { annotationUID: toolData.annotation.annotationUID, data: toolData.annotation.data, + predecessorImageId: toolData.predecessorImageId, metadata: { ...referenceData, toolName: annotationType, diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index 178a6ffb4..469c3dc87 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -34,7 +34,7 @@ "@ohif/extension-default": "3.12.0-beta.90", "@ohif/i18n": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -42,8 +42,8 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "classnames": "2.5.1" } } diff --git a/extensions/cornerstone-dynamic-volume/src/commandsModule.ts b/extensions/cornerstone-dynamic-volume/src/commandsModule.ts index b50589056..6a264f2a6 100644 --- a/extensions/cornerstone-dynamic-volume/src/commandsModule.ts +++ b/extensions/cornerstone-dynamic-volume/src/commandsModule.ts @@ -1,8 +1,10 @@ -import * as importedActions from './actions'; import { utilities, Enums } from '@cornerstonejs/tools'; import { cache } from '@cornerstonejs/core'; +import { utils } from '@ohif/core'; -const LABELMAP = Enums.SegmentationRepresentations.Labelmap; +import * as importedActions from './actions'; + +const { downloadCsv } = utils; const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { const services = servicesManager.services; @@ -37,7 +39,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { }); return computedDisplaySets; }, - exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => { + exportTimeReportCSV: ({ segmentations, summaryStats }) => { const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet(); const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID; @@ -203,15 +205,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { // Generate filename and trigger download const filename = `${instance.PatientID}.csv`; - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - link.setAttribute('href', url); - link.setAttribute('download', filename); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + downloadCsv(csvContent, { filename }); }, swapDynamicWithComputedDisplaySet: ({ displaySet }) => { const computedDisplaySet = displaySet; diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 2133e8fdf..3b6de4f7a 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -40,10 +40,10 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/dicom-image-loader": "4.11.2", + "@cornerstonejs/dicom-image-loader": "4.11.3", "@ohif/core": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -53,12 +53,12 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/adapters": "4.11.2", - "@cornerstonejs/ai": "4.11.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/labelmap-interpolation": "4.11.2", - "@cornerstonejs/polymorphic-segmentation": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/adapters": "4.11.3", + "@cornerstonejs/ai": "4.11.3", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/labelmap-interpolation": "4.11.3", + "@cornerstonejs/polymorphic-segmentation": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "32.12.0", "html2canvas": "1.4.1", diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index fc68a3974..53d987ceb 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -1647,15 +1647,13 @@ function commandsModule({ * Stores a segmentation and shows it in the viewport * @param props.segmentationId - The ID of the segmentation to store */ - storeSegmentationCommand: async ({ segmentationId }) => { + storeSegmentationCommand: async args => { + const { segmentationId } = args; const { segmentationService, viewportGridService } = servicesManager.services; const displaySetInstanceUIDs = await createReportAsync({ servicesManager, - getReport: () => - commandsManager.runCommand('storeSegmentation', { - segmentationId, - }), + getReport: () => commandsManager.runCommand('storeSegmentation', args), reportType: 'Segmentation', }); diff --git a/extensions/cornerstone/src/components/ExportSegmentationSubMenuItem.tsx b/extensions/cornerstone/src/components/ExportSegmentationSubMenuItem.tsx index 77419963c..c0798e342 100644 --- a/extensions/cornerstone/src/components/ExportSegmentationSubMenuItem.tsx +++ b/extensions/cornerstone/src/components/ExportSegmentationSubMenuItem.tsx @@ -18,7 +18,7 @@ interface ExportSegmentationSubMenuItemProps { segmentationRepresentationType: string; allowExport: boolean; actions: { - storeSegmentation: (segmentationId: string) => Promise; + storeSegmentation: (segmentationId: string, modality?: string) => Promise; onSegmentationDownloadRTSS: (segmentationId: string) => void; onSegmentationDownload: (segmentationId: string) => void; downloadCSVSegmentationReport: (segmentationId: string) => void; @@ -34,66 +34,39 @@ export const ExportSegmentationSubMenuItem: React.FC - {segmentationRepresentationType === SegmentationRepresentations.Labelmap && ( - - - - {t('Download & Export')} - - - - - - {t('Download')} - - { - e.preventDefault(); - actions.downloadCSVSegmentationReport(segmentationId); - }} - disabled={!allowExport} - > - {t('CSV Report')} - - { - e.preventDefault(); - actions.onSegmentationDownload(segmentationId); - }} - disabled={!allowExport} - > - {t('DICOM SEG')} - - { - e.preventDefault(); - actions.onSegmentationDownloadRTSS(segmentationId); - }} - disabled={!allowExport} - > - {t('DICOM RTSS')} - - - - - {t('Export')} - - { - e.preventDefault(); - actions.storeSegmentation(segmentationId); - }} - disabled={!allowExport} - > - {t('DICOM SEG')} - - - - - )} - {segmentationRepresentationType === SegmentationRepresentations.Contour && ( - <> + + + + {t('Download & Export')} + + + + + + {t('Download')} + + {segmentationRepresentationType === SegmentationRepresentations.Labelmap && ( + { + e.preventDefault(); + actions.downloadCSVSegmentationReport(segmentationId); + }} + disabled={!allowExport} + > + {t('CSV Report')} + + )} + {segmentationRepresentationType === SegmentationRepresentations.Labelmap && ( + { + e.preventDefault(); + actions.onSegmentationDownload(segmentationId); + }} + disabled={!allowExport} + > + {t('DICOM SEG')} + + )} { e.preventDefault(); @@ -101,11 +74,35 @@ export const ExportSegmentationSubMenuItem: React.FC - - {t('Download DICOM RTSS')} + {t('DICOM RTSS')} - - )} - + + + + {t('Export')} + + {segmentationRepresentationType === SegmentationRepresentations.Labelmap && ( + { + e.preventDefault(); + actions.storeSegmentation(segmentationId, 'SEG'); + }} + disabled={!allowExport} + > + {t('DICOM SEG')} + + )} + { + e.preventDefault(); + actions.storeSegmentation(segmentationId, 'RTSTRUCT'); + }} + disabled={!allowExport} + > + {t('DICOM RTSS')} + + + + ); }; diff --git a/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx b/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx index 507688bfa..0fba8c35f 100644 --- a/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx +++ b/extensions/cornerstone/src/customizations/CustomDropdownMenuContent.tsx @@ -46,21 +46,21 @@ export const CustomDropdownMenuContent = () => { segmentationId = activeSegmentationId; } - // Determine if export is allowed for this segmentation - if (exportOptions && segmentationId) { - const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId); - allowExport = exportOption?.isExportable || false; - } - if (!segmentation || !segmentationId) { return null; } + // Determine if export is allowed for this segmentation + if (exportOptions) { + const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId); + allowExport = exportOption?.isExportable || false; + } + const actions = { - storeSegmentation: async segmentationId => { + storeSegmentation: async (segmentationId, modality = 'SEG') => { commandsManager.run({ commandName: 'storeSegmentation', - commandOptions: { segmentationId }, + commandOptions: { segmentationId, modality }, context: 'CORNERSTONE', }); }, diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index 93eabddd5..9decf4cd4 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -461,6 +461,9 @@ const connectMeasurementServiceToTools = ({ const annotationManager = annotation.state.getAnnotationManager(); const newAnnotation = { annotationUID: measurement.uid, + // Not used in CS3D but stored in the CS3D state so that saving + // can apply the predecessor consistently. + predecessorImageId: measurement?.predecessorImageId, highlighted: false, isLocked: false, // This is used to force a re-render of the annotation to diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 015edacea..365172a8f 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -278,11 +278,13 @@ class SegmentationService extends PubSubService { viewportId: string, { segmentationId, + predecessorImageId, type, config, suppressEvents = false, }: { segmentationId: string; + predecessorImageId?: string; type?: csToolsEnums.SegmentationRepresentations; config?: { blendMode?: csEnums.BlendModes; @@ -291,6 +293,9 @@ class SegmentationService extends PubSubService { } ): Promise { const segmentation = this.getSegmentation(segmentationId); + if (segmentation && !segmentation.predecessorImageId && predecessorImageId) { + segmentation.predecessorImageId = predecessorImageId; + } const csViewport = this.getAndValidateViewport(viewportId); if (!csViewport) { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 42ea9b2e3..2f98e528d 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -1090,8 +1090,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi ? csToolsEnums.SegmentationRepresentations.Labelmap : csToolsEnums.SegmentationRepresentations.Contour; + const { predecessorImageId } = displaySet; segmentationService.addSegmentationRepresentation(viewport.id, { segmentationId, + predecessorImageId, type: representationType, }); diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx index 9e08f4d3f..19e6104a2 100644 --- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx +++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx @@ -1,3 +1,4 @@ +import { utils } from '@ohif/core'; import React, { useEffect, useState } from 'react'; import html2canvas from 'html2canvas'; import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core'; @@ -5,6 +6,8 @@ import { ToolGroupManager, segmentation, Enums } from '@cornerstonejs/tools'; import { getEnabledElement as OHIFgetEnabledElement } from '../state'; import { useSystem } from '@ohif/core/src'; +const { downloadUrl } = utils; + const DEFAULT_SIZE = 512; const MAX_TEXTURE_SIZE = 10000; const VIEWPORT_ID = 'cornerstone-viewport-download-form'; @@ -218,10 +221,7 @@ const CornerstoneViewportDownloadForm = ({ } const canvas = await html2canvas(divForDownloadViewport as HTMLElement); - const link = document.createElement('a'); - link.download = `${filename}.${fileType}`; - link.href = canvas.toDataURL(`image/${fileType}`, 1.0); - link.click(); + downloadUrl(canvas.toDataURL(`image/${fileType}`, 1.0)); }; const ViewportDownloadFormNew = customizationService.getCustomization( diff --git a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts index c27404777..170d26265 100644 --- a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts +++ b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts @@ -13,6 +13,7 @@ Object.defineProperty(global, 'URL', { writable: true, value: { createObjectURL: jest.fn().mockReturnValue('mock-url'), + revokeObjectURL: () => undefined, }, }); diff --git a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts index 023f46df6..d4252a6ab 100644 --- a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts +++ b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.ts @@ -1,3 +1,7 @@ +import { utils } from '@ohif/core'; + +const { downloadCsv } = utils; + export function generateSegmentationCSVReport( segmentationData, info: { @@ -121,19 +125,7 @@ export function generateSegmentationCSVReport( csvString += formattedRow.join(',') + '\n'; } - // Create a download link and trigger the download - const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - - link.setAttribute('href', url); - link.setAttribute( - 'download', - `${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv` - ); - link.style.visibility = 'hidden'; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + downloadCsv(csvString, { + filename: `${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv`, + }); } diff --git a/extensions/cornerstone/src/utils/segmentationExportUtils.ts b/extensions/cornerstone/src/utils/segmentationExportUtils.ts index 4977a2c28..dddd4e6a5 100644 --- a/extensions/cornerstone/src/utils/segmentationExportUtils.ts +++ b/extensions/cornerstone/src/utils/segmentationExportUtils.ts @@ -59,7 +59,7 @@ export const hasExportableLabelMapData = ( SeriesInstanceUID ); - return displaySet?.isReconstructable; + return !!displaySet; }; /** diff --git a/extensions/default/package.json b/extensions/default/package.json index fd3ac861c..f0819ff5c 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -36,7 +36,7 @@ "peerDependencies": { "@ohif/core": "3.12.0-beta.90", "@ohif/i18n": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicomweb-client": "0.10.4", "prop-types": "15.8.1", "react": "18.3.1", diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index 6d485dd79..052d0fe28 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -28,7 +28,8 @@ function PanelStudyBrowser({ const { servicesManager, commandsManager, extensionManager } = useSystem(); const { displaySetService, customizationService } = servicesManager.services; const navigate = useNavigate(); - const studyMode = (customizationService.getCustomization('studyBrowser.studyMode') as string) || 'all'; + const studyMode = + (customizationService.getCustomization('studyBrowser.studyMode') as string) || 'all'; const internalImageViewer = useImageViewer(); const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs; @@ -384,7 +385,11 @@ function PanelStudyBrowser({ const displaySetInstanceUID = jumpToDisplaySet; // It is possible to navigate to a study not currently in view - const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs, activeTabName); + const thumbnailLocation = _findTabAndStudyOfDisplaySet( + displaySetInstanceUID, + tabs, + activeTabName + ); if (!thumbnailLocation) { return; } @@ -489,7 +494,7 @@ function _mapDisplaySets(displaySets, displaySetLoadingState, thumbnailImageSrcM seriesNumber: ds.SeriesNumber, modality: ds.Modality, seriesDate: formatDate(ds.SeriesDate), - numInstances: ds.numImageFrames, + numInstances: ds.numImageFrames ?? ds.instances?.length, loadingProgress, countIcon: ds.countIcon, messages: ds.messages, @@ -538,11 +543,13 @@ function _findTabAndStudyOfDisplaySet( tabs: TabsProps, currentTabName: string ) { - const current = tabs.find(tab => tab.name===currentTabName) || tabs[0]; + const current = tabs.find(tab => tab.name === currentTabName) || tabs[0]; const biasedTabs = [current, ...tabs]; for (let t = 0; t < biasedTabs.length; t++) { - const study = biasedTabs[t].studies.find(study => study.displaySets.find(ds => ds.displaySetInstanceUID ===displaySetInstanceUID)); + const study = biasedTabs[t].studies.find(study => + study.displaySets.find(ds => ds.displaySetInstanceUID === displaySetInstanceUID) + ); if (study) { return { tabName: biasedTabs[t].name, diff --git a/extensions/default/src/Panels/createReportDialogPrompt.tsx b/extensions/default/src/Panels/createReportDialogPrompt.tsx index 17a0316ba..6f39153ca 100644 --- a/extensions/default/src/Panels/createReportDialogPrompt.tsx +++ b/extensions/default/src/Panels/createReportDialogPrompt.tsx @@ -1,12 +1,39 @@ import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES'; +/** + * Creates and shows a report dialog prompt. + * The input for this is: + * - `title` shown in the dialog + * - `modality` being stored, used to query existing series + * - `minSeriesNumber` is the start of new series of this modality type. + * Will get set to 4000 if not determined by the modality + * - predecessorImageId is the image id that this series was currently loaded + * from. That allows defaulting the dialog to show the specified series instead + * of always creating a new series. + * + * The response is: + * - `value`, the default name of the object/series being created + * - `dataSourceName`, where to store the object to + * - `series`, is the series to store do, as referenced by a predecessorImageId value. + * - `priorSeriesNumber` is the previously lowest series number at least minSeriesNumber + * of all the seris of the given modality type. + * + * This should be provided to the DICOM encoder, which will get the predecessor + * sequence from the metaData provider so that the saved series will replace + * the existing instance in the same series. + * This will be falsy for a new series. + */ export default function CreateReportDialogPrompt({ title = 'Create Report', + modality = 'SR', + minSeriesNumber = 0, + predecessorImageId, extensionManager, servicesManager, }): Promise<{ value: string; dataSourceName: string; + priorSeriesNumber?: number; series: string; action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES]; }> { @@ -16,18 +43,33 @@ export default function CreateReportDialogPrompt({ const allowMultipleDataSources = window.config.allowMultiSelectExport; - return new Promise(function (resolve, reject) { + minSeriesNumber ||= + (modality === 'SR' && 3000) || + (modality === 'SEG' && 3100) || + (modality === 'RTSTRUCT' && 3200) || + 4000; + + return new Promise(function (resolve) { uiDialogService.show({ id: 'report-dialog', title, content: ReportDialog, contentProps: { dataSources: allowMultipleDataSources ? dataSources : undefined, - onSave: async ({ reportName, dataSource: selectedDataSource, series }) => { + predecessorImageId, + minSeriesNumber, + modality, + onSave: async ({ + reportName, + dataSource: selectedDataSource, + series, + priorSeriesNumber, + }) => { resolve({ value: reportName, dataSourceName: selectedDataSource, series, + priorSeriesNumber, action: PROMPT_RESPONSES.CREATE_REPORT, }); }, diff --git a/extensions/default/src/customizations/reportDialogCustomization.tsx b/extensions/default/src/customizations/reportDialogCustomization.tsx index 2f740f711..36c25f787 100644 --- a/extensions/default/src/customizations/reportDialogCustomization.tsx +++ b/extensions/default/src/customizations/reportDialogCustomization.tsx @@ -11,28 +11,44 @@ type DataSource = { type ReportDialogProps = { dataSources: DataSource[]; + modality?: string; + predecessorImageId?: string; hide: () => void; - onSave: (data: { reportName: string; dataSource: string | null; series: string | null }) => void; + onSave: (data: { + reportName: string; + dataSource: string | null; + series: string | null; + priorSeriesNumber: number; + }) => void; onCancel: () => void; }; -function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) { +function ReportDialog({ + dataSources, + modality = 'SR', + predecessorImageId, + minSeriesNumber = 3000, + hide, + onSave, + onCancel, +}: ReportDialogProps) { const { servicesManager } = useSystem(); const [selectedDataSource, setSelectedDataSource] = useState( dataSources?.[0]?.value ?? null ); - const [selectedSeries, setSelectedSeries] = useState(null); - const [reportName, setReportName] = useState(''); - const { displaySetService } = servicesManager.services; + const [selectedSeries, setSelectedSeries] = useState(predecessorImageId || null); + const [reportName, setReportName] = useState(''); + const seriesOptions = useMemo(() => { const displaySetsMap = displaySetService.getDisplaySetCache(); const displaySets = Array.from(displaySetsMap.values()); const options = displaySets - .filter(ds => ds.Modality === 'SR') + .filter(ds => ds.Modality === modality) .map(ds => ({ - value: ds.SeriesInstanceUID, + value: ds.predecessorImageId || ds.SeriesInstanceUID, + seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber, description: ds.SeriesDescription, label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`, })); @@ -41,11 +57,12 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps { value: null, description: null, + seriesNumber: minSeriesNumber, label: 'Create new series', }, ...options, ]; - }, [displaySetService]); + }, [displaySetService, modality]); useEffect(() => { const seriesOption = seriesOptions.find(s => s.value === selectedSeries); @@ -58,6 +75,7 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps onSave({ reportName, dataSource: selectedDataSource, + priorSeriesNumber: Math.max(...seriesOptions.map(it => it.seriesNumber)), series: selectedSeries, }); hide(); diff --git a/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js b/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js index d5329a427..8c27139f9 100644 --- a/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js +++ b/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js @@ -32,7 +32,8 @@ export default function getDisplaySetsFromUnsupportedSeries(instances) { SOPClassUID: instance.SOPClassUID, SeriesDescription: instance.SeriesDescription || '', Modality: instance.Modality, - numImageFrames: instances.length, + instances, + instance: instances[instance.length - 1], unsupported: true, SOPClassHandlerId: 'unsupported', isReconstructable: false, diff --git a/extensions/default/src/utils/promptSaveReport.tsx b/extensions/default/src/utils/promptSaveReport.tsx index d53cca6e2..ac219bf6a 100644 --- a/extensions/default/src/utils/promptSaveReport.tsx +++ b/extensions/default/src/utils/promptSaveReport.tsx @@ -3,8 +3,6 @@ import { utils } from '@ohif/core'; import createReportAsync from '../Actions/createReportAsync'; import { createReportDialogPrompt } from '../Panels'; import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES'; -import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber'; -import { getSeriesDateTime } from './getCurrentDicomDateTime'; const { filterAnd, filterMeasurementsByStudyUID, filterMeasurementsBySeriesUID } = utils.MeasurementFilters; @@ -27,9 +25,14 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan } = ctx; let displaySetInstanceUIDs; + const measurementData = measurementService.getMeasurements(measurementFilter); + const predecessorImageId = findPredecessorImageId(measurementData); + try { const promptResult = await createReportDialogPrompt({ title: defaultSaveTitle, + predecessorImageId, + minSeriesNumber: 3000, extensionManager, servicesManager, }); @@ -37,16 +40,9 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) { const dataSources = extensionManager.getDataSources(promptResult.dataSourceName); const dataSource = dataSources[0]; - const measurementData = measurementService.getMeasurements(measurementFilter); - const SeriesDescription = promptResult.value || defaultSaveTitle; - - const { SeriesNumber, InstanceNumber, referenceDisplaySet } = getSRSeriesAndInstanceNumber({ - displaySetService, - SeriesInstanceUid: promptResult.series, - }); - - const { SeriesDate, SeriesTime } = referenceDisplaySet ?? getSeriesDateTime(); + const { series, priorSeriesNumber, value: reportName } = promptResult; + const SeriesDescription = reportName || defaultSaveTitle; const getReport = async () => { return commandsManager.runCommand( @@ -57,11 +53,8 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan additionalFindingTypes: ['ArrowAnnotate'], options: { SeriesDescription, - SeriesNumber, - InstanceNumber, - SeriesInstanceUID: promptResult.series, - SeriesDate, - SeriesTime, + SeriesNumber: 1 + priorSeriesNumber, + predecessorImageId: series, }, }, 'CORNERSTONE_STRUCTURED_REPORT' @@ -90,4 +83,20 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan } } +export function findPredecessorImageId(annotations) { + let predecessorImageId; + for (const annotation of annotations) { + if ( + predecessorImageId && + annotation.predecessorImageId && + annotation.predecessorImageId !== predecessorImageId + ) { + console.warn('Found multiple source predecessors, not defaulting to same series'); + return; + } + predecessorImageId ||= annotation.predecessorImageId; + } + return predecessorImageId; +} + export default promptSaveReport; diff --git a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx index ed367456e..ecb794932 100644 --- a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx +++ b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { callInputDialog } from '@ohif/extension-default'; -import { ExtensionManager, CommandsManager, DicomMetadataStore } from '@ohif/core'; +import { ExtensionManager, CommandsManager, DicomMetadataStore, utils } from '@ohif/core'; import { DataRow } from '@ohif/ui-next'; import { withTranslation, WithTranslation } from 'react-i18next'; import { EVENTS as MicroscopyEvents } from '../../services/MicroscopyService'; import dcmjs from 'dcmjs'; import constructSR from '../../utils/constructSR'; -import { saveByteArray } from '../../utils/saveByteArray'; + +const { downloadDicom } = utils; let saving = false; const { datasetToBuffer } = dcmjs.data; @@ -197,7 +198,7 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) { if (dataSource.wadoRoot == 'saveDicom') { // download as DICOM file const part10Buffer = datasetToBuffer(dataset); - saveByteArray(part10Buffer, `sr-microscopy.dcm`); + downloadDicom(part10Buffer, { filename: `sr-microscopy.dcm` }); } else { // Save into Web Data source const { StudyInstanceUID } = dataset; @@ -277,11 +278,11 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) { * Handler for "Edit" action of an annotation item * @param param0 */ - const onMeasurementItemEditHandler = ({ uid, isActive }: { uid: string; isActive: boolean }) => { + const onMeasurementItemEditHandler = ({ uid }: { uid: string; isActive: boolean }) => { props.commandsManager.runCommand('setLabel', { uid }, 'MICROSCOPY'); }; - const onMeasurementDeleteHandler = ({ uid, isActive }: { uid: string; isActive: boolean }) => { + const onMeasurementDeleteHandler = ({ uid }: { uid: string; isActive: boolean }) => { const roiAnnotation = microscopyService.getAnnotation(uid); microscopyService.removeAnnotation(roiAnnotation); }; diff --git a/extensions/dicom-microscopy/src/utils/saveByteArray.ts b/extensions/dicom-microscopy/src/utils/saveByteArray.ts deleted file mode 100644 index 123f1c17f..000000000 --- a/extensions/dicom-microscopy/src/utils/saveByteArray.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Trigger file download from an array buffer - * @param buffer - * @param filename - */ -export function saveByteArray(buffer: ArrayBuffer, filename: string) { - const blob = new Blob([buffer], { type: 'application/dicom' }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = filename; - link.click(); -} diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index 01e70b024..e05a59ffc 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@ohif/core": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index e1e41bdb1..b1aee7a97 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@ohif/core": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 769092d51..2d07bd82f 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,14 +32,14 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "@ohif/core": "3.12.0-beta.90", "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.90", "@ohif/extension-default": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", "classnames": "2.5.1", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "lodash.debounce": "4.0.8", "prop-types": "15.8.1", "react": "18.3.1", diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 635457f2a..575ed7c16 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -95,7 +95,7 @@ export default function PanelStudyBrowserTracking({ seriesNumber: ds.SeriesNumber, modality: ds.Modality, seriesDate: ds.SeriesDate ? new Date(ds.SeriesDate).toLocaleDateString() : '', - numInstances: ds.numImageFrames, + numInstances: ds.numImageFrames ?? ds.instances?.length, loadingProgress, countIcon: ds.countIcon, messages: ds.messages, diff --git a/extensions/test-extension/package.json b/extensions/test-extension/package.json index 01658c223..7f2032f0a 100644 --- a/extensions/test-extension/package.json +++ b/extensions/test-extension/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@ohif/core": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/tmtv/package.json b/extensions/tmtv/package.json index 1f17c7625..894b87c27 100644 --- a/extensions/tmtv/package.json +++ b/extensions/tmtv/package.json @@ -32,7 +32,7 @@ "peerDependencies": { "@ohif/core": "3.12.0-beta.90", "@ohif/ui": "3.12.0-beta.90", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js b/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js index 8ef216945..acc5fa13c 100644 --- a/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js +++ b/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js @@ -1,3 +1,7 @@ +import { utils } from '@ohif/core'; + +const { downloadCsv } = utils; + export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) { const firstReport = segReport[Object.keys(segReport)[0]]; const columns = Object.keys(firstReport); @@ -44,14 +48,7 @@ export default function createAndDownloadTMTVReport(segReport, additionalReportR csv.push(temp.join(',')); }); - const blob = new Blob([csv.join('\n')], { - type: 'text/csv;charset=utf-8', + downloadCsv(csv.join('\n'), { + filename: options.filename ?? `${firstReport.PatientID}_tmtv.csv`, }); - - const url = URL.createObjectURL(blob); - - const a = document.createElement('a'); - a.href = url; - a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`; - a.click(); } diff --git a/extensions/usAnnotation/package.json b/extensions/usAnnotation/package.json index 545db0f2a..5e7699172 100644 --- a/extensions/usAnnotation/package.json +++ b/extensions/usAnnotation/package.json @@ -42,8 +42,8 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "@ohif/core": "3.12.0-beta.90", "@ohif/extension-cornerstone": "3.12.0-beta.90", "@ohif/extension-default": "3.12.0-beta.90", diff --git a/extensions/usAnnotation/src/getCommandsModule.ts b/extensions/usAnnotation/src/getCommandsModule.ts index 629afc11c..22fdf04b7 100644 --- a/extensions/usAnnotation/src/getCommandsModule.ts +++ b/extensions/usAnnotation/src/getCommandsModule.ts @@ -1,9 +1,11 @@ import { UltrasoundPleuraBLineTool, Enums as csToolsEnums } from '@cornerstonejs/tools'; -import { Types as OhifTypes } from '@ohif/core'; +import { Types as OhifTypes, utils } from '@ohif/core'; import { eventTarget, triggerEvent, utilities } from '@cornerstonejs/core'; import getInstanceByImageId from './getInstanceByImageId'; import { setShowPercentage } from './PleuraBlinePercentage'; +const { downloadBlob } = utils; + const { transformWorldToIndex } = utilities; /** @@ -13,7 +15,6 @@ const { transformWorldToIndex } = utilities; */ function commandsModule({ servicesManager, - commandsManager, }: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { const { viewportGridService, toolGroupService, cornerstoneViewportService } = servicesManager.services as AppTypes.Services; @@ -275,14 +276,9 @@ function commandsModule({ // Create a blob with the JSON data const blob = new Blob([jsonString], { type: 'application/json' }); - - // Create a URL for the blob - const url = URL.createObjectURL(blob); - - // Create an anchor element - const a = document.createElement('a'); - a.href = url; - a.download = `ultrasound_annotations_${new Date().toISOString().slice(0, 10)}.json`; + downloadBlob(blob, { + filename: `ultrasound_annotations_${new Date().toISOString().slice(0, 10)}.json`, + }); // Append to the document, click to download, and remove document.body.appendChild(a); diff --git a/modes/basic/package.json b/modes/basic/package.json index e1f9c2149..e8e4ebdf3 100644 --- a/modes/basic/package.json +++ b/modes/basic/package.json @@ -5,6 +5,7 @@ "author": "OHIF Contributors", "license": "MIT", "main": "dist/ohif-mode-basic.js", + "module": "src/index.tsx", "files": [ "dist/**", "public/**", @@ -14,7 +15,6 @@ "keywords": [ "ohif-mode" ], - "module": "src/index.tsx", "engines": { "node": ">=14", "npm": ">=6", diff --git a/modes/basic/src/index.tsx b/modes/basic/src/index.tsx index 5b999f5a8..9e61fbb64 100644 --- a/modes/basic/src/index.tsx +++ b/modes/basic/src/index.tsx @@ -21,12 +21,17 @@ export const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', thumbnailList: '@ohif/extension-default.panelModule.seriesList', + hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', wsiSopClassHandler: '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', }; export const cornerstone = { measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', + labelMapSegmentationPanel: + '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap', + contourSegmentationPanel: + '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour', segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation', viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', }; @@ -62,6 +67,11 @@ export const dicomRT = { sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', }; +export const segmentation = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', +}; + export const extensionDependencies = { // Can derive the versions at least process.env.from npm_package_version '@ohif/extension-default': '^3.0.0', diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json index e2e202399..246249de5 100644 --- a/modes/longitudinal/package.json +++ b/modes/longitudinal/package.json @@ -42,7 +42,8 @@ "@ohif/extension-default": "3.12.0-beta.90", "@ohif/extension-dicom-pdf": "3.12.0-beta.90", "@ohif/extension-dicom-video": "3.12.0-beta.90", - "@ohif/extension-measurement-tracking": "3.12.0-beta.90" + "@ohif/extension-measurement-tracking": "3.12.0-beta.90", + "@ohif/mode-basic": "3.12.0-beta.90" }, "dependencies": { "@babel/runtime": "7.28.2", diff --git a/modes/segmentation/.webpack/webpack.prod.js b/modes/segmentation/.webpack/webpack.prod.js index 3cf74adf9..513e1194d 100644 --- a/modes/segmentation/.webpack/webpack.prod.js +++ b/modes/segmentation/.webpack/webpack.prod.js @@ -33,6 +33,12 @@ const config = { amd: '@ohif/core', root: '@ohif/core', }, + '@ohif/mode-basic': { + commonjs2: '@ohif/mode-basic', + commonjs: '@ohif/mode-basic', + amd: '@ohif/mode-basic', + root: '@ohif/mode-basic', + }, '@ohif/ui': { commonjs2: '@ohif/ui', commonjs: '@ohif/ui', diff --git a/modes/segmentation/package.json b/modes/segmentation/package.json index 4e8acbacb..9ad4143d3 100644 --- a/modes/segmentation/package.json +++ b/modes/segmentation/package.json @@ -35,6 +35,7 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { + "@ohif/mode-basic": "3.12.0-beta.90", "@ohif/core": "3.12.0-beta.90", "@ohif/extension-cornerstone": "3.12.0-beta.90", "@ohif/extension-cornerstone-dicom-rt": "3.12.0-beta.90", diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 18eae732e..51f08e2d7 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -2,42 +2,8 @@ import { id } from './id'; import toolbarButtons from './toolbarButtons'; import initToolGroups from './initToolGroups'; import setUpAutoTabSwitchHandler from './utils/setUpAutoTabSwitchHandler'; - -const ohif = { - layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', - sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', - hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', - leftPanel: '@ohif/extension-default.panelModule.seriesList', -}; - -const cornerstone = { - viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', - labelMapSegmentationPanel: - '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap', - contourSegmentationPanel: - '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour', - measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', -}; - -const segmentation = { - sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', - viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', -}; - -const dicomRT = { - viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt', - sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', -}; -/** - * Just two dependencies to be able to render a viewport with panels in order - * to make sure that the mode is working. - */ -const extensionDependencies = { - '@ohif/extension-default': '^3.0.0', - '@ohif/extension-cornerstone': '^3.0.0', - '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', - '@ohif/extension-cornerstone-dicom-rt': '^3.0.0', -}; +import { ohif, cornerstone, extensionDependencies, dicomRT, segmentation } from '@ohif/mode-basic'; +export * from './toolbarButtons'; function modeFactory({ modeConfiguration }) { const _unsubscriptions = []; @@ -236,7 +202,7 @@ function modeFactory({ modeConfiguration }) { return { id: ohif.layout, props: { - leftPanels: [ohif.leftPanel], + leftPanels: [ohif.thumbnailList], leftPanelResizable: true, rightPanels: [ cornerstone.labelMapSegmentationPanel, diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts index 69360f4a6..d8750a74e 100644 --- a/modes/segmentation/src/toolbarButtons.ts +++ b/modes/segmentation/src/toolbarButtons.ts @@ -20,7 +20,7 @@ const callbacks = (toolName: string) => [ }, ]; -const toolbarButtons: Button[] = [ +export const toolbarButtons: Button[] = [ { id: 'AdvancedRenderingControls', uiType: 'ohif.advancedRenderingControls', diff --git a/modes/usAnnotation/package.json b/modes/usAnnotation/package.json index 3f6394e9b..7c4bf94cb 100644 --- a/modes/usAnnotation/package.json +++ b/modes/usAnnotation/package.json @@ -34,8 +34,8 @@ }, "dependencies": { "@babel/runtime": "7.28.2", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/tools": "4.11.2", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/tools": "4.11.3", "@ohif/core": "3.12.0-beta.90", "@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.90", "@ohif/extension-ultrasound-pleura-bline": "3.12.0-beta.90", diff --git a/package.json b/package.json index 04e71e07c..385aa23b0 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ "cli": "node ./platform/cli/src/index.js", "build:ui:deploy-preview": "lerna run build:ui:deploy-preview --stream", "build:demo": "lerna run build:viewer:demo --stream", - "build:package-all": "lerna run build:package --parallel --stream", - "build:package-all-1": "lerna run build:package-1 --parallel --stream", + "build:package-all": "lerna run build:package --stream --concurrency 4", + "build:package-all-1": "lerna run build:package-1 --stream --concurrency 4", "dev:fast": "cd platform/app && yarn run dev:fast", "show:config": "echo Config is $APP_CONFIG on $PUBLIC_URL", "dev": "lerna run dev:viewer --stream", @@ -47,7 +47,8 @@ "orthanc:up": "docker compose -f platform/app/.recipes/Nginx-Orthanc/docker-compose.yml up", "install:dev": "cp -f yarn.lock addOns/yarn.lock && cd addOns && yarn install --frozen-lockfile --modules-folder ../node_modules", "install:update-lockfile": "yarn install && bun install --config=./bunfig.update-lockfile.toml", - "install:yarn:frozen-lockfile": "yarn install --frozen-lockfile", + "install:yarn:frozen-lockfile": "echo Deprecated - use install:frozen && yarn install --frozen-lockfile", + "install:frozen": "yarn install --frozen-lockfile", "audit": "bun audit --ignore=GHSA-5j98-mcp5-4vw2", "preinstall": "node preinstall.js", "start": "yarn run dev", @@ -138,7 +139,7 @@ "commander": "8.3.0", "cross-env": "7.0.3", "cross-spawn": "7.0.6", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "path-to-regexp": "0.1.12", "nth-check": "2.1.1", "trim-newlines": "5.0.0", diff --git a/platform/app/package.json b/platform/app/package.json index 3dbc4bf7f..04598225d 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -53,7 +53,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/dicom-image-loader": "4.11.2", + "@cornerstonejs/dicom-image-loader": "4.11.3", "@emotion/serialize": "1.3.3", "@ohif/core": "3.12.0-beta.90", "@ohif/extension-cornerstone": "3.12.0-beta.90", @@ -79,7 +79,7 @@ "classnames": "2.5.1", "core-js": "3.45.1", "cornerstone-math": "0.1.10", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "detect-gpu": "4.0.50", "dicom-parser": "1.8.21", "dotenv-webpack": "1.8.0", diff --git a/platform/app/public/config/kheops.js b/platform/app/public/config/kheops.js index 56f2214f9..4a92cf514 100644 --- a/platform/app/public/config/kheops.js +++ b/platform/app/public/config/kheops.js @@ -126,6 +126,29 @@ window.config = { }, }, }, + { + friendlyName: 'StaticWado default data', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'DCM4CHEE', + wadoUriRoot: '/dicomweb', + qidoRoot: '/dicomweb', + wadoRoot: '/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, { namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy', diff --git a/platform/core/package.json b/platform/core/package.json index bd1b9b7f1..4485f0a5a 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,15 +37,15 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/core": "4.11.2", - "@cornerstonejs/dicom-image-loader": "4.11.2", + "@cornerstonejs/core": "4.11.3", + "@cornerstonejs/dicom-image-loader": "4.11.3", "@ohif/ui": "3.12.0-beta.90", "cornerstone-math": "0.1.9", "dicom-parser": "1.8.21" }, "dependencies": { "@babel/runtime": "7.28.2", - "dcmjs": "0.43.1", + "dcmjs": "0.45.0", "dicomweb-client": "0.10.4", "gl-matrix": "3.4.3", "immutability-helper": "3.1.1", diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts index 541d9ff30..1b8d43085 100644 --- a/platform/core/src/classes/MetadataProvider.ts +++ b/platform/core/src/classes/MetadataProvider.ts @@ -6,7 +6,7 @@ import DicomMetadataStore from '../services/DicomMetadataStore'; import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import toNumber from '../utils/toNumber'; import combineFrameInstance from '../utils/combineFrameInstance'; -import formatPN from '../utils/formatPN'; + const { calibratedPixelSpacingMetadataProvider, getPixelSpacingInformation } = utilities; class MetadataProvider { @@ -123,33 +123,21 @@ class MetadataProvider { switch (wadoImageLoaderTag) { case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE: - const { SeriesDate, SeriesTime } = instance; - - let seriesDate; - let seriesTime; - - if (SeriesDate) { - seriesDate = dicomParser.parseDA(SeriesDate); - } - - if (SeriesTime) { - seriesTime = dicomParser.parseTM(SeriesTime); - } - metadata = { modality: instance.Modality, seriesInstanceUID: instance.SeriesInstanceUID, seriesNumber: toNumber(instance.SeriesNumber), studyInstanceUID: instance.StudyInstanceUID, - seriesDate, - seriesTime, + seriesDescription: instance.SeriesDescription, + seriesDate: instance.SeriesDate, + seriesTime: instance.SeriesTime, }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE: metadata = { - patientAge: toNumber(instance.PatientAge), - patientSize: toNumber(instance.PatientSize), - patientWeight: toNumber(instance.PatientWeight), + patientAge: instance.PatientAge, + patientSize: instance.PatientSize, + patientWeight: instance.PatientWeight, }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_DEMOGRAPHIC_MODULE: @@ -346,16 +334,13 @@ class MetadataProvider { break; case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE: - const { PatientName } = instance; - - let patientName; - if (PatientName) { - patientName = formatPN(PatientName); - } - metadata = { - patientName, + patientName: instance.PatientName, patientId: instance.PatientID, + patientSex: instance.PatientSex, + patientBirthDate: instance.PatientBirthDate, + issuerOfPatientId: instance.IssuerOfPatientID, + otherPatientIDsSequence: instance.OtherPatientIDsSequence, }; break; @@ -373,9 +358,11 @@ class MetadataProvider { case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE: metadata = { studyDescription: instance.StudyDescription, + studyInstanceUID: instance.StudyInstanceUID, studyDate: instance.StudyDate, studyTime: instance.StudyTime, accessionNumber: instance.AccessionNumber, + studyId: instance.StudyID, }; break; @@ -512,9 +499,57 @@ class MetadataProvider { const metadataProvider = new MetadataProvider(); +DicomMetadataStore.setMetaDataProvider(metadataProvider); + export default metadataProvider; +const WADO_IMAGE_LOADER_TAGS = { + // dicomImageLoader specific + GENERAL_SERIES_MODULE: 'generalSeriesModule', + PATIENT_STUDY_MODULE: 'patientStudyModule', + IMAGE_PIXEL_MODULE: 'imagePixelModule', + VOI_LUT_MODULE: 'voiLutModule', + MODALITY_LUT_MODULE: 'modalityLutModule', + SOP_COMMON_MODULE: 'sopCommonModule', + PET_IMAGE_MODULE: 'petImageModule', + PET_ISOTOPE_MODULE: 'petIsotopeModule', + PET_SERIES_MODULE: 'petSeriesModule', + OVERLAY_PLANE_MODULE: 'overlayPlaneModule', + PATIENT_DEMOGRAPHIC_MODULE: 'patientDemographicModule', + + // react-cornerstone-viewport specific + PATIENT_MODULE: 'patientModule', + GENERAL_IMAGE_MODULE: 'generalImageModule', + GENERAL_STUDY_MODULE: 'generalStudyModule', + CINE_MODULE: 'cineModule', + CALIBRATION_MODULE: 'calibrationModule', + + // Computed tags for new data + // Note these get returned in naturalized format + IMAGE_SOP_INSTANCE_REFERENCE: 'ImageSopInstanceReference', +}; + const WADO_IMAGE_LOADER = { + /** Returns information on the current frame reference */ + frameModule: instance => { + const { + frameNumber = 1, + numberOfFrames = 1, + SOPClassUID: sopClassUID, + SOPInstanceUID: sopInstanceUID, + SeriesInstanceUID: seriesInstanceUID, + StudyInstanceUID: studyInstanceUID, + } = instance; + return { + frameNumber, + numberOfFrames, + sopClassUID, + sopInstanceUID, + seriesInstanceUID, + studyInstanceUID, + }; + }, + imagePlaneModule: instance => { const { ImageOrientationPatient, ImagePositionPatient } = instance; @@ -590,26 +625,4 @@ const WADO_IMAGE_LOADER = { }, }; -const WADO_IMAGE_LOADER_TAGS = { - // dicomImageLoader specific - GENERAL_SERIES_MODULE: 'generalSeriesModule', - PATIENT_STUDY_MODULE: 'patientStudyModule', - IMAGE_PIXEL_MODULE: 'imagePixelModule', - VOI_LUT_MODULE: 'voiLutModule', - MODALITY_LUT_MODULE: 'modalityLutModule', - SOP_COMMON_MODULE: 'sopCommonModule', - PET_IMAGE_MODULE: 'petImageModule', - PET_ISOTOPE_MODULE: 'petIsotopeModule', - PET_SERIES_MODULE: 'petSeriesModule', - OVERLAY_PLANE_MODULE: 'overlayPlaneModule', - PATIENT_DEMOGRAPHIC_MODULE: 'patientDemographicModule', - - // react-cornerstone-viewport specific - PATIENT_MODULE: 'patientModule', - GENERAL_IMAGE_MODULE: 'generalImageModule', - GENERAL_STUDY_MODULE: 'generalStudyModule', - CINE_MODULE: 'cineModule', - CALIBRATION_MODULE: 'calibrationModule', -}; - const INSTANCE = 'instance'; diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts index ac6cfd84d..eb321ed68 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts @@ -50,6 +50,12 @@ const _model = { studies: [], }; +let metaDataProvider; + +function _setMetaDataProvider(metaData) { + metaDataProvider = metaData; +} + function _getStudyInstanceUIDs() { return _model.studies.map(aStudy => aStudy.StudyInstanceUID); } @@ -102,7 +108,17 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) { return series.getInstance(SOPInstanceUID); } +/** + * Gets the frame module from the OHIF metadata provider, and then + * uses the study/series/instance uids to get the instance data. + */ function _getInstanceByImageId(imageId) { + const metadataResult = metaDataProvider?.get('frameModule', imageId); + if (metadataResult) { + const { studyInstanceUID, seriesInstanceUID, sopInstanceUID } = metadataResult; + return _getInstance(studyInstanceUID, seriesInstanceUID, sopInstanceUID); + } + console.warn('No metadata result found for', imageId, 'looking through instances'); for (const study of _model.studies) { for (const series of study.series) { for (const instance of series.instances) { @@ -186,7 +202,8 @@ const BaseImplementation = { study = _model.studies[_model.studies.length - 1]; } - naturalizedDataset = numberOfFrames > 1 ? addProxyFields(naturalizedDataset) : naturalizedDataset; + naturalizedDataset = + numberOfFrames > 1 ? addProxyFields(naturalizedDataset) : naturalizedDataset; study.addInstanceToSeries(naturalizedDataset); }, @@ -289,6 +306,7 @@ const BaseImplementation = { getInstance: _getInstance, getInstanceByImageId: _getInstanceByImageId, updateMetadataForSeries: _updateMetadataForSeries, + setMetaDataProvider: _setMetaDataProvider, }; const DicomMetadataStore = Object.assign( // get study diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts index 3bcb79f0a..2bef0070f 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.ts +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts @@ -1,5 +1,5 @@ import { ExtensionManager } from '../../extensions'; -import { DisplaySet, InstanceMetadata } from '../../types'; +import { DisplaySet, InstanceMetadata, ReferencedSeriesSequence } from '../../types'; import { PubSubService } from '../_shared/pubSubServiceInterface'; import EVENTS from './EVENTS'; @@ -115,16 +115,52 @@ export default class DisplaySetService extends PubSubService { return this.activeDisplaySets; } + /** + * Gets the set of display sets with this series instance UID + * + * WARNING: Do not use this method when you have a referenced series sequence + * as this method does NOT check sop instances. Instead, use getDisplaySetsForReference + * to get those with the correct sop instances in them. + */ public getDisplaySetsForSeries = (seriesInstanceUID: string): DisplaySet[] => { return [...displaySetCache.values()].filter( displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID ); }; + /** + * Given a reference to a series/sop, returns the set of display sets + * containing an instance from the references. + */ + public getDisplaySetsForReferences = ( + references: ReferencedSeriesSequence | ReferencedSeriesSequence[] + ): DisplaySet[] => { + const mapSeriesReferences = new Map>(); + const referenceArr = Array.isArray(references) ? references : [references]; + for (const seriesRef of referenceArr) { + const { SeriesInstanceUID, ReferencedInstanceSequence } = seriesRef; + if (!mapSeriesReferences.has(SeriesInstanceUID)) { + mapSeriesReferences.set(SeriesInstanceUID, new Set()); + } + const sops = mapSeriesReferences.get(SeriesInstanceUID); + for (const sopReference of ReferencedInstanceSequence) { + sops.add(sopReference.ReferencedSOPInstanceUID); + } + } + + return [...displaySetCache.values()].filter(displaySet => { + const sopReferences = mapSeriesReferences.get(displaySet.SeriesInstanceUID); + if (!sopReferences || !displaySet.instances) { + return; + } + return displaySet.instances.some(instance => sopReferences.has(instance.SOPInstanceUID)); + }); + }; + public getDisplaySetForSOPInstanceUID( sopInstanceUID: string, seriesInstanceUID: string, - frameNumber?: number + _frameNumber?: number ): DisplaySet { const displaySets = seriesInstanceUID ? this.getDisplaySetsForSeries(seriesInstanceUID) diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 15fba1920..97f978181 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -434,10 +434,13 @@ class MeasurementService extends PubSubService { const internalUID = data.uid || guid(); - const annotationData = data.annotation.data; + const { + annotation: { predecessorImageId, data: annotationData }, + } = data; const newMeasurement = { finding: annotationData.finding, + predecessorImageId, findingSites: annotationData.findingSites, site: annotationData.findingSites?.[0], ...measurement, @@ -643,8 +646,12 @@ class MeasurementService extends PubSubService { if (referencedImageId) { measurement.referencedImageId = referencedImageId; const instance = DicomMetadataStore.getInstanceByImageId(referencedImageId); - measurement.referenceStudyUID = instance.StudyInstanceUID; - measurement.referenceSeriesUID = instance.SeriesInstanceUID; + if (!instance) { + console.warn("Didn't find instance for", referencedImageId); + } else { + measurement.referenceStudyUID = instance.StudyInstanceUID; + measurement.referenceSeriesUID = instance.SeriesInstanceUID; + } } if (displaySetInstanceUID) { @@ -683,8 +690,7 @@ class MeasurementService extends PubSubService { removeMany(measurementUIDs: string[]): void { const measurements = []; for (const measurementUID of measurementUIDs) { - const measurement = - this.measurements.get(measurementUID) + const measurement = this.measurements.get(measurementUID); if (!measurementUID || !measurement) { console.debug(`No uid provided, or unable to find measurement by uid.`); @@ -727,8 +733,7 @@ class MeasurementService extends PubSubService { */ public jumpToMeasurement(viewportId: string, measurementUID: string): void { - const measurement = - this.measurements.get(measurementUID) + const measurement = this.measurements.get(measurementUID); if (!measurement) { log.warn(`No measurement uid, or unable to find by uid.`); @@ -850,8 +855,7 @@ class MeasurementService extends PubSubService { }; public toggleLockMeasurement(measurementUID: string): void { - const measurement = - this.measurements.get(measurementUID) + const measurement = this.measurements.get(measurementUID); if (!measurement) { console.debug(`No measurement found for uid: ${measurementUID}`); @@ -868,8 +872,7 @@ class MeasurementService extends PubSubService { } public toggleVisibilityMeasurement(measurementUID: string, visibility?: boolean): void { - const measurement = - this.measurements.get(measurementUID) + const measurement = this.measurements.get(measurementUID); if (!measurement) { console.debug(`No measurement found for uid: ${measurementUID}`); @@ -893,8 +896,7 @@ class MeasurementService extends PubSubService { } public updateColorMeasurement(measurementUID: string, color: number[]): void { - const measurement = - this.measurements.get(measurementUID) + const measurement = this.measurements.get(measurementUID); if (!measurement) { console.debug(`No measurement found for uid: ${measurementUID}`); diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts index 7eee952ec..9e41f2a1c 100644 --- a/platform/core/src/types/DisplaySet.ts +++ b/platform/core/src/types/DisplaySet.ts @@ -1,5 +1,15 @@ import { InstanceMetadata } from './StudyMetadata'; +export type ReferencedInstance = { + ReferencedSOPClassUID: string; + ReferencedSOPInstanceUID: string; +}; + +export type ReferencedSeriesSequence = { + SeriesInstanceUID: string; + ReferencedInstanceSequence: ReferencedInstance[]; +}; + export type DisplaySet = { displaySetInstanceUID: string; instances: InstanceMetadata[]; @@ -16,6 +26,8 @@ export type DisplaySet = { label?: string; /** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */ isOverlayDisplaySet?: boolean; + /** Flag indicating this is a derived dataset */ + isDerived?: boolean; /** flag indicating if it supports window level */ supportsWindowLevel?: boolean; @@ -49,6 +61,21 @@ export type DisplaySet = { SeriesTime?: string; instance?: InstanceMetadata; + /** + * The predecessor image id refers to the SOP instance that is currently loaded + * into this display set for SEG/SR/RTSTRUCT type values. The name is chosen + * for consistency when this value is used as the origin instance + * for saving a new instance intended to replace this instance where the + * new instance has a "predecessor sequence". + */ + predecessorImageId?: string; + + /** + * isLoaded is used for display sets containing a load operation that + * is required before the display set can be shown. This is separate from + * isHydrated, which means it is loaded into view. + */ + isLoaded?: boolean; isHydrated?: boolean; isRehydratable?: boolean; }; diff --git a/platform/core/src/utils/addProxyFields.js b/platform/core/src/utils/addProxyFields.js index c60eb9941..2d5dfb798 100644 --- a/platform/core/src/utils/addProxyFields.js +++ b/platform/core/src/utils/addProxyFields.js @@ -44,6 +44,13 @@ export function addProxyFields(instance) { enumerable: true, get: () => { return instance._parentInstance?.[fieldProxy] ?? instance._parentInstance?._shared?.[fieldProxy]; + }, + set: (value) => { + Object.defineProperty(instance,fieldProxy, { + writable: true, + enumerable: true, + value, + }) } }); } diff --git a/platform/core/src/utils/downloadBlob.ts b/platform/core/src/utils/downloadBlob.ts new file mode 100644 index 000000000..753886470 --- /dev/null +++ b/platform/core/src/utils/downloadBlob.ts @@ -0,0 +1,36 @@ +/** + * Converts a blob to a URL and downloads immediate + */ +export function downloadBlob(content, options?) { + const url = URL.createObjectURL(content); + downloadUrl(url, options); + URL.revokeObjectURL(url); +} + +/** + * Trigger file download from an array buffer + * @param buffer + * @param filename + */ +export function downloadDicom(buffer: ArrayBuffer, options) { + const blob = new Blob([buffer], { type: 'application/dicom' }); + downloadBlob(blob, options); +} + +/** + * Downloads a URL + */ +export function downloadUrl(url, options?) { + const link = document.createElement('a'); + link.setAttribute('href', url); + const filename = options?.filename || 'file.dcm'; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +export function downloadCsv(csvString: string, options?) { + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); + downloadBlob(blob, options); +} diff --git a/platform/core/src/utils/downloadCSVReport.js b/platform/core/src/utils/downloadCSVReport.js index 70adf9f3b..3c7026f9a 100644 --- a/platform/core/src/utils/downloadCSVReport.js +++ b/platform/core/src/utils/downloadCSVReport.js @@ -1,3 +1,4 @@ +import { downloadUrl } from './downloadBlob'; import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore'; import formatPN from './formatPN'; @@ -97,10 +98,5 @@ function _getCommonRowItems(measurement, seriesMetadata) { function _createAndDownloadFile(csvContent) { const encodedUri = encodeURI(csvContent); - - const link = document.createElement('a'); - link.setAttribute('href', encodedUri); - link.setAttribute('download', 'MeasurementReport.csv'); - document.body.appendChild(link); - link.click(); + downloadUrl(encodedUri, { filename: 'MeasurementReport.csv' }); } diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts index e624dacee..4ba124602 100644 --- a/platform/core/src/utils/index.ts +++ b/platform/core/src/utils/index.ts @@ -48,6 +48,8 @@ import areAllImageOrientationsEqual from './areAllImageOrientationsEqual'; import { structuredCloneWithFunctions } from './structuredCloneWithFunctions'; import { buildButtonCommands } from './buildButtonCommands'; +import { downloadBlob, downloadUrl, downloadCsv, downloadDicom } from './downloadBlob'; + // Commented out unused functionality. // Need to implement new mechanism for derived displaySets using the displaySetManager. @@ -99,6 +101,10 @@ const utils = { getClosestOrientationFromIOP, calculateScanAxisNormal, areAllImageOrientationsEqual, + downloadBlob, + downloadUrl, + downloadCsv, + downloadDicom, }; export { @@ -137,6 +143,10 @@ export { MeasurementFilters, getClosestOrientationFromIOP, buildButtonCommands, + downloadBlob, + downloadUrl, + downloadCsv, + downloadDicom, }; export default utils; diff --git a/platform/docs/docs/migration-guide/3p11-to-3p12/index.md b/platform/docs/docs/migration-guide/3p11-to-3p12/index.md index d24ccf66a..48db144c5 100644 --- a/platform/docs/docs/migration-guide/3p11-to-3p12/index.md +++ b/platform/docs/docs/migration-guide/3p11-to-3p12/index.md @@ -24,3 +24,45 @@ Using the class text size with the ui-button is inconsistent as to whether it will apply or not. Instead, create a new size value to assign the desired size. To support this, a new size enum is created, smallTall, which is used in the worklist for an over-ride. + +## `createReportDialogPrompt` + +The create report dialog prompt (which is MOSTLY an internal component) has +change the API a bit. The input is now: + + - `title` shown in the dialog + - `modality` being stored, used to query existing series + - `minSeriesNumber` is the start of new series of this modality type. + Will get set to 4000 if not determined by the modality + - predecessorImageId is the image id that this series was currently loaded + from. That allows defaulting the dialog to show the specified series instead + of always creating a new series. + +The minSeries and predecessor are both optional, so the input doesn't have to be +updated. + +The output has been enhanced with: + + - `series`, is the series to store do, as referenced by a predecessorImageId value. + This allows exactly specifying which item to replace, which allows selecting it in the + menu by default instead of just guessing what value is being replaced. + - `priorSeriesNumber` is the previously lowest series number at least minSeriesNumber + of all the seris of the given modality type. This allows adding an instance to the + `next` series number by adding 1 to this value. + +The priorSeriesNumber will default to 4000 for an unknown modality type, or +3000 for sr, 3100 for seg and 3200 for rtstruct. + +## metadataProvider and formatted metadata + +The metadata provider has been formatting some fields, which causes inconsistency +between different metadata providers and use of the instance object. The +specific fields that have changed are: + +- `patientName` +- `studyDate`, `studyTime` +- `seriesDate`, `seriesTime` + +If these fields need formatted versions, it is recommended to add a secondary/computed +metadata provider which simply gets the base metadata module and adds the +formatting. That way different metadata providers are all handled identically. diff --git a/platform/docs/versioned_docs/version-3.11/platform/extensions/modules/sop-class-handler.md b/platform/docs/versioned_docs/version-3.11/platform/extensions/modules/sop-class-handler.md index 5d8757e57..4145b8c68 100644 --- a/platform/docs/versioned_docs/version-3.11/platform/extensions/modules/sop-class-handler.md +++ b/platform/docs/versioned_docs/version-3.11/platform/extensions/modules/sop-class-handler.md @@ -39,6 +39,31 @@ return { } ``` +## Replacing Instances in Series + +Series such as segmentations and annotations often have the concept of replacing +an earlier instance with a newer one. The official way to do this is using the +`predecessor document sequence` to reference the old object in the new one. +It isn't that the old one is deleted (never delete data), but rather that the new +one is used in preference to the old one. This results in code that looks like: + +``` + utils.sortStudyInstances(instances); + // Choose the LAST instance in the list as the most recently created one. + const instance = instances[instances.length - 1]; + + ... + const displaySet = { + ... + predecessorImageId: instance.imageId, + numImageFrames: instances.length, + } +``` + +so that the last/latest SOP instance is used by default, and that it is +recorded what imageId was used to load that instance. This then allows saving +new instances in the same series by using a new predecessor sequence. + ## Example SOP Class Handler Module ```js diff --git a/yarn.lock b/yarn.lock index b6a98e9cf..d53ee4ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1280,25 +1280,25 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@cornerstonejs/adapters@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-4.11.2.tgz#f33a4321f53722c30fcf8e08bf76e894ae5c5e5e" - integrity sha512-yYZd4W+BnM9So0SsPYYjYN+cho5FBlXIOg0o3JX+gQGoguSk9UedRfd5xa7f7TYoN4ect7RdU4ytwajdabRY5Q== +"@cornerstonejs/adapters@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-4.11.3.tgz#72b594804b14926d4d5e6fddcf4b2947ce42a94d" + integrity sha512-N0nKn7ZKBr4ryvveSy19h8EnyhAUyYkdmn23KibPrF84VjzFfK99xMIFWYHxxhYWfCJwzcGf7RR4LIop3FD60Q== dependencies: "@babel/runtime-corejs2" "7.26.10" buffer "6.0.3" - dcmjs "0.43.1" + dcmjs "0.45.0" gl-matrix "3.4.3" ndarray "1.0.19" -"@cornerstonejs/ai@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-4.11.2.tgz#e55085c6daa5a804ae64123b385c2aa565425761" - integrity sha512-aUfcRidr8/48Szw9MPK4MFXZPhoPlABxOXltx6pZHFe7da3KE1WYoSLD/Dw0T72AtqV2w8blXQuNpplTL4ErAg== +"@cornerstonejs/ai@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-4.11.3.tgz#2d670f7a2cb2762deb903575f3df4240cad4be82" + integrity sha512-qHrFOuM8iYtCX683+Nii9fBkBvgPQA2x+/9jbZpV17YPHAxUSy5gi6a+3ISMwVzKtFhIVhVZK+JMKAAOcCtVVQ== dependencies: "@babel/runtime-corejs2" "7.26.10" buffer "6.0.3" - dcmjs "0.43.1" + dcmjs "0.45.0" gl-matrix "3.4.3" lodash.clonedeep "4.5.0" ndarray "1.0.19" @@ -1330,20 +1330,20 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.7.tgz#784394d05d8cf735640e9c3baa206535ee8e0376" integrity sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w== -"@cornerstonejs/core@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-4.11.2.tgz#9ba3559d1cddc3fe9f50ec6f6ebc6f803622e39a" - integrity sha512-8Lq3hq+k31C4KbU0sGfUEP/ORn/6nqVYioXvQG4iU+SpLbbQVyhUgEqtQPIKoZVsGaV2mX4HOU3fWXAmMfP4fQ== +"@cornerstonejs/core@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-4.11.3.tgz#bf5fa0b3d187ae00789ebe4ff994b43be6a9bfcd" + integrity sha512-72yGsvuf5XezQ2nV6JPIOPMlIRtXzuABn1JgpDpoCYbjqbrmApL6G0nLswj0Tuo2dmh4E3iS80KSVVggjllcPg== dependencies: "@kitware/vtk.js" "32.12.1" comlink "4.4.2" gl-matrix "3.4.3" loglevel "1.9.2" -"@cornerstonejs/dicom-image-loader@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-4.11.2.tgz#2cb74fa20dc349317a037a96011d7f5ed44facee" - integrity sha512-xIWbAOrkVBgpBpcqlyYIL2fiAZ8KRF453Ao6svDRixoL7Fl2gvhnpJGbG3wWCYhHaPXbYpbhmfzIovT9qZ7ACg== +"@cornerstonejs/dicom-image-loader@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-4.11.3.tgz#552615e35db0dc64ce8ed19eb5aa784574ac9f76" + integrity sha512-SULpwXEpHF0cINusoTehwUpzQOyxakSTrZ+h4WTLu9GKosxxoIPorw3/eQpsJR9tUBxNnrvgSPFkXXGBdEq4Cw== dependencies: "@cornerstonejs/codec-charls" "1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "1.2.2" @@ -1355,25 +1355,25 @@ pako "2.1.0" uuid "9.0.1" -"@cornerstonejs/labelmap-interpolation@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-4.11.2.tgz#4868455c0c5084e0f54d1d0292fa9cde681ff243" - integrity sha512-gS0Gt07U9hDzjoD75I3fKcXRM47sEWsAI706L23/3Q0nN3DGz5NDDySIDo8E+IfZDdSJeX2yBO38RqszVlTkcA== +"@cornerstonejs/labelmap-interpolation@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-4.11.3.tgz#963cb0a912f40536f38caa839ba68b394a4f801b" + integrity sha512-yqIvR+7x8HC1l6cMgTBx75LMQf3ZMXdsfiBbOl4mYOlRiiQ5tEcx+8anU+kllwEhsWThu5MjY7s5ZSRybyks0g== dependencies: "@itk-wasm/morphological-contour-interpolation" "1.1.0" itk-wasm "1.0.0-b.165" -"@cornerstonejs/polymorphic-segmentation@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-4.11.2.tgz#228bd205d0aef2d5cf8d13d4b8811839ccf54fc2" - integrity sha512-cW1kVQOiwzWnsHdQOi6wi/k7ji3+qC1sqWb7WgFqf7uTuBbab4Oh0KO6ODcg3G0SOWhzQCPT+GcZf7tsgQh2Tw== +"@cornerstonejs/polymorphic-segmentation@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-4.11.3.tgz#d0ea417a1de482bd377480a7b574e9ea02c3f251" + integrity sha512-RXKP26mEcQLzpC1xArr/QpMq+minkjO4W02jhhJxmGdTTVypyFyq3Kr9tPJUhosM91yssUHa5mIIHwPwURWxKg== dependencies: "@icr/polyseg-wasm" "0.4.0" -"@cornerstonejs/tools@4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-4.11.2.tgz#185cb573895c0a604868cb42777f22f3b8158841" - integrity sha512-7opk5t0l3HDSEEa2/PfoiuWBtK6wMdHUjCzy1LY9dVqTpkoqE7YRhNsCuByvd4xgzfp/gijaqlcxS1gyZz0hlw== +"@cornerstonejs/tools@4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-4.11.3.tgz#8677a31b91e7834b82fce53f7efc0075cce5dad0" + integrity sha512-L1H57kP0/mfMsmEKc9VhI8XDWYfuPJ0QfNKax2hfF1lma2w/zBsvkYJYNUqpUTZkDFSFVUjPZTBaPxP/BeXcSA== dependencies: "@types/offscreencanvas" "2019.7.3" comlink "4.4.2" @@ -7179,10 +7179,10 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== -dcmjs@0.43.1, dcmjs@^0.41.0: - version "0.43.1" - resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.43.1.tgz#03223e56d52e9ec2c3b8c10afe9200d754befe62" - integrity sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q== +dcmjs@0.45.0, dcmjs@^0.41.0: + version "0.45.0" + resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.45.0.tgz#ce53dd06fc345e8231e28c340a36a8094a62422a" + integrity sha512-MFtACBLQB6vj/3lzHEf6iGBLFC8f/QysoDO+DVUJeLrKYQi4mXYzzBbHmZCpMXqIpj8qXXLvnP/u2ugd9DVu7Q== dependencies: "@babel/runtime-corejs3" "^7.22.5" adm-zip "^0.5.10"