diff --git a/xrspatial/geotiff/_reader.py b/xrspatial/geotiff/_reader.py index 19c2105f..ca776e87 100644 --- a/xrspatial/geotiff/_reader.py +++ b/xrspatial/geotiff/_reader.py @@ -47,11 +47,15 @@ MAX_PIXELS_DEFAULT = 1_000_000_000 +class PixelSafetyLimitError(ValueError): + """Raised when a requested TIFF allocation exceeds max_pixels.""" + + def _check_dimensions(width, height, samples, max_pixels): - """Raise ValueError if the requested allocation exceeds *max_pixels*.""" + """Raise PixelSafetyLimitError if the request exceeds *max_pixels*.""" total = width * height * samples if total > max_pixels: - raise ValueError( + raise PixelSafetyLimitError( f"TIFF image dimensions ({width} x {height} x {samples} = " f"{total:,} pixels) exceed the safety limit of " f"{max_pixels:,} pixels. Pass a larger max_pixels value to " diff --git a/xrspatial/geotiff/_vrt.py b/xrspatial/geotiff/_vrt.py index 5a792ffa..a30013b4 100644 --- a/xrspatial/geotiff/_vrt.py +++ b/xrspatial/geotiff/_vrt.py @@ -626,7 +626,7 @@ def read_vrt(vrt_path: str, *, window=None, ------- (np.ndarray, VRTDataset) tuple """ - from ._reader import read_to_array + from ._reader import PixelSafetyLimitError, read_to_array with open(vrt_path, 'r') as f: xml_str = f.read() @@ -871,10 +871,13 @@ def read_vrt(vrt_path: str, *, window=None, src.filename, window=(read_r0, read_c0, read_r1, read_c1), band=src.band - 1, # convert 1-based to 0-based + max_pixels=max_pixels, ) except ( OSError, ValueError, struct.error, ) + _CODEC_DECODE_EXCEPTIONS as e: + if isinstance(e, PixelSafetyLimitError): + raise # Under XRSPATIAL_GEOTIFF_STRICT=1, surface the read failure # so partial mosaics are caught in CI. Default mode warns # once per missing source then continues, preserving the diff --git a/xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py b/xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py new file mode 100644 index 00000000..7ce72288 --- /dev/null +++ b/xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py @@ -0,0 +1,35 @@ +"""VRT source reads must honor the caller's max_pixels budget (#1796).""" +from __future__ import annotations + +import os + +import numpy as np +import pytest + +from xrspatial.geotiff import to_geotiff, read_vrt + + +def test_vrt_source_read_forwards_max_pixels(tmp_path): + """A tiny VRT output cannot force an oversized source-window decode.""" + src = tmp_path / "tmp_1796_source.tif" + data = np.arange(16, dtype=np.uint8).reshape(4, 4) + to_geotiff(data, str(src), compression='none') + + vrt = tmp_path / "tmp_1796_source_cap.vrt" + vrt.write_text( + '\n' + ' \n' + ' \n' + f' {os.path.basename(src)}' + '\n' + ' 1\n' + ' \n' + ' \n' + ' \n' + ' \n' + '\n' + ) + + with pytest.raises(ValueError, match="exceed"): + read_vrt(str(vrt), max_pixels=1) +