Skip to content

geotiff: reject mixed SampleFormat per band - #1869

Merged
brendancol merged 1 commit into
mainfrom
issue-1868
May 14, 2026
Merged

brendancol merged 1 commit into
mainfrom
issue-1868

Conversation

@brendancol

Copy link
Copy Markdown
Contributor

Closes #1868.

Summary

IFD.sample_format collapsed tuple SampleFormat tags to v[0], so a TIFF with SampleFormat=(3,3,1) (two float bands plus one uint band at the same bit depth) decoded every band as float32. The uint band came back reinterpreted as a float with no error or warning.

Adds resolve_sample_format in _dtypes.py mirroring resolve_bits_per_sample:

  • Scalar passes through.
  • Uniform tuple/list returns the shared value.
  • Mixed tuple raises ValueError with the offending tuple and a pointer to gdal_translate / rasterio.
  • Empty tuple keeps falling back to 1 (uint), so the fuzz regression at tests/test_fuzz_hypothesis_1661.py:311 still passes.

IFD.sample_format delegates to the new helper, so both reader call sites (_reader.py:2507 and _reader.py:2934) get the validation for free.

Test plan

  • New unit tests for resolve_sample_format (scalar, uniform tuple, mixed, empty fallback).
  • New end-to-end tests against open_geotiff for float/uint mix and signed/unsigned mix.
  • Existing test_mixed_bps.py, test_fuzz_hypothesis_1661.py, test_header.py, test_reader.py, test_edge_cases.py all pass (153 tests).

IFD.sample_format previously returned v[0] for tuple SampleFormat tags,
silently collapsing per-band sample formats to whatever the first band
declared. A file with BitsPerSample=(32,32,32) and SampleFormat=(3,3,1)
decoded every band as float32 instead of failing, reinterpreting the
uint band as a float.

Add resolve_sample_format alongside resolve_bits_per_sample. It keeps
the empty-tuple fallback exercised by the fuzz regression at
tests/test_fuzz_hypothesis_1661.py:311 and raises ValueError on mixed
per-band values. IFD.sample_format delegates to it so all readers
inherit the validation.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label May 14, 2026
@brendancol
brendancol requested a review from Copilot May 14, 2026 19:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes silent pixel corruption when a TIFF's SampleFormat tag carries different values per band. Introduces resolve_sample_format mirroring resolve_bits_per_sample, and routes IFD.sample_format through it so mixed per-band values raise a typed ValueError instead of being collapsed to the first band's format.

Changes:

  • Add resolve_sample_format helper in _dtypes.py (scalar passthrough, uniform tuple, mixed-raise, empty-tuple fallback to 1).
  • Replace inline logic in IFD.sample_format with a delegation to the new helper.
  • Add unit and end-to-end tests, including a minimal TIFF builder for multi-band SampleFormat.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
xrspatial/geotiff/_dtypes.py New resolve_sample_format validator with typed error mirroring resolve_bits_per_sample.
xrspatial/geotiff/_header.py IFD.sample_format delegates to resolve_sample_format.
xrspatial/geotiff/tests/test_mixed_sample_format.py New unit + end-to-end tests including a synthetic TIFF builder.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@brendancol
brendancol merged commit e0d08e3 into main May 14, 2026
16 checks passed
@brendancol
brendancol deleted the issue-1868 branch May 15, 2026 04:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

geotiff: mixed SampleFormat per band is silently collapsed to first band

2 participants