wsi-dicom converts whole-slide imaging files that wsi-rs can open into
DICOM VL Whole Slide Microscopy instances. It provides a Rust API, a CLI, and an
optional native GUI.
J2K, the pure-Rust JPEG 2000 codec,
supplies JPEG, JPEG 2000, and HTJ2K codec primitives. wsi-rs
opens vendor WSI formats such as SVS and NDPI. wsi-dicom owns DICOM export,
metadata validation, transfer-syntax routing, reports, and writer errors.
Install the CLI:
cargo install wsi-dicomUse the Rust API:
[dependencies]
wsi-dicom = "0.7.2"GPU support is opt-in:
[dependencies]
wsi-dicom = { version = "0.7.2", features = ["metal"] } # macOS
# or
wsi-dicom = { version = "0.7.2", features = ["cuda"] } # CUDA-capable Linux/WindowsFeature flags:
| Feature | Effect |
|---|---|
default |
CPU-only DICOM export. |
cuda |
Enables CUDA JPEG 2000 encode acceleration when available. wsi-rs CUDA tile decode and direct JPEG-to-HTJ2K CUDA transcode are not exposed by wsi-dicom 0.7.2. |
metal |
Enables Metal JPEG 2000 encode acceleration on macOS, Metal codestream validation decode, and wsi-rs Metal tile decode plumbing. |
For local maximum CPU throughput:
RUSTFLAGS="-C target-cpu=native" cargo build --releaseThe optional GUI lives in apps/wsi-dicom-gui:
cargo run -p wsi-dicom-guiAlways provide metadata JSON/FHIR input or explicitly select research placeholder metadata.
wsi-dicom convert slide.ndpi --out dicom-out --research-placeholder \
--icc source-or-srgbUse --metadata metadata.json for real metadata. --metadata and
--research-placeholder are mutually exclusive. Existing generated .dcm
paths are refused by default; pass --overwrite only when replacement is
intentional. Each conversion writes its complete set to a sibling staging
directory, then promotes the flat .dcm files sequentially under an
output-directory writer lock. The journaled commit is failure-atomic for
ordinary reported errors: newly promoted files are removed and overwritten
files are restored if commit fails. It is not visibility-atomic for concurrent
readers, which may observe a mixed set or a temporarily absent overwritten file
during promotion. An interrupted transaction is recovered before the next
export; a failed rollback returns a recovery-required error and retains its
journal and backups.
Metadata is validated before slide access and output staging. Non-ASCII text is
encoded as UTF-8 and declares DICOM Specific Character Set ISO_IR 192; scalar
text rejects DICOM value delimiters and control characters. The
imaged_volume_depth_mm input is always millimeters. It is converted to
micrometers for Imaged Volume Depth (FL) and remains millimeters for Slice
Thickness (DS).
Important
Regenerate output produced by wsi-dicom 0.7.0 or earlier if it used imaged volume depth (including the 0.001 mm default) or non-ASCII metadata. Those versions could write the depth with the wrong Imaged Volume Depth units or omit the required Specific Character Set declaration.
Per-frame functional-group and extended-offset-table metadata is bounded by
default to 256 MiB per instance and 1 GiB across one export. Trusted large
workloads can override these limits for convert and sustain-convert:
wsi-dicom convert slide.ndpi --out dicom-out --metadata metadata.json \
--max-instance-metadata-mib 512 --max-total-metadata-mib 2048Library callers can set ExportOptions::max_instance_metadata_bytes and
ExportOptions::max_total_metadata_bytes, or use the corresponding Export
builder methods. The writer preflights both limits and enforces the per-instance
limit while streaming metadata.
Generated DICOM UIDs are fresh for each conversion. Reproducible pipelines may
opt into full source-content/configuration identity with
--uid-policy deterministic; this hashes the complete source and is therefore
more expensive on large slides. The deterministic identity also includes the
SHA-256 digest of the effective ICC profile for each generated instance.
DicomMetadata::specimen_uid accepts a governed Specimen UID and preserves it
verbatim across exports. When it is absent, the exporter derives the Specimen
UID from the export identity, specimen identifier, and structured identifier
issuer. Fresh exports therefore receive fresh fallback Specimen UIDs,
deterministic exports repeat them, and issuer namespaces keep otherwise equal
local identifiers separate. FHIR Specimen.identifier.system is emitted as a
universal issuer of type URI.
Every export requires an explicit color-management choice. The CLI defaults to
--icc source-or-srgb for research use; calibrated workflows should select
--icc require-source, a governed calibration registry, or an explicit
profile. Registry and explicit-profile modes are mutually exclusive:
wsi-dicom convert slide.ndpi --out dicom-out --metadata metadata.json \
--icc-calibration-registry calibration/registry.json \
--icc-conflict fail
wsi-dicom convert slide.ndpi --out dicom-out --metadata metadata.json \
--icc-profile vendor-profile.icc --icc-profile-id lab-at2-2026q3 \
--icc-conflict prefer-configuredPortable registries use schema version 1:
{
"schema_version": 1,
"calibrations": [
{
"id": "lab-at2-sn123-2026q3",
"scanner": {
"manufacturer": "Leica Biosystems",
"model_name": "Aperio AT2",
"device_serial_number": "SN123"
},
"icc_profile": "profiles/profile.icc",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
]
}Scanner matching uses the governed DICOM manufacturer, model name, and device serial number. All three values are required; outer whitespace is trimmed and the remaining text is compared exactly and case-sensitively. Filenames and raw vendor properties are not matching inputs. Registries reject unknown fields, duplicate IDs or scanner tuples, malformed hashes, unsafe paths, missing or checksum-mismatched files, and profiles that are not valid RGB input-device ICC profiles. Registry JSON is limited to 1 MiB and 256 entries; each ICC file is limited to 16 MiB. Relative profile paths resolve from the registry directory, and only verified bytes—not local paths—enter conversion state or DICOM output.
When configured and source ICC digests differ, --icc-conflict fail stops the
export before staging or output creation. prefer-configured embeds the
configured bytes; prefer-source retains the source bytes. Equal digests are
accepted as a match. Each instance report records the effective SHA-256 digest,
profile or calibration ID, source, and conflict decision. MONOCHROME2 output
does not write ICC data and reports calibration as not applicable.
Inspect an existing profile or package it into a local portable bundle:
wsi-dicom calibration inspect --icc vendor-profile.icc
wsi-dicom calibration create --icc vendor-profile.icc \
--id lab-at2-sn123-2026q3 \
--manufacturer "Leica Biosystems" --model "Aperio AT2" --serial SN123 \
--out calibrationcalibration create validates and packages an existing vendor- or
target-generated profile. It does not derive scanner calibration from an
ordinary tissue slide. Calibration selection is CLI/API-only in 0.7.2; the GUI
offers source-required, sRGB fallback, and Display P3 fallback choices.
The default conversion preset is lossless-review, which emits HTJ2K Lossless
RPCL. For explicit JPEG Baseline output:
wsi-dicom convert slide.ndpi --out dicom-fast --research-placeholder \
--icc source-or-srgb --preset fast-jpegUseful operational commands:
wsi-dicom doctor --strict --json
wsi-dicom self-test --json --out self-test-evidence --keep-output
wsi-dicom validate dicom-out --strict --json
wsi-dicom coverage slide.ndpi --jsonHTJ2K pixel decode validation auto-detects grk_decompress when it is on
PATH. You can also provide an explicit absolute decoder command:
wsi-dicom validate dicom-out \
--htj2k-decoder "/opt/homebrew/bin/grk_decompress -i {input} -o {output}"Every validation run performs intrinsic checks before optional external
validation. The Pixel Data structure check verifies Number of Frames, native
versus encapsulated representation, nonempty data, and bounded frame mapping
through Basic or Extended Offset Tables; it still runs when
--max-pixel-frames 0. VL Whole Slide Microscopy Image objects additionally
run stable intrinsic-wsi-dicom-2026c-* checks for ICC profiles, monochrome
presentation attributes, lossy declarations, specimen identity, and dimension
ordering, plus a set-level Specimen UID consistency check. These checks do not
decode pixel values or infer unknowable compression history predating the
object. Missing external tools are reported as skipped unless --strict is
set. Directory validation is bounded by file count, depth, timeout, and child
output capture limits; symlink traversal is refused.
Use the builder API for normal exports:
use wsi_dicom::{ColorManagement, Export};
let report = Export::from_slide("slide.ndpi")
.to_directory("out")
.with_research_placeholder_metadata()
.tile_size(512)
.jpeg_quality(90)
.color_management(ColorManagement::SourceOrSrgb)
.run()?;Use request types when an integration needs full control:
use wsi_dicom::{
export_dicom, ColorManagement, ExportOptions, ExportRequest,
JpegDirectHtj2kProfile, MetadataSource, TransferSyntax,
};
let mut options = ExportOptions::lossless_review();
options.transfer_syntax = TransferSyntax::Htj2k;
options.jpeg_direct_htj2k_profile = JpegDirectHtj2kProfile::Lossy97Balanced;
let request = ExportRequest::new(
"slide.ndpi".into(),
"out".into(),
options,
ColorManagement::SourceOrSrgb,
MetadataSource::ResearchPlaceholder,
)?;
let report = export_dicom(request)?;For composed tile samples:
use wsi_dicom::{
encode_dicom_j2k_frame, CodecValidation, EncodeBackendPreference,
FrameSamples, J2kFrameEncodeRequest, TransferSyntax,
};
let pixels = vec![0_u8; 512 * 512 * 3];
let samples = FrameSamples::new(&pixels, 512, 512, 3, 8, false)?;
let frame = encode_dicom_j2k_frame(J2kFrameEncodeRequest::new(
samples,
TransferSyntax::Htj2kLosslessRpcl,
EncodeBackendPreference::CpuOnly,
CodecValidation::RoundTrip,
))?;- Every color optical path carries a validated DICOM input-device ICC profile.
Source profiles are preserved by the source/fallback modes. A synthesized
sRGB or Display P3 input profile is an explicit color-space assumption, not
scanner calibration. Configured/source conflicts are resolved only through
the selected
IccConflictPolicy. - ICC is not applicable to MONOCHROME2 output, which instead carries
Presentation LUT Shape
IDENTITY, Rescale Intercept0, and Rescale Slope1. - Lossy compression history is independent of the final transfer syntax. A lossless transcode does not erase prior lossy JPEG/JPEG 2000/HTJ2K stages.
- JPEG Baseline output preserves compatible native JPEG frames. HTJ2K lossless output rejects nonconformant color JPEG direct routes and falls back through decoded RGB/RCT.
- JPEG 2000 passthrough preserves eligible native source codestreams.
- Route profile and coverage JSON reports expose available frame counts, sampled frame percentages, route counters, pixel profiles, and GPU counters.
- Per-frame functional groups use streamed undefined-length sequences and items instead of an in-memory item list. Frame offsets and lengths are kept in a temporary disk-backed index and replayed when patching Extended Offset Tables.
- Output names encode scene, series, level, Z, channel, and time coordinates; consumers must use report paths rather than assuming the pre-0.7 name shape.
- Passing validators is release evidence, not formal DICOM certification.
Important
Regenerate color, MONOCHROME2, previously lossy, or multi-institution specimen output produced by versions before this remediation. Older objects can contain a display-class fallback ICC profile, omit required monochrome presentation attributes, erase lossy history after lossless transcoding, or derive the same Specimen UID for identifiers governed by different issuers.
Core checks:
cargo fmt --all -- --check
cargo clippy --workspace --no-default-features --all-targets --locked -- -D warnings
cargo test --workspace --no-default-features --all-targets --locked
cargo check -p wsi-dicom-gui --lockedPre-1.0 release gates:
cargo xtask docs-strict
cargo xtask coverage
cargo xtask semver
cargo publish --dry-runBefore a 1.0 release candidate, run these gates against published
dependencies and a representative real-slide corpus covering advertised routes,
metadata modes, color-management policies, validator checks, and any GPU route being
advertised.
Use the GDC benchmark harness only when publishing speed evidence:
./.venv/bin/python bench/gdc_benchmark.py \
--downloads-root ~/Downloads \
--probe-slide-metadata \
--tools wsi-dicom-cpu wsi-dicom-device wsidicomizer \
--profile htj2k-lossless-rpcl \
--scope base \
--runs 1 \
--system-label macos-metal \
--validateRun the same command on the Metal and CUDA hosts with host-specific release
binaries and --system-label values. Merge result directories with
--merge-results, then publish failures, unsupported slides, transfer syntax,
frame geometry, tool versions, host details, and machine-readable results with
any performance claim.
wsi-dicom is pre-1.0. The builder API is the preferred integration surface.
Lower-level request, report, validation, and profiling types are public, but
callers should prefer constructors and defaults over struct literals where
provided. Version 0.7.2 deliberately breaks the pre-1.0 color-management API:
IccProfilePolicy is removed, ExportRequest::new requires a
ColorManagement, and Export requires .color_management(...).
Dual-licensed under either MIT or Apache-2.0, at your option.