dicom_toolkit 0.2.5
dicom_toolkit: ^0.2.5 copied to clipboard
An advanced medical imaging and DICOM processing library for Flutter.
dicom_toolkit #
A composable DICOM imaging toolkit for Flutter. GPU-accelerated rendering, interactive measurement, and clinical precision — across mobile, desktop, and web.
Forked from MostafaSensei106/Flutter-Dicom — the original Rust+DICOM FFI bridge, GPU shader, and 16-bit precision pipeline. This project extends it with a composable toolkit API, web support, measurement tools, rotation, pixel-spacing-aware rulers, and modality-agnostic windowing presets. Licensed under GPL v3 in compliance with the original.
Live example → dicomtoolkit.pages.dev — try the toolkit in your browser.
The kit #
| Tool | What it does |
|---|---|
DicomToolkit |
Entry point — init() loads the native engine, render() is a one-liner |
DicomParser |
Parses DICOM bytes (or files) into DicomParseResult (metadata + pixels) |
DicomDecoder |
Pluggable decoder interface — default RustDecoder hits the Rust FFI |
DicomRenderer |
GPU pipeline: compile shader → pack 16-bit texture → apply windowing + color LUT |
DicomRoi |
Draw a rectangle, get RoiStatistics (min, max, mean, stdDev, median) |
DicomRuler |
Measure distance in mm using pixel spacing (with Imager Pixel Spacing fallback for X-ray) |
DicomExport |
Render + encode to PNG bytes — works on web (no dart:io) |
DicomWindowPreset |
Named presets: CT Hounsfield + modality-agnostic forImage() from pixel range |
DicomViewerController |
Reactive state manager — loading, windowing, color, invert, rotation |
DicomViewer |
Interactive widget: GPU rendering, pinch-zoom, windowing drag, rotation |
Data types #
| Type | Description |
|---|---|
DicomTagId |
Immutable DICOM tag (group, element) — 31 predefined constants |
DicomMetadata |
35 typed getters + generic tag() lookup + bestDate resolution |
DicomPixelData |
Sealed class — DicomInt16PixelData for 16-bit monochrome |
DicomParseResult |
Parsed frame: metadata + pixelData + frame() + computeRoi() |
DicomColorMap |
Enum: grayscale, hotIron, pet, rainbow, cool, bone |
RoiStatistics |
pixelCount, min, max, mean, stdDev, median |
Quick start #
import 'package:dicom_toolkit/dicom_toolkit.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await DicomToolkit.init();
runApp(const MyApp());
}
One-liner render #
final parser = const DicomParser();
final result = await parser.parse(fileBytes);
final image = await DicomToolkit.render(result,
windowCenter: 40, windowWidth: 400,
);
Full viewer #
final controller = DicomViewerController();
await controller.loadFromBytes(bytes: fileBytes);
// In your widget tree:
DicomViewer(controller: controller)
// Don't forget:
@override void dispose() { controller.dispose(); super.dispose(); }
Metadata-only (no GPU) #
final info = await readDicomInfo('/path/to/scan.dcm');
print(info.metadata.patientName); // "John Doe"
print(info.metadata.modality); // "CT"
Every tool, with examples #
DicomParser — parse DICOM bytes #
const parser = DicomParser(); // default: RustDecoder
// const parser = DicomParser(decoder: myDecoder); // inject custom backend
final result = await parser.parse(bytes); // DicomParseResult (metadata + pixels)
final meta = await parser.parseMetadata(bytes); // DicomMetadata only — skipPixels: true, no GPU needed
DicomRenderer — GPU rendering pipeline #
final renderer = DicomRenderer(
colorMap: DicomColorMap.grayscale,
invert: false,
);
final texture = await renderer.createTexture(result); // pack 16-bit → RGBA
final image = await renderer.render(result, // full pipeline
windowCenter: 40, windowWidth: 400,
colorMap: DicomColorMap.hotIron,
rotationSteps: 1, // 90° CW
);
DicomWindowPreset — modality-agnostic presets #
// Image-adaptive presets (works on CT, MR, X-ray, US):
final presets = DicomWindowPreset.forImage(result);
// → [Full Range, DICOM Default, Mid 50%, Narrow 25%, High, Low]
// Or use CT-specific Hounsfield presets:
const bone = DicomWindowPreset.bone(); // W:2000, C:500
const lung = DicomWindowPreset.lung(); // W:1500, C:-500
// All built-in CT presets:
DicomWindowPreset.all; // bone, lung, softTissue, brain, liver, mediastinum, spine
DicomRoi — region of interest statistics #
const roi = DicomRoi(x: 100, y: 100, width: 50, height: 50);
final stats = roi.compute(result);
// or: final stats = result.computeRoi(roi);
print(stats.mean); // 342.5
print(stats.stdDev); // 87.3
print(stats.min); // 102
print(stats.max); // 1892
print(stats.median); // 298
print(stats.pixelCount); // 2500
DicomRuler — real-world distance #
const ruler = DicomRuler();
// Measure between two pixel points:
final mm = ruler.measure(result.metadata,
(x: 100, y: 200), // DicomPoint = ({double x, double y})
(x: 350, y: 200),
);
print('${mm.toStringAsFixed(1)} mm'); // e.g. "125.3 mm"
// Uses Pixel Spacing (0028,0030) — falls back to Imager Pixel Spacing
// (0018,1164) for CR/DX X-ray files. Falls back to pixel units if neither
// tag is present.
DicomExport — PNG output (web-safe) #
const exporter = DicomExport();
final pngBytes = await exporter.toPngBytes(result,
windowCenter: 40,
windowWidth: 400,
colorMap: DicomColorMap.grayscale,
invert: false,
rotationSteps: controller.rotationSteps, // matches display
);
// Write to disk, share, or trigger a browser download — your choice.
DicomViewerController — reactive state #
final controller = DicomViewerController();
// Windowing
controller.updateWindowing(center: 40, width: 400);
controller.adjustWindowing(deltaX: 10, deltaY: -5); // drag gesture
controller.resetWindowing(); // back to DICOM defaults
controller.applyPreset(center: preset.center, width: preset.width);
// Rotation
controller.rotateClockwise(); // 0° → 90° → 180° → 270° → 0°
controller.rotateCounterClockwise(); // reverse
controller.rotationSteps; // 0–3
// Display
await controller.setColorMap(DicomColorMap.hotIron);
controller.toggleInvert();
controller.isMonochrome1; // true for CR/DX
// Lifecycle
controller.clear(); // frees current image
controller.dispose(); // frees GPU resources
DicomViewer — interactive widget #
DicomViewer(
controller: controller,
interactive: true, // enables drag-windowing + pinch-zoom
fit: BoxFit.contain,
)
// Set interactive: false when an outer widget needs to capture
// taps/pans for measurement tools.
DicomTagId — tag constants for generic lookup #
// 31 predefined constants:
DicomTagId.patientName // (0010,0010)
DicomTagId.modality // (0008,0060)
DicomTagId.pixelSpacing // (0028,0030)
DicomTagId.imagerPixelSpacing // (0018,1164)
DicomTagId.toothNumber // (0018,6032) — ISO 3950 FDI
// Create custom tags:
const tag = DicomTagId.fromParts(0x0020, 0x0032); // ImagePositionPatient
final tag = DicomTagId.fromHex('00100010'); // PatientName
// Generic lookup:
meta.tag(DicomTagId.patientName); // "John Doe"
meta.allTags; // Map<DicomTagId, String>
bestDate — resolve the most relevant date #
// DICOM files can carry up to four date tags: Study Date (0008,0020),
// Series Date (0008,0021), Acquisition Date (0008,0022), and
// Content Date (0008,0023). `bestDate` returns the most recent valid
// one that is not in the future:
final date = result.metadata.bestDate; // DateTime? — null if none valid
if (date != null) {
print('${date.year}-${date.month.toString().padLeft(2, '0')}-'
'${date.day.toString().padLeft(2, '0')}');
}
// All four raw date strings are also available as typed getters:
print(result.metadata.studyDate); // "20240315" or "Unknown"
print(result.metadata.seriesDate); // "20240314" or "Unknown"
print(result.metadata.acquisitionDate); // "20240314" or "Unknown"
print(result.metadata.contentDate); // "20240315" or "Unknown"
37 extracted DICOM tags #
| Category | Tags |
|---|---|
| Patient | patientId, patientName, studyDescription |
| Dates | studyDate, seriesDate, acquisitionDate, contentDate |
| Dental | toothInfo (tries standard Tooth Number, Tooth Region, then 6 vendor private tags) |
| Equipment | modality, manufacturer, manufacturerModelName, institutionName |
| UIDs | studyInstanceUid, seriesInstanceUid, sopInstanceUid |
| Acquisition | seriesDescription, bodyPartExamined, sliceThickness, instanceNumber |
| Image | width, height, samplesPerPixel, bitsAllocated, bitsStored, highBit, pixelRepresentation, photometricInterpretation, pixelSpacing |
| Spatial | imagePositionPatient, imageOrientationPatient, sliceLocation, spacingBetweenSlices |
| Volume | numberOfFrames |
| Windowing | windowCenter, windowWidth, rescaleIntercept, rescaleSlope |
Computed properties #
| Property | Returns | Logic |
|---|---|---|
isMultiFrame |
bool |
numberOfFrames > 1 |
hasSpatialInfo |
bool |
imagePositionPatient non-empty AND (spacingBetweenSlices > 0 OR sliceThickness > 0) |
isVolumetric |
bool |
isMultiFrame OR hasSpatialInfo — true for multi-frame cine/MFSC, enhanced CT/MR, or single slices with 3D placement |
Web support #
The web target compiles Rust to WASM with threading (SharedArrayBuffer). This requires three things:
1. Prerequisites #
Nightly Rust (for build-std with atomics) and wasm-pack:
rustup toolchain install nightly
rustup +nightly target add wasm32-unknown-unknown
cargo install wasm-pack
2. Build WASM #
After any change to rust/src/api/**, rebuild the WASM binary:
cd rust
$env:RUSTUP_TOOLCHAIN = 'nightly'
wasm-pack build --dev --target no-modules --out-name dicom_toolkit
# Copy to both package locations:
Copy-Item pkg/* ../web/pkg/ -Force
Copy-Item pkg/* ../example/web/pkg/ -Force
3. Build + serve the Flutter app #
cd example
flutter build web
python serve_debug.py # starts http://localhost:8080
The serve_debug.py script injects the required cross-origin isolation headers:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
Without these, SharedArrayBuffer is disabled by the browser and the WASM worker pool crashes with DataCloneError.
Web performance note: The SSE bridge copies every
List<int>argument from JS→WASM. For large multi-frame DICOMs (150+ MB), this adds seconds of latency and ~2 GB peak RAM. On native platforms, DCO passes a pointer with zero copy. flutter_rust_bridge 2.12 does not auto-select DCO on web even whencrossOriginIsolated: true— see the upstream issue tracker. As a mitigation, the Rust decoder only extracts the first frame's pixel data (raw-byte fast path for uncompressed DICOM, decoder fallback for compressed).
Shader fallback:
FragmentProgram.fromAsset()is unsupported on CanvasKit (web). The renderer catches the compile failure and applies windowing in pure Dart, displaying the result viaRawImageinstead of the GPU shader.
VS Code launch config #
Alternatively, use this launch configuration to serve from VS Code:
{
"name": "dicom_toolkit (web)",
"type": "dart",
"request": "launch",
"program": "example/lib/main.dart",
"deviceId": "chrome",
"args": [
"--web-header=Cross-Origin-Opener-Policy=same-origin",
"--web-header=Cross-Origin-Embedder-Policy=require-corp"
]
}
License #
GPL v3 — see LICENSE.
Original work copyright (c) 2024 MostafaSensei106 (Flutter-Dicom). Modifications and extensions copyright (c) 2026 dicom_toolkit contributors.