⚠️ NOT FOR CLINICAL USE — This software is not a certified medical device. It has not been validated for diagnostic or therapeutic use under any regulatory framework (FDA 510(k), CE marking, MDR, etc.). Use at your own risk.
A pure-Rust port of DCMTK 3.7.0 — a comprehensive DICOM medical imaging toolkit. The port targets feature parity across the four core DCMTK tiers and applies idiomatic Rust patterns throughout.
This is an independent project, not affiliated with or endorsed by OFFIS e.V. See NOTICE for attribution details.
| Tier | Scope | Tests |
|---|---|---|
| 1 — Foundation | dicom-toolkit-core, dicom-toolkit-dict |
43 + 38 |
| 2 — Data model & I/O | dicom-toolkit-data |
153 |
| 3 — Networking | dicom-toolkit-net |
59 |
| 4 — Imaging & codecs | dicom-toolkit-image, dicom-toolkit-codec, dicom-toolkit-jpeg2000 |
44 + 89 + 46 |
| Tools | dicom-toolkit-tools |
10 integration |
| Total | 482 unit/integration + 6 doctests = 488 passing, 0 failed |
| Crate | Ports from DCMTK | Description |
|---|---|---|
dicom-toolkit-core |
ofstd, oficonv, oflog |
Error types, UIDs, full character set support (ISO 2022 + single-byte + UTF-8), logging |
dicom-toolkit-dict |
dcmdata (dict) |
90+ tag constants, all 34 VRs, 13 transfer syntaxes, SOP class UID registry |
dicom-toolkit-data |
dcmdata |
DICOM data model, Part 10 file reader/writer, DICOM JSON (PS3.18), XML, deflate |
dicom-toolkit-net |
dcmnet, dcmtls |
Async DICOM networking: PDU layer, association, C-ECHO/STORE/FIND/GET/MOVE, TLS |
dicom-toolkit-image |
dcmimgle, dcmimage |
Pixel pipeline, Modality/VOI LUT, window/level, overlays, color models, PNG export |
dicom-toolkit-codec |
dcmjpeg, dcmjpls, dcmrle, dcmjp2k |
JPEG baseline, pure-Rust JPEG-LS, pure-Rust JPEG 2000 (lossless & lossy), RLE PackBits, codec registry |
dicom-toolkit-tools |
dcmdump, echoscu, etc. |
CLI utilities: dump, network SCU/SCP including getscu, img2dcm, JPEG-LS/JPEG 2000 compress/decompress (see below) |
dicom-toolkit-jpeg2000 |
internal/published fork | Pure-Rust JPEG 2000 engine used by dicom-toolkit-codec; published fork with native-bit-depth decode plus DICOM-focused encoder |
- Rust 1.80 or later
cargo
No C/C++ compiler or external native libraries are required — all dependencies are pure Rust or bundled.
# Build the whole workspace
cargo build --workspace
# Build CLI tools only
cargo build --bins
# Run all tests
cargo test --workspaceFor crates.io releases, use the helper script from the repository root:
# Show the publish order and versions
bash scripts/publish-crates.sh --plan
# Publish all crates in dependency order
bash scripts/publish-crates.sh
# Resume after a partial publish
bash scripts/publish-crates.sh --from dicom-toolkit-netThere is also a manual GitHub Actions workflow named Publish crates which runs the
same release order with a repository CARGO_REGISTRY_TOKEN secret.
use dicom_toolkit_data::FileFormat;
use dicom_toolkit_dict::tags;
let file = FileFormat::open("image.dcm")?;
let ds = file.dataset();
if let Some(name) = ds.get_string(tags::PATIENT_NAME) {
println!("Patient: {name}");
}
let rows = ds.get_u16(tags::ROWS).unwrap_or(0);
let columns = ds.get_u16(tags::COLUMNS).unwrap_or(0);
println!("Size: {columns}×{rows}");use dicom_toolkit_data::{DataSet, FileFormat};
use dicom_toolkit_dict::{tags, transfer_syntaxes as ts};
use dicom_toolkit_core::uid::Uid;
let mut ds = DataSet::new();
ds.set_string(tags::PATIENT_NAME, "Doe^John")?;
ds.set_string(tags::PATIENT_ID, "12345")?;
ds.set_u16(tags::ROWS, 512)?;
ds.set_u16(tags::COLUMNS, 512)?;
let sop_uid = Uid::generate("2.25")?.to_string();
let file = FileFormat::new(ds, &ts::EXPLICIT_VR_LITTLE_ENDIAN);
file.save("output.dcm")?;use dicom_toolkit_data::{FileFormat, json::DicomJson};
let file = FileFormat::open("image.dcm")?;
let json = DicomJson::encode(file.dataset())?;
println!("{json}");
let ds2 = DicomJson::decode(&json)?;use dicom_toolkit_image::DicomImage;
let image = DicomImage::from_file("ct.dcm")?;
let frame = image.render_frame(0, None)?; // applies Modality + VOI LUT
frame.save_png("ct_frame0.png")?;use dicom_toolkit_codec::registry::GLOBAL_REGISTRY;
use dicom_toolkit_dict::transfer_syntaxes as ts;
let codec = GLOBAL_REGISTRY.get(ts::RLE_LOSSLESS.uid)?;
let raw = codec.decode(&compressed_bytes, width, height, samples)?;use dicom_toolkit_net::{association::Association, config::AssociationConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cfg = AssociationConfig::default();
let mut assoc = Association::request("pacs.example.com:11112", cfg).await?;
assoc.c_echo().await?;
assoc.release().await?;
Ok(())
}DICOM strings use Specific Character Set (0008,0005) to indicate encoding. dicom-toolkit-rs handles this transparently — the reader decodes to UTF-8 on input, the writer re-encodes on output.
| DICOM Term | Encoding | Notes |
|---|---|---|
(empty) / ISO_IR 6 |
ASCII | Default |
ISO_IR 100 |
Latin-1 (Windows-1252 superset) | Western European |
ISO_IR 101 |
Latin-2 (ISO 8859-2) | Central European |
ISO_IR 109 |
Latin-3 (ISO 8859-3) | South European |
ISO_IR 110 |
Latin-4 (ISO 8859-4) | North European |
ISO_IR 144 |
Cyrillic (ISO 8859-5) | Russian, etc. |
ISO_IR 127 |
Arabic (ISO 8859-6) | |
ISO_IR 126 |
Greek (ISO 8859-7) | |
ISO_IR 138 |
Hebrew (ISO 8859-8) | |
ISO_IR 148 |
Latin-5 (ISO 8859-9) | Turkish |
ISO_IR 166 |
Thai (TIS 620) | |
ISO_IR 203 |
Latin-9 (ISO 8859-15) | Adds €, Œ, Ÿ |
ISO_IR 192 |
UTF-8 | Recommended for new data |
GB18030 |
Chinese (GB 18030) | |
GBK |
Chinese (GBK) |
ISO 2022 extensions (multi-charset via escape sequences) are fully supported for Japanese (JIS X 0201/0208/0212), Korean (KS X 1001), and Simplified Chinese (GB 2312).
All string data is stored internally as Rust String (UTF-8), with encoding/decoding occurring at I/O time. Round-trip fidelity is tested for all supported charsets.
All tools are built as part of cargo build --bins and placed in target/debug/ (or target/release/ with --release).
dcmdump [OPTIONS] <FILE>...
Options:
-M, --meta Also print File Meta Information header
-n, --no-limit Do not limit string value output length
--json Output as DICOM JSON
--xml Output as DICOM XML
-v, --verbose Verbose output
Examples
# Human-readable dump
dcmdump image.dcm
# Include file meta group (0002,xxxx)
dcmdump --meta image.dcm
# Export as DICOM JSON
dcmdump --json image.dcm > image.json
# Dump multiple files
dcmdump *.dcmechoscu [OPTIONS] <HOST> <PORT>
Options:
-a, --aetitle <AE> Calling AE title [default: ECHOSCU]
-c, --called-ae <AE> Called AE title [default: ANY-SCP]
-r, --repeat <N> Number of C-ECHO requests [default: 1]
-v, --verbose
Examples
# Verify connectivity to a PACS
echoscu pacs.example.com 11112
# Custom AE titles, send 3 pings
echoscu -a MY_SCU -c ORTHANC -r 3 localhost 4242storescu [OPTIONS] <HOST> <PORT> <FILE>...
Options:
-a, --aetitle <AE> Calling AE title [default: STORESCU]
-c, --called-ae <AE> Called AE title [default: ANY-SCP]
-v, --verbose
Examples
# Send one file
storescu pacs.example.com 11112 image.dcm
# Send a whole study directory
storescu -a MY_SCU -c ORTHANC localhost 4242 study/*.dcmstorescp [OPTIONS] <PORT>
Options:
-a, --aetitle <AE> Called AE title [default: STORESCP]
-d, --output-dir <DIR> Directory to save received files [default: .]
-v, --verbose
Examples
# Listen on port 11112, save to /tmp/incoming
storescp -d /tmp/incoming 11112
# Run in background (receives from storescu above)
storescp -v 4242 &
storescu localhost 4242 *.dcmfindscu [OPTIONS] <HOST> <PORT>
Options:
-a, --aetitle <AE> Calling AE title [default: FINDSCU]
-c, --called-ae <AE> Called AE title [default: ANY-SCP]
-k, --key <TAG=VALUE> Query attribute (repeatable), e.g. "0010,0010=Smith*"
-L, --level <LEVEL> PATIENT | STUDY | SERIES | IMAGE [default: STUDY]
-v, --verbose
Examples
# Find all studies for patients whose name starts with "Smith"
findscu -k "0010,0010=Smith*" pacs.example.com 11112
# Find a specific study by date, at patient level
findscu -L PATIENT -k "0010,0010=" -k "0008,0020=20240101" localhost 4242getscu [OPTIONS] <HOST> <PORT>
Options:
-a, --aetitle <AE> Calling AE title [default: GETSCU]
-c, --called-ae <AE> Called AE title [default: ANY-SCP]
-d, --output-dir <DIR> Directory for retrieved DICOM files [default: .]
-k, --key <TAG=VALUE> Query key (repeatable)
-L, --level <LEVEL> Query/retrieve level [default: STUDY]
-v, --verbose Verbose output
Examples
# Retrieve all instances for a study into ./retrieved
getscu -d retrieved -L STUDY -k "0020,000D=1.2.3.4.5" pacs.example.com 11112
# Retrieve matching series from a local PACS
getscu -a MY_SCU -c ORTHANC -d out -L SERIES -k "0020,000E=1.2.3.4.5.6" localhost 4242Retrieved objects are written as DICOM Part 10 files named after their SOP Instance UID.
img2dcm [OPTIONS] <INPUT> [OUTPUT]
Arguments:
<INPUT> Input PNG file
[OUTPUT] Output DICOM file [default: <input>.dcm]
Options:
-p, --patient-name <NAME> [default: Anonymous]
-P, --patient-id <ID>
-s, --study-description <TEXT>
-S, --series-description <TEXT>
--sop-class <UID> Override SOP Class UID
--sop-instance <UID> Override SOP Instance UID
-v, --verbose
Examples
# Wrap a PNG as a Secondary Capture DICOM file
img2dcm photo.png
# With patient metadata
img2dcm -p "Doe^John" -P "12345" -s "Chest X-Ray" chest.png chest.dcmdcmcjpls [OPTIONS] <INPUT> <OUTPUT>
Arguments:
<INPUT> Input DICOM file (uncompressed or decompressible)
<OUTPUT> Output DICOM file (JPEG-LS compressed)
Options:
-n, --max-deviation <NEAR> Max pixel error; 0 = lossless [default: 0]
-l, --encode-lossless Force lossless mode (NEAR=0)
--encode-nearlossless Near-lossless mode (default NEAR=2)
-v, --verbose
Examples
# Lossless JPEG-LS compression (default)
dcmcjpls image.dcm image_jls.dcm
# Near-lossless with max deviation of 3
dcmcjpls -n 3 -v image.dcm image_lossy.dcm
# Recode HTJ2K DICOM into JPEG-LS
dcmcjpls image_htj2k.dcm image_htj2k_jls.dcm
# Batch compress all files in a directory
for f in study/*.dcm; do dcmcjpls "$f" "compressed/$(basename $f)"; donedcmdjpls [OPTIONS] <INPUT> <OUTPUT>
Arguments:
<INPUT> Input DICOM file (JPEG-LS compressed)
<OUTPUT> Output DICOM file (Explicit VR Little Endian)
Options:
-v, --verbose
Examples
# Decompress a JPEG-LS file
dcmdjpls image_jls.dcm image_raw.dcm
# Verbose — show image parameters and compression ratio
dcmdjpls -v image_jls.dcm image_raw.dcm
# Round-trip: compress then decompress
dcmcjpls -v image.dcm /tmp/compressed.dcm
dcmdjpls -v /tmp/compressed.dcm /tmp/roundtrip.dcmdcmcjp2k [OPTIONS] <INPUT> <OUTPUT>
Arguments:
<INPUT> Input DICOM file (uncompressed or decompressible)
<OUTPUT> Output DICOM file (JPEG 2000 or HTJ2K compressed)
Options:
-l, --encode-lossless Force lossless JPEG 2000 (default)
--encode-lossy Use irreversible JPEG 2000
--htj2k Use High-Throughput JPEG 2000; combine with --encode-lossy for TS .203
-v, --verbose
Examples
# Lossless JPEG 2000 compression (TS .90)
dcmcjp2k image.dcm image_j2k.dcm
# Irreversible/lossy JPEG 2000 (TS .91)
dcmcjp2k --encode-lossy -v image.dcm image_j2k_lossy.dcm
# HTJ2K lossless (TS .201)
dcmcjp2k --htj2k image.dcm image_htj2k.dcm
# HTJ2K generic transfer syntax (TS .203)
dcmcjp2k --htj2k --encode-lossy -v image.dcm image_htj2k_lossy.dcm
# Batch compress
for f in study/*.dcm; do dcmcjp2k "$f" "jp2k/$(basename "$f")"; doneHTJ2K encode currently emits TS .201 for lossless and TS .203 for
--encode-lossy. RPCL HTJ2K TS .202 remains decode-only until the
codestream writer supports RPCL progression order.
HTJ2K lossless output is self-validated before dcmcjp2k writes it. If the
current encoder cannot produce a decodable lossless codestream for a given
image, the command fails with an explicit error instead of emitting a corrupt
file.
dcmdjp2k [OPTIONS] <INPUT> <OUTPUT>
Arguments:
<INPUT> Input DICOM file (JPEG 2000 / HTJ2K compressed)
<OUTPUT> Output DICOM file (Explicit VR Little Endian)
Options:
-v, --verbose
Examples
# Decompress a JPEG 2000 file
dcmdjp2k image_j2k.dcm image_raw.dcm
# Verbose round-trip
dcmcjp2k -v image.dcm /tmp/compressed_j2k.dcm
dcmdjp2k -v /tmp/compressed_j2k.dcm /tmp/roundtrip.dcmReady-to-run scripts live in examples/scripts/ and use the five ABDOM CT slices in examples/testfiles/.
| Script | What it demonstrates |
|---|---|
01_dump |
All dcmdump output modes: plain, --meta, --no-limit, --json, --xml, multi-file batch |
02_network |
Start storescp → C-ECHO verify with echoscu → send all 5 slices with storescu → inspect received files |
03_query |
findscu and getscu command patterns; set RUN_LIVE=1 / $env:RUN_LIVE='1' to query a real PACS |
04_img2dcm |
Generate a PNG with Python stdlib → wrap as Secondary Capture → dump + JSON export |
05_jpegls |
JPEG-LS lossless & near-lossless round-trip, batch compress/decompress, metadata verification |
06_jp2k |
JPEG 2000 lossless round-trip, lossy smoke test, batch compress/decompress, metadata verification |
demo |
Master script — runs all six above in order |
Two equivalent versions are provided for each script:
# Run the full demo
bash examples/scripts/demo.sh
# Or run individual scripts
bash examples/scripts/01_dump.sh
bash examples/scripts/02_network.sh
bash examples/scripts/04_img2dcm.sh
bash examples/scripts/05_jpegls.sh
bash examples/scripts/06_jp2k.shRequires PowerShell 7+ (pwsh) or Windows PowerShell 5.1.
On first run you may need to allow local scripts:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned# Run the full demo
pwsh -File examples/scripts/demo.ps1
# Run with a pause between sections
pwsh -File examples/scripts/demo.ps1 -Pause
# Or run individual scripts
pwsh -File examples/scripts/01_dump.ps1
pwsh -File examples/scripts/02_network.ps1
pwsh -File examples/scripts/04_img2dcm.ps1
pwsh -File examples/scripts/05_jpegls.ps1
pwsh -File examples/scripts/06_jp2k.ps1The PowerShell scripts also work on macOS and Linux with PowerShell Core.
Note:
03_queryshows command-line patterns but does not execute live retrievals by default. Thestorescpbinary now uses theDicomServerframework. C-FIND, C-GET, and C-MOVE SCP handling is available in-process via the library; see DicomServer below. SetRUN_LIVE=1to use an external Orthanc instance with the query scripts.
The port maps DCMTK's deep C++ class hierarchy to idiomatic Rust:
| DCMTK C++ | Rust equivalent |
|---|---|
OFCondition / exception |
thiserror DcmError enum + DcmResult<T> |
DcmObject → DcmElement → DcmByteString … |
Value enum (21 variants) + Element struct |
DcmDataset (std::map) |
DataSet backed by IndexMap<Tag, Element> |
DcmFileFormat |
FileFormat struct |
OFString / OFList |
String / Vec<T> |
oflog / log4cplus |
tracing + tracing-subscriber |
oficonv |
encoding_rs |
DcmTransportLayer (OpenSSL) |
rustls + tokio-rustls |
| Blocking socket I/O | tokio async I/O |
| CharLS (C++ JPEG-LS) | Pure Rust JPEG-LS codec (ISO 14495-1) |
dicom-toolkit-codec includes a pure-Rust JPEG-LS codec ported from the CharLS algorithm bundled with DCMTK. No C/C++ dependencies — works on any Rust target including WASM.
Supported features:
- Lossless mode (DICOM TS
1.2.840.10008.1.2.4.80) - Near-lossless/lossy mode (DICOM TS
1.2.840.10008.1.2.4.81) - 2–16 bit depths (DICOM commonly uses 8, 12, 16)
- Grayscale and multi-component images (1–4 components)
- Interleave modes: ILV_NONE, ILV_LINE
- HP color transforms (APP8 marker)
Architecture (10 modules, ~1,200 LOC):
| Module | Purpose |
|---|---|
params.rs |
Parameters, threshold computation (ISO §C.2.4.1.1) |
sample.rs |
Sample trait for u8/u16 bit-depth dispatch |
bitstream.rs |
BitReader/BitWriter with FF-bitstuffing |
context.rs |
Context statistics (A, B, C, N) + run-mode context |
golomb.rs |
Golomb-Rice coding (encode/decode mapped errors) |
prediction.rs |
Median-edge predictor, gradient quantization |
marker.rs |
JPEG-LS marker parsing/writing (SOF-55, SOS, LSE) |
scan.rs |
Core scan encoder/decoder (line-by-line processing) |
decoder.rs |
Top-level decoder: markers → scan decoder → pixels |
encoder.rs |
Top-level encoder: pixels → scan encoder → bitstream |
dicom-toolkit-codec also provides classic JPEG support for DICOM transfer
syntaxes used in older archives and interoperability paths.
Supported features:
- JPEG Baseline / Extended decode and encode for
1.2.840.10008.1.2.4.50and1.2.840.10008.1.2.4.51 - Classic JPEG Lossless (Process 14) decode and encode for
1.2.840.10008.1.2.4.57and1.2.840.10008.1.2.4.70 - Grayscale and RGB codec coverage in the crate test suite
- Registry-level capability reporting via
supported_decode_transfer_syntaxes()andsupported_encode_transfer_syntaxes()
dicom-toolkit-codec now includes pure-Rust JPEG 2000 support backed by the in-workspace dicom-toolkit-jpeg2000 fork. No C/C++ bindings are used.
Supported features:
- DICOM transfer syntaxes
1.2.840.10008.1.2.4.90(lossless) and1.2.840.10008.1.2.4.91 - Native-bit-depth encode/decode for 8-, 12-, and 16-bit medical images
- Grayscale and RGB pixel data
- Library-level encode/decode plus CLI tools
dcmcjp2kanddcmdjp2k - Multi-fragment decode (one codestream per frame) in the codec/tools path
- HTJ2K decode for transfer syntaxes
.201,.202, and.203, plus encode for.201and.203 - Real-DICOM HTJ2K encode/decode and CLI roundtrip coverage using
examples/testfiles/ABDOM_*.dcm
Current scope:
- JPEG 2000 Part 1 codestreams aimed at common DICOM usage
- Single quality layer in the current encoder
- HTJ2K lossless encode/decode now routes through
openjph-corefor DICOM-style codestreams; the test matrix includes real DICOM registry and CLI regressions - Lossless and basic lossy mode; quality tuning is not yet exposed as a user-facing option
dicom-toolkit-net now ships a generic DicomServer for building full PACS SCPs.
It manages concurrent TCP associations, request routing, and graceful shutdown.
You plug in your own logic via provider traits; the library handles all DICOM protocol mechanics.
use dicom_toolkit_net::server::{DicomServer, FileStoreProvider};
#[tokio::main]
async fn main() {
let server = DicomServer::builder()
.ae_title("MYPACS")
.port(4242)
.store_provider(FileStoreProvider::new("/data/dicom"))
.build()
.await
.expect("bind port");
server.run().await.expect("server error");
}Implement one or more of these traits to add your own business logic:
| Trait | Service | Callback |
|---|---|---|
StoreServiceProvider |
C-STORE | async fn on_store(&self, StoreEvent) -> StoreResult |
FindServiceProvider |
C-FIND | async fn on_find(&self, FindEvent) -> Vec<DataSet> |
GetServiceProvider |
C-GET | async fn on_get(&self, GetEvent) -> Vec<RetrieveItem> |
MoveServiceProvider |
C-MOVE | async fn on_move(&self, MoveEvent) -> Vec<RetrieveItem> |
C-ECHO is always handled automatically without a trait.
DicomServer::builder()
.ae_title("MYPACS") // AE title (default: "DICOMRS")
.port(4242) // TCP port (default: 4242)
.max_associations(100) // Max concurrent associations
.store_provider(my_store) // C-STORE SCP
.find_provider(my_query) // C-FIND SCP
.get_provider(my_get) // C-GET SCP
.move_provider(my_move) // C-MOVE SCP
.move_destination_lookup( // AE→host:port for C-MOVE sub-associations
StaticDestinationLookup::new(vec![
("STORESCP".into(), "10.0.0.1:4242".into()),
])
)
.build()
.await?let token = server.cancellation_token();
// In another task or signal handler:
token.cancel(); // server.run() returns cleanlyShips ready to use — receives DICOM instances and saves them as .dcm files:
.store_provider(FileStoreProvider::new("/tmp/incoming"))- Worklist / MPPS: not yet ported.
- JPEG-LS ILV_SAMPLE: pixel-interleaved multi-component mode not yet supported (ILV_NONE and ILV_LINE work).
- JPEG 2000 advanced profiles: Multi-layer HT decoding and Part 2 multi-component features are not implemented yet. HTJ2K encode is supported for
.201and.203, while.202remains decode-only because the current codestream writer emits LRCP rather than RPCL. Broader external-fixture and interoperability coverage still remains. - JPEG 2000 lossy tuning: current tools expose lossless vs. lossy mode, but not quality/rate controls yet.
Licensed under either of Apache License 2.0 or MIT License at your option.
See NOTICE for attribution of algorithmic references (DCMTK, CharLS).
See CONTRIBUTING.md for guidelines.
See SECURITY.md for vulnerability reporting and DICOM security guidance.