import * as dcmjs from 'dcmjs'; import queryString from 'query-string'; import dicomParser from 'dicom-parser'; import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation'; import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData'; class MetadataProvider { constructor() { // Define the main "metadataLookup" private property as an immutable property. Object.defineProperty(this, 'studies', { configurable: false, enumerable: false, writable: false, value: new Map(), }); Object.defineProperty(this, 'imageIdToUIDs', { configurable: false, enumerable: false, writable: false, value: new Map(), }); } async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) { let dicomJSONDataset; // If Arraybuffer, parse to DICOMJSON before naturalizing. if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) { const dicomData = DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer); dicomJSONDataset = dicomData.dict; } else { dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer; } // Check if dataset is already naturalized. let naturalizedDataset; if (dicomJSONDataset['SeriesInstanceUID'] === undefined) { naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( dicomJSONDataset ); } else { naturalizedDataset = dicomJSONDataset; } const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, } = naturalizedDataset; const study = this._getAndCacheStudy(StudyInstanceUID); const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID); const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID); Object.assign(instance, naturalizedDataset); await this._checkBulkDataAndInlineBinaries(instance, options.server); return instance; } addImageIdToUIDs(imageId, uids) { // 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. this.imageIdToUIDs.set(imageId, uids); } _getAndCacheStudy(StudyInstanceUID) { const studies = this.studies; let study = studies.get(StudyInstanceUID); if (!study) { study = { series: new Map() }; studies.set(StudyInstanceUID, study); } return study; } _getAndCacheSeriesFromStudy(study, SeriesInstanceUID) { let series = study.series.get(SeriesInstanceUID); if (!series) { series = { instances: new Map() }; study.series.set(SeriesInstanceUID, series); } return series; } _getAndCacheInstanceFromStudy(series, SOPInstanceUID) { let instance = series.instances.get(SOPInstanceUID); if (!instance) { instance = {}; series.instances.set(SOPInstanceUID, instance); } return instance; } async _checkBulkDataAndInlineBinaries(instance, server) { await fetchOverlayData(instance, server); if (instance.PhotometricInterpretation === 'PALETTE COLOR') { await fetchPaletteColorLookupTableData(instance, server); } } _getInstance(imageId) { const uids = this._getUIDsFromImageID(imageId); if (!uids) { return; } const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = uids; return this._getInstanceData( StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID ); } get(query, imageId, options = { fallback: false }) { const instance = this._getInstance(imageId); if (query === INSTANCE) { return instance; } return this.getTagFromInstance(query, instance, options); } getTag(query, imageId, options) { return this.get(query, imageId, options); } getInstance(imageId) { return this.get(INSTANCE, imageId); } getTagFromInstance( naturalizedTagOrWADOImageLoaderTag, instance, options = { fallback: false } ) { if (!instance) { return; } // If its a naturalized dcmjs tag present on the instance, return. if (instance[naturalizedTagOrWADOImageLoaderTag]) { return instance[naturalizedTagOrWADOImageLoaderTag]; } // Maybe its a legacy CornerstoneWADOImageLoader tag then: return this._getCornerstoneWADOImageLoaderTag( naturalizedTagOrWADOImageLoaderTag, instance ); } _getCornerstoneWADOImageLoaderTag(wadoImageLoaderTag, instance) { let metadata; switch (wadoImageLoaderTag) { case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE: const { SeriesDate, SeriesTime } = instance; let seriesDate; let seriesTime; if (SeriesDate) { seriesDate = dicomParser.parseDA(SeriesDate); } if (SeriesTime) { seriesTime = dicomParser.parseTM(SeriesTime); } metadata = { modality: instance.Modality, seriesInstanceUID: instance.SeriesInstanceUID, seriesNumber: instance.SeriesNumber, studyInstanceUID: instance.StudyInstanceUID, seriesDate, seriesTime, }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE: metadata = { patientAge: instance.PatientAge, patientSize: instance.PatientSize, patientWeight: instance.PatientWeight, }; break; case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE: const { ImageOrientationPatient } = instance; // Fallback for DX images. // TODO: We should use the rest of the results of this function // to update the UI somehow const { PixelSpacing } = getPixelSpacingInformation(instance); let rowPixelSpacing; let columnPixelSpacing; let rowCosines; let columnCosines; if (PixelSpacing) { rowPixelSpacing = PixelSpacing[0]; columnPixelSpacing = PixelSpacing[1]; } if (ImageOrientationPatient) { rowCosines = ImageOrientationPatient.slice(0, 3); columnCosines = ImageOrientationPatient.slice(3, 6); } metadata = { frameOfReferenceUID: instance.FrameOfReferenceUID, rows: instance.Rows, columns: instance.Columns, imageOrientationPatient: ImageOrientationPatient, rowCosines, columnCosines, imagePositionPatient: instance.ImagePositionPatient, sliceThickness: instance.SliceThickness, sliceLocation: instance.SliceLocation, pixelSpacing: PixelSpacing, rowPixelSpacing, columnPixelSpacing, }; break; case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE: metadata = { samplesPerPixel: instance.SamplesPerPixel, photometricInterpretation: instance.PhotometricInterpretation, rows: instance.Rows, columns: instance.Columns, bitsAllocated: instance.BitsAllocated, bitsStored: instance.BitsStored, highBit: instance.HighBit, pixelRepresentation: instance.PixelRepresentation, planarConfiguration: instance.PlanarConfiguration, pixelAspectRatio: instance.PixelAspectRatio, smallestPixelValue: instance.SmallestPixelValue, largestPixelValue: instance.LargestPixelValue, redPaletteColorLookupTableDescriptor: instance.RedPaletteColorLookupTableDescriptor, greenPaletteColorLookupTableDescriptor: instance.GreenPaletteColorLookupTableDescriptor, bluePaletteColorLookupTableDescriptor: instance.BluePaletteColorLookupTableDescriptor, redPaletteColorLookupTableData: instance.RedPaletteColorLookupTableData, greenPaletteColorLookupTableData: instance.GreenPaletteColorLookupTableData, bluePaletteColorLookupTableData: instance.BluePaletteColorLookupTableData, }; break; case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE: const { WindowCenter, WindowWidth } = instance; const windowCenter = Array.isArray(WindowCenter) ? WindowCenter : [WindowCenter]; const windowWidth = Array.isArray(WindowWidth) ? WindowWidth : [WindowWidth]; metadata = { windowCenter, windowWidth, }; break; case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE: metadata = { rescaleIntercept: instance.RescaleIntercept, rescaleSlope: instance.RescaleSlope, rescaleType: instance.RescaleType, }; break; case WADO_IMAGE_LOADER_TAGS.SOP_COMMON_MODULE: metadata = { sopClassUID: instance.SOPClassUID, sopInstanceUID: instance.SOPInstanceUID, }; break; case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE: const { RadiopharmaceuticalInformationSequence } = instance; if (RadiopharmaceuticalInformationSequence) { const RadiopharmaceuticalInformation = Array.isArray( RadiopharmaceuticalInformationSequence ) ? RadiopharmaceuticalInformationSequence[0] : RadiopharmaceuticalInformationSequence; const { RadiopharmaceuticalStartTime, RadionuclideTotalDose, RadionuclideHalfLife, } = RadiopharmaceuticalInformation; const radiopharmaceuticalInfo = { radiopharmaceuticalStartTime: dicomParser.parseTM( RadiopharmaceuticalStartTime ), radionuclideTotalDose: RadionuclideTotalDose, radionuclideHalfLife: RadionuclideHalfLife, }; metadata = { radiopharmaceuticalInfo, }; } break; case WADO_IMAGE_LOADER_TAGS.OVERLAY_PLANE_MODULE: const overlays = []; for ( let overlayGroup = 0x00; overlayGroup <= 0x1e; overlayGroup += 0x02 ) { let groupStr = `60${overlayGroup.toString(16)}`; if (groupStr.length === 3) { groupStr = `600${overlayGroup.toString(16)}`; } const OverlayDataTag = `${groupStr}3000`; const OverlayData = instance[OverlayDataTag]; if (!OverlayData) { continue; } const OverlayRowsTag = `${groupStr}0010`; const OverlayColumnsTag = `${groupStr}0011`; const OverlayType = `${groupStr}0040`; const OverlayOriginTag = `${groupStr}0050`; const OverlayDescriptionTag = `${groupStr}0022`; const OverlayLabelTag = `${groupStr}1500`; const ROIAreaTag = `${groupStr}1301`; const ROIMeanTag = `${groupStr}1302`; const ROIStandardDeviationTag = `${groupStr}1303`; const OverlayOrigin = instance[OverlayOriginTag]; const overlay = { rows: instance[OverlayRowsTag], columns: instance[OverlayColumnsTag], type: instance[OverlayType], x: OverlayOrigin[0], y: OverlayOrigin[1], pixelData: OverlayData, description: instance[OverlayDescriptionTag], label: instance[OverlayLabelTag], roiArea: instance[ROIAreaTag], roiMean: instance[ROIMeanTag], roiStandardDeviation: instance[ROIStandardDeviationTag], }; overlays.push(overlay); } metadata = { overlays, }; break; case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE: const { PatientName } = instance; let patientName; if (PatientName) { patientName = PatientName.Alphabetic; } metadata = { patientName, patientId: instance.PatientID, }; break; case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE: metadata = { instanceNumber: instance.InstanceNumber, lossyImageCompression: instance.LossyImageCompression, lossyImageCompressionRatio: instance.LossyImageCompressionRatio, lossyImageCompressionMethod: instance.LossyImageCompressionMethod, }; break; case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE: metadata = { studyDescription: instance.StudyDescription, studyDate: instance.StudyDate, studyTime: instance.StudyTime, accessionNumber: instance.AccessionNumber, }; break; case WADO_IMAGE_LOADER_TAGS.CINE_MODULE: metadata = { frameTime: instance.FrameTime, }; break; } return metadata; } _getInstanceData(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) { const study = this.studies.get(StudyInstanceUID); if (!study) { return; } const series = study.series.get(SeriesInstanceUID); if (!series) { return; } const instance = series.instances.get(SOPInstanceUID); return instance; } _getUIDsFromImageID(imageId) { if (imageId.includes('wadors:')) { const strippedImageId = imageId.split('studies/')[1]; const splitImageId = strippedImageId.split('/'); return { StudyInstanceUID: splitImageId[0], // Note: splitImageId[1] === 'series' SeriesInstanceUID: splitImageId[2], // Note: splitImageId[3] === 'instances' SOPInstanceUID: splitImageId[4], }; } if (imageId.includes('wado?requestType=WADO')) { const qs = queryString.parse(imageId); return { StudyInstanceUID: qs.studyUID, SeriesInstanceUID: qs.seriesUID, SOPInstanceUID: qs.objectUID, }; } else { // Maybe its a non-standard imageId return this.imageIdToUIDs.get(imageId); } } } const metadataProvider = new MetadataProvider(); export default metadataProvider; const WADO_IMAGE_LOADER_TAGS = { // CornerstoneWADOImageLoader specific GENERAL_SERIES_MODULE: 'generalSeriesModule', PATIENT_STUDY_MODULE: 'patientStudyModule', IMAGE_PLANE_MODULE: 'imagePlaneModule', IMAGE_PIXEL_MODULE: 'imagePixelModule', VOI_LUT_MODULE: 'voiLutModule', MODALITY_LUT_MODULE: 'modalityLutModule', SOP_COMMON_MODULE: 'sopCommonModule', PET_ISOTOPE_MODULE: 'petIsotopeModule', OVERLAY_PLANE_MODULE: 'overlayPlaneModule', // react-cornerstone-viewport specifc PATIENT_MODULE: 'patientModule', GENERAL_IMAGE_MODULE: 'generalImageModule', GENERAL_STUDY_MODULE: 'generalStudyModule', CINE_MODULE: 'cineModule', }; const INSTANCE = 'instance';