Fix contour annotations RTSS saving (#5545)
* [WIP] Fix contour annotations RTSS saving * fix: Export image sop image reference provider * Fixes for download with filename * Updates to fix metadata issues with CS3D * Updates to save menu * Fixing some additional re-save metadata * Add support for saving with predecessor image sequence * PR comment fixes * fix: Load RTSS after save * fix segmentation save * fix load of mixed seg/rt studies * Use frame module to get information on the frame shown * Fix study browser to use instances.length when numImageFrames not present * fix: Load of display set split from multiple non-FOR referenes * Fix RT and SEG applying to wrong series because of using referenced series sequence without checkout sop instances * Fix save seg * Update to released CS3D versions * test: Add revokeObjectURL to test download blob * Add hard dependency on mode-basic to fix build order issues * fix: build * Try to fix build segmentation * PR comments * PR comment update
This commit is contained in:
1 parent
9c5043ca2c
commit
57205703bb
68 files changed
+962
-648
No files matched your search
@@ -53,7 +53,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "1.3.0",
|
||||
"@cornerstonejs/codec-openjph": "2.4.7",
|
||||
"@cornerstonejs/dicom-image-loader": "4.11.2",
|
||||
"@cornerstonejs/dicom-image-loader": "4.11.3",
|
||||
"@emotion/serialize": "1.3.3",
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/extension-cornerstone": "3.12.0-beta.90",
|
||||
@@ -79,7 +79,7 @@
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.45.1",
|
||||
"cornerstone-math": "0.1.10",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"detect-gpu": "4.0.50",
|
||||
"dicom-parser": "1.8.21",
|
||||
"dotenv-webpack": "1.8.0",
|
||||
|
||||
@@ -126,6 +126,29 @@ window.config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
friendlyName: 'StaticWado default data',
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: '/dicomweb',
|
||||
qidoRoot: '/dicomweb',
|
||||
wadoRoot: '/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy',
|
||||
|
||||
@@ -37,15 +37,15 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "1.3.0",
|
||||
"@cornerstonejs/codec-openjph": "2.4.7",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/dicom-image-loader": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@cornerstonejs/dicom-image-loader": "4.11.3",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "1.8.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicomweb-client": "0.10.4",
|
||||
"gl-matrix": "3.4.3",
|
||||
"immutability-helper": "3.1.1",
|
||||
|
||||
@@ -6,7 +6,7 @@ import DicomMetadataStore from '../services/DicomMetadataStore';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import toNumber from '../utils/toNumber';
|
||||
import combineFrameInstance from '../utils/combineFrameInstance';
|
||||
import formatPN from '../utils/formatPN';
|
||||
|
||||
const { calibratedPixelSpacingMetadataProvider, getPixelSpacingInformation } = utilities;
|
||||
|
||||
class MetadataProvider {
|
||||
@@ -123,33 +123,21 @@ class MetadataProvider {
|
||||
|
||||
switch (wadoImageLoaderTag) {
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE:
|
||||
const { SeriesDate, SeriesTime } = instance;
|
||||
|
||||
let seriesDate;
|
||||
let seriesTime;
|
||||
|
||||
if (SeriesDate) {
|
||||
seriesDate = dicomParser.parseDA(SeriesDate);
|
||||
}
|
||||
|
||||
if (SeriesTime) {
|
||||
seriesTime = dicomParser.parseTM(SeriesTime);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
modality: instance.Modality,
|
||||
seriesInstanceUID: instance.SeriesInstanceUID,
|
||||
seriesNumber: toNumber(instance.SeriesNumber),
|
||||
studyInstanceUID: instance.StudyInstanceUID,
|
||||
seriesDate,
|
||||
seriesTime,
|
||||
seriesDescription: instance.SeriesDescription,
|
||||
seriesDate: instance.SeriesDate,
|
||||
seriesTime: instance.SeriesTime,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
||||
metadata = {
|
||||
patientAge: toNumber(instance.PatientAge),
|
||||
patientSize: toNumber(instance.PatientSize),
|
||||
patientWeight: toNumber(instance.PatientWeight),
|
||||
patientAge: instance.PatientAge,
|
||||
patientSize: instance.PatientSize,
|
||||
patientWeight: instance.PatientWeight,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_DEMOGRAPHIC_MODULE:
|
||||
@@ -346,16 +334,13 @@ class MetadataProvider {
|
||||
break;
|
||||
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE:
|
||||
const { PatientName } = instance;
|
||||
|
||||
let patientName;
|
||||
if (PatientName) {
|
||||
patientName = formatPN(PatientName);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
patientName,
|
||||
patientName: instance.PatientName,
|
||||
patientId: instance.PatientID,
|
||||
patientSex: instance.PatientSex,
|
||||
patientBirthDate: instance.PatientBirthDate,
|
||||
issuerOfPatientId: instance.IssuerOfPatientID,
|
||||
otherPatientIDsSequence: instance.OtherPatientIDsSequence,
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -373,9 +358,11 @@ class MetadataProvider {
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE:
|
||||
metadata = {
|
||||
studyDescription: instance.StudyDescription,
|
||||
studyInstanceUID: instance.StudyInstanceUID,
|
||||
studyDate: instance.StudyDate,
|
||||
studyTime: instance.StudyTime,
|
||||
accessionNumber: instance.AccessionNumber,
|
||||
studyId: instance.StudyID,
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -512,9 +499,57 @@ class MetadataProvider {
|
||||
|
||||
const metadataProvider = new MetadataProvider();
|
||||
|
||||
DicomMetadataStore.setMetaDataProvider(metadataProvider);
|
||||
|
||||
export default metadataProvider;
|
||||
|
||||
const WADO_IMAGE_LOADER_TAGS = {
|
||||
// dicomImageLoader specific
|
||||
GENERAL_SERIES_MODULE: 'generalSeriesModule',
|
||||
PATIENT_STUDY_MODULE: 'patientStudyModule',
|
||||
IMAGE_PIXEL_MODULE: 'imagePixelModule',
|
||||
VOI_LUT_MODULE: 'voiLutModule',
|
||||
MODALITY_LUT_MODULE: 'modalityLutModule',
|
||||
SOP_COMMON_MODULE: 'sopCommonModule',
|
||||
PET_IMAGE_MODULE: 'petImageModule',
|
||||
PET_ISOTOPE_MODULE: 'petIsotopeModule',
|
||||
PET_SERIES_MODULE: 'petSeriesModule',
|
||||
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
|
||||
PATIENT_DEMOGRAPHIC_MODULE: 'patientDemographicModule',
|
||||
|
||||
// react-cornerstone-viewport specific
|
||||
PATIENT_MODULE: 'patientModule',
|
||||
GENERAL_IMAGE_MODULE: 'generalImageModule',
|
||||
GENERAL_STUDY_MODULE: 'generalStudyModule',
|
||||
CINE_MODULE: 'cineModule',
|
||||
CALIBRATION_MODULE: 'calibrationModule',
|
||||
|
||||
// Computed tags for new data
|
||||
// Note these get returned in naturalized format
|
||||
IMAGE_SOP_INSTANCE_REFERENCE: 'ImageSopInstanceReference',
|
||||
};
|
||||
|
||||
const WADO_IMAGE_LOADER = {
|
||||
/** Returns information on the current frame reference */
|
||||
frameModule: instance => {
|
||||
const {
|
||||
frameNumber = 1,
|
||||
numberOfFrames = 1,
|
||||
SOPClassUID: sopClassUID,
|
||||
SOPInstanceUID: sopInstanceUID,
|
||||
SeriesInstanceUID: seriesInstanceUID,
|
||||
StudyInstanceUID: studyInstanceUID,
|
||||
} = instance;
|
||||
return {
|
||||
frameNumber,
|
||||
numberOfFrames,
|
||||
sopClassUID,
|
||||
sopInstanceUID,
|
||||
seriesInstanceUID,
|
||||
studyInstanceUID,
|
||||
};
|
||||
},
|
||||
|
||||
imagePlaneModule: instance => {
|
||||
const { ImageOrientationPatient, ImagePositionPatient } = instance;
|
||||
|
||||
@@ -590,26 +625,4 @@ const WADO_IMAGE_LOADER = {
|
||||
},
|
||||
};
|
||||
|
||||
const WADO_IMAGE_LOADER_TAGS = {
|
||||
// dicomImageLoader specific
|
||||
GENERAL_SERIES_MODULE: 'generalSeriesModule',
|
||||
PATIENT_STUDY_MODULE: 'patientStudyModule',
|
||||
IMAGE_PIXEL_MODULE: 'imagePixelModule',
|
||||
VOI_LUT_MODULE: 'voiLutModule',
|
||||
MODALITY_LUT_MODULE: 'modalityLutModule',
|
||||
SOP_COMMON_MODULE: 'sopCommonModule',
|
||||
PET_IMAGE_MODULE: 'petImageModule',
|
||||
PET_ISOTOPE_MODULE: 'petIsotopeModule',
|
||||
PET_SERIES_MODULE: 'petSeriesModule',
|
||||
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
|
||||
PATIENT_DEMOGRAPHIC_MODULE: 'patientDemographicModule',
|
||||
|
||||
// react-cornerstone-viewport specific
|
||||
PATIENT_MODULE: 'patientModule',
|
||||
GENERAL_IMAGE_MODULE: 'generalImageModule',
|
||||
GENERAL_STUDY_MODULE: 'generalStudyModule',
|
||||
CINE_MODULE: 'cineModule',
|
||||
CALIBRATION_MODULE: 'calibrationModule',
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
@@ -50,6 +50,12 @@ const _model = {
|
||||
studies: [],
|
||||
};
|
||||
|
||||
let metaDataProvider;
|
||||
|
||||
function _setMetaDataProvider(metaData) {
|
||||
metaDataProvider = metaData;
|
||||
}
|
||||
|
||||
function _getStudyInstanceUIDs() {
|
||||
return _model.studies.map(aStudy => aStudy.StudyInstanceUID);
|
||||
}
|
||||
@@ -102,7 +108,17 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
return series.getInstance(SOPInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the frame module from the OHIF metadata provider, and then
|
||||
* uses the study/series/instance uids to get the instance data.
|
||||
*/
|
||||
function _getInstanceByImageId(imageId) {
|
||||
const metadataResult = metaDataProvider?.get('frameModule', imageId);
|
||||
if (metadataResult) {
|
||||
const { studyInstanceUID, seriesInstanceUID, sopInstanceUID } = metadataResult;
|
||||
return _getInstance(studyInstanceUID, seriesInstanceUID, sopInstanceUID);
|
||||
}
|
||||
console.warn('No metadata result found for', imageId, 'looking through instances');
|
||||
for (const study of _model.studies) {
|
||||
for (const series of study.series) {
|
||||
for (const instance of series.instances) {
|
||||
@@ -186,7 +202,8 @@ const BaseImplementation = {
|
||||
study = _model.studies[_model.studies.length - 1];
|
||||
}
|
||||
|
||||
naturalizedDataset = numberOfFrames > 1 ? addProxyFields(naturalizedDataset) : naturalizedDataset;
|
||||
naturalizedDataset =
|
||||
numberOfFrames > 1 ? addProxyFields(naturalizedDataset) : naturalizedDataset;
|
||||
|
||||
study.addInstanceToSeries(naturalizedDataset);
|
||||
},
|
||||
@@ -289,6 +306,7 @@ const BaseImplementation = {
|
||||
getInstance: _getInstance,
|
||||
getInstanceByImageId: _getInstanceByImageId,
|
||||
updateMetadataForSeries: _updateMetadataForSeries,
|
||||
setMetaDataProvider: _setMetaDataProvider,
|
||||
};
|
||||
const DicomMetadataStore = Object.assign(
|
||||
// get study
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { DisplaySet, InstanceMetadata } from '../../types';
|
||||
import { DisplaySet, InstanceMetadata, ReferencedSeriesSequence } from '../../types';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import EVENTS from './EVENTS';
|
||||
|
||||
@@ -115,16 +115,52 @@ export default class DisplaySetService extends PubSubService {
|
||||
return this.activeDisplaySets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the set of display sets with this series instance UID
|
||||
*
|
||||
* <b>WARNING: Do not use this method when you have a referenced series sequence
|
||||
* as this method does NOT check sop instances. Instead, use getDisplaySetsForReference
|
||||
* to get those with the correct sop instances in them.</b>
|
||||
*/
|
||||
public getDisplaySetsForSeries = (seriesInstanceUID: string): DisplaySet[] => {
|
||||
return [...displaySetCache.values()].filter(
|
||||
displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a reference to a series/sop, returns the set of display sets
|
||||
* containing an instance from the references.
|
||||
*/
|
||||
public getDisplaySetsForReferences = (
|
||||
references: ReferencedSeriesSequence | ReferencedSeriesSequence[]
|
||||
): DisplaySet[] => {
|
||||
const mapSeriesReferences = new Map<string, Set<string>>();
|
||||
const referenceArr = Array.isArray(references) ? references : [references];
|
||||
for (const seriesRef of referenceArr) {
|
||||
const { SeriesInstanceUID, ReferencedInstanceSequence } = seriesRef;
|
||||
if (!mapSeriesReferences.has(SeriesInstanceUID)) {
|
||||
mapSeriesReferences.set(SeriesInstanceUID, new Set<string>());
|
||||
}
|
||||
const sops = mapSeriesReferences.get(SeriesInstanceUID);
|
||||
for (const sopReference of ReferencedInstanceSequence) {
|
||||
sops.add(sopReference.ReferencedSOPInstanceUID);
|
||||
}
|
||||
}
|
||||
|
||||
return [...displaySetCache.values()].filter(displaySet => {
|
||||
const sopReferences = mapSeriesReferences.get(displaySet.SeriesInstanceUID);
|
||||
if (!sopReferences || !displaySet.instances) {
|
||||
return;
|
||||
}
|
||||
return displaySet.instances.some(instance => sopReferences.has(instance.SOPInstanceUID));
|
||||
});
|
||||
};
|
||||
|
||||
public getDisplaySetForSOPInstanceUID(
|
||||
sopInstanceUID: string,
|
||||
seriesInstanceUID: string,
|
||||
frameNumber?: number
|
||||
_frameNumber?: number
|
||||
): DisplaySet {
|
||||
const displaySets = seriesInstanceUID
|
||||
? this.getDisplaySetsForSeries(seriesInstanceUID)
|
||||
|
||||
@@ -434,10 +434,13 @@ class MeasurementService extends PubSubService {
|
||||
|
||||
const internalUID = data.uid || guid();
|
||||
|
||||
const annotationData = data.annotation.data;
|
||||
const {
|
||||
annotation: { predecessorImageId, data: annotationData },
|
||||
} = data;
|
||||
|
||||
const newMeasurement = {
|
||||
finding: annotationData.finding,
|
||||
predecessorImageId,
|
||||
findingSites: annotationData.findingSites,
|
||||
site: annotationData.findingSites?.[0],
|
||||
...measurement,
|
||||
@@ -643,8 +646,12 @@ class MeasurementService extends PubSubService {
|
||||
if (referencedImageId) {
|
||||
measurement.referencedImageId = referencedImageId;
|
||||
const instance = DicomMetadataStore.getInstanceByImageId(referencedImageId);
|
||||
measurement.referenceStudyUID = instance.StudyInstanceUID;
|
||||
measurement.referenceSeriesUID = instance.SeriesInstanceUID;
|
||||
if (!instance) {
|
||||
console.warn("Didn't find instance for", referencedImageId);
|
||||
} else {
|
||||
measurement.referenceStudyUID = instance.StudyInstanceUID;
|
||||
measurement.referenceSeriesUID = instance.SeriesInstanceUID;
|
||||
}
|
||||
}
|
||||
|
||||
if (displaySetInstanceUID) {
|
||||
@@ -683,8 +690,7 @@ class MeasurementService extends PubSubService {
|
||||
removeMany(measurementUIDs: string[]): void {
|
||||
const measurements = [];
|
||||
for (const measurementUID of measurementUIDs) {
|
||||
const measurement =
|
||||
this.measurements.get(measurementUID)
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurementUID || !measurement) {
|
||||
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
||||
@@ -727,8 +733,7 @@ class MeasurementService extends PubSubService {
|
||||
*/
|
||||
|
||||
public jumpToMeasurement(viewportId: string, measurementUID: string): void {
|
||||
const measurement =
|
||||
this.measurements.get(measurementUID)
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
log.warn(`No measurement uid, or unable to find by uid.`);
|
||||
@@ -850,8 +855,7 @@ class MeasurementService extends PubSubService {
|
||||
};
|
||||
|
||||
public toggleLockMeasurement(measurementUID: string): void {
|
||||
const measurement =
|
||||
this.measurements.get(measurementUID)
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||
@@ -868,8 +872,7 @@ class MeasurementService extends PubSubService {
|
||||
}
|
||||
|
||||
public toggleVisibilityMeasurement(measurementUID: string, visibility?: boolean): void {
|
||||
const measurement =
|
||||
this.measurements.get(measurementUID)
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||
@@ -893,8 +896,7 @@ class MeasurementService extends PubSubService {
|
||||
}
|
||||
|
||||
public updateColorMeasurement(measurementUID: string, color: number[]): void {
|
||||
const measurement =
|
||||
this.measurements.get(measurementUID)
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { InstanceMetadata } from './StudyMetadata';
|
||||
|
||||
export type ReferencedInstance = {
|
||||
ReferencedSOPClassUID: string;
|
||||
ReferencedSOPInstanceUID: string;
|
||||
};
|
||||
|
||||
export type ReferencedSeriesSequence = {
|
||||
SeriesInstanceUID: string;
|
||||
ReferencedInstanceSequence: ReferencedInstance[];
|
||||
};
|
||||
|
||||
export type DisplaySet = {
|
||||
displaySetInstanceUID: string;
|
||||
instances: InstanceMetadata[];
|
||||
@@ -16,6 +26,8 @@ export type DisplaySet = {
|
||||
label?: string;
|
||||
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
|
||||
isOverlayDisplaySet?: boolean;
|
||||
/** Flag indicating this is a derived dataset */
|
||||
isDerived?: boolean;
|
||||
/** flag indicating if it supports window level */
|
||||
supportsWindowLevel?: boolean;
|
||||
|
||||
@@ -49,6 +61,21 @@ export type DisplaySet = {
|
||||
SeriesTime?: string;
|
||||
instance?: InstanceMetadata;
|
||||
|
||||
/**
|
||||
* The predecessor image id refers to the SOP instance that is currently loaded
|
||||
* into this display set for SEG/SR/RTSTRUCT type values. The name is chosen
|
||||
* for consistency when this value is used as the origin instance
|
||||
* for saving a new instance intended to replace this instance where the
|
||||
* new instance has a "predecessor sequence".
|
||||
*/
|
||||
predecessorImageId?: string;
|
||||
|
||||
/**
|
||||
* isLoaded is used for display sets containing a load operation that
|
||||
* is required before the display set can be shown. This is separate from
|
||||
* isHydrated, which means it is loaded into view.
|
||||
*/
|
||||
isLoaded?: boolean;
|
||||
isHydrated?: boolean;
|
||||
isRehydratable?: boolean;
|
||||
};
|
||||
|
||||
@@ -44,6 +44,13 @@ export function addProxyFields(instance) {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
return instance._parentInstance?.[fieldProxy] ?? instance._parentInstance?._shared?.[fieldProxy];
|
||||
},
|
||||
set: (value) => {
|
||||
Object.defineProperty(instance,fieldProxy, {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
value,
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Converts a blob to a URL and downloads immediate
|
||||
*/
|
||||
export function downloadBlob(content, options?) {
|
||||
const url = URL.createObjectURL(content);
|
||||
downloadUrl(url, options);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger file download from an array buffer
|
||||
* @param buffer
|
||||
* @param filename
|
||||
*/
|
||||
export function downloadDicom(buffer: ArrayBuffer, options) {
|
||||
const blob = new Blob([buffer], { type: 'application/dicom' });
|
||||
downloadBlob(blob, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a URL
|
||||
*/
|
||||
export function downloadUrl(url, options?) {
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
const filename = options?.filename || 'file.dcm';
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
export function downloadCsv(csvString: string, options?) {
|
||||
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
|
||||
downloadBlob(blob, options);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { downloadUrl } from './downloadBlob';
|
||||
import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore';
|
||||
import formatPN from './formatPN';
|
||||
|
||||
@@ -97,10 +98,5 @@ function _getCommonRowItems(measurement, seriesMetadata) {
|
||||
|
||||
function _createAndDownloadFile(csvContent) {
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', encodedUri);
|
||||
link.setAttribute('download', 'MeasurementReport.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
downloadUrl(encodedUri, { filename: 'MeasurementReport.csv' });
|
||||
}
|
||||
@@ -48,6 +48,8 @@ import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
|
||||
import { structuredCloneWithFunctions } from './structuredCloneWithFunctions';
|
||||
import { buildButtonCommands } from './buildButtonCommands';
|
||||
|
||||
import { downloadBlob, downloadUrl, downloadCsv, downloadDicom } from './downloadBlob';
|
||||
|
||||
// Commented out unused functionality.
|
||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||
|
||||
@@ -99,6 +101,10 @@ const utils = {
|
||||
getClosestOrientationFromIOP,
|
||||
calculateScanAxisNormal,
|
||||
areAllImageOrientationsEqual,
|
||||
downloadBlob,
|
||||
downloadUrl,
|
||||
downloadCsv,
|
||||
downloadDicom,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -137,6 +143,10 @@ export {
|
||||
MeasurementFilters,
|
||||
getClosestOrientationFromIOP,
|
||||
buildButtonCommands,
|
||||
downloadBlob,
|
||||
downloadUrl,
|
||||
downloadCsv,
|
||||
downloadDicom,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -24,3 +24,45 @@ Using the class text size with the ui-button is inconsistent as to whether
|
||||
it will apply or not. Instead, create a new size value to assign the desired size.
|
||||
To support this, a new size enum is created, smallTall, which is used in the worklist
|
||||
for an over-ride.
|
||||
|
||||
## `createReportDialogPrompt`
|
||||
|
||||
The create report dialog prompt (which is MOSTLY an internal component) has
|
||||
change the API a bit. The input is now:
|
||||
|
||||
- `title` shown in the dialog
|
||||
- `modality` being stored, used to query existing series
|
||||
- `minSeriesNumber` is the start of new series of this modality type.
|
||||
Will get set to 4000 if not determined by the modality
|
||||
- predecessorImageId is the image id that this series was currently loaded
|
||||
from. That allows defaulting the dialog to show the specified series instead
|
||||
of always creating a new series.
|
||||
|
||||
The minSeries and predecessor are both optional, so the input doesn't have to be
|
||||
updated.
|
||||
|
||||
The output has been enhanced with:
|
||||
|
||||
- `series`, is the series to store do, as referenced by a predecessorImageId value.
|
||||
This allows exactly specifying which item to replace, which allows selecting it in the
|
||||
menu by default instead of just guessing what value is being replaced.
|
||||
- `priorSeriesNumber` is the previously lowest series number at least minSeriesNumber
|
||||
of all the seris of the given modality type. This allows adding an instance to the
|
||||
`next` series number by adding 1 to this value.
|
||||
|
||||
The priorSeriesNumber will default to 4000 for an unknown modality type, or
|
||||
3000 for sr, 3100 for seg and 3200 for rtstruct.
|
||||
|
||||
## metadataProvider and formatted metadata
|
||||
|
||||
The metadata provider has been formatting some fields, which causes inconsistency
|
||||
between different metadata providers and use of the instance object. The
|
||||
specific fields that have changed are:
|
||||
|
||||
- `patientName`
|
||||
- `studyDate`, `studyTime`
|
||||
- `seriesDate`, `seriesTime`
|
||||
|
||||
If these fields need formatted versions, it is recommended to add a secondary/computed
|
||||
metadata provider which simply gets the base metadata module and adds the
|
||||
formatting. That way different metadata providers are all handled identically.
|
||||
+25
@@ -39,6 +39,31 @@ return {
|
||||
}
|
||||
```
|
||||
|
||||
## Replacing Instances in Series
|
||||
|
||||
Series such as segmentations and annotations often have the concept of replacing
|
||||
an earlier instance with a newer one. The official way to do this is using the
|
||||
`predecessor document sequence` to reference the old object in the new one.
|
||||
It isn't that the old one is deleted (never delete data), but rather that the new
|
||||
one is used in preference to the old one. This results in code that looks like:
|
||||
|
||||
```
|
||||
utils.sortStudyInstances(instances);
|
||||
// Choose the LAST instance in the list as the most recently created one.
|
||||
const instance = instances[instances.length - 1];
|
||||
|
||||
...
|
||||
const displaySet = {
|
||||
...
|
||||
predecessorImageId: instance.imageId,
|
||||
numImageFrames: instances.length,
|
||||
}
|
||||
```
|
||||
|
||||
so that the last/latest SOP instance is used by default, and that it is
|
||||
recorded what imageId was used to load that instance. This then allows saving
|
||||
new instances in the same series by using a new predecessor sequence.
|
||||
|
||||
## Example SOP Class Handler Module
|
||||
|
||||
```js
|
||||
|
||||
Reference in new issue
Block a user