fix: OHIF 3.9 Recovery 3 (#4697)

Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
Co-authored-by: Chao Sui <chao.sui@curvebeamai.com>
Co-authored-by: Jaseel-trenser <108331536+Jaseel-trenser@users.noreply.github.com>
This commit is contained in:
Alireza 2025-01-21 10:07:43 -05:00 committed by GitHub
parent b19d7af9ff
commit 5d4f2e2a8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 238 additions and 204 deletions

View File

@ -68,7 +68,7 @@
"prettier-plugin-tailwindcss": "^0.5.4", "prettier-plugin-tailwindcss": "^0.5.4",
"react-refresh": "^0.14.2", "react-refresh": "^0.14.2",
"semver": "^7.5.1", "semver": "^7.5.1",
"serve": "^14.2.0", "serve": "^14.2.4",
"shader-loader": "^1.3.1", "shader-loader": "^1.3.1",
"shx": "^0.3.3", "shx": "^0.3.3",
"source-map-loader": "^4.0.1", "source-map-loader": "^4.0.1",

View File

@ -97,6 +97,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
viewportType: 'volume', viewportType: 'volume',
orientation: viewportOptions.orientation, orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId, viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}} }}
displaySetOptions={[{}, pmapDisplaySetOptions]} displaySetOptions={[{}, pmapDisplaySetOptions]}
></Component> ></Component>

View File

@ -126,6 +126,7 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
SeriesInstanceUID: instance.SeriesInstanceUID, SeriesInstanceUID: instance.SeriesInstanceUID,
ROIContours: [], ROIContours: [],
visible: true, visible: true,
ReferencedSOPInstanceUIDsSet: new Set(),
}; };
for (let i = 0; i < ROIContourSequence.length; i++) { for (let i = 0; i < ROIContourSequence.length; i++) {
@ -142,7 +143,8 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
const contourPoints = []; const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) { for (let c = 0; c < ContourSequenceArray.length; c++) {
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c]; const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } =
ContourSequenceArray[c];
let isSupported = false; let isSupported = false;
@ -172,6 +174,12 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
type: ContourGeometricType, type: ContourGeometricType,
isSupported, isSupported,
}); });
if (ContourImageSequence?.ReferencedSOPInstanceUID) {
structureSet.ReferencedSOPInstanceUIDsSet.add(
ContourImageSequence?.ReferencedSOPInstanceUID
);
}
} }
_setROIContourMetadata( _setROIContourMetadata(

View File

@ -6,7 +6,7 @@ import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
@ -43,8 +43,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportGrid, viewportGridService] = useViewportGrid();
// States // States
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
const [selectedSegment, setSelectedSegment] = useState(1); const [selectedSegment, setSelectedSegment] = useState(1);
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the RT is opened and segments are loaded into the // Hydration means that the RT is opened and segments are loaded into the
// segmentation panel, and RT is also rendered on any viewport that is in the // segmentation panel, and RT is also rendered on any viewport that is in the
@ -123,6 +123,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
toolGroupId: toolGroupId, toolGroupId: toolGroupId,
orientation: viewportOptions.orientation, orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId, viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}} }}
onElementEnabled={evt => { onElementEnabled={evt => {
props.onElementEnabled?.(evt); props.onElementEnabled?.(evt);
@ -185,6 +186,19 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
setRtIsLoading(false); setRtIsLoading(false);
} }
if (rtDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
const { firstSegmentedSliceImageId } = rtDisplaySet;
const { presentationIds } = viewportOptions;
setPositionPresentation(presentationIds.positionPresentationId, {
viewportType: 'stack',
viewReference: {
referencedImageId: firstSegmentedSliceImageId,
},
viewPresentation: {},
});
}
if (evt.overlappingSegments) { if (evt.overlappingSegments) {
uiNotificationService.show({ uiNotificationService.show({
title: 'Overlapping Segments', title: 'Overlapping Segments',
@ -247,8 +261,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId); toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
setToolGroupCreated(true);
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId); segmentationService.removeSegmentationRepresentations(viewportId);

View File

@ -4,6 +4,7 @@ import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } f
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG'; import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup'; const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
@ -41,6 +42,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// States // States
const [selectedSegment, setSelectedSegment] = useState(1); const [selectedSegment, setSelectedSegment] = useState(1);
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the SEG is opened and segments are loaded into the // Hydration means that the SEG is opened and segments are loaded into the
// segmentation panel, and SEG is also rendered on any viewport that is in the // segmentation panel, and SEG is also rendered on any viewport that is in the
@ -198,6 +200,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) { if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
setSegIsLoading(false); setSegIsLoading(false);
} }
if (segDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
const { firstSegmentedSliceImageId } = segDisplaySet;
const { presentationIds } = viewportOptions;
setPositionPresentation(presentationIds.positionPresentationId, {
viewReference: {
referencedImageId: firstSegmentedSliceImageId,
},
});
}
} }
); );

View File

@ -9,24 +9,20 @@ import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport'; import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state'; import { useAppConfig } from '@state';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'; const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup'; const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
commandsManager, props;
children,
dataSource,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const [appConfig] = useAppConfig(); const [appConfig] = useAppConfig();
const { setPositionPresentation } = usePositionPresentationStore();
const { const {
displaySetService, displaySetService,
cornerstoneViewportService, cornerstoneViewportService,
@ -157,30 +153,13 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
setActiveImageDisplaySetData(referencedDisplaySet); setActiveImageDisplaySetData(referencedDisplaySet);
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata); setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
if ( const { presentationIds } = viewportOptions;
referencedDisplaySet.displaySetInstanceUID === const measurement = srDisplaySet.measurements[newMeasurementSelected];
activeImageDisplaySetData?.displaySetInstanceUID setPositionPresentation(presentationIds.positionPresentationId, {
) { viewReference: {
const { measurements } = srDisplaySet; referencedImageId: measurement.imageId,
},
// it means that we have a new referenced display set, and the });
// imageIdIndex will handle it by updating the viewport, but if they
// are the same we just need to use measurementService to jump to the
// new measurement
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
if (imageIdIndex !== -1) {
csViewport.setImageIdIndex(imageIdIndex);
}
}
}); });
}, },
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId] [dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
@ -202,10 +181,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
return null; return null;
} }
const initialImageIndex = activeImageDisplaySetData.images.findIndex(
image => image.imageId === measurement.imageId
);
return ( return (
<Component <Component
{...props} {...props}
@ -230,7 +205,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
props.onElementEnabled?.(evt); props.onElementEnabled?.(evt);
onElementEnabled(evt); onElementEnabled(evt);
}} }}
initialImageIndex={initialImageIndex}
isJumpToMeasurementDisabled={true} isJumpToMeasurementDisabled={true}
></Component> ></Component>
); );

View File

@ -25,7 +25,7 @@ const contentItemFormatters = {
return `${NumericValue} ${CodeValue}`; return `${NumericValue} ${CodeValue}`;
}, },
PNAME: contentItem => { PNAME: contentItem => {
const personName = contentItem.PersonName?.[0]?.Alphabetic; const personName = contentItem.PersonName?.[0];
return personName ? utils.formatPN(personName) : undefined; return personName ? utils.formatPN(personName) : undefined;
}, },
DATE: contentItem => { DATE: contentItem => {

View File

@ -262,7 +262,6 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
} }
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void { function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
debugger;
const { segmentationService } = servicesManager.services; const { segmentationService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations(); const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } = const { seriesMetadata, instance } =

View File

@ -5,12 +5,14 @@ import { metaData, Enums, utilities } from '@cornerstonejs/core';
import type { ImageSliceData } from '@cornerstonejs/core/types'; import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui'; import { ViewportOverlay } from '@ohif/ui';
import type { InstanceMetadata } from '@ohif/core/src/types'; import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils'; import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { utils } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import './CustomizableViewportOverlay.css'; import './CustomizableViewportOverlay.css';
const EPSILON = 1e-4; const EPSILON = 1e-4;
const { formatPN } = utils;
type ViewportData = StackViewportData | VolumeViewportData; type ViewportData = StackViewportData | VolumeViewportData;

View File

@ -52,31 +52,6 @@ export function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
return moment(time, 'HH:mm:ss').format(strFormat); return moment(time, 'HH:mm:ss').format(strFormat);
} }
/**
* Formats a patient name for display purposes
*
* @param {string} name
* @returns {string} formatted name.
*/
export function formatPN(name) {
if (!name) {
return '';
}
if (typeof name === 'object') {
name = name.Alphabetic;
if (!name) {
return '';
}
}
const cleaned = name
.split('^')
.filter(s => !!s)
.join(', ')
.trim();
return cleaned === ',' || cleaned === '' ? '' : cleaned;
}
/** /**
* Gets compression type * Gets compression type
* *

View File

@ -20,7 +20,7 @@ const frameView: Types.HangingProtocol.Protocol = {
}, },
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },

View File

@ -162,6 +162,14 @@ export default function PanelSegmentation({
const firstImageId = referencedImageIds[0]; const firstImageId = referencedImageIds[0];
const instance = metaData.get('instance', firstImageId); const instance = metaData.get('instance', firstImageId);
if (!instance) {
return {
segmentationId,
isExportable: false,
};
}
const { SOPInstanceUID, SeriesInstanceUID } = instance; const { SOPInstanceUID, SeriesInstanceUID } = instance;
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(

View File

@ -478,14 +478,24 @@ class SegmentationService extends PubSubService {
// We should parse the segmentation as separate slices to support overlapping segments. // We should parse the segmentation as separate slices to support overlapping segments.
// This parsing should occur in the CornerstoneJS library adapters. // This parsing should occur in the CornerstoneJS library adapters.
// For now, we use the volume returned from the library and chop it here. // For now, we use the volume returned from the library and chop it here.
let firstSegmentedSliceImageId = null;
for (let i = 0; i < derivedSegmentationImages.length; i++) { for (let i = 0; i < derivedSegmentationImages.length; i++) {
const voxelManager = derivedSegmentationImages[i] const voxelManager = derivedSegmentationImages[i]
.voxelManager as csTypes.IVoxelManager<number>; .voxelManager as csTypes.IVoxelManager<number>;
const scalarData = voxelManager.getScalarData(); const scalarData = voxelManager.getScalarData();
scalarData.set(volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length)); const sliceData = volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length);
scalarData.set(sliceData);
voxelManager.setScalarData(scalarData); voxelManager.setScalarData(scalarData);
// Check if this slice has any non-zero voxels and we haven't found one yet
if (!firstSegmentedSliceImageId && sliceData.some(value => value !== 0)) {
firstSegmentedSliceImageId = derivedSegmentationImages[i].referencedImageId;
}
} }
// assign the first non zero voxel image id to the segDisplaySet
segDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId, segmentationId,
segDisplaySet, segDisplaySet,
@ -542,7 +552,19 @@ class SegmentationService extends PubSubService {
} }
const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID; const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID;
const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
rtDisplaySet.referencedDisplaySetInstanceUID
);
const referencedImageIdsWithGeometry = Array.from(structureSet.ReferencedSOPInstanceUIDsSet);
const referencedImageIds = referencedDisplaySet.instances.map(image => image.imageId);
// find the first image id that contains a referenced SOP instance UID
const firstSegmentedSliceImageId = referencedImageIds.find(imageId =>
referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId))
);
rtDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
// Map ROI contours to RT Struct Data // Map ROI contours to RT Struct Data
const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID); const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID);

View File

@ -605,10 +605,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
let initialImageIndexToUse = let initialImageIndexToUse =
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex; presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
}
const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions(); const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions();
const properties = { ...presentations.lutPresentation?.properties }; const properties = { ...presentations.lutPresentation?.properties };
@ -637,12 +633,26 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}); });
let imageIdsToSet = imageIds; let imageIdsToSet = imageIds;
const res = this._processExtraDisplaySetsForViewport(viewport); const overlayProcessingResult = this._processExtraDisplaySetsForViewport(viewport);
imageIdsToSet = res?.imageIds ?? imageIdsToSet; imageIdsToSet = overlayProcessingResult?.imageIds ?? imageIdsToSet;
const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId;
if (referencedImageId) {
initialImageIndexToUse = imageIdsToSet.indexOf(referencedImageId);
}
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
}
return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => { return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties }); viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo); this.setPresentations(viewport.id, presentations, viewportInfo);
if (overlayProcessingResult?.addOverlayFn) {
overlayProcessingResult.addOverlayFn();
}
if (displayArea) { if (displayArea) {
viewport.setDisplayArea(displayArea); viewport.setDisplayArea(displayArea);
} }
@ -827,10 +837,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}); });
// For SEG and RT viewports // For SEG and RT viewports
this._processExtraDisplaySetsForViewport(viewport); const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {};
await viewport.setVolumes(volumeInputArray); await viewport.setVolumes(volumeInputArray);
if (addOverlayFn) {
addOverlayFn();
}
volumesProperties.forEach(({ properties, volumeId }) => { volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId); viewport.setProperties(properties, volumeId);
}); });
@ -880,8 +894,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
); );
const imageIds = referenceDisplaySet.images.map(image => image.imageId); const imageIds = referenceDisplaySet.images.map(image => image.imageId);
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
return { imageIds }; return {
imageIds,
addOverlayFn: () =>
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport),
};
} }
private addOverlayRepresentationForDisplaySet( private addOverlayRepresentationForDisplaySet(

View File

@ -4,7 +4,7 @@ import { PositionPresentation } from '../types/Presentation';
import { addUniqueIndex, JOIN_STR } from './presentationUtils'; import { addUniqueIndex, JOIN_STR } from './presentationUtils';
const PRESENTATION_TYPE_ID = 'positionPresentationId'; const PRESENTATION_TYPE_ID = 'positionPresentationId';
const DEBUG_STORE = true; const DEBUG_STORE = false;
/** /**
* Represents the state and actions for managing position presentations. * Represents the state and actions for managing position presentations.

View File

@ -142,7 +142,9 @@ function createDicomLocalApi(dicomLocalConfig) {
study.series.forEach(aSeries => { study.series.forEach(aSeries => {
const { SeriesInstanceUID } = aSeries; const { SeriesInstanceUID } = aSeries;
aSeries.instances.forEach(instance => { const isMultiframe = aSeries.instances[0].NumberOfFrames > 1;
aSeries.instances.forEach((instance, index) => {
const { const {
url: imageId, url: imageId,
StudyInstanceUID, StudyInstanceUID,
@ -151,22 +153,14 @@ function createDicomLocalApi(dicomLocalConfig) {
} = instance; } = instance;
instance.imageId = imageId; instance.imageId = imageId;
const numberOfFrames = instance.NumberOfFrames || 1;
// Process all frames consistently, whether single or multiframe // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
for (let i = 0; i < numberOfFrames; i++) { metadataProvider.addImageIdToUIDs(imageId, {
const frameNumber = i + 1; StudyInstanceUID,
const frameImageId = implementation.getImageIdsForInstance({ SeriesInstanceUID,
instance, SOPInstanceUID,
frame: frameNumber, frameIndex: isMultiframe ? index : 1,
}); });
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
metadataProvider.addImageIdToUIDs(frameImageId, {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
frameNumber: numberOfFrames > 1 ? frameNumber : undefined,
});
}
}); });
DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, { DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, {

View File

@ -30,6 +30,7 @@ function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesM
return ( return (
<PanelStudyBrowser <PanelStudyBrowser
servicesManager={servicesManager} servicesManager={servicesManager}
commandsManager={commandsManager}
dataSource={dataSource} dataSource={dataSource}
getImageSrc={_getImageSrcFromImageId} getImageSrc={_getImageSrcFromImageId}
getStudiesForPatientByMRN={_getStudiesForPatientByMRN} getStudiesForPatientByMRN={_getStudiesForPatientByMRN}

View File

@ -63,7 +63,7 @@ const defaultProtocol = {
// It has no affect if nothing is specified in the URL. // It has no affect if nothing is specified in the URL.
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },

View File

@ -24,7 +24,7 @@ const defaultDisplaySetSelector = {
// It has no affect if nothing is specified in the URL. // It has no affect if nothing is specified in the URL.
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },
@ -56,7 +56,7 @@ const priorDisplaySetSelector = {
// It has no affect if nothing is specified in the URL. // It has no affect if nothing is specified in the URL.
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },

View File

@ -50,7 +50,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
// It has no affect if nothing is specified in the URL. // It has no affect if nothing is specified in the URL.
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },

View File

@ -43,7 +43,7 @@ const hpScale: Types.HangingProtocol.Protocol = {
// It has no affect if nothing is specified in the URL. // It has no affect if nothing is specified in the URL.
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },

View File

@ -38,7 +38,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
} }
setPatientInfo({ setPatientInfo({
PatientID: instance.PatientID || null, PatientID: instance.PatientID || null,
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null, PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
PatientSex: instance.PatientSex || null, PatientSex: instance.PatientSex || null,
PatientDOB: formatDate(instance.PatientBirthDate) || null, PatientDOB: formatDate(instance.PatientBirthDate) || null,
}); });

View File

@ -2,7 +2,7 @@ import { create } from 'zustand';
import { devtools } from 'zustand/middleware'; import { devtools } from 'zustand/middleware';
const PRESENTATION_TYPE_ID = 'viewportsByPositionId'; const PRESENTATION_TYPE_ID = 'viewportsByPositionId';
const DEBUG_STORE = true; const DEBUG_STORE = false;
/** /**
* Represents the state and actions for managing viewports by position. * Represents the state and actions for managing viewports by position.

View File

@ -3,7 +3,10 @@ import classnames from 'classnames';
import listComponentGenerator from './listComponentGenerator'; import listComponentGenerator from './listComponentGenerator';
import './ViewportOverlay.css'; import './ViewportOverlay.css';
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision, formatPN } from './utils'; import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { utils } from '@ohif/core';
const { formatPN } = utils;
interface OverlayItem { interface OverlayItem {
id: string; id: string;

View File

@ -52,28 +52,6 @@ export function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
return moment(time, 'HH:mm:ss').format(strFormat); return moment(time, 'HH:mm:ss').format(strFormat);
} }
/**
* Formats a patient name for display purposes
*
* @param {string} name
* @returns {string} formatted name.
*/
export function formatPN(name) {
if (!name) {
return;
}
// Convert the first ^ to a ', '. String.replace() only affects
// the first appearance of the character.
const commaBetweenFirstAndLast = name.replace('^', ', ');
// Replace any remaining '^' characters with spaces
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
// Trim any extraneous whitespace
return cleaned.trim();
}
/** /**
* Gets compression type * Gets compression type
* *

View File

@ -188,7 +188,7 @@ const hpTestSwitch: Types.HangingProtocol.Protocol = {
// It has no affect if nothing is specified in the URL. // It has no affect if nothing is specified in the URL.
{ {
attribute: 'isDisplaySetFromUrl', attribute: 'isDisplaySetFromUrl',
weight: 10, weight: 20,
constraint: { constraint: {
equals: true, equals: true,
}, },

View File

@ -9,8 +9,10 @@ import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructure
import { getWebWorkerManager } from '@cornerstonejs/core'; import { getWebWorkerManager } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools'; import { Enums } from '@cornerstonejs/tools';
import { utils } from '@ohif/core';
const { SegmentationRepresentations } = Enums; const { SegmentationRepresentations } = Enums;
const { formatPN } = utils;
const metadataProvider = classes.MetadataProvider; const metadataProvider = classes.MetadataProvider;
const ROI_THRESHOLD_MANUAL_TOOL_IDS = [ const ROI_THRESHOLD_MANUAL_TOOL_IDS = [
@ -595,7 +597,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
report[id] = { report[id] = {
...segReport, ...segReport,
PatientID: instance.PatientID ?? '000000', PatientID: instance.PatientID ?? '000000',
PatientName: instance.PatientName.Alphabetic, PatientName: formatPN(instance.PatientName),
StudyInstanceUID: instance.StudyInstanceUID, StudyInstanceUID: instance.StudyInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID, SeriesInstanceUID: instance.SeriesInstanceUID,
StudyDate: instance.StudyDate, StudyDate: instance.StudyDate,

View File

@ -23,6 +23,7 @@ window.config = {
wadoRoot: 'http://localhost/dicom-web', wadoRoot: 'http://localhost/dicom-web',
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
supportsReject: true, supportsReject: true,
dicomUploadEnabled: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true, enableStudyLazyLoad: true,

View File

@ -6,10 +6,10 @@ import DicomMetadataStore from '../services/DicomMetadataStore';
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
import toNumber from '../utils/toNumber'; import toNumber from '../utils/toNumber';
import combineFrameInstance from '../utils/combineFrameInstance'; import combineFrameInstance from '../utils/combineFrameInstance';
import formatPN from '../utils/formatPN';
class MetadataProvider { class MetadataProvider {
private readonly imageURIToUIDs: Map<string, any> = new Map(); private readonly imageURIToUIDs: Map<string, any> = new Map();
private readonly imageUIDsByImageId: Map<string, any> = new Map();
// Can be used to store custom metadata for a specific type. // Can be used to store custom metadata for a specific type.
// For instance, the scaling metadata for PET can be stored here // For instance, the scaling metadata for PET can be stored here
// as type "scalingModule" // as type "scalingModule"
@ -25,7 +25,6 @@ class MetadataProvider {
// An example would be dicom hosted at some random site. // An example would be dicom hosted at some random site.
const imageURI = imageIdToURI(imageId); const imageURI = imageIdToURI(imageId);
this.imageURIToUIDs.set(imageURI, uids); this.imageURIToUIDs.set(imageURI, uids);
this.imageUIDsByImageId.set(imageId, uids);
} }
addCustomMetadata(imageId, type, metadata) { addCustomMetadata(imageId, type, metadata) {
@ -347,7 +346,7 @@ class MetadataProvider {
let patientName; let patientName;
if (PatientName) { if (PatientName) {
patientName = PatientName.Alphabetic; patientName = formatPN(PatientName);
} }
metadata = { metadata = {
@ -462,11 +461,6 @@ class MetadataProvider {
} }
getUIDsFromImageID(imageId) { getUIDsFromImageID(imageId) {
const cachedUIDs = this.imageUIDsByImageId.get(imageId);
if (cachedUIDs) {
return cachedUIDs;
}
if (imageId.startsWith('wadors:')) { if (imageId.startsWith('wadors:')) {
const strippedImageId = imageId.split('/studies/')[1]; const strippedImageId = imageId.split('/studies/')[1];
const splitImageId = strippedImageId.split('/'); const splitImageId = strippedImageId.split('/');

View File

@ -1,4 +1,5 @@
import { vec3 } from 'gl-matrix'; import { vec3 } from 'gl-matrix';
import { dicomSplit } from './dicomSplit';
/** /**
* Combine the Per instance frame data, the shared frame data * Combine the Per instance frame data, the shared frame data
@ -15,24 +16,13 @@ const combineFrameInstance = (frame, instance) => {
PerFrameFunctionalGroupsSequence, PerFrameFunctionalGroupsSequence,
SharedFunctionalGroupsSequence, SharedFunctionalGroupsSequence,
NumberOfFrames, NumberOfFrames,
SpacingBetweenSlices, ImageType,
} = instance; } = instance;
instance.ImageType = dicomSplit(ImageType);
if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) { if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) {
const frameNumber = Number.parseInt(frame || 1); const frameNumber = Number.parseInt(frame || 1);
const shared = SharedFunctionalGroupsSequence
? Object.values(SharedFunctionalGroupsSequence[0])
.filter(Boolean)
.map(it => it[0])
.filter(it => typeof it === 'object')
: [];
const perFrame = PerFrameFunctionalGroupsSequence
? Object.values(PerFrameFunctionalGroupsSequence[frameNumber - 1])
.filter(Boolean)
.map(it => it[0])
.filter(it => typeof it === 'object')
: [];
// this is to fix NM multiframe datasets with position and orientation // this is to fix NM multiframe datasets with position and orientation
// information inside DetectorInformationSequence // information inside DetectorInformationSequence
@ -44,8 +34,12 @@ const combineFrameInstance = (frame, instance) => {
let ImagePositionPatientToUse = instance.ImagePositionPatient; let ImagePositionPatientToUse = instance.ImagePositionPatient;
if (!instance.ImagePositionPatient && instance.DetectorInformationSequence) { if (!instance.ImagePositionPatient && instance.DetectorInformationSequence) {
const imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient; let imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient;
const imageOrientationPatient = instance.ImageOrientationPatient; let imageOrientationPatient = instance.ImageOrientationPatient;
imagePositionPatient = imagePositionPatient.map(it => Number(it));
imageOrientationPatient = imageOrientationPatient.map(it => Number(it));
const SpacingBetweenSlices = Number(instance.SpacingBetweenSlices);
// Calculate the position for the current frame // Calculate the position for the current frame
if (imageOrientationPatient && SpacingBetweenSlices) { if (imageOrientationPatient && SpacingBetweenSlices) {
@ -73,27 +67,77 @@ const combineFrameInstance = (frame, instance) => {
ImagePositionPatientToUse = [position[0], position[1], position[2]]; ImagePositionPatientToUse = [position[0], position[1], position[2]];
} }
} }
console.debug('🚀 ~ ImagePositionPatientToUse:', ImagePositionPatientToUse);
const newInstance = Object.assign(instance, { frameNumber: frameNumber }); // Cache the _parentInstance at the top level as a full copy to prevent
// setting values hard.
// merge the shared first then the per frame to override if (!instance._parentInstance) {
[...shared, ...perFrame].forEach(item => { Object.defineProperty(instance, '_parentInstance', {
Object.entries(item).forEach(([key, value]) => { value: { ...instance },
newInstance[key] = value;
}); });
}); }
const sharedInstance = createCombinedValue(
instance._parentInstance,
SharedFunctionalGroupsSequence?.[0],
'_shared'
);
const newInstance = createCombinedValue(
sharedInstance,
PerFrameFunctionalGroupsSequence?.[frameNumber - 1],
frameNumber
);
// Todo: we should cache this combined instance somewhere, maybe add it newInstance.ImagePositionPatient = ImagePositionPatientToUse ??
// back to the dicomMetaStore so we don't have to do this again. newInstance.ImagePositionPatient ?? [0, 0, frameNumber];
return {
...newInstance, Object.defineProperty(newInstance, 'frameNumber', {
ImagePositionPatient: ImagePositionPatientToUse ?? value: frameNumber,
newInstance.ImagePositionPatient ?? [0, 0, frameNumber], writable: true,
}; enumerable: true,
configurable: true,
});
return newInstance;
} else { } else {
return instance; return instance;
} }
}; };
/**
* Creates a combined instance stored in the parent object which
* inherits from the parent instance the attributes in the functional groups.
* The storage key in the parent is in key
*/
function createCombinedValue(parent, functionalGroups, key) {
if (parent[key]) {
return parent[key];
}
// Exclude any proxying values
const newInstance = Object.create(parent);
Object.defineProperty(parent, key, {
value: newInstance,
writable: false,
enumerable: false,
});
if (!functionalGroups) {
return newInstance;
}
const shared = functionalGroups
? Object.values(functionalGroups)
.filter(Boolean)
.map(it => it[0])
.filter(it => typeof it === 'object')
: [];
// merge the shared first then the per frame to override
[...shared].forEach(item => {
if (item.SOPInstanceUID) {
// This sub-item is a previous value information item, so don't merge it
return;
}
Object.entries(item).forEach(([key, value]) => {
newInstance[key] = value;
});
});
return newInstance;
}
export default combineFrameInstance; export default combineFrameInstance;

View File

@ -0,0 +1,5 @@
export function dicomSplit(value) {
return (
(Array.isArray(value) && value) || (typeof value === 'string' && value.split('\\')) || value
);
}

View File

@ -1,4 +1,5 @@
import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore'; import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore';
import formatPN from './formatPN';
export default function downloadCSVReport(measurementData) { export default function downloadCSVReport(measurementData) {
if (measurementData.length === 0) { if (measurementData.length === 0) {
@ -86,7 +87,7 @@ function _getCommonRowItems(measurement, seriesMetadata) {
return { return {
'Patient ID': firstInstance.PatientID, // Patient ID 'Patient ID': firstInstance.PatientID, // Patient ID
'Patient Name': firstInstance.PatientName?.Alphabetic || '', // Patient Name 'Patient Name': formatPN(firstInstance.PatientName) || '', // Patient Name
StudyInstanceUID: measurement.referenceStudyUID, // StudyInstanceUID StudyInstanceUID: measurement.referenceStudyUID, // StudyInstanceUID
SeriesInstanceUID: measurement.referenceSeriesUID, // SeriesInstanceUID SeriesInstanceUID: measurement.referenceSeriesUID, // SeriesInstanceUID
SOPInstanceUID: measurement.SOPInstanceUID, // SOPInstanceUID SOPInstanceUID: measurement.SOPInstanceUID, // SOPInstanceUID

View File

@ -48,7 +48,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
} }
setPatientInfo({ setPatientInfo({
PatientID: instance.PatientID || null, PatientID: instance.PatientID || null,
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null, PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
PatientSex: instance.PatientSex || null, PatientSex: instance.PatientSex || null,
PatientDOB: formatDate(instance.PatientBirthDate) || null, PatientDOB: formatDate(instance.PatientBirthDate) || null,
}); });

View File

@ -1 +1 @@
3.9.2 3.9.3

View File

@ -12594,13 +12594,6 @@ fast-uri@^3.0.1:
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024"
integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==
fast-url-parser@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==
dependencies:
punycode "^1.3.2"
fastest-levenshtein@^1.0.12: fastest-levenshtein@^1.0.12:
version "1.0.16" version "1.0.16"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
@ -19130,7 +19123,7 @@ path-to-regexp@1.9.0, path-to-regexp@^1.7.0:
dependencies: dependencies:
isarray "0.0.1" isarray "0.0.1"
path-to-regexp@2.2.1, path-to-regexp@3.3.0: path-to-regexp@3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
@ -20438,7 +20431,7 @@ pumpify@^1.3.3:
inherits "^2.0.3" inherits "^2.0.3"
pump "^2.0.0" pump "^2.0.0"
punycode@^1.3.2, punycode@^1.4.1: punycode@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
@ -22034,21 +22027,7 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
dependencies: dependencies:
randombytes "^2.1.0" randombytes "^2.1.0"
serve-handler@6.1.5: serve-handler@6.1.6, serve-handler@^6.1.6:
version "6.1.5"
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375"
integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==
dependencies:
bytes "3.0.0"
content-disposition "0.5.2"
fast-url-parser "1.1.3"
mime-types "2.1.18"
minimatch "3.1.2"
path-is-inside "1.0.2"
path-to-regexp "2.2.1"
range-parser "1.2.0"
serve-handler@^6.1.6:
version "6.1.6" version "6.1.6"
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1"
integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==
@ -22084,10 +22063,10 @@ serve-static@1.16.2:
parseurl "~1.3.3" parseurl "~1.3.3"
send "0.19.0" send "0.19.0"
serve@^14.2.0: serve@^14.2.4:
version "14.2.3" version "14.2.4"
resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.3.tgz#047ba2b349354255bc09e0332cd41a92787836c9" resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.4.tgz#ba4c425c3c965f496703762e808f34b913f42fb0"
integrity sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ== integrity sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==
dependencies: dependencies:
"@zeit/schemas" "2.36.0" "@zeit/schemas" "2.36.0"
ajv "8.12.0" ajv "8.12.0"
@ -22098,7 +22077,7 @@ serve@^14.2.0:
clipboardy "3.0.0" clipboardy "3.0.0"
compression "1.7.4" compression "1.7.4"
is-port-reachable "4.0.0" is-port-reachable "4.0.0"
serve-handler "6.1.5" serve-handler "6.1.6"
update-check "1.5.4" update-check "1.5.4"
set-blocking@^2.0.0: set-blocking@^2.0.0: