diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx
index d082465..12aed99 100644
--- a/dicom-viewer/src/App.tsx
+++ b/dicom-viewer/src/App.tsx
@@ -2,8 +2,10 @@ import { useEffect, useRef, useCallback, useState } from "react"
import {
RenderingEngine,
Enums,
+ imageLoader,
+ utilities as csUtilities,
} from "@cornerstonejs/core"
-import { init as csRenderInit, utilities as csUtilities } from "@cornerstonejs/core"
+import { init as csRenderInit } from "@cornerstonejs/core"
import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool, utilities as csToolsUtilities } from "@cornerstonejs/tools"
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools"
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
@@ -257,6 +259,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (!imageId) return false;
// Treat wadouri: as DICOM (our existing loader)
if (imageId.startsWith('wadouri:')) return false;
+ if (imageId.startsWith('external:')) return true;
// blob: or data:image/ or common extensions
if (imageId.startsWith('blob:') || imageId.startsWith('data:image/')) return true;
const lower = imageId.toLowerCase();
@@ -266,7 +269,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Enable simple pan & zoom interactions for an
placed inside a viewport element.
// Attaches event listeners and returns a cleanup function.
- function enableImagePanZoom(container: HTMLElement, img: HTMLImageElement) {
+ function enableImagePanZoom(container: HTMLElement, img: HTMLImageElement, options?: { allowPan?: boolean, allowZoom?: boolean }) {
+ const allowPanInit = options?.allowPan ?? true;
+ const allowZoomInit = options?.allowZoom ?? true;
let scale = 1;
let translateX = 0;
let translateY = 0;
@@ -286,7 +291,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
};
+ let allowPan = allowPanInit;
+ let allowZoom = allowZoomInit;
+
const onWheel = (e: WheelEvent) => {
+ if (!allowZoom) return;
// Zoom on wheel
e.preventDefault();
const rect = img.getBoundingClientRect();
@@ -305,6 +314,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const onMouseDown = (e: MouseEvent) => {
if (e.button !== 0) return;
+ if (!allowPan) return;
isPanning = true;
startX = translateX;
startY = translateY;
@@ -345,9 +355,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const onTouchStart = (e: TouchEvent) => {
if (e.touches.length === 1) {
- isPanning = true;
- lastClientX = e.touches[0].clientX;
- lastClientY = e.touches[0].clientY;
+ if (allowPan) {
+ isPanning = true;
+ lastClientX = e.touches[0].clientX;
+ lastClientY = e.touches[0].clientY;
+ }
} else if (e.touches.length === 2) {
lastTouchDist = getTouchDist(e.touches[0], e.touches[1]);
}
@@ -362,7 +374,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
lastClientX = e.touches[0].clientX;
lastClientY = e.touches[0].clientY;
apply();
- } else if (e.touches.length === 2) {
+ } else if (e.touches.length === 2 && allowZoom) {
const curDist = getTouchDist(e.touches[0], e.touches[1]);
if (lastTouchDist > 0) {
const zoomFactor = curDist / lastTouchDist;
@@ -412,7 +424,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
img.style.willChange = '';
};
- return cleanup;
+ // Return control functions so we can update allowed interactions later
+ return {
+ cleanup,
+ setAllowPan: (v: boolean) => { allowPan = v; },
+ setAllowZoom: (v: boolean) => { allowZoom = v; },
+ };
}
const [stackOrderModified, setStackOrderModified] = useState(false);
@@ -614,7 +631,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
try {
const img = new Image();
img.crossOrigin = 'anonymous';
- img.src = chosenImageId as string;
+ const src = (chosenImageId as string).replace(/^external:/, '');
+ img.src = src;
await new Promise((resolve, reject) => {
img.onload = () => resolve(true);
img.onerror = (e) => reject(e);
@@ -1144,6 +1162,28 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
toolGroup.setToolActive(tool, { bindings });
});
+ // Also update external image handlers (pan/zoom) to respect selected tools
+ try {
+ const primaryTool = mouseToolBindings.Primary;
+ const wheelTool = mouseToolBindings.Wheel;
+ const auxTool = mouseToolBindings.Auxiliary;
+ // Determine whether pan/zoom should be enabled for primary/wheel interactions
+ const allowPan = primaryTool === PanTool.toolName || auxTool === PanTool.toolName;
+ const allowZoom = primaryTool === ZoomTool.toolName || wheelTool === ZoomTool.toolName || mouseToolBindings.Primary_and_Secondary === ZoomTool.toolName;
+
+ // Iterate viewports and update external handlers
+ viewportRefs.current.forEach((el) => {
+ if (!el) return;
+ const ext = (el as any)._externalPanZoom;
+ if (ext) {
+ if (typeof ext.setAllowPan === 'function') ext.setAllowPan(!!allowPan);
+ if (typeof ext.setAllowZoom === 'function') ext.setAllowZoom(!!allowZoom);
+ }
+ });
+ } catch (e) {
+ // ignore
+ }
+
//// Always re-apply the mouse wheel binding for StackScrollTool
//toolGroup.setToolActive(StackScrollTool.toolName, {
// bindings: [
@@ -1187,11 +1227,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}
}
- // Create imageIds: use wadouri: for DICOM files, direct blob URLs for image files
+ // Create imageIds: use wadouri: for DICOM files, `external:` for image files
const imageIds = fileArray.map((file) => {
const isImage = file.type && file.type.startsWith('image/');
if (isImage) {
- return URL.createObjectURL(file);
+ return `external:${URL.createObjectURL(file)}`;
}
return `wadouri:${URL.createObjectURL(file)}`;
});
@@ -1321,6 +1361,53 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
await csToolsInit(
);
dicomImageLoaderInit();
+ // Register client-side image loader for external images (JPEG/PNG/blob/data)
+ try {
+ imageLoader.registerImageLoader('external', async (imageId: string) => {
+ // imageId expected as 'external:'
+ const url = imageId.replace(/^external:/, '');
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+ img.src = url;
+ await new Promise((resolve, reject) => {
+ img.onload = () => resolve(true);
+ img.onerror = (e) => reject(e);
+ });
+
+ const w = img.naturalWidth || img.width;
+ const h = img.naturalHeight || img.height;
+ const canvas = document.createElement('canvas');
+ canvas.width = w;
+ canvas.height = h;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) throw new Error('Unable to create canvas context');
+ ctx.drawImage(img, 0, 0, w, h);
+ const im = ctx.getImageData(0, 0, w, h);
+
+ const image = {
+ imageId,
+ minPixelValue: 0,
+ maxPixelValue: 255,
+ slope: 1.0,
+ intercept: 0,
+ rows: h,
+ columns: w,
+ height: h,
+ width: w,
+ color: true,
+ rgba: true,
+ sizeInBytes: im.data.length,
+ getPixelData: () => im.data,
+ // Some Cornerstone APIs expect `getImageData` or `getCanvas` - provide canvas and ImageData
+ getImageData: () => im,
+ getCanvas: () => canvas,
+ } as any;
+
+ return image;
+ });
+ } catch (e) {
+ console.warn('Failed to register external image loader', e);
+ }
}
@@ -1438,14 +1525,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
img.style.height = '100%';
img.style.objectFit = 'contain';
img.style.display = 'block';
- img.src = firstId as string;
+ img.src = (firstId as string).replace(/^external:/, '');
// allow drag to open in new tab etc.
img.setAttribute('draggable', 'false');
element.appendChild(img);
// Attach pan/zoom interactions to this external image
- const cleanup = enableImagePanZoom(element, img);
- (element as any)._externalPanZoom = { cleanup };
+ const ext = enableImagePanZoom(element, img);
+ (element as any)._externalPanZoom = ext;
// Do not attach DICOM-specific tools to external images
mainToolGroup.removeViewports(renderingEngineId, viewportId);