Skip to content

fix(dicom): reject chunks that do not fit the volume buffer - #931

Merged
PaulHax merged 1 commit into
Kitware:mainfrom
PaulHax:dicom-chunk-dimension-mismatch
Aug 27, 2026
Merged

fix(dicom): reject chunks that do not fit the volume buffer#931
PaulHax merged 1 commit into
Kitware:mainfrom
PaulHax:dicom-chunk-dimension-mismatch

Conversation

@PaulHax

@PaulHax PaulHax commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

A series whose instances disagree on Rows, Columns, or SamplesPerPixel is still
grouped into one volume, and that volume's buffer is sized from the first
instance. Each decoded chunk was written into a slot computed from the series
dimensions, with no check that it fit.

An instance decoding larger than its slot made TypedArray.prototype.set throw
RangeError: offset is out of bounds, naming neither the file nor what was
wrong with it. An instance decoding smaller wrote entirely inside the buffer and
raised nothing at all: the frame is smeared across the top of a larger slot,
silently. The silent case is the more serious one.

The guard already in place covered only a multi-frame instance inside a
multi-file series.

Change

dicomChunkImage.ts computes the slot each chunk may occupy, one frame per
chunk in a multi-file volume and the full depth for a lone multi-frame chunk,
and validates the decoded chunk's width, height, frame count, and component
count against it before writing:

File 1.2.826.0.1.3680043.10.999... (chunk 2) does not fit the volume it belongs
to. It decoded to 10x8x1 with 1 component(s), but the volume has room for 6x4x1
with 1 component(s). Every file in a volume must have the same Rows, Columns,
and SamplesPerPixel.

Files are identified by SOP Instance UID, since chunks carry no filename or
source URI. The closing advice is omitted for single-file volumes, where it is
meaningless. Only the offending instance is rejected.

One supporting fix, needed to make a rejection survivable:

A rejected chunk no longer wedges the loading state. computeStatus treated
anything but Loaded as still coming, so an errored chunk left the image
incomplete with loading never cleared: progress indicators ran forever and
image-stats never computed auto window/level. Errored is now terminal. An
image settles once every chunk is Loaded or Errored, and settling clears
loading. Status reaches complete only if at least one chunk loaded, so a
volume where everything was rejected settles without claiming to be a finished
image. Auto window/level then computes from the chunks that did load.

Scope

This contains the damage; it does not make heterogeneous series display
correctly. A rejected instance leaves its slot zeroed, so a scout in an axial
stack becomes a blank slice plus one named error rather than a volume of its
own. There is no benign case being preserved: the buffer is sized from the
first instance and the write is a raw copy into a fixed-size slot, so a
differing instance is either a RangeError, a smear, or transposed garbage.

The final solution is to partition on pixel geometry during categorize, as a
sibling of SeparateOnImageOrientation, so the mismatched instances become
their own volume and this guard never fires. That is a separate change: it
needs the itk-wasm build and it changes volume IDs, which appear in saved state
files. This guard is still worth keeping afterwards, because grouping and
allocation read metadata tags while the guard compares the size ITK actually
decoded, so a file whose tags disagree with its pixel data is caught here and
nowhere else.

@netlify

netlify Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploy Preview for volview-dev ready!

Name Link
🔨 Latest commit 7c72b1c
🔍 Latest deploy log https://app.netlify.com/projects/volview-dev/deploys/6a90c29ab639700008dee342
😎 Deploy Preview https://deploy-preview-931--volview-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@PaulHax
PaulHax added this pull request to the merge queue Aug 27, 2026
@PaulHax
PaulHax removed this pull request from the merge queue due to a manual request Aug 27, 2026
@PaulHax
PaulHax force-pushed the dicom-chunk-dimension-mismatch branch from bb65efa to 9d675f1 Compare August 27, 2026 23:02
A series whose instances disagree on Rows, Columns, or SamplesPerPixel is
still grouped into one volume, whose buffer is sized from the first
instance. An instance that decoded larger than its slot threw
"RangeError: offset is out of bounds" from TypedArray.set, naming neither
the file nor what was wrong with it. One that decoded smaller wrote inside
the buffer and raised nothing at all, leaving a frame smeared across a
larger slot.

Validate each decoded chunk against its slot before writing, and name the
file and both sizes in the error.

Rejecting a chunk also has to leave the image usable. An errored chunk is
now terminal: an image settles, and stops reporting itself as loading, once
every chunk is either loaded or errored, so the views drop their progress
indicator and auto window/level is computed from the chunks that did load.
An image whose every chunk errored settles but stays incomplete, since it
holds no pixel data.
@PaulHax
PaulHax force-pushed the dicom-chunk-dimension-mismatch branch from 9d675f1 to 7c72b1c Compare August 27, 2026 23:04
@PaulHax
PaulHax added this pull request to the merge queue Aug 27, 2026
Merged via the queue into Kitware:main with commit f482c6d Aug 27, 2026
8 checks passed
@PaulHax
PaulHax deleted the dicom-chunk-dimension-mismatch branch August 27, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant