|
24 | 24 | from .._attrs import (_EXPERIMENTAL_CODECS, _LEVEL_RANGES, _VALID_COMPRESSIONS, _extract_rich_tags, |
25 | 25 | _resolve_nodata_attr, _should_restore_nan_sentinel) |
26 | 26 | from .._backends._gpu_helpers import _is_gpu_data |
27 | | -from .._coords import _BAND_DIM_NAMES, _has_no_georef_marker |
| 27 | +from .._coords import ROTATION_SHEAR_TOL, _BAND_DIM_NAMES, _has_no_georef_marker |
28 | 28 | from .._coords import require_transform_for_georeferenced as _require_transform_for_georeferenced |
29 | 29 | from .._coords import resolve_georef as _resolve_georef |
30 | 30 | from .._crs import _validate_crs_arg, _validate_crs_fallback, _wkt_to_epsg |
@@ -357,6 +357,47 @@ def to_geotiff(data: xr.DataArray | np.ndarray, |
357 | 357 | entry_point="to_geotiff", |
358 | 358 | ) |
359 | 359 |
|
| 360 | + # Issue #2301: ``attrs['transform']`` carrying a rasterio |
| 361 | + # ``Affine`` with non-zero rotation/shear (``b != 0`` or ``d != 0``) |
| 362 | + # silently slipped past ``transform_from_attr`` because ``Affine`` |
| 363 | + # iterates as a 9-element augmented matrix and the 6-tuple gate |
| 364 | + # there ran ``len(seq) != 6 -> return None``. Without the |
| 365 | + # rejection the writer falls back to coord-derived or no-georef |
| 366 | + # output and the rotation is lost on disk. Detect the Affine shape |
| 367 | + # by duck-typing the (b, d) attrs and surface the same diagnostic |
| 368 | + # the 6-tuple branch raises (#1987 PR 3 wording, kept verbatim so |
| 369 | + # the existing match patterns still hit). |
| 370 | + _attr_transform = _drop_rotation_attrs.get('transform') |
| 371 | + if (_attr_transform is not None |
| 372 | + and hasattr(_attr_transform, 'b') |
| 373 | + and hasattr(_attr_transform, 'd')): |
| 374 | + try: |
| 375 | + _b = float(_attr_transform.b) |
| 376 | + _d = float(_attr_transform.d) |
| 377 | + except (TypeError, ValueError) as _exc: |
| 378 | + # Fail-closed on a malformed ``.b`` / ``.d`` rather than |
| 379 | + # zero-defaulting: an unconvertable value inside an attr |
| 380 | + # claiming to be an affine transform is itself a writer |
| 381 | + # input contract violation. Without the explicit raise the |
| 382 | + # branch would bypass every downstream georef gate that |
| 383 | + # would otherwise catch the bad value. |
| 384 | + raise ValueError( |
| 385 | + f"attrs['transform'] has unconvertable rotation/shear " |
| 386 | + f"terms (b={_attr_transform.b!r}, " |
| 387 | + f"d={_attr_transform.d!r}); expected numeric values on " |
| 388 | + f"a rasterio Affine-like object." |
| 389 | + ) from _exc |
| 390 | + if abs(_b) > ROTATION_SHEAR_TOL or abs(_d) > ROTATION_SHEAR_TOL: |
| 391 | + raise ValueError( |
| 392 | + f"attrs['transform'] has non-zero rotation/shear " |
| 393 | + f"(b={_b!r}, d={_d!r}); rotated or skewed affines are " |
| 394 | + f"not supported by the GeoTIFF writers in this module " |
| 395 | + f"because the on-disk GeoTIFF representation is " |
| 396 | + f"axis-aligned and would be written at the wrong " |
| 397 | + f"location. Reproject the raster to an axis-aligned " |
| 398 | + f"grid before writing." |
| 399 | + ) |
| 400 | + |
360 | 401 | # Issue #2075: reject zero-height / zero-width inputs before any |
361 | 402 | # dispatch decision. Clip / window pipelines naturally produce empty |
362 | 403 | # rasters and the writers used to accept them, produce a TIFF whose |
|
0 commit comments