Forum Discussion
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.