@@ -15,6 +15,49 @@ GeoTIFF / COG
1515 checklist that lists every promised feature on this page, its tier,
1616 its one-line acceptance, and the regression test that locks it.
1717
18+ :ref: `user_guide.attrs_contract ` -- the user-guide page that defines
19+ which attrs keys are canonical, which are aliases, and which are
20+ pass-through, and the round-trip guarantees that apply to each
21+ tier.
22+
23+ GPU support (experimental)
24+ ==========================
25+
26+ The GPU read and write paths are tagged ``experimental `` in
27+ :data: `xrspatial.geotiff.SUPPORTED_FEATURES `. Both
28+ ``SUPPORTED_FEATURES['reader.gpu'] `` and
29+ ``SUPPORTED_FEATURES['writer.gpu'] `` report ``experimental ``: the paths
30+ work and are covered by tests, but the surface can shift without a
31+ deprecation window. The GPU paths are not a release blocker -- a
32+ regression on a GPU row does not fail the build the way a regression
33+ on the stable CPU surface does.
34+
35+ What you can expect:
36+
37+ * GPU read and write produce the same pixels and the same canonical
38+ attrs as the CPU path on the supported codec subset. The eager and
39+ dask GPU readers are covered by
40+ ``xrspatial/geotiff/tests/test_golden_corpus_gpu_1930.py `` and
41+ ``xrspatial/geotiff/tests/test_golden_corpus_dask_gpu_1930.py ``.
42+ * Integer and float nodata sentinels survive the GPU round-trip; see
43+ ``xrspatial/geotiff/tests/test_gpu_nodata_1542.py ``.
44+ * On GPU failure the reader emits
45+ :class: `xrspatial.geotiff.GeoTIFFFallbackWarning ` and falls back to
46+ CPU unless ``on_gpu_failure='strict' `` or
47+ ``XRSPATIAL_GEOTIFF_STRICT=1 `` is set; see
48+ ``xrspatial/geotiff/tests/test_gpu_strict_fallback_1516.py ``.
49+
50+ What you should NOT rely on:
51+
52+ * GPU support for every codec on the CPU path. ``allow_experimental_codecs ``
53+ does NOT widen the GPU codec set; on the GPU writer, codecs outside the
54+ GPU-supported set route through a CPU fallback inside
55+ ``write_geotiff_gpu `` rather than executing on the GPU. Locked by
56+ ``xrspatial/geotiff/tests/test_gpu_writer_cpu_fallback_codecs_2026_05_12.py ``.
57+ * GPU promotion to ``stable `` inside this release cycle. See the GPU
58+ rows in :ref: `reference.geotiff_release_gate ` for the current tier
59+ and the regression tests behind each row.
60+
1861Stable COG contract
1962===================
2063
@@ -58,6 +101,85 @@ the corresponding caveats:
58101* HTTP / range COG (tracked separately; see the byte-budget contract in
59102 #2298).
60103
104+ Rotated and sheared transforms
105+ ==============================
106+
107+ Read posture. ``open_geotiff `` rejects a file whose affine transform
108+ has non-zero rotation or shear coefficients by default. Pass
109+ ``allow_rotated=True `` to opt in: the read then surfaces the rotated
110+ 6-tuple on ``attrs['rotated_affine'] `` and drops ``attrs['crs'] `` so
111+ downstream math cannot silently mix a rotated grid with an
112+ axis-aligned CRS. The dropped-CRS rule is locked by
113+ ``xrspatial/geotiff/tests/test_allow_rotated_crs_drop_2126.py ``,
114+ ``xrspatial/geotiff/tests/test_allow_rotated_no_crs_2122.py ``, and
115+ ``xrspatial/geotiff/tests/test_allow_rotated_geotiff_2115.py ``. The
116+ HTTP dask path honours the same opt-in via
117+ ``xrspatial/geotiff/tests/test_http_dask_allow_rotated_2130.py ``.
118+ Without ``allow_rotated=True `` the read raises a typed error; see
119+ ``xrspatial/geotiff/tests/test_rotated_typed_error_2267.py ``.
120+
121+ Write posture. ``to_geotiff `` rejects a DataArray carrying
122+ ``attrs['rotated_affine'] `` unless the caller also passes
123+ ``drop_rotation=True ``. With the opt-in, the writer drops the rotated
124+ affine and writes an axis-aligned file from the coords. This is
125+ locked by ``xrspatial/geotiff/tests/test_to_geotiff_drop_rotation_2216.py ``.
126+ A rotated or skewed 6-tuple supplied through ``attrs['transform'] ``
127+ or through a VRT source is also rejected; see
128+ ``xrspatial/geotiff/tests/test_unsupported_features_2349.py ``
129+ (``test_eager_writer_rejects_rotated_6tuple_transform `` and
130+ ``test_vrt_with_skewed_geotransform_rejected ``).
131+
132+ Failure-closed combinations. The following inputs raise rather than
133+ silently emit a mislabeled raster:
134+
135+ * Rotated read without ``allow_rotated=True `` -- raises across eager,
136+ dask, and windowed paths
137+ (``xrspatial/geotiff/tests/test_release_gate_negative_2341.py ``).
138+ * Rotated write without ``drop_rotation=True `` -- raises ``ValueError ``
139+ (``xrspatial/geotiff/tests/test_to_geotiff_drop_rotation_2216.py ``).
140+ * Rotated or skewed source inside a VRT -- raises at parse
141+ (``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py ``).
142+
143+ Nodata lifecycle
144+ ================
145+
146+ This page summarises the read / write contract. The full lifecycle
147+ of every attrs key, including which keys are canonical, which are
148+ aliases, and which are pass-through, lives in
149+ :ref: `user_guide.attrs_contract `. Do not duplicate that page here;
150+ this section is the brief.
151+
152+ * Integer nodata. The on-disk sentinel survives the read bit-exact
153+ and is preserved on the next write. ``attrs['nodata'] `` carries
154+ the sentinel as a Python ``int ``. Out-of-range sentinels for the
155+ band dtype are rejected at write
156+ (``xrspatial/geotiff/tests/test_nodata_out_of_range_1581.py ``).
157+ * Float nodata. The on-disk sentinel is recorded on
158+ ``attrs['nodata'] `` and surfaces as NaN in pixel data only when the
159+ read promotes via ``mask_nodata=True `` (the default for float
160+ outputs). With ``mask_nodata=False `` the raw float sentinel passes
161+ through, so downstream callers can branch on the exact value;
162+ ``xrspatial/geotiff/tests/test_mask_nodata_kwarg_2052.py `` pins this
163+ split.
164+ * NaN nodata. A file that declares ``nodata=NaN `` is read with NaN in
165+ both ``attrs['nodata'] `` and pixel data (NaN propagates either way).
166+ * ``attrs['masked_nodata'] ``. Every read sets a boolean lifecycle
167+ signal: ``True `` when the read produced NaN-masked output distinct
168+ from the on-disk sentinel, ``False `` when pixel data carries the
169+ raw sentinel. The signal is part of the canonical attrs contract;
170+ ``xrspatial/geotiff/tests/test_masked_nodata_attr_2092.py `` pins
171+ the canonical form and
172+ ``xrspatial/geotiff/tests/test_vrt_masked_nodata_attr_2159.py ``
173+ covers the VRT mosaic case.
174+ * Mixed-band nodata. A VRT whose sources declare disagreeing per-band
175+ nodata sentinels raises ``MixedBandMetadataError `` by default. Pass
176+ ``band_nodata='first' `` to opt back into the legacy flatten-to-band-0
177+ behaviour; see ``xrspatial/geotiff/tests/test_vrt_band_nodata_1598.py ``.
178+
179+ The lifecycle is locked end-to-end by
180+ ``xrspatial/geotiff/tests/test_nodata_lifecycle_attrs_2135.py `` and
181+ ``xrspatial/geotiff/tests/test_nodata_lifecycle_parity_2211.py ``.
182+
61183Reading
62184=======
63185.. autosummary ::
@@ -147,6 +269,49 @@ If you run an integration test against a local HTTP server (e.g.
147269``XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS=1 `` for the duration of the
148270test.
149271
272+ Remote-read safety limits and env vars
273+ --------------------------------------
274+
275+ The reader applies a layered budget to every remote ``http:// `` or
276+ ``https:// `` read so a single hostile file cannot exhaust memory or
277+ turn the process into a port scanner. The knobs are:
278+
279+ * ``max_cloud_bytes `` (kwarg) / ``XRSPATIAL_GEOTIFF_MAX_CLOUD_BYTES ``
280+ (env). Per-call total byte budget for a remote read. The kwarg wins
281+ over the env var; the env var wins over the built-in default. Pass
282+ ``max_cloud_bytes=None `` to disable the cap on a single call. Locked
283+ by ``xrspatial/geotiff/tests/test_max_cloud_bytes_dispatcher_silent_drop_2026_05_15.py ``,
284+ ``xrspatial/geotiff/tests/test_open_geotiff_max_cloud_bytes_annot_2106.py ``,
285+ and ``xrspatial/geotiff/tests/test_http_read_all_bounded_2051.py ``.
286+ * ``XRSPATIAL_COG_MAX_TILE_BYTES ``. Per-tile / per-strip compressed
287+ byte cap (default 256 MiB). Locked by
288+ ``xrspatial/geotiff/tests/test_local_tile_byte_cap_1664.py ``,
289+ ``xrspatial/geotiff/tests/test_cloud_read_byte_limit_1928.py ``, and
290+ ``xrspatial/geotiff/tests/test_gpu_tile_byte_cap_2026_05_18.py ``.
291+ * ``XRSPATIAL_GEOTIFF_HTTP_CONNECT_TIMEOUT `` and
292+ ``XRSPATIAL_GEOTIFF_HTTP_READ_TIMEOUT ``. Per-request connect / read
293+ timeouts in seconds. Positive floats only; other values fall back
294+ to the defaults (10 s and 30 s). Range coalescing inside one read
295+ shares a single connection so the connect timeout applies once per
296+ host, not once per range.
297+ * ``XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS ``. Set to ``1 `` (or
298+ ``true `` / ``yes ``) to disable the private-host reject. Off by
299+ default; locked by
300+ ``xrspatial/geotiff/tests/test_ssrf_hardening_1664.py ``,
301+ ``xrspatial/geotiff/tests/test_dns_rebinding_pin_issue_1846.py ``,
302+ and ``xrspatial/geotiff/tests/test_uppercase_scheme_ssrf_2323.py ``.
303+ * ``XRSPATIAL_VRT_ALLOWED_ROOTS ``. Colon-separated list of additional
304+ directory roots that a VRT is allowed to reference. The default
305+ containment rule (sources must live under the VRT's directory) is
306+ locked by ``xrspatial/geotiff/tests/test_vrt_path_containment_1671.py ``.
307+ * ``XRSPATIAL_GEOTIFF_STRICT ``. Promotes the fallback warnings into
308+ raised exceptions, including the GPU-fallback path; see the next
309+ section.
310+
311+ The same byte budget applies to sidecar fetches, not just the parent
312+ file
313+ (``xrspatial/geotiff/tests/test_sidecar_max_cloud_bytes_2121.py ``).
314+
150315Strict mode (``XRSPATIAL_GEOTIFF_STRICT ``)
151316==========================================
152317
@@ -336,3 +501,70 @@ single-band and 3-band, one overview level, plus an auto-promotion row
336501that drives the threshold via the IFD-overhead helper rather than
337502allocating a multi-gigabyte buffer. Promotion to ``stable `` follows the
338503same release-cycle soak rule as the rest of the COG surface.
504+
505+ Known unsupported combinations
506+ ==============================
507+
508+ The combinations below fail closed today: they raise a typed error
509+ rather than emit a possibly-wrong raster. Each row names the
510+ regression test that locks the behaviour.
511+
512+ .. list-table ::
513+ :header-rows: 1
514+ :widths: 35 65
515+
516+ * - Combination
517+ - Regression test
518+ * - ``to_geotiff(cog=True, tiled=False) ``
519+ - ``xrspatial/geotiff/tests/test_cog_requires_tiled_2312.py ``
520+ * - ``to_geotiff(cog=True, tile_size <= 0) ``
521+ - ``xrspatial/geotiff/tests/test_cog_tile_size_hang_2311.py ``
522+ * - Warped VRT
523+ (``<VRTDataset subClass="VRTWarpedDataset"> `` or
524+ ``<VRTRasterBand subClass="VRTWarpedRasterBand"> ``)
525+ - ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py ``,
526+ ``xrspatial/geotiff/tests/test_vrt_capability_validator_2371.py ``
527+ * - Nested VRT (a ``<SourceFilename> `` that resolves to a ``.vrt ``)
528+ - ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py ``
529+ (``test_nested_vrt_source_raises ``,
530+ ``test_nested_vrt_open_geotiff_raises ``)
531+ * - Mixed-CRS VRT (sources disagree on CRS without an opt-in)
532+ - ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py ``,
533+ ``xrspatial/geotiff/tests/test_vrt_capability_validator_2371.py ``
534+ * - Mixed per-band nodata across VRT sources (default
535+ ``band_nodata=None ``)
536+ - ``xrspatial/geotiff/tests/test_vrt_band_nodata_1598.py ``,
537+ ``xrspatial/geotiff/tests/test_unsupported_features_2349.py ``
538+ (``test_mixed_per_source_nodata_rejected ``)
539+ * - Rotated read without ``allow_rotated=True ``
540+ - ``xrspatial/geotiff/tests/test_release_gate_negative_2341.py ``,
541+ ``xrspatial/geotiff/tests/test_rotated_typed_error_2267.py ``
542+ * - Rotated write without ``drop_rotation=True ``
543+ - ``xrspatial/geotiff/tests/test_to_geotiff_drop_rotation_2216.py ``,
544+ ``xrspatial/geotiff/tests/test_unsupported_features_2349.py ``
545+ (``test_eager_writer_rejects_rotated_6tuple_transform ``,
546+ ``test_eager_writer_rejects_rotated_affine_attr ``)
547+ * - Skewed VRT geotransform
548+ - ``xrspatial/geotiff/tests/test_unsupported_features_2349.py ``
549+ (``test_vrt_with_skewed_geotransform_rejected ``)
550+ * - Complex source / mask band / alpha band in a VRT
551+ - ``xrspatial/geotiff/tests/test_vrt_unsupported_2370.py ``,
552+ ``xrspatial/geotiff/tests/test_vrt_capability_validator_2371.py ``
553+ * - VRT source path escapes the VRT directory tree
554+ - ``xrspatial/geotiff/tests/test_vrt_path_containment_1671.py ``
555+ * - 1xN / Nx1 write without ``attrs['transform'] `` or
556+ ``assume_square_pixels_for_degenerate_axis=True ``
557+ - ``xrspatial/geotiff/tests/test_degenerate_pixel_size_2214.py ``;
558+ see also "Degenerate-axis writes" above.
559+ * - HTTP read against a private / loopback / link-local host
560+ without ``XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS=1 ``
561+ - ``xrspatial/geotiff/tests/test_ssrf_hardening_1664.py ``,
562+ ``xrspatial/geotiff/tests/test_dns_rebinding_pin_issue_1846.py ``
563+ * - Unsupported feature flags more broadly (codec, layout, and
564+ writer combos that ``SUPPORTED_FEATURES `` does not promise)
565+ - ``xrspatial/geotiff/tests/test_unsupported_features_2349.py ``
566+
567+ This list is the prose mirror of the negative rows in
568+ :ref: `reference.geotiff_release_gate `. When a row gets promoted or
569+ removed, update both pages in the same PR so the docs and the runtime
570+ constant stay in sync.
0 commit comments