Add stripWadouriPrefix function and refactor image ID handling for improved URL normalization

This commit is contained in:
Ross
2026-05-18 12:17:35 +01:00
parent f169238057
commit 99df1cd1c1
+124 -34
View File
@@ -166,6 +166,12 @@ function getToolIcon(toolName?: string) {
return found?.icon || '🔖';
}
function stripWadouriPrefix(imageId?: string): string {
if (!imageId) return "";
if (!imageId.startsWith("wadouri:")) return imageId;
return imageId.replace(/^wadouri:+/, "").replace(/^:+/, "");
}
// Mouse button labels
const MOUSE_BUTTONS = [
{ label: "Left", value: "Primary" },
@@ -601,7 +607,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
try {
const first = imageIds[0];
if (typeof fetch === 'function' && typeof (window as any).dicomParser !== 'undefined' && first.startsWith('wadouri:')) {
const url = first.slice(7);
const url = stripWadouriPrefix(first);
const resp = await fetch(url);
const buffer = await resp.arrayBuffer();
const ds = (window as any).dicomParser.parseDicom(new Uint8Array(buffer));
@@ -908,7 +914,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
for (const imageId of imageIds) {
if (imageId.startsWith("wadouri:")) {
try {
const url = imageId.slice(8);
const url = stripWadouriPrefix(imageId);
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
// @ts-ignore
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
@@ -1018,11 +1024,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
() => Array(MAX_VIEWPORTS).fill(null).map(() => [])
);
const viewportStackSignaturesRef = useRef<string[]>(Array(MAX_VIEWPORTS).fill(""));
const defaultVoiRangeByViewportRef = useRef<Array<{ lower: number; upper: number } | null>>(Array(MAX_VIEWPORTS).fill(null));
const [viewportModes, setViewportModes] = useState<Array<'stack' | 'volume'>>(
() => Array(MAX_VIEWPORTS).fill('stack')
);
// State for preset menu open/close per viewport
const [presetsOpenArr, setPresetsOpenArr] = useState<boolean[]>(
() => Array(viewportGrid.rows * viewportGrid.cols).fill(false)
@@ -1675,7 +1681,27 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const currentImageId = imageIds[currentIndex];
if (currentImageId) {
const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any;
const planeMeta = metaData.get("imagePlaneModule", currentImageId) as any;
const pixelMeta = metaData.get("imagePixelModule", currentImageId) as any;
modality = seriesMeta?.modality || "";
if (pixelMeta?.rows && pixelMeta?.columns) {
resolution = `${pixelMeta.columns} x ${pixelMeta.rows}`;
}
const spacingParts = [
planeMeta?.columnPixelSpacing,
planeMeta?.rowPixelSpacing,
planeMeta?.sliceThickness,
].filter((value) => value !== undefined && value !== null && value !== "");
if (spacingParts.length > 0) {
spacing = spacingParts.join(" x ");
}
}
if (typeof viewport.getCamera === "function") {
const camera = viewport.getCamera();
if (camera?.focalPoint && Array.isArray(camera.focalPoint)) {
focalPoint = camera.focalPoint.map((value: number) => value.toFixed(1)).join(", ");
}
}
if (currentIndex >= 0) {
@@ -1697,6 +1723,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const { lower, upper } = props.voiRange;
windowCenter = ((upper + lower) / 2).toFixed(0);
windowWidth = (upper - lower).toFixed(0);
if (defaultVoiRangeByViewportRef.current[viewportIdx] === null) {
defaultVoiRangeByViewportRef.current[viewportIdx] = { lower, upper };
}
}
}
} else if (viewportModes[viewportIdx] === "volume") {
@@ -1721,6 +1750,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const { lower, upper } = props.voiRange;
windowCenter = ((upper + lower) / 2).toFixed(0);
windowWidth = (upper - lower).toFixed(0);
if (defaultVoiRangeByViewportRef.current[viewportIdx] === null) {
defaultVoiRangeByViewportRef.current[viewportIdx] = { lower, upper };
}
}
}
// Try to get slice position in mm (distance along viewPlaneNormal from origin)
@@ -1787,6 +1819,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const signature = `${ids.length}:${ids[0] || ""}:${ids[ids.length - 1] || ""}`;
if (viewportStackSignaturesRef.current[i] !== signature) {
viewportStackSignaturesRef.current[i] = signature;
defaultVoiRangeByViewportRef.current[i] = null;
next[i] = [];
changed = true;
}
@@ -2290,6 +2323,52 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}
}, [updateOverlay]);
const applyDefaultWindowLevel = useCallback((viewportIdx: number, resetCamera = false) => {
const viewportId = `CT_${viewportIdx}`;
try {
const renderingEngine = renderingEngineRef.current;
const viewport = renderingEngine?.getViewport(viewportId);
if (!viewport) return false;
const ids = typeof viewport.getImageIds === "function"
? viewport.getImageIds()
: (viewportImageIds[viewportIdx] || []);
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
? viewport.getCurrentImageIdIndex()
: 0;
const imageId = ids[currentIndex] || ids[0];
const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx];
const voiMeta = imageId ? (metaData.get("voiLutModule", imageId) || {}) as any : {};
const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth;
if (defaultVoi) {
viewport.setProperties({ voiRange: defaultVoi });
} else if (Number.isFinite(Number(centerMeta)) && Number.isFinite(Number(widthMeta)) && Number(widthMeta) > 0) {
const center = Number(centerMeta);
const width = Number(widthMeta);
viewport.setProperties({
voiRange: {
lower: center - width / 2,
upper: center + width / 2,
},
});
} else if (typeof viewport.resetProperties === "function") {
viewport.resetProperties();
}
if (resetCamera && typeof viewport.resetCamera === "function") {
viewport.resetCamera();
}
viewport.render();
updateOverlay(viewportIdx, viewport);
return true;
} catch (err) {
console.warn("Failed to apply default window level", err);
return false;
}
}, [updateOverlay, viewportImageIds]);
const handleAutoLevel = useCallback((viewportIdx: number) => {
const viewportId = `CT_${viewportIdx}`;
try {
@@ -2307,39 +2386,51 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const imageId = ids[currentIndex] || ids[0];
if (!imageId) {
if (typeof viewport.resetProperties === "function") viewport.resetProperties();
viewport.render();
updateOverlay(viewportIdx, viewport);
applyDefaultWindowLevel(viewportIdx);
return;
}
const pixelMeta = (metaData.get("imagePixelModule", imageId) || {}) as any;
const lutMeta = (metaData.get("modalityLutModule", imageId) || {}) as any;
const voiMeta = (metaData.get("voiLutModule", imageId) || {}) as any;
const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx];
const minRaw = Number(pixelMeta.smallestPixelValue ?? pixelMeta.smallestImagePixelValue);
const maxRaw = Number(pixelMeta.largestPixelValue ?? pixelMeta.largestImagePixelValue);
const slope = Number(lutMeta.rescaleSlope ?? 1);
const intercept = Number(lutMeta.rescaleIntercept ?? 0);
const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth;
if (!Number.isFinite(minRaw) || !Number.isFinite(maxRaw) || maxRaw <= minRaw) {
if (typeof viewport.resetProperties === "function") viewport.resetProperties();
viewport.render();
updateOverlay(viewportIdx, viewport);
return;
}
if (Number.isFinite(minRaw) && Number.isFinite(maxRaw) && maxRaw > minRaw) {
viewport.setProperties({
voiRange: {
lower: minRaw * slope + intercept,
upper: maxRaw * slope + intercept,
},
});
} else if (Number.isFinite(Number(centerMeta)) && Number.isFinite(Number(widthMeta)) && Number(widthMeta) > 0) {
const center = Number(centerMeta);
const width = Number(widthMeta);
viewport.setProperties({
voiRange: {
lower: center - width / 2,
upper: center + width / 2,
},
});
} else if (defaultVoi) {
viewport.setProperties({ voiRange: defaultVoi });
} else {
applyDefaultWindowLevel(viewportIdx);
return;
}
viewport.render();
updateOverlay(viewportIdx, viewport);
} catch (e) {
// Ignore failures and keep existing VOI.
applyDefaultWindowLevel(viewportIdx);
}
}, [updateOverlay, viewportImageIds]);
}, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]);
const toggleViewerFullscreen = useCallback(() => {
const root = appRootRef.current;
@@ -2461,7 +2552,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
try {
const firstId = wadouriImageIds[0];
if (firstId && firstId.startsWith("wadouri:")) {
const url = firstId.slice(8);
const url = stripWadouriPrefix(firstId);
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
@@ -2686,8 +2777,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (!viewport || typeof viewport.getImageIds !== "function") return;
const imageIds = viewport.getImageIds();
// Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison
const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, ""));
const normTarget = imageId.replace(/^wadouri:/, "");
const normImageIds = imageIds.map(id => stripWadouriPrefix(id));
const normTarget = stripWadouriPrefix(imageId);
const idx = normImageIds.indexOf(normTarget);
if (idx !== -1 && typeof viewport.setImageIdIndex === "function") {
csUtilities.jumpToSlice(viewport.element, { imageIndex: idx });
@@ -2720,7 +2811,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
let uid = studyInstanceUID;
if (!uid && imageIds[0]?.startsWith("wadouri:")) {
try {
const url = imageIds[0].slice(8);
const url = stripWadouriPrefix(imageIds[0]);
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
@@ -3790,6 +3881,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
<div>
WC: {imageInfos[i]?.windowCenter} &nbsp; WW: {imageInfos[i]?.windowWidth}
</div>
{volumeDiagnosticsEnabled && (
<>
<div>Ori: {imageInfos[i]?.orientation || "N/A"}</div>
{imageInfos[i]?.resolution && <div>Res: {imageInfos[i]?.resolution}</div>}
{imageInfos[i]?.spacing && <div>Spacing: {imageInfos[i]?.spacing}</div>}
{imageInfos[i]?.focalPoint && <div>Pos: {imageInfos[i]?.focalPoint}</div>}
</>
)}
</>
) : (
<div>
@@ -3907,19 +4006,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
onClick={e => {
e.stopPropagation(); // <-- prevent bubbling to viewport
if (preset.action === "reset") {
try {
const viewportId = `CT_${i}`;
const renderingEngine = renderingEngineRef.current;
const viewport = renderingEngine?.getViewport(viewportId);
if (viewport) {
if (typeof viewport.resetProperties === "function") viewport.resetProperties();
if (typeof viewport.resetCamera === "function") viewport.resetCamera();
if (typeof viewport.render === "function") viewport.render();
updateOverlay(i, viewport);
}
} catch (err) {
console.warn("Failed to reset viewport properties", err);
}
applyDefaultWindowLevel(i, true);
} else if (preset.action === "autolevel") {
handleAutoLevel(i);
} else {
@@ -4722,6 +4809,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
width: "auto",
pointerEvents: "auto",
transition: "top 180ms ease",
paddingTop: 28,
marginTop: -28,
paddingBottom: 12,
}}
onMouseEnter={() => setTopControlsPeek(true)}
onMouseLeave={() => {
@@ -4752,7 +4842,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}}
>
<span style={{ fontSize: 16, lineHeight: 1 }}></span>
<span>Viewer</span>
<span>Fullscreen</span>
</button>
<div style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "stretch" }}>
@@ -4993,7 +5083,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const midIndex = Math.floor(imageIds.length / 2);
const chosenImage = imageIds[midIndex] || imageIds[0];
// Remove the wadouri: scheme if present. Use replace to avoid off-by-one slice errors.
const thumbnailUrl = chosenImage ? chosenImage.replace(/^wadouri:/, '') : null;
const thumbnailUrl = chosenImage ? stripWadouriPrefix(chosenImage) : null;
return (
<div