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
This commit is contained in:
Bill Wallace 2025-11-21 13:07:26 -05:00 committed by GitHub
parent 9c5043ca2c
commit 57205703bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 962 additions and 648 deletions

View File

@ -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",

402
bun.lock
View File

@ -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=="],

View File

@ -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"
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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"
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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);
}

View File

@ -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"
}
}

View File

@ -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' });
},
/**

View File

@ -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,

View File

@ -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,

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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",

View File

@ -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',
});

View File

@ -18,7 +18,7 @@ interface ExportSegmentationSubMenuItemProps {
segmentationRepresentationType: string;
allowExport: boolean;
actions: {
storeSegmentation: (segmentationId: string) => Promise<void>;
storeSegmentation: (segmentationId: string, modality?: string) => Promise<unknown>;
onSegmentationDownloadRTSS: (segmentationId: string) => void;
onSegmentationDownload: (segmentationId: string) => void;
downloadCSVSegmentationReport: (segmentationId: string) => void;
@ -34,66 +34,39 @@ export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuIt
const { t } = useTranslation('SegmentationTable');
return (
<>
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download & Export')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Download className="h-5 w-5" />
<span className="pl-1">{t('Download')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.downloadCSVSegmentationReport(segmentationId);
}}
disabled={!allowExport}
>
{t('CSV Report')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownload(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownloadRTSS(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM RTSS')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Export className="h-5 w-5" />
<span className="pl-1 pt-1">{t('Export')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.storeSegmentation(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)}
{segmentationRepresentationType === SegmentationRepresentations.Contour && (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download & Export')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Download className="h-5 w-5" />
<span className="pl-1">{t('Download')}</span>
</DropdownMenuLabel>
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.downloadCSVSegmentationReport(segmentationId);
}}
disabled={!allowExport}
>
{t('CSV Report')}
</DropdownMenuItem>
)}
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownload(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={e => {
e.preventDefault();
@ -101,11 +74,35 @@ export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuIt
}}
disabled={!allowExport}
>
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download DICOM RTSS')}</span>
{t('DICOM RTSS')}
</DropdownMenuItem>
</>
)}
</>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Export className="h-5 w-5" />
<span className="pl-1 pt-1">{t('Export')}</span>
</DropdownMenuLabel>
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.storeSegmentation(segmentationId, 'SEG');
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.storeSegmentation(segmentationId, 'RTSTRUCT');
}}
disabled={!allowExport}
>
{t('DICOM RTSS')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
};

View File

@ -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',
});
},

View File

@ -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

View File

@ -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<void> {
const segmentation = this.getSegmentation(segmentationId);
if (segmentation && !segmentation.predecessorImageId && predecessorImageId) {
segmentation.predecessorImageId = predecessorImageId;
}
const csViewport = this.getAndValidateViewport(viewportId);
if (!csViewport) {

View File

@ -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,
});

View File

@ -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(

View File

@ -13,6 +13,7 @@ Object.defineProperty(global, 'URL', {
writable: true,
value: {
createObjectURL: jest.fn().mockReturnValue('mock-url'),
revokeObjectURL: () => undefined,
},
});

View File

@ -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`,
});
}

View File

@ -59,7 +59,7 @@ export const hasExportableLabelMapData = (
SeriesInstanceUID
);
return displaySet?.isReconstructable;
return !!displaySet;
};
/**

View File

@ -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",

View File

@ -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,

View File

@ -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,
});
},

View File

@ -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<string | null>(
dataSources?.[0]?.value ?? null
);
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
const [reportName, setReportName] = useState('');
const { displaySetService } = servicesManager.services;
const [selectedSeries, setSelectedSeries] = useState<string | null>(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();

View File

@ -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,

View File

@ -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;

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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();
}

View File

@ -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",

View File

@ -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);

View File

@ -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",

View File

@ -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',

View File

@ -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",

View File

@ -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',

View File

@ -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",

View File

@ -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,

View File

@ -20,7 +20,7 @@ const callbacks = (toolName: string) => [
},
];
const toolbarButtons: Button[] = [
export const toolbarButtons: Button[] = [
{
id: 'AdvancedRenderingControls',
uiType: 'ohif.advancedRenderingControls',

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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',

View File

@ -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",

View File

@ -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';

View File

@ -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

View File

@ -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
*
* <b>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.</b>
*/
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<string, Set<string>>();
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<string>());
}
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)

View File

@ -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}`);

View File

@ -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;
};

View File

@ -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,
})
}
});
}

View File

@ -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);
}

View File

@ -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' });
}

View File

@ -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;

View File

@ -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.

View File

@ -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

View File

@ -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"