NRRD Format Boundary
Single source of truth for NRRD file I/O.
Ownership
ritk-nrrd owns the NRRD file reader and writer. ritk-io::format::nrrd
is a facade re-export.
read_nrrd_header parses the bounded header without decoding the payload. Its
result exposes canonical standard fields, the declared format version,
comment lines, an effective custom key/value map, and source-ordered decoded
key/value records, including repeated keys. The effective map follows NRRD’s
rule that the last value for a repeated key is the stored value; the record
list retains earlier values for inspection during format conversion. The
parser limits headers to 16 MiB and 65,536 retained entries across standard
fields, comments, and key/value records. These header records do not imply
that payload conversion preserves every format’s metadata.
Field identifiers are case-insensitive. Teem’s equivalent spellings
(byteskip, lineskip, datafile, axismins, axismaxs, centerings,
blocksize, oldmin, oldmax, and sampleunits) map to canonical lowercase
field names before duplicate checks. Empty comment strings are ignored;
retained comment lines keep their leading # and source order. NRRD header
lines use ASCII; non-ASCII bytes return a typed parse error. These delimiter,
key, field, alias, and comment rules follow the Teem NRRD format
definition, Sections 1.2, 1.6,
5, and 6.
Spatial Contract
NRRD file-axis [x,y,z] maps to RITK [depth,row,col] via crates/ritk-nrrd/src/spatial.rs.
The reader constructs the tensor directly as [nz,ny,nx] from X-fastest NRRD
raw bytes; the writer emits RITK ZYX flat data directly.
Direction Vectors
- Reader: NRRD
space directionsvectors[x,y,z]become internal metadata columns[depth,row,col] = [z,y,x]. - Writer: NRRD
space directionsare generated from internal columns[col,row,depth].
Every finite positive spacing retains its direction, including values below
1e-9. Stored-volume construction rejects an axis component whose
direction-times-spacing product overflows or underflows to zero, before a
format writer can emit unrepresentable geometry.
A rank-2 array can be embedded in either a two- or three-dimensional world.
For a named patient space such as LPS or RAS, each direction vector and the
origin use three world coordinates even though the array has two axes. RITK
derives the missing slice direction from the plane normal and promotes the
array to [1,Y,X] with a one-millimeter through-plane step. When no named
space is declared, rank-2 vectors retain the two-component interpretation and
are promoted into the XY plane. Component count follows the world-space
dimension, independently of array rank, as specified by Teem’s NRRD space
fields.
NRRD’s named patient spaces are normalized to RITK’s LPS millimeter
coordinates. RAS reverses the first two physical components; LAS reverses the
anterior/posterior component. Supported space units (mm, cm, m, um,
and nm) scale both origins and direction vectors before spacing and
orientation are derived. Unknown spaces, anonymous coordinate frames, and
unrecognized units fail instead of being relabeled as patient coordinates.
These mappings follow Section 4 of the Teem NRRD format specification.
Invariant
NRRD parser/writer dependency changes stay behind ritk-nrrd; callers
in ritk-io, CLI, and viewer code consume the same authoritative API.
Diffusion gradient metadata
read_nrrd_gradient_scheme implements the NA-MIC DWI convention. One nominal
DWMRI_b-value is combined with each DWMRI_gradient_XXXX squared norm to
recover the per-volume effective b-value. The measurement frame maps gradient
coordinates to world coordinates; RAS world coordinates are converted once to
RITK LPS. When the field is absent, the reader uses the identity frame. A
nonzero gradient therefore uses scanner coordinates directly unless an
explicit frame is present; an all-zero baseline also needs no frame. See
Teem’s NRRD specification,
section 4 for the coordinate-
frame contract.
Every encoded nonzero weighting is preserved, including values below the
scanner-input baseline threshold used by ritk-diffusion-scheme constructors.
Missing indices, non-finite values, DWMRI_NEX, and B-matrix encodings fail
explicitly rather than being guessed.
Document construction and retained metadata
NrrdDocument::new(series, comments, records) consumes a StoredSeries, a
Vec<String> of comments, and a Vec<(String, String)> of custom key/value
records. It constructs a document without an intermediate file. The borrowed
series(), comments(), and records() accessors expose its samples and
retained metadata. Construction uses the same validation as the writer:
identity calibration, supported series axes, physical geometry, matching
volume shapes, sample types, geometry and coordinate maps, and serialized
header byte and entry limits. Generated header entries count toward the
16 MiB and 65,536-entry limits.
read_nrrd_document(path, budget) checks retained metadata and the regenerated
header limits before decoding samples. Its header and attached payload remain
bound to one opened source file. The resulting document keeps the stored
sample representation and values through StoredSeries.
Retained order
Comments retain their leading #, text, and relative order. Non-empty forms
such as # note and ##note are supported. The header parser drops comment
lines containing only # and spaces; the constructor rejects those forms,
non-ASCII text, embedded carriage returns or newlines, and caller-supplied
copies of # Complete NRRD file written by ritk. The document reader filters
that generated banner, and each write emits it once.
Custom records are decoded key/value strings, kept in their supplied or source
order, including repeated keys. For example, source:=scanner followed by
source:=reviewed remains two records in records(), while the header’s
effective map reports reviewed. Writing escapes backslashes again. Keys must
be non-empty ASCII, must not start with # or contain :=, and keys and values
must not contain carriage returns or newlines. Values must also be ASCII.
The writer groups retained comments before custom records, after generated fields. It preserves order within each list, rather than their original interleaving with one another or with standard fields. Whitespace, escaping, field spelling, and the original header bytes are not a document round-trip contract.
Regenerated fields and typed loss
Serialization derives type, dimension, sizes, kinds, space, space units, space directions, and space origin from the typed series. Output
uses patient LPS millimeter coordinates, encoding: raw, endian: little,
and an attached payload. Acquisition volumes use the trailing contiguous axis.
Coordinate-map records and diffusion metadata (modality:=DWMRI,
DWMRI_b-value, gradient records, and the identity measurement frame) are
regenerated where applicable. Output uses NRRD0005 for diffusion and NRRD0004
otherwise; source version and encoding are not retained as formatting choices.
Callers supply the corresponding typed values, rather than copies of generated
records in the custom-record list.
The current document reader rejects standard fields outside that generated
structural set, including content, labels, data file, spacings, and
skip fields. The constructor likewise rejects standard field names passed as
custom records. These failures return NrrdDocumentError::UnsupportedField;
the error’s field may describe a metadata category rather than the specific
source field. Reserved coordinate-map or DWMRI records, malformed or repeated
generated records on input, and unsupported DWMRI extensions are also rejected.
read_nrrd_header remains available to inspect those fields. The stored-sample
reader’s support for a payload form, such as detached data, does not imply that
the document boundary can retain its header semantics.
Validation before destination mutation
write_nrrd_document builds the bounded output header and validates the whole
series before File::create can create or truncate the destination. Metadata
conflicts, unsupported fields, header limits, calibration, and series mismatch
errors therefore leave an existing destination unchanged. Construction performs
these checks too; writing rechecks them before opening the destination. This
is a validation guarantee: a later I/O error during header or payload writing,
or flushing, can leave partial output.
Stored samples and format conversion
NrrdDocument combines a validated StoredSeries with retained comments and
custom records without an intermediate file. Writing derives structural fields
from the samples, validates first, and leaves an existing destination
unchanged on rejection.
The document boundary retains only the generated structural fields and the
custom records it can emit unchanged. It rejects standard fields such as
content, labels, data file, and spacings, detached payload references,
and malformed generated records rather than dropping them. It also rejects
empty # and ## comments, non-ASCII comments, and the writer’s generated
banner. The header parser retains non-empty forms such as # note and
##note; the document boundary preserves those values unchanged.
The document constructor runs the same header, series, geometry, calibration,
and entry-limit checks as its writer, so construction cannot produce a value
that serialization would reject.
Use read_nrrd_stored when NRRD is an input to a format conversion. It
returns ritk_image_io::StoredVolume, retaining the element type, each stored
value, the spatial metadata, the coordinate map, and the calibration state.
The ordinary read_nrrd API remains the compute path and converts values to
f32.
The stored reader supports signed and unsigned 8-, 16-, 32-, and 64-bit
integers plus IEEE 754 32- and 64-bit floats. It accepts the aliases listed
in the NRRD type table and both endian markers. NRRD’s encoding field is
required. RITK reads raw, ascii (text, txt), and gzip (gz). Binary
multi-byte samples require an explicit endian marker; one-byte samples and
ASCII samples do not, as specified in Section 5 of the Teem NRRD
format. ASCII values are
whitespace-delimited and each token is limited to 128 bytes to bound parser
scratch space. Readers consume the declared array payload and ignore following
bytes, as permitted by the NRRD encoding rules in Section 5.
Binary reads and writes retain signed zero and NaN payload bits. ASCII values
are parsed from text, so they do not carry binary NaN payload bits. Element type
names follow the Teem NRRD type table and payload rules,
including the standard int8 through uint64 aliases. RITK also accepts the
historical char alias as signed 8-bit for existing files, although Teem does
not list bare char as a NRRD type descriptor.
read_nrrd_stored_series returns one stored volume per acquisition entry. It
preserves acquisition order for both leading interleaved axes and trailing
contiguous axes. write_nrrd_stored_series writes the trailing contiguous
layout; the existing compute-image series writer keeps its leading NA-MIC
layout.
Diffusion series output uses the NRRD0005 magic and writes an explicit identity
measurement frame because its gradient vectors are already in LPS coordinates.
Other stored series retain NRRD0004. There is no implicit diffusion default: modality:=DWMRI, a declared acquisition axis, one nominal DWMRI_b-value, and contiguous gradient entries are required before a diffusion series is constructed. The reader accepts the standard
axismins, axismaxs, and centerings aliases and canonicalizes them before
checking duplicate fields. Per-axis physical units without space directions
or spacings are rejected because they cannot determine sample geometry.
Stored scalar or list data with a measurement frame but no DWMRI scheme is
rejected before payload reading because the stored-volume model cannot retain
that frame. Axis support bounds and cell/node centering are likewise rejected
instead of being discarded.
The custom RITK coordinate-map field serializes Cartesian, curvilinear, phased-array, and per-slice transform maps. Slice-series transforms store nine row-major direction components and three translation coordinates per depth slice, with round-trip decimal formatting.
NRRD does not have a standard modality-calibration field. The stored writer
therefore accepts only value-identity calibration and returns
NrrdStoredWriteError::UnsupportedCalibration before it creates an output
file when the volume carries a non-identity linear or lookup-table transform.
Convert to a target format that can encode the calibration, or apply it
explicitly before choosing a format whose contract stores only pixel values.
Detached data files must be relative to the NRRD header’s directory. Absolute
paths and parent traversal are rejected at the read boundary; raw and
decompressed payloads are bounded by the declared shape and element type.
Detached data currently accepts one file name. line skip and byte skip
fields are parsed before payload decoding; positive byte skips on gzip data
apply after decompression, while byte skip: -1 is accepted only for raw data.
Stored reads accept an ImageReadBudget that caps encoded payload bytes,
decoded sample bytes, gzip-expanded payload bytes including discarded byte-skip
data, and the number of volumes in an acquisition series.
The default limits are 1 GiB for each byte count and 65,536 volumes. The
adapter checks declared counts before allocating decoded sample storage. The
stored writer validates format semantics before opening the destination and
streams samples through a buffered file writer without making a second
volume-sized encoded payload.