Summary
to_geotiff silently drops rotation when called on a DataArray carrying attrs['rotated_affine'] from an allow_rotated=True read. The reader spells out the read-only contract at xrspatial/geotiff/_geotags.py:635, but the writer accepts the input without warning and writes an identity-affine file. The round-trip corrupts georeferencing with no signal to the caller.
Steps to reproduce
from xrspatial.geotiff import open_geotiff, to_geotiff
# Source file with a rotated ModelTransformationTag.
da = open_geotiff("rotated_src.tif", allow_rotated=True)
assert "rotated_affine" in da.attrs
# This succeeds today and writes a file with identity geotransform.
to_geotiff(da, "out.tif")
# Read it back -- rotation is gone, no warning was emitted.
da2 = open_geotiff("out.tif")
assert "rotated_affine" not in da2.attrs # rotation silently lost
Expected behaviour
to_geotiff should reject inputs carrying attrs['rotated_affine'] by default and raise ValueError with a message that names the offending attr. The caller can then reproject onto an axis-aligned grid, or opt in to dropping rotation via a new drop_rotation=True keyword whose docstring spells out the silent-loss consequence.
Why this matters
Silent loss of georeferencing is exactly the kind of downstream poison this module should avoid. Code that branches on attrs['rotated_affine'] cannot tell that the round-trip lost the mapping, and consumers reading the output file have no way to recover the original transform. The allow_rotated=True contract was explicitly written as read-only at _geotags.py:635; the writer should fail closed rather than paper over the gap.
Proposed fix
- Add
drop_rotation: bool = False to to_geotiff (and write_geotiff_gpu for parity).
- When
attrs['rotated_affine'] is present and drop_rotation is False, raise ValueError naming the attr and pointing at the opt-in.
- When
drop_rotation=True, write the axis-aligned file and make sure the attr does not propagate to the output.
- Document the silent-loss consequence in the docstring.
Scope
Writer-side only. The read-side allow_rotated=True contract stays unchanged. The ModelTransformationTag emit path that would let writes preserve rotation is tracked separately (#2115 follow-up); this issue closes the silent-loss gap until that work lands.
Summary
to_geotiffsilently drops rotation when called on a DataArray carryingattrs['rotated_affine']from anallow_rotated=Trueread. The reader spells out the read-only contract atxrspatial/geotiff/_geotags.py:635, but the writer accepts the input without warning and writes an identity-affine file. The round-trip corrupts georeferencing with no signal to the caller.Steps to reproduce
Expected behaviour
to_geotiffshould reject inputs carryingattrs['rotated_affine']by default and raiseValueErrorwith a message that names the offending attr. The caller can then reproject onto an axis-aligned grid, or opt in to dropping rotation via a newdrop_rotation=Truekeyword whose docstring spells out the silent-loss consequence.Why this matters
Silent loss of georeferencing is exactly the kind of downstream poison this module should avoid. Code that branches on
attrs['rotated_affine']cannot tell that the round-trip lost the mapping, and consumers reading the output file have no way to recover the original transform. Theallow_rotated=Truecontract was explicitly written as read-only at_geotags.py:635; the writer should fail closed rather than paper over the gap.Proposed fix
drop_rotation: bool = Falsetoto_geotiff(andwrite_geotiff_gpufor parity).attrs['rotated_affine']is present anddrop_rotationis False, raiseValueErrornaming the attr and pointing at the opt-in.drop_rotation=True, write the axis-aligned file and make sure the attr does not propagate to the output.Scope
Writer-side only. The read-side
allow_rotated=Truecontract stays unchanged. TheModelTransformationTagemit path that would let writes preserve rotation is tracked separately (#2115 follow-up); this issue closes the silent-loss gap until that work lands.