diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 4b75a05..72bd919 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -252,6 +252,18 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer return null; } + // Helper to detect external (non-DICOM) image ids + function isExternalImageId(imageId: string | null | undefined) { + if (!imageId) return false; + // Treat wadouri: as DICOM (our existing loader) + if (imageId.startsWith('wadouri:')) return false; + // blob: or data:image/ or common extensions + if (imageId.startsWith('blob:') || imageId.startsWith('data:image/')) return true; + const lower = imageId.toLowerCase(); + if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.gif')) return true; + return false; + } + const [stackOrderModified, setStackOrderModified] = useState(false); const [loadingState, setLoadingState] = useState(null); // Load annotations and viewer state from props if provided @@ -446,6 +458,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer return; } + // If the chosen image is an external non-DICOM image, draw it to a canvas directly + if (isExternalImageId(chosenImageId)) { + try { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.src = chosenImageId as string; + await new Promise((resolve, reject) => { + img.onload = () => resolve(true); + img.onerror = (e) => reject(e); + }); + const canvas = document.createElement('canvas'); + const size = 160; + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (ctx) { + // draw with aspect-fit + const ratio = Math.min(size / img.width, size / img.height); + const w = img.width * ratio; + const h = img.height * ratio; + const x = (size - w) / 2; + const y = (size - h) / 2; + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, size, size); + ctx.drawImage(img, x, y, w, h); + const dataUrl = canvas.toDataURL('image/png'); + setStackThumbnails(prev => ({ ...prev, [stackIdx]: dataUrl })); + } else { + setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' })); + } + } catch (err) { + console.warn('generateThumbnail: failed to generate thumbnail for external image', err); + setStackThumbnails(prev => ({ ...prev, [stackIdx]: 'BROKEN' })); + } + return; + } + const renderingEngine = renderingEngineRef.current; if (!renderingEngine) { console.warn('generateThumbnail: no renderingEngine yet for', stackIdx); @@ -971,14 +1020,30 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name)); let studyInstanceUID: string | undefined = undefined; - const arrayBuffer = await fileArray[0].arrayBuffer(); - const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer)); - studyInstanceUID = dataSet.string('x0020000d'); - console.log("StudyInstanceUID:", studyInstanceUID); - const imageIds = fileArray.map( - file => `wadouri:${URL.createObjectURL(file)}` - ); + // If the first file is likely a DICOM (no mime or .dcm extension), try to parse. + const firstFile = fileArray[0]; + const isImageFile = firstFile.type && firstFile.type.startsWith('image/'); + if (!isImageFile) { + try { + const arrayBuffer = await firstFile.arrayBuffer(); + const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer)); + studyInstanceUID = dataSet.string('x0020000d'); + console.log("StudyInstanceUID:", studyInstanceUID); + } catch (e) { + // not DICOM or failed to parse; leave UID undefined + console.debug('handleFilesSelected: first file is not a DICOM or failed to parse', e); + } + } + + // Create imageIds: use wadouri: for DICOM files, direct blob URLs for image files + const imageIds = fileArray.map((file) => { + const isImage = file.type && file.type.startsWith('image/'); + if (isImage) { + return URL.createObjectURL(file); + } + return `wadouri:${URL.createObjectURL(file)}`; + }); setAvailableStacks(prev => { const next = [...prev, { imageIds, studyInstanceUID }]; @@ -1205,7 +1270,33 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (viewportModes[i] === 'stack') { if (imageIds && imageIds.length > 0) { - viewport.setStack(imageIds); + // If this is an external (non-DICOM) image stack, render an element as a fallback + const firstId = imageIds[0]; + if (isExternalImageId(firstId)) { + try { + // Clear existing content and insert an image element + element.innerHTML = ''; + const img = document.createElement('img'); + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'contain'; + img.style.display = 'block'; + img.src = firstId as string; + // allow drag to open in new tab etc. + img.setAttribute('draggable', 'false'); + element.appendChild(img); + // Do not attach DICOM-specific tools to external images + mainToolGroup.removeViewports(renderingEngineId, viewportId); + } catch (err) { + console.warn('Failed to render external image in viewport', err); + } + } else { + try { + viewport.setStack(imageIds); + } catch (err) { + console.warn('viewport.setStack failed', err); + } + } } // CrosshairsTool is not applicable for stack viewports if (!crossHairsEnabled) {