From c33680516e0931833f04785304cca746968aff08 Mon Sep 17 00:00:00 2001 From: pbaetz99 <128055813+pbaetz99@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:35:12 +0200 Subject: [PATCH] fix(overlay): read overlay planes that dcmjs names by keyword Since dcmjs 0.50, naturalizeDataset names group 60xx tags by keyword (OverlayData, OverlayRows, ...) instead of keeping the hex tag as the key. The overlayPlaneModule handler in MetadataProvider reads only hex keys such as '60003000'. With dcmjs 0.52.0 (bumped in #5806) it returns no overlays, and ImageOverlayViewer never loads the overlay bits. Some CT scanners store their protocol page as a Secondary Capture image with zero pixel data and the text in overlay 6000; OHIF now shows it black. Read the keyword when the hex key is missing. dcmjs gives every overlay group the same keywords, so the handler reads them as group 6000 only. The hex-key path used with dcmjs < 0.50 is unchanged. --- .../core/src/classes/MetadataProvider.test.ts | 50 +++++++++++++++++++ platform/core/src/classes/MetadataProvider.ts | 36 +++++++------ 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/platform/core/src/classes/MetadataProvider.test.ts b/platform/core/src/classes/MetadataProvider.test.ts index c61e57a9ec1..512bdef1fc2 100644 --- a/platform/core/src/classes/MetadataProvider.test.ts +++ b/platform/core/src/classes/MetadataProvider.test.ts @@ -107,4 +107,54 @@ describe('MetadataProvider', () => { }); }); }); + + describe('the overlay plane module', () => { + it('reads an overlay that dcmjs named by keyword', () => { + // What dcmjs >= 0.50 makes of group 6000 in DICOMweb metadata. + const instance = { + OverlayRows: 512, + OverlayColumns: 256, + OverlayType: 'G', + OverlayOrigin: [1, 1], + OverlayBitsAllocated: 1, + OverlayBitPosition: 0, + OverlayData: { BulkDataURI: 'http://localhost/bulk/overlay-6000' }, + }; + + const { overlays } = metadataProvider.getTagFromInstance('overlayPlaneModule', instance); + + expect(overlays).toHaveLength(1); + expect(overlays[0]).toMatchObject({ + rows: 512, + columns: 256, + type: 'G', + x: 1, + y: 1, + pixelData: { BulkDataURI: 'http://localhost/bulk/overlay-6000' }, + }); + }); + + it('reads overlays that dcmjs kept under their tag', () => { + // dcmjs < 0.50 keeps the hex tag as the key. + const instance = { + '60000010': 512, + '60000011': 256, + '60000040': 'G', + '60000050': [1, 1], + '60003000': { BulkDataURI: 'http://localhost/bulk/overlay-6000' }, + '60020010': 64, + '60020011': 32, + '60020040': 'R', + '60020050': [5, 7], + '60023000': { BulkDataURI: 'http://localhost/bulk/overlay-6002' }, + }; + + const { overlays } = metadataProvider.getTagFromInstance('overlayPlaneModule', instance); + + expect(overlays).toMatchObject([ + { rows: 512, columns: 256, type: 'G', x: 1, y: 1 }, + { rows: 64, columns: 32, type: 'R', x: 5, y: 7 }, + ]); + }); + }); }); diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts index b9d2c2a0607..a03cb34bebf 100644 --- a/platform/core/src/classes/MetadataProvider.ts +++ b/platform/core/src/classes/MetadataProvider.ts @@ -279,8 +279,14 @@ class MetadataProvider { groupStr = `600${overlayGroup.toString(16)}`; } + // dcmjs >= 0.50 names 60xx tags by keyword (OverlayData, OverlayRows, ...) instead of + // by tag. All overlay groups share these keywords, so dcmjs keeps at most one overlay + // under them; it is read as group 6000. + const getValue = (tag: string, keyword: string) => + instance[tag] ?? (overlayGroup === 0x00 ? instance[keyword] : undefined); + const OverlayDataTag = `${groupStr}3000`; - const OverlayData = instance[OverlayDataTag]; + const OverlayData = getValue(OverlayDataTag, 'OverlayData'); if (!OverlayData) { continue; @@ -295,24 +301,26 @@ class MetadataProvider { const ROIAreaTag = `${groupStr}1301`; const ROIMeanTag = `${groupStr}1302`; const ROIStandardDeviationTag = `${groupStr}1303`; - const OverlayOrigin = instance[OverlayOriginTag]; + const OverlayOrigin = getValue(OverlayOriginTag, 'OverlayOrigin'); + const OverlayRows = getValue(OverlayRowsTag, 'OverlayRows'); + const OverlayColumns = getValue(OverlayColumnsTag, 'OverlayColumns'); let rows = 0; - if (instance[OverlayRowsTag] instanceof Array) { + if (OverlayRows instanceof Array) { // The DICOM VR for overlay rows is US (unsigned short). - const rowsInt16Array = new Uint16Array(instance[OverlayRowsTag][0]); + const rowsInt16Array = new Uint16Array(OverlayRows[0]); rows = rowsInt16Array[0]; } else { - rows = instance[OverlayRowsTag]; + rows = OverlayRows; } let columns = 0; - if (instance[OverlayColumnsTag] instanceof Array) { + if (OverlayColumns instanceof Array) { // The DICOM VR for overlay columns is US (unsigned short). - const columnsInt16Array = new Uint16Array(instance[OverlayColumnsTag][0]); + const columnsInt16Array = new Uint16Array(OverlayColumns[0]); columns = columnsInt16Array[0]; } else { - columns = instance[OverlayColumnsTag]; + columns = OverlayColumns; } let x = 0; @@ -330,15 +338,15 @@ class MetadataProvider { const overlay = { rows: rows, columns: columns, - type: instance[OverlayType], + type: getValue(OverlayType, 'OverlayType'), x, y, pixelData: OverlayData, - description: instance[OverlayDescriptionTag], - label: instance[OverlayLabelTag], - roiArea: instance[ROIAreaTag], - roiMean: instance[ROIMeanTag], - roiStandardDeviation: instance[ROIStandardDeviationTag], + description: getValue(OverlayDescriptionTag, 'OverlayDescription'), + label: getValue(OverlayLabelTag, 'OverlayLabel'), + roiArea: getValue(ROIAreaTag, 'ROIArea'), + roiMean: getValue(ROIMeanTag, 'ROIMean'), + roiStandardDeviation: getValue(ROIStandardDeviationTag, 'ROIStandardDeviation'), }; overlays.push(overlay);