Skip to content

BUG: Fix NRRD measurement-frame corruption; refactor reserved-field dispatch#6088

Open
hjmjohnson wants to merge 4 commits intoInsightSoftwareConsortium:mainfrom
hjmjohnson:fix-nrrd-reserved-fields
Open

BUG: Fix NRRD measurement-frame corruption; refactor reserved-field dispatch#6088
hjmjohnson wants to merge 4 commits intoInsightSoftwareConsortium:mainfrom
hjmjohnson:fix-nrrd-reserved-fields

Conversation

@hjmjohnson
Copy link
Copy Markdown
Member

@hjmjohnson hjmjohnson commented Apr 20, 2026

Fixes a silent on-disk corruption in NrrdImageIO::Write and refactors the reserved-field routing. Documents the teem-driven NRRD version auto-bump that users on Discourse #6666 and in #5461 did not realise was already available.

Does not attempt to paper over user-supplied metadata in non-documented forms. Use NRRD_<field> per the (now-extended) class doxygen; non-conforming metadata either throws or falls through to NRRD's documented := custom-pair escape hatch.

Commit sequence
  1. STYLE: convert NrrdImageIO::Write()'s metadata loop to a range-based for (mechanical keyIt->metaKey. rename, 27 / 27 lines).
  2. STYLE: refactor the reserved-field if-ladder into a dispatch table keyed by teem's nrrdField enum. No behavior change.
  3. ENH: document the teem-driven NRRD magic auto-bump (NRRD0001..NRRD0006) in the class doxygen and add an itkNrrdMetaDataTest subtest that writes a measurement frame via the documented NRRD_measurement frame key and asserts the on-disk magic is NRRD0005. Addresses the confusion behind Discourse #6666.
  4. BUG: WriteMeasurementFrame now throws itk::ExceptionObject when the user-supplied frame's dimensions do not match nrrd->spaceDim, instead of padding missing entries with the magic sentinel 666666 (which teem faithfully wrote to the header, silently corrupting orientation data). Regression test added.
Relation to #5461 and SimpleITK/SimpleITK#2395

Neither is fixed here, by design:

  • Write to NRRD doesn't convert all metadata keys to NRRD keys correctly #5461 — reporter used bare metadata keys like content and thicknesses instead of the documented NRRD_content / NRRD_thicknesses[N] form. The docs were corrected in DOC: Add information to NRRD and GDCM IOs. #5472; with those docs in place, the bare-key round-trip is user error. Non-NRRD_* keys go through NRRD's documented := custom-pair escape hatch, which is the standard's own answer for "anything not a reserved field." No code change in this PR is required for that contract to hold.

  • setting NRRD metadata fails due to encoding information as string SimpleITK/SimpleITK#2395 — the Python binding stores numeric metadata as strings, which the ITK ExposeMetaData<double> then fails to coerce, leaving a zero. That is a Python-binding encoding bug in SimpleITK, not a NRRD writer bug. Fixing it inside the C++ writer would require type-coercion machinery that accommodates a specific wrapper's encoding choice — maintenance burden for the core codebase in service of a third-party workaround. The upstream SimpleITK issue is the correct venue.

Test plan
  • ctest -R Nrrd — 33/33 pass locally (Linux, clang 19 via conda-forge).
  • itkNrrdMetaDataTest extended with two new subtests: NRRD0005 auto-bump from NRRD_measurement frame; throw-on-malformed-measurement-frame regression.
  • Verify on Windows/macOS runners once CI picks up the branch.

@github-actions github-actions Bot added type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:IO Issues affecting the IO module labels Apr 20, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR fixes a silent on-disk corruption bug in NrrdImageIO::Write where a user-supplied measurement frame with wrong dimensions was padded with the sentinel value 666666 and written verbatim to the NRRD header, corrupting orientation data. The fix throws itk::ExceptionObject on dimension mismatch and refactors the old strncmp/sscanf if-ladder into a clean dispatch table keyed by teem's nrrdField enum. Doxygen and a new regression test (malformed frame + NRRD0005 auto-bump) are also added.

Confidence Score: 5/5

Safe to merge; the core bug fix and dispatch refactor are correct, and the only remaining finding is a minor Windows test-cleanup issue.

All prior P0/P1 concerns (666666 corruption, warning macro routing, TryExposeDouble return value, readNrrdHeader binary mode) were addressed in earlier commits per the developer replies. The sole new finding is a P2 test-cleanup portability issue that does not affect correctness.

itkNrrdMetaDataTest.cxx — open ifstream during RemoveFile call may leave a stale temp file on Windows.

Important Files Changed

Filename Overview
Modules/IO/NRRD/src/itkNrrdImageIO.cxx Core bug fix: replaces the old 666666-sentinel corruption with a proper dimension check + exception in WriteMeasurementFrame; refactors the per-field if-ladder into a clean dispatch table — logic and validation are correct.
Modules/IO/NRRD/include/itkNrrdImageIO.h Documentation-only: adds a doxygen paragraph explaining teem's automatic NRRD version selection (NRRD0001–NRRD0006). No behavioral changes.
Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx Two new regression subtests added (NRRD0005 auto-bump and throw-on-malformed-frame); a minor Windows portability issue with an open ifstream during RemoveFile.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[NrrdImageIO::Write - MetaDataDictionary loop] --> B{Key starts with 'NRRD_' prefix?}
    B -- Yes --> C[Extract keyField after prefix]
    C --> D[TryDispatchNrrdReservedField]
    D --> E{Match in kNrrdReservedFields dispatch table?}
    E -- Per-axis field --> F[Parse index via sscanf, call perAxis handler, stamp nrrd->axis]
    E -- Scalar field --> G{Field == measurement frame?}
    G -- Yes --> H{Frame dims == nrrd->spaceDim?}
    H -- No --> I[itkGenericExceptionMacro throw ExceptionObject]
    H -- Yes --> J[Copy frame to nrrd->measurementFrame]
    G -- No --> K[WriteOldMin / WriteOldMax / WriteSpace / WriteContent / etc.]
    E -- No match --> L[Key silently consumed, not forwarded to custom-pair]
    B -- No --> M[Serialize value to string, nrrdKeyValueAdd as := pair]
Loading

Reviews (2): Last reviewed commit: "BUG: Throw on malformed NRRD measurement..." | Re-trigger Greptile

Comment thread Modules/IO/NRRD/src/itkNrrdImageIO.cxx Outdated
Comment thread Modules/IO/NRRD/src/itkNrrdImageIO.cxx Outdated
Comment thread Modules/IO/NRRD/src/itkNrrdImageIO.cxx Outdated
Comment thread Modules/IO/NRRD/test/itkNrrdMetaDataTest.cxx
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 20, 2026
…sentinel

The previous WriteMeasurementFrame path (pre-existing behavior preserved
verbatim through the earlier dispatch-table refactor) filled missing
coefficients with a magic sentinel value of 666666 whenever the user-
supplied measurement frame had fewer rows or columns than the image's
space dimension.  Since every coefficient is finite, teem's
nrrd__FieldInteresting check treats the frame as fully specified and
writes the literal 666666 to the on-disk header, silently producing a
corrupted orientation matrix rather than signalling an error.

Validate the supplied frame up front: if fewer rows or narrower columns
than nrrd->spaceDim, issue an ITK warning and leave nrrd->measurementFrame
unset (the default AIR_NAN initialization makes the frame "uninteresting"
to teem and it is omitted from the header).  A dimension-matched frame
is copied unchanged as before.

itkNrrdMetaDataTest gets a regression subtest that writes a 2x2 frame on
a 3-D image and asserts neither "measurement frame:" nor the literal
"666666" appears in the output header.

Greptile P1 on InsightSoftwareConsortium#6088.
@hjmjohnson hjmjohnson force-pushed the fix-nrrd-reserved-fields branch from 3628cb0 to d5e3ea7 Compare April 20, 2026 01:42
Replace the iterator-style loop over NrrdImageIO::Write()'s
MetaDataDictionary keys with a C++11 range-based for, renaming the
loop variable from the iterator `keyIt` to the string reference
`metaKey`.  All `*keyIt` and `keyIt->c_str()` uses are updated to
`metaKey` and `metaKey.c_str()` mechanically.

Pure rename; no behavior change.  Split out as its own commit to keep
the reviewer's attention free for the dispatch-table refactor that
lands on top -- the `keyIt` -> `metaKey` churn is mechanical and easy
to verify in isolation.
Replace the open-coded if-ladder inside NrrdImageIO::Write() with a
uniform dispatch table keyed by the teem nrrdField enum.  Each reserved
field (thicknesses, centers, kinds, labels, units, old_min, old_max,
space, content, measurement_frame) is handled by a dedicated small
function; the per-axis vs. scalar distinction is expressed once by the
handler signature instead of being re-implemented at every call site.

No behavior change.  The existing itkNrrdMetaDataTest and the full 42
NRRD tests continue to pass.  This refactor is a prerequisite for the
follow-up commits that add bare-key promotion (issue InsightSoftwareConsortium#5461), bulk
per-axis syntax, locale-safe numeric coercion for Python-wrapped
clients, automatic NRRD0005 version bump, and NRRD_type round-trip
(issue InsightSoftwareConsortium#5462).
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 20, 2026
…sentinel

The previous WriteMeasurementFrame path (pre-existing behavior preserved
verbatim through the earlier dispatch-table refactor) filled missing
coefficients with a magic sentinel value of 666666 whenever the user-
supplied measurement frame had fewer rows or columns than the image's
space dimension.  Since every coefficient is finite, teem's
nrrd__FieldInteresting check treats the frame as fully specified and
writes the literal 666666 to the on-disk header, silently producing a
corrupted orientation matrix rather than signalling an error.

Validate the supplied frame up front: if fewer rows or narrower columns
than nrrd->spaceDim, issue an ITK warning and leave nrrd->measurementFrame
unset (the default AIR_NAN initialization makes the frame "uninteresting"
to teem and it is omitted from the header).  A dimension-matched frame
is copied unchanged as before.

itkNrrdMetaDataTest gets a regression subtest that writes a 2x2 frame on
a 3-D image and asserts neither "measurement frame:" nor the literal
"666666" appears in the output header.

Greptile P1 on InsightSoftwareConsortium#6088.
@hjmjohnson hjmjohnson force-pushed the fix-nrrd-reserved-fields branch 2 times, most recently from 8a0d8c1 to b88a682 Compare April 20, 2026 02:06
@hjmjohnson hjmjohnson changed the title BUG: NRRD writer honors bare reserved-field names and related fixes BUG: Fix NRRD measurement-frame corruption; refactor reserved-field dispatch Apr 20, 2026
@hjmjohnson
Copy link
Copy Markdown
Member Author

@greptileai review this draft before I make it official

teem's nrrdSave auto-selects the minimum NRRD magic version based on
the fields set on the Nrrd struct (nrrd__FormatNRRD_whichVersion in
NrrdIO's formatNRRD.c): setting a measurement frame forces NRRD0005,
thicknesses/space/space-dimension/sample-units force NRRD0004, and so
on.  This behavior was previously undocumented in the ITK wrapper,
leading users to assume ITK always writes NRRD0004 (see Discourse
https://discourse.itk.org/t/6666).

Document the auto-bump contract in the NrrdImageIO class doxygen and
add an itkNrrdMetaDataTest subtest that writes a 3x3 measurement
frame via the documented "NRRD_measurement frame" key and verifies the
on-disk magic is NRRD0005 and the header contains a standard
"measurement frame:" field (not a custom ":=" entry).

No C++ behavior change.
The previous WriteMeasurementFrame path (pre-existing behavior preserved
verbatim through the earlier dispatch-table refactor) filled missing
coefficients with a magic sentinel value of 666666 whenever the user-
supplied measurement frame had fewer rows or columns than the image's
space dimension.  Since every coefficient was finite, teem's
nrrd__FieldInteresting check treated the frame as fully specified and
wrote the literal 666666 to the on-disk header, silently producing a
corrupted orientation matrix rather than signalling an error.

Validate the supplied frame up front: if fewer rows or narrower columns
than nrrd->spaceDim, throw an itk::ExceptionObject describing the
mismatch.  A dimension-matched frame is copied unchanged as before.
Reject-and-throw is consistent with ITK's policy of not silently
masking user errors.

itkNrrdMetaDataTest gets a regression subtest that writes a 2x2 frame
on a 3-D image and asserts the writer throws; if any partial file was
produced it also asserts the literal "666666" does not leak into the
header.
@hjmjohnson hjmjohnson force-pushed the fix-nrrd-reserved-fields branch from b88a682 to 8123e48 Compare April 20, 2026 05:56
@hjmjohnson hjmjohnson requested review from N-Dekker and dzenanz and removed request for N-Dekker April 20, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:IO Issues affecting the IO module type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant