Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/source/user_guide/geotiff_safe_io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ the ambiguous-metadata family at once.
- ``attrs['crs']`` and ``attrs['crs_wkt']`` do not canonicalise to
the same WKT on write.
- Resolve the conflict in caller code before writing.
* - :class:`~xrspatial.geotiff.InconsistentGeoKeysError`
- The source's GeoKey directory is internally contradictory:
``ModelTypeGeoKey`` disagrees with the type-specific keys
actually populated, or ``ProjectedCSTypeGeoKey`` and
``GeographicTypeGeoKey`` resolve to different EPSG codes.
- ``allow_inconsistent_geokeys=True`` to keep the legacy silent
acceptance for known-quirky historical files.
* - :class:`~xrspatial.geotiff.ConflictingNodataError`
- ``attrs['nodata']`` and ``attrs['nodatavals']`` disagree on
write.
Expand Down
26 changes: 24 additions & 2 deletions xrspatial/geotiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@
transform_tuple_from_pixel_geometry as _transform_tuple_from_pixel_geometry # noqa: F401
from ._crs import _resolve_crs_to_wkt, _wkt_to_epsg # noqa: F401
from ._errors import (ConflictingCRSError, ConflictingNodataError, GeoTIFFAmbiguousMetadataError,
InvalidCRSCodeError, MixedBandMetadataError, NonRepresentableEPSGCRSError,
NonUniformCoordsError, RotatedTransformError, UnknownCRSModelTypeError,
InconsistentGeoKeysError, InvalidCRSCodeError, MixedBandMetadataError,
NonRepresentableEPSGCRSError, NonUniformCoordsError, RotatedTransformError,
UnknownCRSModelTypeError,
UnparseableCRSError, UnsupportedGeoTIFFFeatureError)
from ._geotags import RASTER_PIXEL_IS_AREA, RASTER_PIXEL_IS_POINT, GeoTransform # noqa: F401
from ._reader import _MAX_CLOUD_BYTES_SENTINEL, CloudSizeLimitError, UnsafeURLError
Expand Down Expand Up @@ -108,6 +109,7 @@
'GEOREF_STATUS_ROTATED_DROPPED',
'GEOREF_STATUS_TRANSFORM_ONLY',
'GEOREF_STATUS_VALUES',
'InconsistentGeoKeysError',
'InvalidCRSCodeError',
'MixedBandMetadataError',
'NonRepresentableEPSGCRSError',
Expand Down Expand Up @@ -376,6 +378,7 @@ def open_geotiff(source: str | BinaryIO, *,
missing_sources: str = _MISSING_SOURCES_SENTINEL,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -574,6 +577,18 @@ def open_geotiff(source: str | BinaryIO, *,
behaviour where the citation field passes through unchanged.
Matches the same kwarg on ``to_geotiff`` / ``write_geotiff_gpu``
so a value the reader accepted can survive a round-trip.
allow_inconsistent_geokeys : bool, default False
[advanced] Read-side opt-in for GeoTIFF sources whose GeoKey
directory is internally contradictory: ``ModelTypeGeoKey``
disagrees with the type-specific keys actually populated, or
``ProjectedCSTypeGeoKey`` and ``GeographicTypeGeoKey`` resolve
to different EPSG codes. The legacy reader took the projected
code first and silently fabricated an
``attrs['crs']`` / ``attrs['crs_wkt']`` from contradictory
inputs (issue #2417). When ``False`` (the default), the read
raises ``InconsistentGeoKeysError``. Set to ``True`` to keep
the legacy permissive behaviour for files known to carry
quirky-but-trusted GeoKey layouts.
allow_experimental_codecs : bool, default False
Read-side opt-in for sources compressed with the Tier 3
experimental codecs (``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``).
Expand Down Expand Up @@ -721,6 +736,8 @@ def open_geotiff(source: str | BinaryIO, *,
max_pixels=max_pixels,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
band_nodata=band_nodata,
Expand All @@ -743,6 +760,8 @@ def open_geotiff(source: str | BinaryIO, *,
max_pixels=max_pixels,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
allow_experimental_codecs=(
allow_experimental_codecs),
allow_internal_only_jpeg=(
Expand All @@ -758,6 +777,8 @@ def open_geotiff(source: str | BinaryIO, *,
max_pixels=max_pixels, name=name,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
allow_experimental_codecs=(
allow_experimental_codecs),
allow_internal_only_jpeg=(
Expand Down Expand Up @@ -814,6 +835,7 @@ def open_geotiff(source: str | BinaryIO, *,
name=name,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)


Expand Down
23 changes: 22 additions & 1 deletion xrspatial/geotiff/_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@
from ._coords import coords_from_geo_info as _coords_from_geo_info
from ._coords import resolve_georef as _resolve_georef
from ._coords import transform_tuple_from_pixel_geometry as _transform_tuple_from_pixel_geometry
from ._geotags import _NO_GEOREF_KEY, RASTER_PIXEL_IS_AREA, RASTER_PIXEL_IS_POINT
from ._geotags import (_NO_GEOREF_KEY, GEOKEY_GEOGRAPHIC_TYPE, GEOKEY_MODEL_TYPE,
GEOKEY_PROJECTED_CS_TYPE, RASTER_PIXEL_IS_AREA, RASTER_PIXEL_IS_POINT)

# Per-codec valid compression-level ranges, used by ``to_geotiff`` for
# friendly up-front validation. Codecs not listed here either reject any
Expand Down Expand Up @@ -1051,6 +1052,7 @@ def _validate_read_geo_info(
window=None,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
band_nodata: str | None = None,
band_nodata_values: list | None = None,
) -> None:
Expand Down Expand Up @@ -1102,11 +1104,26 @@ def _validate_read_geo_info(
and getattr(geo_info, 'has_georef', True))
else None
)
# Pull the GeoKey shape onto the context so
# ``_check_read_inconsistent_geokeys`` (issue #2417) can audit
# ModelType / ProjectedCSType / GeographicType for contradictions.
# ``geo_info.geokeys`` is the parsed dict; missing keys map to
# ``None`` so the check treats the slot as "not declared" rather
# than as a default-zero, which would otherwise look like
# ``ModelType = undefined`` instead of "no model type tag at all".
raw_geokeys = getattr(geo_info, 'geokeys', None) or {}
model_type_ctx = raw_geokeys.get(GEOKEY_MODEL_TYPE)
proj_cs_ctx = raw_geokeys.get(GEOKEY_PROJECTED_CS_TYPE)
geog_ctx = raw_geokeys.get(GEOKEY_GEOGRAPHIC_TYPE)
validate_read_metadata({
'allow_rotated': allow_rotated,
'allow_unparseable_crs': allow_unparseable_crs,
'allow_inconsistent_geokeys': allow_inconsistent_geokeys,
'transform': transform_for_check,
'crs_wkt': geo_info.crs_wkt,
'model_type': model_type_ctx,
'projected_cs_type': proj_cs_ctx,
'geographic_type': geog_ctx,
'band_nodata': band_nodata,
'band_nodata_values': band_nodata_values,
})
Expand Down Expand Up @@ -1500,6 +1517,7 @@ def _finalize_eager_read(
name,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
attrs_in: dict | None = None,
):
"""Validate, populate attrs, mask, cast, and build an eager DataArray.
Expand Down Expand Up @@ -1546,6 +1564,7 @@ def _finalize_eager_read(
geo_info, window=window,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)

# Step 2: populate attrs from geo_info onto a fresh dict (or onto a
Expand Down Expand Up @@ -1613,6 +1632,7 @@ def _finalize_lazy_read_attrs(
window,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
band_nodata: str | None = None,
band_nodata_values: list | None = None,
attrs_in: dict | None = None,
Expand Down Expand Up @@ -1685,6 +1705,7 @@ def _finalize_lazy_read_attrs(
geo_info, window=window,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
band_nodata=band_nodata,
band_nodata_values=band_nodata_values,
)
Expand Down
11 changes: 11 additions & 0 deletions xrspatial/geotiff/_backends/dask.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def read_geotiff_dask(source: str, *,
missing_sources: str = _MISSING_SOURCES_SENTINEL,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -126,6 +127,14 @@ def read_geotiff_dask(source: str, *,
instead of carrying the unrecognised payload through
``attrs['crs_wkt']``. See ``open_geotiff`` for the full
description.
allow_inconsistent_geokeys : bool, default False
[advanced] Read-side opt-in for sources whose GeoKey directory
is internally contradictory (``ModelTypeGeoKey`` disagrees
with the populated type-specific keys, or
``ProjectedCSTypeGeoKey`` and ``GeographicTypeGeoKey`` resolve
to different EPSG codes). The default raises
``InconsistentGeoKeysError``. See ``open_geotiff`` for the
full description (issue #2417).
allow_experimental_codecs : bool, default False
[advanced] Read-side opt-in for Tier 3 experimental codecs
(``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``). Fires at graph
Expand Down Expand Up @@ -209,6 +218,7 @@ def read_geotiff_dask(source: str, *,
chunks=chunks, max_pixels=max_pixels,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
band_nodata=band_nodata,
mask_nodata=mask_nodata,
**vrt_kwargs,
Expand Down Expand Up @@ -499,6 +509,7 @@ def read_geotiff_dask(source: str, *,
window=window,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)

if isinstance(chunks, int):
Expand Down
19 changes: 19 additions & 0 deletions xrspatial/geotiff/_backends/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def read_geotiff_gpu(source: str, *,
missing_sources: str = _MISSING_SOURCES_SENTINEL,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -203,6 +204,12 @@ def read_geotiff_gpu(source: str, *,
since #1929) raises ``UnparseableCRSError``; ``True`` keeps
the pre-#1929 permissive behaviour. See ``open_geotiff`` for
the full description.
allow_inconsistent_geokeys : bool, default False
[experimental] Read-side opt-in for sources whose GeoKey
directory is internally contradictory. ``False`` (the default)
raises ``InconsistentGeoKeysError``; ``True`` restores the
legacy silent acceptance. See ``open_geotiff`` for the full
description (issue #2417).
allow_experimental_codecs : bool, default False
[experimental] Read-side opt-in for Tier 3 experimental codecs
(``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``). The GPU read path
Expand Down Expand Up @@ -330,6 +337,7 @@ def read_geotiff_gpu(source: str, *,
name=name, max_pixels=max_pixels,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
mask_nodata=mask_nodata,
Expand Down Expand Up @@ -381,6 +389,7 @@ def read_geotiff_gpu(source: str, *,
overview_level=overview_level, band=band, name=name,
max_pixels=max_pixels, allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
mask_nodata=mask_nodata,
Expand Down Expand Up @@ -612,6 +621,7 @@ def read_geotiff_gpu(source: str, *,
name=name,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)
# ``chunks`` was previously honoured only on the tiled path,
# so stripped TIFFs returned an unchunked DataArray even when
Expand Down Expand Up @@ -1019,6 +1029,7 @@ def _read_once():
name=name,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)

# ``chunks=`` is handled at function entry via
Expand All @@ -1044,6 +1055,7 @@ def _read_geotiff_gpu_eager_via_cpu(source, *, dtype, window, overview_level,
band, name, max_pixels,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
mask_nodata: bool = True):
Expand Down Expand Up @@ -1127,6 +1139,7 @@ def _read_geotiff_gpu_eager_via_cpu(source, *, dtype, window, overview_level,
name=name,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)


Expand Down Expand Up @@ -1276,6 +1289,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level,
window, band, name, max_pixels,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
mask_nodata: bool = True):
Expand Down Expand Up @@ -1391,6 +1405,8 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level,
name=name, max_pixels=max_pixels,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
mask_nodata=mask_nodata,
)
except Exception:
Expand All @@ -1405,6 +1421,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level,
max_pixels=max_pixels, name=name,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
mask_nodata=mask_nodata,
Expand Down Expand Up @@ -1432,6 +1449,7 @@ def _read_geotiff_gpu_chunked_gds(source, ifd, geo_info, header, *,
max_pixels,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
mask_nodata: bool = True):
"""Build a Dask+CuPy graph that decodes each chunk disk->GPU.

Expand Down Expand Up @@ -1660,6 +1678,7 @@ def _chunk_task(meta, r0, c0, r1, c1):
window=window,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
)

if name is None:
Expand Down
14 changes: 14 additions & 0 deletions xrspatial/geotiff/_backends/vrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def read_vrt(source: str, *,
missing_sources: str = 'raise',
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -259,6 +260,15 @@ def read_vrt(source: str, *,
#1929) raises ``UnparseableCRSError`` rather than carrying the
unrecognised payload through. See ``open_geotiff`` for the
full description.
allow_inconsistent_geokeys : bool, default False
[advanced] Read-side opt-in for sources whose GeoKey directory
is internally contradictory. Accepted for signature symmetry
with ``open_geotiff`` and the GeoTIFF readers; VRT capability
validation itself does not parse GeoKeys (it consumes the GDAL
``<SRS>`` field), and the legacy VRT internal reader does not
thread per-GeoTIFF-source kwargs, so this kwarg is currently a
no-op on the VRT path. See ``open_geotiff`` for the full
description (issue #2417).
allow_experimental_codecs : bool, default False
[advanced] Read-side opt-in for Tier 3 experimental codecs in
any source file referenced by the VRT. Forwarded to the
Expand Down Expand Up @@ -440,6 +450,7 @@ def read_vrt(source: str, *,
missing_sources=missing_sources,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
band_nodata=band_nodata,
Expand Down Expand Up @@ -702,6 +713,7 @@ def read_vrt(source: str, *,
window=window,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
band_nodata=band_nodata,
band_nodata_values=_band_nodata_values,
attrs_in=attrs_seed,
Expand Down Expand Up @@ -789,6 +801,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype,
max_pixels, missing_sources,
allow_rotated: bool = False,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -1221,6 +1234,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype,
window=helper_window,
allow_rotated=allow_rotated,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
band_nodata=band_nodata,
band_nodata_values=band_nodata_values,
attrs_in=attrs_seed,
Expand Down
Loading
Loading