From dbb8b1526e49b97c883367fbe1926797e2b77d3d Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 9 Jul 2026 20:54:23 -0400 Subject: [PATCH] feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Update to use pnpm (#6031) [simulated squash-merge] * feat: load DICOM SEG images via imageLoader * feat: load DICOM SEG images via imageLoader * PR review fixes * Test fragment of compressed multiframes * fix: Removed dependencies incorrectly * Update frame as a targetted change to avoid stripping remaining params * lock * Update test to agree with the missing representation branch * test(contour): contour color change coverage (#6042) * test(contour): contour interactions delete segment (#6069) --------- Co-authored-by: Bill Wallace * chore(version): Update package versions to 3.13.0-beta.98 [skip ci] * feat(testing): OHIF Test Agent Skills (#5993) * chore(version): Update package versions to 3.13.0-beta.99 [skip ci] * test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072) * chore(version): Update package versions to 3.13.0-beta.100 [skip ci] * chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099) * update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64) * chore(version): Update package versions to 3.13.0-beta.101 [skip ci] * Debug fixes * perf(seg): pass explicit frame decode concurrency (16) to SEG loader Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into createFromDicomSegImageId rather than relying on the adapter default, so the SEG frame fetch/decode parallelism is set at the call site and is ready to become configurable. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal Start a "Behaviours" docs section for documenting how the system and UI behave end-to-end (observed behaviours, design proposals, and failure modes), with an index README and a Docusaurus category. First entry is the proposal for loading a multiframe SEG as a single Part 10 instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs), parse it with dcmjs (handling multipart/related vs raw DICOM), and register the per-frame compressed pixels into the Cornerstone3D core image cache (the single uniform frame registry) so the per-frame load path is served locally while the standard decode path is unchanged. Best-effort: falls back to per-frame fetches on any failure. Co-Authored-By: Claude Opus 4.8 (1M context) * Pre-cache the image data using a full part10 file for performance * Add customizations to specify type of segmentation save * Add clearcache of the cacheData * Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin OHIF to that release. The local branch changes still reach the app via the cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned. Co-Authored-By: Claude Fable 5 * Fix seg OOM * fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader - dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that dcmjs wrote as a garbage (0000,0000) element into every saved file's meta group (download / clipboard / local wadouri blob / local store); keep the hex 00020010 assignment, which is required for string-form _meta fallbacks. - registerNaturalizedDatasetForLocalWadouri: keep the computed frame count local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus NumberOfFrames element in their serialized form. - rsbuild.config: resolve source-map-loader opportunistically (it is not a project dependency; the rule only serves the gitignored cs3d-link workflow) so fresh clones no longer crash at config load. - Regression tests for both writer fixes (mutation-checked). Co-Authored-By: Claude Fable 5 * PR review comment fixes * Update versions --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: diattamo Co-authored-by: ohif-bot Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> --- .../cornerstone-dicom-pmap/package.json | 4 +- extensions/cornerstone-dicom-seg/package.json | 4 +- .../src/commandsModule.ts | 43 +- .../segmentationCustomization.ts | 29 ++ .../src/getCustomizationModule.ts | 10 + .../src/getSopClassHandlerModule.ts | 370 ++++++++++++++++-- .../cornerstone-dicom-seg/src/index.tsx | 2 + .../src/utils/segLocalImageIds.ts | 32 ++ .../src/utils/segmentationConfig.ts | 100 +++++ .../viewports/OHIFCornerstoneSEGViewport.tsx | 14 +- extensions/cornerstone-dicom-sr/package.json | 8 +- .../cornerstone-dynamic-volume/package.json | 6 +- extensions/cornerstone/package.json | 18 +- .../Overlays/CustomizableViewportOverlay.tsx | 5 +- extensions/cornerstone/src/commandsModule.ts | 11 +- extensions/cornerstone/src/index.tsx | 6 + .../SegmentationService.test.ts | 4 + .../SegmentationService.ts | 56 ++- .../CornerstoneViewportService.ts | 132 ++++++- .../src/utils/segmentationHandlers.ts | 12 +- extensions/default/package.json | 3 +- .../default/src/DicomLocalDataSource/index.js | 32 +- .../default/src/DicomWebDataSource/index.ts | 103 ++++- extensions/default/src/commandsModule.ts | 27 +- .../reportDialogCustomization.tsx | 71 +++- extensions/default/src/index.ts | 4 + .../src/utils/appendFrameQueryToImageId.js | 9 + .../default/src/utils/dicomWriter.test.ts | 228 +++++++++++ extensions/default/src/utils/dicomWriter.ts | 110 ++++++ ...gisterNaturalizedDatasetForLocalWadouri.js | 146 +++++++ ...rNaturalizedDatasetForLocalWadouri.test.js | 177 +++++++++ extensions/dicom-pdf/package.json | 2 +- extensions/dicom-video/package.json | 2 +- extensions/measurement-tracking/package.json | 7 +- extensions/test-extension/package.json | 2 +- extensions/tmtv/package.json | 2 +- extensions/usAnnotation/package.json | 4 +- modes/basic-dev-mode/package.json | 1 + modes/basic-test-mode/package.json | 1 + modes/basic/package.json | 1 + modes/longitudinal/package.json | 1 + modes/tmtv/package.json | 1 + modes/usAnnotation/package.json | 4 +- platform/app/.env | 2 +- platform/app/package.json | 4 +- platform/app/public/config/default.js | 6 +- .../customizations/segmentation/binary.jsonc | 19 + .../segmentation/uncompressed.jsonc | 19 + platform/core/package.json | 8 +- platform/core/src/classes/MetadataProvider.ts | 26 +- .../core/src/utils/combineFrameInstance.ts | 16 +- platform/core/src/utils/imageIdToURI.js | 17 + platform/docs/docs/behaviours/README.md | 44 +++ platform/docs/docs/behaviours/_category_.json | 8 + ...segmentation-multiframe-part10-prefetch.md | 368 +++++++++++++++++ .../specificCustomizations.md | 119 ++++++ pnpm-lock.yaml | 336 +++++++++------- pnpm-workspace.yaml | 2 +- rsbuild.config.ts | 25 ++ 59 files changed, 2501 insertions(+), 322 deletions(-) create mode 100644 extensions/cornerstone-dicom-seg/src/customizations/segmentationCustomization.ts create mode 100644 extensions/cornerstone-dicom-seg/src/getCustomizationModule.ts create mode 100644 extensions/cornerstone-dicom-seg/src/utils/segLocalImageIds.ts create mode 100644 extensions/cornerstone-dicom-seg/src/utils/segmentationConfig.ts create mode 100644 extensions/default/src/utils/appendFrameQueryToImageId.js create mode 100644 extensions/default/src/utils/dicomWriter.test.ts create mode 100644 extensions/default/src/utils/dicomWriter.ts create mode 100644 extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.js create mode 100644 extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.test.js create mode 100644 platform/app/public/customizations/segmentation/binary.jsonc create mode 100644 platform/app/public/customizations/segmentation/uncompressed.jsonc create mode 100644 platform/docs/docs/behaviours/README.md create mode 100644 platform/docs/docs/behaviours/_category_.json create mode 100644 platform/docs/docs/behaviours/segmentation-multiframe-part10-prefetch.md diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json index a828e42bd..f96307e73 100644 --- a/extensions/cornerstone-dicom-pmap/package.json +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -41,8 +41,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.1.3", - "@cornerstonejs/core": "5.1.3", + "@cornerstonejs/adapters": "5.4.12", + "@cornerstonejs/core": "5.4.12", "@kitware/vtk.js": "35.5.3" }, "devDependencies": { diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index d26776e5e..3372e6067 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -41,8 +41,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.1.3", - "@cornerstonejs/core": "5.1.3", + "@cornerstonejs/adapters": "5.4.12", + "@cornerstonejs/core": "5.4.12", "@kitware/vtk.js": "35.5.3" }, "devDependencies": { diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 1243f6f24..43ed2dff8 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -6,6 +6,7 @@ import { adaptersRT, adaptersSEG } from '@cornerstonejs/adapters'; import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default'; import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES'; +import { getSegmentationSaveOptions } from './utils/segmentationConfig'; const getTargetViewport = ({ viewportId, viewportGridService }) => { const { viewports, activeViewportId } = viewportGridService.getState(); @@ -34,7 +35,7 @@ const commandsModule = ({ extensionManager, commandsManager, }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { - const { segmentationService, displaySetService, viewportGridService } = + const { segmentationService, displaySetService, viewportGridService, customizationService } = servicesManager.services as AppTypes.Services; const actions = { @@ -88,13 +89,37 @@ const commandsModule = ({ * @returns Returns the generated segmentation data. */ generateSegmentation: ({ segmentationId, options = {} }) => { + // `dataSource` (a data source name) is consumed here to resolve the store + // overrides; it must not be forwarded to the adapter's generateSegmentation. + const { dataSource: dataSourceName, ...generateOptions } = options; const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); - const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId; + const predecessorImageId = + generateOptions.predecessorImageId ?? segmentation.predecessorImageId; + + // A data source may override the app-wide `segmentation.store.*` defaults + // via `configuration.segmentation.store` (different back ends support + // different SEG encodings). Use the named target data source when storing, + // otherwise the active one (e.g. download). + const dataSourceDefinition = dataSourceName + ? extensionManager.getDataSourceDefinition(dataSourceName) + : extensionManager.getActiveDataSourceDefinition(); + const dataSourceStoreOverride = dataSourceDefinition?.configuration?.segmentation?.store; const { imageIds } = segmentation.representationData.Labelmap; const segImages = imageIds.map(imageId => cache.getImage(imageId)); - const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId)); + const referencedImages = segImages.map((segImage, sliceIndex) => { + const referencedImage = cache.getImage(segImage.referencedImageId); + + if (!referencedImage) { + throw new Error( + `Referenced source image not in cache for segmentation slice ${sliceIndex} ` + + `(referencedImageId: ${segImage.referencedImageId}). Ensure the referenced series is fully loaded before storing.` + ); + } + + return referencedImage; + }); const labelmaps2D = []; @@ -173,7 +198,8 @@ const commandsModule = ({ const generatedSegmentation = generateSegmentation(referencedImages, labelmap3D, metaData, { predecessorImageId, - ...options, + ...getSegmentationSaveOptions(customizationService, dataSourceStoreOverride), + ...generateOptions, }); return generatedSegmentation; @@ -257,6 +283,8 @@ const commandsModule = ({ const args = { segmentationId, options: { + // Resolve store overrides against the data source we are storing into. + dataSource: dataSourceName, SeriesDescription: series ? undefined : reportName || label || 'Contour Series', SeriesNumber: series ? undefined : 1 + priorSeriesNumber, predecessorImageId: series, @@ -289,6 +317,8 @@ const commandsModule = ({ generateContour: async args => { const { segmentationId, options } = args; + // `dataSource` is only used by the SEG store path; keep it out of the RTSS options. + const { dataSource: _dataSource, ...contourOptions } = options ?? {}; const segmentations = segmentationService.getSegmentation(segmentationId); // inject colors to the segmentIndex @@ -301,10 +331,11 @@ const commandsModule = ({ Number(segmentIndex) ); }); - const predecessorImageId = options?.predecessorImageId ?? segmentations.predecessorImageId; + const predecessorImageId = + contourOptions.predecessorImageId ?? segmentations.predecessorImageId; const dataset = await generateRTSSFromRepresentation(segmentations, { predecessorImageId, - ...options, + ...contourOptions, }); return { dataset }; }, diff --git a/extensions/cornerstone-dicom-seg/src/customizations/segmentationCustomization.ts b/extensions/cornerstone-dicom-seg/src/customizations/segmentationCustomization.ts new file mode 100644 index 000000000..d6f2d379f --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/customizations/segmentationCustomization.ts @@ -0,0 +1,29 @@ +import { + DEFAULT_SEG_STORE_MODE, + DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID, + type SegmentationMode, +} from '../utils/segmentationConfig'; + +export type SegmentLabelCustomization = { + enabledByDefault?: boolean; + labelColor?: number[]; + hoverTimeout?: number; + background?: string; +}; + +export type SegmentationStoreCustomization = { + defaultMode?: SegmentationMode; + transferSyntaxUID?: string; +}; + +/** Extension-registered defaults: Label Map SEG + RLE Lossless. */ +const segmentationCustomization = { + 'segmentation.store.defaultMode': DEFAULT_SEG_STORE_MODE, + 'segmentation.store.transferSyntaxUID': DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID, + 'segmentation.segmentLabel': { + enabledByDefault: false, + hoverTimeout: 1, + } satisfies SegmentLabelCustomization, +}; + +export default segmentationCustomization; diff --git a/extensions/cornerstone-dicom-seg/src/getCustomizationModule.ts b/extensions/cornerstone-dicom-seg/src/getCustomizationModule.ts new file mode 100644 index 000000000..4321fb98a --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/getCustomizationModule.ts @@ -0,0 +1,10 @@ +import segmentationCustomization from './customizations/segmentationCustomization'; + +export default function getCustomizationModule() { + return [ + { + name: 'default', + value: segmentationCustomization, + }, + ]; +} diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts index 1bb2b3a8f..88cd7a166 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts @@ -1,16 +1,237 @@ -import { utils, Types as OhifTypes } from '@ohif/core'; +import { utils, Types as OhifTypes, DicomMetadataStore, classes, log } from '@ohif/core'; import i18n from '@ohif/i18n'; -import { metaData, eventTarget } from '@cornerstonejs/core'; +import { metaData, eventTarget, utilities as csUtils } from '@cornerstonejs/core'; import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; import { SOPClassHandlerId } from './id'; import { dicomlabToRGB } from './utils/dicomlabToRGB'; +import { getSegmentationParserType } from './utils/segmentationConfig'; +import { + getFrameIndexFromImageId, + isLocalSchemeImageId, + stripFrameFromImageId, +} from './utils/segLocalImageIds'; const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4', '1.2.840.10008.5.1.4.1.1.66.7']; +const LABELMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.7'; const loadPromises = {}; +const SEG_LOAD_LOG_PREFIX = '[SEG load]'; + +// Max number of SEG frames fetched/decoded concurrently by the segmentation +// loader. Hard-coded to 16 for now; intended to become configurable (and to +// pair with the full-instance prefetch capability) in a follow-up. +const SEG_FRAME_DECODE_CONCURRENCY = 16; + + +function _normalizeImageId(imageId: string | string[] | undefined): string | undefined { + if (imageId == null) { + return undefined; + } + return Array.isArray(imageId) ? imageId[0] : imageId; +} + +/** + * Expands a WADO-RS frame imageId (…/frames/1) into one imageId per frame. + * Multiframe SEG is stored as separate /frames/N resources on the server. + */ +function getFrameImageIds(segImageId: string, numberOfFrames: number): string[] { + const frameMatch = segImageId.match(/(.*\/frames\/)(\d+)(.*)$/); + if (!frameMatch || numberOfFrames <= 1) { + return [segImageId]; + } + + const prefix = frameMatch[1]; + const suffix = frameMatch[3] || ''; + const frameImageIds: string[] = []; + + for (let frameNumber = 1; frameNumber <= numberOfFrames; frameNumber++) { + frameImageIds.push(`${prefix}${frameNumber}${suffix}`); + } + + return frameImageIds; +} + +function _getSegNumberOfFrames(instance: Record): number { + const fromTag = Number(instance.NumberOfFrames); + if (fromTag > 0) { + return fromTag; + } + + const perFrame = instance.PerFrameFunctionalGroupsSequence; + if (Array.isArray(perFrame) && perFrame.length > 0) { + return perFrame.length; + } + + return 1; +} + +function _ensureSegImageIdMetadataRegistered( + imageId: string | undefined, + instance: Record +): void { + if (!imageId) { + return; + } + + const metadataProvider = classes.MetadataProvider; + + const StudyInstanceUID = instance.StudyInstanceUID as string | undefined; + const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined; + const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as + | string + | undefined; + + if (!StudyInstanceUID || !SeriesInstanceUID || !SOPInstanceUID) { + return; + } + + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + frameNumber: getFrameIndexFromImageId(imageId), + }); +} + +/** Ensures metadataProvider.get('instance', imageId) resolves for frame-qualified local SEG ids. */ +function _ensureSegInstanceMetadataAvailable( + imageId: string | undefined, + instance: Record +): void { + if (!imageId) { + return; + } + + _ensureSegImageIdMetadataRegistered(imageId, instance); + + if (metaData.get('instance', imageId)) { + return; + } + + const StudyInstanceUID = instance.StudyInstanceUID as string | undefined; + const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined; + const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as + | string + | undefined; + + const storedInstance = + StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID + ? DicomMetadataStore.getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) + : undefined; + + classes.MetadataProvider.addCustomMetadata( + imageId, + 'instance', + storedInstance || instance + ); +} + +function _getSegDataSource(extensionManager, instance: Record) { + const StudyInstanceUID = instance.StudyInstanceUID as string | undefined; + const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined; + const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as + | string + | undefined; + + let localUrl: string | undefined; + + if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) { + const storedInstance = DicomMetadataStore.getInstance( + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID + ); + localUrl = storedInstance?.url as string | undefined; + } + + localUrl = localUrl || (instance.url as string | undefined); + + if (localUrl && isLocalSchemeImageId(localUrl)) { + const dicomLocal = extensionManager.getDataSources('dicomlocal'); + + if (dicomLocal?.[0]) { + return dicomLocal[0]; + } + } + + return extensionManager.getActiveDataSource()[0]; +} + +function _getSegImageIdFromInstance( + instance: Record, + dataSource: { getImageIdsForInstance?: (args: { instance: unknown; frame?: number }) => unknown } +): string | undefined { + const numberOfFrames = _getSegNumberOfFrames(instance); + const frame = numberOfFrames > 1 ? 1 : undefined; + + return _normalizeImageId( + dataSource.getImageIdsForInstance?.({ instance, frame }) as string | string[] | undefined + ); +} + +function _resolveFrameImageIds( + segImageIdStr: string, + instance: Record, + dataSource: { getImageIdsForInstance?: (args: { instance: unknown; frame?: number }) => unknown } +): string[] { + const numberOfFrames = _getSegNumberOfFrames(instance); + const fromFrameUrl = getFrameImageIds(segImageIdStr, numberOfFrames); + + if (fromFrameUrl.length > 1) { + return fromFrameUrl; + } + + if (numberOfFrames <= 1) { + return [segImageIdStr]; + } + + const frameImageIds: string[] = []; + + for (let frame = 1; frame <= numberOfFrames; frame++) { + const frameImageId = _normalizeImageId( + dataSource.getImageIdsForInstance?.({ instance, frame }) as string | string[] | undefined + ); + + if (frameImageId) { + frameImageIds.push(frameImageId); + } + } + + return frameImageIds.length ? frameImageIds : [segImageIdStr]; +} + +function _logSegImageIds({ + segDisplaySet, + segImageIdStr, + frameImageIds, + referencedImageIds, +}: { + segDisplaySet: AppTypes.DisplaySet; + segImageIdStr: string; + frameImageIds: string[]; + referencedImageIds: string[]; +}) { + const instance = segDisplaySet.instance as Record; + const numberOfFrames = Number(instance?.NumberOfFrames) || 1; + + log.debug(SEG_LOAD_LOG_PREFIX, 'Loading SEG pixel data', { + SOPInstanceUID: segDisplaySet.SOPInstanceUID, + SeriesInstanceUID: segDisplaySet.SeriesInstanceUID, + SOPClassUID: segDisplaySet.SOPClassUID, + NumberOfFrames: numberOfFrames, + segmentCount: Object.keys(segDisplaySet.segments || {}).length, + referencedDisplaySetInstanceUID: segDisplaySet.referencedDisplaySetInstanceUID, + referencedImageIdCount: referencedImageIds.length, + referencedImageIds, + segImageIdForMetadata: segImageIdStr, + frameImageIds, + loadSegFramesIndividually: frameImageIds.length > 1, + }); +} + function _getDisplaySetsFromSeries( instances, servicesManager: AppTypes.ServicesManager, @@ -171,6 +392,11 @@ function _load( }); }); + // Expose the in-flight load promise so observers (e.g. the viewport service + // waiting to attach the representation) can react to a load failure without + // re-invoking load(). + segDisplaySet.loadingPromise = loadPromises[SOPInstanceUID]; + return loadPromises[SOPInstanceUID]; } @@ -178,16 +404,18 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, - headers, -}: withAppTypes) { - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.common' - ); +}: withAppTypes<{ segDisplaySet: AppTypes.DisplaySet }>) { + const { segmentationService, uiNotificationService, customizationService } = + servicesManager.services; + const instance = segDisplaySet.instance as Record; + const dataSource = _getSegDataSource(extensionManager, instance); + const segImageIdStr = _getSegImageIdFromInstance(instance, dataSource); - const { segmentationService, uiNotificationService } = servicesManager.services; - - const { dicomLoaderService } = utilityModule.exports; - const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); + if (!segImageIdStr) { + throw new Error( + 'Could not get imageId for SEG instance (no local wadouri url and getImageIdsForInstance returned nothing).' + ); + } const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID( segDisplaySet.referencedDisplaySetInstanceUID @@ -197,31 +425,114 @@ async function _loadSegments({ throw new Error('referencedDisplaySet is missing for SEG'); } + // Prefer cached stack imageIds (multiframe SEG fix #4890), then data source expansion. let { imageIds } = referencedDisplaySet; - if (!imageIds) { - // try images - const { images } = referencedDisplaySet; - imageIds = images.map(image => image.imageId); + if (!imageIds?.length) { + imageIds = dataSource.getImageIdsForDisplaySet?.(referencedDisplaySet); } - // Todo: what should be defaults here + if (!imageIds?.length) { + imageIds = (referencedDisplaySet as { images?: { imageId: string }[] }).images?.map( + (img: { imageId: string }) => img.imageId + ); + } + + if (!imageIds?.length) { + throw new Error('referencedDisplaySet has no imageIds'); + } + + (segDisplaySet as AppTypes.DisplaySet & { referencedImageIds?: string[] }).referencedImageIds = + imageIds; + + if (!referencedDisplaySet.imageIds?.length) { + referencedDisplaySet.imageIds = imageIds; + } + + const frameImageIds = _resolveFrameImageIds( + segImageIdStr, + segDisplaySet.instance as Record, + dataSource + ); + + const segImageIdForMetadata = isLocalSchemeImageId(segImageIdStr) + ? stripFrameFromImageId(segImageIdStr) + : segImageIdStr; + + _logSegImageIds({ + segDisplaySet, + segImageIdStr: segImageIdForMetadata, + frameImageIds, + referencedImageIds: imageIds, + }); + + _ensureSegInstanceMetadataAvailable(segImageIdForMetadata, instance); + frameImageIds.forEach(id => _ensureSegInstanceMetadataAvailable(id, instance)); + const tolerance = 0.001; - eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => { + const onProgress = evt => { const { percentComplete } = evt.detail; segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, { percentComplete, }); - }); + }; + eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress); - const results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer( - imageIds, - arrayBuffer, - { metadataProvider: metaData, tolerance } - ); + // Fetch the whole SEG instance as a single Part 10 object and register its + // per-frame compressed pixels into the Cornerstone3D frame registry, so the + // per-frame loads below are served locally instead of one network request + // per frame: SEG frames are so small and numerous that one bulk fetch beats + // hundreds of tiny requests. Enabled by default; per-frame loading is the + // exception (loadMultiframeAsPart10: false in the data source config, or the + // cornerstone.segmentation.loadMultiframeAsPart10 customization). The + // prefetch is awaited until it completes OR fails — deliberately no timeout: + // a failed/unsupported instance fetch resolves quickly and falls back to + // per-frame, while a slow large fetch is still the fastest way to all frames. + const loadMultiframeAsPart10 = + (dataSource?.getConfig?.()?.loadMultiframeAsPart10 as boolean | undefined) ?? + (customizationService?.getCustomization?.( + 'cornerstone.segmentation.loadMultiframeAsPart10' + ) as boolean | undefined) ?? + true; + + let prefetch; + if (loadMultiframeAsPart10) { + prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({ + instance, + imageId: segImageIdForMetadata, + }); + + if (prefetch?.done) { + await prefetch.done; + } + } + + let results; + try { + results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDicomSegImageId( + imageIds, + segImageIdForMetadata, + { + metadataProvider: metaData, + tolerance, + parserType: getSegmentationParserType( + segDisplaySet.SOPClassUID, + customizationService + ), + frameImageIds, + concurrency: SEG_FRAME_DECODE_CONCURRENCY, + } + ); + } finally { + eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress); + prefetch?.cancel?.(); + } let usedRecommendedDisplayCIELabValue = true; - results.segMetadata.data.forEach((data, i) => { + const resultsTyped = results as { + segMetadata: { data: { rgba?: number[]; RecommendedDisplayCIELabValue?: number[] }[] }; + }; + resultsTyped.segMetadata.data.forEach((data, i) => { if (i > 0) { data.rgba = data.RecommendedDisplayCIELabValue; @@ -246,6 +557,17 @@ async function _loadSegments({ } Object.assign(segDisplaySet, results); + + const labelMapImageIds = (results as { labelMapImages?: { imageId: string }[][] }) + .labelMapImages?.flat() + .map(image => image.imageId); + + log.debug(SEG_LOAD_LOG_PREFIX, 'SEG parse complete', { + SOPInstanceUID: segDisplaySet.SOPInstanceUID, + labelMapImageCount: labelMapImageIds?.length ?? 0, + labelMapImageIds, + segmentIndices: Object.keys(segDisplaySet.segments || {}), + }); } function _segmentationExists(segDisplaySet) { diff --git a/extensions/cornerstone-dicom-seg/src/index.tsx b/extensions/cornerstone-dicom-seg/src/index.tsx index e1315b631..b9fb15a70 100644 --- a/extensions/cornerstone-dicom-seg/src/index.tsx +++ b/extensions/cornerstone-dicom-seg/src/index.tsx @@ -4,6 +4,7 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; import getHangingProtocolModule from './getHangingProtocolModule'; import getCommandsModule from './commandsModule'; +import getCustomizationModule from './getCustomizationModule'; import { getToolbarModule } from './getToolbarModule'; const Component = React.lazy(() => { @@ -28,6 +29,7 @@ const extension = { */ id, getCommandsModule, + getCustomizationModule, getToolbarModule, getViewportModule({ servicesManager, extensionManager, commandsManager }) { const ExtendedOHIFCornerstoneSEGViewport = props => { diff --git a/extensions/cornerstone-dicom-seg/src/utils/segLocalImageIds.ts b/extensions/cornerstone-dicom-seg/src/utils/segLocalImageIds.ts new file mode 100644 index 000000000..6a4b63883 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/segLocalImageIds.ts @@ -0,0 +1,32 @@ +export function isLocalSchemeImageId(imageId: string): boolean { + return /^(wadouri:|dicomfile:|dicomweb:)/.test(imageId); +} + +export function stripFrameFromImageId(imageId: string): string { + const queryIndex = imageId.indexOf('?'); + if (queryIndex === -1) { + return imageId; + } + + const basePath = imageId.slice(0, queryIndex); + const rebuiltQuery = imageId + .slice(queryIndex + 1) + .split('&') + .filter(param => param.split('=')[0] !== 'frame') + .join('&'); + + return rebuiltQuery ? `${basePath}?${rebuiltQuery}` : basePath; +} + +export function appendFrameToImageId(baseImageId: string, frame: number): string { + const withoutFrame = stripFrameFromImageId(baseImageId); + const separator = withoutFrame.includes('?') ? '&' : '?'; + + return `${withoutFrame}${separator}frame=${frame}`; +} + +export function getFrameIndexFromImageId(imageId: string): number { + const frameMatch = imageId.match(/(?:&|\?)frame=(\d+)/); + + return frameMatch ? Number(frameMatch[1]) : 1; +} diff --git a/extensions/cornerstone-dicom-seg/src/utils/segmentationConfig.ts b/extensions/cornerstone-dicom-seg/src/utils/segmentationConfig.ts new file mode 100644 index 000000000..b2ff1f166 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/segmentationConfig.ts @@ -0,0 +1,100 @@ +export const LABELMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.7'; +export const BITMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.4'; +/** RLE Lossless — OHIF default SEG store transfer syntax. */ +export const DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.5'; +/** OHIF default SEG store mode (Label Map Segmentation SOP Class). */ +export const DEFAULT_SEG_STORE_MODE = 'labelmap' as const; + +export type SegmentationMode = 'labelmap' | 'bitmap'; + +/** + * Per-data-source overrides for the SEG store defaults. A data source may set + * these under `configuration.segmentation.store` to override the values coming + * from the `segmentation.store.*` customizations. Different back ends support + * different SEG encodings, so the data source is allowed to win over the + * app-wide customization default. + */ +export type SegmentationStoreOverride = { + defaultMode?: SegmentationMode; + transferSyntaxUID?: string; +}; + +type SegmentationCustomizationReader = { + getCustomization: (customizationId: string) => unknown; +}; + +function getStoreDefaultMode( + customizationService?: SegmentationCustomizationReader, + override?: SegmentationStoreOverride +): SegmentationMode { + const mode = + override?.defaultMode ?? + (customizationService?.getCustomization('segmentation.store.defaultMode') as + | SegmentationMode + | undefined) ?? + DEFAULT_SEG_STORE_MODE; + + return mode === 'bitmap' ? 'bitmap' : 'labelmap'; +} + +function getStoreTransferSyntaxUID( + customizationService?: SegmentationCustomizationReader, + override?: SegmentationStoreOverride +): string { + return ( + override?.transferSyntaxUID ?? + (customizationService?.getCustomization( + 'segmentation.store.transferSyntaxUID' + ) as string | undefined) ?? + DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID + ); +} + +/** + * Resolves the parser type for loading a DICOM SEG instance. + * Uses the instance SOP Class UID when it is a known SEG class; otherwise falls back to store defaultMode. + */ +export function getSegmentationParserType( + sopClassUID: string | undefined, + customizationService?: SegmentationCustomizationReader +): SegmentationMode { + if (sopClassUID === LABELMAP_SEG_SOP_CLASS_UID) { + return 'labelmap'; + } + + if (sopClassUID === BITMAP_SEG_SOP_CLASS_UID) { + return 'bitmap'; + } + + return getStoreDefaultMode(customizationService); +} + +/** + * Options passed to @cornerstonejs/adapters generateSegmentation when exporting or storing SEG. + * + * Defaults to **Label Map + RLE Lossless**. Customizations (or per-data-source + * `configuration.segmentation.store`) are only needed to opt into bitmap and/or + * uncompressed Explicit VR Little Endian. + */ +export function getSegmentationSaveOptions( + customizationService?: SegmentationCustomizationReader, + override?: SegmentationStoreOverride +): { + sopClassUID: string; + transferSyntaxUID: string; + transferSyntaxUid: string; +} { + const defaultMode = getStoreDefaultMode(customizationService, override); + const sopClassUID = + defaultMode === 'bitmap' ? BITMAP_SEG_SOP_CLASS_UID : LABELMAP_SEG_SOP_CLASS_UID; + const transferSyntaxUID = getStoreTransferSyntaxUID( + customizationService, + override + ); + + return { + sopClassUID, + transferSyntaxUID, + transferSyntaxUid: transferSyntaxUID, + }; +} diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 4cc6edcc3..c565d5292 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -95,10 +95,13 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { }; const getCornerstoneViewport = useCallback(() => { + // Stack uses the referenced series (data[0]); SEG is applied as an overlay (data[1]). + // Passing only the SEG display set leaves the stack with derived labelmap imageIds, + // which are not displayable without the underlying grayscale series. return ( ); - }, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]); + }, [ + viewportId, + segDisplaySet, + referencedDisplaySet, + toolGroupId, + props, + viewportOptions, + ]); useEffect(() => { if (segIsLoading) { diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index fb1fa385c..726322310 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -32,7 +32,7 @@ "@ohif/core": "workspace:*", "@ohif/extension-cornerstone": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -40,9 +40,9 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.1.3", - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/tools": "5.1.3", + "@cornerstonejs/adapters": "5.4.12", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/tools": "5.4.12", "classnames": "2.5.1" }, "devDependencies": { diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json index bb788e22e..e1a96e17c 100644 --- a/extensions/cornerstone-dynamic-volume/package.json +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -37,7 +37,7 @@ "@ohif/extension-default": "workspace:*", "@ohif/i18n": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -45,8 +45,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/tools": "5.1.3", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/tools": "5.4.12", "classnames": "2.5.1" }, "devDependencies": { diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 9557c1745..f9cc17330 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -38,11 +38,11 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/dicom-image-loader": "5.1.3", + "@cornerstonejs/dicom-image-loader": "5.4.12", "@ohif/core": "workspace:*", "@ohif/extension-default": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", @@ -52,13 +52,13 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/adapters": "5.1.3", - "@cornerstonejs/ai": "5.1.3", - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/labelmap-interpolation": "5.1.3", - "@cornerstonejs/metadata": "5.1.3", - "@cornerstonejs/polymorphic-segmentation": "5.1.3", - "@cornerstonejs/tools": "5.1.3", + "@cornerstonejs/adapters": "5.4.12", + "@cornerstonejs/ai": "5.4.12", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/labelmap-interpolation": "5.4.12", + "@cornerstonejs/metadata": "5.4.12", + "@cornerstonejs/polymorphic-segmentation": "5.4.12", + "@cornerstonejs/tools": "5.4.12", "@icr/polyseg-wasm": "0.4.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0", "@kitware/vtk.js": "35.5.3", diff --git a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx index f2f0ee1ff..e9a91a920 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx @@ -350,7 +350,10 @@ function _getInstanceNumberFromVolume( const imageId = imageIds[imageIndex]; if (!imageId) { - return {}; + // No image at this index (e.g. a single-image volume scrolled out of + // range). Return undefined so the overlay falls back to the slice count + // instead of rendering an empty object as "[object Object]". + return; } const { instanceNumber } = metaData.get('generalImageModule', imageId) || {}; diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index a2e059d57..ad77a0aba 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -1982,8 +1982,15 @@ function commandsModule({ * Use it before initializing the toolGroup with the tools. */ initializeSegmentLabelTool: ({ tools }) => { - const appConfig = extensionManager.appConfig; - const segmentLabelConfig = appConfig.segmentation?.segmentLabel; + const { customizationService } = servicesManager.services; + const segmentLabelConfig = customizationService.getCustomization( + 'segmentation.segmentLabel' + ) as { + enabledByDefault?: boolean; + labelColor?: number[]; + hoverTimeout?: number; + background?: string; + }; if (segmentLabelConfig?.enabledByDefault) { const activeTools = tools?.active ?? []; diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index f2e2305bf..730ee4aad 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -7,6 +7,7 @@ import { imageRetrievalPoolManager, } from '@cornerstonejs/core'; import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; +import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata'; import { Types } from '@ohif/core'; import Enums from './enums'; @@ -169,6 +170,11 @@ const cornerstoneExtension: Types.Extensions.Extension = { imageRetrievalPoolManager.clearRequestStack(type); }); + // Release the typed metadata registry (NATURALIZED instances registered via + // prefetchPart10Instance hold full compressed Part 10 buffers that live + // outside the size-capped cornerstone image cache) + csMetadataUtilities.clearCacheData(); + cineService.setIsCineEnabled(false); enabledElementReset(); diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts index 95581eaad..8ad9bd959 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.test.ts @@ -1799,6 +1799,10 @@ describe('SegmentationService', () => { const segmentationId = 'segmentationId'; const segmentationData = { segmentationId, + representation: { + type: csToolsEnums.SegmentationRepresentations.Labelmap, + data: {}, + }, config: { label: 'Segmentation 1', }, diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index a5cc6b861..7c75978a4 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -302,7 +302,15 @@ class SegmentationService extends PubSubService { } ): Promise { const segmentation = this.getSegmentation(segmentationId); - if (segmentation && !segmentation.predecessorImageId && predecessorImageId) { + + if (!segmentation) { + console.warn( + `addSegmentationRepresentation: segmentation "${segmentationId}" is not in state yet` + ); + return; + } + + if (!segmentation.predecessorImageId && predecessorImageId) { segmentation.predecessorImageId = predecessorImageId; } const csViewport = this.getAndValidateViewport(viewportId); @@ -488,10 +496,22 @@ class SegmentationService extends PubSubService { throw new Error('No instances were provided for the referenced display set of the SEG'); } - const imageIds = images.map(image => image.imageId); + // Use the same imageIds as SEG parse (_loadSegments stores these on segDisplaySet). + const imageIds = + segDisplaySet.referencedImageIds || + (referencedDisplaySet.imageIds as string[] | undefined) || + images.map(image => image.imageId); + + if (!imageIds?.length) { + throw new Error('referencedDisplaySet has no imageIds for SEG'); + } const derivedImages = labelMapImages?.flat(); const derivedImageIds = derivedImages.map(image => image.imageId); + // Note: instance runtime props (frameNumber, imageId, url, ...) are + // intentionally non-enumerable, so this spread deliberately does NOT copy + // them — frameNumber must not be carried onto these derived image entries. + // Read such props off the original instance, never off a copy. segDisplaySet.images = derivedImages.map(image => ({ ...image, ...metaData.get('instance', image.referencedImageId), @@ -572,11 +592,6 @@ class SegmentationService extends PubSubService { const colorLUTIndex = addColorLUT(colorLUT); this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); - this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { - segmentationId, - segDisplaySet, - }); - const seg: cstTypes.SegmentationPublicInput = { segmentationId, representation: { @@ -596,8 +611,18 @@ class SegmentationService extends PubSubService { segDisplaySet.isLoaded = true; + // Add the segmentation to cornerstone state BEFORE broadcasting that loading is + // complete. Subscribers (e.g. CornerstoneViewportService) react synchronously and + // call addSegmentationRepresentation, which now early-returns when the segmentation + // is not yet in cornerstone state. Broadcasting first would make that guard always + // fire on initial load, silently preventing the representation from being attached. this.addOrUpdateSegmentation(seg); + this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { + segmentationId, + segDisplaySet, + }); + return segmentationId; } @@ -772,9 +797,16 @@ class SegmentationService extends PubSubService { if (existingSegmentation) { // Update the existing segmentation this.updateSegmentationInSource(segmentationId, data as Partial); - } else { + } else if ( + 'representation' in data && + (data as cstTypes.SegmentationPublicInput).representation + ) { // Add a new segmentation this.addSegmentationToSource(data as cstTypes.SegmentationPublicInput); + } else { + console.warn( + `addOrUpdateSegmentation: skipping add for ${segmentationId} — missing representation` + ); } } @@ -1596,7 +1628,13 @@ class SegmentationService extends PubSubService { private determineViewportAndSegmentationType(csViewport, segmentation) { const isVolumeViewport = isVolumeViewportType(csViewport); - const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP]; + const labelmapData = segmentation?.representationData?.[LABELMAP]; + + if (!labelmapData) { + return { isVolumeViewport, isVolumeSegmentation: false }; + } + + const isVolumeSegmentation = 'volumeId' in labelmapData; return { isVolumeViewport, isVolumeSegmentation }; } diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 79b982087..7d9734d05 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -1294,24 +1294,122 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi ? csToolsEnums.SegmentationRepresentations.Labelmap : csToolsEnums.SegmentationRepresentations.Contour; - const { predecessorImageId } = displaySet; - const segmentationRepresentationPromise = segmentationService.addSegmentationRepresentation( - viewport.id, - { - segmentationId, - predecessorImageId, - type: representationType, - config: { - blendMode: - viewport?.getBlendMode?.() === 1 - ? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND - : undefined, - }, + const applyRepresentation = () => { + const { predecessorImageId } = displaySet; + const segmentationRepresentationPromise = + segmentationService.addSegmentationRepresentation(viewport.id, { + segmentationId, + predecessorImageId, + type: representationType, + config: { + blendMode: + viewport?.getBlendMode?.() === 1 + ? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND + : undefined, + }, + }); + this.storePresentation({ viewportId: viewport.id }); + return segmentationRepresentationPromise; + }; + + // SEG overlay is registered during stack setup, but cornerstone segmentation state + // is created in displaySet.load() (async). Wait until it exists before adding representation. + if (displaySet.Modality === 'SEG') { + if (segmentationService.getSegmentation(segmentationId)) { + return applyRepresentation(); } - ); - // store the segmentation presentation id in the viewport info - this.storePresentation({ viewportId: viewport.id }); - return segmentationRepresentationPromise; + + // Bound the wait so a failed/aborted SEG load (where SEGMENTATION_LOADING_COMPLETE + // never fires) cannot leave this promise — and the viewport setup awaiting it — + // hanging indefinitely. + const SEG_LOADING_TIMEOUT_MS = 120000; + + return new Promise(resolve => { + let settled = false; + let timeoutId; + + // Final resolution — extra calls are harmless (resolve is a no-op after + // the first), which lets the timeout stay armed while a representation + // apply is in flight and still bound it. + const finish = () => { + clearTimeout(timeoutId); + resolve(); + }; + + // Give up without applying (load failure / timeout before the event). + const settleWithoutApply = () => { + settled = true; + unsubscribe(); + finish(); + }; + + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + async (evt: { segDisplaySet?: OhifTypes.DisplaySet }) => { + if (settled || evt.segDisplaySet?.displaySetInstanceUID !== segmentationId) { + return; + } + + // Dedupe immediately, but resolve only after the representation is + // applied: awaiters continue into rotation/flip/render and broadcast + // VIEWPORT_DATA_CHANGED (hanging-protocol callbacks, toolbar, e2e + // readiness), which must not observe a viewport whose overlay + // doesn't exist yet. applyRepresentation can be genuinely async + // (stack→volume viewport conversion). The timeout is deliberately + // NOT cleared here — a hung apply stays bounded. + settled = true; + unsubscribe(); + + try { + await applyRepresentation(); + } catch (error) { + console.warn( + `Failed to apply segmentation representation for "${segmentationId}":`, + error + ); + } finally { + finish(); + } + } + ); + + // Stop waiting immediately if the SEG load itself fails — otherwise the + // loading-complete event never fires and we would idle until the timeout. + const loadingPromise = (displaySet as { loadingPromise?: Promise }) + .loadingPromise; + loadingPromise?.catch(error => { + if (settled) { + return; + } + + console.warn( + `Segmentation "${segmentationId}" failed to load; skipping representation setup.`, + error + ); + settleWithoutApply(); + }); + + timeoutId = setTimeout(() => { + if (settled) { + // The load completed but applyRepresentation is hung — resolve so + // viewport setup is never blocked past the bound (best-effort + // semantics; the apply may still land later). + console.warn( + `Timed out applying segmentation representation for "${segmentationId}"; resolving viewport readiness anyway.` + ); + resolve(); + return; + } + + console.warn( + `Timed out waiting for segmentation "${segmentationId}" to load; skipping representation setup.` + ); + settleWithoutApply(); + }, SEG_LOADING_TIMEOUT_MS); + }); + } + + return applyRepresentation(); } private async _addOverlayRepresentations( diff --git a/extensions/cornerstone/src/utils/segmentationHandlers.ts b/extensions/cornerstone/src/utils/segmentationHandlers.ts index 3b7cd4970..15e02f202 100644 --- a/extensions/cornerstone/src/utils/segmentationHandlers.ts +++ b/extensions/cornerstone/src/utils/segmentationHandlers.ts @@ -53,10 +53,14 @@ export function setupSegmentationDataModifiedHandler({ }); if (!isUnsubscribed && updatedSegmentation) { - segmentationService.addOrUpdateSegmentation({ - segmentationId, - segments: updatedSegmentation.segments, - }); + const existingSegmentation = segmentationService.getSegmentation(segmentationId); + + if (existingSegmentation) { + segmentationService.addOrUpdateSegmentation({ + segmentationId, + segments: updatedSegmentation.segments, + }); + } } }, 1000 diff --git a/extensions/default/package.json b/extensions/default/package.json index 3c89c873d..a41e98c8c 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -31,7 +31,7 @@ "peerDependencies": { "@ohif/core": "workspace:*", "@ohif/i18n": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicomweb-client": "0.10.4", "prop-types": "15.8.1", "react": "18.3.1", @@ -48,6 +48,7 @@ "react-color": "2.19.3" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" }, "keywords": [ diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index 462fb8751..3ada37767 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -1,6 +1,11 @@ import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; import OHIF from '@ohif/core'; -import dcmjs from 'dcmjs'; +import { + datasetToDicomBlob, + makeExistingPropertiesNonEnumerable, + setNonEnumerableInstanceProperty, +} from '../utils/dicomWriter'; +import { appendFrameQueryToImageId } from '../utils/appendFrameQueryToImageId'; const metadataProvider = OHIF.classes.MetadataProvider; const { EVENTS } = DicomMetadataStore; @@ -152,14 +157,15 @@ function createDicomLocalApi(dicomLocalConfig) { SOPInstanceUID, } = instance; - instance.imageId = imageId; + setNonEnumerableInstanceProperty(instance, 'imageId', imageId); + makeExistingPropertiesNonEnumerable(instance); // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. metadataProvider.addImageIdToUIDs(imageId, { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, - frameIndex: isMultiframe ? index : 1, + frameNumber: isMultiframe ? index + 1 : 1, }); }); @@ -174,7 +180,7 @@ function createDicomLocalApi(dicomLocalConfig) { }, store: { dicom: naturalizedReport => { - const reportBlob = dcmjs.data.datasetToBlob(naturalizedReport); + const reportBlob = datasetToDicomBlob(naturalizedReport); //Create a URL for the binary. var objectUrl = URL.createObjectURL(reportBlob); @@ -211,9 +217,6 @@ function createDicomLocalApi(dicomLocalConfig) { getImageIdsForInstance({ instance, frame }) { // Important: Never use instance.imageId because it might be multiframe, // which would make it an invalid imageId. - // if (instance.imageId) { - // return instance.imageId; - // } const { StudyInstanceUID, SeriesInstanceUID } = instance; const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID; @@ -223,13 +226,20 @@ function createDicomLocalApi(dicomLocalConfig) { SOPInstanceUID ); - let imageId = storedInstance.url; + const baseImageId = storedInstance?.url || instance.url; - if (frame !== undefined) { - imageId += `&frame=${frame}`; + if (!baseImageId) { + return; } - return imageId; + const numberOfFrames = Number(storedInstance?.NumberOfFrames || instance.NumberOfFrames) || 1; + + if (numberOfFrames > 1) { + const frameNumber = frame !== undefined ? frame : 1; + return appendFrameQueryToImageId(baseImageId, frameNumber); + } + + return baseImageId; }, deleteStudyMetadataPromise() { console.log('deleteStudyMetadataPromise not implemented'); diff --git a/extensions/default/src/DicomWebDataSource/index.ts b/extensions/default/src/DicomWebDataSource/index.ts index 9a4d31d95..249bdb2e7 100644 --- a/extensions/default/src/DicomWebDataSource/index.ts +++ b/extensions/default/src/DicomWebDataSource/index.ts @@ -12,11 +12,17 @@ import dcm4cheeReject from './dcm4cheeReject.js'; import getImageId from './utils/getImageId.js'; import dcmjs from 'dcmjs'; +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStudyMetadata.js'; import StaticWadoClient from './utils/StaticWadoClient'; import getDirectURL from '../utils/getDirectURL'; import { fixBulkDataURI } from './utils/fixBulkDataURI'; import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders'; +import { + getDatasetTransferSyntaxUID, + setNonEnumerableInstanceProperty, + writeDicomDictToPart10Buffer, +} from '../utils/dicomWriter'; import { getGetThumbnailSrc, ThumbnailContext } from './retrieveThumbnail'; import { getRenderedURL } from './retrieveRendered'; @@ -26,7 +32,6 @@ const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0'; const ImplementationVersionName = 'OHIF-3.11.0'; -const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; const metadataProvider = classes.MetadataProvider; @@ -308,6 +313,86 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { */ getWadoDicomWebClient: () => wadoDicomWebClient, + /** + * Best-effort prefetch of a whole multiframe instance as a single Part 10 + * object, registered into the Cornerstone3D NATURALIZED frame registry so + * subsequent per-frame image loads are served locally instead of issuing + * one network request per frame (see the "Behaviours" doc + * segmentation-multiframe-part10-prefetch). + * + * Whether to use it at all is the caller's policy (the SEG handler + * resolves the `loadMultiframeAsPart10` config/customization, defaulting + * it on — per-frame loading is the explicit opt-out there). + * Never throws into the caller — on any failure it resolves `done` to + * `false` and the normal per-frame load path is used. + * + * @returns `{ done: Promise, cancel: () => void }`. + */ + prefetchInstanceFrames: ({ instance, imageId }) => { + const noop = { done: Promise.resolve(false), cancel: () => {} }; + + if (!instance || !imageId) { + return noop; + } + + const StudyInstanceUID = instance.StudyInstanceUID; + const SeriesInstanceUID = instance.SeriesInstanceUID; + const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID; + + if (!StudyInstanceUID || !SeriesInstanceUID || !SOPInstanceUID) { + return noop; + } + + let cancelled = false; + + // Lazy resolver: dicomweb-client.retrieveInstance returns the Part 10 + // instance as an ArrayBuffer, unwrapping multipart/related transparently + // (and returning the raw object for single-part responses). + const resolvePart10 = async () => { + wadoDicomWebClient.headers = getAuthorizationHeader(); + const result = await wadoDicomWebClient.retrieveInstance({ + studyInstanceUID: StudyInstanceUID, + seriesInstanceUID: SeriesInstanceUID, + sopInstanceUID: SOPInstanceUID, + }); + + if (cancelled) { + throw new Error('prefetchInstanceFrames cancelled'); + } + + if (result instanceof ArrayBuffer) { + return result; + } + if (Array.isArray(result) && result[0] instanceof ArrayBuffer) { + return result[0]; + } + if (result && (result as { buffer?: ArrayBuffer }).buffer instanceof ArrayBuffer) { + return (result as ArrayBufferView).buffer as ArrayBuffer; + } + throw new Error('Unexpected retrieveInstance result for instance prefetch'); + }; + + const done = (async () => { + try { + await dicomImageLoader.prefetchPart10Instance(imageId, resolvePart10); + return !cancelled; + } catch (error) { + console.warn( + '[prefetchInstanceFrames] full-instance prefetch failed; falling back to per-frame loads', + error + ); + return false; + } + })(); + + return { + done, + cancel: () => { + cancelled = true; + }, + }; + }, + bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => { qidoDicomWebClient.headers = getAuthorizationHeader(); const options = { @@ -372,7 +457,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, MediaStorageSOPClassUID: dataset.SOPClassUID, MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, - TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + TransferSyntaxUID: getDatasetTransferSyntaxUID(dataset), ImplementationClassUID, ImplementationVersionName, }; @@ -384,7 +469,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { effectiveDicomDict = defaultDicomDict; } - const part10Buffer = effectiveDicomDict.write(); + const part10Buffer = writeDicomDictToPart10Buffer(effectiveDicomDict); const options = { datasets: [part10Buffer], @@ -445,9 +530,9 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { instance, }); - instance.imageId = imageId; - instance.wadoRoot = dicomWebConfig.wadoRoot; - instance.wadoUri = dicomWebConfig.wadoUri; + setNonEnumerableInstanceProperty(instance, 'imageId', imageId); + setNonEnumerableInstanceProperty(instance, 'wadoRoot', dicomWebConfig.wadoRoot); + setNonEnumerableInstanceProperty(instance, 'wadoUri', dicomWebConfig.wadoUri); metadataProvider.addImageIdToUIDs(imageId, { StudyInstanceUID, @@ -544,8 +629,8 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { // Adding instanceMetadata to OHIF MetadataProvider naturalizedInstances.forEach(instance => { - instance.wadoRoot = dicomWebConfig.wadoRoot; - instance.wadoUri = dicomWebConfig.wadoUri; + setNonEnumerableInstanceProperty(instance, 'wadoRoot', dicomWebConfig.wadoRoot); + setNonEnumerableInstanceProperty(instance, 'wadoUri', dicomWebConfig.wadoUri); const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; const numberOfFrames = instance.NumberOfFrames || 1; @@ -571,7 +656,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { const imageId = implementation.getImageIdsForInstance({ instance, }); - instance.imageId = imageId; + setNonEnumerableInstanceProperty(instance, 'imageId', imageId); }); DicomMetadataStore.addInstances(naturalizedInstances, madeInClient); diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 2442ce66a..40ef854d9 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -1,5 +1,6 @@ import { Types, DicomMetadataStore, utils } from '@ohif/core'; -import dcmjs from 'dcmjs'; +import { datasetToDicomBlob, setNonEnumerableInstanceProperty } from './utils/dicomWriter'; +import { registerNaturalizedDatasetsForLocalWadouri } from './utils/registerNaturalizedDatasetForLocalWadouri'; const { downloadBlob } = utils; @@ -784,11 +785,12 @@ const commandsModule = ({ if (dataSource === 'download') { return async dicom => { const instances = Array.isArray(dicom) ? dicom : [dicom]; + registerNaturalizedDatasetsForLocalWadouri(instances); DicomMetadataStore.addInstances(instances, true); if (instances.length !== 1) { throw new Error('Download only supports a single DICOM instance'); } - const reportBlob = dcmjs.data.datasetToBlob(instances[0]); + const reportBlob = datasetToDicomBlob(instances[0]); downloadBlob(reportBlob, { filename: defaultFileName || 'dicom.dcm' }); }; } @@ -796,14 +798,15 @@ const commandsModule = ({ if (dataSource === 'copyToClipboard') { return async dicom => { const instances = Array.isArray(dicom) ? dicom : [dicom]; + registerNaturalizedDatasetsForLocalWadouri(instances); DicomMetadataStore.addInstances(instances, true); if (instances.length !== 1) { throw new Error('Copy to clipboard only supports a single DICOM instance'); } - const reportBlob = dcmjs.data.datasetToBlob(instances[0]); + const reportBlob = datasetToDicomBlob(instances[0]); const type = defaultContentType || 'application/dicom'; await navigator.clipboard.write([ - new ClipboardItem({ [type]: new Blob([reportBlob], { type }) }), + new ClipboardItem({ [type]: reportBlob }), ]); }; } @@ -817,12 +820,18 @@ const commandsModule = ({ return async (dicom, { dicomDict } = {}) => { const instances = Array.isArray(dicom) ? dicom : [dicom]; - const config = resolvedDataSource.getConfig?.(); - if (config?.wadoRoot) { - instances.forEach(instance => { - instance.wadoRoot = config.wadoRoot; - }); + // Always keep an in-memory wadouri copy so DICOM can be read without re-fetching. + registerNaturalizedDatasetsForLocalWadouri(instances); + + if (dataSource !== 'dicomlocal') { + const config = resolvedDataSource.getConfig?.(); + if (config?.wadoRoot) { + instances.forEach(instance => { + setNonEnumerableInstanceProperty(instance, 'wadoRoot', config.wadoRoot); + }); + } } + DicomMetadataStore.addInstances(instances, true); for (const instance of instances) { await resolvedDataSource.store.dicom(instance, null, dicomDict); diff --git a/extensions/default/src/customizations/reportDialogCustomization.tsx b/extensions/default/src/customizations/reportDialogCustomization.tsx index 3ac79db98..fc193a734 100644 --- a/extensions/default/src/customizations/reportDialogCustomization.tsx +++ b/extensions/default/src/customizations/reportDialogCustomization.tsx @@ -10,6 +10,18 @@ type DataSource = { placeHolder: string; }; +/** Radix Select item value for "Create new series" (state remains null). */ +const NEW_SERIES_SELECT_VALUE = '__new_series_id__'; + +type SeriesOption = { + optionKey: string; + selectValue: string; + value: string | null; + seriesNumber: number; + description: string | null; + label: string; +}; + type ReportDialogProps = { dataSources: DataSource[]; modality?: string; @@ -46,20 +58,32 @@ function ReportDialog({ const [selectedSeries, setSelectedSeries] = useState(predecessorImageId || null); const [reportName, setReportName] = useState(''); - const seriesOptions = useMemo(() => { + const seriesOptions = useMemo((): SeriesOption[] => { const displaySetsMap = displaySetService.getDisplaySetCache(); const displaySets = Array.from(displaySetsMap.values()); const options = displaySets .filter(ds => ds.Modality === modality) - .map(ds => ({ - value: ds.predecessorImageId || ds.SeriesInstanceUID, - seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber, - description: ds.SeriesDescription, - label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`, - })); + .map(ds => { + const value = ds.predecessorImageId || ds.SeriesInstanceUID; + const selectValue = value || ds.displaySetInstanceUID; + return { + optionKey: `series-${ds.displaySetInstanceUID}`, + selectValue, + value: value || null, + seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber, + description: ds.SeriesDescription, + label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`, + }; + }) + .filter( + option => + option.selectValue && option.selectValue !== NEW_SERIES_SELECT_VALUE + ); return [ { + optionKey: NEW_SERIES_SELECT_VALUE, + selectValue: NEW_SERIES_SELECT_VALUE, value: null, description: null, seriesNumber: minSeriesNumber, @@ -67,7 +91,17 @@ function ReportDialog({ }, ...options, ]; - }, [displaySetService, modality]); + }, [displaySetService, modality, minSeriesNumber]); + + const handleSeriesChange = useCallback( + (selectValue: string) => { + const option = seriesOptions.find(o => o.selectValue === selectValue); + setSelectedSeries( + selectValue === NEW_SERIES_SELECT_VALUE ? null : (option?.value ?? selectValue) + ); + }, + [seriesOptions] + ); useEffect(() => { const seriesOption = seriesOptions.find(s => s.value === selectedSeries); @@ -115,6 +149,11 @@ function ReportDialog({ const showDataSourceSelect = dataSources?.length > 1; const showDownloadButton = enableDownload; + const selectedSeriesSelectValue = + selectedSeries == null + ? NEW_SERIES_SELECT_VALUE + : (seriesOptions.find(o => o.value === selectedSeries)?.selectValue ?? + selectedSeries); return (
@@ -146,8 +185,8 @@ function ReportDialog({
Series
@@ -181,8 +220,8 @@ function ReportDialog({ {seriesOptions.map(series => ( {series.label} diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts index 133d5878e..f2e9dcf47 100644 --- a/extensions/default/src/index.ts +++ b/extensions/default/src/index.ts @@ -27,6 +27,7 @@ import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolSt import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore'; import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore'; import { callInputDialogAutoComplete, callInputDialog } from './utils/callInputDialog'; +import { releaseLocalWadouriRegistrations } from './utils/registerNaturalizedDatasetForLocalWadouri'; import colorPickerDialog from './utils/colorPickerDialog'; import promptSaveReport from './utils/promptSaveReport'; @@ -52,6 +53,9 @@ const defaultExtension: Types.Extensions.Extension = { useHangingProtocolStageIndexStore.getState().clearHangingProtocolStageIndexMap(); useToggleHangingProtocolStore.getState().clearToggleHangingProtocol(); useViewportsByPositionStore.getState().clearViewportsByPosition(); + // Free the Part 10 Blobs (and any parsed copies) retained for locally + // generated instances (SEG etc.) registered with the wadouri loader. + releaseLocalWadouriRegistrations(); }, getDataSourcesModule, getViewportModule, diff --git a/extensions/default/src/utils/appendFrameQueryToImageId.js b/extensions/default/src/utils/appendFrameQueryToImageId.js new file mode 100644 index 000000000..22190cc6c --- /dev/null +++ b/extensions/default/src/utils/appendFrameQueryToImageId.js @@ -0,0 +1,9 @@ +/** + * Appends ?frame= or &frame= to a local/wadouri imageId. + */ +export function appendFrameQueryToImageId(baseImageId, frame) { + const withoutFrame = baseImageId.split('&frame=')[0].split('?frame=')[0]; + const separator = withoutFrame.includes('?') ? '&' : '?'; + + return `${withoutFrame}${separator}frame=${frame}`; +} diff --git a/extensions/default/src/utils/dicomWriter.test.ts b/extensions/default/src/utils/dicomWriter.test.ts new file mode 100644 index 000000000..b7fe10641 --- /dev/null +++ b/extensions/default/src/utils/dicomWriter.test.ts @@ -0,0 +1,228 @@ +import { TextEncoder, TextDecoder } from 'util'; +import dcmjs from 'dcmjs'; +import { + DICOM_WRITE_OPTIONS, + datasetToDicomPart10Buffer, + writeDicomDictToPart10Buffer, +} from './dicomWriter'; + +// jsdom does not expose TextEncoder/TextDecoder, which dcmjs's buffer streams need. +if (typeof (global as { TextEncoder?: unknown }).TextEncoder === 'undefined') { + (global as { TextEncoder?: unknown }).TextEncoder = TextEncoder; +} +if (typeof (global as { TextDecoder?: unknown }).TextDecoder === 'undefined') { + (global as { TextDecoder?: unknown }).TextDecoder = TextDecoder; +} + +const RLE_LOSSLESS = '1.2.840.10008.1.2.5'; +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; +const IMPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2'; +const PIXEL_DATA_TAG = '7FE00010'; + +// Use frames larger than dcmjs's 20KB fragment size so that, if a frame were +// ever split, we'd see more than one fragment per frame. +const FRAME_BYTES = 30000; + +function makeDicomDict(transferSyntaxUID, vr, pixelDataValue) { + const dict = new dcmjs.data.DicomDict({ + '00020010': { vr: 'UI', Value: [transferSyntaxUID] }, + '00020002': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.66.4'] }, + '00020003': { vr: 'UI', Value: ['1.2.3.4.5'] }, + }); + dict.upsertTag(PIXEL_DATA_TAG, vr, pixelDataValue); + return dict; +} + +/** + * Locates the PixelData element in a Part-10 buffer and, when it is encapsulated, + * walks the encapsulated items (Basic Offset Table + fragments). + */ +function inspectPixelData(arrayBuffer) { + const dv = new DataView(arrayBuffer); + + // Skip the 128-byte preamble + "DICM", then read the file-meta group length + // from (0002,0000) UL to find where the dataset begins. + const metaGroupLength = dv.getUint32(140, true); + let offset = 144 + metaGroupLength; + + // PixelData (7FE0,0010), explicit VR (OB/OW) — long form: VR(2) + reserved(2) + length(4). + const group = dv.getUint16(offset, true); + const element = dv.getUint16(offset + 2, true); + if (group !== 0x7fe0 || element !== 0x0010) { + throw new Error( + `Expected PixelData at offset ${offset}, found ${group.toString(16)},${element.toString(16)}` + ); + } + + const length = dv.getUint32(offset + 8, true); + if (length !== 0xffffffff) { + return { encapsulated: false, length }; + } + + // Encapsulated: first item is the Basic Offset Table, then one item per fragment, + // terminated by the Sequence Delimitation Item (FFFE,E0DD). + let p = offset + 12; + const itemSizes: number[] = []; + + while (p + 8 <= dv.byteLength) { + const itemGroup = dv.getUint16(p, true); + const itemElement = dv.getUint16(p + 2, true); + const itemLength = dv.getUint32(p + 4, true); + p += 8; + + if (itemGroup === 0xfffe && itemElement === 0xe0dd) { + break; // sequence delimiter + } + if (itemGroup !== 0xfffe || itemElement !== 0xe000) { + throw new Error(`Unexpected item tag at ${p - 8}`); + } + + itemSizes.push(itemLength); + p += itemLength; + } + + // itemSizes[0] is the Basic Offset Table; the rest are frame fragments. + return { + encapsulated: true, + totalItems: itemSizes.length, + basicOffsetTableSize: itemSizes[0], + fragmentSizes: itemSizes.slice(1), + }; +} + +describe('dicomWriter DICOM_WRITE_OPTIONS fragmentation', () => { + it('writes exactly one fragment per frame (plus the Basic Offset Table) for compressed pixel data', () => { + const frames = [new Uint8Array(FRAME_BYTES).buffer, new Uint8Array(FRAME_BYTES).buffer]; + const dict = makeDicomDict(RLE_LOSSLESS, 'OB', frames); + + const buffer = writeDicomDictToPart10Buffer(dict); + const result = inspectPixelData(buffer); + + expect(result.encapsulated).toBe(true); + // 1 Basic Offset Table item + 1 fragment per frame. + expect(result.totalItems).toBe(frames.length + 1); + expect(result.fragmentSizes).toHaveLength(frames.length); + // Each frame stayed in a single fragment despite exceeding the 20KB fragment size. + result.fragmentSizes.forEach(size => expect(size).toBe(FRAME_BYTES)); + }); + + it('does not fragment uncompressed pixel data', () => { + const pixelData = new Uint16Array(FRAME_BYTES).buffer; + const dict = makeDicomDict(EXPLICIT_VR_LITTLE_ENDIAN, 'OW', [pixelData]); + + const buffer = writeDicomDictToPart10Buffer(dict); + const result = inspectPixelData(buffer); + + expect(result.encapsulated).toBe(false); + expect(result.length).toBe(pixelData.byteLength); + }); + + it('confirms fragmentMultiframe:false is what prevents a single large frame from splitting', () => { + // Control: with fragmentMultiframe enabled, a >20KB frame splits into multiple + // fragments — proving DICOM_WRITE_OPTIONS.fragmentMultiframe:false is meaningful. + const frames = [new Uint8Array(FRAME_BYTES).buffer]; + const dict = makeDicomDict(RLE_LOSSLESS, 'OB', frames); + + const buffer = dict.write({ ...DICOM_WRITE_OPTIONS, fragmentMultiframe: true }); + const result = inspectPixelData(buffer); + + expect(result.encapsulated).toBe(true); + expect(result.fragmentSizes.length).toBeGreaterThan(1); + }); +}); + +/** + * Byte-level walk of the file meta group. Returns the meta element tags in file + * order and asserts that FileMetaInformationGroupLength spans them exactly. + */ +function readFileMetaTags(arrayBuffer: ArrayBuffer) { + const dv = new DataView(arrayBuffer); + + // Preamble(128) + 'DICM'(4), then (0002,0000) UL must come first. + expect(dv.getUint16(132, true)).toBe(0x0002); + expect(dv.getUint16(134, true)).toBe(0x0000); + const groupLength = dv.getUint32(140, true); + const end = 144 + groupLength; + + const tags: string[] = []; + let p = 144; + while (p < end) { + const group = dv.getUint16(p, true); + const element = dv.getUint16(p + 2, true); + const vr = String.fromCharCode(dv.getUint8(p + 4), dv.getUint8(p + 5)); + + // File meta is always Explicit VR Little Endian. + let length: number; + if (['OB', 'OW', 'OF', 'SQ', 'UT', 'UN'].includes(vr)) { + length = dv.getUint32(p + 8, true); + p += 12 + length; + } else { + length = dv.getUint16(p + 6, true); + p += 8 + length; + } + + tags.push( + `${group.toString(16).padStart(4, '0')}${element.toString(16).padStart(4, '0')}` + ); + } + + // The declared group length covers the meta elements exactly — a garbage + // element counted into it (or omitted from it) would break this. + expect(p).toBe(end); + + return tags; +} + +function makeNaturalizedDataset(meta) { + return { + _meta: meta, + SOPClassUID: '1.2.840.10008.5.1.4.1.1.88.33', + SOPInstanceUID: '1.2.3.4.5', + StudyInstanceUID: '1.2.3.4', + SeriesInstanceUID: '1.2.3.4.1', + Modality: 'SR', + PatientID: 'TEST-PATIENT', + }; +} + +describe('datasetToDicomPart10Buffer file meta', () => { + it('writes only group-0002 elements (no garbage (0000,0000) tag) into the file meta', () => { + const dataset = makeNaturalizedDataset({ + TransferSyntaxUID: { vr: 'UI', Value: [EXPLICIT_VR_LITTLE_ENDIAN] }, + }); + + const buffer = datasetToDicomPart10Buffer(dataset); + + const tags = readFileMetaTags(buffer); + expect(tags.length).toBeGreaterThan(0); + tags.forEach(tag => expect(tag.startsWith('0002')).toBe(true)); + // Elements must appear in ascending tag order within group 2. + expect(tags).toEqual([...tags].sort()); + + const reread = dcmjs.data.DicomMessage.readFile(buffer); + const metaKeys = Object.keys(reread.meta); + expect(metaKeys).not.toContain('00000000'); + metaKeys.forEach(key => expect(key.startsWith('0002')).toBe(true)); + expect(reread.meta['00020010'].Value).toEqual([EXPLICIT_VR_LITTLE_ENDIAN]); + }); + + it('honors a plain-string _meta.TransferSyntaxUID that dcmjs alone would default away', () => { + // dcmjs datasetToDict only reads the object form (_meta.TransferSyntaxUID.Value[0]) + // and silently falls back to Explicit VR LE for a plain string — the hex-key + // assignment in applyTransferSyntaxToFileMeta is what preserves it. + const dataset = makeNaturalizedDataset({ + TransferSyntaxUID: IMPLICIT_VR_LITTLE_ENDIAN, + }); + + const buffer = datasetToDicomPart10Buffer(dataset); + + const tags = readFileMetaTags(buffer); + tags.forEach(tag => expect(tag.startsWith('0002')).toBe(true)); + + const reread = dcmjs.data.DicomMessage.readFile(buffer); + expect(Object.keys(reread.meta)).not.toContain('00000000'); + expect(reread.meta['00020010'].Value).toEqual([IMPLICIT_VR_LITTLE_ENDIAN]); + // The dataset body survives a round trip under the preserved syntax. + expect(reread.dict['00080018'].Value).toEqual(['1.2.3.4.5']); + }); +}); diff --git a/extensions/default/src/utils/dicomWriter.ts b/extensions/default/src/utils/dicomWriter.ts new file mode 100644 index 000000000..3cc34dc7f --- /dev/null +++ b/extensions/default/src/utils/dicomWriter.ts @@ -0,0 +1,110 @@ +import dcmjs from 'dcmjs'; + +export const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; + +export const DICOM_WRITE_OPTIONS = { + allowInvalidVRLength: false, + // `fragmentMultiframe` only governs whether a SINGLE frame is split across + // multiple fragments (dcmjs splits frames larger than its 20KB fragment size). + // It does NOT merge frames: in an encapsulated (compressed) transfer syntax + // every frame is always written as its own fragment, preceded by the Basic + // Offset Table; in an uncompressed syntax pixel data is never fragmented at + // all. Keeping this `false` therefore yields exactly one fragment per frame + // for compressed SEG — the conformant layout — without splitting large frames. + fragmentMultiframe: false, +}; + +/** OHIF runtime fields — not DICOM tags; must not be enumerable for dcmjs datasetToDict. */ +export const RUNTIME_INSTANCE_PROPERTY_KEYS = [ + 'url', + 'wadorsuri', + 'wadouri', + 'wadoRoot', + 'wadoUri', + 'wadoUriRoot', + 'imageRendering', + 'imageId', + '_parentInstance', + 'frameNumber', +] as const; + +/** + * Attaches OHIF runtime data on an instance without enumerable keys (safe for dcmjs datasetToDict). + */ +export function setNonEnumerableInstanceProperty( + instance: Record, + key: string, + value: unknown +) { + Object.defineProperty(instance, key, { + value, + enumerable: false, + writable: true, + configurable: true, + }); +} + +/** + * Re-defines any existing enumerable runtime properties as non-enumerable (keeps values). + */ +export function makeExistingPropertiesNonEnumerable(instance: Record) { + for (const key of RUNTIME_INSTANCE_PROPERTY_KEYS) { + if (!Object.prototype.hasOwnProperty.call(instance, key)) { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(instance, key); + + if (!descriptor || descriptor.enumerable === false) { + continue; + } + + setNonEnumerableInstanceProperty(instance, key, descriptor.value); + } +} + +export function getDatasetTransferSyntaxUID(dataset) { + const fromMeta = dataset?._meta?.TransferSyntaxUID; + + if (typeof fromMeta === 'string') { + return fromMeta; + } + + if (Array.isArray(fromMeta?.Value)) { + return fromMeta.Value[0]; + } + + if (typeof dataset?.TransferSyntaxUID === 'string') { + return dataset.TransferSyntaxUID; + } + + return EXPLICIT_VR_LITTLE_ENDIAN; +} + +function applyTransferSyntaxToFileMeta(dicomDict, transferSyntaxUID) { + if (!transferSyntaxUID || !dicomDict?.meta) { + return; + } + + // Hex key only: DicomMessage.write treats every meta key as a tag string, so a + // naturalized key like 'TransferSyntaxUID' would be parsed via parseInt(..., 16) + // into tag (0000,0000) and written as a garbage element in group 2. + dicomDict.meta['00020010'] = { vr: 'UI', Value: [transferSyntaxUID] }; +} + +export function datasetToDicomPart10Buffer(dataset) { + makeExistingPropertiesNonEnumerable(dataset); + const transferSyntaxUID = getDatasetTransferSyntaxUID(dataset); + const dicomDict = dcmjs.data.datasetToDict(dataset); + applyTransferSyntaxToFileMeta(dicomDict, transferSyntaxUID); + return dicomDict.write(DICOM_WRITE_OPTIONS); +} + +export function datasetToDicomBlob(dataset) { + const part10Buffer = datasetToDicomPart10Buffer(dataset); + return new Blob([part10Buffer], { type: 'application/dicom' }); +} + +export function writeDicomDictToPart10Buffer(dicomDict) { + return dicomDict.write(DICOM_WRITE_OPTIONS); +} diff --git a/extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.js b/extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.js new file mode 100644 index 000000000..7318e0c06 --- /dev/null +++ b/extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.js @@ -0,0 +1,146 @@ +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; +import OHIF from '@ohif/core'; + +import { + datasetToDicomBlob, + makeExistingPropertiesNonEnumerable, + setNonEnumerableInstanceProperty, +} from './dicomWriter'; +import { appendFrameQueryToImageId } from './appendFrameQueryToImageId'; + +const metadataProvider = OHIF.classes.MetadataProvider; + +const SEG_REGISTER_LOG_PREFIX = '[SEG register]'; + +// Every registration pins a full Part 10 Blob in the fileManager (and, once +// loaded, a parsed copy in the dataSetCacheManager) with no other owner, so +// track the imageIds and release them on mode exit. +const registeredImageIds = new Set(); + +/** + * Releases everything previously registered via + * {@link registerNaturalizedDatasetForLocalWadouri}: the Part 10 Blob held by the + * wadouri fileManager and any parsed dataset the dataSetCacheManager retained. + * Call on mode exit. + */ +export function releaseLocalWadouriRegistrations() { + const { fileManager, dataSetCacheManager } = dicomImageLoader.wadouri; + + for (const imageId of registeredImageIds) { + // 'dicomfile:' — the index doubles as the fileManager slot and the + // dataSetCacheManager uri (the scheme is stripped for cache keys). + const uri = imageId.substring(imageId.indexOf(':') + 1); + const fileIndex = Number(uri); + + if (Number.isInteger(fileIndex) && fileIndex >= 0) { + fileManager.remove(fileIndex); + } + + // unload() decrements a refcount shared with cornerstone's image decache; + // nothing may outlive mode exit, so drive it to zero. + for (let guard = 0; guard < 100000 && dataSetCacheManager.isLoaded(uri); guard++) { + dataSetCacheManager.unload(uri); + } + } + + registeredImageIds.clear(); +} + +/** + * Registers a naturalized DICOM dataset with the wadouri file manager so it can be + * loaded like other locally stored instances (blob URL), instead of remote wadors URLs. + * + * Datasets without pixel data (SR, RTSTRUCT, ...) are skipped: they have no frames + * to serve through the image loader, so registering them would only pin their + * Part 10 Blob in memory for the rest of the session. + * + * @param {object} dataset - Naturalized DICOM instance (e.g. generated SEG). + * @param {object} [options] + * @param {string[]} [options.referencedImageIds] - Used for logging / frame count hints. + * @returns {string|undefined} wadouri imageId assigned to dataset.url, or undefined when skipped + */ +export function registerNaturalizedDatasetForLocalWadouri(dataset, options = {}) { + const { referencedImageIds = [] } = options; + + if ( + dataset.PixelData === undefined && + !Array.isArray(dataset.PerFrameFunctionalGroupsSequence) + ) { + OHIF.log.debug( + SEG_REGISTER_LOG_PREFIX, + 'Skipping local wadouri registration (no pixel data)', + { SOPClassUID: dataset.SOPClassUID, SOPInstanceUID: dataset.SOPInstanceUID } + ); + return undefined; + } + + const blob = datasetToDicomBlob(dataset); + const imageId = dicomImageLoader.wadouri.fileManager.add(blob); + registeredImageIds.add(imageId); + + setNonEnumerableInstanceProperty(dataset, 'url', imageId); + + const { StudyInstanceUID, SeriesInstanceUID } = dataset; + const SOPInstanceUID = dataset.SOPInstanceUID || dataset.SopInstanceUID; + + const perFrameGroups = dataset.PerFrameFunctionalGroupsSequence; + if (Array.isArray(perFrameGroups) && !dataset.NumberOfFrames) { + dataset.NumberOfFrames = perFrameGroups.length; + } + + // Local only — never written back to the dataset: single-frame IODs (SR, + // RTSTRUCT) must not gain a NumberOfFrames element in their serialized form. + const numberOfFrames = Math.max( + Number(dataset.NumberOfFrames) || 0, + Array.isArray(perFrameGroups) ? perFrameGroups.length : 0, + 1 + ); + + const frameImageIds = []; + if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) { + const registerMapping = (id, frameNumber = 1) => { + metadataProvider.addImageIdToUIDs(id, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + frameNumber, + }); + }; + + for (let frame = 1; frame <= numberOfFrames; frame++) { + const frameImageId = + numberOfFrames > 1 ? appendFrameQueryToImageId(imageId, frame) : imageId; + + frameImageIds.push(frameImageId); + registerMapping(frameImageId, frame); + } + } + + makeExistingPropertiesNonEnumerable(dataset); + + OHIF.log.debug(SEG_REGISTER_LOG_PREFIX, 'Registered local wadouri instance', { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + numberOfFrames, + baseImageId: imageId, + frameImageIds, + blobSizeBytes: blob.size, + referencedImageIdCount: referencedImageIds.length, + SegmentationType: dataset.SegmentationType, + SOPClassUID: dataset.SOPClassUID, + }); + + return imageId; +} + +export function registerNaturalizedDatasetsForLocalWadouri(instances, options = {}) { + const list = Array.isArray(instances) ? instances : [instances]; + const { referencedImageIds = [] } = options; + + list.forEach(instance => + registerNaturalizedDatasetForLocalWadouri(instance, { referencedImageIds }) + ); + + return list; +} diff --git a/extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.test.js b/extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.test.js new file mode 100644 index 000000000..e63d38886 --- /dev/null +++ b/extensions/default/src/utils/registerNaturalizedDatasetForLocalWadouri.test.js @@ -0,0 +1,177 @@ +import { TextEncoder, TextDecoder } from 'util'; +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; +import OHIF from '@ohif/core'; +import { + registerNaturalizedDatasetForLocalWadouri, + releaseLocalWadouriRegistrations, +} from './registerNaturalizedDatasetForLocalWadouri'; + +// jsdom does not expose TextEncoder/TextDecoder, which dcmjs's buffer streams need. +if (typeof global.TextEncoder === 'undefined') { + global.TextEncoder = TextEncoder; +} +if (typeof global.TextDecoder === 'undefined') { + global.TextDecoder = TextDecoder; +} + +jest.mock('@cornerstonejs/dicom-image-loader', () => ({ + __esModule: true, + default: { + wadouri: { + fileManager: { + add: jest.fn(() => 'dicomfile:0'), + remove: jest.fn(), + }, + dataSetCacheManager: { + isLoaded: jest.fn(() => false), + unload: jest.fn(), + }, + }, + }, +})); + +jest.mock('@ohif/core', () => ({ + __esModule: true, + default: { + classes: { + MetadataProvider: { + addImageIdToUIDs: jest.fn(), + }, + }, + log: { + debug: jest.fn(), + }, + }, +})); + +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; + +const addBlob = dicomImageLoader.wadouri.fileManager.add; +const addImageIdToUIDs = OHIF.classes.MetadataProvider.addImageIdToUIDs; + +function makeDataset(extra = {}) { + return { + _meta: { TransferSyntaxUID: { vr: 'UI', Value: [EXPLICIT_VR_LITTLE_ENDIAN] } }, + SOPInstanceUID: '1.2.3.4.5', + StudyInstanceUID: '1.2.3.4', + SeriesInstanceUID: '1.2.3.4.1', + PatientID: 'TEST-PATIENT', + ...extra, + }; +} + +describe('registerNaturalizedDatasetForLocalWadouri', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + // Drain the module-level registration tracking between tests. + releaseLocalWadouriRegistrations(); + }); + + it('skips datasets without pixel data (SR/RTSTRUCT) and does not mutate them', () => { + const dataset = makeDataset({ + SOPClassUID: '1.2.840.10008.5.1.4.1.1.88.33', + Modality: 'SR', + }); + + const imageId = registerNaturalizedDatasetForLocalWadouri(dataset); + + // No frames to serve through the image loader: nothing registered, no Blob pinned. + expect(imageId).toBeUndefined(); + expect(addBlob).not.toHaveBeenCalled(); + expect(addImageIdToUIDs).not.toHaveBeenCalled(); + + // The serialized form of an SR must not gain a (0028,0008) element or a url. + expect(Object.prototype.hasOwnProperty.call(dataset, 'NumberOfFrames')).toBe(false); + expect(Object.prototype.hasOwnProperty.call(dataset, 'url')).toBe(false); + }); + + it('registers a single-frame pixel-bearing dataset without stamping NumberOfFrames', () => { + const dataset = makeDataset({ + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + Modality: 'OT', + PixelData: new Uint8Array(4).buffer, + }); + + const imageId = registerNaturalizedDatasetForLocalWadouri(dataset); + + expect(Object.prototype.hasOwnProperty.call(dataset, 'NumberOfFrames')).toBe(false); + + expect(imageId).toBe('dicomfile:0'); + expect(addBlob).toHaveBeenCalledTimes(1); + // Single frame: the bare imageId is mapped, no ?frame= query. + expect(addImageIdToUIDs).toHaveBeenCalledTimes(1); + expect(addImageIdToUIDs).toHaveBeenCalledWith('dicomfile:0', { + StudyInstanceUID: '1.2.3.4', + SeriesInstanceUID: '1.2.3.4.1', + SOPInstanceUID: '1.2.3.4.5', + frameNumber: 1, + }); + }); + + it('still registers per-frame imageIds (and NumberOfFrames) for a multiframe SEG', () => { + const dataset = makeDataset({ + SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4', + Modality: 'SEG', + PerFrameFunctionalGroupsSequence: [{}, {}, {}], + }); + + registerNaturalizedDatasetForLocalWadouri(dataset); + + // Legitimately multiframe: the count is derived from the per-frame groups + // and must be serialized (enumerable). + expect(dataset.NumberOfFrames).toBe(3); + expect(Object.getOwnPropertyDescriptor(dataset, 'NumberOfFrames').enumerable).toBe(true); + + expect(addImageIdToUIDs).toHaveBeenCalledTimes(3); + for (let frame = 1; frame <= 3; frame++) { + expect(addImageIdToUIDs).toHaveBeenNthCalledWith(frame, `dicomfile:0?frame=${frame}`, { + StudyInstanceUID: '1.2.3.4', + SeriesInstanceUID: '1.2.3.4.1', + SOPInstanceUID: '1.2.3.4.5', + frameNumber: frame, + }); + } + }); + + it('keeps the wadouri imageId on the dataset without making it serializable', () => { + const dataset = makeDataset({ + SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4', + Modality: 'SEG', + PixelData: new Uint8Array(4).buffer, + }); + + registerNaturalizedDatasetForLocalWadouri(dataset); + + expect(dataset.url).toBe('dicomfile:0'); + expect(Object.getOwnPropertyDescriptor(dataset, 'url').enumerable).toBe(false); + }); + + it('releases the retained Blob and cached dataset on releaseLocalWadouriRegistrations', () => { + const dataset = makeDataset({ + SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4', + Modality: 'SEG', + PixelData: new Uint8Array(4).buffer, + }); + dicomImageLoader.wadouri.fileManager.add.mockReturnValueOnce('dicomfile:7'); + // Simulate a parsed dataset held with a refcount of 2 (e.g. two frame loads). + dicomImageLoader.wadouri.dataSetCacheManager.isLoaded + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValue(false); + + registerNaturalizedDatasetForLocalWadouri(dataset); + releaseLocalWadouriRegistrations(); + + expect(dicomImageLoader.wadouri.fileManager.remove).toHaveBeenCalledWith(7); + expect(dicomImageLoader.wadouri.dataSetCacheManager.unload).toHaveBeenCalledTimes(2); + expect(dicomImageLoader.wadouri.dataSetCacheManager.unload).toHaveBeenCalledWith('7'); + + // Idempotent: the tracking set was drained. + dicomImageLoader.wadouri.fileManager.remove.mockClear(); + releaseLocalWadouriRegistrations(); + expect(dicomImageLoader.wadouri.fileManager.remove).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index 45a081217..7a913cb68 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index 08ba5af1b..62645a5c5 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 534c9dd21..91dd67418 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -27,14 +27,14 @@ "start": "pnpm run dev" }, "peerDependencies": { - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/tools": "5.1.3", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/tools": "5.4.12", "@ohif/core": "workspace:*", "@ohif/extension-cornerstone-dicom-sr": "workspace:*", "@ohif/extension-default": "workspace:*", "@ohif/ui": "workspace:*", "classnames": "2.5.1", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "lodash.debounce": "4.0.8", "prop-types": "15.8.1", "react": "18.3.1", @@ -48,6 +48,7 @@ "xstate": "4.38.3" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" }, "keywords": [ diff --git a/extensions/test-extension/package.json b/extensions/test-extension/package.json index 28a3c4a36..06edb6abf 100644 --- a/extensions/test-extension/package.json +++ b/extensions/test-extension/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/tmtv/package.json b/extensions/tmtv/package.json index 867a72b83..b25ca2791 100644 --- a/extensions/tmtv/package.json +++ b/extensions/tmtv/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "workspace:*", "@ohif/ui": "workspace:*", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicom-parser": "1.8.21", "hammerjs": "2.0.8", "prop-types": "15.8.1", diff --git a/extensions/usAnnotation/package.json b/extensions/usAnnotation/package.json index fc3b53337..3c57f69e5 100644 --- a/extensions/usAnnotation/package.json +++ b/extensions/usAnnotation/package.json @@ -36,8 +36,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/tools": "5.1.3", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/tools": "5.4.12", "@ohif/core": "workspace:*", "@ohif/extension-cornerstone": "workspace:*", "@ohif/extension-default": "workspace:*", diff --git a/modes/basic-dev-mode/package.json b/modes/basic-dev-mode/package.json index ef6ebebd9..0a70ab274 100644 --- a/modes/basic-dev-mode/package.json +++ b/modes/basic-dev-mode/package.json @@ -41,6 +41,7 @@ "i18next": "17.3.1" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" } } diff --git a/modes/basic-test-mode/package.json b/modes/basic-test-mode/package.json index 2f59a2386..d71225260 100644 --- a/modes/basic-test-mode/package.json +++ b/modes/basic-test-mode/package.json @@ -43,6 +43,7 @@ "i18next": "17.3.1" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" }, "keywords": [ diff --git a/modes/basic/package.json b/modes/basic/package.json index 4ef1d27bf..294d55ecf 100644 --- a/modes/basic/package.json +++ b/modes/basic/package.json @@ -38,6 +38,7 @@ "@babel/runtime": "7.29.7" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" }, "keywords": [ diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json index 531642a69..8fd4691c4 100644 --- a/modes/longitudinal/package.json +++ b/modes/longitudinal/package.json @@ -45,6 +45,7 @@ "i18next": "17.3.1" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" }, "keywords": [ diff --git a/modes/tmtv/package.json b/modes/tmtv/package.json index 0b9136747..4acb2b701 100644 --- a/modes/tmtv/package.json +++ b/modes/tmtv/package.json @@ -42,6 +42,7 @@ "i18next": "17.3.1" }, "devDependencies": { + "cross-env": "7.0.3", "webpack-merge": "5.10.0" }, "keywords": [ diff --git a/modes/usAnnotation/package.json b/modes/usAnnotation/package.json index 773a28e76..8a9404ea9 100644 --- a/modes/usAnnotation/package.json +++ b/modes/usAnnotation/package.json @@ -29,8 +29,8 @@ }, "dependencies": { "@babel/runtime": "7.29.7", - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/tools": "5.1.3", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/tools": "5.4.12", "@ohif/core": "workspace:*", "@ohif/extension-cornerstone-dicom-sr": "workspace:*", "@ohif/extension-ultrasound-pleura-bline": "workspace:*", diff --git a/platform/app/.env b/platform/app/.env index 5c17741b2..6b1551cd9 100644 --- a/platform/app/.env +++ b/platform/app/.env @@ -7,5 +7,5 @@ # PUBLIC_URL=/ -APP_CONFIG=config/default.js +# Can choose a default app config, but this over-rides dev scripts: APP_CONFIG=config/default.js USE_HASH_ROUTER=false diff --git a/platform/app/package.json b/platform/app/package.json index a6eb5f86d..26a0cff16 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -51,7 +51,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/dicom-image-loader": "5.1.3", + "@cornerstonejs/dicom-image-loader": "5.4.12", "@emotion/serialize": "1.3.3", "@ohif/core": "workspace:*", "@ohif/i18n": "workspace:*", @@ -62,7 +62,7 @@ "classnames": "2.5.1", "core-js": "3.45.1", "cornerstone-math": "0.1.10", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "detect-gpu": "4.0.50", "dicom-parser": "1.8.21", "file-loader": "6.2.0", diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 990de7324..bd72b2e37 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -37,7 +37,6 @@ window.config = { // '/remote/': 'https://cdn.example.com/ohif-custom/', // ?customization=/remote/siteA // }, // ---------------------------------------------------------------------------- - showStudyList: true, // some windows systems have issues with more than 3 web workers maxNumberOfWebWorkers: 3, @@ -80,6 +79,11 @@ window.config = { supportsFuzzyMatching: true, supportsWildcard: true, staticWado: true, + // Multiframe SEG loads fetch the whole instance as a single Part 10 + // object by default and wait for it: the per-frame endpoint is + // efficient, but SEG frames are so small and numerous that one bulk + // fetch beats hundreds of tiny requests. Per-frame loading is the + // exception — set loadMultiframeAsPart10: false here to force it. singlepart: 'bulkdata,video', bulkDataURI: { enabled: true, diff --git a/platform/app/public/customizations/segmentation/binary.jsonc b/platform/app/public/customizations/segmentation/binary.jsonc new file mode 100644 index 000000000..3cc902f41 --- /dev/null +++ b/platform/app/public/customizations/segmentation/binary.jsonc @@ -0,0 +1,19 @@ +// URL-loaded customization: segmentation/binary +// +// Stores exported/saved DICOM SEG objects as a BINARY Segmentation — SOP Class +// 1.2.840.10008.5.1.4.1.1.66.4 (one frame per segment) — instead of the OHIF +// default Label Map Segmentation 1.2.840.10008.5.1.4.1.1.66.7. Binary SEG is +// broadly compatible with existing PACS/viewers; Label Map was only added to +// DICOM in 2024 and many back ends reject it. +// +// Applied in the `global` phase so it overrides the default registered by the +// cornerstone-dicom-seg extension. A data source may still override this per +// back end via `configuration.segmentation.store.defaultMode`. +// +// Load it with `?customization=segmentation/binary` (combine with +// `&customization=segmentation/uncompressed` to also drop RLE compression). +{ + "global": { + "segmentation.store.defaultMode": { "$set": "bitmap" } + } +} diff --git a/platform/app/public/customizations/segmentation/uncompressed.jsonc b/platform/app/public/customizations/segmentation/uncompressed.jsonc new file mode 100644 index 000000000..212de95ff --- /dev/null +++ b/platform/app/public/customizations/segmentation/uncompressed.jsonc @@ -0,0 +1,19 @@ +// URL-loaded customization: segmentation/uncompressed +// +// Stores exported/saved DICOM SEG objects UNCOMPRESSED — Explicit VR Little +// Endian (1.2.840.10008.1.2.1) — instead of the OHIF default of RLE Lossless +// (1.2.840.10008.1.2.5). Use this for back ends that reject compressed SEG +// PixelData. +// +// Applied in the `global` phase. OHIF already defaults to RLE Lossless; this +// customization opts into uncompressed Explicit VR Little Endian instead. A data +// source may still override per back end via +// `configuration.segmentation.store.transferSyntaxUID`. +// +// Load it with `?customization=segmentation/uncompressed` (combine with +// `&customization=segmentation/binary` to also switch the SOP class). +{ + "global": { + "segmentation.store.transferSyntaxUID": { "$set": "1.2.840.10008.1.2.1" } + } +} diff --git a/platform/core/package.json b/platform/core/package.json index b3ea9d5e3..9edee599b 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -35,16 +35,16 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjph": "2.4.7", - "@cornerstonejs/core": "5.1.3", - "@cornerstonejs/dicom-image-loader": "5.1.3", - "@cornerstonejs/metadata": "5.1.3", + "@cornerstonejs/core": "5.4.12", + "@cornerstonejs/dicom-image-loader": "5.4.12", + "@cornerstonejs/metadata": "5.4.12", "@ohif/ui": "workspace:*", "cornerstone-math": "0.1.10", "dicom-parser": "1.8.21" }, "dependencies": { "@babel/runtime": "7.29.7", - "dcmjs": "0.49.4", + "dcmjs": "0.52.0", "dicomweb-client": "0.10.4", "gl-matrix": "3.4.3", "immutability-helper": "3.1.1", diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts index 1b8d43085..57f9abff3 100644 --- a/platform/core/src/classes/MetadataProvider.ts +++ b/platform/core/src/classes/MetadataProvider.ts @@ -1,7 +1,7 @@ import queryString from 'query-string'; import dicomParser from 'dicom-parser'; import { utilities } from '@cornerstonejs/core'; -import { imageIdToURI } from '../utils'; +import { baseImageURIForMetadata } from '../utils/imageIdToURI'; import DicomMetadataStore from '../services/DicomMetadataStore'; import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import toNumber from '../utils/toNumber'; @@ -24,12 +24,12 @@ class MetadataProvider { // This method is a fallback for when you don't have WADO-URI or WADO-RS. // You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here. // An example would be dicom hosted at some random site. - const imageURI = imageIdToURI(imageId); + const imageURI = baseImageURIForMetadata(imageId); this.imageURIToUIDs.set(imageURI, uids); } addCustomMetadata(imageId, type, metadata) { - const imageURI = imageIdToURI(imageId); + const imageURI = baseImageURIForMetadata(imageId); if (!this.customMetadata.has(type)) { this.customMetadata.set(type, {}); } @@ -76,7 +76,7 @@ class MetadataProvider { // check inside custom metadata if (this.customMetadata.has(query)) { const customMetadata = this.customMetadata.get(query); - const imageURI = imageIdToURI(imageId); + const imageURI = baseImageURIForMetadata(imageId); if (customMetadata[imageURI]) { return customMetadata[imageURI]; } @@ -445,6 +445,9 @@ class MetadataProvider { if (imageId.includes('/frames')) { return getInformationFromURL('/frames', '/'); } + if (imageId.includes('?frame=')) { + return getInformationFromURL('?frame=', '&'); + } if (imageId.includes('&frame=')) { return getInformationFromURL('&frame=', '&'); } @@ -473,20 +476,7 @@ class MetadataProvider { }; } - // Maybe its a non-standard imageId - // check if the imageId starts with http:// or https:// using regex - // Todo: handle non http imageIds - let imageURI; - const urlRegex = /^(http|https|dicomfile):\/\//; - if (urlRegex.test(imageId)) { - imageURI = imageId; - } else { - imageURI = imageIdToURI(imageId); - } - - // remove &frame=number from imageId - imageURI = imageURI.split('&frame=')[0]; - + const imageURI = baseImageURIForMetadata(imageId); const uids = this.imageURIToUIDs.get(imageURI); const frameNumber = this.getFrameInformationFromURL(imageId) || '1'; diff --git a/platform/core/src/utils/combineFrameInstance.ts b/platform/core/src/utils/combineFrameInstance.ts index 657619d00..137dc9aff 100644 --- a/platform/core/src/utils/combineFrameInstance.ts +++ b/platform/core/src/utils/combineFrameInstance.ts @@ -10,6 +10,12 @@ import { dicomSplit } from './dicomSplit'; * will be ignored. * This can be safely called with an undefined frame in order to handle * single frame data. (eg frame is undefined is the same as frame===1). + * + * Note: instances carry non-enumerable runtime props (frameNumber, imageId, + * url, wadoRoot, ...). This is intentional: dcmjs serialization skips them and + * spreads/copies (including of anything from `metaData.get('instance', ...)`) + * deliberately do not carry them — frameNumber in particular must not be + * copied onto other objects. Read runtime props off the original instance. */ const combineFrameInstance = (frame, instance) => { const { @@ -83,6 +89,9 @@ const combineFrameInstance = (frame, instance) => { if (!instance._parentInstance) { Object.defineProperty(instance, '_parentInstance', { value: { ...instance }, + enumerable: false, + writable: false, + configurable: false, }); } const sharedInstance = createCombinedValue( @@ -102,7 +111,7 @@ const combineFrameInstance = (frame, instance) => { Object.defineProperty(newInstance, 'frameNumber', { value: frameNumber, writable: true, - enumerable: true, + enumerable: false, configurable: true, }); return newInstance; @@ -113,6 +122,9 @@ const combineFrameInstance = (frame, instance) => { if (!instance._parentInstance) { Object.defineProperty(instance, '_parentInstance', { value: { ...instance }, + enumerable: false, + writable: false, + configurable: false, }); } @@ -144,7 +156,7 @@ const combineFrameInstance = (frame, instance) => { Object.defineProperty(newInstance, 'frameNumber', { value: frameNumber, writable: true, - enumerable: true, + enumerable: false, configurable: true, }); diff --git a/platform/core/src/utils/imageIdToURI.js b/platform/core/src/utils/imageIdToURI.js index 7be55b075..dd094c93c 100644 --- a/platform/core/src/utils/imageIdToURI.js +++ b/platform/core/src/utils/imageIdToURI.js @@ -10,3 +10,20 @@ export default function imageIdToURI(imageId) { return imageId.substring(colonIndex + 1); } + +/** + * Normalizes an imageId to the metadata lookup key (scheme stripped, frame query removed). + * Must match MetadataProvider.getUIDsFromImageID lookup behavior. + */ +export function baseImageURIForMetadata(imageId) { + const urlRegex = /^(http|https|dicomfile):\/\//; + let imageURI; + + if (urlRegex.test(imageId)) { + imageURI = imageId; + } else { + imageURI = imageIdToURI(imageId); + } + + return imageURI.split('&frame=')[0].split('?frame=')[0]; +} diff --git a/platform/docs/docs/behaviours/README.md b/platform/docs/docs/behaviours/README.md new file mode 100644 index 000000000..08eea9437 --- /dev/null +++ b/platform/docs/docs/behaviours/README.md @@ -0,0 +1,44 @@ +--- +id: README +title: Behaviours +sidebar_position: 0 +--- + +# Behaviours + +This section documents **how the system and UI actually behave** — the +end-to-end behaviours that emerge from the interaction of services, extensions, +the data source, and Cornerstone3D, rather than the API of any single module. + +It is the place for: + +- **Observed behaviours** — how a feature works today across the stack (e.g. how + a DICOM SEG is fetched, decoded, and rendered; what the viewport does on study + change; how measurements round-trip to SR). +- **Design proposals** — proposed or in-progress changes to a behaviour, captured + before/while they are implemented so the intent and trade-offs are recorded. + These are clearly marked as proposals until they land. +- **Edge cases and failure modes** — what happens when something is missing, + slow, or malformed, and how the system is expected to degrade. + +The goal is a durable, discoverable record of *behaviour* — the kind of +cross-cutting knowledge that is otherwise only in people's heads or scattered +across code comments. Prefer linking to the relevant source files (with line +references) so each behaviour doc stays anchored to the code it describes. + +## Index + +- [Segmentation: loading a multiframe SEG as a single Part 10 instance](./segmentation-multiframe-part10-prefetch.md) + — _implemented, enabled by default_. Prefetch the whole instance in one request + and register it into the Cornerstone3D NATURALIZED frame registry so the + per-frame load path (WADO-RS and WADO-URI) is served locally, while keeping the + standard decode path unchanged. Per-frame loading is the exception — disable + via `loadMultiframeAsPart10: false` (data source config or the + `cornerstone.segmentation.loadMultiframeAsPart10` customization). + +## Writing a new behaviour doc + +1. Add a `kebab-case.md` file in this directory. +2. State whether it documents **current behaviour** or is a **proposal**. +3. Link to the code (`path:line`) that implements or will implement it. +4. Add it to the **Index** above. diff --git a/platform/docs/docs/behaviours/_category_.json b/platform/docs/docs/behaviours/_category_.json new file mode 100644 index 000000000..7dec1b789 --- /dev/null +++ b/platform/docs/docs/behaviours/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Behaviours", + "position": 14, + "link": { + "type": "doc", + "id": "behaviours/README" + } +} diff --git a/platform/docs/docs/behaviours/segmentation-multiframe-part10-prefetch.md b/platform/docs/docs/behaviours/segmentation-multiframe-part10-prefetch.md new file mode 100644 index 000000000..b2ef20361 --- /dev/null +++ b/platform/docs/docs/behaviours/segmentation-multiframe-part10-prefetch.md @@ -0,0 +1,368 @@ +# Full-instance prefetch for segmentation (and multiframe) loading + +Status: **Implemented — enabled and awaited to completion by default; +per-frame loading is the explicit opt-out** + +The boolean `loadMultiframeAsPart10` resolves, in order: the data source +`configuration`, the global customization +`cornerstone.segmentation.loadMultiframeAsPart10`, then the built-in default of +`true` — i.e. by default the SEG load **waits for the whole-instance +fetch+parse to complete or fail** (deliberately no timeout) and serves every +frame from the registry. Set `loadMultiframeAsPart10: false` explicitly to +force per-frame loading — the exception, for back ends that need to fetch the +individual images instead (e.g. servers that cannot serve a whole-instance +retrieve, or deployments where holding the full Part 10 object in memory is +undesirable). A failed or unsupported instance fetch resolves quickly and +falls back to per-frame regardless of the setting, so it never wedges the load. + +Note the per-frame endpoint itself is not inefficient — each frame request is +cheap — but SEG frames are so small and numerous that one bulk Part 10 fetch +beats hundreds of tiny requests (see Problem below). This holds even for very +large SEG objects (hundreds of MB): any finite race cap would simply expire on +those and storm per-frame anyway while abandoning the bulk fetch's benefit, +which is why there is no timeout. + +Implemented across: + +- `@cornerstonejs/dicom-image-loader` + - `imageLoader/prefetchPart10Instance.ts` — registers a Part 10 instance into + the frame registry (thin wrapper over `addDicomPart10Instance`). + - `imageLoader/wadors/loadImageFromRegistry.ts` + + `wadors/loadImage.ts` — WADO-RS loads now consult the registry first. +- `@ohif/extension-default` `DicomWebDataSource` — `retrieve.prefetchInstanceFrames`. +- `@ohif/extension-cornerstone-dicom-seg` `getSopClassHandlerModule.ts` — call + site (resolves the config and awaits the prefetch). + +Related: `@cornerstonejs/adapters` `labelmapImagesFromBuffer.ts` +(`decodeSegPixelDataFromFrameIds`, bounded by `concurrency`, default 16). + +## Problem + +The SEG load path now fetches/decodes frames through the cornerstone image +loader, one loadable `imageId` per frame, parallelized up to `N=16` +(`concurrency`). For a SEG (or any multiframe instance) with **800+ small +frames**, this means 800+ independent HTTP requests, each with its own +request/response overhead. Even at 16-wide concurrency the *per-request* latency +floor dominates: each frame is < 1 KB of payload but pays a full round trip. + +Streaming **one large object** (the entire Part 10 / multiframe instance) is far +cheaper than streaming hundreds of tiny ones — a single connection, a single set +of headers, no per-frame TTFB. The whole instance for an 800-frame binary SEG is +typically a few hundred KB to a few MB. + +## Goal + +Add a **generic data-source capability** that, _just before_ the segmentation +loader runs, optionally fetches the **entire original instance** in one request +and **registers it into the Cornerstone3D frame registry** (the +`@cornerstonejs/metadata` NATURALIZED + `COMPRESSED_FRAME_DATA` framework, parsed +by the dcmjs async reader) — so the existing per-frame `imageId` fetch path +transparently hits local data instead of the network, while the **cornerstone +decode path stays byte-for-byte identical** (same decompressor, same workers). + +Crucially this is **best-effort**: + +- By default (`loadMultiframeAsPart10` unset → `true`) the load **waits for the + full-instance fetch+parse to complete or fail** — no timeout — then every + frame is served from the registry. +- If `loadMultiframeAsPart10` resolves to `false` (explicitly configured), the + capability is **disabled** — no full-instance fetch is attempted. Per-frame + loading is the exception, opted into per deployment. +- If the full-instance fetch or parse **fails for any reason**, it must **never** + fail the segmentation decode. We log and fall back to per-frame fetches. + +## Why this is safe / transparent + +There is **one uniform frame registry: the Cornerstone3D `@cornerstonejs/metadata` +NATURALIZED framework**, populated by `addDicomPart10Instance` (which parses the +Part 10 with the dcmjs `AsyncDicomReader`) and read per-frame via the +`COMPRESSED_FRAME_DATA` typed provider. Frame imageIds are normalized to the base +instance by `baseImageIdQueryFilter` (strips `/frames/N`, `?frame=N`, `&frame=N`), +so one registration under the instance serves every frame. + +This is the **same registry the WADO-URI / `dicomweb` loader already uses**: +`wadouri/loadImage.ts`'s `loadImageFromNaturalizedMetadata` resolves each frame +from `COMPRESSED_FRAME_DATA` and decodes it with `createImage`. The gap was that +**WADO-RS** (`wadors/loadImage.ts`) always issued a per-frame `/frames/N` request +and never consulted the registry. The adapter closes that gap: + +- `wadors/loadImageFromRegistry.ts` — `loadImageFromCompressedFrameRegistry(imageId)` + looks up `COMPRESSED_FRAME_DATA` for the frame; if present it decodes via the + **same `createImage`** path and returns the image; if absent it returns + `undefined`. +- `wadors/loadImage.ts` calls it first and short-circuits when the registry has + the frame, otherwise falls through to the existing network path. + +So once `prefetchPart10Instance` has registered the instance, **both WADO-URI and +WADO-RS** serve every frame from the single registry, with the **same decode +pipeline** (`createImage` → worker decoder). From cornerstone's perspective +nothing changed except the compressed bytes came from the registry instead of the +wire. + +> Rejected alternatives: (1) a bespoke `Map` registry plus +> `getPixelData` shims — duplicates a registry that already exists; (2) seeding +> the core image cache (`cache.putImageLoadObject`) per frame — works, but the +> per-frame compressed data already lives in the NATURALIZED registry, so basing +> the design on that registry (and teaching WADO-RS to read it) keeps a single +> source of truth instead of copying frames into a second cache. + +## API (as implemented) + +A generic capability on the data source's `retrieve` namespace (so it works for +any multiframe instance, not just SEG, and alternate data sources can override +it), plus the SEG handler call site that awaits it. + +`IWebApiDataSource.create` passes `retrieve` through verbatim, so the capability +is added there (no `@ohif/core` change): + +```ts +// On the data source (DicomWebDataSource): +dataSource.retrieve.prefetchInstanceFrames({ + instance, // study/series/sop UIDs for retrieveInstance + imageId, // SEG instance imageId (frame qualifiers normalized away) +}): { + done: Promise; // resolves true if fetched+registered, false if skipped/failed + cancel: () => void; +}; +``` + +The implementation fetches the instance with `wadoDicomWebClient.retrieveInstance` +(which returns the Part 10 as an `ArrayBuffer`, unwrapping `multipart/related` +transparently) and passes a **lazy resolver** to +`dicomImageLoader.prefetchPart10Instance(imageId, resolver)`. All work is wrapped +so any failure resolves `done` to `false` — it never throws into the loader. + +### Call site (just before the loader) + +In `getSopClassHandlerModule.ts`, immediately before +`createFromDicomSegImageId(...)` (abridged from the actual code): + +```ts +const loadMultiframeAsPart10 = + (dataSource?.getConfig?.()?.loadMultiframeAsPart10 as boolean | undefined) ?? + (customizationService?.getCustomization?.( + 'cornerstone.segmentation.loadMultiframeAsPart10' + ) as boolean | undefined) ?? + true; + +let prefetch; +if (loadMultiframeAsPart10) { + prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({ + instance, + imageId: segImageIdForMetadata, + }); + if (prefetch?.done) { + // Wait for the bulk fetch to complete or fail — no timeout. + await prefetch.done; + } +} + +try { + results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDicomSegImageId( + imageIds, + segImageIdForMetadata, + { metadataProvider: metaData, tolerance, parserType, frameImageIds, + concurrency: SEG_FRAME_DECODE_CONCURRENCY } + ); +} finally { + eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress); + prefetch?.cancel?.(); +} +``` + +The SEG loader and adapter are **unchanged** — the loader still asks the image +loader for each frame. Registered frames resolve from the registry; the rest +fetch normally. + +## Mechanism (the 4 requested points) + +### 1. Async DICOM reader (dcmjs) to parse the instance + +Parsing is done by `@cornerstonejs/metadata`'s `addDicomPart10Instance`, which +uses the dcmjs **`AsyncDicomReader`** (`naturalizePart10Buffer` in +`naturalizedHandlers.ts`) and understands encapsulated PixelData fragmentation, +so the NATURALIZED instance exposes pixel data as an array of per-frame +ArrayBuffer fragments. `COMPRESSED_FRAME_DATA` then slices out the requested +frame. We did not re-implement any parsing — the prefetch just feeds the fetched +Part 10 buffer to this existing reader. + +**Implemented as full-buffer v1**: the whole instance is fetched, then parsed. +`addDicomPart10Instance` accepts a lazy resolver, so the fetch is what the race +waits on. Progressive/streaming registration (registering frames as bytes arrive) +is a future optimization — see rollout step 4. + +#### Response envelope: `multipart/related` vs raw DICOM + +The full-instance fetch can come back in **two shapes**, and the parser must +handle both before any DICOM parsing happens: + +- **`multipart/related`** — a WADO-RS instance retrieve + (`GET …/instances/{sop}` with `Accept: multipart/related; type="application/dicom"`) + wraps the Part 10 object(s) in a MIME multipart envelope: leading part headers, + a `--boundary` marker, the DICOM bytes, then a terminal `--boundary--`. The + inner DICOM byte offsets are **shifted** by the MIME header length, so dcmjs + must be fed the *unwrapped* inner part, never the raw response. +- **raw DICOM** — WADO-URI, a direct file/object URL, or a `bulkDataURI` that + returns a single `application/dicom` body: the response **is** the Part 10 + bytes, no envelope. + +**As implemented**, the full-buffer path delegates this to **`dicomweb-client`'s +`retrieveInstance`**, which performs the WADO-RS instance retrieve and returns the +Part 10 as an `ArrayBuffer` with the `multipart/related` envelope already +unwrapped (and returns the single-part body as-is). `prefetchInstanceFrames` +additionally tolerates an `[ArrayBuffer]` or `ArrayBufferView` shape defensively. +So the implemented path is: + +``` +retrieveInstance() → ArrayBuffer (multipart unwrapped by dicomweb-client) + → prefetchPart10Instance → addDicomPart10Instance → dcmjs AsyncDicomReader +``` + +Detection by `Content-Type` and manual unwrapping (the approach `wadors/extractMultipart.ts` +uses: `contentType.indexOf('multipart') === -1` → bytes as-is; else strip the +boundary/part headers) becomes relevant only for the **future streaming path** +(rollout step 4), where these wrinkles must be handled directly: + +- The multipart preamble/boundary stripped at the **start** of the stream and the + terminal boundary detected at the **end**; a boundary token can **straddle chunk + boundaries**, so the unwrapper must buffer across chunks (the existing + `findIndexOfString` + `tokenIndex` carry-over in `extractMultipart` is built for + incremental calls). +- `multipart/related` may contain **multiple parts**; a single-instance retrieve + yields one — guard for >1. +- A truncated/partial body (no terminal boundary yet) must not be mis-parsed. + +### 2. Register the compressed data into the frame registry + +The fetched Part 10 buffer is registered **once per instance** via the +`prefetchPart10Instance` adapter (a thin wrapper over `addDicomPart10Instance`): + +```ts +// @cornerstonejs/dicom-image-loader: imageLoader/prefetchPart10Instance.ts +import { utilities } from '@cornerstonejs/metadata'; +const { addDicomPart10Instance } = utilities; + +export function prefetchPart10Instance(baseImageId, part10 /* buffer | resolver */) { + return addDicomPart10Instance(baseImageId, part10); +} +``` + +That populates NATURALIZED for the instance; `COMPRESSED_FRAME_DATA` then yields +each frame's compressed bytes + transfer syntax on demand. Frame imageIds +normalize to the instance key automatically (`baseImageIdQueryFilter`), so there +is no per-frame registration loop and no second cache to keep coherent — the +registry **is** the existing NATURALIZED framework. + +The "few new adapters" the task calls for: + +1. `prefetchPart10Instance(baseImageId, part10)` — entry point to register a + fetched instance into the registry (exported from + `@cornerstonejs/dicom-image-loader`). +2. `loadImageFromCompressedFrameRegistry(imageId, options)` in + `wadors/loadImageFromRegistry.ts` — teaches the **WADO-RS** loader to read the + registry. It looks up `COMPRESSED_FRAME_DATA` for the frame and, if present, + decodes via the same `createImage` the network path uses; `wadors/loadImage.ts` + calls it first and short-circuits on a hit. (WADO-URI already reads the + registry via `loadImageFromNaturalizedMetadata`.) + +Metadata the decoder needs (`imagePixelModule`, transfer syntax) comes from the +same NATURALIZED instance, plus the SEG handler's existing +`_ensureSegInstanceMetadataAvailable` per frame imageId. + +### 3. Cornerstone uses its existing decompressor — identical downstream + +Both the WADO-URI path and the new WADO-RS registry path decode via +`createImage` (→ the same web-worker decoder used for every network frame), so +encapsulated transfer syntaxes are decompressed exactly as if fetched per-frame +and native/uncompressed frames pass through unchanged. No new decode path, no +codec changes, no divergence in pixel output — the only difference is that the +compressed bytes came from the registry instead of the wire. + +### 4. Failure is non-fatal — fall back to per-frame fetches + +`prefetchInstanceFrames` wraps the fetch + parse so that **any** failure (network +error, cancel, parse error, unexpected `retrieveInstance` shape) resolves +`done` to `false` and logs a warning — it never throws into the SEG load. When +the registry has no data for a frame, `loadImageFromCompressedFrameRegistry` +returns `undefined` and the WADO-RS loader falls through to its normal +`/frames/N` request. So if the prefetch is slow, disabled, or fails, loading is +exactly today's per-frame behaviour. + +Edge cases that degrade gracefully: + +- `cancel()` (viewport closed / segmentation removed mid-load) → the resolver + throws on its cancelled flag; `done` resolves `false`; any already-registered + data stays usable. +- A misbehaving metadata provider → the registry lookup is wrapped in try/catch + and returns `undefined`, so per-frame loading is never broken. + +## Semantics (the `loadMultiframeAsPart10` knob) + +- Unset (default) or `true` → start the full-instance fetch+parse and **await + it to completion or failure** (`await prefetch.done`) — deliberately no + timeout; every frame is then served from the registry. Fetch failure resolves + `done` quickly, so unsupported servers fall straight through to per-frame. +- `false` (explicitly configured) → **disabled**; per-frame loading is the + exception, opted into per deployment. + +> `addDicomPart10Instance` registers the instance atomically (one parse), so +> registration is all-or-nothing rather than progressive. Progressive +> registration (rollout step 4) would let frames be served as they arrive. + +There is no timeout because a bounded wait loses on exactly the objects where +the prefetch matters most: a large SEG (hundreds of MB) cannot finish inside +any small cap, so a capped race expires and storms per-frame anyway while +abandoning the bulk fetch's benefit. The only failure mode a timeout would +bound — a fetch that never settles — is already covered by the browser's +network-level failure surfacing through `done`. + +## Where the generic capability lives + +- The **registry is the existing `@cornerstonejs/metadata` NATURALIZED framework** + — no new cache. New code in `@cornerstonejs/dicom-image-loader` is the thin + adapter `prefetchPart10Instance()` plus the WADO-RS read path + (`wadors/loadImageFromRegistry.ts`, wired into `wadors/loadImage.ts`). +- The **fetch + register orchestration** lives in the **data source** + (`extensions/default/.../DicomWebDataSource` → `retrieve.prefetchInstanceFrames`), + because it knows how to retrieve a full instance (auth headers, `wadoRoot`, + WADO-RS instance retrieve via `dicomweb-client`, CORS), and so alternate data + sources can implement/override it. +- The SEG handler only **reads the configuration and awaits the prefetch**. + +This keeps the layering clean: data source = "how to get bytes", image loader = +"how to register/decode bytes", SEG handler = "when to ask". + +## Open questions / risks + +1. **NATURALIZED registry key alignment.** Both registration + (`addDicomPart10Instance`) and the WADO-RS read normalize frame imageIds to the + base instance via `baseImageIdQueryFilter`, so they agree. Worth a sanity check + per data source that the SEG `imageId` passed to `prefetchInstanceFrames` and + the frame imageIds the SEG loader requests share the same instance base. +2. **Memory.** Holding the full instance plus the still-compressed frames in the + registry raises memory for that instance until it is evicted. Compressed frames + are small and decode is on-demand. Registry/cache eviction lifecycle for + prefetched instances should be verified (especially repeated SEG loads). +3. **Streaming availability.** v1 uses a single full-buffer `retrieveInstance`. A + future streaming path (step 4) would register frames as bytes arrive. +4. **Auth / headers.** Handled — the resolver sets + `wadoDicomWebClient.headers = getAuthorizationHeader()` before retrieve, reusing + the data source's existing decoration. +5. **Per-frame overhead.** Every WADO-RS load now does one cheap + `COMPRESSED_FRAME_DATA` lookup (a Map get after base normalization) before + falling through. Negligible, and only short-circuits when an instance was + actually prefetched. + +## Incremental rollout + +1. ✅ `prefetchPart10Instance` adapter + WADO-RS registry read + (`wadors/loadImageFromRegistry.ts`) in dicom-image-loader. No behaviour change + until something registers an instance. +2. ✅ `retrieve.prefetchInstanceFrames` in DicomWebDataSource (full-buffer v1 via + `retrieveInstance`); whether to call it is the caller's policy. +3. ✅ SEG handler call site. Now **enabled and awaited to completion by + default** (no timeout); per-frame loading requires an explicit + `loadMultiframeAsPart10: false`. +4. ⏳ Progressive (streaming) parse to register frames as they arrive (handle the + `multipart/related` unwrap + boundary-straddle directly). +5. ⏳ Extend to other multiframe loaders (the registry path is generic). diff --git a/platform/docs/docs/platform/services/customization-service/specificCustomizations.md b/platform/docs/docs/platform/services/customization-service/specificCustomizations.md index 6680b54e0..848c3b6e7 100644 --- a/platform/docs/docs/platform/services/customization-service/specificCustomizations.md +++ b/platform/docs/docs/platform/services/customization-service/specificCustomizations.md @@ -232,3 +232,122 @@ integration must trigger loading explicitly (for example by calling `customizationService.applyCustomizationUrlSearchParams` or `customizationService.requires` with the new list). New module keys not seen before can still be loaded that way; unloading or replacing an already-loaded pack is not supported out of the box. + +## `segmentation.store.*` (DICOM SEG export encoding) + +These customizations control how the `@ohif/extension-cornerstone-dicom-seg` extension +encodes a DICOM SEG when it is **stored or downloaded**, and are read when a SEG is +generated. + +| Key | Values | Default | Controls | +| --- | --- | --- | --- | +| `segmentation.store.defaultMode` | `'labelmap'` \| `'bitmap'` | `'labelmap'` | SEG SOP Class | +| `segmentation.store.transferSyntaxUID` | a transfer-syntax UID string | RLE Lossless (`1.2.840.10008.1.2.5`) | PixelData encoding | + +- **`segmentation.store.defaultMode`** + - `'labelmap'` → Label Map Segmentation Storage (`1.2.840.10008.5.1.4.1.1.66.7`). One + multi-valued frame per slice. Added to DICOM in 2024, so many PACS/viewers do not + accept it yet. **This is the OHIF default.** + - `'bitmap'` → (binary) Segmentation Storage (`1.2.840.10008.5.1.4.1.1.66.4`). One frame + per segment; broadly compatible with existing back ends and viewers. Opt in via + customization when needed. +- **`segmentation.store.transferSyntaxUID`** + - Default → RLE Lossless (`1.2.840.10008.1.2.5`, compressed). Set by + `getSegmentationSaveOptions` in `@ohif/extension-cornerstone-dicom-seg` and registered + as an extension customization — no app config required. + - `1.2.840.10008.1.2.1` → Explicit VR Little Endian (**uncompressed**). Opt in via + customization for back ends that reject compressed SEG PixelData. + +> OHIF's effective default is **Label Map + RLE Lossless (compressed)**. Customizations +> (or per-data-source `configuration.segmentation.store`) are only needed to switch to +> bitmap and/or uncompressed. + +### URL-loaded files (recommended) + +Two ready-made public customization files ship under +`platform/app/public/customizations/segmentation/`, so a user can flip either default +straight from the URL (requires `customizationUrlPrefixes.default` to be configured — it +is in the `dev` / `netlify` configs): + +| File | `?customization=` value | Effect | +| --- | --- | --- | +| `segmentation/uncompressed.jsonc` | `segmentation/uncompressed` | Explicit VR Little Endian instead of RLE | +| `segmentation/binary.jsonc` | `segmentation/binary` | Binary SEG (66.4) instead of Label Map (66.7) | + +They are independent and combine, so you can apply both at once: + +``` +http://host/viewer?customization=segmentation/uncompressed&customization=segmentation/binary +``` + +Each file just `$set`s one key in the `global` phase, e.g. `segmentation/binary.jsonc`: + +```jsonc +{ + "global": { + "segmentation.store.defaultMode": { "$set": "bitmap" } + } +} +``` + +The remaining forms below set the same keys directly in `window.config` instead of via the +URL. + +### Store an uncompressed SEG instead of the compressed default + +```js +window.config = { + customizationService: [ + { + 'segmentation.store.transferSyntaxUID': { + $set: '1.2.840.10008.1.2.1', // Explicit VR Little Endian (uncompressed) + }, + }, + ], +}; +``` + +### Store a binary SEG (66.4) instead of the default Label Map (66.7) + +```js +window.config = { + customizationService: [ + { + 'segmentation.store.defaultMode': { $set: 'bitmap' }, + }, + ], +}; +``` + +### Per data source override + +A data source may override the app-wide default for one back end only, under +`configuration.segmentation.store`. **The data source value wins over the customization +default**, so different back ends can pick the SEG encoding they support: + +```js +window.config = { + // App-wide default (optional): Label Map + RLE unless set here. + customizationService: [ + { 'segmentation.store.defaultMode': { $set: 'labelmap' } }, + ], + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'myPacs', + configuration: { + // ...wado/qido/stow roots... + segmentation: { + store: { + defaultMode: 'bitmap', // this PACS only accepts binary SEG + transferSyntaxUID: '1.2.840.10008.1.2.1', // and only uncompressed + }, + }, + }, + }, + ], +}; +``` + +When storing, the override is resolved against the data source being written to; for +download (no target data source) the active data source's override applies. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e6e1e5b7..19576a32d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ overrides: commander: 8.3.0 cross-env: 7.0.3 cross-spawn: 7.0.6 - dcmjs: 0.49.4 + dcmjs: 0.52.0 path-to-regexp: 0.1.13 nth-check: 2.1.1 trim-newlines: 5.0.0 @@ -190,11 +190,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/ai': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/codec-charls': specifier: 1.2.3 version: 1.2.3 @@ -208,23 +208,23 @@ importers: specifier: 2.4.7 version: 2.4.7 '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/dicom-image-loader': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21) '@cornerstonejs/labelmap-interpolation': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)) '@cornerstonejs/metadata': - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.4.12 + version: 5.4.12 '@cornerstonejs/polymorphic-segmentation': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)) '@cornerstonejs/tools': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@icr/polyseg-wasm': specifier: 0.4.0 version: 0.4.0 @@ -244,8 +244,8 @@ importers: specifier: workspace:* version: link:../../platform/ui dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -299,11 +299,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@kitware/vtk.js': specifier: 35.5.3 version: 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) @@ -388,11 +388,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@kitware/vtk.js': specifier: 35.5.3 version: 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) @@ -437,14 +437,14 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/adapters': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)) '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -458,8 +458,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -483,11 +483,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -507,8 +507,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -541,8 +541,8 @@ importers: specifier: workspace:* version: link:../../platform/i18n dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicomweb-client: specifier: 0.10.4 version: 0.10.4 @@ -571,6 +571,9 @@ importers: specifier: 1.8.11 version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -651,8 +654,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -685,8 +688,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -710,11 +713,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -734,8 +737,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 lodash.debounce: specifier: 4.0.8 version: 4.0.8 @@ -752,6 +755,9 @@ importers: specifier: 4.38.3 version: 4.38.3 devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -771,8 +777,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -805,8 +811,8 @@ importers: specifier: 2.5.1 version: 2.5.1 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -830,11 +836,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -954,6 +960,9 @@ importers: specifier: workspace:* version: link:../../extensions/dicom-video devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -985,6 +994,9 @@ importers: specifier: 17.3.1 version: 17.3.1 devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -1022,6 +1034,9 @@ importers: specifier: 17.3.1 version: 17.3.1 devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -1065,6 +1080,9 @@ importers: specifier: 17.3.1 version: 17.3.1 devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -1240,6 +1258,9 @@ importers: specifier: 17.3.1 version: 17.3.1 devDependencies: + cross-env: + specifier: 7.0.3 + version: 7.0.3 webpack-merge: specifier: 5.10.0 version: 5.10.0 @@ -1250,11 +1271,11 @@ importers: specifier: 7.29.7 version: 7.29.7 '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/tools': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@ohif/core': specifier: workspace:* version: link:../../platform/core @@ -1338,8 +1359,8 @@ importers: specifier: 2.4.7 version: 2.4.7 '@cornerstonejs/dicom-image-loader': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21) '@emotion/serialize': specifier: 1.3.3 version: 1.3.3 @@ -1371,8 +1392,8 @@ importers: specifier: 0.1.10 version: 0.1.10 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 detect-gpu: specifier: 4.0.50 version: 4.0.50 @@ -1549,14 +1570,14 @@ importers: specifier: 2.4.7 version: 2.4.7 '@cornerstonejs/core': - specifier: 5.1.3 - version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + specifier: 5.4.12 + version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@cornerstonejs/dicom-image-loader': - specifier: 5.1.3 - version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21) + specifier: 5.4.12 + version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21) '@cornerstonejs/metadata': - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.4.12 + version: 5.4.12 '@ohif/ui': specifier: workspace:* version: link:../ui @@ -1564,8 +1585,8 @@ importers: specifier: 0.1.10 version: 0.1.10 dcmjs: - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.52.0 + version: 0.52.0 dicom-parser: specifier: 1.8.21 version: 1.8.21 @@ -3325,8 +3346,8 @@ packages: resolution: {integrity: sha512-JfoPiD7f/vvd/PaOfu5cr9CyzwDMPg4T0nX3MQr6IgTq49DhjvUcmjmjA7j6+xih1Evq+QKZnge1SoIlYozv/Q==} engines: {node: '>=6.9.0'} - '@babel/runtime-corejs3@7.29.0': - resolution: {integrity: sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==} + '@babel/runtime-corejs3@7.29.2': + resolution: {integrity: sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==} engines: {node: '>=6.9.0'} '@babel/runtime@7.29.2': @@ -3368,17 +3389,17 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cornerstonejs/adapters@5.1.3': - resolution: {integrity: sha512-cA0NSsvL3tJABTsxTBgpBk7e5PMHsDEMaR0wGJ48kJ/Q9s3B5JMlYoC8Gnw89KOpxxAkgLwtmzQ6Dr1FXPHtkQ==} + '@cornerstonejs/adapters@5.4.12': + resolution: {integrity: sha512-WjNtvkxFXSlwJw2GS2oPRvt+rfZ10wHiT0xy7YubqdIqKfuB3T1RB1yziYut9RkOXYyBj7mgsZLKfddyzvwWiA==} peerDependencies: - '@cornerstonejs/core': 5.1.3 - '@cornerstonejs/tools': 5.1.3 + '@cornerstonejs/core': 5.4.12 + '@cornerstonejs/tools': 5.4.12 - '@cornerstonejs/ai@5.1.3': - resolution: {integrity: sha512-5gpgj7E6lbvoYfhEadKGZofYcihpNX2YbURM2asehQPn1VdLzewfcQsdUhdfXNwROuz6zyU795xs4ZcynvQkHQ==} + '@cornerstonejs/ai@5.4.12': + resolution: {integrity: sha512-b9HlH4rxL8G0V59h8WHMraq8dISZAf+/WJNEvz+jKJDSdsOzXoMNq+G5vrI84Y/SqsMmWP3Tca14pUi2otZDsw==} peerDependencies: - '@cornerstonejs/core': 5.1.3 - '@cornerstonejs/tools': 5.1.3 + '@cornerstonejs/core': 5.4.12 + '@cornerstonejs/tools': 5.4.12 '@cornerstonejs/calculate-suv@1.0.3': resolution: {integrity: sha512-2SwVJKzC1DzyxdxJtCht9dhTND2GFjLwhhkDyyC7vJq5tIgbhxgPk1CSwovO1pxmoybAXzjOxnaubllxLgoT+w==} @@ -3404,40 +3425,40 @@ packages: resolution: {integrity: sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w==} engines: {node: '>=0.14'} - '@cornerstonejs/core@5.1.3': - resolution: {integrity: sha512-Ly9nvO4s5ZuIta/Ozf6u8/aoAkTCX0p1LI0vkxCdyPL/5Tr4g8UuWhftDTxZKoGs99LsNLTaFz/yjpsPs09gYg==} + '@cornerstonejs/core@5.4.12': + resolution: {integrity: sha512-ieMKKmA+GsvLMePObU6C2+VyMAoMyfKqlwQiDcaneg3hF5VtMIXYY84pDMvxC45+MtatMqhlC08ubSy+eMZokA==} peerDependencies: - '@cornerstonejs/metadata': 5.1.3 - '@cornerstonejs/utils': 5.1.3 + '@cornerstonejs/metadata': 5.4.12 + '@cornerstonejs/utils': 5.4.12 - '@cornerstonejs/dicom-image-loader@5.1.3': - resolution: {integrity: sha512-aaJq8kQs6BiloCanGlrZT/5YoHKflDZ1fbz8SRV12mi4yQNf8XWhKglSm9qk2do+tIY45VwbsBFAlHU2+CMSnA==} + '@cornerstonejs/dicom-image-loader@5.4.12': + resolution: {integrity: sha512-TwJuK2YcRwqmXXNMAVTs5qF7JnL6q5WsWU2UqCbfBpXAprqXkcuWYVe9Xp7DS8kZxuQ0X+bcLdHdtiekfMEAqg==} peerDependencies: - '@cornerstonejs/core': 5.1.3 - '@cornerstonejs/metadata': 5.1.3 + '@cornerstonejs/core': 5.4.12 + '@cornerstonejs/metadata': 5.4.12 dicom-parser: 1.8.21 - '@cornerstonejs/labelmap-interpolation@5.1.3': - resolution: {integrity: sha512-xrrNW/4wG/wIoivSJeyN0y1l50PXTEfhl/OMo98h+KA09k8R19Dj4bpyqle+tcXr7MId10gPvLNP88qMkJPa7Q==} + '@cornerstonejs/labelmap-interpolation@5.4.12': + resolution: {integrity: sha512-yTjg7jp16T9+mxVvDHrwePEzA1+fvn/rQmCB/x9Ry+6J81epQEgykUsNDOP1O8r3800fDqyy0Zl/akrcF+ZYJg==} peerDependencies: - '@cornerstonejs/core': 5.1.3 - '@cornerstonejs/tools': 5.1.3 + '@cornerstonejs/core': 5.4.12 + '@cornerstonejs/tools': 5.4.12 '@kitware/vtk.js': 35.5.3 - '@cornerstonejs/metadata@5.1.3': - resolution: {integrity: sha512-U/bQEj6+JYjXjKfDTWK+ThTCeAmy+o7+0Kw+vCuKNLpM9GMhq5l7uyihWoOT8Rk4ICfJ44BAZoD+27Fo5varIg==} + '@cornerstonejs/metadata@5.4.12': + resolution: {integrity: sha512-CkTvEbdGsyMHLmrVW9c0HyYe93Vh35Ok4mhtlv7SSlfXuSjIWedVQoNpQHhjFHQmj3aHzrvibWvuUL0VI+zC4w==} - '@cornerstonejs/polymorphic-segmentation@5.1.3': - resolution: {integrity: sha512-9y7K2XRR20axzQRHT//Jkq7zN66gpn+fBvlf8nFFNLKBN5jBuUktjWbr9RAPxkD69OzmrrLbA4KoYLj5Cjlf6g==} + '@cornerstonejs/polymorphic-segmentation@5.4.12': + resolution: {integrity: sha512-mLklbspaCVwtfxus0Ou+63rKzlGWlHHTaqU1xORl4V9HyQ95RCXUde8S+86PpMoJAQ3Mq4B1+oCLgAOuCId1Cg==} peerDependencies: - '@cornerstonejs/core': 5.1.3 - '@cornerstonejs/tools': 5.1.3 + '@cornerstonejs/core': 5.4.12 + '@cornerstonejs/tools': 5.4.12 '@kitware/vtk.js': 35.5.3 - '@cornerstonejs/tools@5.1.3': - resolution: {integrity: sha512-lRR8CgzGaLurYsrJc1/oHuZUT15Iye1JV4UOSh/rl982B9yF6TSzy50TxNXDe0ckJj0nMGHGqvK4EyqoG8VUMQ==} + '@cornerstonejs/tools@5.4.12': + resolution: {integrity: sha512-ldmzgCtaVRsKjhDEMj8NIfczIAdR7KTYjH1FGPknpMU1HstZ6Eg+4AyDEz0vctyO4gW/tLXCME5ct1GhD6GiFA==} peerDependencies: - '@cornerstonejs/core': 5.1.3 + '@cornerstonejs/core': 5.4.12 '@kitware/vtk.js': 35.5.3 '@types/d3-array': 3.2.1 '@types/d3-interpolate': 3.0.4 @@ -3445,8 +3466,8 @@ packages: d3-interpolate: 3.0.1 gl-matrix: 3.4.3 - '@cornerstonejs/utils@5.1.3': - resolution: {integrity: sha512-tqIjh7Q8jJ8U7BFbIH7LcipvACgGeExo6pbOPXytdAVSPcrXQ5i5BOJsgZ/yzW5KANX2lzpx57Hx3/L6HUS4Ag==} + '@cornerstonejs/utils@5.4.12': + resolution: {integrity: sha512-ycG7nL+oaRDgp2P18d7pXFqpaNQM7Eck5THnjIQng3CfUHyg6VwOgA3zQnAiK1mlyphvIunH8/+oEq5sx2WDwQ==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -6336,8 +6357,8 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - adm-zip@0.5.16: - resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} engines: {node: '>=12.0'} agent-base@6.0.2: @@ -7045,6 +7066,10 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} + clipper2-ts@2.0.1: + resolution: {integrity: sha512-raIgNMpYN/PFYk/T9iRzaG99rf5nlXBWL1FyAdR3wjEYSzJy+pPfolLH5bCdRqCFQqn/eYUsF1jb3cQEMxmWGw==} + engines: {node: '>=14.0.0'} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -7590,8 +7615,9 @@ packages: dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - dcmjs@0.49.4: - resolution: {integrity: sha512-w77Gde5JvLjg37FyGIAsTB+oWZIqkO/5NvBeheEVKy6k9XjBgeGsoAbVAByjuGAFLpGqRbf9iWI0edBq87yu/g==} + dcmjs@0.52.0: + resolution: {integrity: sha512-0/mT9NACKnwnOnkUx7JAqmWxRcUr7HiCoptw1KhN2StNYE0Hy6+tmpI7CU+lJuBc4rFpcMCalvI8QO6JkJ+7Fg==} + engines: {node: '>=22.13'} debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -8508,6 +8534,9 @@ packages: gl-matrix@3.4.3: resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -16201,7 +16230,7 @@ snapshots: core-js: 3.45.1 regenerator-runtime: 0.14.1 - '@babel/runtime-corejs3@7.29.0': + '@babel/runtime-corejs3@7.29.2': dependencies: core-js-pure: 3.49.0 @@ -16260,23 +16289,23 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@cornerstonejs/adapters@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': + '@cornerstonejs/adapters@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': dependencies: '@babel/runtime-corejs2': 7.26.10 - '@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) - '@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + '@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) buffer: 6.0.3 - dcmjs: 0.49.4 + dcmjs: 0.52.0 gl-matrix: 3.4.3 ndarray: 1.0.19 - '@cornerstonejs/ai@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': + '@cornerstonejs/ai@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))': dependencies: '@babel/runtime-corejs2': 7.26.10 - '@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) - '@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + '@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) buffer: 6.0.3 - dcmjs: 0.49.4 + dcmjs: 0.52.0 gl-matrix: 3.4.3 lodash.clonedeep: 4.5.0 ndarray: 1.0.19 @@ -16295,10 +16324,10 @@ snapshots: '@cornerstonejs/codec-openjph@2.4.7': {} - '@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)': + '@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)': dependencies: - '@cornerstonejs/metadata': 5.1.3 - '@cornerstonejs/utils': 5.1.3 + '@cornerstonejs/metadata': 5.4.12 + '@cornerstonejs/utils': 5.4.12 '@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) comlink: 4.4.2 gl-matrix: 3.4.3 @@ -16308,24 +16337,24 @@ snapshots: - autoprefixer - wslink - '@cornerstonejs/dicom-image-loader@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21)': + '@cornerstonejs/dicom-image-loader@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21)': dependencies: '@cornerstonejs/codec-charls': 1.2.3 '@cornerstonejs/codec-libjpeg-turbo-8bit': 1.2.2 '@cornerstonejs/codec-openjpeg': 1.3.0 '@cornerstonejs/codec-openjph': 2.4.7 - '@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) - '@cornerstonejs/metadata': 5.1.3 + '@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + '@cornerstonejs/metadata': 5.4.12 comlink: 4.4.2 dicom-parser: 1.8.21 jpeg-lossless-decoder-js: 2.1.2 pako: 2.1.0 uuid: 9.0.1 - '@cornerstonejs/labelmap-interpolation@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))': + '@cornerstonejs/labelmap-interpolation@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))': dependencies: - '@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) - '@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + '@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@itk-wasm/morphological-contour-interpolation': 1.1.0 '@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) itk-wasm: 1.0.0-b.165 @@ -16333,36 +16362,37 @@ snapshots: - debug - supports-color - '@cornerstonejs/metadata@5.1.3': + '@cornerstonejs/metadata@5.4.12': dependencies: '@cornerstonejs/calculate-suv': 1.0.3 - '@cornerstonejs/utils': 5.1.3 - dcmjs: 0.49.4 + '@cornerstonejs/utils': 5.4.12 + dcmjs: 0.52.0 gl-matrix: 3.4.3 - '@cornerstonejs/polymorphic-segmentation@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))': + '@cornerstonejs/polymorphic-segmentation@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))': dependencies: - '@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) - '@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) + '@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + '@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3) '@icr/polyseg-wasm': 0.4.0 '@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) - '@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)': + '@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)': dependencies: - '@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) + '@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0) '@types/d3-array': 3.2.1 '@types/d3-interpolate': 3.0.4 '@types/offscreencanvas': 2019.7.3 + clipper2-ts: 2.0.1 comlink: 4.4.2 d3-array: 3.2.4 d3-interpolate: 3.0.1 gl-matrix: 3.4.3 lodash.get: 4.4.2 - '@cornerstonejs/utils@5.1.3': + '@cornerstonejs/utils@5.4.12': dependencies: - dcmjs: 0.49.4 + dcmjs: 0.52.0 '@cspotcode/source-map-support@0.8.1': dependencies: @@ -20293,7 +20323,7 @@ snapshots: address@1.2.2: {} - adm-zip@0.5.16: {} + adm-zip@0.5.17: {} agent-base@6.0.2: dependencies: @@ -21180,6 +21210,8 @@ snapshots: cli-width@3.0.0: {} + clipper2-ts@2.0.1: {} + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -21830,11 +21862,11 @@ snapshots: dayjs@1.11.20: {} - dcmjs@0.49.4: + dcmjs@0.52.0: dependencies: - '@babel/runtime-corejs3': 7.29.0 - adm-zip: 0.5.16 - gl-matrix: 3.4.3 + '@babel/runtime-corejs3': 7.29.2 + adm-zip: 0.5.17 + gl-matrix: 3.4.4 lodash.clonedeep: 4.5.0 loglevel: 1.9.2 ndarray: 1.0.19 @@ -22029,7 +22061,7 @@ snapshots: '@cornerstonejs/codec-openjpeg': 1.3.0 '@cornerstonejs/codec-openjph': 2.4.7 colormap: 2.3.2 - dcmjs: 0.49.4 + dcmjs: 0.52.0 dicomicc: 0.1.0 dicomweb-client: 0.10.4 image-type: 4.1.0 @@ -22255,7 +22287,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -22319,7 +22351,7 @@ snapshots: es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 es-to-primitive@1.3.0: dependencies: @@ -22882,6 +22914,8 @@ snapshots: gl-matrix@3.4.3: {} + gl-matrix@3.4.4: {} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -23560,7 +23594,7 @@ snapshots: is-core-module@2.16.2: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5c4f4c910..9db464fa4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -42,7 +42,7 @@ overrides: commander: 8.3.0 cross-env: 7.0.3 cross-spawn: 7.0.6 - dcmjs: 0.49.4 + dcmjs: 0.52.0 path-to-regexp: 0.1.13 nth-check: 2.1.1 trim-newlines: 5.0.0 diff --git a/rsbuild.config.ts b/rsbuild.config.ts index d99ac45e2..34ea904a5 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -36,6 +36,17 @@ const OHIF_OPEN = process.env.OHIF_OPEN !== 'false'; const WATCH_IGNORED = /node_modules[\\/](?!@cornerstonejs(?:[\\/]|$))/; const WATCH_AGGREGATE_TIMEOUT = Number(process.env.WATCH_AGGREGATE_TIMEOUT || 1500); +// `source-map-loader` is not a project dependency — it only serves the local +// cs3d-linking workflow (libs/@cornerstonejs, gitignored), so it is resolved +// opportunistically and the rule is skipped on installs that lack it. +const SOURCE_MAP_LOADER = (() => { + try { + return require.resolve('source-map-loader'); + } catch { + return null; + } +})(); + export default defineConfig({ dev: { lazyCompilation: false, @@ -75,6 +86,20 @@ export default defineConfig({ }, module: { rules: [ + // Consume the source maps emitted by the linked local Cornerstone + // packages (libs/@cornerstonejs, via cs3d:link + cs3d:watch) so browser + // stack traces and breakpoints resolve to the original .ts instead of + // the bundled dist/esm .js. Scoped to the linked packages only. + ...(SOURCE_MAP_LOADER + ? [ + { + test: /\.js$/, + enforce: 'pre' as const, + use: [SOURCE_MAP_LOADER], + include: /libs[\\/]@cornerstonejs[\\/]packages[\\/][^\\/]+[\\/]dist[\\/]esm/, + }, + ] + : []), { test: /\.css$/, use: [