diff --git a/.circleci/config.yml b/.circleci/config.yml index 567ef3fd99..6f730ae4b1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,22 +13,13 @@ defaults: &defaults commands: install_bun: steps: - - restore_cache: - keys: - - bun-cache-v2-{{ arch }}-latest - run: name: Install Bun command: | - if [ ! -d "$HOME/.bun" ]; then - curl -fsSL https://bun.sh/install | bash - fi + curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.23" echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV source $BASH_ENV - - save_cache: - key: bun-cache-v2-{{ arch }}-latest - paths: - - ~/.bun jobs: CHECKOUT: diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a3eefd2bab..b35e1434b2 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -24,6 +24,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.2.23 - uses: actions/setup-node@v4 with: node-version: 20 # Or your desired Node version diff --git a/.github/workflows/docusaurus-build.yml b/.github/workflows/docusaurus-build.yml index db1311cbb2..d234e5dbbe 100644 --- a/.github/workflows/docusaurus-build.yml +++ b/.github/workflows/docusaurus-build.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.2.23 - name: Install root dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 22411a3351..b4e7781e8c 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.2.23 - name: Install dependencies run: bun install diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 434f499c3c..48637afd49 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,6 +18,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.2.23 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc82425a38..ac5d011093 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest # Or pin to a specific version like '1.1.18' + bun-version: 1.2.23 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d6753fcb..ddc2a50092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,126 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +### Bug Fixes + +- Wrong position on rehydrate annotation ([#2404](https://github.com/cornerstonejs/cornerstone3D/issues/2404)) ([1a84852](https://github.com/cornerstonejs/cornerstone3D/commit/1a84852e1448bed31925a623c2dbd09f0cab23fb)) + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package root + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +### Bug Fixes + +- planar freehand roi perimeter calculation ([#2341](https://github.com/cornerstonejs/cornerstone3D/issues/2341)) ([d3b607c](https://github.com/cornerstonejs/cornerstone3D/commit/d3b607c2596aec33d93cb7f2834c01ca2783b412)) + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +### Bug Fixes + +- **CrosshairTool:** Fix "setToolCenter" to apply the modification to the viewports camera ([#2402](https://github.com/cornerstonejs/cornerstone3D/issues/2402)) ([4028207](https://github.com/cornerstonejs/cornerstone3D/commit/402820739678f8b9db0b2bddd8c03dc447fabc85)) + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +### Bug Fixes + +- set volume id on creation ([#2345](https://github.com/cornerstonejs/cornerstone3D/issues/2345)) ([#2396](https://github.com/cornerstonejs/cornerstone3D/issues/2396)) ([a47b125](https://github.com/cornerstonejs/cornerstone3D/commit/a47b125193743290c8c412cea85c990738c339ab)) + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +### Bug Fixes + +- **dicomImageLoader:** :bug: set usingDefaultValues in ImagePlane metadata module in the WadoUri loader ([#2391](https://github.com/cornerstonejs/cornerstone3D/issues/2391)) ([d0ea9fd](https://github.com/cornerstonejs/cornerstone3D/commit/d0ea9fdbd3af2df8dc538a5bba15ea8a241dfb0e)) + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +### Bug Fixes + +- **segmentation:** Lock added contour annotations when segment is locked ([#2399](https://github.com/cornerstonejs/cornerstone3D/issues/2399)) ([15cfdd9](https://github.com/cornerstonejs/cornerstone3D/commit/15cfdd93ffdc55dfa70f15bec8cb1be15d1290df)) + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package root + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +### Bug Fixes + +- **segmentation:** Lock contour annotations when segment is locked. ([#2395](https://github.com/cornerstonejs/cornerstone3D/issues/2395)) ([83b4092](https://github.com/cornerstonejs/cornerstone3D/commit/83b4092d76c6e115adfd12a50d399e96483622a4)) + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +### Bug Fixes + +- **MagnifyTool:** Fix MagnifyTool freeze when pressing right click ([#2388](https://github.com/cornerstonejs/cornerstone3D/issues/2388)) ([a13fce2](https://github.com/cornerstonejs/cornerstone3D/commit/a13fce2d65aa1d5b8de849ddf6255e3d42dc841a)) + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +### Bug Fixes + +- update toLowHighRange function to handle SIGMOID function bounds as a temporary fix ([#2394](https://github.com/cornerstonejs/cornerstone3D/issues/2394)) ([662a8b3](https://github.com/cornerstonejs/cornerstone3D/commit/662a8b3c7b3268557e9787fd6abc67e923ab731b)) + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package root + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +### Bug Fixes + +- **bun:** Use bun version 1.2.23. ([#2390](https://github.com/cornerstonejs/cornerstone3D/issues/2390)) ([222e3a8](https://github.com/cornerstonejs/cornerstone3D/commit/222e3a8d7ea0297a4be327df03510d0ab77d938f)) + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +### Bug Fixes + +- Updated BasicStatsCalculator to change from private to protected ([#2382](https://github.com/cornerstonejs/cornerstone3D/issues/2382)) ([89a8db5](https://github.com/cornerstonejs/cornerstone3D/commit/89a8db54bec5b96a61624dcca65464982d5e40b3)) + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +### Bug Fixes + +- **interpolation:** Fixed contour segmentation interpolation for spline contours. ([#2381](https://github.com/cornerstonejs/cornerstone3D/issues/2381)) ([7d61b2c](https://github.com/cornerstonejs/cornerstone3D/commit/7d61b2c7cd1627c34b0547967c5616d01fc29f96)) + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +### Bug Fixes + +- clear preview segmentation when AI tools are disabled ([#2373](https://github.com/cornerstonejs/cornerstone3D/issues/2373)) ([cfc232c](https://github.com/cornerstonejs/cornerstone3D/commit/cfc232c0472aa89eba859d2e2da97af58ca50bfc)) + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +### Bug Fixes + +- **segmentation:** Added SEGMENTATION_REPRESENTATION_MODIFIED event listener to LabelmapEditWithContour tool to ensure a contour representation is added for the tool. ([#2377](https://github.com/cornerstonejs/cornerstone3D/issues/2377)) ([563fc6c](https://github.com/cornerstonejs/cornerstone3D/commit/563fc6c6b8edf987b115b540d4069a2238b88fee)) + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +### Bug Fixes + +- limit pan off viewport ([#2375](https://github.com/cornerstonejs/cornerstone3D/issues/2375)) ([1850916](https://github.com/cornerstonejs/cornerstone3D/commit/18509162c91b4c2be08c61d95ce75dbeeb4329f2)) + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +### Bug Fixes + +- **segmentation:** For contour segmentation logical operators, avoid updating segment color and label when they are not defined/specified. ([#2376](https://github.com/cornerstonejs/cornerstone3D/issues/2376)) ([969369a](https://github.com/cornerstonejs/cornerstone3D/commit/969369a623ea39ba9740e547fb94867183ec396b)) + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +### Features + +- **BrushTool:** segmentation brush interpolation ([#2374](https://github.com/cornerstonejs/cornerstone3D/issues/2374)) ([690ec6e](https://github.com/cornerstonejs/cornerstone3D/commit/690ec6e90499b24364c06ad404fd0d2b0c521134)) + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +### Bug Fixes + +- ellipse ROI stats ([#2368](https://github.com/cornerstonejs/cornerstone3D/issues/2368)) ([c2c6c99](https://github.com/cornerstonejs/cornerstone3D/commit/c2c6c99fc7846677ad28a36f7697fce7744080e8)) + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) ### Bug Fixes diff --git a/README.md b/README.md index 246b19b31a..23554a6d6c 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,28 @@ Read our guide on [How-to Contribute](https://cornerstonejs.org/docs/category/co ### License Cornerstone is [MIT licensed](./LICENSE). + + +## Development notes + +For developing locally and instantly testing with another test project, we need +to link this repository to the other project. This can be done as follows: + +- In this project: + +``` +yarn install +yarn build +``` + +- In the test project: `npm install /path/to/this/repository` +- In this project: `npm link` +- In the test project: `npm link cornerstone-core` + + +To immediately reload the test project when something changes here (e.g. in the +tools package), run the build server as follow: + +``` +yarn workspace @cornerstonejs/tools build:esm:watch +``` diff --git a/commit.txt b/commit.txt index 8c34d8f4ba..68dee94cb2 100644 --- a/commit.txt +++ b/commit.txt @@ -1 +1 @@ -d859a20051ed92b625d6e71b92d44c2ef9ced79e \ No newline at end of file +1a84852e1448bed31925a623c2dbd09f0cab23fb \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index ec872ce94f..e89e948a06 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,7 +1,33 @@ +// @ts-check const path = require('path'); +const os = require('os'); process.env.CHROME_BIN = require('puppeteer').executablePath(); +/** + * + * Tests for the dicomImageLoaders require support for Web Workers and loading + * wasm files required for image decoding. + * + * In order to support this, the karma config requires some customisation. This + * is based on + * https://github.com/codymikol/karma-webpack/issues/498#issuecomment-790040818 + * + * The changes are: + * - Define a custom output path for webpack to emit files to + * - Serve the output path via a `files` entry in the karma config + * + * Without this, webpack correctly bundles and outputs the worker and wasm + * files, but they can't be loaded by the tests. Trying to load the worker or + * wasm files returns a 404. + * + * Manually create an output path. This is the same as the default + * karma-webpack config + * https://github.com/codymikol/karma-webpack?tab=readme-ov-file#default-webpack-configuration + */ +const outputPath = path.join(os.tmpdir(), '_karma_webpack_') + Math.floor(Math.random() * 1000000) + +/** @param {import('karma').Config} config */ module.exports = function (config) { config.set({ reporters: ['junit', 'coverage', 'spec'], @@ -54,7 +80,28 @@ module.exports = function (config) { files: [ 'packages/core/test/**/*_test.js', 'packages/tools/test/**/*_test.js', + // Serve dicomImageLoad test images + { + pattern: 'packages/dicomImageLoader/testImages/**/*', + watched: false, + included: false, + served: true + }, + /** + * Required to allow karma to load wasm and worker files built via webpack. + * See the comment at the top of this file for more details. + */ + { + pattern: `${outputPath}/**/*`, + included: false, + served: true, + watched: false + } ], + proxies: { + // Simplified path to access test images in tests + '/testImages/': '/base/packages/dicomImageLoader/testImages/', + }, preprocessors: { 'packages/core/test/**/*_test.js': ['webpack'], 'packages/tools/test/**/*_test.js': ['webpack'], @@ -73,6 +120,15 @@ module.exports = function (config) { webpack: { devtool: 'eval-source-map', mode: 'development', + output: { + /** + * Override default karma-webpack output path with the one we defined + * above this allows webpack generated files including wasm and workers + * to be served by karma without this, the default config won't allow + * tests to load web workers or wasm files. + */ + path: outputPath, + }, module: { rules: [ { @@ -115,7 +171,7 @@ module.exports = function (config) { ], }, experiments: { - asyncWebAssembly: true, + asyncWebAssembly: true }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], @@ -126,6 +182,7 @@ module.exports = function (config) { alias: { '@cornerstonejs/core': path.resolve('packages/core/src/index'), '@cornerstonejs/tools': path.resolve('packages/tools/src/index'), + '@cornerstonejs/dicom-image-loader': path.resolve('packages/dicomImageLoader/src/index'), }, }, }, diff --git a/lerna.json b/lerna.json index d83755fe84..5acbbaa857 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "4.4.1", + "version": "4.5.19", "packages": [ "packages/core", "packages/tools", diff --git a/packages/adapters/CHANGELOG.md b/packages/adapters/CHANGELOG.md index 0109e6c9c1..104c92a3a3 100644 --- a/packages/adapters/CHANGELOG.md +++ b/packages/adapters/CHANGELOG.md @@ -3,6 +3,92 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +### Bug Fixes + +- Wrong position on rehydrate annotation ([#2404](https://github.com/cornerstonejs/cornerstone3D/issues/2404)) ([1a84852](https://github.com/cornerstonejs/cornerstone3D/commit/1a84852e1448bed31925a623c2dbd09f0cab23fb)) + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/adapters + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/adapters + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/adapters + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/adapters diff --git a/packages/adapters/package.json b/packages/adapters/package.json index a64211ddc5..28ccdcfe9c 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/adapters", - "version": "4.4.1", + "version": "4.5.19", "description": "Adapters for Cornerstone3D to/from formats including DICOM SR and others", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", @@ -89,7 +89,7 @@ "ndarray": "1.0.19" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1", - "@cornerstonejs/tools": "4.4.1" + "@cornerstonejs/core": "4.5.19", + "@cornerstonejs/tools": "4.5.19" } } diff --git a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts index 59e3c06349..f3ea820fe8 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/MeasurementReport.ts @@ -309,11 +309,13 @@ export default class MeasurementReport { metadata }) { const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence; - const { ReferencedSOPInstanceUID, ReferencedFrameNumber } = + const { ReferencedSOPInstanceUID, ReferencedFrameNumber = 1 } = ReferencedSOPSequence; const referencedImageId = - sopInstanceUIDToImageIdMap[ReferencedSOPInstanceUID]; + sopInstanceUIDToImageIdMap[ + `${ReferencedSOPInstanceUID}:${ReferencedFrameNumber}` + ]; const imagePlaneModule = metadata.get( "imagePlaneModule", referencedImageId diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts index 0b27f29998..dc58959408 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.ts @@ -370,6 +370,7 @@ export function insertPixelDataPlanar({ const imageIdIndex = imageIdMaps.indices[imageId]; const labelmapImage = labelMapImages[imageIdIndex]; const labelmap2DView = labelmapImage.getPixelData(); + const imageVoxelManager = labelmapImage.voxelManager; const data = alignedPixelDataI.data; @@ -400,7 +401,16 @@ export function insertPixelDataPlanar({ }) ); } - labelmap2DView[x] = segmentIndex; + if (imageVoxelManager) { + // Ensure voxelManager updates boundaries + imageVoxelManager.setAtIndex( + x, + segmentIndex + ); + } else { + // Directly assign pixel data when volume is not managed via voxelManager. + labelmap2DView[x] = segmentIndex; + } indexCache.push(x); } } diff --git a/packages/adapters/src/version.ts b/packages/adapters/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/adapters/src/version.ts +++ b/packages/adapters/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f5b802aca0..12dad4a9d7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -3,6 +3,92 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +### Bug Fixes + +- clear preview segmentation when AI tools are disabled ([#2373](https://github.com/cornerstonejs/cornerstone3D/issues/2373)) ([cfc232c](https://github.com/cornerstonejs/cornerstone3D/commit/cfc232c0472aa89eba859d2e2da97af58ca50bfc)) + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/ai + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/ai + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/ai + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/ai diff --git a/packages/ai/package.json b/packages/ai/package.json index d9f23d3aaf..865b4923a2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/ai", - "version": "4.4.1", + "version": "4.5.19", "description": "AI and ML Interfaces for Cornerstone3D", "files": [ "dist" @@ -61,7 +61,7 @@ "onnxruntime-web": "1.17.1" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1", - "@cornerstonejs/tools": "4.4.1" + "@cornerstonejs/core": "4.5.19", + "@cornerstonejs/tools": "4.5.19" } } diff --git a/packages/ai/src/LabelmapSlicePropagationTool.ts b/packages/ai/src/LabelmapSlicePropagationTool.ts index 6f63d82941..100c02ed12 100644 --- a/packages/ai/src/LabelmapSlicePropagationTool.ts +++ b/packages/ai/src/LabelmapSlicePropagationTool.ts @@ -106,6 +106,7 @@ class LabelmapSlicePropagationTool extends LabelmapBaseTool { if (this.segmentAI) { this.segmentAI.enabled = false; } + this.rejectPreview(); }; onSetToolPassive = (): void => { @@ -113,6 +114,7 @@ class LabelmapSlicePropagationTool extends LabelmapBaseTool { if (this.segmentAI) { this.segmentAI.enabled = false; } + this.rejectPreview(); }; /** diff --git a/packages/ai/src/MarkerLabelmapTool.ts b/packages/ai/src/MarkerLabelmapTool.ts index 2925e4fa54..6cb1351993 100644 --- a/packages/ai/src/MarkerLabelmapTool.ts +++ b/packages/ai/src/MarkerLabelmapTool.ts @@ -235,6 +235,7 @@ class MarkerLabelmapTool extends LabelmapBaseTool { if (this.segmentAI) { this.segmentAI.enabled = false; } + this.rejectPreview(); }; /** diff --git a/packages/ai/src/version.ts b/packages/ai/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/ai/src/version.ts +++ b/packages/ai/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index b6dce5ed58..fe9dcf20c2 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -3,6 +3,94 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +### Bug Fixes + +- set volume id on creation ([#2345](https://github.com/cornerstonejs/cornerstone3D/issues/2345)) ([#2396](https://github.com/cornerstonejs/cornerstone3D/issues/2396)) ([a47b125](https://github.com/cornerstonejs/cornerstone3D/commit/a47b125193743290c8c412cea85c990738c339ab)) + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +### Bug Fixes + +- update toLowHighRange function to handle SIGMOID function bounds as a temporary fix ([#2394](https://github.com/cornerstonejs/cornerstone3D/issues/2394)) ([662a8b3](https://github.com/cornerstonejs/cornerstone3D/commit/662a8b3c7b3268557e9787fd6abc67e923ab731b)) + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/core + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/core + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/core + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) ### Bug Fixes diff --git a/packages/core/package.json b/packages/core/package.json index 2221edc302..71cde19f82 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/core", - "version": "4.4.1", + "version": "4.5.19", "description": "Cornerstone3D Core", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", diff --git a/packages/core/src/cache/classes/ImageVolume.ts b/packages/core/src/cache/classes/ImageVolume.ts index 70df8351ea..1712f59715 100644 --- a/packages/core/src/cache/classes/ImageVolume.ts +++ b/packages/core/src/cache/classes/ImageVolume.ts @@ -133,6 +133,7 @@ export class ImageVolume { dimensions, imageIds, numberOfComponents, + id: volumeId, }); this.numVoxels = diff --git a/packages/core/src/utilities/windowLevel.ts b/packages/core/src/utilities/windowLevel.ts index 11d8ed7400..62a1737e1f 100644 --- a/packages/core/src/utilities/windowLevel.ts +++ b/packages/core/src/utilities/windowLevel.ts @@ -58,7 +58,13 @@ function toLowHighRange( lower: number; upper: number; } { - if (voiLUTFunction === VOILUTFunctionType.LINEAR) { + // Note: The SIGMOID function is currently treated the same as LINEAR + // because we don't have a good way to define "bounds" for it. + // Remove or statement when fixed + if ( + voiLUTFunction === VOILUTFunctionType.LINEAR || + voiLUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID + ) { // From C.11.2.1.2.1 (linear function) return { lower: windowCenter - 0.5 - (windowWidth - 1) / 2, @@ -70,15 +76,18 @@ function toLowHighRange( lower: windowCenter - windowWidth / 2, upper: windowCenter + windowWidth / 2, }; - } else if (voiLUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID) { - // From C.11.2.1.3.1 (sigmoid function) - // Sigmoid: y = 1 / (1 + exp(-4*(x - c)/w)) - const xLower = logit(0.01, windowCenter, windowWidth); - const xUpper = logit(0.99, windowCenter, windowWidth); - return { - lower: xLower, - upper: xUpper, - }; + // Note: The SIGMOID function is currently treated the same as LINEAR + // because we don't have a good way to define "bounds" for it. + // Uncomment when fixed + // } else if (voiLUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID) { + // // From C.11.2.1.3.1 (sigmoid function) + // // Sigmoid: y = 1 / (1 + exp(-4*(x - c)/w)) + // const xLower = logit(0.01, windowCenter, windowWidth); + // const xUpper = logit(0.99, windowCenter, windowWidth); + // return { + // lower: xLower, + // upper: xUpper, + // }; } else { throw new Error('Invalid VOI LUT function'); } diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/core/test/dicomImageLoader_wadors_test.js b/packages/core/test/dicomImageLoader_wadors_test.js new file mode 100644 index 0000000000..bbd0877ff7 --- /dev/null +++ b/packages/core/test/dicomImageLoader_wadors_test.js @@ -0,0 +1,68 @@ +// @ts-check +import { cache, imageLoader, metaData } from '@cornerstonejs/core'; +import { + init as dicomImageLoaderInit, + wadors, +} from '@cornerstonejs/dicom-image-loader'; +import { WADO_RS_TEST as CtBigEndian_1_2_840_10008_1_2_2 } from '../../dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2'; +import { WADO_RS_TEST as JpegBaselineWadoRS } from '../../dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422'; +import { WADO_RS_TEST as JpegBaselineYbrFullTest } from '../../dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull'; +import { WADO_RS_TEST as NoPixelSpacingWadoRS } from '../../dicomImageLoader/testImages/no-pixel-spacing'; + +/** @type {import("../../dicomImageLoader/testImages/tests.models").IWadoRsTest[]} */ +const WADO_RS_TESTS = [ + CtBigEndian_1_2_840_10008_1_2_2, + JpegBaselineWadoRS, + JpegBaselineYbrFullTest, + NoPixelSpacingWadoRS, +]; + +/** + * These are paramaterized tests for dicomImageLoader. Theses tests are for + * validating the WADO-RS loader works correctly for a wide variety of images. + * Currently, the WADO-RS tests only test the WADO-RS Metadata Provider. + * + * Future improvement can extend these tests to match the functionality of + * WADO-URI tests including: + * 1. Testing pixel data matches the expected hash + * 2. Testing the Image object returned by `loadImage` + */ +describe('dicomImageLoader - WADO-RS', () => { + beforeEach(() => { + wadors.register(); + dicomImageLoaderInit(); + }); + + afterEach(() => { + // Before each test, purge all the metadata tags so that they are + // loaded fresh + wadors.metaDataManager.purge(); + cache.purgeCache(); + imageLoader.unregisterAllImageLoaders(); + }); + + for (const t of WADO_RS_TESTS) { + describe(t.name, () => { + for (const frame of t.frames) { + // WADO-RS Loader Tests + if (frame.metadataModule && t.wadorsMetadata) { + for (const [ + metadataModuleName, + expectedModuleValues, + ] of Object.entries(frame.metadataModule)) { + it(`should get the ${metadataModuleName} metadata from the ${t.name} image`, async () => { + wadors.metaDataManager.add(t.wadorsUrl, t.wadorsMetadata); + + const actualModuleValue = metaData.get( + metadataModuleName, + t.wadorsUrl + ); + + expect(actualModuleValue).toEqual(expectedModuleValues); + }); + } + } + } + }); + } +}); diff --git a/packages/core/test/dicomImageLoader_wadouri_test.js b/packages/core/test/dicomImageLoader_wadouri_test.js new file mode 100644 index 0000000000..81d5ccd167 --- /dev/null +++ b/packages/core/test/dicomImageLoader_wadouri_test.js @@ -0,0 +1,239 @@ +// @ts-check + +import { cache, imageLoader, metaData } from '@cornerstonejs/core'; +import { + init as dicomImageLoaderInit, + wadouri, +} from '@cornerstonejs/dicom-image-loader'; +import { createImageHash } from '../../../utils/test/pixel-data-hash'; +import { WADOURI_TEST as CtBigEndian_1_2_840_1008_1_2_2 } from '../../dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2'; +import { WADOURI_TEST as CtJpeg2000Lossless_1_2_840_10008_1_2_4_90 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90'; +import { WADOURI_TEST as CtJpeg2000_1_2_840_10008_1_2_4_91 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91'; +import { WADOURI_TEST as CtJpegLsLossless_1_2_840_10008_1_2_4_80 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80'; +import { WADOURI_TEST as CtJpegLsLossless_1_2_840_10008_1_2_4_81 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81'; +import { WADOURI_TEST as CtJpegProcess14V1_1_2_840_10008_1_2_4_70 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70'; +import { WADOURI_TEST as CtJpegProcess14_1_2_840_10008_1_2_4_57 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57'; +import { WADOURI_TEST as CtJpegProcess1_1_2_840_10008_1_2_4_50 } from '../../dicomImageLoader/testImages/CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50'; +import { WADOURI_TEST as CtLittleEndian_1_2_840_10008_1_2_1 } from '../../dicomImageLoader/testImages/CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1'; +import { WADOURI_TEST as CtLittleEndian_1_2_840_10008_1_2 } from '../../dicomImageLoader/testImages/CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2'; +import { WADOURI_TEST as CtRLELossless_1_2_840_10008_1_2_5 } from '../../dicomImageLoader/testImages/CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5'; +import { WADOURI_TEST as TestPattern_JpegBaselineYbr422 } from '../../dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422'; +import { WADOURI_TEST as TestPatternJpegBaselineYbrFull } from '../../dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull'; +import { WADOURI_TEST as TestPatternJpegLsLossless } from '../../dicomImageLoader/testImages/TestPattern_JPEG-LS-Lossless'; +import { WADOURI_TEST as TestPatternJpegLsNearLossless } from '../../dicomImageLoader/testImages/TestPattern_JPEG-LS-NearLossless'; +import { WADOURI_TEST as TestPatternJpegLosslessRgb } from '../../dicomImageLoader/testImages/TestPattern_JPEG-Lossless_RGB'; +import { WADOURI_TEST as TestPatternPalette } from '../../dicomImageLoader/testImages/TestPattern_Palette'; +import { WADOURI_TEST as TestPatternPalette_16 } from '../../dicomImageLoader/testImages/TestPattern_Palette_16'; +import { WADOURI_TEST as TestPatternRGB } from '../../dicomImageLoader/testImages/TestPattern_RGB'; +import { WADOURI_TEST as NoPixelSpacing } from '../../dicomImageLoader/testImages/no-pixel-spacing'; +import { WADOURI_TEST as ParamapTest } from '../../dicomImageLoader/testImages/paramap'; +import { WADOURI_TEST as ParamapFloatTest } from '../../dicomImageLoader/testImages/paramap-float'; +import { WADOURI_TEST as UsMultiframeYbrFull422 } from '../../dicomImageLoader/testImages/us-multiframe-ybr-full-422'; + +/** @type {import("../../dicomImageLoader/testImages/tests.models").IWadoUriTest[]} */ +const tests = [ + CtBigEndian_1_2_840_1008_1_2_2, + CtJpeg2000_1_2_840_10008_1_2_4_91, + CtJpeg2000Lossless_1_2_840_10008_1_2_4_90, + CtJpegLsLossless_1_2_840_10008_1_2_4_80, + CtJpegLsLossless_1_2_840_10008_1_2_4_81, + CtJpegProcess1_1_2_840_10008_1_2_4_50, + CtBigEndian_1_2_840_1008_1_2_2, + CtJpegProcess14_1_2_840_10008_1_2_4_57, + CtJpegProcess14V1_1_2_840_10008_1_2_4_70, + CtLittleEndian_1_2_840_10008_1_2_1, + CtLittleEndian_1_2_840_10008_1_2, + CtRLELossless_1_2_840_10008_1_2_5, + NoPixelSpacing, + ParamapFloatTest, + ParamapTest, + TestPattern_JpegBaselineYbr422, + TestPatternJpegBaselineYbrFull, + TestPatternJpegLosslessRgb, + TestPatternJpegLsLossless, + TestPatternJpegLsNearLossless, + TestPatternPalette_16, + TestPatternPalette, + TestPatternRGB, + UsMultiframeYbrFull422, +]; + +/** + * These are paramaterized tests for dicomImageLoader. It allows us to test + * that different images are loaded correctly, and that the metadata returned by + * the loader is as expected. + * + * These tests are setup to test different aspects from loading single and + * multi-frame dicom images via WADO-URI. The tests include: + * + * 1. Loading the image and comparing the pixelData hash with an expected hash. + * 2. Loading the image and comparing the returned image object with an expected + * image object. + * 3. Retrieving metadata modules and comparing them with expected metadata + * modules. + */ +describe('dicomImageLoader - WADO-URI', () => { + beforeEach(() => { + // register the wadouri loader + wadouri.register(); + // re-initialise the loader before each test to clear any previous config + dicomImageLoaderInit(); + }); + + afterEach(() => { + // Purge any loaded data so each test loads the image + wadouri.dataSetCacheManager.purge(); + cache.purgeCache(); + imageLoader.unregisterAllImageLoaders(); + }); + + it('should allow customising the http request with beforeSend', async () => { + const test = CtLittleEndian_1_2_840_10008_1_2; + const beforeSpy = jasmine.createSpy('beforeHandler').and.resolveTo(); + + dicomImageLoaderInit({ + beforeSend: beforeSpy, + }); + + await imageLoader.loadImage(test.wadouri); + + const expectedHeaders = {}; + const expectedImageId = test.wadouri; + const expectedUrl = test.wadouri.replace('wadouri:', ''); + + expect(beforeSpy).toHaveBeenCalledWith( + jasmine.any(XMLHttpRequest), + expectedImageId, + expectedHeaders, + { + url: expectedUrl, + deferred: { + resolve: jasmine.any(Function), + reject: jasmine.any(Function), + }, + imageId: expectedImageId, + } + ); + }); + + it('should call request lifecycle callbacks', async () => { + const test = CtLittleEndian_1_2_840_10008_1_2; + const onreadystatechangeSpy = jasmine.createSpy('onreadystatechange'); + const onprogressSpy = jasmine.createSpy('onprogress'); + const onloadendSpy = jasmine.createSpy('onloadend'); + const onloadstartSpy = jasmine.createSpy('onloadstart'); + + dicomImageLoaderInit({ + onreadystatechange: onreadystatechangeSpy, + onprogress: onprogressSpy, + onloadend: onloadendSpy, + onloadstart: onloadstartSpy, + }); + + await imageLoader.loadImage(test.wadouri); + + const expectedImageId = test.wadouri; + const expectedUrl = test.wadouri.replace('wadouri:', ''); + const expectedLoaderParams = { + url: expectedUrl, + deferred: { + resolve: jasmine.any(Function), + reject: jasmine.any(Function), + }, + imageId: expectedImageId, + }; + + expect(onloadstartSpy).toHaveBeenCalledOnceWith( + jasmine.any(Event), + expectedLoaderParams + ); + + expect(onprogressSpy).toHaveBeenCalled(); + + expect(onreadystatechangeSpy).toHaveBeenCalledTimes(3); + expect(onreadystatechangeSpy.calls.argsFor(0)).toEqual([ + jasmine.any(Event), + expectedLoaderParams, + ]); + + expect(onloadendSpy).toHaveBeenCalledOnceWith( + jasmine.any(Event), + expectedLoaderParams + ); + }); + + for (const t of tests) { + describe(t.name, () => { + for (const frame of t.frames) { + // Determine the frame to use (default to 1 if not specified) + const frameIndex = frame.index || 1; + + if (frame.pixelDataHash) { + it(`decodes the image and the pixel data hash for frame ${frameIndex} of ${t.name} is correct`, async () => { + // first load the image without the frame so that it is loaded into + // the cache + const { imageId } = await imageLoader.loadImage(t.wadouri); + /** + * If the test case has `.pixelDataHash` defined, then we want to + * load the image and check that the pixel data matches the expected + * hash. + */ + const image = await imageLoader.loadImage( + imageIdWithFrame(imageId, frameIndex) + ); + const hash = await createImageHash(image.getPixelData()); + + expect(hash).toBe(frame.pixelDataHash); + }); + } + + if ('image' in frame && frame.image) { + it(`returns the correct image object for ${frameIndex} of the ${t.name} image`, async () => { + // first load the image without the frame so that it is loaded into + // the cache + const { imageId } = await imageLoader.loadImage(t.wadouri); + + // now load the frame specific imageId + const imagObj = await imageLoader.loadImage( + imageIdWithFrame(imageId, frameIndex) + ); + + expect(imagObj).toEqual(frame.image); + }); + } + + // WADO-RS Loader Tests + if (frame.metadataModule) { + for (const [ + metadataModuleName, + expectedModuleValues, + ] of Object.entries(frame.metadataModule)) { + it(`returns the correct ${metadataModuleName} metadata for frame ${frameIndex} of ${t.name} image`, async () => { + const { imageId } = await imageLoader.loadImage(t.wadouri); + const imageIdWithFrameIndex = imageIdWithFrame( + imageId, + frameIndex + ); + const actualModuleValue = metaData.get( + metadataModuleName, + imageIdWithFrameIndex + ); + + expect(actualModuleValue).toEqual(expectedModuleValues); + }); + } + } + } + }); + } +}); + +/** + * + * @param {string} imageId + * @param {number} frame 1 based frame index. + * @returns {string} + */ +function imageIdWithFrame(imageId, frame) { + return `${imageId}?frame=${frame}`; +} diff --git a/packages/dicomImageLoader/CHANGELOG.md b/packages/dicomImageLoader/CHANGELOG.md index 0d339ca09e..2a650aa1ef 100644 --- a/packages/dicomImageLoader/CHANGELOG.md +++ b/packages/dicomImageLoader/CHANGELOG.md @@ -3,6 +3,92 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +### Bug Fixes + +- **dicomImageLoader:** :bug: set usingDefaultValues in ImagePlane metadata module in the WadoUri loader ([#2391](https://github.com/cornerstonejs/cornerstone3D/issues/2391)) ([d0ea9fd](https://github.com/cornerstonejs/cornerstone3D/commit/d0ea9fdbd3af2df8dc538a5bba15ea8a241dfb0e)) + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/dicom-image-loader + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/dicom-image-loader diff --git a/packages/dicomImageLoader/package.json b/packages/dicomImageLoader/package.json index 88ab356ee2..dffac2763a 100644 --- a/packages/dicomImageLoader/package.json +++ b/packages/dicomImageLoader/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/dicom-image-loader", - "version": "4.4.1", + "version": "4.5.19", "description": "Cornerstone Image Loader for DICOM WADO-URI and WADO-RS and Local file", "keywords": [ "DICOM", @@ -112,7 +112,7 @@ "uuid": "9.0.1" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1", + "@cornerstonejs/core": "4.5.19", "dicom-parser": "1.8.21" }, "config": { diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts index 938daae129..bac439bcb9 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts @@ -149,9 +149,14 @@ export function metadataForDataset( let rowPixelSpacing = null; + let usingDefaultValues = false; if (pixelSpacing) { rowPixelSpacing = pixelSpacing[0]; columnPixelSpacing = pixelSpacing[1]; + } else { + usingDefaultValues = true; + rowPixelSpacing = 1; + columnPixelSpacing = 1; } let rowCosines = null; @@ -190,6 +195,7 @@ export function metadataForDataset( pixelSpacing, rowPixelSpacing, columnPixelSpacing, + usingDefaultValues, }; } diff --git a/packages/dicomImageLoader/src/version.ts b/packages/dicomImageLoader/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/dicomImageLoader/src/version.ts +++ b/packages/dicomImageLoader/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.ts new file mode 100644 index 0000000000..b1aed148ee --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.ts @@ -0,0 +1,170 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; +import { tags } from './CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.wado-rs-tags'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Incorrect type + calibration: {}, + color: false, + columnPixelSpacing: 0.675781, + columns: 512, + dataType: 'Int16Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + getCanvas: undefined, + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 512, + imageFrame: { + bitsAllocated: 16, + bitsStored: 16, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 512, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 1378, + photometricInterpretation: 'MONOCHROME2', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Int16Array), + pixelDataLength: 262144, + pixelRepresentation: 1, + planarConfiguration: undefined, + preScale: { + enabled: true, + scalingParameters: { + rescaleSlope: 1, + rescaleIntercept: -1024, + modality: 'CT', + }, + scaled: true, + }, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 512, + samplesPerPixel: 1, + smallestPixelValue: -3024, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: -1024, + invert: false, + maxPixelValue: 1378, + minPixelValue: -3024, + numberOfComponents: 1, + preScale: { + enabled: true, + scalingParameters: { + rescaleSlope: 1, + rescaleIntercept: -1024, + modality: 'CT', + }, + scaled: true, + }, + rgba: false, + rowPixelSpacing: 0.675781, + rows: 512, + sizeInBytes: 524288, + slope: 1, + voiLUTFunction: undefined, + width: 512, + windowCenter: 40, + windowWidth: 400, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: [0, 1, 0], + columnPixelSpacing: 0.675781, + columns: 512, + frameOfReferenceUID: + '1.2.840.113619.2.30.1.1762295590.1623.978668949.886.8493.0.12', + // @ts-expect-error invalid type in ImagePlaneModule + imageOrientationPatient: [1, 0, 0, 0, 1, 0], + imagePositionPatient: [-161.399994, -148.800003, 4.7], + pixelSpacing: [0.675781, 0.675781], + rowCosines: [1, 0, 0], + rowPixelSpacing: 0.675781, + rows: 512, + sliceLocation: 4.6999998093, + sliceThickness: 5, + usingDefaultValues: false, +}; +// Should be `Types.ImagePixelModule` the actual metadata doesn't conform to it. +const WADO_URI_IMAGE_PIXEL_MODULE = { + bitsAllocated: 16, + bitsStored: 16, + columns: 512, + highBit: 15, + largestPixelValue: undefined, + photometricInterpretation: 'MONOCHROME2', + pixelAspectRatio: undefined, + pixelRepresentation: 1, + planarConfiguration: undefined, + rows: 512, + samplesPerPixel: 1, + smallestPixelValue: undefined, +}; + +/** + * WADO-RS Pixel Module contains additional fields that are not present in + * WADO-URI Pixel Module. + */ +const WADO_RS_IMAGE_PIXEL_MODULE = { + ...WADO_URI_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, +}; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + metadataModule: { + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + }, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.dcm`, + wadorsMetadata: tags, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.wado-rs-tags.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.wado-rs-tags.ts new file mode 100644 index 0000000000..c2c2207bdc --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_BigEndianExplicitTransferSyntax_1.2.840.10008.1.2.2.wado-rs-tags.ts @@ -0,0 +1,1010 @@ +export const tags = { + '00080005': { + vr: 'CS', + Value: ['ISO_IR 192'], + }, + '00080008': { + vr: 'CS', + Value: ['ORIGINAL', 'PRIMARY', 'AXIAL'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.2'], + }, + '00080018': { + vr: 'UI', + Value: ['1.2.840.113619.2.30.1.1762295590.1623.978668950.109'], + }, + '00080020': { + vr: 'DA', + Value: ['20010105'], + }, + '00080021': { + vr: 'DA', + Value: ['20010105'], + }, + '00080022': { + vr: 'DA', + Value: ['20010105'], + }, + '00080023': { + vr: 'DA', + Value: ['20010105'], + }, + '00080030': { + vr: 'TM', + Value: ['083501'], + }, + '00080031': { + vr: 'TM', + Value: ['083709'], + }, + '00080032': { + vr: 'TM', + Value: ['083848'], + }, + '00080033': { + vr: 'TM', + Value: ['083852'], + }, + '00080050': { + vr: 'SH', + Value: ['0000000001'], + }, + '00080060': { + vr: 'CS', + Value: ['CT'], + }, + '00080070': { + vr: 'LO', + Value: ['GE MEDICAL SYSTEMS'], + }, + '00080080': { + vr: 'LO', + }, + '00080090': { + vr: 'PN', + }, + '00081010': { + vr: 'SH', + Value: ['STATION_NAME'], + }, + '00081030': { + vr: 'LO', + Value: ['CHEST'], + }, + '0008103E': { + vr: 'LO', + Value: ['HELICAL CHEST'], + }, + '00081060': { + vr: 'PN', + }, + '00081070': { + vr: 'PN', + }, + '00081090': { + vr: 'LO', + Value: ['HiSpeed CT/i'], + }, + '00090010': { + vr: 'LO', + Value: ['GEMS_IDEN_01'], + }, + '00091001': { + vr: 'UN', + InlineBinary: 'R0VfR0VORVNJU19GRiA=', + }, + '00091002': { + vr: 'UN', + InlineBinary: 'VFdEMQ==', + }, + '00091004': { + vr: 'UN', + InlineBinary: 'SGlTcGVlZCBDVC9p', + }, + '00091027': { + vr: 'UN', + InlineBinary: 'CohVOg==', + }, + '00091030': { + vr: 'UN', + }, + '00091031': { + vr: 'UN', + }, + '000910E6': { + vr: 'UN', + InlineBinary: 'MDU=', + }, + '000910E7': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '000910E9': { + vr: 'UN', + InlineBinary: 'tYdVOg==', + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'MISTER^CT', + }, + ], + }, + '00100020': { + vr: 'LO', + Value: ['2178309'], + }, + '00100030': { + vr: 'DA', + }, + '00100040': { + vr: 'CS', + }, + '00101010': { + vr: 'AS', + }, + '00101030': { + vr: 'DS', + }, + '001021B0': { + vr: 'LT', + }, + '00110010': { + vr: 'LO', + Value: ['GEMS_PATI_01'], + }, + '00111010': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00180022': { + vr: 'CS', + Value: ['HELICAL MODE'], + }, + '00180050': { + vr: 'DS', + Value: [5.0], + }, + '00180060': { + vr: 'DS', + Value: [120], + }, + '00180088': { + vr: 'DS', + Value: [6.5], + }, + '00180090': { + vr: 'DS', + Value: [480.0], + }, + '00181020': { + vr: 'LO', + Value: ['05'], + }, + '00181100': { + vr: 'DS', + Value: [346.0], + }, + '00181110': { + vr: 'DS', + Value: [1099.3100585938], + }, + '00181111': { + vr: 'DS', + Value: [630.0], + }, + '00181120': { + vr: 'DS', + Value: [0.0], + }, + '00181130': { + vr: 'DS', + Value: [167.100006], + }, + '00181140': { + vr: 'CS', + Value: ['CW'], + }, + '00181150': { + vr: 'IS', + Value: [800], + }, + '00181151': { + vr: 'IS', + Value: [200], + }, + '00181152': { + vr: 'IS', + Value: [200], + }, + '00181160': { + vr: 'SH', + Value: ['BODY FILTER'], + }, + '00181190': { + vr: 'DS', + Value: [0.7], + }, + '00181210': { + vr: 'SH', + Value: ['STANDARD'], + }, + '00185100': { + vr: 'CS', + Value: ['FFS'], + }, + '00190010': { + vr: 'LO', + Value: ['GEMS_ACQU_01'], + }, + '00191002': { + vr: 'UN', + InlineBinary: 'kAMAAA==', + }, + '00191003': { + vr: 'UN', + InlineBinary: 'MzczLjc1MDAwMA==', + }, + '00191004': { + vr: 'UN', + InlineBinary: 'MS4wMTY2MDA=', + }, + '0019100F': { + vr: 'UN', + InlineBinary: 'ODQ5LjI5OTk4OA==', + }, + '00191011': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191013': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191014': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191015': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191016': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191017': { + vr: 'UN', + InlineBinary: 'AgA=', + }, + '00191018': { + vr: 'UN', + InlineBinary: 'UyA=', + }, + '00191019': { + vr: 'UN', + InlineBinary: 'MTUwLjAwMDAwMA==', + }, + '0019101A': { + vr: 'UN', + InlineBinary: 'UyA=', + }, + '0019101B': { + vr: 'UN', + InlineBinary: 'MTUwLjAwMDAwMA==', + }, + '0019101E': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '00191023': { + vr: 'UN', + InlineBinary: 'OC4xMjUwMDA=', + }, + '00191024': { + vr: 'UN', + InlineBinary: 'MC40MDA1MTA=', + }, + '00191025': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '00191026': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00191027': { + vr: 'UN', + InlineBinary: 'MC44MDAwMDA=', + }, + '0019102A': { + vr: 'UN', + InlineBinary: 'MTcwLjU5NTcwMw==', + }, + '0019102B': { + vr: 'UN', + InlineBinary: 'MTUzNi42NjcxMTQg', + }, + '0019102C': { + vr: 'UN', + InlineBinary: 'nwsAAA==', + }, + '0019102E': { + vr: 'UN', + InlineBinary: 'OS40MDQyOTc=', + }, + '0019102F': { + vr: 'UN', + InlineBinary: 'OTgwLjAwMDAwMA==', + }, + '00191039': { + vr: 'UN', + InlineBinary: 'EAA=', + }, + '00191040': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191041': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '00191042': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191043': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191044': { + vr: 'UN', + InlineBinary: 'MS4wMDAwMDA=', + }, + '00191047': { + vr: 'UN', + InlineBinary: '6AM=', + }, + '0019104A': { + vr: 'UN', + InlineBinary: 'BgA=', + }, + '0019104B': { + vr: 'UN', + InlineBinary: 'PzkAAA==', + }, + '00191052': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '00191057': { + vr: 'UN', + InlineBinary: 'ov8=', + }, + '00191058': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '0019105E': { + vr: 'UN', + InlineBinary: '+wIAAA==', + }, + '0019105F': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '00191060': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '00191061': { + vr: 'UN', + InlineBinary: 'EAMAAA==', + }, + '00191062': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '0019106A': { + vr: 'UN', + InlineBinary: 'AgA=', + }, + '0019106B': { + vr: 'UN', + InlineBinary: 'VAM=', + }, + '00191070': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '00191071': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00191072': { + vr: 'UN', + InlineBinary: 'MS4wMDAwMDA=', + }, + '00191073': { + vr: 'UN', + InlineBinary: 'MS4wMDAwMDA=', + }, + '00191074': { + vr: 'UN', + InlineBinary: 'MS4wMDAwMDA=', + }, + '00191075': { + vr: 'UN', + InlineBinary: 'MS4wMDAwMDA=', + }, + '00191076': { + vr: 'UN', + InlineBinary: 'MS4wMDAwMDA=', + }, + '001910DA': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '001910DB': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '001910DC': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '001910DD': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '001910DE': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '0020000D': { + vr: 'UI', + Value: ['1.2.840.113619.2.30.1.1762295590.1623.978668949.886'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.2.840.113619.2.30.1.1762295590.1623.978668949.890'], + }, + '00200010': { + vr: 'SH', + Value: ['40933'], + }, + '00200011': { + vr: 'IS', + Value: [2], + }, + '00200012': { + vr: 'IS', + Value: [1], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200032': { + vr: 'DS', + Value: [-161.399994, -148.800003, 4.7], + }, + '00200037': { + vr: 'DS', + Value: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0], + }, + '00200052': { + vr: 'UI', + Value: ['1.2.840.113619.2.30.1.1762295590.1623.978668949.886.8493.0.12'], + }, + '00200060': { + vr: 'CS', + }, + '00201040': { + vr: 'LO', + Value: ['SN'], + }, + '00201041': { + vr: 'DS', + Value: [4.6999998093], + }, + '00210010': { + vr: 'LO', + Value: ['GEMS_RELA_01'], + }, + '00211003': { + vr: 'UN', + InlineBinary: 'AgA=', + }, + '00211005': { + vr: 'UN', + InlineBinary: 'MDU=', + }, + '00211007': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00211015': { + vr: 'UN', + InlineBinary: '5Z8=', + }, + '00211016': { + vr: 'UN', + InlineBinary: 'AgA=', + }, + '00211018': { + vr: 'UN', + InlineBinary: 'MDU=', + }, + '00211019': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00211037': { + vr: 'UN', + InlineBinary: 'EAA=', + }, + '0021104A': { + vr: 'UN', + }, + '00211090': { + vr: 'UN', + InlineBinary: 'twE=', + }, + '00211091': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00211092': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00211093': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00230010': { + vr: 'LO', + Value: ['GEMS_STDY_01'], + }, + '00231070': { + vr: 'UN', + InlineBinary: 'RtFLDMQqzUE=', + }, + '00231074': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '0023107D': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00250010': { + vr: 'LO', + Value: ['GEMS_SERS_01'], + }, + '00251006': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00251007': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '00251010': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00251011': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00251017': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00251018': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00251019': { + vr: 'UN', + InlineBinary: 'AwAAAA==', + }, + '0025101A': { + vr: 'UN', + }, + '00270010': { + vr: 'LO', + Value: ['GEMS_IMAG_01'], + }, + '00271006': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00271010': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '0027101C': { + vr: 'UN', + InlineBinary: 'yAAAAA==', + }, + '0027101D': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '0027101E': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '0027101F': { + vr: 'UN', + InlineBinary: 'yAAAAA==', + }, + '00271020': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00271030': { + vr: 'UN', + }, + '00271035': { + vr: 'UN', + InlineBinary: 'AgA=', + }, + '00271040': { + vr: 'UN', + InlineBinary: 'UyA=', + }, + '00271041': { + vr: 'UN', + InlineBinary: 'ZmaWQA==', + }, + '00271042': { + vr: 'UN', + InlineBinary: 'mpk5wQ==', + }, + '00271043': { + vr: 'UN', + InlineBinary: 'mpnBwQ==', + }, + '00271044': { + vr: 'UN', + InlineBinary: 'ZmaWQA==', + }, + '00271045': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00271046': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00271047': { + vr: 'UN', + InlineBinary: 'AACAvw==', + }, + '00271048': { + vr: 'UN', + InlineBinary: 'mpk4ww==', + }, + '00271049': { + vr: 'UN', + InlineBinary: 'zcwUQw==', + }, + '0027104A': { + vr: 'UN', + InlineBinary: 'ZmaWQA==', + }, + '0027104B': { + vr: 'UN', + InlineBinary: 'mpk4ww==', + }, + '0027104C': { + vr: 'UN', + InlineBinary: 'MzNFww==', + }, + '0027104D': { + vr: 'UN', + InlineBinary: 'ZmaWQA==', + }, + '00271050': { + vr: 'UN', + InlineBinary: 'Zmb+QA==', + }, + '00271051': { + vr: 'UN', + InlineBinary: 'sLiFwQ==', + }, + '00271052': { + vr: 'UN', + InlineBinary: 'TCA=', + }, + '00271053': { + vr: 'UN', + InlineBinary: 'UCA=', + }, + '00271054': { + vr: 'UN', + InlineBinary: 'UyA=', + }, + '00271055': { + vr: 'UN', + InlineBinary: 'SSA=', + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [512], + }, + '00280011': { + vr: 'US', + Value: [512], + }, + '00280030': { + vr: 'DS', + Value: [0.675781, 0.675781], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [1], + }, + '00280120': { + vr: 'SS', + Value: [-32768], + }, + '00281050': { + vr: 'DS', + Value: [40], + }, + '00281051': { + vr: 'DS', + Value: [400], + }, + '00281052': { + vr: 'DS', + Value: [-1024], + }, + '00281053': { + vr: 'DS', + Value: [1], + }, + '00290010': { + vr: 'LO', + Value: ['GEMS_IMPS_01'], + }, + '00291004': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00291005': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '00291006': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '00291007': { + vr: 'UN', + InlineBinary: 'VwAAAA==', + }, + '00291008': { + vr: 'UN', + }, + '00291009': { + vr: 'UN', + }, + '0029100A': { + vr: 'UN', + InlineBinary: '/AI=', + }, + '00291026': { + vr: 'UN', + InlineBinary: 'AgA=', + }, + '00291034': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00291035': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00430010': { + vr: 'LO', + Value: ['GEMS_PARM_01'], + }, + '00431010': { + vr: 'UN', + InlineBinary: 'kAE=', + }, + '00431011': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00431012': { + vr: 'UN', + InlineBinary: 'DwACAAQA', + }, + '00431013': { + vr: 'UN', + InlineBinary: 'awAXAAQAAgAUAA==', + }, + '00431014': { + vr: 'UN', + InlineBinary: 'BAAEAAIA', + }, + '00431015': { + vr: 'UN', + InlineBinary: 'nws=', + }, + '00431016': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00431017': { + vr: 'UN', + InlineBinary: 'MC4wOTUwMDA=', + }, + '00431018': { + vr: 'UN', + InlineBinary: 'MC4wODUwMDBcMS4xMDIwMDBcMC4wOTUwMDA=', + }, + '00431019': { + vr: 'UN', + InlineBinary: 'XgE=', + }, + '0043101A': { + vr: 'UN', + InlineBinary: 'BwAAAA==', + }, + '0043101B': { + vr: 'UN', + InlineBinary: 'AAAAAAAAAAAAAA==', + }, + '0043101C': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '0043101D': { + vr: 'UN', + InlineBinary: 'KAA=', + }, + '0043101E': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '0043101F': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00431020': { + vr: 'UN', + InlineBinary: 'MC4wMDAwMDA=', + }, + '00431021': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '00431025': { + vr: 'UN', + InlineBinary: 'AQACAAMA7ALtAu4C', + }, + '00431026': { + vr: 'UN', + InlineBinary: 'AQABAAEAAQABAAEA', + }, + '00431027': { + vr: 'UN', + InlineBinary: 'LzEuMzox', + }, + '00431028': { + vr: 'UN', + InlineBinary: + 'AAAAAgAAAJpJ5dVZwdqF+gAIAAAAAyYkAAAAAAAAAAAAAAAAAAAAAPgw+DD4MPgw+DD4MPgw+DD4MPgw+DD4MPgw+DD4MPgw+DD4MPgw+DA=', + }, + '00431029': { + vr: 'UN', + InlineBinary: + 'AAAAAUPwGxICfwAACWIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', + }, + '0043102A': { + vr: 'UN', + InlineBinary: 'AAAAAgAAAJpJ5dVZwdqF+gAIAAAAAyYkAAAAAAAAAAAAAAAAAAAAAA==', + }, + '0043102B': { + vr: 'UN', + InlineBinary: 'AgAEAAAAAAA=', + }, + '00431031': { + vr: 'UN', + InlineBinary: 'LTExLjYwMDAwMFwtMjQuMjAwMDAxIA==', + }, + '00431040': { + vr: 'UN', + InlineBinary: 'gJgqQw==', + }, + '00431041': { + vr: 'UN', + InlineBinary: 'ScKqRA==', + }, + '00431042': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00431043': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '00431044': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '00431045': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '00431046': { + vr: 'UN', + InlineBinary: 'AwAAAA==', + }, + '00431047': { + vr: 'UN', + InlineBinary: '/////w==', + }, + '00431048': { + vr: 'UN', + InlineBinary: 'AQAAAA==', + }, + '00431049': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '0043104A': { + vr: 'UN', + InlineBinary: 'AQA=', + }, + '0043104B': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '0043104C': { + vr: 'UN', + InlineBinary: 'AAA=', + }, + '0043104D': { + vr: 'UN', + InlineBinary: 'AAAAAA==', + }, + '0043104E': { + vr: 'UN', + InlineBinary: 'JUlCQA==', + }, + '7FE00010': { + vr: 'OW', + }, +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_DeflatedExplicitVRLittleEndianTransferSyntax_1.2.840.10008.1.2.1.99.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_DeflatedExplicitVRLittleEndianTransferSyntax_1.2.840.10008.1.2.1.99.ts new file mode 100644 index 0000000000..7deadba4ad --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_DeflatedExplicitVRLittleEndianTransferSyntax_1.2.840.10008.1.2.1.99.ts @@ -0,0 +1,29 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +/** + * Currently decoding this isn't supported + */ +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_DeflatedExplicitVRLittleEndianTransferSyntax_1.2.840.10008.1.2.1.99'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_DeflatedExplicitVRLittleEndianTransferSyntax_1.2.840.10008.1.2.1.99.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_DeflatedExplicitVRLittleEndianTransferSyntax_1.2.840.10008.1.2.1.99.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90.ts new file mode 100644 index 0000000000..425f6a7461 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEG2000LosslessOnlyTransferSyntax_1.2.840.10008.1.2.4.90.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91.ts new file mode 100644 index 0000000000..deef43f0e3 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91.ts @@ -0,0 +1,25 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + '0c1d1516c0619ce2482ae7aa6d45b099c83d29cd55c398626b5b07c607771b4c'; +const TEST_NAME = 'CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEG2000TransferSyntax_1.2.840.10008.1.2.4.91.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80.ts new file mode 100644 index 0000000000..b0df9d8f9f --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGLSLosslessTransferSyntax_1.2.840.10008.1.2.4.80.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81.ts new file mode 100644 index 0000000000..562d7df250 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + '24558c9527160735784e5601f29a15b6939a213f141f871c7c4cfcc639ada493'; +const TEST_NAME = + 'CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGLSLossyTransferSyntax_1.2.840.10008.1.2.4.81.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess10_12TransferSyntax_1.2.840.10008.1.2.4.55.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess10_12TransferSyntax_1.2.840.10008.1.2.4.55.ts new file mode 100644 index 0000000000..08504f6c24 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess10_12TransferSyntax_1.2.840.10008.1.2.4.55.ts @@ -0,0 +1,30 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +/** + * Not supported ?? + */ + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_JPEGProcess10_12TransferSyntax_1.2.840.10008.1.2.4.55'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGProcess10_12TransferSyntax_1.2.840.10008.1.2.4.55.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGProcess10_12TransferSyntax_1.2.840.10008.1.2.4.55.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70.ts new file mode 100644 index 0000000000..57d2a3e18f --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + '4343dec9982c17612b714c36238cc64d3bff466d55b9da6e95af26e2e086d00c'; +const TEST_NAME = + 'CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGProcess14SV1TransferSyntax_1.2.840.10008.1.2.4.70.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57.ts new file mode 100644 index 0000000000..27d7426d28 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGProcess14TransferSyntax_1.2.840.10008.1.2.4.57.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50.ts new file mode 100644 index 0000000000..c64a79d31a --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'e3f2fc815cfd235d2c3873ef35cff6771a11ba3b2bc77fa6ade776bb64174c5a'; +const TEST_NAME = + 'CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGProcess1TransferSyntax_1.2.840.10008.1.2.4.50.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess6_8TransferSyntax_1.2.840.10008.1.2.4.53.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess6_8TransferSyntax_1.2.840.10008.1.2.4.53.ts new file mode 100644 index 0000000000..7976489483 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_JPEGProcess6_8TransferSyntax_1.2.840.10008.1.2.4.53.ts @@ -0,0 +1,30 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +/** + * Not supported?? + */ + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_JPEGProcess6_8TransferSyntax_1.2.840.10008.1.2.4.53'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_JPEGProcess6_8TransferSyntax_1.2.840.10008.1.2.4.53.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_JPEGProcess6_8TransferSyntax_1.2.840.10008.1.2.4.53.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1.ts new file mode 100644 index 0000000000..112d18318a --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_LittleEndianExplicitTransferSyntax_1.2.840.10008.1.2.1.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2.ts new file mode 100644 index 0000000000..b202733356 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2.ts @@ -0,0 +1,26 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = + 'CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_LittleEndianImplicitTransferSyntax_1.2.840.10008.1.2.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5.ts b/packages/dicomImageLoader/testImages/CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5.ts new file mode 100644 index 0000000000..4b0ba152c3 --- /dev/null +++ b/packages/dicomImageLoader/testImages/CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5.ts @@ -0,0 +1,25 @@ +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const IMAGE_HASH = + 'd36b58a8274fd5882a3863693bb84d2fb5719fff73c0ee21c98bfcb9abbb05c4'; +const TEST_NAME = 'CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/CTImage.dcm_RLELosslessTransferSyntax_1.2.840.10008.1.2.5.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422.ts new file mode 100644 index 0000000000..5b1b10d226 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422.ts @@ -0,0 +1,178 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import { tags } from './TestPattern_JPEG-Baseline_YBR422.wado-rs-tags'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + // @ts-expect-error jasmine matcher + imageData: jasmine.any(ImageData), + largestPixelValue: 255, + photometricInterpretation: 'YBR_FULL_422', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 400, + samplesPerPixel: 3, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: [0, 1, 0], + columnPixelSpacing: 1, + columns: 640, + frameOfReferenceUID: undefined, + // @ts-expect-error Incorrect type in core + imageOrientationPatient: [1, 0, 0, 0, 1, 0], + imagePositionPatient: [0.5, 0.5, 0.5], + pixelSpacing: [1, 1], + rowCosines: [1, 0, 0], + rowPixelSpacing: 1, + rows: 400, + sliceLocation: undefined, + sliceThickness: undefined, + usingDefaultValues: false, +}; +// Should be `Types.ImagePixelModule` the actual metadata doesn't conform to it. +const WADO_URI_IMAGE_PIXEL_MODULE = { + bitsAllocated: 8, + bitsStored: 8, + columns: 640, + highBit: 7, + largestPixelValue: undefined, + photometricInterpretation: 'YBR_FULL_422', + pixelAspectRatio: undefined, + pixelRepresentation: 0, + planarConfiguration: 0, + rows: 400, + samplesPerPixel: 3, + smallestPixelValue: undefined, +}; + +/** + * WADO-RS Pixel Module contains additional fields that are not present in + * WADO-URI Pixel Module. + */ +const WADO_RS_IMAGE_PIXEL_MODULE = { + ...WADO_URI_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, +}; + +const SERIES_MODULE: Types.GeneralSeriesModuleMetadata = { + modality: 'OT', + seriesDate: undefined, + seriesInstanceUID: '1.3.6.1.4.1.34261.90254037371867.41912.1553085024.3', + seriesNumber: 1, + seriesTime: undefined, + studyInstanceUID: '1.3.6.1.4.1.34261.90254037371867.41912.1553085024.2', + // @ts-expect-error The following fields are not defined in GeneralSeriesModuleMetadata + acquisitionDate: undefined, + acquisitionTime: undefined, + seriesDescription: undefined, +}; + +const CALIBRATION_MODULE = undefined; + +/** + * The expected image hash on MacOS is + * `16ceb0ebf838cf705ae3d641c0acda06c9122f2ed42fe8bf250555bd4faa41e5 In GH + * Actions it is + * `f8b3f1c75e9ac773a200bba9ce94f8fd1df97d6f27d4e164af002ddcab6a025b` + */ +const IMAGE_HASH = + 'f8b3f1c75e9ac773a200bba9ce94f8fd1df97d6f27d4e164af002ddcab6a025b'; +const TEST_NAME = 'JPEG Baseline YBR422'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_JPEG-Baseline_YBR422.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_JPEG-Baseline_YBR422.dcm`, + wadorsMetadata: tags, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422.wado-rs-tags.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422.wado-rs-tags.ts new file mode 100644 index 0000000000..62b947d605 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBR422.wado-rs-tags.ts @@ -0,0 +1,220 @@ +/** + * Created by `dcm2json TestPattern_JPEG-Baseline_YBR422.dcm` + */ +export const tags = { + '00080008': { + vr: 'CS', + Value: ['DERIVED', 'SECONDARY', 'OTHER', 'MEVISLAB'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00080018': { + vr: 'UI', + Value: ['1.2.276.0.7230010.3.1.4.253549293.23032.1555332170.441'], + }, + '00080020': { + vr: 'DA', + Value: ['20190320'], + }, + '00080023': { + vr: 'DA', + Value: ['20190320'], + }, + '00080030': { + vr: 'TM', + Value: ['133024.526000'], + }, + '00080033': { + vr: 'TM', + Value: ['133024.526000'], + }, + '00080050': { + vr: 'SH', + Value: ['121320'], + }, + '00080060': { + vr: 'CS', + Value: ['OT'], + }, + '00080064': { + vr: 'CS', + Value: ['WSD'], + }, + '00080090': { + vr: 'PN', + Value: [''], + }, + '00082111': { + vr: 'ST', + Value: [ + 'Lossy compression with JPEG baseline, IJG quality factor 90, compression ratio 17.0447', + ], + }, + '00082112': { + vr: 'SQ', + Value: [ + { + '00081150': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00081155': { + vr: 'UI', + Value: ['1.3.6.1.4.1.34261.90254037371867.41912.1553085024.0'], + }, + '0040A170': { + vr: 'SQ', + Value: [ + { + '00080100': { + vr: 'SH', + Value: ['121320'], + }, + '00080102': { + vr: 'SH', + Value: ['DCM'], + }, + '00080104': { + vr: 'LO', + Value: ['Uncompressed predecessor'], + }, + }, + ], + }, + }, + ], + }, + '00089215': { + vr: 'SQ', + Value: [ + { + '00080100': { + vr: 'SH', + Value: ['113040'], + }, + '00080102': { + vr: 'SH', + Value: ['DCM'], + }, + '00080104': { + vr: 'LO', + Value: ['Lossy Compression'], + }, + }, + ], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'Test^Pattern', + }, + ], + }, + '00100020': { + vr: 'LO', + Value: ['Test^Pattern'], + }, + '00100030': { + vr: 'DA', + Value: ['20131005'], + }, + '00100040': { + vr: 'CS', + Value: ['O'], + }, + '0020000D': { + vr: 'UI', + Value: ['1.3.6.1.4.1.34261.90254037371867.41912.1553085024.2'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.6.1.4.1.34261.90254037371867.41912.1553085024.3'], + }, + '00200010': { + vr: 'SH', + Value: ['1'], + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200020': { + vr: 'CS', + }, + '00200032': { + vr: 'DS', + Value: [0.5, 0.5, 0.5], + }, + '00200037': { + vr: 'DS', + Value: [1, 0, 0, 0, 1, 0], + }, + '00204000': { + vr: 'LT', + Value: [ + 'MeVisLab, original image from https://pixabay.com/vectors/test-pattern-tv-tv-test-pattern-152459/', + ], + }, + '00280002': { + vr: 'US', + Value: [3], + }, + '00280004': { + vr: 'CS', + Value: ['YBR_FULL_422'], + }, + '00280006': { + vr: 'US', + Value: [0], + }, + '00280010': { + vr: 'US', + Value: [400], + }, + '00280011': { + vr: 'US', + Value: [640], + }, + '00280030': { + vr: 'DS', + Value: [1, 1], + }, + '00280100': { + vr: 'US', + Value: [8], + }, + '00280101': { + vr: 'US', + Value: [8], + }, + '00280102': { + vr: 'US', + Value: [7], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00282110': { + vr: 'CS', + Value: ['01'], + }, + '00282112': { + vr: 'DS', + Value: [17.0447], + }, + '00282114': { + vr: 'CS', + Value: ['ISO_10918_1'], + }, + '7FE00010': { + vr: 'OB', + }, +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull.ts new file mode 100644 index 0000000000..dc8f553541 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull.ts @@ -0,0 +1,179 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import { tags } from './TestPattern_JPEG-Baseline_YBRFull.wado-rs-tags'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageData: jasmine.any(ImageData), + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 255, + photometricInterpretation: 'YBR_FULL', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 400, + samplesPerPixel: 3, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: [0, 1, 0], + columnPixelSpacing: 1, + columns: 640, + frameOfReferenceUID: undefined, + // @ts-expect-error Incorrect type in core + imageOrientationPatient: [1, 0, 0, 0, 1, 0], + imagePositionPatient: [0.5, 0.5, 0.5], + pixelSpacing: [1, 1], + rowCosines: [1, 0, 0], + rowPixelSpacing: 1, + rows: 400, + sliceLocation: undefined, + sliceThickness: undefined, + usingDefaultValues: false, +}; +// Should be `Types.ImagePixelModule` the actual metadata doesn't conform to it. +const WADO_URI_IMAGE_PIXEL_MODULE = { + bitsAllocated: 8, + bitsStored: 8, + columns: 640, + highBit: 7, + largestPixelValue: undefined, + photometricInterpretation: 'YBR_FULL', + pixelAspectRatio: undefined, + pixelRepresentation: 0, + planarConfiguration: 0, + rows: 400, + samplesPerPixel: 3, + smallestPixelValue: undefined, +}; + +/** + * WADO-RS Pixel Module contains additional fields that are not present in + * WADO-URI Pixel Module. + */ +const WADO_RS_IMAGE_PIXEL_MODULE = { + ...WADO_URI_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, +}; + +const SERIES_MODULE: Types.GeneralSeriesModuleMetadata = { + modality: 'OT', + seriesDate: undefined, + seriesInstanceUID: '1.3.6.1.4.1.34261.90254037371867.41912.1553085024.3', + seriesNumber: 1, + seriesTime: undefined, + studyInstanceUID: '1.3.6.1.4.1.34261.90254037371867.41912.1553085024.2', + // @ts-expect-error The following fields are not defined in GeneralSeriesModuleMetadata + acquisitionDate: undefined, + acquisitionTime: undefined, + seriesDescription: undefined, +}; + +const CALIBRATION_MODULE = undefined; + +/** + * On MacOS the expected image hash is + * `1ba91b409f0cecc9f0605c3a1ce184782afec2984a56274d5f2abc895c57f540` + * In GH Actions it is + * `7e07e65b853f83c2239a253037dd29278d22638fbaaa8d80e7ae31f300606586` + */ +const IMAGE_HASH = + '1ba91b409f0cecc9f0605c3a1ce184782afec2984a56274d5f2abc895c57f540'; +const TEST_NAME = 'JPEG Baseline YBR Full'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_JPEG-Baseline_YBRFull.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_JPEG-Baseline_YBRFull.dcm`, + wadorsMetadata: tags, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull.wado-rs-tags.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull.wado-rs-tags.ts new file mode 100644 index 0000000000..17d1c709c8 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Baseline_YBRFull.wado-rs-tags.ts @@ -0,0 +1,218 @@ +/** + * Created by `dcm2json TestPattern_JPEG-Baseline_YBRFull.dcm` + */ +export const tags = { + '00080008': { + vr: 'CS', + Value: ['DERIVED', 'SECONDARY', 'OTHER', 'MEVISLAB'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00080018': { + vr: 'UI', + Value: ['1.2.276.0.7230010.3.1.4.253549293.26360.1555332281.164'], + }, + '00080020': { + vr: 'DA', + Value: ['20190320'], + }, + '00080023': { + vr: 'DA', + Value: ['20190320'], + }, + '00080030': { + vr: 'TM', + Value: ['133024.526000'], + }, + '00080033': { + vr: 'TM', + Value: ['133024.526000'], + }, + '00080050': { + vr: 'SH', + }, + '00080060': { + vr: 'CS', + Value: ['OT'], + }, + '00080064': { + vr: 'CS', + Value: ['WSD'], + }, + '00080090': { + vr: 'PN', + }, + '00082111': { + vr: 'ST', + Value: [ + 'Lossy compression with JPEG baseline, IJG quality factor 90, compression ratio 16.071', + ], + }, + '00082112': { + vr: 'SQ', + Value: [ + { + '00081150': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00081155': { + vr: 'UI', + Value: ['1.3.6.1.4.1.34261.90254037371867.41912.1553085024.0'], + }, + '0040A170': { + vr: 'SQ', + Value: [ + { + '00080100': { + vr: 'SH', + Value: ['121320'], + }, + '00080102': { + vr: 'SH', + Value: ['DCM'], + }, + '00080104': { + vr: 'LO', + Value: ['Uncompressed predecessor'], + }, + }, + ], + }, + }, + ], + }, + '00089215': { + vr: 'SQ', + Value: [ + { + '00080100': { + vr: 'SH', + Value: ['113040'], + }, + '00080102': { + vr: 'SH', + Value: ['DCM'], + }, + '00080104': { + vr: 'LO', + Value: ['Lossy Compression'], + }, + }, + ], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'Test^Pattern', + }, + ], + }, + '00100020': { + vr: 'LO', + Value: ['Test^Pattern'], + }, + '00100030': { + vr: 'DA', + Value: ['20131005'], + }, + '00100040': { + vr: 'CS', + Value: ['O'], + }, + '0020000D': { + vr: 'UI', + Value: ['1.3.6.1.4.1.34261.90254037371867.41912.1553085024.2'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.6.1.4.1.34261.90254037371867.41912.1553085024.3'], + }, + '00200010': { + vr: 'SH', + Value: ['1'], + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200020': { + vr: 'CS', + }, + '00200032': { + vr: 'DS', + Value: [0.5, 0.5, 0.5], + }, + '00200037': { + vr: 'DS', + Value: [1, 0, 0, 0, 1, 0], + }, + '00204000': { + vr: 'LT', + Value: [ + 'MeVisLab, original image from https://pixabay.com/vectors/test-pattern-tv-tv-test-pattern-152459/', + ], + }, + '00280002': { + vr: 'US', + Value: [3], + }, + '00280004': { + vr: 'CS', + Value: ['YBR_FULL'], + }, + '00280006': { + vr: 'US', + Value: [0], + }, + '00280010': { + vr: 'US', + Value: [400], + }, + '00280011': { + vr: 'US', + Value: [640], + }, + '00280030': { + vr: 'DS', + Value: [1, 1], + }, + '00280100': { + vr: 'US', + Value: [8], + }, + '00280101': { + vr: 'US', + Value: [8], + }, + '00280102': { + vr: 'US', + Value: [7], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00282110': { + vr: 'CS', + Value: ['01'], + }, + '00282112': { + vr: 'DS', + Value: [16.071], + }, + '00282114': { + vr: 'CS', + Value: ['ISO_10918_1'], + }, + '7FE00010': { + vr: 'OB', + }, +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-LS-Lossless.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-LS-Lossless.ts new file mode 100644 index 0000000000..abf6a042d7 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-LS-Lossless.ts @@ -0,0 +1,126 @@ +import type { Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + bytesPerPixel: 1, + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + nearLossless: 0, + interleaveMode: 1, + bitsPerPixel: 8, + componentsPerPixel: 3, + encodeOptions: { + nearLossless: 0, + interleaveMode: 1, + frameInfo: { + width: 640, + height: 400, + bitsPerSample: 8, + componentCount: 3, + }, + }, + frameInfo: { + bitsPerSample: 8, + componentCount: 3, + height: 400, + width: 640, + }, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + imageInfo: { + columns: 640, + rows: 400, + bitsPerPixel: 8, + signed: false, + bytesPerPixel: 1, + componentsPerPixel: 3, + }, + largestPixelValue: 255, + photometricInterpretation: 'RGB', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 400, + samplesPerPixel: 3, + signed: false, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const IMAGE_HASH = + 'db754473cb0f7754a77e709c199e787285f68beb675a934f8d4567328ab8f107'; +const TEST_NAME = 'TestPattern_JPEG-LS-Lossless'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_JPEG-LS-Lossless.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_JPEG-LS-Lossless.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-LS-NearLossless.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-LS-NearLossless.ts new file mode 100644 index 0000000000..184f682a1a --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-LS-NearLossless.ts @@ -0,0 +1,126 @@ +import type { Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + bytesPerPixel: 1, + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + nearLossless: 2, + interleaveMode: 1, + bitsPerPixel: 8, + componentsPerPixel: 3, + encodeOptions: { + nearLossless: 2, + interleaveMode: 1, + frameInfo: { + width: 640, + height: 400, + bitsPerSample: 8, + componentCount: 3, + }, + }, + frameInfo: { + bitsPerSample: 8, + componentCount: 3, + height: 400, + width: 640, + }, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + imageInfo: { + columns: 640, + rows: 400, + bitsPerPixel: 8, + signed: false, + bytesPerPixel: 1, + componentsPerPixel: 3, + }, + largestPixelValue: 255, + photometricInterpretation: 'RGB', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 400, + samplesPerPixel: 3, + signed: false, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const IMAGE_HASH = + 'b004180226bc4fa4f5982f914e924b31ccb2cd19782c149ea303a773e465c4d7'; +const TEST_NAME = 'TestPattern_JPEG-LS-NearLossless'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_JPEG-LS-NearLossless.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_JPEG-LS-NearLossless.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_JPEG-Lossless_RGB.ts b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Lossless_RGB.ts new file mode 100644 index 0000000000..fd2626e371 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_JPEG-Lossless_RGB.ts @@ -0,0 +1,96 @@ +import type { Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 255, + photometricInterpretation: 'RGB', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 400, + samplesPerPixel: 3, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const IMAGE_HASH = + 'db754473cb0f7754a77e709c199e787285f68beb675a934f8d4567328ab8f107'; +const TEST_NAME = 'JPEG Lossless RGB'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_JPEG-Lossless_RGB.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_JPEG-Lossless_RGB.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_Palette.ts b/packages/dicomImageLoader/testImages/TestPattern_Palette.ts new file mode 100644 index 0000000000..c6e5a1c8b7 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_Palette.ts @@ -0,0 +1,101 @@ +import type { Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8ClampedArray', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + // @ts-expect-error jasmine matcher + bluePaletteColorLookupTableData: jasmine.any(Array), + bluePaletteColorLookupTableDescriptor: [256, 0, 8], + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + greenPaletteColorLookupTableData: jasmine.any(Array), + greenPaletteColorLookupTableDescriptor: [256, 0, 8], + // @ts-expect-error jasmine matcher + imageData: { data: jasmine.any(Uint8ClampedArray) }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 0, + photometricInterpretation: 'PALETTE COLOR', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8ClampedArray), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: undefined, + // @ts-expect-error jasmine matcher + redPaletteColorLookupTableData: jasmine.any(Array), + redPaletteColorLookupTableDescriptor: [256, 0, 8], + rows: 400, + samplesPerPixel: 1, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 0, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const IMAGE_HASH = + '7cd0371eb339cb41d63862964d8908a03d548427694a8918bd4debc94f54eeea'; +const TEST_NAME = 'TestPattern_Palette'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_Palette.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_Palette.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_Palette_16.ts b/packages/dicomImageLoader/testImages/TestPattern_Palette_16.ts new file mode 100644 index 0000000000..bbc73476a1 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_Palette_16.ts @@ -0,0 +1,101 @@ +import type { Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8ClampedArray', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + // @ts-expect-error jasmine matcher + bluePaletteColorLookupTableData: jasmine.any(Array), + bluePaletteColorLookupTableDescriptor: [256, 0, 16], + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + greenPaletteColorLookupTableData: jasmine.any(Array), + greenPaletteColorLookupTableDescriptor: [256, 0, 16], + // @ts-expect-error jasmine matcher + imageData: { data: jasmine.any(Uint8ClampedArray) }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 0, + photometricInterpretation: 'PALETTE COLOR', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8ClampedArray), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: undefined, + // @ts-expect-error jasmine matcher + redPaletteColorLookupTableData: jasmine.any(Array), + redPaletteColorLookupTableDescriptor: [256, 0, 16], + rows: 400, + samplesPerPixel: 1, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 0, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const IMAGE_HASH = + '7cd0371eb339cb41d63862964d8908a03d548427694a8918bd4debc94f54eeea'; +const TEST_NAME = 'TestPattern_Palette_16'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_Palette_16.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_Palette_16.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/TestPattern_RGB.ts b/packages/dicomImageLoader/testImages/TestPattern_RGB.ts new file mode 100644 index 0000000000..42e3d58954 --- /dev/null +++ b/packages/dicomImageLoader/testImages/TestPattern_RGB.ts @@ -0,0 +1,96 @@ +import type { Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: true, + columnPixelSpacing: 1, + columns: 640, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 400, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 640, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 255, + photometricInterpretation: 'RGB', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 768000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 400, + samplesPerPixel: 3, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 400, + sizeInBytes: 768000, + slope: 1, + voiLUTFunction: undefined, + width: 640, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const IMAGE_HASH = + 'db754473cb0f7754a77e709c199e787285f68beb675a934f8d4567328ab8f107'; +const TEST_NAME = 'TestPattern_RGB'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/TestPattern_RGB.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/TestPattern_RGB.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/no-pixel-spacing.ts b/packages/dicomImageLoader/testImages/no-pixel-spacing.ts new file mode 100644 index 0000000000..d7c3ac2dc0 --- /dev/null +++ b/packages/dicomImageLoader/testImages/no-pixel-spacing.ts @@ -0,0 +1,100 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import { tags } from './no-pixel-spacing.wado-rs-tags'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const WADO_RS_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: [0, 1, 0], + columnPixelSpacing: 1, + columns: 800, + frameOfReferenceUID: undefined, + // @ts-expect-error Incorrect type in core + imageOrientationPatient: [1, 0, 0, 0, 1, 0], + imagePositionPatient: [0, 0, 0], + pixelSpacing: undefined, + rowCosines: [1, 0, 0], + rowPixelSpacing: 1, + rows: 600, + sliceLocation: undefined, + sliceThickness: undefined, + usingDefaultValues: true, +}; + +/** + * WADO-URI Image Plan Module returns different values for some fields + */ +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + ...WADO_RS_IMAGE_PLANE_MODULE, + columnCosines: null, + imageOrientationPatient: undefined, + imagePositionPatient: undefined, + rowCosines: null, +}; + +const WADO_RS_IMAGE_PIXEL_MODULE: Types.ImagePixelModule = { + bitsAllocated: 8, + bitsStored: 8, + // @ts-expect-error Types.ImagePixelModule is missing bluePaletteColorLookupTableData + // greenPaletteColorLookupTableData, redPaletteColorLookupTableData + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: [256, 0, 16], + columns: 800, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: [256, 0, 16], + highBit: 7, + largestPixelValue: undefined, + photometricInterpretation: 'PALETTE COLOR', + pixelAspectRatio: undefined, + pixelRepresentation: 0, + planarConfiguration: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: [256, 0, 16], + rows: 600, + samplesPerPixel: 1, + smallestPixelValue: undefined, +}; + +/** + * WADO-URI Pixel Module returns arrays for the + * blue/gree/redPaletteColorLookupTableData where as WADO-RS returns undefined. + * + * Match using `jasmine.any(Array)` to just check that an array is returned. + */ +const WADO_URI_IMAGE_PIXEL_MODULE = { + ...WADO_RS_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: jasmine.any(Array), + greenPaletteColorLookupTableData: jasmine.any(Array), + redPaletteColorLookupTableData: jasmine.any(Array), +}; + +const IMAGE_HASH = + 'd4c564cf88bf9fe0654d380ad45744c9579b4e456150e40e5577f87103949c4a'; +const TEST_NAME = 'No Pixel Spacing'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/no-pixel-spacing.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + }, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/no-pixel-spacing.dcm`, + wadorsMetadata: tags, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.IMAGE_PLANE]: WADO_RS_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/no-pixel-spacing.wado-rs-tags.ts b/packages/dicomImageLoader/testImages/no-pixel-spacing.wado-rs-tags.ts new file mode 100644 index 0000000000..daa2f8c4eb --- /dev/null +++ b/packages/dicomImageLoader/testImages/no-pixel-spacing.wado-rs-tags.ts @@ -0,0 +1,739 @@ +/** + * Created by `dcm2json no-pixel-spacing.dcm` + */ +export const tags = { + '00080005': { + vr: 'CS', + Value: ['ISO_IR 192'], + }, + '00080008': { + vr: 'CS', + Value: ['ORIGINAL', 'PRIMARY', 'INTRACARDIAC'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.6.1'], + }, + '00080018': { + vr: 'UI', + Value: ['1.3.46.670589.14.1000.312.2.101370.20180503030558.3.0'], + }, + '00080020': { + vr: 'DA', + Value: ['20180503'], + }, + '00080022': { + vr: 'DA', + Value: ['20180503'], + }, + '00080023': { + vr: 'DA', + Value: ['20180503'], + }, + '0008002A': { + vr: 'DT', + Value: ['20180503050558.170000'], + }, + '00080030': { + vr: 'TM', + Value: ['050544.000000'], + }, + '00080032': { + vr: 'TM', + Value: ['050558.170000'], + }, + '00080033': { + vr: 'TM', + Value: ['050558.170000'], + }, + '00080050': { + vr: 'SH', + }, + '00080060': { + vr: 'CS', + Value: ['US'], + }, + '00080070': { + vr: 'LO', + Value: ['Philips Medical Systems'], + }, + '00080080': { + vr: 'LO', + Value: ['Cardiologie LATOUR'], + }, + '00080090': { + vr: 'PN', + }, + '00081010': { + vr: 'SH', + Value: ['USCARD3'], + }, + '00081090': { + vr: 'LO', + Value: ['CX50'], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'TEST', + }, + ], + }, + '00100020': { + vr: 'LO', + Value: ['1234'], + }, + '00100030': { + vr: 'DA', + }, + '00100040': { + vr: 'CS', + }, + '00181020': { + vr: 'LO', + Value: [ + 'CX50_3.1.2', + '"453561826131__PRINTERS.11.217__PRINTERS__[2015/07/02]-04:40"', + '453561845591__3.1.2.463__Ultrasound_Applicat--[2015/11/20]-16:41', + '"453561779031__DRIVERS.51.11__DRIVERS__[2014/06/27]-14:33"', + '453561726031__OS.12.676__OS__[2013/08/29]-15:43', + ], + }, + '00185010': { + vr: 'LO', + Value: ['S5-1', 'UNUSED', 'UNUSED'], + }, + '00185020': { + vr: 'LO', + Value: ['CARD_ADULT'], + }, + '00186011': { + vr: 'SQ', + Value: [ + { + '00186012': { + vr: 'US', + Value: [1], + }, + '00186014': { + vr: 'US', + Value: [1], + }, + '00186016': { + vr: 'UL', + Value: [3], + }, + '00186018': { + vr: 'UL', + Value: [120], + }, + '0018601A': { + vr: 'UL', + Value: [60], + }, + '0018601C': { + vr: 'UL', + Value: [800], + }, + '0018601E': { + vr: 'UL', + Value: [518], + }, + '00186020': { + vr: 'SL', + Value: [340], + }, + '00186022': { + vr: 'SL', + Value: [4], + }, + '00186024': { + vr: 'US', + Value: [3], + }, + '00186026': { + vr: 'US', + Value: [3], + }, + '00186028': { + vr: 'FD', + Value: [0], + }, + '0018602A': { + vr: 'FD', + Value: [0], + }, + '0018602C': { + vr: 'FD', + Value: [0.03305420890260027], + }, + '0018602E': { + vr: 'FD', + Value: [0.03305420890260027], + }, + }, + { + '00186012': { + vr: 'US', + Value: [4], + }, + '00186014': { + vr: 'US', + Value: [10], + }, + '00186016': { + vr: 'UL', + Value: [3], + }, + '00186018': { + vr: 'UL', + Value: [176], + }, + '0018601A': { + vr: 'UL', + Value: [522], + }, + '0018601C': { + vr: 'UL', + Value: [622], + }, + '0018601E': { + vr: 'UL', + Value: [576], + }, + '00186020': { + vr: 'SL', + Value: [-176], + }, + '00186022': { + vr: 'SL', + Value: [-522], + }, + '00186024': { + vr: 'US', + Value: [4], + }, + '00186026': { + vr: 'US', + Value: [0], + }, + '00186028': { + vr: 'FD', + Value: [0], + }, + '0018602A': { + vr: 'FD', + Value: [0], + }, + '0018602C': { + vr: 'FD', + Value: [0.004499943750703116], + }, + '0018602E': { + vr: 'FD', + Value: [0], + }, + }, + { + '00186012': { + vr: 'US', + Value: [4], + }, + '00186014': { + vr: 'US', + Value: [10], + }, + '00186016': { + vr: 'UL', + Value: [3], + }, + '00186018': { + vr: 'UL', + Value: [743], + }, + '0018601A': { + vr: 'UL', + Value: [522], + }, + '0018601C': { + vr: 'UL', + Value: [743], + }, + '0018601E': { + vr: 'UL', + Value: [576], + }, + '00186020': { + vr: 'SL', + Value: [-743], + }, + '00186022': { + vr: 'SL', + Value: [-522], + }, + '00186024': { + vr: 'US', + Value: [4], + }, + '00186026': { + vr: 'US', + Value: [0], + }, + '00186028': { + vr: 'FD', + Value: [0], + }, + '0018602A': { + vr: 'FD', + Value: [0], + }, + '0018602C': { + vr: 'FD', + Value: [0.004499943750703116], + }, + '0018602E': { + vr: 'FD', + Value: [0], + }, + }, + ], + }, + '00186031': { + vr: 'CS', + Value: ['SECTOR_PHASED'], + }, + '0020000D': { + vr: 'UI', + Value: ['1.3.46.670589.14.1000.312.4.101370.20180503030544.3.0'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.46.670589.14.1000.312.3.101370.20180503030545.3.0'], + }, + '00200010': { + vr: 'SH', + Value: ['344'], + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200020': { + vr: 'CS', + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['PALETTE COLOR'], + }, + '00280010': { + vr: 'US', + Value: [600], + }, + '00280011': { + vr: 'US', + Value: [800], + }, + '00280014': { + vr: 'US', + Value: [1], + }, + '00280100': { + vr: 'US', + Value: [8], + }, + '00280101': { + vr: 'US', + Value: [8], + }, + '00280102': { + vr: 'US', + Value: [7], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00281101': { + vr: 'US', + Value: [256, 0, 16], + }, + '00281102': { + vr: 'US', + Value: [256, 0, 16], + }, + '00281103': { + vr: 'US', + Value: [256, 0, 16], + }, + '00281201': { + vr: 'OW', + InlineBinary: + 'AAAAAQABAAEAAQABAAIAAgACAAMAAwADAAMABAAEAAUABQAFAAUABQAGAAYABgAGAAcABwAHAAgACQAJAAkACQALAAsACwAMAAwADAANAA0ADgAPAA8ADwAQABEAEQARABIAEwATABQAFQAVABYAFgAXABcAGAAZABoAGgAbABwAHAAdAB4AHgAgACAAIQAiACIAJAAkACUAJgAnACgAKQApACoAKwAsAC0ALwAvADEAMQAyADMANAA1ADYAOAA4ADsAOwA8AD4APgA/AD8AQgBCAEQARABEAEUARwBIAEkATABNAE4AUABRAFIAUwBVAFcAWABbAFwAXQBeAGAAYQBjAGUAZgBnAGkAagBrAGwAbwBxAHQAdQB2AHcAegB7AH0AfgB/AIIAhACGAIkAiwCMAI4AjwCRAJQAlQCXAJkAmgCcAJ0AnwCiAKQApgCoAKkAqwCtAK8ArwCyALQAtgC4ALoAuwC7AL0AvwDFAMUAxwDJAMsAywDNANEA0QDWANYA2ADcANwA3gDeAOAA4wDlAOUA5wDnAOwA7ADsAO4A8ADwAPAA8ADwAPAA9QD1APUA+AD4APgA+AD4APoA+gD6APoA+gD6APoA+gD6AP8AugCmAKgAewAtABkAhgB1AHUARwAbAA8ADQAIAAUAAADlACYAMQAlAEQAMAAVAAE=', + }, + '00281202': { + vr: 'OW', + InlineBinary: + 'AAAAAQABAAEAAQABAAIAAgACAAMAAwADAAQABAAEAAUABQAGAAYABgAGAAcABwAHAAgACAAJAAkACgAKAAoACgALAAsADAAMAAwADQANAA4ADgAPABAAEAARABEAEgASABMAEwAUABUAFgAWABYAFwAYABgAGQAaABsAGwAcAB0AHQAeAB8AHwAhACEAIgAjACQAJQAlACYAKAAoACkAKgArACsALQAtAC4AMAAxADIAMwAzADUANQA2ADcAOQA6ADsAPAA9AD8APwBAAEEARABFAEcASABJAEoATABMAE0AUABRAFIAVABVAFYAVwBZAFwAXABeAGAAYQBiAGMAZgBmAGoAawBtAG8AcABxAHIAdQB2AHkAegB7AH0AfwCBAIIAhACFAIgAiQCLAI4AjwCRAJIAlACVAJkAmgCcAJ0AnwChAKIApACoAKkAqwCtAK8AsACyALYAtgC6ALsAvQC/AMEAwwDDAMUAxwDLAMsAywDNAM8AzwDRANMA0wDYANgA2gDcANwA3gDeAOAA5QDnAOcA6QDpAOwA7ADsAO4A8ADwAPAA8wDzAPMA9QD1APUA+AD4APgA+AD4APoA+gD6APoA+gD6APoA+gD6AP8AugCmAKgAewAtABkAcQA0AEMAagA2AB0AFwAOAAgA/wAFAJoAMQAlAGQATgApAAE=', + }, + '00281203': { + vr: 'OW', + InlineBinary: + 'AAAAAQABAAEAAQABAAIAAgADAAMAAwAEAAQABQAFAAYABgAHAAcACAAIAAkACQAKAAsACwAMAA0ADQAOAA4ADwARABEAEgATABMAFAAUABUAFgAXABgAGAAZABoAGgAbABwAHQAeAB4AIAAgACEAIQAiACMAJAAlACUAJgAoACgAKQAqACsAKwAtAC4ALwAwADAAMgAzADMANQA1ADUANwA4ADgAOgA7ADsAPgA/AEAAQQBCAEQARABFAEYASQBKAEsATABMAE4AUABRAFEAVABVAFcAVwBYAFkAWwBcAF0AYQBiAGMAZQBlAGYAZwBqAGwAbQBvAHAAcQByAHQAdgB3AHkAegB9AH4AfwCBAIIAhQCFAIgAiQCLAIwAjgCPAJEAkgCUAJcAmQCaAJ0AnwChAKIApACmAKgAqwCtAK8ArwCwALIAtgC4ALoAuwC9AL8AwQDDAMMAwwDHAMkAywDLAM0AzwDPANEA0wDWANYA2ADaANwA3ADeAOAA4ADjAOMA5QDnAOcA6QDpAOwA7ADuAO4A8ADwAPAA8ADwAPMA9QD1APUA9QD1APUA+AD4APgA+gD6APoA+gD6AP0A/QD9AP0A/QD9AP0A/QD9AP8AugCmAKgAewAtABkAJAAMABoApAB1ADYAKAAWAAsAGwAFAP8AMQAlAKIAjgBUAAE=', + }, + '00282110': { + vr: 'CS', + Value: ['00'], + }, + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 113'], + }, + '200D0011': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1001': { + vr: 'LO', + Value: ['1.3.46.670589.14.1000.312'], + }, + '200D1002': { + vr: 'UL', + Value: [1], + }, + '200D1003': { + vr: 'UL', + Value: [202], + }, + '200D1004': { + vr: 'UL', + Value: [0], + }, + '200D1005': { + vr: 'UL', + Value: [0], + }, + '200D1006': { + vr: 'UL', + Value: [1], + }, + '200D1007': { + vr: 'CS', + Value: ['NONE'], + }, + '200D100B': { + vr: 'CS', + Value: ['FALSE'], + }, + '200D1012': { + vr: 'UL', + Value: [6], + }, + '200D1013': { + vr: 'FL', + Value: [0, 4.49900007, 8.99800014, 13.4969997, 17.9960003, 22.4950008], + }, + '200D1014': { + vr: 'SS', + Value: [0, 0, 0, 0, 0, 0], + }, + '200D1015': { + vr: 'UL', + Value: [0], + }, + '200D1017': { + vr: 'FD', + Value: [340], + }, + '200D1018': { + vr: 'FD', + Value: [0.1], + }, + '200D1019': { + vr: 'FD', + Value: [0], + }, + '200D101A': { + vr: 'FD', + Value: [0.008726646259971648], + }, + '200D101B': { + vr: 'FD', + Value: [0], + }, + '200D101C': { + vr: 'FD', + Value: [0.15], + }, + '200D101D': { + vr: 'UL', + Value: [170], + }, + '200D101E': { + vr: 'UL', + Value: [0], + }, + '200D101F': { + vr: 'FD', + Value: [1], + }, + '200D1020': { + vr: 'FD', + Value: [0], + }, + '200D1021': { + vr: 'FD', + Value: [0], + }, + '200D1022': { + vr: 'UL', + Value: [0], + }, + '200D1024': { + vr: 'UL', + Value: [0], + }, + '200D1025': { + vr: 'UL', + Value: [0], + }, + '200D1026': { + vr: 'UL', + Value: [0], + }, + '200D1027': { + vr: 'UL', + Value: [4294967295], + }, + '200D1028': { + vr: 'UL', + Value: [0], + }, + '200D1031': { + vr: 'UL', + Value: [0], + }, + '200D1108': { + vr: 'CS', + Value: ['FALSE'], + }, + '200D110D': { + vr: 'SQ', + Value: [ + { + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1000': { + vr: 'US', + Value: [1], + }, + '200D1001': { + vr: 'SQ', + Value: [ + { + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1002': { + vr: 'ST', + Value: ['IFI_PN'], + }, + '200D1003': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1004': { + vr: 'SL', + Value: [95, 6, 559, 25], + }, + '200D1005': { + vr: 'UL', + Value: [255, 255, 255], + }, + '200D1006': { + vr: 'UL', + Value: [37, 62, 94], + }, + '200D1007': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1013': { + vr: 'US', + Value: [84, 69, 83, 84], + }, + }, + { + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1002': { + vr: 'ST', + Value: ['IFI_MRN'], + }, + '200D1003': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1004': { + vr: 'SL', + Value: [95, 33, 258, 52], + }, + '200D1005': { + vr: 'UL', + Value: [255, 255, 255], + }, + '200D1006': { + vr: 'UL', + Value: [37, 62, 94], + }, + '200D1007': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1013': { + vr: 'US', + Value: [49, 50, 51, 52], + }, + }, + { + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1002': { + vr: 'ST', + Value: ['IFI_INS'], + }, + '200D1003': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1004': { + vr: 'SL', + Value: [258, 33, 559, 52], + }, + '200D1005': { + vr: 'UL', + Value: [255, 255, 255], + }, + '200D1006': { + vr: 'UL', + Value: [37, 62, 94], + }, + '200D1007': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1013': { + vr: 'US', + Value: [ + 67, 97, 114, 100, 105, 111, 108, 111, 103, 105, 101, 32, 76, + 65, 84, 79, 85, 82, + ], + }, + }, + { + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1002': { + vr: 'ST', + Value: ['IFI_DT'], + }, + '200D1003': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1004': { + vr: 'SL', + Value: [670, 6, 794, 25], + }, + '200D1005': { + vr: 'UL', + Value: [255, 255, 255], + }, + '200D1006': { + vr: 'UL', + Value: [37, 62, 94], + }, + '200D1007': { + vr: 'CS', + Value: ['TRUE'], + }, + }, + { + '200D0010': { + vr: 'LO', + Value: ['Philips US Imaging DD 109'], + }, + '200D1002': { + vr: 'ST', + Value: ['IFI_TM'], + }, + '200D1003': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D1004': { + vr: 'SL', + Value: [670, 33, 794, 52], + }, + '200D1005': { + vr: 'UL', + Value: [255, 255, 255], + }, + '200D1006': { + vr: 'UL', + Value: [37, 62, 94], + }, + '200D1007': { + vr: 'CS', + Value: ['TRUE'], + }, + }, + ], + }, + '200D1014': { + vr: 'CS', + Value: ['FALSE'], + }, + }, + ], + }, + '200D110E': { + vr: 'CS', + Value: ['TRUE'], + }, + '200D110F': { + vr: 'LO', + Value: ['CARD_ADULT'], + }, + '200D1110': { + vr: 'SL', + Value: [1], + }, + '200D1111': { + vr: 'LO', + }, + '200D1112': { + vr: 'SL', + Value: [-1], + }, + '7FE00010': { + vr: 'OB', + }, +}; diff --git a/packages/dicomImageLoader/testImages/paramap-float.ts b/packages/dicomImageLoader/testImages/paramap-float.ts new file mode 100644 index 0000000000..2765fe4efe --- /dev/null +++ b/packages/dicomImageLoader/testImages/paramap-float.ts @@ -0,0 +1,180 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: false, + columnPixelSpacing: 0.7031, + columns: 256, + dataType: 'Float32Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + getCanvas: undefined, + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 256, + imageFrame: { + bitsAllocated: 32, + bitsStored: undefined, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 256, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 0.004095000214874744, + photometricInterpretation: 'MONOCHROME2', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Float32Array), + pixelDataLength: 65536, + pixelRepresentation: undefined, + planarConfiguration: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 256, + samplesPerPixel: 1, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 0.004095000214874744, + minPixelValue: 0, + numberOfComponents: 1, + preScale: undefined, + rgba: false, + rowPixelSpacing: 0.7031, + rows: 256, + sizeInBytes: 262144, + slope: 1, + voiLUTFunction: undefined, + width: 256, + windowCenter: 0.5020475001074374, + windowWidth: 1.0040950002148747, + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: [-0.005401652, 0.9847554, 0.1738611], + columnPixelSpacing: 0.7031, + columns: 256, + frameOfReferenceUID: + '1.3.6.1.4.1.14519.5.2.1.3671.7001.241598906086676267096591752663', + // @ts-expect-error Incorrect type in core + imageOrientationPatient: [ + 0.999981, 0.004800584, 0.003877514, -0.005401652, 0.9847554, 0.1738611, + ], + imagePositionPatient: [-90.0225, -108.462, -43.9748], + pixelSpacing: [0.7031, 0.7031], + rowCosines: [0.999981, 0.004800584, 0.003877514], + rowPixelSpacing: 0.7031, + rows: 256, + sliceLocation: undefined, + sliceThickness: 2.999902, + usingDefaultValues: false, +}; +// Should be `Types.ImagePixelModule` the actual metadata doesn't conform to it. +const WADO_URI_IMAGE_PIXEL_MODULE = { + bitsAllocated: 32, + bitsStored: undefined, + columns: 256, + highBit: undefined, + largestPixelValue: undefined, + photometricInterpretation: 'MONOCHROME2', + pixelAspectRatio: undefined, + pixelRepresentation: undefined, + planarConfiguration: undefined, + rows: 256, + samplesPerPixel: 1, + smallestPixelValue: undefined, +}; + +/** + * WADO-RS Pixel Module contains additional fields that are not present in + * WADO-URI Pixel Module. + */ +const WADO_RS_IMAGE_PIXEL_MODULE = { + ...WADO_URI_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, +}; + +const SERIES_MODULE: Types.GeneralSeriesModuleMetadata = { + modality: 'MR', + seriesDate: { year: 2016, month: 9, day: 29 }, + seriesInstanceUID: '1.2.276.0.7230010.3.1.3.0.50783.1475186871.651944', + seriesNumber: 701, + seriesTime: { + hours: 18, + minutes: 7, + seconds: 51, + fractionalSeconds: undefined, + }, + studyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.3671.7001.133687106572018334063091507027', + // @ts-expect-error The following fields are not defined in GeneralSeriesModuleMetadata + acquisitionDate: undefined, + acquisitionTime: undefined, + seriesDescription: 'Apparent Diffusion Coefficient', +}; + +const CALIBRATION_MODULE = undefined; + +const IMAGE_HASH = + '461466d00428dc8a55013aef870cb76bfa13cb41e4e3a0d211ccaf83162f4383'; +const TEST_NAME = 'paramap-float'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/paramap-float.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: EXPECTED_IMAGE, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; + +/** + * Disabled - dcm2json reports an error + */ +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/paramap-float.dcm`, + // wadorsMetadata: tags, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/paramap.ts b/packages/dicomImageLoader/testImages/paramap.ts new file mode 100644 index 0000000000..8aa3754e41 --- /dev/null +++ b/packages/dicomImageLoader/testImages/paramap.ts @@ -0,0 +1,181 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const CS_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: {}, + color: false, + columnPixelSpacing: 0.7031, + columns: 256, + dataType: 'Uint16Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + getCanvas: undefined, + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 256, + imageFrame: { + bitsAllocated: 32, + bitsStored: undefined, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 256, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + + largestPixelValue: 4095, + photometricInterpretation: 'MONOCHROME2', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint16Array), + pixelDataLength: 65536, + pixelRepresentation: undefined, + planarConfiguration: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 256, + samplesPerPixel: 1, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 4095, + minPixelValue: 0, + numberOfComponents: 1, + preScale: undefined, + rgba: false, + rowPixelSpacing: 0.7031, + rows: 256, + sizeInBytes: 131072, + slope: 1, + voiLUTFunction: undefined, + width: 256, + windowCenter: 2048, + windowWidth: 4096, + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: [-0.005401652, 0.9847554, 0.1738611], + columnPixelSpacing: 0.7031, + columns: 256, + frameOfReferenceUID: + '1.3.6.1.4.1.14519.5.2.1.3671.7001.241598906086676267096591752663', + // @ts-expect-error Incorrect type in core + imageOrientationPatient: [ + 0.999981, 0.004800584, 0.003877514, -0.005401652, 0.9847554, 0.1738611, + ], + imagePositionPatient: [-90.0225, -108.462, -43.9748], + pixelSpacing: [0.7031, 0.7031], + rowCosines: [0.999981, 0.004800584, 0.003877514], + rowPixelSpacing: 0.7031, + rows: 256, + sliceLocation: undefined, + sliceThickness: 2.999902, + usingDefaultValues: false, +}; +// Should be `Types.ImagePixelModule` the actual metadata doesn't conform to it. +const WADO_URI_IMAGE_PIXEL_MODULE = { + bitsAllocated: 32, + bitsStored: undefined, + columns: 256, + highBit: undefined, + largestPixelValue: undefined, + photometricInterpretation: 'MONOCHROME2', + pixelAspectRatio: undefined, + pixelRepresentation: undefined, + planarConfiguration: undefined, + rows: 256, + samplesPerPixel: 1, + smallestPixelValue: undefined, +}; + +/** + * WADO-RS Pixel Module contains additional fields that are not present in + * WADO-URI Pixel Module. + */ +const WADO_RS_IMAGE_PIXEL_MODULE = { + ...WADO_URI_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, +}; + +const SERIES_MODULE: Types.GeneralSeriesModuleMetadata = { + modality: 'MR', + seriesDate: { year: 2016, month: 9, day: 29 }, + seriesInstanceUID: '1.2.276.0.7230010.3.1.3.0.50782.1475186871.424076', + seriesNumber: 701, + seriesTime: { + hours: 18, + minutes: 7, + seconds: 51, + fractionalSeconds: undefined, + }, + studyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.3671.7001.133687106572018334063091507027', + // @ts-expect-error The following fields are not defined in GeneralSeriesModuleMetadata + acquisitionDate: undefined, + acquisitionTime: undefined, + seriesDescription: 'Apparent Diffusion Coefficient', +}; + +const CALIBRATION_MODULE = undefined; + +const IMAGE_HASH = + '5f5f97c3b88747f6498c38c99f41b484a1814acf7b1e140fcc77c3a2afe2d17c'; +const TEST_NAME = 'paramap'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/paramap.dcm`, + frames: [ + { + pixelDataHash: IMAGE_HASH, + image: CS_IMAGE, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; + +/** + * Disabled - dcm2json reports an error + */ +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/paramap.dcm`, + // wadorsMetadata: tags, + frames: [ + { + pixelDataHash: IMAGE_HASH, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.GENERAL_SERIES]: SERIES_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/tests.models.ts b/packages/dicomImageLoader/testImages/tests.models.ts new file mode 100644 index 0000000000..3815ffe0c5 --- /dev/null +++ b/packages/dicomImageLoader/testImages/tests.models.ts @@ -0,0 +1,108 @@ +import type { Types } from '@cornerstonejs/core'; +import type { WADORSMetaData } from '../src/types'; + +/** + * This is an interface for defining parametrised tests for WADO-URL and WADO-RS + * image loaders. + * + * This interface is used in `packages/core/test/dicomImageLoader_wadors_test.js` and + * `packages/core/test/dicomImageLoader_wadouri_test.js`. + */ +export interface IWadoUriTest { + /** + * A descriptive name for the test case. + */ + name: string; + /** + * The Wado-uri url to load the image. + * Karma is setup to serve files in `packages/dicomImageLoader/testImages/` at + * the `/testImages/` path. + * + * So a file at `packages/dicomImageLoader/testImages/no-pixel-spacing.dcm` + * can be referenced as `wadouri:/testImages/no-pixel-spacing.dcm` + * + * Note: The `wadouri:` prefix is required to trigger the dicomImageLoader. + * + * Example: `wadouri:/testImages/no-pixel-spacing.dcm` + * + */ + wadouri: string; + /** + * Frame specific expected test values. + * + * This is useful for multi-frame images where the expected metadata + * modules/pixel data hash is different for each frame. + */ + frames: Array<{ + /** + * Frame index. If not specified, defaults to 1. + * Note: This is a 1-based index, so counts from frame 1 to numberOfFrames + */ + index?: number; + /** + * The expected SHA256 hash of the pixel data for the frame. This is used to + * verify that the pixel data was loaded and decoded correctly. + */ + pixelDataHash?: string; + /** + * Expected Cornerstone IImage object to return for this frame. + */ + image?: Types.IImage | unknown; + /** + * Expected metadata module values for this frame. + */ + metadataModule?: { + [key: string]: object | undefined; + }; + }>; +} + +export interface IWadoRsTest { + /** + * A descriptive name for the test case. + */ + name: string; + /** + * Not used currently. + * + * @todo add ability for tests to load images from + * a mock Wado-RS server. + */ + wadorsUrl: string; + /** + * + * Wado-RS JSON metadata to allow loading of metadata without + * having to fetch it from a Wado-RS server. This is used in the tess + * to verify that metadata is parsed correctly. + * + * This can be created using `dcm2json source.dcm` + */ + wadorsMetadata?: WADORSMetaData | Record; + /** + * Frame specific expected test values. + * + * This is useful for multi-frame images where the expected metadata + * modules/pixel data hash is different for each frame. + */ + frames: Array<{ + /** + * Frame index. If not specified, defaults to 1. + * Note: This is a 1-based index, so counts from frame 1 to numberOfFrames + */ + index?: number; + /** + * Not currently used - pending implementation of Wado-RS image loading. + */ + pixelDataHash?: string; + /** + * Not currently used - pending implementation of Wado-RS image loading. + */ + image?: Types.IImage | unknown; + /** + * Expected metadata module value for this frame. + */ + metadataModule?: { + [key: string]: object | undefined; + }; + }>; +} diff --git a/packages/dicomImageLoader/testImages/us-multiframe-ybr-full-422.ts b/packages/dicomImageLoader/testImages/us-multiframe-ybr-full-422.ts new file mode 100644 index 0000000000..bd5ef14519 --- /dev/null +++ b/packages/dicomImageLoader/testImages/us-multiframe-ybr-full-422.ts @@ -0,0 +1,195 @@ +import { Enums, type Types } from '@cornerstonejs/core'; +import { tags } from './us-multiframe-ybr-full-422.wado-rs-tags'; +import type { IWadoRsTest, IWadoUriTest } from './tests.models'; + +const CALIBRATION_MODULE = { + sequenceOfUltrasoundRegions: [ + { + physicalDeltaX: 0.041258670759110834, + physicalDeltaY: 0.041258670759110834, + physicalUnitsXDirection: 3, + physicalUnitsYDirection: 3, + referencePhysicalPixelValueX: undefined, + referencePhysicalPixelValueY: undefined, + referencePixelX0: null, + referencePixelY0: null, + regionDataType: 1, + regionFlags: 2, + regionLocationMaxX1: 788, + regionLocationMaxY1: 592, + regionLocationMinX0: 11, + regionLocationMinY0: 30, + regionSpatialFormat: 1, + transducerFrequency: undefined, + }, + ], +}; + +const EXPECTED_IMAGE: Types.IImage = { + // @ts-expect-error Extra fields not defined in IImage + calibration: CALIBRATION_MODULE, + color: true, + columnPixelSpacing: 1, + columns: 800, + dataType: 'Uint8Array', + data: jasmine.any(Object), + // @ts-expect-error Extra fields not defined in IImage + decodeTimeInMS: jasmine.any(Number), + floatPixelData: undefined, + // @ts-expect-error jasmine matcher + getCanvas: jasmine.any(Function), + // @ts-expect-error jasmine matcher + getPixelData: jasmine.any(Function), + height: 600, + imageFrame: { + bitsAllocated: 8, + bitsStored: 8, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + columns: 800, + decodeLevel: undefined, + // @ts-expect-error jasmine matcher + decodeTimeInMS: jasmine.any(Number), + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + largestPixelValue: 255, + photometricInterpretation: 'YBR_FULL_422', + // @ts-expect-error jasmine matcher + pixelData: jasmine.any(Uint8Array), + pixelDataLength: 960000, + pixelRepresentation: 0, + planarConfiguration: 0, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, + rows: 600, + samplesPerPixel: 3, + smallestPixelValue: 0, + }, + // @ts-expect-error jasmine matcher + imageId: jasmine.any(String), + intercept: 0, + invert: false, + maxPixelValue: 255, + minPixelValue: 0, + numberOfComponents: 3, + preScale: undefined, + rgba: undefined, + rowPixelSpacing: 1, + rows: 600, + sizeInBytes: 960000, + slope: 1, + voiLUTFunction: undefined, + width: 800, + windowCenter: 128, + windowWidth: 256, + // @todo - add tests for voxelManager. + // @ts-expect-error jasmine matcher + voxelManager: jasmine.any(Object), + // @ts-expect-error jasmine matcher + loadTimeInMS: jasmine.any(Number), + totalTimeInMS: jasmine.any(Number), + // @ts-expect-error jasmine matcher + imageQualityStatus: jasmine.any(Number), +}; + +const WADO_URI_IMAGE_PLANE_MODULE: Types.ImagePlaneModule = { + columnCosines: null, + columnPixelSpacing: 1, + columns: 800, + frameOfReferenceUID: undefined, + imageOrientationPatient: undefined, + imagePositionPatient: undefined, + pixelSpacing: undefined, + rowCosines: null, + rowPixelSpacing: 1, + rows: 600, + sliceLocation: undefined, + sliceThickness: undefined, + usingDefaultValues: true, +}; +// Should be `Types.ImagePixelModule` the actual metadata doesn't conform to it. +const WADO_URI_IMAGE_PIXEL_MODULE = { + bitsAllocated: 8, + bitsStored: 8, + columns: 800, + highBit: 7, + largestPixelValue: undefined, + photometricInterpretation: 'YBR_FULL_422', + pixelAspectRatio: undefined, + pixelRepresentation: 0, + planarConfiguration: 0, + rows: 600, + samplesPerPixel: 3, + smallestPixelValue: undefined, +}; + +/** + * WADO-RS Pixel Module contains additional fields that are not present in + * WADO-URI Pixel Module. + */ +const WADO_RS_IMAGE_PIXEL_MODULE = { + ...WADO_URI_IMAGE_PIXEL_MODULE, + bluePaletteColorLookupTableData: undefined, + bluePaletteColorLookupTableDescriptor: undefined, + greenPaletteColorLookupTableData: undefined, + greenPaletteColorLookupTableDescriptor: undefined, + redPaletteColorLookupTableData: undefined, + redPaletteColorLookupTableDescriptor: undefined, +}; + +const MULTIFRAME_MODULE = { + NumberOfFrames: 78, + PerFrameFunctionalInformation: {}, + SharedFunctionalInformation: {}, +}; +const TEST_NAME = 'US Multiframe YBR Full 422'; + +const FRAME_1_PIXEL_DATA_HASH = + '969155018b2b569d530b22bfdc537650c7162c56bad3783f1d1ecab2d558abf0'; +const FRAME_78_PIXEL_DATA_HASH = + '31f6c10f923f9b970dc81e7bd6b2a68376abceeb47b91efa6ea481391df1c6c0'; + +export const WADOURI_TEST: IWadoUriTest = { + name: TEST_NAME, + wadouri: `wadouri:/testImages/us-multiframe-ybr-full-422.dcm`, + frames: [ + { + index: 1, + pixelDataHash: FRAME_1_PIXEL_DATA_HASH, + image: EXPECTED_IMAGE, + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_URI_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.MULTIFRAME]: MULTIFRAME_MODULE, + }, + }, + { + index: 78, + pixelDataHash: FRAME_78_PIXEL_DATA_HASH, + }, + ], +}; + +export const WADO_RS_TEST: IWadoRsTest = { + name: TEST_NAME, + wadorsUrl: `wadors:/testImages/us-multiframe-ybr-full-422.dcm`, + wadorsMetadata: tags, + frames: [ + { + pixelDataHash: FRAME_1_PIXEL_DATA_HASH, + image: EXPECTED_IMAGE, + // these aren't working yet - the wado-rs metadata provider + // doesn't return anything for these modules. Need to fix + // how the data is being cached by the wado-rs loader. + metadataModule: { + [Enums.MetadataModules.CALIBRATION]: CALIBRATION_MODULE, + [Enums.MetadataModules.IMAGE_PLANE]: WADO_URI_IMAGE_PLANE_MODULE, + [Enums.MetadataModules.IMAGE_PIXEL]: WADO_RS_IMAGE_PIXEL_MODULE, + [Enums.MetadataModules.MULTIFRAME]: MULTIFRAME_MODULE, + }, + }, + ], +}; diff --git a/packages/dicomImageLoader/testImages/us-multiframe-ybr-full-422.wado-rs-tags.ts b/packages/dicomImageLoader/testImages/us-multiframe-ybr-full-422.wado-rs-tags.ts new file mode 100644 index 0000000000..c67487ee6a --- /dev/null +++ b/packages/dicomImageLoader/testImages/us-multiframe-ybr-full-422.wado-rs-tags.ts @@ -0,0 +1,299 @@ +/** + * Created by `dcm2json us-multiframe-ybr-full-422.dcm` + */ +export const tags = { + '00080005': { + vr: 'CS', + Value: ['ISO_IR 192'], + }, + '00080008': { + vr: 'CS', + Value: ['DERIVED', 'PRIMARY', 'ABDOMINAL'], + }, + '00080013': { + vr: 'TM', + Value: ['133717'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.3.1'], + }, + '00080018': { + vr: 'UI', + Value: ['9999.34338454114017967847836346716065726945'], + }, + '00080020': { + vr: 'DA', + Value: ['20121025'], + }, + '00080023': { + vr: 'DA', + Value: ['20121025'], + }, + '00080030': { + vr: 'TM', + }, + '00080033': { + vr: 'TM', + }, + '00080050': { + vr: 'SH', + Value: ['1291447253958726'], + }, + '00080060': { + vr: 'CS', + Value: ['US'], + }, + '00080068': { + vr: 'CS', + Value: ['FOR PRESENTATION'], + }, + '00080070': { + vr: 'LO', + }, + '00080090': { + vr: 'PN', + }, + '00081030': { + vr: 'LO', + Value: ['US KIDNEYS'], + }, + '00081032': { + vr: 'SQ', + Value: [ + { + '00080100': { + vr: 'SH', + Value: ['IMG5038'], + }, + '00080102': { + vr: 'SH', + Value: ['RP'], + }, + '00080104': { + vr: 'LO', + Value: ['US KIDNEYS'], + }, + }, + ], + }, + '0008103E': { + vr: 'LO', + Value: ['US KIDNEYS'], + }, + '00082144': { + vr: 'IS', + Value: [26], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: '2215832610', + }, + ], + }, + '00100020': { + vr: 'LO', + Value: ['2215832610'], + }, + '00100030': { + vr: 'DA', + Value: ['19630626'], + }, + '00100040': { + vr: 'CS', + }, + '00120062': { + vr: 'CS', + Value: ['YES'], + }, + '00180040': { + vr: 'IS', + Value: [26], + }, + '00181063': { + vr: 'DS', + Value: [38.714], + }, + '00181088': { + vr: 'IS', + Value: [0], + }, + '00185010': { + vr: 'LO', + Value: ['C5_1', '54629', null], + }, + '00185020': { + vr: 'LO', + Value: ['ABD_RENAL'], + }, + '00186011': { + vr: 'SQ', + Value: [ + { + '00186012': { + vr: 'US', + Value: [1], + }, + '00186014': { + vr: 'US', + Value: [1], + }, + '00186016': { + vr: 'UL', + Value: [2], + }, + '00186018': { + vr: 'UL', + Value: [11], + }, + '0018601A': { + vr: 'UL', + Value: [30], + }, + '0018601C': { + vr: 'UL', + Value: [788], + }, + '0018601E': { + vr: 'UL', + Value: [592], + }, + '00186024': { + vr: 'US', + Value: [3], + }, + '00186026': { + vr: 'US', + Value: [3], + }, + '0018602C': { + vr: 'FD', + Value: [0.0412587], + }, + '0018602E': { + vr: 'FD', + Value: [0.0412587], + }, + }, + ], + }, + '0020000D': { + vr: 'UI', + Value: ['9999.4873689601340706578125708148444354948'], + }, + '0020000E': { + vr: 'UI', + Value: ['9999.21671276797191441480984915045348094693'], + }, + '00200010': { + vr: 'SH', + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00200013': { + vr: 'IS', + Value: [14], + }, + '00280002': { + vr: 'US', + Value: [3], + }, + '00280004': { + vr: 'CS', + Value: ['YBR_FULL_422'], + }, + '00280006': { + vr: 'US', + Value: [0], + }, + '00280008': { + vr: 'IS', + Value: [78], + }, + '00280009': { + vr: 'AT', + Value: ['00181063'], + }, + '00280010': { + vr: 'US', + Value: [600], + }, + '00280011': { + vr: 'US', + Value: [800], + }, + '00280014': { + vr: 'US', + Value: [0], + }, + '00280100': { + vr: 'US', + Value: [8], + }, + '00280101': { + vr: 'US', + Value: [8], + }, + '00280102': { + vr: 'US', + Value: [7], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00280301': { + vr: 'CS', + Value: ['NO'], + }, + '00282110': { + vr: 'CS', + Value: ['01'], + }, + '00282112': { + vr: 'DS', + Value: [100], + }, + '00282114': { + vr: 'CS', + Value: ['ISO_10918_1'], + }, + '0032000A': { + vr: 'CS', + Value: ['READ'], + }, + '0032000C': { + vr: 'CS', + Value: ['MED'], + }, + '00400245': { + vr: 'TM', + Value: ['133414'], + }, + '00400260': { + vr: 'SQ', + Value: [ + { + '00080100': { + vr: 'SH', + Value: ['IMG5038'], + }, + '00080102': { + vr: 'SH', + Value: ['SA'], + }, + '00080104': { + vr: 'LO', + Value: ['US KIDNEYS'], + }, + }, + ], + }, + '7FE00010': { + vr: 'OW', + }, +}; diff --git a/packages/docs/package.json b/packages/docs/package.json index dca08fca78..f4e96ca122 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -38,11 +38,11 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@cornerstonejs/adapters": "4.4.1", - "@cornerstonejs/core": "4.4.1", - "@cornerstonejs/dicom-image-loader": "4.4.1", - "@cornerstonejs/nifti-volume-loader": "4.4.1", - "@cornerstonejs/tools": "4.4.1", + "@cornerstonejs/adapters": "4.5.19", + "@cornerstonejs/core": "4.5.19", + "@cornerstonejs/dicom-image-loader": "4.5.19", + "@cornerstonejs/nifti-volume-loader": "4.5.19", + "@cornerstonejs/tools": "4.5.19", "@docusaurus/core": "3.6.3", "@docusaurus/faster": "3.6.3", "@docusaurus/module-type-aliases": "3.6.3", diff --git a/packages/docs/src/version.ts b/packages/docs/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/docs/src/version.ts +++ b/packages/docs/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/labelmap-interpolation/CHANGELOG.md b/packages/labelmap-interpolation/CHANGELOG.md index 1ea4c205b3..fd0501f8c0 100644 --- a/packages/labelmap-interpolation/CHANGELOG.md +++ b/packages/labelmap-interpolation/CHANGELOG.md @@ -3,6 +3,90 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/labelmap-interpolation + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/labelmap-interpolation diff --git a/packages/labelmap-interpolation/package.json b/packages/labelmap-interpolation/package.json index fb29763520..8a7b129be8 100644 --- a/packages/labelmap-interpolation/package.json +++ b/packages/labelmap-interpolation/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/labelmap-interpolation", - "version": "4.4.1", + "version": "4.5.19", "description": "Labelmap Interpolation utility for Cornerstone3D", "files": [ "dist" @@ -55,8 +55,8 @@ "itk-wasm": "1.0.0-b.165" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1", - "@cornerstonejs/tools": "4.4.1", + "@cornerstonejs/core": "4.5.19", + "@cornerstonejs/tools": "4.5.19", "@kitware/vtk.js": "32.12.1" } } diff --git a/packages/labelmap-interpolation/src/version.ts b/packages/labelmap-interpolation/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/labelmap-interpolation/src/version.ts +++ b/packages/labelmap-interpolation/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/nifti-volume-loader/CHANGELOG.md b/packages/nifti-volume-loader/CHANGELOG.md index b0fcfd5a53..9f3f863bdd 100644 --- a/packages/nifti-volume-loader/CHANGELOG.md +++ b/packages/nifti-volume-loader/CHANGELOG.md @@ -3,6 +3,90 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/nifti-volume-loader + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/nifti-volume-loader diff --git a/packages/nifti-volume-loader/package.json b/packages/nifti-volume-loader/package.json index 31056b3ae9..f227626970 100644 --- a/packages/nifti-volume-loader/package.json +++ b/packages/nifti-volume-loader/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/nifti-volume-loader", - "version": "4.4.1", + "version": "4.5.19", "description": "Nifti Image Loader for Cornerstone3D", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", @@ -65,7 +65,7 @@ "nifti-reader-js": "0.6.9" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1" + "@cornerstonejs/core": "4.5.19" }, "contributors": [ { diff --git a/packages/nifti-volume-loader/src/version.ts b/packages/nifti-volume-loader/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/nifti-volume-loader/src/version.ts +++ b/packages/nifti-volume-loader/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/polymorphic-segmentation/CHANGELOG.md b/packages/polymorphic-segmentation/CHANGELOG.md index 05b90285a8..d42deadc49 100644 --- a/packages/polymorphic-segmentation/CHANGELOG.md +++ b/packages/polymorphic-segmentation/CHANGELOG.md @@ -3,6 +3,90 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +**Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/polymorphic-segmentation diff --git a/packages/polymorphic-segmentation/package.json b/packages/polymorphic-segmentation/package.json index 09bc8e178d..1149ad3338 100644 --- a/packages/polymorphic-segmentation/package.json +++ b/packages/polymorphic-segmentation/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/polymorphic-segmentation", - "version": "4.4.1", + "version": "4.5.19", "description": "Polymorphic Segmentation utility for Cornerstone3D", "files": [ "dist" @@ -53,8 +53,8 @@ "@icr/polyseg-wasm": "0.4.0" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1", - "@cornerstonejs/tools": "4.4.1", + "@cornerstonejs/core": "4.5.19", + "@cornerstonejs/tools": "4.5.19", "@kitware/vtk.js": "32.12.1" } } diff --git a/packages/polymorphic-segmentation/src/version.ts b/packages/polymorphic-segmentation/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/polymorphic-segmentation/src/version.ts +++ b/packages/polymorphic-segmentation/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 10d2ca2a06..89da7e0fcc 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -3,6 +3,116 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.5.19](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.18...v4.5.19) (2025-10-18) + +### Bug Fixes + +- Wrong position on rehydrate annotation ([#2404](https://github.com/cornerstonejs/cornerstone3D/issues/2404)) ([1a84852](https://github.com/cornerstonejs/cornerstone3D/commit/1a84852e1448bed31925a623c2dbd09f0cab23fb)) + +## [4.5.18](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.17...v4.5.18) (2025-10-18) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.17](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.16...v4.5.17) (2025-10-18) + +### Bug Fixes + +- planar freehand roi perimeter calculation ([#2341](https://github.com/cornerstonejs/cornerstone3D/issues/2341)) ([d3b607c](https://github.com/cornerstonejs/cornerstone3D/commit/d3b607c2596aec33d93cb7f2834c01ca2783b412)) + +## [4.5.16](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.15...v4.5.16) (2025-10-17) + +### Bug Fixes + +- **CrosshairTool:** Fix "setToolCenter" to apply the modification to the viewports camera ([#2402](https://github.com/cornerstonejs/cornerstone3D/issues/2402)) ([4028207](https://github.com/cornerstonejs/cornerstone3D/commit/402820739678f8b9db0b2bddd8c03dc447fabc85)) + +## [4.5.15](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.14...v4.5.15) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.14](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.13...v4.5.14) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.13](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.12...v4.5.13) (2025-10-16) + +### Bug Fixes + +- **segmentation:** Lock added contour annotations when segment is locked ([#2399](https://github.com/cornerstonejs/cornerstone3D/issues/2399)) ([15cfdd9](https://github.com/cornerstonejs/cornerstone3D/commit/15cfdd93ffdc55dfa70f15bec8cb1be15d1290df)) + +## [4.5.12](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.11...v4.5.12) (2025-10-16) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.11](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.10...v4.5.11) (2025-10-16) + +### Bug Fixes + +- **segmentation:** Lock contour annotations when segment is locked. ([#2395](https://github.com/cornerstonejs/cornerstone3D/issues/2395)) ([83b4092](https://github.com/cornerstonejs/cornerstone3D/commit/83b4092d76c6e115adfd12a50d399e96483622a4)) + +## [4.5.10](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.9...v4.5.10) (2025-10-16) + +### Bug Fixes + +- **MagnifyTool:** Fix MagnifyTool freeze when pressing right click ([#2388](https://github.com/cornerstonejs/cornerstone3D/issues/2388)) ([a13fce2](https://github.com/cornerstonejs/cornerstone3D/commit/a13fce2d65aa1d5b8de849ddf6255e3d42dc841a)) + +## [4.5.9](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.8...v4.5.9) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.8](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.7...v4.5.8) (2025-10-15) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.7](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.6...v4.5.7) (2025-10-14) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.6](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.5...v4.5.6) (2025-10-10) + +### Bug Fixes + +- Updated BasicStatsCalculator to change from private to protected ([#2382](https://github.com/cornerstonejs/cornerstone3D/issues/2382)) ([89a8db5](https://github.com/cornerstonejs/cornerstone3D/commit/89a8db54bec5b96a61624dcca65464982d5e40b3)) + +## [4.5.5](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.4...v4.5.5) (2025-10-09) + +### Bug Fixes + +- **interpolation:** Fixed contour segmentation interpolation for spline contours. ([#2381](https://github.com/cornerstonejs/cornerstone3D/issues/2381)) ([7d61b2c](https://github.com/cornerstonejs/cornerstone3D/commit/7d61b2c7cd1627c34b0547967c5616d01fc29f96)) + +## [4.5.4](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.3...v4.5.4) (2025-10-08) + +**Note:** Version bump only for package @cornerstonejs/tools + +## [4.5.3](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.2...v4.5.3) (2025-10-08) + +### Bug Fixes + +- **segmentation:** Added SEGMENTATION_REPRESENTATION_MODIFIED event listener to LabelmapEditWithContour tool to ensure a contour representation is added for the tool. ([#2377](https://github.com/cornerstonejs/cornerstone3D/issues/2377)) ([563fc6c](https://github.com/cornerstonejs/cornerstone3D/commit/563fc6c6b8edf987b115b540d4069a2238b88fee)) + +## [4.5.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.1...v4.5.2) (2025-10-07) + +### Bug Fixes + +- limit pan off viewport ([#2375](https://github.com/cornerstonejs/cornerstone3D/issues/2375)) ([1850916](https://github.com/cornerstonejs/cornerstone3D/commit/18509162c91b4c2be08c61d95ce75dbeeb4329f2)) + +## [4.5.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.5.0...v4.5.1) (2025-10-07) + +### Bug Fixes + +- **segmentation:** For contour segmentation logical operators, avoid updating segment color and label when they are not defined/specified. ([#2376](https://github.com/cornerstonejs/cornerstone3D/issues/2376)) ([969369a](https://github.com/cornerstonejs/cornerstone3D/commit/969369a623ea39ba9740e547fb94867183ec396b)) + +# [4.5.0](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.2...v4.5.0) (2025-10-07) + +### Features + +- **BrushTool:** segmentation brush interpolation ([#2374](https://github.com/cornerstonejs/cornerstone3D/issues/2374)) ([690ec6e](https://github.com/cornerstonejs/cornerstone3D/commit/690ec6e90499b24364c06ad404fd0d2b0c521134)) + +## [4.4.2](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.1...v4.4.2) (2025-10-06) + +### Bug Fixes + +- ellipse ROI stats ([#2368](https://github.com/cornerstonejs/cornerstone3D/issues/2368)) ([c2c6c99](https://github.com/cornerstonejs/cornerstone3D/commit/c2c6c99fc7846677ad28a36f7697fce7744080e8)) + ## [4.4.1](https://github.com/cornerstonejs/cornerstone3D/compare/v4.4.0...v4.4.1) (2025-10-03) **Note:** Version bump only for package @cornerstonejs/tools diff --git a/packages/tools/examples/stackManipulationTools/index.ts b/packages/tools/examples/stackManipulationTools/index.ts index 2ae2d86b37..8f19589b49 100644 --- a/packages/tools/examples/stackManipulationTools/index.ts +++ b/packages/tools/examples/stackManipulationTools/index.ts @@ -144,7 +144,7 @@ async function run() { // Add tools to the tool group toolGroup.addTool(WindowLevelTool.toolName); - toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(PanTool.toolName, { limitToViewport: true }); toolGroup.addTool(ZoomTool.toolName); toolGroup.addTool(StackScrollTool.toolName, { loop: false }); toolGroup.addTool(PlanarRotateTool.toolName); diff --git a/packages/tools/examples/volumeAnnotationTools/index.ts b/packages/tools/examples/volumeAnnotationTools/index.ts index cbbeb3ee64..fcd36a6b33 100644 --- a/packages/tools/examples/volumeAnnotationTools/index.ts +++ b/packages/tools/examples/volumeAnnotationTools/index.ts @@ -18,7 +18,7 @@ console.warn( ); const { - LengthTool, + LengthToolZoom, ToolGroupManager, StackScrollTool, ZoomTool, @@ -87,7 +87,7 @@ async function run() { const toolGroupId = 'STACK_TOOL_GROUP_ID'; // Add tools to Cornerstone3D - cornerstoneTools.addTool(LengthTool); + cornerstoneTools.addTool(LengthToolZoom); cornerstoneTools.addTool(ZoomTool); cornerstoneTools.addTool(StackScrollTool); @@ -96,13 +96,13 @@ async function run() { const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); // Add the tools to the tool group and specify which volume they are pointing at - toolGroup.addTool(LengthTool.toolName, { volumeId }); + toolGroup.addTool(LengthToolZoom.toolName, { volumeId }); toolGroup.addTool(ZoomTool.toolName, { volumeId }); toolGroup.addTool(StackScrollTool.toolName); // Set the initial state of the tools, here we set one tool active on left click. // This means left click will draw that tool. - toolGroup.setToolActive(LengthTool.toolName, { + toolGroup.setToolActive(LengthToolZoom.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click diff --git a/packages/tools/jest.config.js b/packages/tools/jest.config.js index 6f8cd76c24..682de0e841 100644 --- a/packages/tools/jest.config.js +++ b/packages/tools/jest.config.js @@ -5,6 +5,7 @@ const path = require('path'); module.exports = { ...base, displayName: 'tools', + testMatch: [...base.testMatch, '/src/**/*.spec.ts'], moduleNameMapper: { '^@cornerstonejs/(.*)$': path.resolve(__dirname, '../$1/src'), }, diff --git a/packages/tools/package.json b/packages/tools/package.json index 40304559c2..e28cdec7b2 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@cornerstonejs/tools", - "version": "4.4.1", + "version": "4.5.19", "description": "Cornerstone3D Tools", "types": "./dist/esm/index.d.ts", "module": "./dist/esm/index.js", @@ -108,7 +108,7 @@ "canvas": "3.1.0" }, "peerDependencies": { - "@cornerstonejs/core": "4.4.1", + "@cornerstonejs/core": "4.5.19", "@kitware/vtk.js": "32.12.1", "@types/d3-array": "3.2.1", "@types/d3-interpolate": "3.0.4", diff --git a/packages/tools/src/drawingSvg/drawLine.ts b/packages/tools/src/drawingSvg/drawLine.ts index 1d324b4425..156cdcc858 100644 --- a/packages/tools/src/drawingSvg/drawLine.ts +++ b/packages/tools/src/drawingSvg/drawLine.ts @@ -28,6 +28,7 @@ export default function drawLine( markerEndId = null, shadow = false, strokeOpacity = 1, + lineCap, } = options as { color?: string; width?: string; @@ -37,6 +38,7 @@ export default function drawLine( markerEndId?: string; shadow?: boolean; strokeOpacity?: number; + lineCap?: 'butt' | 'round' | 'square'; }; // for supporting both lineWidth and width options @@ -45,8 +47,21 @@ export default function drawLine( const svgns = 'http://www.w3.org/2000/svg'; const svgNodeHash = _getHash(annotationUID, 'line', lineUID); const existingLine = svgDrawingHelper.getSvgNode(svgNodeHash); - const layerId = svgDrawingHelper.svgLayerElement.id; - const dropShadowStyle = shadow ? `filter:url(#shadow-${layerId});` : ''; + // Ursprünglich wurde die ID des aktuellen Layer- verwendet. Der Shadow-Filter + // ist jedoch auf dem Root- ("svg-layer-") definiert. + // In Safari führt eine Referenz auf einen nicht existierenden Filter dazu, dass die Linie + // (teilweise) nicht gerendert wird. Wir traversieren daher zum übergeordneten . + let filterHost: Element | null = svgDrawingHelper.svgLayerElement as Element; + if (filterHost && filterHost.tagName.toLowerCase() !== 'svg') { + // Nutzung von closest('svg') ist breit unterstützt; Falls nicht vorhanden, fallback auf ursprüngliches Element + const parentSvg = (filterHost as HTMLElement).closest('svg'); + if (parentSvg) { + filterHost = parentSvg; + } + } + const rootSvgId = (filterHost as SVGElement)?.id; + const dropShadowStyle = + shadow && rootSvgId ? `filter:url(#shadow-${rootSvgId});` : ''; const attributes = { x1: `${start[0]}`, @@ -60,7 +75,8 @@ export default function drawLine( 'marker-start': markerStartId ? `url(#${markerStartId})` : '', 'marker-end': markerEndId ? `url(#${markerEndId})` : '', 'stroke-opacity': strokeOpacity, - }; + ...(lineCap ? { 'stroke-linecap': lineCap } : {}), + } as Record; if (existingLine) { // This is run to avoid re-rendering annotations that actually haven't changed diff --git a/packages/tools/src/drawingSvg/drawLink.ts b/packages/tools/src/drawingSvg/drawLink.ts index 1e5dd70e2b..7d1f14dabe 100644 --- a/packages/tools/src/drawingSvg/drawLink.ts +++ b/packages/tools/src/drawingSvg/drawLink.ts @@ -41,6 +41,12 @@ function drawLink( options ); + const linkColor = (mergedOptions as { linkColor?: string }).linkColor; + + if (linkColor) { + mergedOptions.color = linkColor; + } + drawLine( svgDrawingHelper, annotationUID, diff --git a/packages/tools/src/drawingSvg/drawLinkedTextBox.ts b/packages/tools/src/drawingSvg/drawLinkedTextBox.ts index 987a6e6544..0e6ff82dc4 100644 --- a/packages/tools/src/drawingSvg/drawLinkedTextBox.ts +++ b/packages/tools/src/drawingSvg/drawLinkedTextBox.ts @@ -23,10 +23,15 @@ function drawLinkedTextBox( x: false, y: true, // yCenter, }, + drawLink: true, }, options ); + const { drawLink: shouldDrawLink, ...forwardedOptions } = mergedOptions as { + drawLink?: boolean; + } & Record; + // Draw the text box const canvasBoundingBox = drawTextBox( svgDrawingHelper, @@ -34,19 +39,21 @@ function drawLinkedTextBox( textBoxUID, textLines, textBoxPosition, - mergedOptions + forwardedOptions ); // if (textBox.hasMoved) { // // Draw dashed link line between tool and text - drawLink( - svgDrawingHelper, - annotationUID, - textBoxUID, - annotationAnchorPoints, // annotationAnchorPoints - textBoxPosition, // refPoint (text) - canvasBoundingBox, // textBoxBoundingBox - mergedOptions - ); + if (shouldDrawLink) { + drawLink( + svgDrawingHelper, + annotationUID, + textBoxUID, + annotationAnchorPoints, // annotationAnchorPoints + textBoxPosition, // refPoint (text) + canvasBoundingBox, // textBoxBoundingBox + forwardedOptions + ); + } // } // const { top, left, width, height } = canvasBoundingBox diff --git a/packages/tools/src/drawingSvg/drawTextBox.ts b/packages/tools/src/drawingSvg/drawTextBox.ts index 8289fe1af8..7bf926d741 100644 --- a/packages/tools/src/drawingSvg/drawTextBox.ts +++ b/packages/tools/src/drawingSvg/drawTextBox.ts @@ -24,11 +24,19 @@ function drawTextBox( { fontFamily: 'Helvetica, Arial, sans-serif', fontSize: '14px', + fontWeight: 'normal', + fontStyle: 'normal', + lineHeight: '1.2em', + textShadow: '', color: 'rgb(255, 255, 0)', background: '', padding: 25, centerX: false, centerY: true, + borderColor: '', + borderWidth: 0, + borderRadius: 0, + backgroundPadding: 0, }, options ); @@ -55,7 +63,19 @@ function _drawTextGroup( // eslint-disable-next-line @typescript-eslint/no-explicit-any options: Record ): SVGRect { - const { padding, color, fontFamily, fontSize, background } = options; + const { + padding, + color, + fontFamily, + fontSize, + background, + fontWeight, + fontStyle, + lineHeight, + } = options; + + const defaultLineHeight = lineHeight || '1.2em'; + const lineHeightDy = _resolveLineHeightDy(defaultLineHeight); let textGroupBoundingBox; const [x, y] = [position[0] + padding, position[1] + padding]; @@ -75,13 +95,14 @@ function _drawTextGroup( const text = textLines[i] || ''; textSpanElement.textContent = text; + textSpanElement.setAttribute('dy', lineHeightDy); } // if the textLines have changed size, we need to create textSpans for them if (textLines.length > textSpans.length) { for (let i = 0; i < textLines.length - textSpans.length; i++) { const textLine = textLines[i + textSpans.length]; - const textSpan = _createTextSpan(textLine); + const textSpan = _createTextSpan(textLine, lineHeightDy); textElement.appendChild(textSpan); } @@ -90,10 +111,15 @@ function _drawTextGroup( svgDrawingHelper.appendNode(existingTextGroup, svgNodeHash); } + const combinedStyle = _getTextElementStyle(svgDrawingHelper, options); + const textAttributes = { fill: color, 'font-size': fontSize, 'font-family': fontFamily, + ...(fontStyle ? { 'font-style': fontStyle } : {}), + ...(fontWeight ? { 'font-weight': fontWeight } : {}), + style: combinedStyle, }; const textGroupAttributes = { @@ -107,7 +133,7 @@ function _drawTextGroup( // Add data attribute for annotation UID existingTextGroup.setAttribute('data-annotation-uid', annotationUID); - textGroupBoundingBox = _drawTextBackground(existingTextGroup, background); + textGroupBoundingBox = _drawTextBackground(existingTextGroup, options); svgDrawingHelper.setNodeTouched(svgNodeHash); } else { @@ -121,14 +147,14 @@ function _drawTextGroup( const textElement = _createTextElement(svgDrawingHelper, options); for (let i = 0; i < textLines.length; i++) { const textLine = textLines[i]; - const textSpan = _createTextSpan(textLine); + const textSpan = _createTextSpan(textLine, lineHeightDy); textElement.appendChild(textSpan); } textGroup.appendChild(textElement); svgDrawingHelper.appendNode(textGroup, svgNodeHash); - textGroupBoundingBox = _drawTextBackground(textGroup, background); + textGroupBoundingBox = _drawTextBackground(textGroup, options); } // We translate the group using `position` @@ -147,26 +173,29 @@ function _createTextElement( // eslint-disable-next-line @typescript-eslint/no-explicit-any options: Record ): SVGElement { - const { color, fontFamily, fontSize } = options; + const { color, fontFamily, fontSize, fontWeight, fontStyle } = options; const svgns = 'http://www.w3.org/2000/svg'; const textElement = document.createElementNS(svgns, 'text'); - const noSelectStyle = - 'user-select: none; pointer-events: none; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);'; - const dropShadowStyle = `filter:url(#shadow-${svgDrawingHelper.svgLayerElement.id});`; - const combinedStyle = `${noSelectStyle}${dropShadowStyle}`; + const combinedStyle = _getTextElementStyle(svgDrawingHelper, options); textElement.setAttribute('x', '0'); textElement.setAttribute('y', '0'); textElement.setAttribute('fill', color); textElement.setAttribute('font-family', fontFamily); textElement.setAttribute('font-size', fontSize); + if (fontWeight) { + textElement.setAttribute('font-weight', fontWeight); + } + if (fontStyle) { + textElement.setAttribute('font-style', fontStyle); + } textElement.setAttribute('style', combinedStyle); textElement.setAttribute('pointer-events', 'visible'); return textElement; } -function _createTextSpan(text): SVGElement { +function _createTextSpan(text, lineHeight): SVGElement { const svgns = 'http://www.w3.org/2000/svg'; const textSpanElement = document.createElementNS(svgns, 'tspan'); @@ -175,18 +204,86 @@ function _createTextSpan(text): SVGElement { // TODO: centerY textSpanElement.setAttribute('x', '0'); - textSpanElement.setAttribute('dy', '1.2em'); + textSpanElement.setAttribute('dy', lineHeight || '1.2em'); textSpanElement.textContent = text; return textSpanElement; } -function _drawTextBackground(group: SVGGElement, color: string) { - let element = group.querySelector('rect.background'); +function _resolveLineHeightDy(lineHeight: string): string { + if (!lineHeight) { + return '1.2em'; + } + + if (lineHeight.startsWith('var(')) { + const fallbackMatch = lineHeight.match(/var\([^,]+,\s*([^)]+)\)/); + + if (fallbackMatch && fallbackMatch[1]) { + return fallbackMatch[1].trim(); + } + } - // If we have no background color, remove any element that exists and return - // the bounding box of the text - if (!color) { + return lineHeight; +} + +function _getTextElementStyle( + svgDrawingHelper: SVGDrawingHelper, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: Record +): string { + const noSelectStyle = + 'user-select: none; pointer-events: none; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);'; + const dropShadowStyle = `filter:url(#shadow-${svgDrawingHelper.svgLayerElement.id});`; + const textShadowStyle = options.textShadow + ? `text-shadow: ${options.textShadow};` + : ''; + const lineHeightStyle = options.lineHeight + ? `line-height: ${options.lineHeight};` + : ''; + + return `${noSelectStyle}${dropShadowStyle}${textShadowStyle}${lineHeightStyle}`; +} + +function _drawTextBackground( + group: SVGGElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: Record +) { + const { + background: color, + borderColor, + borderWidth, + borderRadius, + backgroundPadding, + } = options; + let element = group.querySelector('rect.background') as SVGRectElement | null; + + const borderWidthValue = + borderWidth === undefined + ? 0 + : typeof borderWidth === 'number' + ? borderWidth + : parseFloat(borderWidth); + const borderRadiusValue = + borderRadius === undefined + ? 0 + : typeof borderRadius === 'number' + ? borderRadius + : parseFloat(borderRadius); + const backgroundPaddingValue = + backgroundPadding === undefined + ? 0 + : typeof backgroundPadding === 'number' + ? backgroundPadding + : parseFloat(backgroundPadding); + + const hasBorderColor = typeof borderColor === 'string' && borderColor !== ''; + const hasBorder = Boolean(hasBorderColor && borderWidthValue > 0); + const shouldDrawRect = Boolean(color || hasBorder); + + // If we have no background color and no border, remove any element that exists + // and return the bounding box of the text + if (!shouldDrawRect) { if (element) { group.removeChild(element); } @@ -196,25 +293,37 @@ function _drawTextBackground(group: SVGGElement, color: string) { // Otherwise, check if we have a element. If not, create one if (!element) { - element = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + element = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'rect' + ) as SVGRectElement; element.setAttribute('class', 'background'); group.insertBefore(element, group.firstChild); } - // Get the text groups's bounding box and use it to draw the background rectangle - const bBox = group.getBBox(); + // Measure the text bounding box to expand it with background padding + const textElement = group.querySelector('text') as SVGGraphicsElement | null; + const textBBox = textElement ? textElement.getBBox() : group.getBBox(); + const xWithPadding = textBBox.x - backgroundPaddingValue; + const yWithPadding = textBBox.y - backgroundPaddingValue; + const widthWithPadding = textBBox.width + backgroundPaddingValue * 2; + const heightWithPadding = textBBox.height + backgroundPaddingValue * 2; const attributes = { - x: `${bBox.x}`, - y: `${bBox.y}`, - width: `${bBox.width}`, - height: `${bBox.height}`, - fill: color, + x: `${xWithPadding}`, + y: `${yWithPadding}`, + width: `${widthWithPadding}`, + height: `${heightWithPadding}`, + fill: color || 'none', + stroke: hasBorder ? borderColor : 'none', + 'stroke-width': hasBorder ? `${borderWidthValue}` : '0', + rx: borderRadiusValue > 0 ? `${borderRadiusValue}` : '0', + ry: borderRadiusValue > 0 ? `${borderRadiusValue}` : '0', }; setAttributesIfNecessary(attributes, element); - return bBox; + return element!.getBBox(); } export default drawTextBox; diff --git a/packages/tools/src/drawingSvg/getSvgDrawingHelper.ts b/packages/tools/src/drawingSvg/getSvgDrawingHelper.ts index 3cc8698cbc..5e431baf01 100644 --- a/packages/tools/src/drawingSvg/getSvgDrawingHelper.ts +++ b/packages/tools/src/drawingSvg/getSvgDrawingHelper.ts @@ -20,14 +20,17 @@ function getSvgDrawingHelper(element: HTMLDivElement): SVGDrawingHelper { state.svgNodeCache[canvasHash][cacheKey].touched = false; }); - return { - svgLayerElement: svgLayerElement, + const svgDrawingHelper = { + svgLayerElement: svgLayerElement as unknown as Element, svgNodeCacheForCanvas: state.svgNodeCache, getSvgNode: getSvgNode.bind(this, canvasHash), - appendNode: appendNode.bind(this, svgLayerElement, canvasHash), + appendNode: (svgNode: SVGElement, cacheKey: string) => + appendNode(svgDrawingHelper, canvasHash, svgNode, cacheKey), setNodeTouched: setNodeTouched.bind(this, canvasHash), - clearUntouched: clearUntouched.bind(this, svgLayerElement, canvasHash), - }; + clearUntouched: () => clearUntouched(svgDrawingHelper, canvasHash), + } as SVGDrawingHelper; + + return svgDrawingHelper; } /** @@ -57,10 +60,15 @@ function getSvgNode(canvasHash, cacheKey) { } } -function appendNode(svgLayerElement, canvasHash, svgNode, cacheKey) { +function appendNode( + svgDrawingHelper: SVGDrawingHelper, + canvasHash, + svgNode, + cacheKey +) { // If state has been reset if (!state.svgNodeCache[canvasHash]) { - return null; + return; } state.svgNodeCache[canvasHash][cacheKey] = { @@ -68,7 +76,11 @@ function appendNode(svgLayerElement, canvasHash, svgNode, cacheKey) { domRef: svgNode, }; - svgLayerElement.appendChild(svgNode); + const targetLayer = svgDrawingHelper.svgLayerElement; + + if (targetLayer) { + targetLayer.appendChild(svgNode); + } } function setNodeTouched(canvasHash, cacheKey) { @@ -82,17 +94,25 @@ function setNodeTouched(canvasHash, cacheKey) { } } -function clearUntouched(svgLayerElement, canvasHash) { +function clearUntouched(svgDrawingHelper: SVGDrawingHelper, canvasHash) { // If state has been reset if (!state.svgNodeCache[canvasHash]) { return; } + const rootLayer = svgDrawingHelper.svgLayerElement; + Object.keys(state.svgNodeCache[canvasHash]).forEach((cacheKey) => { const cacheEntry = state.svgNodeCache[canvasHash][cacheKey]; if (!cacheEntry.touched && cacheEntry.domRef) { - svgLayerElement.removeChild(cacheEntry.domRef); + const parent = cacheEntry.domRef.parentNode; + + if (parent) { + parent.removeChild(cacheEntry.domRef); + } else if (rootLayer?.contains(cacheEntry.domRef)) { + rootLayer.removeChild(cacheEntry.domRef); + } delete state.svgNodeCache[canvasHash][cacheKey]; } }); diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 670f962cff..94cc70aa4f 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -88,6 +88,8 @@ import { LabelMapEditWithContourTool, } from './tools'; +import LengthToolZoom from './tools/annotation/LengthToolZoom/LengthToolZoom'; + import VideoRedactionTool from './tools/annotation/VideoRedactionTool'; import * as Enums from './enums'; @@ -120,6 +122,7 @@ export { // Annotation Tools LabelTool, LengthTool, + LengthToolZoom, HeightTool, CrosshairsTool, ReferenceLinesTool, diff --git a/packages/tools/src/stateManagement/segmentation/segmentLocking.ts b/packages/tools/src/stateManagement/segmentation/segmentLocking.ts index 6fca8077d1..62a3d002cc 100644 --- a/packages/tools/src/stateManagement/segmentation/segmentLocking.ts +++ b/packages/tools/src/stateManagement/segmentation/segmentLocking.ts @@ -1,5 +1,37 @@ import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation'; +import type { Segmentation } from '../../types'; +import { setAnnotationLocked } from '../annotation/annotationLocking'; import { triggerSegmentationModified } from './triggerSegmentationEvents'; +import { getAnnotationsUIDMapFromSegmentation } from './utilities'; + +/** + * Set the locked status of every annotation in a segment. + * @param segmentation - The segmentation to set the locked status for. + * @param segmentIndex - The index of the segment to set the locked status for. + * @param locked - The locked status to set. + */ +function _setContourSegmentationSegmentAnnotationsLocked( + segmentation: Segmentation, + segmentIndex: number, + locked: boolean +) { + const annotationUIDsMap = getAnnotationsUIDMapFromSegmentation( + segmentation.segmentationId + ); + + if (!annotationUIDsMap) { + return; + } + + const annotationUIDs = annotationUIDsMap.get(segmentIndex); + if (!annotationUIDs) { + return; + } + + annotationUIDs.forEach((annotationUID) => { + setAnnotationLocked(annotationUID, locked); + }); +} /** * Get the locked status for a segment index in a segmentation @@ -43,6 +75,14 @@ function setSegmentIndexLocked( segments[segmentIndex].locked = locked; + if (segmentation?.representationData?.Contour) { + _setContourSegmentationSegmentAnnotationsLocked( + segmentation, + segmentIndex, + locked + ); + } + triggerSegmentationModified(segmentationId); } diff --git a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts index 8537b8aba8..026f536bed 100644 --- a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts +++ b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts @@ -653,6 +653,13 @@ export default class ToolGroup { return cursor; } + // Fall back to a standard CSS cursor registered for the tool name. + cursor = MouseCursor.getDefinedCursor(cursorName); + + if (cursor) { + return cursor; + } + return MouseCursor.getDefinedCursor('default'); } diff --git a/packages/tools/src/tools/CrosshairsTool.ts b/packages/tools/src/tools/CrosshairsTool.ts index c122fb0ca5..0dd3474bea 100644 --- a/packages/tools/src/tools/CrosshairsTool.ts +++ b/packages/tools/src/tools/CrosshairsTool.ts @@ -4,7 +4,7 @@ import vtkMatrixBuilder from '@kitware/vtk.js/Common/Core/MatrixBuilder'; import { AnnotationTool } from './base'; -import type { Types } from '@cornerstonejs/core'; +import { getRenderingEngine, type Types } from '@cornerstonejs/core'; import { getEnabledElementByIds, getEnabledElement, @@ -412,13 +412,57 @@ class CrosshairsTool extends AnnotationTool { setToolCenter(toolCenter: Types.Point3, suppressEvents = false): void { // prettier-ignore - this.toolCenter = toolCenter; const viewportsInfo = this._getViewportsInfo(); - // assuming all viewports are in the same rendering engine - triggerAnnotationRenderForViewportIds( - viewportsInfo.map(({ viewportId }) => viewportId) - ); + viewportsInfo.map(({ renderingEngineId, viewportId }) => { + const renderingEngine = getRenderingEngine(renderingEngineId); + + const viewport = renderingEngine.getViewport(viewportId); + const camera = viewport.getCamera(); + const { focalPoint, position, viewPlaneNormal } = camera; + + // Calculate the delta between the current camera focal point and the new tool center + const delta = [ + toolCenter[0] - focalPoint[0], + toolCenter[1] - focalPoint[1], + toolCenter[2] - focalPoint[2], + ]; + + // Project this vector onto the view plane normal. + // This isolates the component of the movement that corresponds to the "scroll" (slice change). + const scroll = + delta[0] * viewPlaneNormal[0] + + delta[1] * viewPlaneNormal[1] + + delta[2] * viewPlaneNormal[2]; + + const scrollDelta = [ + scroll * viewPlaneNormal[0], + scroll * viewPlaneNormal[1], + scroll * viewPlaneNormal[2], + ]; + + // Apply this "scroll" to the position and focal point of the camera. + const newFocalPoint: Types.Point3 = [ + focalPoint[0] + scrollDelta[0], + focalPoint[1] + scrollDelta[1], + focalPoint[2] + scrollDelta[2], + ]; + const newPosition: Types.Point3 = [ + position[0] + scrollDelta[0], + position[1] + scrollDelta[1], + position[2] + scrollDelta[2], + ]; + + viewport.setCamera({ + focalPoint: newFocalPoint, + position: newPosition, + }); + + viewport.render(); + }); + + this.toolCenter = toolCenter; + if (!suppressEvents) { triggerEvent(eventTarget, Events.CROSSHAIR_TOOL_CENTER_CHANGED, { toolGroupId: this.toolGroupId, diff --git a/packages/tools/src/tools/MagnifyTool.ts b/packages/tools/src/tools/MagnifyTool.ts index a97838b051..93f58fd707 100644 --- a/packages/tools/src/tools/MagnifyTool.ts +++ b/packages/tools/src/tools/MagnifyTool.ts @@ -204,6 +204,15 @@ class MagnifyTool extends BaseTool { triggerAnnotationRenderForViewportIds(viewportIdsToRender); }; + _cancelCallback = (evt: EventTypes.InteractionEventType) => { + // Empêche l'affichage du menu contextuel par défaut du navigateur + evt.preventDefault(); + evt.stopPropagation(); + + // Appelle la fonction qui désactive et nettoie l'outil + this._dragEndCallback(evt); + }; + _dragCallback = (evt: EventTypes.InteractionEventType) => { const eventDetail = evt.detail; @@ -253,7 +262,15 @@ class MagnifyTool extends BaseTool { }; _dragEndCallback = (evt: EventTypes.InteractionEventType) => { - const { element } = evt.detail; + let { element } = evt.detail; + + if (element === undefined) { + const { enabledElement } = this.editData; + + const { viewport } = enabledElement; + element = viewport.element; + } + const enabledElement = getEnabledElement(element); const { renderingEngine } = enabledElement; @@ -289,6 +306,11 @@ class MagnifyTool extends BaseTool { this._dragEndCallback as EventListener ); + element.addEventListener( + 'contextmenu', + this._cancelCallback as EventListener + ); + element.addEventListener( Events.TOUCH_END, this._dragEndCallback as EventListener @@ -314,6 +336,11 @@ class MagnifyTool extends BaseTool { Events.MOUSE_CLICK, this._dragEndCallback as EventListener ); + + element.removeEventListener( + 'contextmenu', + this._cancelCallback as EventListener + ); element.removeEventListener( Events.TOUCH_END, this._dragEndCallback as EventListener diff --git a/packages/tools/src/tools/PanTool.ts b/packages/tools/src/tools/PanTool.ts index ff96b39c4f..9c664f7451 100644 --- a/packages/tools/src/tools/PanTool.ts +++ b/packages/tools/src/tools/PanTool.ts @@ -1,5 +1,5 @@ import { BaseTool } from './base'; -import { getEnabledElement } from '@cornerstonejs/core'; +import { getEnabledElement, utilities as csUtils } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; import type { EventTypes, PublicToolProps, ToolProps } from '../types'; @@ -13,6 +13,9 @@ class PanTool extends BaseTool { toolProps: PublicToolProps = {}, defaultToolProps: ToolProps = { supportedInteractionTypes: ['Mouse', 'Touch'], + configuration: { + limitToViewport: false, + }, } ) { super(toolProps, defaultToolProps); @@ -26,11 +29,79 @@ class PanTool extends BaseTool { this._dragCallback(evt); } + _checkImageInViewport(viewport, deltaPointsCanvas: Types.Point2) { + const { canvas } = viewport; + const ratio = window.devicePixelRatio; + + const viewportLeft = 0; + const viewportRight = canvas.width / ratio; + const viewportTop = 0; + const viewportBottom = canvas.height / ratio; + + const defaultActor = viewport.getDefaultActor(); + const renderer = viewport.getRenderer(); + + let bounds; + if (defaultActor && csUtils.isImageActor(defaultActor)) { + // Use the default actor's bounds + const imageData = defaultActor.actor.getMapper().getInputData(); + bounds = imageData.getBounds(); + } else { + // Fallback to all actors if no default image actor is found + bounds = renderer.computeVisiblePropBounds(); + } + + const [imageLeft, imageTop] = viewport.worldToCanvas([ + bounds[0], + bounds[2], + bounds[4], + ]); + const [imageRight, imageBottom] = viewport.worldToCanvas([ + bounds[1], + bounds[3], + bounds[5], + ]); + + const zoom = viewport.getZoom(); + + // Check image bounds against viewport bounds + if (zoom <= 1) { + if ( + (imageLeft + deltaPointsCanvas[0] < viewportLeft && + deltaPointsCanvas[0] < 0) || + (imageRight + deltaPointsCanvas[0] > viewportRight && + deltaPointsCanvas[0] > 0) || + (imageTop + deltaPointsCanvas[1] < viewportTop && + deltaPointsCanvas[1] < 0) || + (imageBottom + deltaPointsCanvas[1] > viewportBottom && + deltaPointsCanvas[1] > 0) + ) { + return false; + } + } else { + if ( + (imageLeft + deltaPointsCanvas[0] > viewportLeft && + deltaPointsCanvas[0] > 0) || + (imageRight + deltaPointsCanvas[0] < viewportRight && + deltaPointsCanvas[0] < 0) || + (imageTop + deltaPointsCanvas[1] > viewportTop && + deltaPointsCanvas[1] > 0) || + (imageBottom + deltaPointsCanvas[1] < viewportBottom && + deltaPointsCanvas[1] < 0) + ) { + return false; + } + } + + return true; + } + _dragCallback(evt: EventTypes.InteractionEventType) { const { element, deltaPoints } = evt.detail; const enabledElement = getEnabledElement(element); const deltaPointsWorld = deltaPoints.world; + const deltaPointsCanvas = deltaPoints.canvas; // This occurs when the mouse event is fired but the mouse hasn't moved a full pixel yet (high resolution mice) if ( deltaPointsWorld[0] === 0 && @@ -39,9 +110,17 @@ class PanTool extends BaseTool { ) { return; } - const camera = enabledElement.viewport.getCamera(); + const viewport = enabledElement.viewport; + const camera = viewport.getCamera(); const { focalPoint, position } = camera; + if ( + this.configuration.limitToViewport && + !this._checkImageInViewport(viewport, deltaPointsCanvas) + ) { + return; + } + const updatedPosition = [ position[0] - deltaPointsWorld[0], position[1] - deltaPointsWorld[1], @@ -54,11 +133,11 @@ class PanTool extends BaseTool { focalPoint[2] - deltaPointsWorld[2], ]; - enabledElement.viewport.setCamera({ + viewport.setCamera({ focalPoint: updatedFocalPoint, position: updatedPosition, }); - enabledElement.viewport.render(); + viewport.render(); } } diff --git a/packages/tools/src/tools/annotation/ArrowAnnotateTool.ts b/packages/tools/src/tools/annotation/ArrowAnnotateTool.ts index 52850b09c8..f01812bf8e 100644 --- a/packages/tools/src/tools/annotation/ArrowAnnotateTool.ts +++ b/packages/tools/src/tools/annotation/ArrowAnnotateTool.ts @@ -149,6 +149,7 @@ class ArrowAnnotateTool extends AnnotationTool { addNewAnnotation = ( evt: EventTypes.InteractionEventType ): ArrowAnnotation => { + this.startGroupRecording(); const eventDetail = evt.detail; const { currentPoints, element } = eventDetail; const worldPos = currentPoints.world; @@ -372,7 +373,8 @@ class ArrowAnnotateTool extends AnnotationTool { // This is only new if it wasn't already memoed this.createMemo(element, annotation, { newAnnotation: !!this.memo }); setAnnotationLabel(annotation, element, label); - + this.endGroupRecording(); + this.doneEditMemo(); triggerAnnotationRenderForViewportIds(viewportIdsToRender); }); } else if (!movingTextBox) { diff --git a/packages/tools/src/tools/annotation/EllipticalROITool.ts b/packages/tools/src/tools/annotation/EllipticalROITool.ts index d836879c75..8a37a3a202 100644 --- a/packages/tools/src/tools/annotation/EllipticalROITool.ts +++ b/packages/tools/src/tools/annotation/EllipticalROITool.ts @@ -5,10 +5,14 @@ import { VolumeViewport, utilities as csUtils, getEnabledElementByViewportId, + EPSILON, } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits'; +import { + getCalibratedAspect, + getCalibratedLengthUnitsAndScale, +} from '../../utilities/getCalibratedUnits'; import throttle from '../../utilities/throttle'; import { addAnnotation, @@ -1039,111 +1043,118 @@ class EllipticalROITool extends AnnotationTool { pos1Index[1] = Math.floor(pos1Index[1]); pos1Index[2] = Math.floor(pos1Index[2]); - const post2Index = transformWorldToIndex(imageData, worldPos2); + const pos2Index = transformWorldToIndex(imageData, worldPos2); - post2Index[0] = Math.floor(post2Index[0]); - post2Index[1] = Math.floor(post2Index[1]); - post2Index[2] = Math.floor(post2Index[2]); + pos2Index[0] = Math.floor(pos2Index[0]); + pos2Index[1] = Math.floor(pos2Index[1]); + pos2Index[2] = Math.floor(pos2Index[2]); // Check if one of the indexes are inside the volume, this then gives us // Some area to do stats over. - this.isHandleOutsideImage = !this._isInsideVolume( - pos1Index, - post2Index, - dimensions - ); - - const iMin = Math.min(pos1Index[0], post2Index[0]); - const iMax = Math.max(pos1Index[0], post2Index[0]); - - const jMin = Math.min(pos1Index[1], post2Index[1]); - const jMax = Math.max(pos1Index[1], post2Index[1]); - - const kMin = Math.min(pos1Index[2], post2Index[2]); - const kMax = Math.max(pos1Index[2], post2Index[2]); - - const boundsIJK = [ - [iMin, iMax], - [jMin, jMax], - [kMin, kMax], - ] as [Types.Point2, Types.Point2, Types.Point2]; - - const center = [ - (topLeftWorld[0] + bottomRightWorld[0]) / 2, - (topLeftWorld[1] + bottomRightWorld[1]) / 2, - (topLeftWorld[2] + bottomRightWorld[2]) / 2, - ] as Types.Point3; - - const ellipseObj = { - center, - xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2, - yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2, - zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2, - }; - - const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints( - viewPlaneNormal, - viewUp, - worldPos1, - worldPos2 - ); - const isEmptyArea = worldWidth === 0 && worldHeight === 0; + if (this._isInsideVolume(pos1Index, pos2Index, dimensions)) { + const iMin = Math.min(pos1Index[0], pos2Index[0]); + const iMax = Math.max(pos1Index[0], pos2Index[0]); + + const jMin = Math.min(pos1Index[1], pos2Index[1]); + const jMax = Math.max(pos1Index[1], pos2Index[1]); + + const kMin = Math.min(pos1Index[2], pos2Index[2]); + const kMax = Math.max(pos1Index[2], pos2Index[2]); + + const boundsIJK = [ + [iMin, iMax], + [jMin, jMax], + [kMin, kMax], + ] as [Types.Point2, Types.Point2, Types.Point2]; + + const center = [ + (topLeftWorld[0] + bottomRightWorld[0]) / 2, + (topLeftWorld[1] + bottomRightWorld[1]) / 2, + (topLeftWorld[2] + bottomRightWorld[2]) / 2, + ] as Types.Point3; + + const xRadius = Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2; + const yRadius = Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2; + const zRadius = Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2; + + const ellipseObj = { + center, + xRadius: xRadius < EPSILON / 2 ? 0 : xRadius, + yRadius: yRadius < EPSILON / 2 ? 0 : yRadius, + zRadius: zRadius < EPSILON / 2 ? 0 : zRadius, + }; - const handles = [pos1Index, post2Index]; - const { scale, areaUnit } = getCalibratedLengthUnitsAndScale( - image, - handles - ); + const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints( + viewPlaneNormal, + viewUp, + worldPos1, + worldPos2 + ); + const isEmptyArea = worldWidth === 0 && worldHeight === 0; - const area = - Math.abs(Math.PI * (worldWidth / 2) * (worldHeight / 2)) / - scale / - scale; + const handles = [pos1Index, pos2Index]; + const { scale, unit, areaUnit } = getCalibratedLengthUnitsAndScale( + image, + handles + ); + const aspect = getCalibratedAspect(image); + const area = Math.abs( + Math.PI * + (worldWidth / scale / 2) * + (worldHeight / aspect / scale / 2) + ); - const pixelUnitsOptions = { - isPreScaled: isViewportPreScaled(viewport, targetId), + const pixelUnitsOptions = { + isPreScaled: isViewportPreScaled(viewport, targetId), + isSuvScaled: this.isSuvScaled( + viewport, + targetId, + annotation.metadata.referencedImageId + ), + }; - isSuvScaled: this.isSuvScaled( - viewport, - targetId, - annotation.metadata.referencedImageId - ), - }; + const modalityUnit = getPixelValueUnits( + metadata.Modality, + annotation.metadata.referencedImageId, + pixelUnitsOptions + ); - const modalityUnit = getPixelValueUnits( - metadata.Modality, - annotation.metadata.referencedImageId, - pixelUnitsOptions - ); + let pointsInShape; + if (voxelManager) { + pointsInShape = voxelManager.forEach( + this.configuration.statsCalculator.statsCallback, + { + isInObject: (pointLPS) => + pointInEllipse(ellipseObj, pointLPS, { fast: true }), + boundsIJK, + imageData, + returnPoints: this.configuration.storePointData, + } + ); + } + const stats = this.configuration.statsCalculator.getStatistics(); + + cachedStats[targetId] = { + Modality: metadata.Modality, + area, + mean: stats.mean?.value, + max: stats.max?.value, + min: stats.min?.value, + stdDev: stats.stdDev?.value, + statsArray: stats.array, + pointsInShape, + isEmptyArea, + areaUnit, + modalityUnit, + }; + } else { + this.isHandleOutsideImage = true; - let pointsInShape; - if (voxelManager) { - const pointsInShape = voxelManager.forEach( - this.configuration.statsCalculator.statsCallback, - { - boundsIJK, - imageData, - isInObject: (pointLPS) => - pointInEllipse(ellipseObj, pointLPS, { fast: true }), - returnPoints: this.configuration.storePointData, - } - ); + cachedStats[targetId] = { + Modality: metadata.Modality, + }; } - const stats = this.configuration.statsCalculator.getStatistics(); - cachedStats[targetId] = { - Modality: metadata.Modality, - area, - mean: stats.mean?.value, - max: stats.max?.value, - min: stats.min?.value, - stdDev: stats.stdDev?.value, - statsArray: stats.array, - pointsInShape, - isEmptyArea, - areaUnit, - modalityUnit, - }; } const invalidated = annotation.invalidated; diff --git a/packages/tools/src/tools/annotation/LengthToolZoom/LengthToolZoom.ts b/packages/tools/src/tools/annotation/LengthToolZoom/LengthToolZoom.ts new file mode 100644 index 0000000000..5b048ca51c --- /dev/null +++ b/packages/tools/src/tools/annotation/LengthToolZoom/LengthToolZoom.ts @@ -0,0 +1,1983 @@ +import { Events, ChangeTypes } from '../../../enums'; +import { + getEnabledElement, + utilities as csUtils, + utilities, + getEnabledElementByViewportId, +} from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; + +import { getCalibratedLengthUnitsAndScale } from '../../../utilities/getCalibratedUnits'; +import { AnnotationTool } from '../../base'; +import throttle from '../../../utilities/throttle'; +import { + addAnnotation, + getAnnotations, + removeAnnotation, +} from '../../../stateManagement/annotation/annotationState'; +import { isAnnotationVisible } from '../../../stateManagement/annotation/annotationVisibility'; +import { + triggerAnnotationCompleted, + triggerAnnotationModified, +} from '../../../stateManagement/annotation/helpers/state'; +import { + deselectAnnotation, + isAnnotationSelected, +} from '../../../stateManagement/annotation/annotationSelection'; +import * as lineSegment from '../../../utilities/math/line'; + +import { + drawHandles as drawHandlesSvg, + drawLine as drawLineSvg, + drawLinkedTextBox as drawLinkedTextBoxSvg, +} from '../../../drawingSvg'; +import { state } from '../../../store/state'; +import { getViewportIdsWithToolToRender } from '../../../utilities/viewportFilters'; +import { getTextBoxCoordsCanvas } from '../../../utilities/drawing'; +import triggerAnnotationRenderForViewportIds from '../../../utilities/triggerAnnotationRenderForViewportIds'; + +import { + resetElementCursor, + hideElementCursor, +} from '../../../cursors/elementCursor'; +import { MouseCursor } from '../../../cursors'; + +import type { + EventTypes, + ToolHandle, + TextBoxHandle, + Annotations, + PublicToolProps, + ToolProps, + SVGDrawingHelper, + Annotation, +} from '../../../types'; +import type { LengthAnnotation } from '../../../types/ToolSpecificAnnotationTypes'; +import type { StyleSpecifier } from '../../../types/AnnotationStyle'; +import { getStyleProperty } from '../../../stateManagement/annotation/config/helpers'; + +import { LensOverlay, type LensOverlayConfig } from './LensOverlay'; + +const { transformWorldToIndex } = csUtils; + +const pointerCursor = MouseCursor.getDefinedCursor('pointer'); + +if (pointerCursor) { + MouseCursor.setDefinedCursor('LengthZoom', pointerCursor); +} + +const HANDLE_RADIUS = 8; +const HANDLE_COLOR = '#1284FF'; +const HANDLE_FILL = '#ffffff'; +const HANDLE_LINE_WIDTH = 2; +const ACTIVE_HANDLE_RADIUS = 12; +const MOVEMENT_EPSILON = 1e-3; +const HANDLE_MOVE_LINGER_FRAMES = 20; +const HANDLE_GLOW_RADIUS = 26; +const HANDLE_GLOW_COLOR_30 = 'rgba(18, 132, 255, 0.3)'; +const HANDLE_GLOW_COLOR_50 = 'rgba(18, 132, 255, 0.5)'; +const CROSSBAR_HALF_LENGTH = 8; +const HIGHLIGHT_LAYER_CLASS = 'lengthtool-zoom__highlight-layer'; +const MAIN_LAYER_CLASS = 'lengthtool-zoom__main-layer'; +const HANDLE_LAYER_CLASS = 'lengthtool-zoom__handle-layer'; +const TEXTBOX_HORIZONTAL_OFFSET = HANDLE_RADIUS + 10; +const TEXTBOX_VERTICAL_OFFSET = ACTIVE_HANDLE_RADIUS + 16; +const TEXTBOX_PADDING = 12; // Must stay in sync with TEXTBOX_FIXED_STYLE padding +const TEXTBOX_BACKGROUND_PADDING = 4; +const MOVING_BACKGROUND_PADDING = 6; +const LENGTH_COLOR = 'rgb(var(--ui-2, 236, 102, 2))'; +const LINK_LINE_DASH = '8,8'; +const TEXTBOX_FIXED_STYLE = { + color: 'var(--text-white, #FFF)', + textShadow: + '0 0 2px #000, 0 0 4px #000, -1px -1px 4px #000, 1px 1px 4px #000', + borderColor: LENGTH_COLOR, + borderWidth: 2, + borderRadius: 3, + padding: TEXTBOX_PADDING, + backgroundPadding: TEXTBOX_BACKGROUND_PADDING, +}; + +type CreationStage = 'placingFirst' | 'placingSecond' | 'waitingSecond'; +type LengthZoomMetadata = LengthAnnotation['metadata'] & { + creationStage?: CreationStage; +}; + +/** + * LengthToolZoom let you draw annotations that measures the length of two drawing + * points on a slice. You can use the LengthToolZoom in all imaging planes even in oblique + * reconstructed planes. Note: annotation tools in cornerstone3DTools exists in the exact location + * in the physical 3d space, as a result, by default, all annotations that are + * drawing in the same frameOfReference will get shared between viewports that + * are in the same frameOfReference. + * + * The resulting annotation's data (statistics) and metadata (the + * state of the viewport while drawing was happening) will get added to the + * ToolState manager and can be accessed from the ToolState by calling getAnnotations + * or similar methods. + * + * ```js + * cornerstoneTools.addTool(LengthToolZoom) + * + * const toolGroup = ToolGroupManager.createToolGroup('toolGroupId') + * + * toolGroup.addTool(LengthToolZoom.toolName) + * + * toolGroup.addViewport('viewportId', 'renderingEngineId') + * + * toolGroup.setToolActive(LengthToolZoom.toolName, { + * bindings: [ + * { + * mouseButton: MouseBindings.Primary, // Left Click + * }, + * ], + * }) + * ``` + * + * Read more in the Docs section of the website. + + */ + +class LengthToolZoom extends AnnotationTool { + static toolName = 'LengthZoom'; + + _throttledCalculateCachedStats: Function; + editData: { + annotation: Annotation; + viewportIdsToRender: string[]; + handleIndex?: number; + movingTextBox?: boolean; + newAnnotation?: boolean; + hasMoved?: boolean; + stage?: 'placingFirst' | 'placingSecond'; + isHandleMoving?: boolean; + handleMoveLinger?: number; + textBoxAnchorCanvas?: Types.Point2; + } | null; + isDrawing: boolean; + isHandleOutsideImage: boolean; + private pendingAnnotation: LengthAnnotation | null; + private _handleMoveAnimationFrame: number | null; + private currentMousePosition: Types.Point2 | null = null; + private lensOverlay: LensOverlay; + + constructor( + toolProps: PublicToolProps = {}, + defaultToolProps: ToolProps = { + supportedInteractionTypes: ['Mouse', 'Touch'], + configuration: { + preventHandleOutsideImage: false, + getTextLines: defaultGetTextLines, + textBoxDetachThreshold: 75, // Canvas pixels distance before label detaches + magnifier: { + enabled: true, + radius: 100, + zoomFactor: 2, + borderWidth: 2, + borderColor: '#FFFFFF', + showCrosshair: true, + }, + actions: { + // TODO - bind globally - but here is actually pretty good as it + // is almost always active. + undo: { + method: 'undo', + bindings: [{ key: 'z' }], + }, + redo: { + method: 'redo', + bindings: [{ key: 'y' }], + }, + }, + }, + } + ) { + super(toolProps, defaultToolProps); + + this._throttledCalculateCachedStats = throttle( + this._calculateCachedStats, + 100, + { trailing: true } + ); + + this.pendingAnnotation = null; + this._handleMoveAnimationFrame = null; + + // Initialize lens overlay with configuration + this.lensOverlay = new LensOverlay( + this.configuration.magnifier as LensOverlayConfig + ); + + if (!toolProps.configuration?.getTextLines) { + this.configuration.getTextLines = this._getTextLinesWithLabel; + } + } + + public mouseMoveCallback = ( + evt: EventTypes.MouseMoveEventType, + filteredAnnotations?: Annotations + ): boolean => { + const eventDetail = evt.detail; + this.currentMousePosition = eventDetail.currentPoints.canvas; + + // Update lens overlay mouse position + this.lensOverlay.updateMousePosition(this.currentMousePosition); + + // Lupe bei aktiver Platzierung anzeigen + if (this._shouldShowMagnifier()) { + this.lensOverlay.show(eventDetail.element); + } else { + this.lensOverlay.hide(); + } + + if (this.editData) { + // Call parent class mouseMoveCallback by calling the parent method directly + return AnnotationTool.prototype.mouseMoveCallback.call( + this, + evt, + filteredAnnotations + ); + } + return false; + }; + + static hydrate = ( + viewportId: string, + points: Types.Point3[], + options?: { + annotationUID?: string; + toolInstance?: LengthToolZoom; + referencedImageId?: string; + viewplaneNormal?: Types.Point3; + viewUp?: Types.Point3; + } + ): LengthAnnotation => { + const enabledElement = getEnabledElementByViewportId(viewportId); + if (!enabledElement) { + return; + } + const { + FrameOfReferenceUID, + referencedImageId, + viewPlaneNormal, + instance, + viewport, + } = this.hydrateBase( + LengthToolZoom, + enabledElement, + points, + options + ); + + // Exclude toolInstance from the options passed into the metadata + const { toolInstance, ...serializableOptions } = options || {}; + + const annotation = { + annotationUID: options?.annotationUID || utilities.uuidv4(), + data: { + handles: { + points, + }, + }, + highlighted: false, + autoGenerated: false, + invalidated: false, + isLocked: false, + isVisible: true, + metadata: { + toolName: instance.getToolName(), + viewPlaneNormal, + FrameOfReferenceUID, + referencedImageId, + ...serializableOptions, + }, + }; + addAnnotation(annotation, viewport.element); + + triggerAnnotationRenderForViewportIds([viewport.id]); + }; + + /** + * Based on the current position of the mouse and the current imageId to create + * a Length Annotation and stores it in the annotationManager + * + * @param evt - EventTypes.NormalizedMouseEventType + * @returns The annotation object. + * + */ + addNewAnnotation = ( + evt: EventTypes.InteractionEventType + ): LengthAnnotation => { + const eventDetail = evt.detail; + const { currentPoints, element } = eventDetail; + + if ( + this.pendingAnnotation && + (this.pendingAnnotation.metadata as LengthZoomMetadata)?.creationStage === + 'waitingSecond' + ) { + return this._beginSecondPointPlacement(evt, this.pendingAnnotation); + } + + const worldPos = currentPoints.world; + + hideElementCursor(element); + this.isDrawing = true; + + const annotation = ( + this.createAnnotation(evt, [ + [...worldPos], + [...worldPos], + ]) + ); + + (annotation.metadata as LengthZoomMetadata).creationStage = 'placingFirst'; + annotation.data.handles.activeHandleIndex = 0; + + this._assignAnnotationLabel(annotation, element); + addAnnotation(annotation, element); + + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + handleIndex: 0, + movingTextBox: false, + newAnnotation: true, + hasMoved: false, + stage: 'placingFirst', + isHandleMoving: false, + handleMoveLinger: 0, + }; + this._activateDraw(element); + + // Lupe sofort nach dem Start der Zeichnung anzeigen + this.currentMousePosition = eventDetail.currentPoints.canvas; + this.lensOverlay.updateMousePosition(this.currentMousePosition); + if (this._shouldShowMagnifier()) { + this.lensOverlay.show(element); + } + + evt.preventDefault(); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + this.pendingAnnotation = annotation; + + return annotation; + }; + + public postMouseDownCallback = ( + evt: EventTypes.MouseDownEventType + ): boolean => { + if ( + this.pendingAnnotation && + (this.pendingAnnotation.metadata as LengthZoomMetadata)?.creationStage === + 'waitingSecond' + ) { + return false; + } + + if (this.editData?.stage === 'placingFirst' || this.isDrawing) { + return false; + } + + const { element } = evt.detail; + const annotations = getAnnotations(this.getToolName(), element) ?? []; + + const shouldDeselect = annotations.some((annotation) => { + const isSelected = isAnnotationSelected(annotation.annotationUID); + const hasActiveHandle = + annotation.data?.handles?.activeHandleIndex !== null && + annotation.data?.handles?.activeHandleIndex !== undefined; + + return isSelected || annotation.highlighted || hasActiveHandle; + }); + + // Wenn nichts zum Deselektieren da ist, lassen wir das Tool normal weiterarbeiten + // (kann eine neue Linie beginnen bei Drag) + if (!shouldDeselect) { + return false; + } + + const changed = this._deselectAllLengthAnnotations(element); + + if (changed) { + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + } + + evt.preventDefault(); + + // Nur true zurückgeben, wenn tatsächlich deselektiert wurde + return changed; + }; + + public postTouchStartCallback = ( + evt: EventTypes.TouchStartEventType + ): boolean => { + if ( + this.pendingAnnotation && + (this.pendingAnnotation.metadata as LengthZoomMetadata)?.creationStage === + 'waitingSecond' + ) { + return false; + } + + if (this.editData?.stage === 'placingFirst' || this.isDrawing) { + return false; + } + + const { element } = evt.detail; + const annotations = getAnnotations(this.getToolName(), element) ?? []; + + const shouldDeselect = annotations.some((annotation) => { + const isSelected = isAnnotationSelected(annotation.annotationUID); + const hasActiveHandle = + annotation.data?.handles?.activeHandleIndex !== null && + annotation.data?.handles?.activeHandleIndex !== undefined; + + return isSelected || annotation.highlighted || hasActiveHandle; + }); + + // Wenn nichts zum Deselektieren da ist, lassen wir das Tool normal weiterarbeiten + // (kann eine neue Linie beginnen bei Drag) + if (!shouldDeselect) { + return false; + } + + const changed = this._deselectAllLengthAnnotations(element); + + if (changed) { + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + } + + evt.preventDefault(); + + // Nur true zurückgeben, wenn tatsächlich deselektiert wurde + return changed; + }; + + private _beginSecondPointPlacement( + evt: EventTypes.InteractionEventType, + annotation: LengthAnnotation + ): LengthAnnotation { + const eventDetail = evt.detail; + const { currentPoints, element } = eventDetail; + const worldPos = currentPoints.world; + + hideElementCursor(element); + this.isDrawing = true; + + const points = annotation.data.handles.points; + points[1] = [...worldPos]; + (annotation.metadata as LengthZoomMetadata).creationStage = 'placingSecond'; + annotation.data.handles.activeHandleIndex = 1; + annotation.highlighted = true; + annotation.invalidated = true; + + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + handleIndex: 1, + movingTextBox: false, + newAnnotation: true, + hasMoved: false, + stage: 'placingSecond', + isHandleMoving: false, + handleMoveLinger: 0, + }; + + this._activateDraw(element); + + // Lupe sofort nach dem Start der zweiten Punktplatzierung anzeigen + this.currentMousePosition = eventDetail.currentPoints.canvas; + this.lensOverlay.updateMousePosition(this.currentMousePosition); + if (this._shouldShowMagnifier()) { + this.lensOverlay.show(element); + } + + evt.preventDefault(); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + this.pendingAnnotation = annotation; + + return annotation; + } + + /** + * It returns if the canvas point is near the provided length annotation in the provided + * element or not. A proximity is passed to the function to determine the + * proximity of the point to the annotation in number of pixels. + * + * @param element - HTML Element + * @param annotation - Annotation + * @param canvasCoords - Canvas coordinates + * @param proximity - Proximity to tool to consider + * @returns Boolean, whether the canvas point is near tool + */ + isPointNearTool = ( + element: HTMLDivElement, + annotation: LengthAnnotation, + canvasCoords: Types.Point2, + proximity: number + ): boolean => { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + const { data } = annotation; + const [point1, point2] = data.handles.points; + const canvasPoint1 = viewport.worldToCanvas(point1); + const canvasPoint2 = viewport.worldToCanvas(point2); + + const line = { + start: { + x: canvasPoint1[0], + y: canvasPoint1[1], + }, + end: { + x: canvasPoint2[0], + y: canvasPoint2[1], + }, + }; + + const distanceToPoint = lineSegment.distanceToPoint( + [line.start.x, line.start.y], + [line.end.x, line.end.y], + [canvasCoords[0], canvasCoords[1]] + ); + + if (distanceToPoint <= proximity) { + return true; + } + + return false; + }; + + toolSelectedCallback = ( + evt: EventTypes.InteractionEventType, + annotation: LengthAnnotation + ): void => { + const eventDetail = evt.detail; + const { element } = eventDetail; + + annotation.highlighted = true; + annotation.data.handles.activeHandleIndex = null; + + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + movingTextBox: false, + isHandleMoving: false, + handleMoveLinger: 0, + }; + + this._activateModify(element); + + hideElementCursor(element); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + evt.preventDefault(); + }; + + handleSelectedCallback( + evt: EventTypes.InteractionEventType, + annotation: LengthAnnotation, + handle: ToolHandle + ): void { + const eventDetail = evt.detail; + const { element } = eventDetail; + const { data } = annotation; + + annotation.highlighted = true; + + let movingTextBox = false; + let handleIndex; + + if ((handle as TextBoxHandle).worldPosition) { + movingTextBox = true; + } else { + handleIndex = data.handles.points.findIndex((p) => p === handle); + } + + if (movingTextBox || handleIndex === undefined || handleIndex === -1) { + data.handles.activeHandleIndex = null; + } else { + data.handles.activeHandleIndex = handleIndex; + } + + // Find viewports to render on drag. + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + handleIndex, + movingTextBox, + isHandleMoving: false, + handleMoveLinger: 0, + }; + this._activateModify(element); + + hideElementCursor(element); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + evt.preventDefault(); + } + + public getHandleNearImagePoint( + element: HTMLDivElement, + annotation: Annotation, + canvasCoords: Types.Point2, + proximity: number + ): ToolHandle | undefined { + const selected = isAnnotationSelected(annotation.annotationUID); + + if (!this.editData && !selected) { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + const { textBox } = annotation.data.handles; + const worldBoundingBox = textBox?.worldBoundingBox; + + if (textBox && worldBoundingBox) { + const canvasBoundingBox = { + topLeft: viewport.worldToCanvas(worldBoundingBox.topLeft), + topRight: viewport.worldToCanvas(worldBoundingBox.topRight), + bottomLeft: viewport.worldToCanvas(worldBoundingBox.bottomLeft), + bottomRight: viewport.worldToCanvas(worldBoundingBox.bottomRight), + }; + + const withinBounds = + canvasCoords[0] >= canvasBoundingBox.topLeft[0] && + canvasCoords[0] <= canvasBoundingBox.bottomRight[0] && + canvasCoords[1] >= canvasBoundingBox.topLeft[1] && + canvasCoords[1] <= canvasBoundingBox.bottomRight[1]; + + if (withinBounds) { + annotation.data.handles.activeHandleIndex = null; + return textBox as ToolHandle; + } + } + + return; + } + + return super.getHandleNearImagePoint( + element, + annotation, + canvasCoords, + proximity + ); + } + + _endCallback = (evt: EventTypes.InteractionEventType): void => { + const eventDetail = evt.detail; + const { element } = eventDetail; + + // Lupe ausblenden bei Ende der Zeichnung + this.lensOverlay.hide(); + + if (!this.editData) { + return; + } + + const { annotation, viewportIdsToRender, newAnnotation, hasMoved, stage } = + this.editData; + const { data } = annotation; + + if (data?.handles?.textBox) { + data.handles.textBox.isMoving = false; + } + // Clear stored anchor (will be re-established on next drag if needed) + if (this.editData) { + this.editData.textBoxAnchorCanvas = undefined; + } + + if (stage === 'placingFirst') { + (annotation.metadata as LengthZoomMetadata).creationStage = + 'waitingSecond'; + data.handles.activeHandleIndex = null; + + this._deactivateModify(element); + this._deactivateDraw(element); + resetElementCursor(element); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + this.doneEditMemo(); + + this.editData = null; + this.isDrawing = false; + + return; + } + + if (stage !== 'placingSecond' && newAnnotation && !hasMoved) { + // when user starts the drawing by click, and moving the mouse, instead + // of click and drag + return; + } + + data.handles.activeHandleIndex = null; + + this._deactivateModify(element); + this._deactivateDraw(element); + resetElementCursor(element); + this._cancelHandleMoveLingerTick(); + + if ( + this.isHandleOutsideImage && + this.configuration.preventHandleOutsideImage + ) { + removeAnnotation(annotation.annotationUID); + } + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + this.doneEditMemo(); + + if (newAnnotation) { + triggerAnnotationCompleted(annotation); + } + + if (newAnnotation) { + annotation.highlighted = false; + deselectAnnotation(annotation.annotationUID); + } + + this.editData = null; + this.isDrawing = false; + delete (annotation.metadata as LengthZoomMetadata).creationStage; + this.pendingAnnotation = null; + }; + + _dragCallback = (evt: EventTypes.InteractionEventType): void => { + this.isDrawing = true; + const eventDetail = evt.detail; + const { element, currentPoints } = eventDetail; + + // Mausposition für die Lupe während des Drags aktualisieren + this.currentMousePosition = currentPoints.canvas; + this.lensOverlay.updateMousePosition(this.currentMousePosition); + if (this._shouldShowMagnifier()) { + this.lensOverlay.show(element); + } + + const { + annotation, + viewportIdsToRender, + handleIndex, + movingTextBox, + newAnnotation, + stage, + } = this.editData; + const { data } = annotation; + + this.createMemo(element, annotation, { newAnnotation }); + + const deltaPointsWorld = ( + eventDetail as { deltaPoints?: { world?: Types.Point3 } } + ).deltaPoints?.world as Types.Point3 | undefined; + + const movedByWorld = Boolean( + deltaPointsWorld && + (Math.abs(deltaPointsWorld[0]) > MOVEMENT_EPSILON || + Math.abs(deltaPointsWorld[1]) > MOVEMENT_EPSILON || + Math.abs(deltaPointsWorld[2]) > MOVEMENT_EPSILON) + ); + + let movedThisFrame = false; + + const mouseEvent = eventDetail.event as MouseEvent; + const buttons = + (eventDetail as unknown as { buttons?: number }).buttons ?? + (mouseEvent instanceof MouseEvent ? mouseEvent.buttons : undefined); + + if (stage && typeof buttons === 'number' && buttons === 0) { + return; + } + + if (stage === 'placingFirst') { + const { currentPoints } = eventDetail; + const worldPos = currentPoints.world; + const previousPoint = data.handles.points[0]; + const moved = + movedByWorld || this._hasPointChanged(previousPoint, worldPos); + + data.handles.points[0] = [...worldPos]; + data.handles.points[1] = [...worldPos]; + annotation.invalidated = true; + + movedThisFrame = moved; + if (moved) { + this.editData.handleMoveLinger = HANDLE_MOVE_LINGER_FRAMES; + this.editData.isHandleMoving = true; + this._scheduleHandleMoveLingerTick(); + } + this.editData.hasMoved = this.editData.hasMoved || moved; + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + return; + } + + if (movingTextBox) { + // Drag mode - moving text box (refactored to use editData.textBoxAnchorCanvas) + const { deltaPoints } = eventDetail as EventTypes.MouseDragEventDetail; + const worldPosDelta = deltaPoints.world; + + data.handles.activeHandleIndex = null; + + const { textBox } = data.handles; + const { worldPosition } = textBox; + const enabledEl = getEnabledElement(element); + const { viewport } = enabledEl; + + const prevCanvas = viewport.worldToCanvas(worldPosition); + + // Apply delta + worldPosition[0] += worldPosDelta[0]; + worldPosition[1] += worldPosDelta[1]; + worldPosition[2] += worldPosDelta[2]; + + const newCanvas = viewport.worldToCanvas(worldPosition); + + // Establish persistent anchor (first point canvas position) once while still attached + if (!textBox.hasMoved && !this.editData.textBoxAnchorCanvas) { + const firstPointWorld = data.handles.points[0]; + this.editData.textBoxAnchorCanvas = viewport.worldToCanvas( + firstPointWorld + ) as Types.Point2; + } + + const anchorCanvas: Types.Point2 = this.editData.textBoxAnchorCanvas + ? { ...this.editData.textBoxAnchorCanvas } + : (viewport.worldToCanvas(data.handles.points[0]) as Types.Point2); + + // Approximate center of textbox (prefer last rendered bounding box) + let centerCanvas: Types.Point2 = newCanvas as Types.Point2; + const lastBBox = textBox.worldBoundingBox; + if (lastBBox) { + const tl = viewport.worldToCanvas(lastBBox.topLeft); + const br = viewport.worldToCanvas(lastBBox.bottomRight); + if (tl && br) { + centerCanvas = [ + tl[0] + (br[0] - tl[0]) / 2, + tl[1] + (br[1] - tl[1]) / 2, + ]; + } + } + + const dx = centerCanvas[0] - anchorCanvas[0]; + const dy = centerCanvas[1] - anchorCanvas[1]; + const distToAnchor = Math.sqrt(dx * dx + dy * dy); + + const threshold = this.configuration.textBoxDetachThreshold ?? 75; + if (!textBox.hasMoved && distToAnchor >= threshold) { + textBox.hasMoved = true; + } + + textBox.isMoving = true; + movedThisFrame = + movedByWorld || + Math.abs(newCanvas[0] - prevCanvas[0]) > 0.01 || + Math.abs(newCanvas[1] - prevCanvas[1]) > 0.01; + } else if (handleIndex === undefined) { + // Drag mode - moving handle + const { deltaPoints } = eventDetail as EventTypes.MouseDragEventDetail; + const worldPosDelta = deltaPoints.world; + + const points = data.handles.points; + + if (!['placingFirst', 'placingSecond'].includes(stage)) { + data.handles.activeHandleIndex = null; + } + + points.forEach((point) => { + point[0] += worldPosDelta[0]; + point[1] += worldPosDelta[1]; + point[2] += worldPosDelta[2]; + }); + annotation.invalidated = annotation.invalidated || movedByWorld; + movedThisFrame = movedByWorld; + } else { + // Move mode - after double click, and mouse move to draw + const { currentPoints } = eventDetail; + const worldPos = currentPoints.world; + const previousPoint = data.handles.points[handleIndex]; + const moved = + movedByWorld || this._hasPointChanged(previousPoint, worldPos); + + if (moved) { + data.handles.points[handleIndex] = [...worldPos]; + annotation.invalidated = true; + } + + movedThisFrame = moved; + } + + if (movedThisFrame && !movingTextBox) { + this.editData.handleMoveLinger = HANDLE_MOVE_LINGER_FRAMES; + this.editData.isHandleMoving = true; + this._scheduleHandleMoveLingerTick(); + } + this.editData.hasMoved = this.editData.hasMoved || movedThisFrame; + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + if (annotation.invalidated) { + triggerAnnotationModified( + annotation, + element, + ChangeTypes.HandlesUpdated + ); + } + }; + + cancel = (element: HTMLDivElement) => { + // If it is mid-draw or mid-modify + if (this.isDrawing) { + this.isDrawing = false; + this._deactivateDraw(element); + this._deactivateModify(element); + resetElementCursor(element); + this._cancelHandleMoveLingerTick(); + + const { annotation, viewportIdsToRender, newAnnotation, stage } = + this.editData; + const { data } = annotation; + + annotation.highlighted = false; + data.handles.activeHandleIndex = null; + + if (data?.handles?.textBox) { + data.handles.textBox.isMoving = false; + } + if (this.editData) { + this.editData.textBoxAnchorCanvas = undefined; + } + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + if (stage === 'placingFirst') { + removeAnnotation(annotation.annotationUID); + } else if (newAnnotation) { + triggerAnnotationCompleted(annotation); + } + + this.editData = null; + delete (annotation.metadata as LengthZoomMetadata).creationStage; + this.pendingAnnotation = null; + return annotation.annotationUID; + } + + if ( + this.pendingAnnotation && + (this.pendingAnnotation.metadata as LengthZoomMetadata)?.creationStage === + 'waitingSecond' + ) { + const annotation = this.pendingAnnotation; + removeAnnotation(annotation.annotationUID); + triggerAnnotationRenderForViewportIds( + getViewportIdsWithToolToRender(element, this.getToolName()) + ); + delete (annotation.metadata as LengthZoomMetadata).creationStage; + this.pendingAnnotation = null; + return annotation.annotationUID; + } + }; + + _activateModify = (element: HTMLDivElement) => { + state.isInteractingWithTool = true; + + element.addEventListener( + Events.MOUSE_UP, + this._endCallback as EventListener + ); + element.addEventListener( + Events.MOUSE_DRAG, + this._dragCallback as EventListener + ); + element.addEventListener( + Events.MOUSE_CLICK, + this._endCallback as EventListener + ); + + element.addEventListener( + Events.TOUCH_END, + this._endCallback as EventListener + ); + element.addEventListener( + Events.TOUCH_DRAG, + this._dragCallback as EventListener + ); + element.addEventListener( + Events.TOUCH_TAP, + this._endCallback as EventListener + ); + }; + + _deactivateModify = (element: HTMLDivElement) => { + state.isInteractingWithTool = false; + + element.removeEventListener( + Events.MOUSE_UP, + this._endCallback as EventListener + ); + element.removeEventListener( + Events.MOUSE_DRAG, + this._dragCallback as EventListener + ); + element.removeEventListener( + Events.MOUSE_CLICK, + this._endCallback as EventListener + ); + + element.removeEventListener( + Events.TOUCH_END, + this._endCallback as EventListener + ); + element.removeEventListener( + Events.TOUCH_DRAG, + this._dragCallback as EventListener + ); + element.removeEventListener( + Events.TOUCH_TAP, + this._endCallback as EventListener + ); + }; + + _activateDraw = (element: HTMLDivElement) => { + state.isInteractingWithTool = true; + + element.addEventListener( + Events.MOUSE_UP, + this._endCallback as EventListener + ); + element.addEventListener( + Events.MOUSE_DRAG, + this._dragCallback as EventListener + ); + element.addEventListener( + Events.MOUSE_MOVE, + this._dragCallback as EventListener + ); + element.addEventListener( + Events.MOUSE_CLICK, + this._endCallback as EventListener + ); + + element.addEventListener( + Events.TOUCH_END, + this._endCallback as EventListener + ); + element.addEventListener( + Events.TOUCH_DRAG, + this._dragCallback as EventListener + ); + element.addEventListener( + Events.TOUCH_TAP, + this._endCallback as EventListener + ); + }; + + _deactivateDraw = (element: HTMLDivElement) => { + state.isInteractingWithTool = false; + + element.removeEventListener( + Events.MOUSE_UP, + this._endCallback as EventListener + ); + element.removeEventListener( + Events.MOUSE_DRAG, + this._dragCallback as EventListener + ); + element.removeEventListener( + Events.MOUSE_MOVE, + this._dragCallback as EventListener + ); + element.removeEventListener( + Events.MOUSE_CLICK, + this._endCallback as EventListener + ); + + element.removeEventListener( + Events.TOUCH_END, + this._endCallback as EventListener + ); + element.removeEventListener( + Events.TOUCH_DRAG, + this._dragCallback as EventListener + ); + element.removeEventListener( + Events.TOUCH_TAP, + this._endCallback as EventListener + ); + + this._cancelHandleMoveLingerTick(); + this.lensOverlay.hide(); + }; + + /** + * it is used to draw the length annotation in each + * request animation frame. It calculates the updated cached statistics if + * data is invalidated and cache it. + * + * @param enabledElement - The Cornerstone's enabledElement. + * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing. + */ + renderAnnotation = ( + enabledElement: Types.IEnabledElement, + svgDrawingHelper: SVGDrawingHelper + ): boolean => { + let renderStatus = false; + const { viewport } = enabledElement; + const { element } = viewport; + + let annotations = getAnnotations(this.getToolName(), element); + + // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender + if (!annotations?.length) { + return renderStatus; + } + + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + + if (!annotations?.length) { + return renderStatus; + } + + const targetId = this.getTargetId(viewport); + const renderingEngine = viewport.getRenderingEngine(); + + const styleSpecifier: StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + const highlightLayer = this._getOrCreateLayer( + svgDrawingHelper, + HIGHLIGHT_LAYER_CLASS + ); + const mainLayer = this._getOrCreateLayer( + svgDrawingHelper, + MAIN_LAYER_CLASS + ); + const handleLayer = this._getOrCreateLayer( + svgDrawingHelper, + HANDLE_LAYER_CLASS + ); + + // Draw SVG + for (let i = 0; i < annotations.length; i++) { + const annotation = annotations[i] as LengthAnnotation; + const { annotationUID, data } = annotation; + const { points, activeHandleIndex } = data.handles; + + styleSpecifier.annotationUID = annotationUID; + + const { color, lineWidth, lineDash, shadow } = this.getAnnotationStyle({ + annotation, + styleSpecifier, + }); + + // Ensure consistent world-to-canvas transformation + // This fixes the issue where lines shift position when zooming out + const canvasCoordinates = points.map((p) => { + try { + return viewport.worldToCanvas(p); + } catch (error) { + // Fallback to prevent rendering issues + console.warn( + 'Failed to transform world coordinates to canvas:', + error + ); + return [0, 0] as Types.Point2; + } + }); + + const editDataForAnnotation = + this.editData && this.editData.annotation === annotation + ? this.editData + : null; + + const creationStage = (annotation.metadata as LengthZoomMetadata) + ?.creationStage; + + if ( + creationStage === 'placingFirst' || + creationStage === 'waitingSecond' + ) { + const handleGroupUID = 'preview'; + const annotationIsSelected = isAnnotationSelected(annotationUID); + const isFirstHandleGrabbed = + creationStage === 'placingFirst' && annotationIsSelected; + const isDraggingFirstHandle = + isFirstHandleGrabbed && + Boolean(editDataForAnnotation?.isHandleMoving); + const previewHandleStyle = isFirstHandleGrabbed + ? { + color: HANDLE_FILL, + lineWidth: HANDLE_LINE_WIDTH, + handleRadius: `${ACTIVE_HANDLE_RADIUS}`, + fill: HANDLE_COLOR, + } + : { + color: HANDLE_COLOR, + lineWidth: HANDLE_LINE_WIDTH, + handleRadius: `${HANDLE_RADIUS}`, + fill: HANDLE_FILL, + }; + + this._withLayer(svgDrawingHelper, handleLayer, () => { + if (isDraggingFirstHandle) { + const previewGlowUID = 'preview-glow'; + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + previewGlowUID, + [canvasCoordinates[0]], + { + color: HANDLE_GLOW_COLOR_30, + lineDash: undefined, + lineWidth: 0, + handleRadius: `${HANDLE_GLOW_RADIUS}`, + fill: HANDLE_GLOW_COLOR_30, + } + ); + } + + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + handleGroupUID, + [canvasCoordinates[0]], + previewHandleStyle + ); + }); + + renderStatus = true; + continue; + } + + const isPreviewingSecondPoint = creationStage === 'placingSecond'; + + // If cachedStats does not exist, or the unit is missing (as part of import/hydration etc.), + // force to recalculate the stats from the points + if ( + !data.cachedStats[targetId] || + data.cachedStats[targetId].unit == null + ) { + data.cachedStats[targetId] = { + length: null, + unit: null, + }; + + this._calculateCachedStats(annotation, renderingEngine, enabledElement); + } else if (annotation.invalidated) { + this._throttledCalculateCachedStats( + annotation, + renderingEngine, + enabledElement + ); + } + + // If rendering engine has been destroyed while rendering + if (!viewport.getRenderingEngine()) { + console.warn('Rendering Engine has been destroyed'); + return renderStatus; + } + + if (!isAnnotationVisible(annotationUID)) { + continue; + } + + const annotationIsSelected = + Boolean(editDataForAnnotation) || isAnnotationSelected(annotationUID); + + const showHandlesAlways = Boolean( + getStyleProperty('showHandlesAlways', {} as StyleSpecifier) + ); + const dragHandleIndex = + editDataForAnnotation && + typeof editDataForAnnotation.handleIndex === 'number' && + !editDataForAnnotation.movingTextBox + ? editDataForAnnotation.handleIndex + : null; + + const shouldDrawHandles = + isPreviewingSecondPoint || + annotationIsSelected || + showHandlesAlways || + dragHandleIndex !== null; + + const [startPoint, endPoint] = canvasCoordinates; + const dx = endPoint[0] - startPoint[0]; + const dy = endPoint[1] - startPoint[1]; + const length = Math.sqrt(dx * dx + dy * dy); + + if (annotationIsSelected) { + this._withLayer(svgDrawingHelper, highlightLayer, () => { + const highlightLineUID = 'selected-highlight'; + drawLineSvg( + svgDrawingHelper, + annotationUID, + highlightLineUID, + canvasCoordinates[0], + canvasCoordinates[1], + { + color: 'rgba(18, 132, 255, 0.5)', + width: 16, + lineDash: undefined, + shadow: false, + lineCap: 'round', + }, + `${annotationUID}-selected-highlight` + ); + }); + } + + this._withLayer(svgDrawingHelper, mainLayer, () => { + const dataId = `${annotationUID}-line`; + const lineUID = '1'; + drawLineSvg( + svgDrawingHelper, + annotationUID, + lineUID, + canvasCoordinates[0], + canvasCoordinates[1], + { + color: LENGTH_COLOR, + width: 2, + lineDash, + shadow: { + color: 'rgba(0, 0, 0, 0.8)', + offsetX: 0, + offsetY: 1, + blur: 1, + }, + }, + dataId + ); + + const crossbarStyle = { + color: LENGTH_COLOR, + width: 2, + lineDash: undefined, + shadow: { + color: 'rgba(0, 0, 0, 0.8)', + offsetX: 0, + offsetY: 1, + blur: 1, + }, + }; + + if (length > 0.0001) { + const invLength = 1 / length; + const perpX = -dy * invLength * CROSSBAR_HALF_LENGTH; + const perpY = dx * invLength * CROSSBAR_HALF_LENGTH; + + const startCrossStart: Types.Point2 = [ + startPoint[0] - perpX, + startPoint[1] - perpY, + ]; + const startCrossEnd: Types.Point2 = [ + startPoint[0] + perpX, + startPoint[1] + perpY, + ]; + + const endCrossStart: Types.Point2 = [ + endPoint[0] - perpX, + endPoint[1] - perpY, + ]; + const endCrossEnd: Types.Point2 = [ + endPoint[0] + perpX, + endPoint[1] + perpY, + ]; + + drawLineSvg( + svgDrawingHelper, + annotationUID, + 'start-crossbar', + startCrossStart, + startCrossEnd, + crossbarStyle, + `${annotationUID}-start-crossbar` + ); + + drawLineSvg( + svgDrawingHelper, + annotationUID, + 'end-crossbar', + endCrossStart, + endCrossEnd, + crossbarStyle, + `${annotationUID}-end-crossbar` + ); + } + }); + + this._withLayer(svgDrawingHelper, handleLayer, () => { + if (!shouldDrawHandles) { + return; + } + + const handleGroupUID = '0'; + + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + handleGroupUID, + canvasCoordinates, + { + color: HANDLE_COLOR, + lineDash, + lineWidth: HANDLE_LINE_WIDTH, + handleRadius: `${HANDLE_RADIUS}`, + fill: HANDLE_FILL, + } + ); + + const highlightHandleIndex = dragHandleIndex; + const highlightIsMoving = Boolean( + editDataForAnnotation?.isHandleMoving + ); + + if ( + highlightHandleIndex !== null && + highlightHandleIndex >= 0 && + highlightHandleIndex < canvasCoordinates.length + ) { + const activeHandleGroupUID = 'active'; + const highlightStroke = HANDLE_FILL; + const highlightFill = HANDLE_COLOR; + + if (highlightIsMoving) { + const glowGroupUID = 'active-glow'; + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + glowGroupUID, + [canvasCoordinates[highlightHandleIndex]], + { + color: HANDLE_GLOW_COLOR_30, + lineDash: undefined, + lineWidth: 0, + handleRadius: `${HANDLE_GLOW_RADIUS}`, + fill: HANDLE_GLOW_COLOR_30, + } + ); + } + + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + activeHandleGroupUID, + [canvasCoordinates[highlightHandleIndex]], + { + color: highlightStroke, + lineDash, + lineWidth: HANDLE_LINE_WIDTH, + handleRadius: `${ACTIVE_HANDLE_RADIUS}`, + fill: highlightFill, + } + ); + } + }); + + renderStatus = true; + + // If rendering engine has been destroyed while rendering + if (!viewport.getRenderingEngine()) { + console.warn('Rendering Engine has been destroyed'); + return renderStatus; + } + + const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation); + + if (!options.visibility) { + data.handles.textBox = { + hasMoved: false, + isMoving: false, + worldPosition: [0, 0, 0], + worldBoundingBox: { + topLeft: [0, 0, 0], + topRight: [0, 0, 0], + bottomLeft: [0, 0, 0], + bottomRight: [0, 0, 0], + }, + }; + continue; + } + + const textLines = this.configuration.getTextLines(data, targetId); + + data.handles.textBox.isMoving ??= false; + + // While the textbox is not detached we only re-anchor it if the user is NOT actively dragging it. + // This allows immediate visual feedback: during drag the box follows the cursor even before threshold. + if (!data.handles.textBox.hasMoved && !data.handles.textBox.isMoving) { + const canvasTextBoxCoords = + this._getAnchoredTextBoxCanvasCoords(canvasCoordinates); + data.handles.textBox.worldPosition = + viewport.canvasToWorld(canvasTextBoxCoords); + } + + const textBoxPosition = viewport.worldToCanvas( + data.handles.textBox.worldPosition + ); + + const textBoxUID = '1'; + const hasDetachedTextBox = Boolean(data.handles.textBox.hasMoved); + // hasMoved becomes true only after the drag distance from the anchor exceeds textBoxDetachThreshold. + // Until then the label is logically attached (auto re-anchored when not being dragged) but still follows the cursor during drag. + const textBoxStyleOverrides = data.handles.textBox.isMoving + ? { + borderColor: '', + borderWidth: 0, + borderRadius: 6, + background: HANDLE_GLOW_COLOR_50, + backgroundPadding: MOVING_BACKGROUND_PADDING, + } + : { + borderColor: TEXTBOX_FIXED_STYLE.borderColor, + borderWidth: TEXTBOX_FIXED_STYLE.borderWidth, + borderRadius: TEXTBOX_FIXED_STYLE.borderRadius, + background: '', + backgroundPadding: TEXTBOX_FIXED_STYLE.backgroundPadding, + }; + + const textBoxOptions = { + ...options, + ...TEXTBOX_FIXED_STYLE, + linkColor: LENGTH_COLOR, + lineDash: LINK_LINE_DASH, + lineWidth: 2, + ...textBoxStyleOverrides, + ...(hasDetachedTextBox ? { drawLink: false } : {}), + }; + + const boundingBox = drawLinkedTextBoxSvg( + svgDrawingHelper, + annotationUID, + textBoxUID, + textLines, + textBoxPosition, + canvasCoordinates, + {}, + textBoxOptions + ); + + const { x: left, y: top, width, height } = boundingBox; + + data.handles.textBox.worldBoundingBox = { + topLeft: viewport.canvasToWorld([left, top]), + topRight: viewport.canvasToWorld([left + width, top]), + bottomLeft: viewport.canvasToWorld([left, top + height]), + bottomRight: viewport.canvasToWorld([left + width, top + height]), + }; + + if (hasDetachedTextBox) { + const labelTextLines = this._getLabelOnlyTextLines(annotation.data); + + if (labelTextLines?.length) { + const anchoredCanvasCoords = + this._getAnchoredTextBoxCanvasCoords(canvasCoordinates); + + const labelTextBoxOptions = { + ...options, + ...TEXTBOX_FIXED_STYLE, + linkColor: LENGTH_COLOR, + lineDash: LINK_LINE_DASH, + lineWidth: 2, + drawLink: false, + }; + + drawLinkedTextBoxSvg( + svgDrawingHelper, + annotationUID, + 'label', + labelTextLines, + anchoredCanvasCoords, + canvasCoordinates, + {}, + labelTextBoxOptions + ); + } + } + } + + return renderStatus; + }; + + private _assignAnnotationLabel( + annotation: LengthAnnotation, + element: HTMLDivElement + ): void { + const existingAnnotations = + getAnnotations(this.getToolName(), element) ?? []; + + const existingMaxIndex = existingAnnotations.reduce((maxIndex, ann) => { + if (ann === annotation) { + return maxIndex; + } + + const label = (ann as LengthAnnotation)?.data?.label; + const match = label && /^d(\d+)$/i.exec(label); + + if (!match) { + return maxIndex; + } + + const index = Number(match[1]); + + return Number.isFinite(index) && index > maxIndex ? index : maxIndex; + }, 0); + + const currentLabel = annotation.data?.label; + + if (currentLabel && /^d\d+$/i.test(currentLabel)) { + return; + } + + annotation.data.label = `d${existingMaxIndex + 1}`; + } + + private _getTextLinesWithLabel(data, targetId): string[] | undefined { + const cachedStats = data?.cachedStats?.[targetId]; + + if (!cachedStats) { + return; + } + + const { length, unit } = cachedStats; + + if (length === undefined || length === null || isNaN(length)) { + return; + } + const hasMillimeterUnit = + typeof unit === 'string' && unit.toLowerCase().startsWith('mm'); + + const convertedLength = hasMillimeterUnit ? length / 10 : length; + const convertedUnit = hasMillimeterUnit + ? unit.replace(/mm/i, 'cm') + : (unit ?? ''); + + const lengthText = `${convertedLength.toFixed(1)} ${convertedUnit}`; + const label = data?.label; + + return [label ? `${label}: ${lengthText}` : lengthText]; + } + + private _getLabelOnlyTextLines( + data: LengthAnnotation['data'] | undefined + ): string[] | undefined { + const label = data?.label; + + if (!label) { + return; + } + + return [label]; + } + + private _scheduleHandleMoveLingerTick(): void { + if (typeof window === 'undefined') { + return; + } + + if (this._handleMoveAnimationFrame !== null) { + return; + } + + const tick = () => { + this._handleMoveAnimationFrame = null; + + const editData = this.editData; + + if (!editData) { + return; + } + + const linger = editData.handleMoveLinger ?? 0; + + if (linger <= 0) { + editData.handleMoveLinger = 0; + editData.isHandleMoving = false; + + const viewportIds = editData.viewportIdsToRender?.length + ? editData.viewportIdsToRender + : []; + + if (viewportIds.length) { + triggerAnnotationRenderForViewportIds(viewportIds); + } + + return; + } + + editData.handleMoveLinger = linger - 1; + editData.isHandleMoving = editData.handleMoveLinger > 0; + + const viewportIds = editData.viewportIdsToRender?.length + ? editData.viewportIdsToRender + : []; + + if (viewportIds.length) { + triggerAnnotationRenderForViewportIds(viewportIds); + } + + if (editData.handleMoveLinger > 0) { + this._handleMoveAnimationFrame = window.requestAnimationFrame(tick); + } else { + editData.isHandleMoving = false; + } + }; + + this._handleMoveAnimationFrame = window.requestAnimationFrame(tick); + } + + private _cancelHandleMoveLingerTick(): void { + if ( + typeof window !== 'undefined' && + this._handleMoveAnimationFrame !== null + ) { + window.cancelAnimationFrame(this._handleMoveAnimationFrame); + this._handleMoveAnimationFrame = null; + } + } + + private _getOrCreateLayer( + svgDrawingHelper: SVGDrawingHelper, + className: string + ): SVGGElement { + const root = svgDrawingHelper.svgLayerElement as unknown as SVGGElement; + let layer = root.querySelector(`:scope > g.${className}`) as SVGGElement; + if (!layer) { + layer = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + layer.classList.add(className); + if (!layer.id) { + const baseId = root.id ? `${root.id}-${className}` : className; + layer.id = baseId; + } + root.appendChild(layer); + } + return layer; + } + + private _withLayer( + svgDrawingHelper: SVGDrawingHelper, + layer: SVGGElement, + drawFn: () => void + ): void { + const originalLayer = svgDrawingHelper.svgLayerElement; + svgDrawingHelper.svgLayerElement = layer; + try { + drawFn(); + } finally { + svgDrawingHelper.svgLayerElement = originalLayer; + } + } + + private _deselectAllLengthAnnotations(element: HTMLDivElement): boolean { + let changed = false; + const annotations = getAnnotations(this.getToolName(), element) ?? []; + + annotations.forEach((annotation) => { + if (annotation.highlighted) { + annotation.highlighted = false; + changed = true; + } + + const handles = annotation.data?.handles; + + if (handles && handles.activeHandleIndex !== null) { + handles.activeHandleIndex = null; + changed = true; + } + + if (isAnnotationSelected(annotation.annotationUID)) { + deselectAnnotation(annotation.annotationUID); + changed = true; + } + }); + + if (changed && this.editData) { + this._deactivateModify(element); + this._deactivateDraw(element); + resetElementCursor(element); + this._cancelHandleMoveLingerTick(); + this.editData = null; + this.isDrawing = false; + this.doneEditMemo(); + } + + this.pendingAnnotation = null; + + return changed; + } + + private _getAnchoredTextBoxCanvasCoords( + canvasCoordinates: Array + ): Types.Point2 { + const [firstPoint, secondPoint] = canvasCoordinates; + + if (!firstPoint || !secondPoint) { + return getTextBoxCoordsCanvas(canvasCoordinates); + } + + const isSecondAboveFirst = secondPoint[1] < firstPoint[1]; + const verticalOffset = isSecondAboveFirst + ? TEXTBOX_VERTICAL_OFFSET + : -TEXTBOX_VERTICAL_OFFSET; + + const anchorPaddingCorrection = + TEXTBOX_PADDING * 2 + TEXTBOX_BACKGROUND_PADDING; + + return [ + firstPoint[0] + TEXTBOX_HORIZONTAL_OFFSET - anchorPaddingCorrection, + firstPoint[1] + verticalOffset - anchorPaddingCorrection, + ]; + } + + private _hasPointChanged( + previous: Types.Point3, + current: Types.Point3 + ): boolean { + return ( + Math.abs(previous[0] - current[0]) > MOVEMENT_EPSILON || + Math.abs(previous[1] - current[1]) > MOVEMENT_EPSILON || + Math.abs(previous[2] - current[2]) > MOVEMENT_EPSILON + ); + } + + _calculateLength(pos1, pos2) { + const dx = pos1[0] - pos2[0]; + const dy = pos1[1] - pos2[1]; + const dz = pos1[2] - pos2[2]; + + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + _calculateCachedStats(annotation, renderingEngine, enabledElement) { + const data = annotation.data; + const { element } = enabledElement.viewport; + + const worldPos1 = data.handles.points[0]; + const worldPos2 = data.handles.points[1]; + const { cachedStats } = data; + const targetIds = Object.keys(cachedStats); + + // TODO clean up, this doesn't need a length per volume, it has no stats derived from volumes. + + for (let i = 0; i < targetIds.length; i++) { + const targetId = targetIds[i]; + + const image = this.getTargetImageData(targetId); + + // If image does not exists for the targetId, skip. This can be due + // to various reasons such as if the target was a volumeViewport, and + // the volumeViewport has been decached in the meantime. + if (!image) { + continue; + } + + const { imageData, dimensions } = image; + + const index1 = transformWorldToIndex(imageData, worldPos1); + const index2 = transformWorldToIndex(imageData, worldPos2); + const handles = [index1, index2]; + const { scale, unit } = getCalibratedLengthUnitsAndScale(image, handles); + + const length = this._calculateLength(worldPos1, worldPos2) / scale; + + if (this._isInsideVolume(index1, index2, dimensions)) { + this.isHandleOutsideImage = false; + } else { + this.isHandleOutsideImage = true; + } + + // TODO -> Do we instead want to clip to the bounds of the volume and only include that portion? + // Seems like a lot of work for an unrealistic case. At the moment bail out of stat calculation if either + // corner is off the canvas. + + // todo: add insideVolume calculation, for removing tool if outside + cachedStats[targetId] = { + length, + unit, + }; + } + + const invalidated = annotation.invalidated; + annotation.invalidated = false; + + // Dispatching annotation modified only if it was invalidated + if (invalidated) { + triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated); + } + + return cachedStats; + } + + _isInsideVolume(index1, index2, dimensions) { + return ( + csUtils.indexWithinDimensions(index1, dimensions) && + csUtils.indexWithinDimensions(index2, dimensions) + ); + } + + private _shouldShowMagnifier(): boolean { + return ( + this.configuration.magnifier?.enabled && + this.currentMousePosition !== null && + (this.editData?.stage === 'placingFirst' || + this.editData?.stage === 'placingSecond' || + (this.editData && + this.editData.handleIndex !== undefined && + this.isDrawing)) + ); + } +} + +function defaultGetTextLines(data, targetId): string[] { + const cachedVolumeStats = data.cachedStats[targetId]; + const { length, unit } = cachedVolumeStats; + + // Can be null on load + if (length === undefined || length === null || isNaN(length)) { + return; + } + + const textLines = [`${csUtils.roundNumber(length)} ${unit}`]; + + return textLines; +} + +export default LengthToolZoom; diff --git a/packages/tools/src/tools/annotation/LengthToolZoom/LensOverlay.ts b/packages/tools/src/tools/annotation/LengthToolZoom/LensOverlay.ts new file mode 100644 index 0000000000..b925baba2a --- /dev/null +++ b/packages/tools/src/tools/annotation/LengthToolZoom/LensOverlay.ts @@ -0,0 +1,297 @@ +import { getEnabledElement } from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; + +export interface LensOverlayConfig { + enabled: boolean; + radius: number; + zoomFactor: number; + borderWidth: number; + borderColor: string; + showCrosshair: boolean; +} + +export class LensOverlay { + private canvas: HTMLCanvasElement | null = null; + private context: CanvasRenderingContext2D | null = null; + private visible: boolean = false; + private currentMousePosition: Types.Point2 | null = null; + private animationFrame: number | null = null; + private currentElement: HTMLDivElement | null = null; + private config: LensOverlayConfig; + + constructor(config: LensOverlayConfig) { + this.config = config; + } + + public updateMousePosition(position: Types.Point2 | null): void { + this.currentMousePosition = position; + } + + public show(element: HTMLDivElement): void { + if (this.visible) { + return; + } + + this.currentElement = element; + this.initializeCanvas(element); + this.visible = true; + this.scheduleRender(); + } + + public hide(): void { + if (!this.visible) { + return; + } + + this.visible = false; + this.cancelAnimationFrame(); + + if (this.canvas && this.canvas.parentElement) { + this.canvas.parentElement.removeChild(this.canvas); + this.canvas = null; + this.context = null; + } + } + + public isVisible(): boolean { + return this.visible; + } + + public updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + private initializeCanvas(element: HTMLDivElement): void { + if (this.canvas) { + return; + } + + const { radius } = this.config; + const canvasSize = radius * 2; + + this.canvas = document.createElement('canvas'); + this.canvas.width = canvasSize; + this.canvas.height = canvasSize; + this.canvas.style.cssText = ` + position: absolute; + width: ${canvasSize}px; + height: ${canvasSize}px; + pointer-events: none; + z-index: 1000; + display: block; + `; + + this.context = this.canvas.getContext('2d'); + + const viewportElement = element.querySelector('.viewport-element'); + if (viewportElement) { + viewportElement.appendChild(this.canvas); + } + } + + private scheduleRender(): void { + if (typeof window === 'undefined' || !this.visible) { + return; + } + + if (this.animationFrame !== null) { + window.cancelAnimationFrame(this.animationFrame); + } + + const render = () => { + this.animationFrame = null; + + if (!this.visible) { + return; + } + + this.render(); + + // Continue animation frame loop + if (this.visible) { + this.animationFrame = window.requestAnimationFrame(render); + } + }; + + this.animationFrame = window.requestAnimationFrame(render); + } + + private cancelAnimationFrame(): void { + if (typeof window !== 'undefined' && this.animationFrame !== null) { + window.cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + } + + private render(): void { + if (!this.canvas || !this.context || !this.currentMousePosition) { + return; + } + + const { radius, zoomFactor, borderWidth, borderColor } = this.config; + const [mouseX, mouseY] = this.currentMousePosition; + + // Canvas leeren + this.context.clearRect(0, 0, radius * 2, radius * 2); + + // Clipping-Pfad für runde Lupe + this.context.save(); + this.context.beginPath(); + this.context.arc(radius, radius, radius, 0, 2 * Math.PI); + this.context.clip(); + + // Bildausschnitt zeichnen + this.drawMagnifiedRegion(mouseX, mouseY, radius, zoomFactor); + + this.context.restore(); + + // Rahmen und Hilfslinien zeichnen + this.drawOverlay(radius, borderWidth, borderColor); + + // Positionierung der Lupe + this.positionLens(mouseX, mouseY, radius); + } + + private drawMagnifiedRegion( + centerX: number, + centerY: number, + radius: number, + zoomFactor: number + ): void { + if (!this.currentElement) return; + + const enabledElement = getEnabledElement(this.currentElement); + if (!enabledElement) return; + + const { viewport } = enabledElement; + const sourceCanvas = viewport.getCanvas(); + + // Berücksichtige devicePixelRatio für korrekte Koordinaten + const devicePixelRatio = window.devicePixelRatio || 1; + + // Canvas-Dimensionen für Grenzprüfung + const canvasWidth = sourceCanvas.width; + const canvasHeight = sourceCanvas.height; + + // Die tatsächliche CSS-Größe des Canvas + const canvasRect = sourceCanvas.getBoundingClientRect(); + const cssToCanvasScaleX = canvasWidth / canvasRect.width; + const cssToCanvasScaleY = canvasHeight / canvasRect.height; + + // Transformiere CSS-Koordinaten in Canvas-Koordinaten + const canvasCenterX = centerX * cssToCanvasScaleX; + const canvasCenterY = centerY * cssToCanvasScaleY; + + const sourceSize = (radius / zoomFactor) * cssToCanvasScaleX; // Verwende X-Skala für beide Dimensionen für Konsistenz + const destSize = radius * 2; + + // Berechne den Quellbereich um die Mausposition zentriert + const srcX = canvasCenterX - sourceSize; + const srcY = canvasCenterY - sourceSize; + const srcWidth = sourceSize * 2; + const srcHeight = sourceSize * 2; + + // Erstelle temporären Canvas für die Pixelwiederholung + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = srcWidth; + tempCanvas.height = srcHeight; + const tempContext = tempCanvas.getContext('2d'); + + if (!tempContext) return; + + // Hole die ImageData des Quell-Canvas + const sourceImageData = sourceCanvas + .getContext('2d') + ?.getImageData(0, 0, canvasWidth, canvasHeight); + if (!sourceImageData) return; + + // Erstelle ImageData für temporären Canvas + const tempImageData = tempContext.createImageData(srcWidth, srcHeight); + + // Fülle temporäres ImageData mit wiederholten Randpixeln + for (let y = 0; y < srcHeight; y++) { + for (let x = 0; x < srcWidth; x++) { + // Berechne die tatsächliche Position im Quell-Canvas + let sourceX = Math.floor(srcX + x); + let sourceY = Math.floor(srcY + y); + + // Clampe die Koordinaten auf den gültigen Bereich (0 bis canvasWidth/Height - 1) + sourceX = Math.max(0, Math.min(canvasWidth - 1, sourceX)); + sourceY = Math.max(0, Math.min(canvasHeight - 1, sourceY)); + + // Berechne Indizes für Quell- und Ziel-ImageData + const sourceIndex = (sourceY * canvasWidth + sourceX) * 4; + const tempIndex = (y * srcWidth + x) * 4; + + // Kopiere Pixelwerte + tempImageData.data[tempIndex] = sourceImageData.data[sourceIndex]; // R + tempImageData.data[tempIndex + 1] = + sourceImageData.data[sourceIndex + 1]; // G + tempImageData.data[tempIndex + 2] = + sourceImageData.data[sourceIndex + 2]; // B + tempImageData.data[tempIndex + 3] = + sourceImageData.data[sourceIndex + 3]; // A + } + } + + // Setze die ImageData auf temporären Canvas + tempContext.putImageData(tempImageData, 0, 0); + + // Zeichne den temporären Canvas auf den Lens-Canvas + this.context?.drawImage( + tempCanvas, + 0, // Quell-X + 0, // Quell-Y + srcWidth, // Quell-Breite + srcHeight, // Quell-Höhe + 0, // Ziel-X + 0, // Ziel-Y + destSize, // Ziel-Breite + destSize // Ziel-Höhe + ); + } + + private drawOverlay( + radius: number, + borderWidth: number, + borderColor: string + ): void { + if (!this.context) return; + + // Rahmen zeichnen (2px dick) + this.context.strokeStyle = borderColor; + this.context.lineWidth = 2; + this.context.beginPath(); + this.context.arc(radius, radius, radius - 1, 0, 2 * Math.PI); + this.context.stroke(); + + // 4x4 Pixel großen Punkt in der Mitte zeichnen + this.context.fillStyle = '#EC6602'; + this.context.fillRect(radius - 2, radius - 2, 4, 4); + } + + private positionLens(mouseX: number, mouseY: number, radius: number): void { + if (!this.canvas) return; + + // Positionierung oberhalb und rechts der Maus, um nicht die Sicht zu blockieren + const offset = 20; + const viewportRect = this.canvas.parentElement?.getBoundingClientRect(); + + if (viewportRect) { + let posX = mouseX + offset; + let posY = mouseY - radius - offset; + + // Sicherstellen, dass die Lupe innerhalb des Viewports bleibt + if (posX + radius * 2 > viewportRect.width) { + posX = mouseX - radius * 2 - offset; + } + + if (posY < 0) { + posY = mouseY + offset; + } + + this.canvas.style.left = `${posX}px`; + this.canvas.style.top = `${posY}px`; + } + } +} diff --git a/packages/tools/src/tools/annotation/LengthToolZoom/index.ts b/packages/tools/src/tools/annotation/LengthToolZoom/index.ts new file mode 100644 index 0000000000..150a7568a3 --- /dev/null +++ b/packages/tools/src/tools/annotation/LengthToolZoom/index.ts @@ -0,0 +1,2 @@ +export { default } from './LengthToolZoom'; +export { LensOverlay, type LensOverlayConfig } from './LensOverlay'; diff --git a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts index 72d242fcad..c6d727dde6 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts @@ -865,13 +865,11 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { } else { this.updateOpenCachedStats({ metadata, - canvasCoordinates, targetId, cachedStats, modalityUnit, calibratedScale, - deltaInX, - deltaInY, + points, }); } } @@ -946,8 +944,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { // Convert from canvas_pixels ^2 to mm^2 area *= deltaInX * deltaInY; - let perimeter = calculatePerimeter(canvasCoordinates, closed) / scale; - perimeter *= Math.sqrt(Math.pow(deltaInX, 2) + Math.pow(deltaInY, 2)); + const perimeter = calculatePerimeter(points, closed) / scale; // Expand bounding box const iDelta = 0.01 * (iMax - iMin); @@ -1043,17 +1040,14 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { protected updateOpenCachedStats({ targetId, metadata, - canvasCoordinates, cachedStats, modalityUnit, calibratedScale, - deltaInX, - deltaInY, + points, }) { const { scale, unit } = calibratedScale; - let length = calculatePerimeter(canvasCoordinates, closed) / scale; - length *= Math.sqrt(Math.pow(deltaInX, 2) + Math.pow(deltaInY, 2)); + const length = calculatePerimeter(points, closed) / scale; cachedStats[targetId] = { Modality: metadata.Modality, diff --git a/packages/tools/src/tools/annotation/SplineContourSegmentationTool.ts b/packages/tools/src/tools/annotation/SplineContourSegmentationTool.ts index b8542ee967..7b1cd2576b 100644 --- a/packages/tools/src/tools/annotation/SplineContourSegmentationTool.ts +++ b/packages/tools/src/tools/annotation/SplineContourSegmentationTool.ts @@ -45,6 +45,7 @@ class SplineContourSegmentationTool extends SplineROITool { protected annotationCutMergeCompleted(evt) { const { sourceAnnotation: annotation } = evt.detail; if ( + this.toolName !== annotation?.metadata?.toolName || !this.splineToolNames.includes(annotation?.metadata?.toolName) || !this.configuration.simplifiedSpline ) { diff --git a/packages/tools/src/tools/base/AnnotationTool.ts b/packages/tools/src/tools/base/AnnotationTool.ts index 98744e4d6c..3c932c8e85 100644 --- a/packages/tools/src/tools/base/AnnotationTool.ts +++ b/packages/tools/src/tools/base/AnnotationTool.ts @@ -630,6 +630,15 @@ abstract class AnnotationTool extends AnnotationDisplayTool { ); } + protected startGroupRecording() { + DefaultHistoryMemo.startGroupRecording(); + } + + /** Ends a group recording of history memo */ + protected endGroupRecording() { + DefaultHistoryMemo.endGroupRecording(); + } + protected static hydrateBase( ToolClass: new () => T, enabledElement: Types.IEnabledElement, diff --git a/packages/tools/src/tools/index.ts b/packages/tools/src/tools/index.ts index 186d2c7d76..94b879a6c7 100644 --- a/packages/tools/src/tools/index.ts +++ b/packages/tools/src/tools/index.ts @@ -24,6 +24,7 @@ import VolumeRotateTool from './VolumeRotateTool'; import BidirectionalTool from './annotation/BidirectionalTool'; import LabelTool from './annotation/LabelTool'; import LengthTool from './annotation/LengthTool'; +import LengthToolZoom from './annotation/LengthToolZoom'; import HeightTool from './annotation/HeightTool'; import ProbeTool from './annotation/ProbeTool'; import DragProbeTool from './annotation/DragProbeTool'; @@ -92,6 +93,7 @@ export { BidirectionalTool, LabelTool, LengthTool, + LengthToolZoom, HeightTool, ProbeTool, RectangleROITool, diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 1cf6eccaa9..539295546d 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -37,6 +37,12 @@ import { getStrategyData } from './strategies/utils/getStrategyData'; */ class BrushTool extends LabelmapBaseTool { static toolName; + // Remember the last drag position in both canvas and world space so we can + // pass a full stroke segment to the strategies instead of a single point. + private _lastDragInfo: { + canvas: Types.Point2; + world: Types.Point3; + } | null = null; constructor( toolProps: PublicToolProps = {}, @@ -180,8 +186,9 @@ class BrushTool extends LabelmapBaseTool { evt: EventTypes.MouseDownActivateEventType ): boolean => { const eventData = evt.detail; - const { element } = eventData; + const { element, currentPoints } = eventData; const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; // @ts-expect-error this._editData = this.createEditData(element); @@ -194,6 +201,15 @@ class BrushTool extends LabelmapBaseTool { // This might be a mouse down this._previewData.isDrag = false; this._previewData.timerStart = Date.now(); + const canvasPoint = vec2.clone(currentPoints.canvas) as Types.Point2; + const worldPoint = viewport.canvasToWorld([ + canvasPoint[0], + canvasPoint[1], + ]) as Types.Point3; + this._lastDragInfo = { + canvas: canvasPoint, + world: vec3.clone(worldPoint) as Types.Point3, + }; const hoverData = this._hoverData || this.createHoverData(element); @@ -343,6 +359,7 @@ class BrushTool extends LabelmapBaseTool { const eventData = evt.detail; const { element, currentPoints } = eventData; const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; this.updateCursor(evt); @@ -370,17 +387,53 @@ class BrushTool extends LabelmapBaseTool { window.clearTimeout(this._previewData.timer); this._previewData.timer = null; } + if (!this._lastDragInfo) { + const startCanvas = this._previewData.startPoint; + const startWorld = viewport.canvasToWorld([ + startCanvas[0], + startCanvas[1], + ]) as Types.Point3; + this._lastDragInfo = { + canvas: vec2.clone(startCanvas) as Types.Point2, + world: vec3.clone(startWorld) as Types.Point3, + }; + } + + const currentCanvas = currentPoints.canvas; + const currentWorld = viewport.canvasToWorld([ + currentCanvas[0], + currentCanvas[1], + ]) as Types.Point3; + + this._hoverData = this.createHoverData(element, currentCanvas); + + this._calculateCursor(element, currentCanvas); + + const operationData = this.getOperationData(element); + // Hand the strategy the exact stroke segment we just traversed so it can + // paint a continuous capsule in one pass instead of trying to infer the + // path from scattered samples. + operationData.strokePointsWorld = [ + vec3.clone(this._lastDragInfo.world) as Types.Point3, + vec3.clone(currentWorld) as Types.Point3, + ]; this._previewData.preview = this.applyActiveStrategy( enabledElement, - this.getOperationData(element) + operationData ); + + const currentCanvasClone = vec2.clone(currentCanvas) as Types.Point2; + this._lastDragInfo = { + canvas: currentCanvasClone, + world: vec3.clone(currentWorld) as Types.Point3, + }; this._previewData.element = element; // Add a bit of time to the timer start so small accidental movements dont // cause issues on clicking this._previewData.timerStart = Date.now() + dragTimeMs; this._previewData.isDrag = true; - this._previewData.startPoint = currentPoints.canvas; + this._previewData.startPoint = currentCanvasClone; }; private _calculateCursor(element, centerCanvas) { @@ -484,6 +537,8 @@ class BrushTool extends LabelmapBaseTool { this._editData = null; + this._lastDragInfo = null; + this.applyActiveStrategyCallback( enabledElement, operationData, diff --git a/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts b/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts index 5bce524e02..253d91c192 100644 --- a/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts +++ b/packages/tools/src/tools/segmentation/LabelmapEditWithContour.ts @@ -121,6 +121,10 @@ class LabelMapEditWithContourTool extends PlanarFreehandContourSegmentationTool Events.SEGMENTATION_MODIFIED, this.onSegmentationModifiedBinded ); + eventTarget.addEventListener( + Events.SEGMENTATION_REPRESENTATION_MODIFIED, + this.onSegmentationModifiedBinded + ); } /** @@ -155,6 +159,10 @@ class LabelMapEditWithContourTool extends PlanarFreehandContourSegmentationTool Events.SEGMENTATION_MODIFIED, this.onSegmentationModified.bind(this) ); + eventTarget.removeEventListener( + Events.SEGMENTATION_REPRESENTATION_MODIFIED, + this.onSegmentationModified.bind(this) + ); } /** diff --git a/packages/tools/src/tools/segmentation/strategies/__tests__/fillCircle.spec.ts b/packages/tools/src/tools/segmentation/strategies/__tests__/fillCircle.spec.ts new file mode 100644 index 0000000000..84d5aeae66 --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/__tests__/fillCircle.spec.ts @@ -0,0 +1,34 @@ +import type { Types } from '@cornerstonejs/core'; + +import { createPointInEllipse } from '../fillCircle'; + +describe('createPointInEllipse', () => { + const corners: Types.Point3[] = [ + [-1, 1, 0], + [1, -1, 0], + [-1, -1, 0], + [1, 1, 0], + ]; + + it('detects points inside the base circle', () => { + const predicate = createPointInEllipse(corners); + + expect(predicate([0, 0, 0] as Types.Point3)).toBe(true); + expect(predicate([0.5, 0.5, 0] as Types.Point3)).toBe(true); + expect(predicate([1.2, 0, 0] as Types.Point3)).toBe(false); + }); + + it('covers interpolated stroke segments', () => { + const predicate = createPointInEllipse(corners, { + strokePointsWorld: [ + [-2, 0, 0] as Types.Point3, + [2, 0, 0] as Types.Point3, + ], + radius: 1, + }); + + expect(predicate([0, 0, 0] as Types.Point3)).toBe(true); + expect(predicate([1.5, 0, 0] as Types.Point3)).toBe(true); + expect(predicate([3.2, 0, 0] as Types.Point3)).toBe(false); + }); +}); diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index d5ab593796..613d588290 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -1,6 +1,8 @@ import { vec3 } from 'gl-matrix'; +import type { ReadonlyVec3 } from 'gl-matrix'; import { utilities as csUtils } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; +import type vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import { getBoundingBoxAroundShapeIJK } from '../../../utilities/boundingBox'; import BrushStrategy from './BrushStrategy'; @@ -10,7 +12,7 @@ import { StrategyCallbacks } from '../../../enums'; import compositions from './compositions'; import { pointInSphere } from '../../../utilities/math/sphere'; -const { transformWorldToIndex, isEqual } = csUtils; +const { transformWorldToIndex, transformIndexToWorld, isEqual } = csUtils; /** * Returns the corners of an ellipse in canvas coordinates. @@ -30,12 +32,119 @@ export function getEllipseCornersFromCanvasCoordinates( return [topLeft, bottomRight, bottomLeft, topRight]; } +function createCircleCornersForCenter( + center: Types.Point3, + viewUp: ReadonlyVec3, + viewRight: ReadonlyVec3, + radius: number +): Types.Point3[] { + const centerVec = vec3.fromValues(center[0], center[1], center[2]); + + const top = vec3.create(); + vec3.scaleAndAdd(top, centerVec, viewUp, radius); + + const bottom = vec3.create(); + vec3.scaleAndAdd(bottom, centerVec, viewUp, -radius); + + const right = vec3.create(); + vec3.scaleAndAdd(right, centerVec, viewRight, radius); + + const left = vec3.create(); + vec3.scaleAndAdd(left, centerVec, viewRight, -radius); + + return [ + bottom as Types.Point3, + top as Types.Point3, + left as Types.Point3, + right as Types.Point3, + ]; +} + +// Build a lightweight capsule predicate that covers every sampled point and +// the straight segment in between. The previous approach re-ran the brush +// strategy for many intermediate samples, which was unnecessarily expensive +// and still missed fast mouse moves. This predicate lets us describe the full +// swept volume in constant time per segment when the strategy runs. +function createStrokePredicate(centers: Types.Point3[], radius: number) { + if (!centers.length || radius <= 0) { + return null; + } + + const radiusSquared = radius * radius; + const centerVecs = centers.map( + (point) => [point[0], point[1], point[2]] as Types.Point3 + ); + const segments = [] as Array<{ + start: Types.Point3; + vector: [number, number, number]; + lengthSquared: number; + }>; + + for (let i = 1; i < centerVecs.length; i++) { + const start = centerVecs[i - 1]; + const end = centerVecs[i]; + const dx = end[0] - start[0]; + const dy = end[1] - start[1]; + const dz = end[2] - start[2]; + const lengthSquared = dx * dx + dy * dy + dz * dz; + + segments.push({ start, vector: [dx, dy, dz], lengthSquared }); + } + + return (worldPoint: Types.Point3) => { + if (!worldPoint) { + return false; + } + + for (const centerVec of centerVecs) { + const dx = worldPoint[0] - centerVec[0]; + const dy = worldPoint[1] - centerVec[1]; + const dz = worldPoint[2] - centerVec[2]; + if (dx * dx + dy * dy + dz * dz <= radiusSquared) { + return true; + } + } + + for (const { start, vector, lengthSquared } of segments) { + if (lengthSquared === 0) { + const dx = worldPoint[0] - start[0]; + const dy = worldPoint[1] - start[1]; + const dz = worldPoint[2] - start[2]; + if (dx * dx + dy * dy + dz * dz <= radiusSquared) { + return true; + } + continue; + } + + const dx = worldPoint[0] - start[0]; + const dy = worldPoint[1] - start[1]; + const dz = worldPoint[2] - start[2]; + const dot = dx * vector[0] + dy * vector[1] + dz * vector[2]; + const t = Math.max(0, Math.min(1, dot / lengthSquared)); + const projX = start[0] + vector[0] * t; + const projY = start[1] + vector[1] * t; + const projZ = start[2] + vector[2] * t; + const distX = worldPoint[0] - projX; + const distY = worldPoint[1] - projY; + const distZ = worldPoint[2] - projZ; + + if (distX * distX + distY * distY + distZ * distZ <= radiusSquared) { + return true; + } + } + + return false; + }; +} + const initializeCircle = { [StrategyCallbacks.Initialize]: (operationData: InitializedOperationData) => { const { points, // bottom, top, left, right viewport, segmentationImageData, + viewUp, + viewPlaneNormal, } = operationData; // Happens on a preview setup @@ -60,6 +169,9 @@ const initializeCircle = { center as Types.Point3 ); + const brushRadius = + points.length >= 2 ? vec3.distance(points[0], points[1]) / 2 : 0; + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p) ) as CanvasCoordinates; @@ -69,20 +181,58 @@ const initializeCircle = { const cornersInWorld = corners.map((corner) => viewport.canvasToWorld(corner) ); - // 2. Find the extent of the ellipse (circle) in IJK index space of the image - const circleCornersIJK = points.map((world) => { - return transformWorldToIndex(segmentationImageData, world); - }); - // get the bounds from the circle points since in oblique images the - // circle will not be axis aligned + const normalizedViewUp = vec3.fromValues(viewUp[0], viewUp[1], viewUp[2]); + vec3.normalize(normalizedViewUp, normalizedViewUp); + + const normalizedPlaneNormal = vec3.fromValues( + viewPlaneNormal[0], + viewPlaneNormal[1], + viewPlaneNormal[2] + ); + vec3.normalize(normalizedPlaneNormal, normalizedPlaneNormal); + + const viewRight = vec3.create(); + vec3.cross(viewRight, normalizedViewUp, normalizedPlaneNormal); + vec3.normalize(viewRight, viewRight); + + // Build a set of explicit stroke centers. When we only looked at the last + // sample, quick cursor moves left holes behind. Feeding the full segment + // gives us deterministic coverage regardless of device speed. + const strokeCentersSource = + operationData.strokePointsWorld && + operationData.strokePointsWorld.length > 0 + ? operationData.strokePointsWorld + : [operationData.centerWorld]; + + const strokeCenters = strokeCentersSource.map( + (point) => vec3.clone(point) as Types.Point3 + ); + + const strokeCornersWorld = strokeCenters.flatMap((centerPoint) => + createCircleCornersForCenter( + centerPoint, + normalizedViewUp, + viewRight, + brushRadius + ) + ); + + const circleCornersIJK = strokeCornersWorld.map((world) => + transformWorldToIndex(segmentationImageData, world) + ); + const boundsIJK = getBoundingBoxAroundShapeIJK( circleCornersIJK, segmentationImageData.getDimensions() ); - // 3. Derives the ellipse function from the corners - operationData.isInObject = createPointInEllipse(cornersInWorld); + operationData.strokePointsWorld = strokeCenters; + operationData.isInObject = createPointInEllipse(cornersInWorld, { + strokePointsWorld: strokeCenters, + segmentationImageData, + radius: brushRadius, + }); operationData.isInObjectBoundsIJK = boundsIJK; }, @@ -96,7 +246,14 @@ const initializeCircle = { * sphere shape (same radius in two or three dimensions), or an elliptical shape * if they differ. */ -function createPointInEllipse(cornersInWorld: Types.Point3[] = []) { +function createPointInEllipse( + cornersInWorld: Types.Point3[] = [], + options: { + strokePointsWorld?: Types.Point3[]; + segmentationImageData?: vtkImageData; + radius?: number; + } = {} +) { if (!cornersInWorld || cornersInWorld.length !== 4) { throw new Error('createPointInEllipse: cornersInWorld must have 4 points'); } @@ -125,6 +282,12 @@ function createPointInEllipse(cornersInWorld: Types.Point3[] = []) { vec3.normalize(normal, normal); // If radii are equal, treat as sphere + const radiusForStroke = options.radius ?? Math.max(xRadius, yRadius); + const strokePredicate = createStrokePredicate( + options.strokePointsWorld || [], + radiusForStroke + ); + if (isEqual(xRadius, yRadius)) { const radius = xRadius; const sphereObj = { @@ -132,14 +295,55 @@ function createPointInEllipse(cornersInWorld: Types.Point3[] = []) { radius, radius2: radius * radius, }; - return (pointLPS) => pointInSphere(sphereObj, pointLPS); + return (pointLPS: Types.Point3 | null, pointIJK?: Types.Point3) => { + let worldPoint: Types.Point3 | null = pointLPS; + + // When the iterator only supplies IJK coordinates we reconstruct the + // world position once here instead of forcing callers to do the + // conversion (the previous code re-did this work on every sample). + if (!worldPoint && pointIJK && options.segmentationImageData) { + worldPoint = transformIndexToWorld( + options.segmentationImageData, + pointIJK as Types.Point3 + ) as Types.Point3; + } + + if (!worldPoint) { + return false; + } + + if (strokePredicate?.(worldPoint)) { + return true; + } + + return pointInSphere(sphereObj, worldPoint); + }; } // Otherwise, treat as ellipse in oblique plane - return (pointLPS: Types.Point3) => { - // Project point onto the plane + return (pointLPS: Types.Point3 | null, pointIJK?: Types.Point3) => { + let worldPoint: Types.Point3 | null = pointLPS; + + if (!worldPoint && pointIJK && options.segmentationImageData) { + worldPoint = transformIndexToWorld( + options.segmentationImageData, + pointIJK as Types.Point3 + ) as Types.Point3; + } + + if (!worldPoint) { + return false; + } + + if (strokePredicate?.(worldPoint)) { + return true; + } + + // Project point onto the plane so we can evaluate the ellipse equation in + // plane coordinates. We do this once per sample; previously the repeated + // conversions happened on callers for every interpolated point. const pointVec = vec3.create(); - vec3.subtract(pointVec, pointLPS, center); + vec3.subtract(pointVec, worldPoint, center); // Remove component along normal const distToPlane = vec3.dot(pointVec, normal); const proj = vec3.create(); @@ -213,5 +417,6 @@ export { CIRCLE_THRESHOLD_STRATEGY, fillInsideCircle, thresholdInsideCircle, + createPointInEllipse, createPointInEllipse as createEllipseInPoint, }; diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 59583c1b11..6d70e3ab7a 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -39,7 +39,7 @@ const sphereComposition = { center as Types.Point3 ); - const { boundsIJK: newBoundsIJK } = getSphereBoundsInfoFromViewport( + const baseExtent = getSphereBoundsInfoFromViewport( points.slice(0, 2) as [Types.Point3, Types.Point3], segmentationImageData, viewport @@ -54,8 +54,97 @@ const sphereComposition = { viewport.canvasToWorld(corner) ); - operationData.isInObjectBoundsIJK = newBoundsIJK; - operationData.isInObject = createEllipseInPoint(cornersInWorld); + const strokeRadius = + points.length >= 2 ? vec3.distance(points[0], points[1]) / 2 : undefined; + + const strokeCenters = + operationData.strokePointsWorld && + operationData.strokePointsWorld.length > 0 + ? operationData.strokePointsWorld + : [operationData.centerWorld]; + + // The original implementation recalculated the expensive sphere bounds for + // every interpolated point. That repeats a handful of world-to-index + // conversions per sample, which adds up quickly during fast brushes. We + // know each stroke point simply translates the same sphere, so we can reuse + // the base bounds and slide them by the delta in IJK space instead. + const baseBounds = baseExtent.boundsIJK; + const baseCenterIJK = operationData.centerIJK; + const boundsForStroke = strokeCenters.reduce( + (acc, centerPoint) => { + if (!centerPoint) { + return acc; + } + + const translatedCenterIJK = transformWorldToIndex( + segmentationImageData, + centerPoint as Types.Point3 + ); + const deltaIJK = [ + translatedCenterIJK[0] - baseCenterIJK[0], + translatedCenterIJK[1] - baseCenterIJK[1], + translatedCenterIJK[2] - baseCenterIJK[2], + ]; + + const translatedBounds: Types.BoundsIJK = [ + [baseBounds[0][0] + deltaIJK[0], baseBounds[0][1] + deltaIJK[0]], + [baseBounds[1][0] + deltaIJK[1], baseBounds[1][1] + deltaIJK[1]], + [baseBounds[2][0] + deltaIJK[2], baseBounds[2][1] + deltaIJK[2]], + ]; + + if (!acc) { + return translatedBounds; + } + + return [ + [ + Math.min(acc[0][0], translatedBounds[0][0]), + Math.max(acc[0][1], translatedBounds[0][1]), + ], + [ + Math.min(acc[1][0], translatedBounds[1][0]), + Math.max(acc[1][1], translatedBounds[1][1]), + ], + [ + Math.min(acc[2][0], translatedBounds[2][0]), + Math.max(acc[2][1], translatedBounds[2][1]), + ], + ] as Types.BoundsIJK; + }, + null + ); + + const boundsToUse = boundsForStroke ?? baseExtent.boundsIJK; + + if (segmentationImageData) { + const dimensions = segmentationImageData.getDimensions(); + // Clamp once at the end to keep the bounds valid for downstream + // iteration. We were clamping each partial result previously, which was + // redundant and still left us doing extra work when a drag crossed the + // image edges. + operationData.isInObjectBoundsIJK = [ + [ + Math.max(0, Math.min(boundsToUse[0][0], dimensions[0] - 1)), + Math.max(0, Math.min(boundsToUse[0][1], dimensions[0] - 1)), + ], + [ + Math.max(0, Math.min(boundsToUse[1][0], dimensions[1] - 1)), + Math.max(0, Math.min(boundsToUse[1][1], dimensions[1] - 1)), + ], + [ + Math.max(0, Math.min(boundsToUse[2][0], dimensions[2] - 1)), + Math.max(0, Math.min(boundsToUse[2][1], dimensions[2] - 1)), + ], + ] as Types.BoundsIJK; + } else { + operationData.isInObjectBoundsIJK = boundsToUse; + } + + operationData.isInObject = createEllipseInPoint(cornersInWorld, { + strokePointsWorld: operationData.strokePointsWorld, + segmentationImageData, + radius: strokeRadius, + }); // } }, } as Composition; diff --git a/packages/tools/src/types/AnnotationTypes.ts b/packages/tools/src/types/AnnotationTypes.ts index 7baa0d9ce3..cc6b9c0a52 100644 --- a/packages/tools/src/types/AnnotationTypes.ts +++ b/packages/tools/src/types/AnnotationTypes.ts @@ -130,6 +130,8 @@ export type Handles = { textBox?: { /** whether the text box has moved */ hasMoved?: boolean; + /** whether the text box is currently moving */ + isMoving?: boolean; /** the world location of the text box */ worldPosition?: Types.Point3; /** text box bounding box information */ diff --git a/packages/tools/src/types/LabelmapToolOperationData.ts b/packages/tools/src/types/LabelmapToolOperationData.ts index 128de8f25b..1aa36b32ed 100644 --- a/packages/tools/src/types/LabelmapToolOperationData.ts +++ b/packages/tools/src/types/LabelmapToolOperationData.ts @@ -21,6 +21,11 @@ type LabelmapToolOperationData = { viewUp: number[]; activeStrategy: string; points: Types.Point3[]; + /** + * Optional pair of world points describing the last stroke segment so the + * strategy can cover the full path instead of a single sample location. + */ + strokePointsWorld?: Types.Point3[]; voxelManager; override: { voxelManager: Types.IVoxelManager; diff --git a/packages/tools/src/types/SVGDrawingHelper.ts b/packages/tools/src/types/SVGDrawingHelper.ts index 81a3839fe1..b8cdf61918 100644 --- a/packages/tools/src/types/SVGDrawingHelper.ts +++ b/packages/tools/src/types/SVGDrawingHelper.ts @@ -1,5 +1,5 @@ type SVGDrawingHelper = { - svgLayerElement: HTMLDivElement; + svgLayerElement: Element; svgNodeCacheForCanvas: Record; getSvgNode: (cacheKey: string) => SVGGElement | undefined; appendNode: (svgNode: SVGElement, cacheKey: string) => void; diff --git a/packages/tools/src/types/ToolHandle.ts b/packages/tools/src/types/ToolHandle.ts index 2a24f934bb..90215bce38 100644 --- a/packages/tools/src/types/ToolHandle.ts +++ b/packages/tools/src/types/ToolHandle.ts @@ -10,6 +10,7 @@ type AnnotationHandle = Types.Point3; */ type TextBoxHandle = { hasMoved: boolean; + isMoving?: boolean; worldBoundingBox: { bottomLeft: Types.Point3; bottomRight: Types.Point3; diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index 6bf88df04a..a770f779f7 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -73,6 +73,7 @@ export interface LengthAnnotation extends Annotation { activeHandleIndex: number | null; textBox: { hasMoved: boolean; + isMoving?: boolean; worldPosition: Types.Point3; worldBoundingBox: { topLeft: Types.Point3; diff --git a/packages/tools/src/utilities/contourSegmentation/addContourSegmentationAnnotation.ts b/packages/tools/src/utilities/contourSegmentation/addContourSegmentationAnnotation.ts index 4a5429b0e6..83d4c10423 100644 --- a/packages/tools/src/utilities/contourSegmentation/addContourSegmentationAnnotation.ts +++ b/packages/tools/src/utilities/contourSegmentation/addContourSegmentationAnnotation.ts @@ -1,3 +1,4 @@ +import { setAnnotationLocked } from '../../stateManagement/annotation/annotationLocking'; import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation'; import type { ContourSegmentationAnnotation } from '../../types'; @@ -38,6 +39,11 @@ export function addContourSegmentationAnnotation( annotationUIDsMap.set(segmentIndex, annotationsUIDsSet); } + // Lock the annotation if the segment is locked. + if (segmentation.segments[segmentIndex].locked) { + setAnnotationLocked(annotation.annotationUID, true); + } + annotationUIDsMap.set( segmentIndex, annotationsUIDsSet.add(annotation.annotationUID) diff --git a/packages/tools/src/utilities/contourSegmentation/convertContourSegmentation.ts b/packages/tools/src/utilities/contourSegmentation/convertContourSegmentation.ts index 5b03da21a8..e3cfaf8084 100644 --- a/packages/tools/src/utilities/contourSegmentation/convertContourSegmentation.ts +++ b/packages/tools/src/utilities/contourSegmentation/convertContourSegmentation.ts @@ -6,6 +6,7 @@ import { addAnnotation, removeAnnotation } from '../../stateManagement'; import type { ContourSegmentationAnnotation } from '../../types'; import { removeContourSegmentationAnnotation } from './removeContourSegmentationAnnotation'; import { addContourSegmentationAnnotation } from './addContourSegmentationAnnotation'; +import { triggerAnnotationModified } from '../../stateManagement/annotation/helpers/state'; // Default tool name used for converted contour segmentation annotations const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool'; @@ -106,6 +107,8 @@ export default function convertContourSegmentationAnnotation( addAnnotation(newAnnotation, annotation.metadata.FrameOfReferenceUID); addContourSegmentationAnnotation(newAnnotation); + triggerAnnotationModified(newAnnotation); + // Return the newly created annotation return newAnnotation; } diff --git a/packages/tools/src/utilities/contourSegmentation/logicalOperators.ts b/packages/tools/src/utilities/contourSegmentation/logicalOperators.ts index 4e5d05904b..f239176b26 100644 --- a/packages/tools/src/utilities/contourSegmentation/logicalOperators.ts +++ b/packages/tools/src/utilities/contourSegmentation/logicalOperators.ts @@ -164,14 +164,26 @@ function addSegmentInSegmentation( if (!segmentation?.segments) { return; } - segmentation.segments[segmentIndex] = { + + const segmentData: SegmentInfo = segmentation.segments[segmentIndex] ?? { active: false, locked: false, - label, segmentIndex, cachedStats: {}, + label, color, }; + + // Only update label if it's defined + if (label !== undefined) { + segmentData.label = label; + } + // Only update color if it's defined + if (color !== undefined) { + segmentData.color = color; + } + + segmentation.segments[segmentIndex] = segmentData; } /** diff --git a/packages/tools/src/utilities/contours/calculatePerimeter.ts b/packages/tools/src/utilities/contours/calculatePerimeter.ts index 08fc4ba6b7..a1208db327 100644 --- a/packages/tools/src/utilities/contours/calculatePerimeter.ts +++ b/packages/tools/src/utilities/contours/calculatePerimeter.ts @@ -1,3 +1,6 @@ +import type { Types } from '@cornerstonejs/core'; +import { vec3 } from 'gl-matrix'; + /** * Calculates the perimeter of a polyline. * @@ -5,24 +8,23 @@ * @param closed - Indicates whether the polyline is closed or not. * @returns The perimeter of the polyline. */ -function calculatePerimeter(polyline: number[][], closed: boolean): number { +export function calculatePerimeter( + polyline: number[][], + closed: boolean +): number { let perimeter = 0; for (let i = 0; i < polyline.length - 1; i++) { - const point1 = polyline[i]; - const point2 = polyline[i + 1]; - perimeter += Math.sqrt( - Math.pow(point2[0] - point1[0], 2) + Math.pow(point2[1] - point1[1], 2) - ); + const point1 = polyline[i] as Types.Point3; + const point2 = polyline[i + 1] as Types.Point3; + + perimeter += vec3.dist(point1, point2); } if (closed) { - const firstPoint = polyline[0]; - const lastPoint = polyline[polyline.length - 1]; - perimeter += Math.sqrt( - Math.pow(lastPoint[0] - firstPoint[0], 2) + - Math.pow(lastPoint[1] - firstPoint[1], 2) - ); + const firstPoint = polyline[0] as Types.Point3; + const lastPoint = polyline[polyline.length - 1] as Types.Point3; + perimeter += vec3.dist(firstPoint, lastPoint); } return perimeter; diff --git a/packages/tools/src/utilities/getViewportForAnnotation.ts b/packages/tools/src/utilities/getViewportForAnnotation.ts index 177898ed8d..7bdea41732 100644 --- a/packages/tools/src/utilities/getViewportForAnnotation.ts +++ b/packages/tools/src/utilities/getViewportForAnnotation.ts @@ -16,5 +16,15 @@ export default function getViewportForAnnotation( ): Types.IStackViewport | Types.IVolumeViewport | undefined { const viewports = getViewportsForAnnotation(annotation); - return viewports.length ? viewports[0] : undefined; + if (!viewports?.length) { + return undefined; + } + + const viewport = viewports.find((viewport) => + viewport + .getImageIds() + .some((imageId) => imageId === annotation.metadata.referencedImageId) + ); + + return viewport ?? viewports[0]; } diff --git a/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts b/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts index ee847f318a..1698f71088 100644 --- a/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts +++ b/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts @@ -293,7 +293,7 @@ function basicGetStatistics( * A static basic stats calculator that uses shared helper functions. */ export class BasicStatsCalculator extends Calculator { - private static state: BasicStatsState = createBasicStatsState(true); + protected static state: BasicStatsState = createBasicStatsState(true); public static statsInit(options: { storePointData: boolean }): void { if (!options.storePointData) { diff --git a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts index ee9c5a2f81..bc02d66d06 100644 --- a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts +++ b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts @@ -119,13 +119,17 @@ export default function filterAnnotationsWithinSlice( const annotationsWithinSlice = []; for (const annotation of annotationsWithParallelNormals) { - const data = annotation.data; + const { data, metadata, isVisible } = annotation; - const point = data.handles.points[0] || data.contour?.polyline[0]; - - if (!annotation.isVisible) { + if (!isVisible) { continue; } + + const point = + metadata.planeRestriction?.point || + data.handles.points[0] || + data.contour?.polyline[0]; + // A = point // B = focal point // P = normal @@ -133,8 +137,6 @@ export default function filterAnnotationsWithinSlice( // B-A dot P => Distance in the view direction. // this should be less than half the slice distance. - const dir = vec3.create(); - // If the handles has no values, eg a key image or other annotation, it // should just be included. if (!point) { @@ -142,11 +144,11 @@ export default function filterAnnotationsWithinSlice( continue; } - vec3.sub(dir, focalPoint, point); + const dir = vec3.sub(vec3.create(), focalPoint, point); - const dot = vec3.dot(dir, viewPlaneNormal); + const dot = Math.abs(vec3.dot(dir, viewPlaneNormal)); - if (Math.abs(dot) < halfSpacingInNormalDirection) { + if (dot < halfSpacingInNormalDirection) { annotationsWithinSlice.push(annotation); } } diff --git a/packages/tools/src/version.ts b/packages/tools/src/version.ts index 5c8a8fe535..c4612f26ad 100644 --- a/packages/tools/src/version.ts +++ b/packages/tools/src/version.ts @@ -2,4 +2,4 @@ * Auto-generated from version.json * Do not modify this file directly */ -export const version = '4.4.1'; +export const version = '4.5.19'; diff --git a/packages/tools/test/groundTruth/imageURI_64_64_10_5_1_1_0_SEG_Mocked_Brushed.png b/packages/tools/test/groundTruth/imageURI_64_64_10_5_1_1_0_SEG_Mocked_Brushed.png index dc4cc5f776..b90d30396d 100644 Binary files a/packages/tools/test/groundTruth/imageURI_64_64_10_5_1_1_0_SEG_Mocked_Brushed.png and b/packages/tools/test/groundTruth/imageURI_64_64_10_5_1_1_0_SEG_Mocked_Brushed.png differ diff --git a/utils/test/pixel-data-hash.ts b/utils/test/pixel-data-hash.ts new file mode 100644 index 0000000000..8f8b48685a --- /dev/null +++ b/utils/test/pixel-data-hash.ts @@ -0,0 +1,21 @@ +/** + * + * @description Creates a SHA-256 hash of the given image's pixel data. + * This can be used to verify that the pixel data matches an expected value. + * + * @param {ArrayBufferLike} image + * @returns {Promise} + */ +export async function createImageHash( + image: BufferSource +): Promise { + const hashBuffer = await crypto.subtle.digest( + 'SHA-256', + image + ); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; +} diff --git a/version.json b/version.json index 4bcc4df255..35ff33958a 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "4.4.1", - "commit": "d859a20051ed92b625d6e71b92d44c2ef9ced79e" + "version": "4.5.19", + "commit": "1a84852e1448bed31925a623c2dbd09f0cab23fb" } \ No newline at end of file diff --git a/version.txt b/version.txt index 4f3470c166..dbde476f15 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.4.1 \ No newline at end of file +4.5.19 \ No newline at end of file