Pure Swift DICOM decoder for iOS and macOS. Read DICOM files, extract medical metadata, and process pixel data without UIKit or Objective-C dependencies.
Suitable for lightweight DICOM viewers, PACS clients, telemedicine apps, and research tools.
- Repository:
ThalesMMS/DICOM-Decoder - Latest release:
1.0.0 - Documentation: Getting Started | Glossary | Troubleshooting
- Overview
- Features
- Performance
- Quick Start
- Installation
- Usage Examples
- Architecture
- Documentation
- Integration
- Contributing
- License
- Support
This project is a full DICOM decoder written in Swift, modernized from a legacy medical viewer. It provides:
- Complete DICOM file parsing (metadata and pixels)
- Pixel extraction for 8-bit, 16-bit grayscale and 24-bit RGB images
- Window/level with medical presets and automatic suggestions
- Modern async/await APIs for non-blocking operations
- File validation before processing
- Zero external dependencies
DICOM (Digital Imaging and Communications in Medicine) is the standard for medical imaging used by CT, MRI, X-ray, ultrasound, and hospital PACS systems.
- Little/Big Endian, Explicit/Implicit VR
- Grayscale 8/16-bit and RGB 24-bit
- Native JPEG Lossless decoding (Process 14, Selection Value 1) for transfer syntaxes 1.2.840.10008.1.2.4.57 and 1.2.840.10008.1.2.4.70
- Best-effort single-frame JPEG and JPEG2000 decoding via ImageIO
- Automatic memory mapping for large files (>10MB)
- Downsampling for fast thumbnail generation
- Parses Image Orientation (Patient) (0020,0037) and Image Position (Patient) (0020,0032); exposes normalized row/column vectors and origin.
- Reads Pixel Spacing (0028,0030) and slice spacing/thickness; exposes spacingX/Y/Z.
- Exposes width/height, bitsAllocated, pixelRepresentation (signed/unsigned), rescale slope/intercept.
- Returns Series Description and raw tag access via
info(for:).
- Directory-level loader that scans
.dcmfiles, orders slices by IPP projection on the IOP normal (fallback: Instance Number), and computes Z spacing from IPP deltas. - Validates single-channel 16-bit geometry consistency and assembles a contiguous volume buffer (signed/unsigned preserved).
- Progress callback per slice and lightweight
DicomSeriesVolumewith voxels, spacing, orientation matrix, origin, rescale parameters, and description.
- Window/Level with medical presets (CT, mammography, PET, and more)
- Automatic preset suggestions based on modality and body part
- Quality metrics (SNR, contrast, dynamic range)
- Basic helpers for contrast stretching and noise reduction (CLAHE placeholder, simple blur)
- Hounsfield Unit conversions for CT images
- Async/await (iOS 13+, macOS 10.15+)
- Validation before loading
- Convenience metadata helpers (patient, study, series)
- Tag caching for frequent lookups
- Complete documentation with practical examples
- DICOM glossary
- Troubleshooting guide for common issues
- Tests covering parsing and series loading
The library uses vDSP (Accelerate framework) as the baseline CPU implementation for window/level operations. vDSP leverages hand-tuned ARM NEON assembly for SIMD operations, providing optimal CPU performance on Apple Silicon and Intel processors.
For applications requiring higher throughput, Metal GPU acceleration delivers significant performance gains over the vDSP baseline:
| Image Size | vDSP (CPU) | Metal (GPU) | Speedup |
|---|---|---|---|
| 512Ă—512 | 2.14 ms | 1.16 ms | 1.84Ă— |
| 1024Ă—1024 | 8.67 ms | 2.20 ms | 3.94Ă— |
Benchmark Environment:
- Hardware: Apple M4 (2024)
- OS: macOS 15+
- Iterations: 100 (after 20 warmup iterations)
- Algorithm: Window/level transformation on 16-bit grayscale DICOM pixels
Key Findings:
- vDSP baseline is optimal - Uses ARM NEON assembly; further CPU SIMD optimizations yield negligible gains
- Metal GPU shines on larger images - 3.94Ă— speedup on 1024Ă—1024 images (typical CT/MRI size)
- Small images favor CPU - 512Ă—512 images show 1.84Ă— speedup due to GPU setup overhead
- Production recommendation - Use vDSP for small images (<800Ă—800), Metal for large medical datasets
Note: Metal GPU acceleration is currently available in the standalone
MetalBenchmarkvalidation tool. Integration into the main library is planned for a future release.
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/ThalesMMS/DICOM-Decoder.git", from: "1.0.0")
]import DicomCore
let decoder = DCMDecoder()
decoder.setDicomFilename("/path/to/image.dcm")
guard decoder.dicomFileReadSuccess else {
print("Failed to load file")
return
}
print("Dimensions: \(decoder.width) x \(decoder.height)")
print("Modality: \(decoder.info(for: 0x00080060))")
print("Patient: \(decoder.info(for: 0x00100010))")
if let pixels = decoder.getPixels16() {
print("\(pixels.count) pixels loaded")
}For a detailed walkthrough, see GETTING_STARTED.md.
- File -> Add Packages...
- Paste
https://github.com/ThalesMMS/DICOM-Decoder.git - Select version
1.0.0or later - Add Package
// Package.swift
dependencies: [
.package(url: "https://github.com/ThalesMMS/DICOM-Decoder.git", from: "1.0.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "DicomCore", package: "DICOM-Decoder")
]
)
]- Swift 5.9+
- iOS 13.0+ or macOS 12.0+
- Xcode 14.0+
import DicomCore
let decoder = DCMDecoder()
decoder.setDicomFilename("/path/to/ct_scan.dcm")
guard decoder.dicomFileReadSuccess else {
print("Load error")
return
}
print("Patient: \(decoder.info(for: 0x00100010))")
print("Modality: \(decoder.info(for: 0x00080060))")
print("Dimensions: \(decoder.width) x \(decoder.height)")
if let pixels = decoder.getPixels16() {
// Process image...
}func loadDICOM() async {
let decoder = DCMDecoder()
let success = await decoder.loadDICOMFileAsync("/path/to/image.dcm")
guard success else { return }
if let pixels = await decoder.getPixels16Async() {
await showImage(pixels, decoder.width, decoder.height)
}
}guard let pixels = decoder.getPixels16() else { return }
let modality = decoder.info(for: 0x00080060)
let suggestions = DCMWindowingProcessor.suggestPresets(for: modality)
let lungPreset = DCMWindowingProcessor.getPresetValues(preset: .lung)
let lungImage = DCMWindowingProcessor.applyWindowLevel(
pixels16: pixels,
center: lungPreset.center,
width: lungPreset.width
)
if let optimal = decoder.calculateOptimalWindow() {
let optimizedImage = DCMWindowingProcessor.applyWindowLevel(
pixels16: pixels,
center: optimal.center,
width: optimal.width
)
}let validation = decoder.validateDICOMFile("/path/to/image.dcm")
if !validation.isValid {
print("Invalid file:")
for issue in validation.issues {
print(" - \(issue)")
}
return
}
decoder.setDicomFilename("/path/to/image.dcm")let patient = decoder.getPatientInfo()
let study = decoder.getStudyInfo()
let series = decoder.getSeriesInfo()if let thumb = decoder.getDownsampledPixels16(maxDimension: 150) {
let thumbWindowed = DCMWindowingProcessor.applyWindowLevel(
pixels16: thumb.pixels,
center: 40.0,
width: 80.0
)
}if let metrics = decoder.getQualityMetrics() {
print("Image quality:")
print(" Mean: \(metrics["mean"] ?? 0)")
print(" Standard deviation: \(metrics["std_deviation"] ?? 0)")
print(" SNR: \(metrics["snr"] ?? 0)")
print(" Contrast: \(metrics["contrast"] ?? 0)")
print(" Dynamic range: \(metrics["dynamic_range"] ?? 0) dB")
}let pixelValue: Double = 1024.0
let hu = decoder.applyRescale(to: pixelValue)
if hu < -500 {
print("Likely air or lung")
} else if hu > 700 {
print("Likely bone")
}More examples: USAGE_EXAMPLES.md.
| Component | Description | Primary Use |
|---|---|---|
DCMDecoder |
Core DICOM decoder | Load files, extract pixels and metadata |
DCMWindowingProcessor |
Image processing | Window/level, presets, quality metrics |
StudyDataService |
Data service | Scan directories, group studies |
DICOMError |
Error system | Typed error handling |
DCMDictionary |
Tag dictionary | Map numeric tags to names |
1. DICOM file
|
2. validateDICOMFile() (optional but recommended)
|
3. setDicomFilename() / loadDICOMFileAsync()
|
4. Decoder parses:
- Header (128 bytes + "DICM")
- Meta Information
- Dataset (tags + values)
- Pixel Data (lazy loading)
|
5. Access data:
- info(for:) -> Metadata
- getPixels16() -> Pixel buffer
- applyWindowLevel() -> Processed pixels
DICOM-Decoder/
|-- Package.swift
|-- Sources/DicomCore/
| |-- DCMDecoder.swift
| |-- DCMWindowingProcessor.swift
| |-- DICOMError.swift
| |-- StudyDataService.swift
| |-- PatientModel.swift
| |-- Protocols.swift
| |-- DCMDictionary.swift
| `-- Resources/DCMDictionary.plist
|-- Tests/DicomCoreTests/
`-- ViewerReference/
| Document | Description | Best For |
|---|---|---|
| Getting Started | End-to-end tutorial | New to DICOM |
| DICOM Glossary | Terminology reference | Understanding terms |
| Troubleshooting | Common issues and fixes | Debugging problems |
| Document | Description | Best For |
|---|---|---|
| Usage Examples | Complete, ready-to-use code samples | Copy and adapt |
| CHANGELOG | Release history | Tracking changes |
Controls brightness and contrast of DICOM images:
- Level (Center): brightness
- Width: contrast
// Lung: Center -600 HU, Width 1500 HU
// Bone: Center 400 HU, Width 1800 HU
// Brain: Center 40 HU, Width 80 HUNumeric identifiers for metadata:
0x00100010 // Patient Name
0x00080060 // Modality (CT, MR, etc.)
0x00280010 // Image heightDensity scale in CT imaging:
- Air: -1000 HU
- Lung: -500 HU
- Water: 0 HU
- Muscle: +40 HU
- Bone: +700 to +3000 HU
- Use background processing for large files:
Task.detached {
await decoder.loadDICOMFileAsync(path)
}- Validate before loading to improve UX:
let validation = decoder.validateDICOMFile(path)
if !validation.isValid {
showError(validation.issues)
}- Use thumbnails for image lists:
let thumb = decoder.getDownsampledPixels16(maxDimension: 150)- Cache decoder instances per study:
var decoders: [String: DCMDecoder] = [:]
decoders[studyUID] = decoder- Release memory during batch processing:
autoreleasepool {
// Process file
}- Compressed transfer syntaxes: Native support for JPEG Lossless (Process 14, Selection Value 1). Best-effort single-frame JPEG/JPEG2000 via ImageIO. RLE and multi-frame encapsulated compression are not supported - convert first if needed.
- Thread safety: The decoder is not thread-safe. Use one instance per thread or synchronize access.
- Very large files (>1GB): May consume significant memory. Process in chunks or downsample.
Native Apple frameworks only:
FoundationCoreGraphicsImageIOAccelerate
Contributions are welcome.
- Fork the repository.
- Create a feature branch (
git checkout -b feature/MyFeature). - Update code in
Sources/DicomCore/and add tests. - Run the tests:
swift test swift build - Commit with a clear message.
- Push to your branch.
- Open a Pull Request.
- Documentation improvements
- Additional test cases
- Bug fixes
- Performance optimizations
- New medical presets
- Internationalization
- Be respectful and constructive.
- Follow Swift code conventions.
- Add tests for new functionality.
- Preserve backward compatibility.
MIT License. See LICENSE for details.
MIT License
Copyright (c) 2024 ThalesMMS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software...
This project originates from the Objective-C DICOM decoder by kesalin. The Swift package modernizes that codebase while preserving credit to the original author.
- Documentation: GETTING_STARTED.md
- Bug reports: GitHub Issues
- Discussions: GitHub Discussions
- Email: Please open an issue first
If this project is useful, consider starring the repository or contributing improvements.