diff --git a/xrspatial/geotiff/__init__.py b/xrspatial/geotiff/__init__.py index feffe706..f212be14 100644 --- a/xrspatial/geotiff/__init__.py +++ b/xrspatial/geotiff/__init__.py @@ -460,7 +460,7 @@ def _read_geo_info(source, *, overview_level: int | None = None): from ._dtypes import resolve_bits_per_sample, tiff_dtype_to_numpy from ._geotags import extract_geo_info_with_overview_inheritance from ._header import parse_all_ifds, parse_header, select_overview_ifd - from ._reader import _coerce_path, _is_file_like + from ._reader import _CloudSource, _coerce_path, _is_file_like, _is_fsspec_uri source = _coerce_path(source) if _is_file_like(source): @@ -477,6 +477,17 @@ def _read_geo_info(source, *, overview_level: int | None = None): except (OSError, AttributeError): pass close_data = False + elif isinstance(source, str) and _is_fsspec_uri(source): + # fsspec URI (s3://, gs://, az://, memory://, ...): pull the + # whole file via _CloudSource for metadata parsing. Per-chunk + # pixel reads in the dask graph go through _read_to_array + # which opens its own _CloudSource, so this fetch is metadata-only. + _src = _CloudSource(source) + try: + data = _src.read_all() + finally: + _src.close() + close_data = False elif isinstance(source, str): with open(source, 'rb') as f: import mmap diff --git a/xrspatial/geotiff/tests/test_features.py b/xrspatial/geotiff/tests/test_features.py index 19863785..706e8b95 100644 --- a/xrspatial/geotiff/tests/test_features.py +++ b/xrspatial/geotiff/tests/test_features.py @@ -1051,6 +1051,39 @@ def test_memory_filesystem_full_roundtrip(self, tmp_path): fs.rm('/roundtrip.tif') + def test_dask_path_fsspec_uri_1749(self, tmp_path): + """read_geotiff_dask supports fsspec URIs (issue #1749). + + The eager path already routed through _CloudSource via + _read_to_array. The dask path's _read_geo_info used plain + open(), which failed on memory://, s3://, etc. + """ + pytest.importorskip('fsspec') + import fsspec + + arr = np.arange(64, dtype=np.float32).reshape(8, 8) + + local_path = str(tmp_path / 'src.tif') + to_geotiff(arr, local_path, compression='none') + with open(local_path, 'rb') as f: + tiff_bytes = f.read() + + fs = fsspec.filesystem('memory') + fs.pipe('/dask_1749.tif', tiff_bytes) + + try: + eager = open_geotiff('memory:///dask_1749.tif') + lazy = open_geotiff('memory:///dask_1749.tif', chunks=4) + + # Lazy path is dask-backed + import dask.array as da + assert isinstance(lazy.data, da.Array) + + np.testing.assert_array_equal(lazy.values, eager.values) + np.testing.assert_array_equal(lazy.values, arr) + finally: + fs.rm('/dask_1749.tif') + def test_writer_cloud_scheme_detection(self): """Writer detects cloud schemes.""" from xrspatial.geotiff._writer import _is_fsspec_uri