add basic thumbnail support
This commit is contained in:
@@ -252,6 +252,18 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
return null;
|
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 [stackOrderModified, setStackOrderModified] = useState(false);
|
||||||
const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null);
|
const [loadingState, setLoadingState] = useState<null | "annotations" | "viewerState">(null);
|
||||||
// Load annotations and viewer state from props if provided
|
// Load annotations and viewer state from props if provided
|
||||||
@@ -446,6 +458,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
return;
|
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;
|
const renderingEngine = renderingEngineRef.current;
|
||||||
if (!renderingEngine) {
|
if (!renderingEngine) {
|
||||||
console.warn('generateThumbnail: no renderingEngine yet for', stackIdx);
|
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));
|
const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
let studyInstanceUID: string | undefined = undefined;
|
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(
|
// If the first file is likely a DICOM (no mime or .dcm extension), try to parse.
|
||||||
file => `wadouri:${URL.createObjectURL(file)}`
|
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 => {
|
setAvailableStacks(prev => {
|
||||||
const next = [...prev, { imageIds, studyInstanceUID }];
|
const next = [...prev, { imageIds, studyInstanceUID }];
|
||||||
@@ -1205,7 +1270,33 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
|
|
||||||
if (viewportModes[i] === 'stack') {
|
if (viewportModes[i] === 'stack') {
|
||||||
if (imageIds && imageIds.length > 0) {
|
if (imageIds && imageIds.length > 0) {
|
||||||
viewport.setStack(imageIds);
|
// If this is an external (non-DICOM) image stack, render an <img> 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
|
// CrosshairsTool is not applicable for stack viewports
|
||||||
if (!crossHairsEnabled) {
|
if (!crossHairsEnabled) {
|
||||||
|
|||||||
Reference in New Issue
Block a user