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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Refresh the geotiff mmap cache when a file is replaced under the same path so re-reads after an atomic-rename overwrite no longer return stale bytes
- Decode TIFF predictor=3 un-transpose by file byte order so big-endian floating-point TIFFs read back exactly
- Default internal `_vrt.read_vrt` `missing_sources` to `'raise'` so an unreadable VRT source no longer produces a silent zero-fill hole on integer rasters; pass `missing_sources='warn'` to opt back into the previous lenient behaviour (#1843)
- Deprecate read-side emission of matplotlib colormap-derived attrs (cmap, colormap_rgba) on palette TIFFs; the writer cannot set Photometric=3 so they do not round-trip. Construct ListedColormap from attrs['colormap'] in caller code. These attrs still emit for now but trigger a DeprecationWarning. Removal planned for a future release. (#1984)


### Version 0.9.9 - 2026-05-05
Expand Down
33 changes: 28 additions & 5 deletions docs/source/user_guide/attrs_contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,38 @@ must not assume a specific pass-through key survives a round-trip.
* - ``colormap``
- tuple
- Raw ``ColorMap`` TIFF tag (tag id 320) values.


Deprecated keys
===============

These keys are still emitted on read for one release cycle, but each
emission triggers a ``DeprecationWarning``. The writer cannot
reconstruct them from canonical attrs, so they do not round-trip.
Callers should migrate to the canonical alternative listed below
before the warning-only window closes. See issue #1984.

.. list-table::
:header-rows: 1
:widths: 22 18 60

* - Key
- Type
- Definition and migration
* - ``colormap_rgba``
- array
- Decoded RGBA colormap. Emitted only when the file's photometric
interpretation is ``Photometric == 3`` (palette).
- Decoded RGBA colormap. Emitted on read when the file's
photometric interpretation is ``Photometric == 3`` (palette).
The writer cannot set ``Photometric == 3`` so the attr does
not round-trip. Reshape ``attrs['colormap']`` to
``(n_colors, 3)`` and append an alpha channel in caller code
if needed.
* - ``cmap``
- ``matplotlib.colors.ListedColormap``
- Matplotlib colormap built from ``colormap_rgba``. Present only
when matplotlib is importable and the same ``Photometric == 3``
gate is satisfied.
- Matplotlib colormap built from the palette. Same
``Photometric == 3`` gate, same round-trip gap. Construct a
``ListedColormap`` from ``attrs['colormap']`` in caller code
if needed.


Round-trip invariants
Expand Down
102 changes: 96 additions & 6 deletions xrspatial/geotiff/_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@

- ``image_description``: TIFF ImageDescription tag.
- ``extra_samples``: TIFF ExtraSamples tag.
- ``colormap``, ``colormap_rgba``, ``cmap``: palette data attached to
single-band paletted images.
- ``colormap``: raw uint16 RGB triples from the TIFF ColorMap tag (320),
attached to single-band paletted images.

Deprecated (will be removed in a future release; see issue #1984):

Expand Down Expand Up @@ -103,6 +103,17 @@
reason as ``vertical_crs``.
- ``vertical_units``: VerticalUnitsGeoKey value. Same deprecation reason
as ``vertical_crs``.
Colormap variants (different root cause: photometric gate, not GeoKey):

- ``colormap_rgba``: RGBA palette array, only emitted on read when the
source file is Photometric==3 (palette). The writer never selects
Photometric=3, so this attr does not round-trip. Reshape
``attrs['colormap']`` to ``(n_colors, 3)`` and append an alpha
channel in caller code if needed.
- ``cmap``: matplotlib ``ListedColormap`` built from the palette. Same
Photometric==3 gate, same round-trip gap. Construct a
``ListedColormap`` from ``attrs['colormap']`` in caller code if
needed.

Migration recipe (the canonical replacement is ``crs`` / ``crs_wkt``
plus a one-liner with :mod:`pyproj` when a derived value is needed)::
Expand Down Expand Up @@ -208,6 +219,25 @@
)


# Shared wording for the colormap-variants slice of the PR 7 deprecation
# series. The root cause is a different one (the writer cannot set
# ``Photometric=3``) so the GeoKey-tier reason templates above don't
# fit; these constants are spliced into ``_emit_deprecated_attr`` with
# a per-attr migration recipe so users see how to derive an RGBA palette
# or matplotlib ``ListedColormap`` from canonical ``attrs['colormap']``.
_DEPRECATED_COLORMAP_REASON = (
"the writer cannot set Photometric=3 so it does not round-trip"
)
_DEPRECATED_CMAP_MIGRATION = (
"Construct a ListedColormap from attrs['colormap'] in caller code "
"if needed"
)
_DEPRECATED_COLORMAP_RGBA_MIGRATION = (
"Reshape attrs['colormap'] to (n_colors, 3) and append an alpha "
"channel in caller code if needed"
)


def _deprecated_geokey_warning(name: str, *, reason: str) -> str:
"""Warning text for a deprecated GeoKey-derived attr.

Expand Down Expand Up @@ -332,6 +362,54 @@ def _emit_deprecated_geographic_geokey(attrs: dict, name: str, value) -> None:
)


def _emit_deprecated_attr(
attrs: dict,
name: str,
value,
*,
reason: str,
migration: str | None = None,
) -> None:
"""Emit a deprecated attr with a ``DeprecationWarning`` and a
per-attr migration recipe.

Sibling of :func:`_emit_deprecated_geokey_attr` that adds support
for an optional ``migration`` clause spliced into the warning. The
GeoKey-tier helper uses a fixed sentence ("... so it will not
round-trip ..."); the colormap-variants tier needs to point users
at how to derive an RGBA palette or matplotlib ``ListedColormap``
from canonical ``attrs['colormap']``, which doesn't fit that
template. A follow-up may unify the two helpers; for now they live
side-by-side because the warning-text contracts are pinned by
separate test suites and converging them is out of scope for the
colormap slice.

Warning text shape::

xrspatial.geotiff: attrs['<name>'] is deprecated; <reason>
<migration?> It will be removed in a future release. See
issue #1984.

The ``stacklevel`` is taken from
:func:`_stacklevel_to_external_caller` so the warning is attributed
to the user's call site, matching the GeoKey-tier slices.
"""
parts = [
f"xrspatial.geotiff: attrs[{name!r}] is deprecated;",
reason.rstrip('.') + '.',
]
if migration:
parts.append(migration.rstrip('.') + '.')
parts.append("It will be removed in a future release. See issue #1984.")
warnings.warn(
' '.join(parts),
DeprecationWarning,
stacklevel=_stacklevel_to_external_caller(),
)
attrs[name] = value



def _extent_to_window(transform, file_height, file_width,
y_min, y_max, x_min, x_max):
"""Convert geographic extent to pixel window (row_start, col_start, row_stop, col_stop).
Expand Down Expand Up @@ -513,11 +591,23 @@ def _populate_attrs_from_geo_info(attrs: dict, geo_info, *, window=None) -> None
if geo_info.colormap is not None:
try:
from matplotlib.colors import ListedColormap
attrs['cmap'] = ListedColormap(
geo_info.colormap, name='tiff_palette')
attrs['colormap_rgba'] = geo_info.colormap
_emit_deprecated_attr(
attrs, 'cmap',
ListedColormap(geo_info.colormap, name='tiff_palette'),
reason=_DEPRECATED_COLORMAP_REASON,
migration=_DEPRECATED_CMAP_MIGRATION,
)
_emit_deprecated_attr(
attrs, 'colormap_rgba', geo_info.colormap,
reason=_DEPRECATED_COLORMAP_REASON,
migration=_DEPRECATED_COLORMAP_RGBA_MIGRATION,
)
except ImportError:
attrs['colormap_rgba'] = geo_info.colormap
_emit_deprecated_attr(
attrs, 'colormap_rgba', geo_info.colormap,
reason=_DEPRECATED_COLORMAP_REASON,
migration=_DEPRECATED_COLORMAP_RGBA_MIGRATION,
)

if geo_info.extra_tags is not None:
for _tag_id, _tt, _tc, _tv in geo_info.extra_tags:
Expand Down
Loading
Loading