Forum Discussion

Krish_1000_Wala's avatar
Krish_1000_Wala
Copper Contributor
Apr 11, 2025

dotnet maui blazor hybrid for Local viewer.

Hi Everyone, `Preformatted text`

I am a dotnet Developer. Currently working with .net maui blazor hybrid app.
I want to create a Local Dicom Viewer using this template for Windows.
i have created a simple project as selecting a single dicom file from local drive and showing its metadata and image.

Razor Page::
PAGE "/viewer"
@inject IJSRuntime JSRuntime

<div class="container">
    <h1>DICOM Viewer</h1>
    <div class="row mb-3">
        <div class="col">
            <div class="input-group">
                <InputFile OnChange="@LoadFiles" class="form-control" accept=".dcm" id="selectFile" />
                <button @onclick="ResetViewer" class="btn btn-secondary">Reset</button>
            </div>
            <div class="mt-2">
                <small class="text-muted">Select a DICOM file or drag and drop (if supported)</small>
            </div>
        </div>
    </div>

    <div class="row">
        <div class="col-md-8">
            <div id="cornerstone-element" style="width: 100%; height: 500px; border: 1px solid #ccc; position: relative;">
                <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #888;">
                    Select a DICOM file to view
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="card">
                <div class="card-header">DICOM Info</div>
                <div class="card-body">
                    <table class="table table-sm">
                        <tbody>
                            <tr><td>Transfer Syntax:</td><td id="transfersyntax">-</td></tr>
                            <tr><td>SOP Class UID:</td><td id="sopclassuid">-</td></tr>
                            <tr><td>SOP Instance UID:</td><td id="sopinstanceuid">-</td></tr>
                            <tr><td>Rows:</td><td id="rows">-</td></tr>
                            <tr><td>Columns:</td><td id="columns">-</td></tr>
                            <tr><td>Spacing:</td><td id="spacing">-</td></tr>
                            <tr><td>Direction:</td><td id="direction">-</td></tr>
                            <tr><td>Origin:</td><td id="origin">-</td></tr>
                            <tr><td>Modality:</td><td id="modality">-</td></tr>
                            <tr><td>Pixel Representation:</td><td id="pixelrepresentation">-</td></tr>
                            <tr><td>Bits Allocated:</td><td id="bitsallocated">-</td></tr>
                            <tr><td>Bits Stored:</td><td id="bitsstored">-</td></tr>
                            <tr><td>High Bit:</td><td id="highbit">-</td></tr>
                            <tr><td>Photometric Interpretation:</td><td id="photometricinterpretation">-</td></tr>
                            <tr><td>Window Center:</td><td id="windowcenter">-</td></tr>
                            <tr><td>Window Width:</td><td id="windowwidth">-</td></tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            try
            {
                // Initialize the DICOM viewer using the global interop object
                await JSRuntime.InvokeVoidAsync("dicomViewerInterop.initDicomViewer");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error initializing DICOM viewer: {ex.Message}");
            }
        }
    }

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        var file = e.File;
        if (file != null)
        {
            try
            {
                // Read the file content
                using var stream = file.OpenReadStream(maxAllowedSize: 30000000); // Adjust max allowed size if needed
                using var ms = new MemoryStream();
                await stream.CopyToAsync(ms);
                byte[] fileData = ms.ToArray();
                var filename=file.Name;
                // Send the file data to JS
                await JSRuntime.InvokeVoidAsync("dicomViewerInterop.loadDicomFileFromArray", fileData, file.Name);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error loading DICOM file: {ex.Message}");
            }
        }
    }

    private async Task ResetViewer()
    {
        try
        {
            await JSRuntime.InvokeVoidAsync("dicomViewerInterop.resetViewer");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error resetting viewer: {ex.Message}");
        }
    }
}


Javascript Interop:

// Cornerstone interop code for MAUI Blazor
// Global variables to store references
var cornerstone, cornerstoneTools, dicomImageLoader, dicomParser;
var renderingEngine = null;
var viewport = null;
var toolGroup = null;
var initialized = false;
var uids = {};

// Initialize the UIds for SOP Class descriptions
function initUids() {
    // Common DICOM UIDs
    uids = {
        '1.2.840.10008.5.1.4.1.1.1': 'CR Image Storage',
        '1.2.840.10008.5.1.4.1.1.1.1': 'Digital X-Ray Image - For Presentation',
        '1.2.840.10008.5.1.4.1.1.1.1.1': 'Digital X-Ray Image - For Processing',
        '1.2.840.10008.5.1.4.1.1.2': 'CT Image Storage',
        '1.2.840.10008.5.1.4.1.1.3.1': 'Ultrasound Multi-frame Image Storage',
        '1.2.840.10008.5.1.4.1.1.4': 'MR Image Storage',
        '1.2.840.10008.5.1.4.1.1.6.1': 'Ultrasound Image Storage',
        '1.2.840.10008.5.1.4.1.1.7': 'Secondary Capture Image Storage',
        '1.2.840.10008.5.1.4.1.1.128': 'Positron Emission Tomography Image Storage',
        '1.2.840.10008.5.1.4.1.1.13.1.1': 'X-Ray 3D Angiographic Image Storage',
        '1.2.840.10008.5.1.4.1.1.13.1.2': 'X-Ray 3D Craniofacial Image Storage',
        '1.2.840.10008.5.1.4.1.1.13.1.3': 'Breast Tomosynthesis Image Storage'
    };
}

// Check if libraries are loaded
function checkLibraryLoading() {
    console.log("DOM content loaded");

    if (typeof dicomViewer === 'undefined') {
        console.error("dicomViewer bundle is not loaded");
        return false;
    }

    if (typeof dicomViewer.cornerstoneCore === 'undefined') {
        console.error(" Cornerstone3D is not loaded");
        return false;
    }

    if (typeof dicomViewer.cornerstoneTools === 'undefined') {
        console.error(" CornerstoneTools is not loaded");
        return false;
    }

    if (typeof dicomViewer.dicomImageLoader === 'undefined') {
        console.error(" DICOM Image Loader is not loaded");
        return false;
    }

    if (typeof dicomViewer.dicomParser === 'undefined') {
        console.error(" DICOM Parser is not loaded");
        return false;
    }

    return true;
}

// Initialize volume loader (compatible with Cornerstone3D v3.8.0)
async function initVolumeLoader() {
    try {
        // Register the volume loader using newer API approach
        if (cornerstone.volumeLoader) {
            // Some versions might have this interface
            cornerstone.volumeLoader.registerUnknownVolumeLoader();
            cornerstone.volumeLoader.registerVolumeLoader('dicom', function () {
                return {
                    loadVolume: function () {
                        return Promise.resolve(null);
                    }
                };
            });
        } else {
            // Fallback for other versions
            console.log("Using fallback volume loader approach");
        }
    } catch (error) {
        console.error("Error in initVolumeLoader:", error);
    }
}

// Initialize providers
async function initProviders() {
    try {
        // Simplified initialization for adapting to different versions
        if (cornerstone.registerVolumeLoader) {
            cornerstone.registerVolumeLoader('dicomweb', function () {
                return {
                    loadVolume: function () {
                        return Promise.resolve(null);
                    }
                };
            });
        }
    } catch (error) {
        console.error("Error in initProviders:", error);
    }
}

// Setup HTML 
function setupHTML() {
    const element = document.getElementById('cornerstone-element');
    if (element) {
        // Clear any existing content
        element.innerHTML = '';

        // Set up drop zone functionality
        element.addEventListener('dragover', handleDragOver, false);
        element.addEventListener('drop', handleFileSelect, false);
    }
    return element;
}

// Drag over handler
function handleDragOver(evt) {
    evt.stopPropagation();
    evt.preventDefault();
    evt.dataTransfer.dropEffect = 'copy'; // Show as a copy operation
}

// File drop handler
function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    // Get the file that was dropped
    const files = evt.dataTransfer.files;
    if (files.length > 0) {
        const file = files[0];

        // Read the file and load it
        const reader = new FileReader();
        reader.onload = function (e) {
            const arrayBuffer = e.target.result;
            loadDicomFileFromArrayBuffer(arrayBuffer, file.name);
        };
        reader.readAsArrayBuffer(file);
    }
}

// Initialize the DICOM viewer (called from Blazor)
function initDicomViewer() {
    try {
        // Check if libraries are loaded
        if (!checkLibraryLoading()) {
            return Promise.reject("Libraries not loaded");
        }

        // Assign the libraries from the global bundle
        cornerstone = dicomViewer.cornerstoneCore;
        cornerstoneTools = dicomViewer.cornerstoneTools;
        dicomImageLoader = dicomViewer.dicomImageLoader;
        dicomParser = dicomViewer.dicomParser;

        // Init UIDs
        initUids();

        // Initialize image loader
        return Promise.resolve()
            .then(() => initVolumeLoader())
            .then(() => {
                // Initialize DICOM image loader
                if (dicomImageLoader.init) {
                    dicomImageLoader.init();
                }
                return initProviders();
            })
            .then(() => {
                // Set CPU rendering for compatibility
                if (cornerstone.setUseCPURendering) {
                    cornerstone.setUseCPURendering(true);
                }

                // Add tools if they exist
                if (!cornerstoneTools.addTool) {
                    console.warn("Tool functions not available in this version");
                    return;
                }

                // Extract tool classes and enums
                const PanTool = cornerstoneTools.PanTool;
                const WindowLevelTool = cornerstoneTools.WindowLevelTool;
                const StackScrollTool = cornerstoneTools.StackScrollTool;
                const ZoomTool = cornerstoneTools.ZoomTool;
                const ToolGroupManager = cornerstoneTools.ToolGroupManager;

                // Get mouse bindings from enums if available
                const MouseBindings = cornerstoneTools.Enums?.MouseBindings || {
                    Primary: 1,
                    Auxiliary: 2,
                    Secondary: 3,
                    Wheel: 4
                };

                // Add tools
                if (PanTool) cornerstoneTools.addTool(PanTool);
                if (WindowLevelTool) cornerstoneTools.addTool(WindowLevelTool);
                if (StackScrollTool) cornerstoneTools.addTool(StackScrollTool);
                if (ZoomTool) cornerstoneTools.addTool(ZoomTool);

                // Create tool group if ToolGroupManager exists
                if (ToolGroupManager && ToolGroupManager.createToolGroup) {
                    const toolGroupId = 'myToolGroup';
                    toolGroup = ToolGroupManager.createToolGroup(toolGroupId);

                    // Add tools to the group
                    if (WindowLevelTool) toolGroup.addTool(WindowLevelTool.toolName);
                    if (PanTool) toolGroup.addTool(PanTool.toolName);
                    if (ZoomTool) toolGroup.addTool(ZoomTool.toolName);
                    if (StackScrollTool) toolGroup.addTool(StackScrollTool.toolName);

                    // Set tool bindings
                    if (WindowLevelTool) {
                        toolGroup.setToolActive(WindowLevelTool.toolName, {
                            bindings: [{
                                mouseButton: MouseBindings.Primary, // Left Click
                            }],
                        });
                    }

                    if (PanTool) {
                        toolGroup.setToolActive(PanTool.toolName, {
                            bindings: [{
                                mouseButton: MouseBindings.Auxiliary, // Middle Click
                            }],
                        });
                    }

                    if (ZoomTool) {
                        toolGroup.setToolActive(ZoomTool.toolName, {
                            bindings: [{
                                mouseButton: MouseBindings.Secondary, // Right Click
                            }],
                        });
                    }

                    if (StackScrollTool) {
                        toolGroup.setToolActive(StackScrollTool.toolName, {
                            bindings: [{ mouseButton: MouseBindings.Wheel }],
                        });
                    }
                }

                // Setup the HTML element
                const element = setupHTML();
                if (!element) {
                    console.error('Element not found');
                    return;
                }

                // Create the rendering engine if the RenderingEngine class exists
                if (cornerstone.RenderingEngine) {
                    const renderingEngineId = 'myRenderingEngine';
                    renderingEngine = new cornerstone.RenderingEngine(renderingEngineId);

                    // Create the viewport
                    const viewportId = 'CT_STACK';
                    const viewportInput = {
                        viewportId,
                        type: cornerstone.Enums.ViewportType.STACK,
                        element,
                        defaultOptions: {
                            background: [0.2, 0, 0.2],
                        },
                    };

                    // Enable the viewport
                    renderingEngine.enableElement(viewportInput);

                    // Get the viewport we created
                    viewport = renderingEngine.getViewport(viewportId);

                    // Add the viewport to the tool group
                    if (toolGroup) {
                        toolGroup.addViewport(viewportId, renderingEngineId);
                    }
                } else {
                    console.error("RenderingEngine not available");
                    return;
                }

                // Mark as initialized
                initialized = true;
                console.log('DICOM viewer initialized successfully');
                return Promise.resolve();
            });
    } catch (error) {
        console.error('Error initializing DICOM viewer:', error);
        return Promise.reject(error);
    }
}

// Load a DICOM file from an array buffer
function loadDicomFileFromArrayBuffer(arrayBuffer, fileName) {
    if (!initialized || !viewport) {
        console.error('DICOM viewer not initialized');
        return Promise.reject('DICOM viewer not initialized');
    }

    try {
        // Use the DICOM Image Loader to create an image ID
        const uint8Array = new Uint8Array(arrayBuffer);
        let imageId;

        if (dicomImageLoader.wadouri.fileManager.addByteArray) {
            imageId = dicomImageLoader.wadouri.fileManager.addByteArray(uint8Array);
        } else if (dicomImageLoader.wadouri.fileManager.add) {
            // Create a File-like object
            const blob = new Blob([uint8Array]);
            const file = new File([blob], fileName);
            console.log(file, fileName);
            imageId = dicomImageLoader.wadouri.fileManager.add(file);
            console.log(imageId);
        } else {
            console.error("No appropriate method to load DICOM file");
            return Promise.reject("No appropriate method to load DICOM file");
        }

        // Create a stack with this image
        const stack = [imageId];

        // Set the stack on the viewport
        viewport.setStack(stack)

        // Render the image
        viewport.render();

        // Update metadata display
        updateMetadataDisplay(imageId);

 

        
        

 

        console.log('DICOM file loaded successfully:', fileName);
        // return Promise.resolve();

    } catch (error) {
        console.error('Error loading DICOM file:', error);
        return Promise.reject(error);
    }
}

 

// Update the metadata display
function updateMetadataDisplay(imageId) {
    try {
        if (!viewport) return;

        const imageData = viewport.getImageData();
        const { metaData } = cornerstone;

        // Get metadata from Cornerstone
        const pixelModule = metaData.get('imagePixelModule', imageId);
        const voiLutModule = metaData.get('voiLutModule', imageId);
        const sopCommonModule = metaData.get('sopCommonModule', imageId);
        const transferSyntax = metaData.get('transferSyntax', imageId);

        // Update UI elements with the metadata
        document.getElementById('transfersyntax').innerHTML = transferSyntax?.transferSyntaxUID || '-';

        if (sopCommonModule?.sopClassUID) {
            const sopClassDesc = uids[sopCommonModule.sopClassUID] || 'Unknown';
            document.getElementById('sopclassuid').innerHTML =
                `${sopCommonModule.sopClassUID} [${sopClassDesc}]`;
        } else {
            document.getElementById('sopclassuid').innerHTML = '-';
        }

        document.getElementById('sopinstanceuid').innerHTML = sopCommonModule?.sopInstanceUID || '-';
        document.getElementById('rows').innerHTML = imageData?.dimensions[0] || '-';
        document.getElementById('columns').innerHTML = imageData?.dimensions[1] || '-';
        document.getElementById('spacing').innerHTML = imageData?.spacing.join('\\') || '-';

        const formattedDirection = imageData?.direction
            ? imageData.direction.map(x => Math.round(x * 100) / 100).join(',')
            : '-';
        document.getElementById('direction').innerHTML = formattedDirection;

        const formattedOrigin = imageData?.origin
            ? imageData.origin.map(x => Math.round(x * 100) / 100).join(',')
            : '-';
        document.getElementById('origin').innerHTML = formattedOrigin;

        document.getElementById('modality').innerHTML = imageData?.metadata?.Modality || '-';
        document.getElementById('pixelrepresentation').innerHTML = pixelModule?.pixelRepresentation || '-';
        document.getElementById('bitsallocated').innerHTML = pixelModule?.bitsAllocated || '-';
        document.getElementById('bitsstored').innerHTML = pixelModule?.bitsStored || '-';
        document.getElementById('highbit').innerHTML = pixelModule?.highBit || '-';
        document.getElementById('photometricinterpretation').innerHTML = pixelModule?.photometricInterpretation || '-';
        document.getElementById('windowcenter').innerHTML = voiLutModule?.windowCenter || '-';
        document.getElementById('windowwidth').innerHTML = voiLutModule?.windowWidth || '-';
    } catch (error) {
        console.error('Error updating metadata display:', error);
    }
}

// Load a DICOM file from a byte array (called from Blazor)
function loadDicomFileFromArray(byteArray, fileName) {
    const arrayBuffer = new Uint8Array(byteArray).buffer;
    return loadDicomFileFromArrayBuffer(arrayBuffer, fileName);
}

// Reset the viewer
function resetViewer() {
    if (viewport) {
        viewport.reset();
        viewport.render();

        // Reset the metadata display
        const elements = [
            'transfersyntax', 'sopclassuid', 'sopinstanceuid', 'rows', 'columns',
            'spacing', 'direction', 'origin', 'modality', 'pixelrepresentation',
            'bitsallocated', 'bitsstored', 'highbit', 'photometricinterpretation',
            'windowcenter', 'windowwidth'
        ];

        elements.forEach(id => {
            document.getElementById(id).innerHTML = '-';
        });

        return Promise.resolve();
    }
    return Promise.reject("Viewport not initialized");
}

// Add to window for accessibility from Blazor
window.dicomViewerInterop = {
    initDicomViewer: initDicomViewer,
    loadDicomFileFromArray: loadDicomFileFromArray,
    resetViewer: resetViewer
};

// Log when the script is loaded
console.log("Cornerstone interop script loaded");

// Check if DOM is already loaded, otherwise wait for it
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', checkLibraryLoading);
} else {
    checkLibraryLoading();
}

i am able to load the cornerstone modules from my bundle file , but unable to get metadata and unable to render the image.

No RepliesBe the first to reply

Resources