|
1 | 1 | import { Niivue, NVImage, DRAG_MODE, SLICE_TYPE, MULTIPLANAR_TYPE, SHOW_RENDER } from '@niivue/niivue' |
2 | 2 | import { Dcm2niix } from '@niivue/dcm2niix' |
| 3 | +import { dicomLoader } from '@niivue/dicom-loader' |
3 | 4 | import './niivue.css' |
4 | 5 |
|
5 | | -// page-wide niivue instance |
6 | | -const nv = new Niivue({ |
7 | | - dragAndDropEnabled: false, // disable drag and drop since we want to use input from the file input element |
8 | | -}) |
| 6 | +/** |
| 7 | + * Load DICOM files from a manifest URL using the Dcm2niix WebAssembly backend. |
| 8 | + * |
| 9 | + * This function fetches a text-based manifest file containing relative paths to DICOM images, |
| 10 | + * downloads and wraps them as `File` objects with proper `webkitRelativePath` attributes, |
| 11 | + * runs the DICOM-to-NIfTI conversion via `@niivue/dcm2niix`, and updates the UI with the result. |
| 12 | + * |
| 13 | + * This is a manual reference implementation that mirrors the functionality of `@niivue/dicom-loader`. |
| 14 | + * |
| 15 | + * @param manifestUrl - A full URL pointing to a manifest text file with relative DICOM paths |
| 16 | + * @returns A promise that resolves to an array of converted NIfTI `File` objects |
| 17 | + */ |
| 18 | +async function loadDicomsUsingDcm2niixFromManifest(manifestUrl) { |
| 19 | + console.log('Starting manual DICOM load from manifest via dcm2niix...') |
| 20 | + try { |
| 21 | + hideSaveButton() |
| 22 | + showLoadingCircle() |
| 23 | + |
| 24 | + const baseUrl = new URL(manifestUrl) |
| 25 | + const response = await fetch(manifestUrl) |
| 26 | + const text = await response.text() |
| 27 | + const urls = text.trim().split('\n') |
| 28 | + |
| 29 | + const dicomFiles = await Promise.all( |
| 30 | + urls.map(async (relativePath) => { |
| 31 | + const url = new URL(relativePath, baseUrl) |
| 32 | + const res = await fetch(url) |
| 33 | + if (!res.ok) throw new Error(`Failed to fetch DICOM: ${url}`) |
| 34 | + const arrayBuffer = await res.arrayBuffer() |
| 35 | + const filename = url.pathname.split('/').pop() |
| 36 | + const fullPath = `series/${filename}` |
| 37 | + const file = new File([arrayBuffer], filename) |
| 38 | + Object.defineProperty(file, 'webkitRelativePath', { |
| 39 | + value: fullPath, |
| 40 | + writable: false |
| 41 | + }) |
| 42 | + return file |
| 43 | + }) |
| 44 | + ) |
| 45 | + |
| 46 | + const dcm2niix = new Dcm2niix() |
| 47 | + await dcm2niix.init() |
| 48 | + let resultFileList = await dcm2niix.input(dicomFiles).run() |
| 49 | + resultFileList = resultFileList.filter(f => f.name.endsWith('.nii') || f.name.endsWith('.nii.gz')) |
| 50 | + |
| 51 | + updateSelectItems(resultFileList) |
| 52 | + hideLoadingCircle() |
| 53 | + showFileSelect() |
| 54 | + |
| 55 | + fileSelect.value = 0 |
| 56 | + fileSelect.dispatchEvent(new Event('change')) |
| 57 | + showText('Loaded via manual dcm2niix') |
| 58 | + |
| 59 | + return resultFileList |
| 60 | + } catch (err) { |
| 61 | + console.error('Error in loadDicomsUsingDcm2niixFromManifest:', err) |
| 62 | + hideLoadingCircle() |
| 63 | + hideFileSelect() |
| 64 | + showText('Error loading DICOMs manually') |
| 65 | + return [] |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +/** |
| 70 | + * Load DICOMs from a manifest using @niivue/dicom-loader |
| 71 | + * and display the time taken to load and decode them. |
| 72 | + * |
| 73 | + * @param manifestURL - URL to a text manifest with relative DICOM file paths |
| 74 | + */ |
| 75 | +async function loadDicomsWithNiivueLoader(manifestURL) { |
| 76 | + console.log('Loading DICOM manifest via dicom-loader...') |
| 77 | + showLoadingCircle() |
| 78 | + const startTime = performance.now() |
| 79 | + |
| 80 | + nv.useDicomLoader({ loader: dicomLoader }) |
| 81 | + |
| 82 | + await nv.loadDicoms([ |
| 83 | + { |
| 84 | + url: manifestURL, |
| 85 | + isManifest: true |
| 86 | + } |
| 87 | + ]) |
| 88 | + |
| 89 | + const vol = nv.volumes[nv.volumes.length - 1] |
| 90 | + const name = vol?.name || 'Unnamed volume' |
| 91 | + const endTime = performance.now() |
| 92 | + const elapsed = ((endTime - startTime) / 1000).toFixed(2) |
| 93 | + hideLoadingCircle() |
| 94 | + showText(`Loaded ${name} in ${elapsed} seconds`) |
| 95 | + showSaveButton() |
| 96 | +} |
| 97 | + |
| 98 | +// NiiVue instance |
| 99 | +const nv = new Niivue({ dragAndDropEnabled: false }) |
| 100 | + |
9 | 101 | // reference to the results list from dcm2niix for use later |
10 | 102 | let resultFileList = [] |
11 | 103 | let conversionTime = 0 |
12 | 104 | let downloadFile = null |
13 | 105 |
|
14 | 106 |
|
15 | | -const handleSaveButtonClick = () => { |
16 | | - let url = URL.createObjectURL(downloadFile); |
17 | | - const downloadLink = document.createElement('a'); |
18 | | - downloadLink.href = url; |
19 | | - downloadLink.download = downloadFile.name; |
20 | | - downloadLink.click() |
| 107 | +const handleSaveButtonClick = async () => { |
| 108 | + |
| 109 | + if (nv.volumes.length === 0) { |
| 110 | + if(downloadFile) { |
| 111 | + let url = URL.createObjectURL(downloadFile); |
| 112 | + const downloadLink = document.createElement('a'); |
| 113 | + downloadLink.href = url; |
| 114 | + downloadLink.download = downloadFile.name; |
| 115 | + downloadLink.click() |
| 116 | + |
| 117 | + } |
| 118 | + else { |
| 119 | + console.log('no volumes found') |
| 120 | + } |
| 121 | + return |
| 122 | + } |
| 123 | + const vol = nv.volumes[0] |
| 124 | + const name = vol.name || 'volume' |
| 125 | + const ext = vol.niiFile?.name?.endsWith('.nii.gz') ? '.nii.gz' : '.nii' |
| 126 | + console.log('saving ', name) |
| 127 | + await nv.saveImage({filename: `${name}${ext}`}) |
21 | 128 | } |
22 | 129 |
|
23 | 130 | const showSaveButton = () => { |
@@ -240,6 +347,11 @@ async function main() { |
240 | 347 | // when user clicks save |
241 | 348 | saveButton.onclick = handleSaveButtonClick |
242 | 349 |
|
| 350 | + // when a user clicks load manifest |
| 351 | + document.getElementById('loadManifestBtn').onclick = () => { |
| 352 | + loadDicomsWithNiivueLoader('https://niivue.github.io/niivue-demo-images/dicom/niivue-manifest.txt') |
| 353 | + } |
| 354 | + |
243 | 355 | // crosshair location change event |
244 | 356 | nv.onLocationChange = handleLocationChange |
245 | 357 | // get canvas element |
|
0 commit comments