async-geotiff 0.1.0b4__tar.gz → 0.1.0b5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. async_geotiff-0.1.0b5/PKG-INFO +128 -0
  2. async_geotiff-0.1.0b5/README.md +95 -0
  3. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/pyproject.toml +12 -7
  4. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/__init__.py +12 -1
  5. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_array.py +31 -12
  6. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_fetch.py +36 -20
  7. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_geotiff.py +37 -36
  8. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_overview.py +18 -13
  9. async_geotiff-0.1.0b5/src/async_geotiff/_read.py +185 -0
  10. async_geotiff-0.1.0b5/src/async_geotiff/_tile.py +26 -0
  11. async_geotiff-0.1.0b5/src/async_geotiff/_windows.py +76 -0
  12. async_geotiff-0.1.0b5/src/async_geotiff/colormap.py +104 -0
  13. async_geotiff-0.1.0b5/src/async_geotiff/exceptions.py +5 -0
  14. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/tms.py +4 -4
  15. async_geotiff-0.1.0b4/PKG-INFO +0 -53
  16. async_geotiff-0.1.0b4/README.md +0 -20
  17. async_geotiff-0.1.0b4/src/async_geotiff/_ifd.py +0 -18
  18. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/LICENSE +0 -0
  19. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_crs.py +0 -0
  20. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_transform.py +0 -0
  21. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_version.py +0 -0
  22. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/enums.py +0 -0
  23. {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/py.typed +0 -0
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: async-geotiff
3
+ Version: 0.1.0b5
4
+ Summary: Async GeoTIFF reader for Python
5
+ Keywords: geotiff,tiff,async,cog,raster,gis
6
+ Author: Kyle Barron
7
+ Author-email: Kyle Barron <kyle@developmentseed.org>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: GIS
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: affine>=2.4.0
21
+ Requires-Dist: async-tiff>=0.5.0b3
22
+ Requires-Dist: numpy>=2.0
23
+ Requires-Dist: pyproj>=3.3.0
24
+ Requires-Dist: morecantile>=7.0,<8.0 ; extra == 'morecantile'
25
+ Requires-Python: >=3.11
26
+ Project-URL: Changelog, https://github.com/developmentseed/async-geotiff/blob/main/CHANGELOG.md
27
+ Project-URL: Documentation, https://developmentseed.github.io/async-geotiff/
28
+ Project-URL: Homepage, https://github.com/developmentseed/async-geotiff
29
+ Project-URL: Issues, https://github.com/developmentseed/async-geotiff/issues
30
+ Project-URL: Repository, https://github.com/developmentseed/async-geotiff
31
+ Provides-Extra: morecantile
32
+ Description-Content-Type: text/markdown
33
+
34
+ # async-geotiff
35
+
36
+ Fast, async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping the Rust-based [Async-TIFF][async-tiff] library.
37
+
38
+ [async-tiff]: https://github.com/developmentseed/async-tiff
39
+ [cogeo]: https://cogeo.org/
40
+
41
+ ## Features
42
+
43
+ - Read-only support for GeoTIFF and COG formats.
44
+ - High-level, familiar, easy to use API.
45
+ - Performance-focused:
46
+ - Rust core ensures native performance.
47
+ - CPU-bound tasks like image decoding happen in a thread pool, without blocking the async executor.
48
+ - Buffer protocol integration for zero-copy data sharing between Rust and Python.
49
+ - Lightweight with no GDAL dependency.
50
+ - Integration with [obstore] for efficient data access on object stores.
51
+ - Full type hinting for all operations.
52
+ - Broad decompression support: Deflate, LZW, JPEG, JPEG2000, WebP, ZSTD.
53
+
54
+ **Anti-Features** (features explicitly not in scope):
55
+
56
+ - No pixel resampling.
57
+ - No warping/reprojection.
58
+
59
+ Resampling and warping bring significant additional complexity and are out of scope for this library.
60
+
61
+ [obstore]: https://developmentseed.org/obstore/latest/
62
+ [obspec]: https://developmentseed.org/obspec/latest/
63
+
64
+ ## Example
65
+
66
+ First create a "store", such as an [`S3Store`][S3Store], [`GCSStore`][GCSStore], [`AzureStore`][AzureStore], or [`LocalStore`][LocalStore] for reading data from AWS S3, Google Cloud, Azure Storage, or local files. Refer to [obstore] documentation for more information.
67
+
68
+ [S3Store]: https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Store
69
+ [GCSStore]: https://developmentseed.org/obstore/latest/api/store/gcs/#obstore.store.GCSStore
70
+ [AzureStore]: https://developmentseed.org/obstore/latest/api/store/azure/#obstore.store.AzureStore
71
+ [LocalStore]: https://developmentseed.org/obstore/latest/api/store/local/#obstore.store.LocalStore
72
+
73
+ ```py
74
+ from obstore.store import S3Store
75
+
76
+ store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
77
+ path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/TCI.tif"
78
+ ```
79
+
80
+ Then open a `GeoTIFF`:
81
+
82
+ ```py
83
+ from async_geotiff import GeoTIFF
84
+
85
+ geotiff = await GeoTIFF.open(path, store=store)
86
+ ```
87
+
88
+ On the `GeoTIFF` instance you have metadata about the image, such as its affine transform and Coordinate Reference System:
89
+
90
+ ```py
91
+ geotiff.transform
92
+ # Affine(10.0, 0.0, 300000.0,
93
+ # 0.0, -10.0, 4100040.0)
94
+
95
+ geotiff.crs
96
+ # <Projected CRS: EPSG:32612>
97
+ # Name: WGS 84 / UTM zone 12N
98
+ ```
99
+
100
+ For a COG, you can access the overviews, or reduced resolution versions, of the image:
101
+
102
+ ```py
103
+ # Overviews are ordered from finest to coarsest resolution
104
+ # In this case, access the second-coarsest resolution version of the image
105
+ overview = geotiff.overviews[-2]
106
+ ```
107
+
108
+ Then we can read data from the image. This loads a 512-pixel square from the
109
+ upper-left corner of the selected overview.
110
+
111
+ ```py
112
+ from async_geotiff import Window
113
+
114
+ window = Window(col_off=0, row_off=0, width=512, height=512)
115
+ array = await overview.read(window=window)
116
+ ```
117
+
118
+ This `Array` instance has `data`, `mask`, and some other metadata about the fetched array data.
119
+
120
+ Plot, using [`rasterio.plot.show`](https://rasterio.readthedocs.io/en/stable/api/rasterio.plot.html#rasterio.plot.show) (requires `matplotlib`):
121
+
122
+ ```py
123
+ import rasterio.plot
124
+
125
+ rasterio.plot.show(array.data)
126
+ ```
127
+
128
+ ![](assets/sentinel_2_plot.jpg)
@@ -0,0 +1,95 @@
1
+ # async-geotiff
2
+
3
+ Fast, async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping the Rust-based [Async-TIFF][async-tiff] library.
4
+
5
+ [async-tiff]: https://github.com/developmentseed/async-tiff
6
+ [cogeo]: https://cogeo.org/
7
+
8
+ ## Features
9
+
10
+ - Read-only support for GeoTIFF and COG formats.
11
+ - High-level, familiar, easy to use API.
12
+ - Performance-focused:
13
+ - Rust core ensures native performance.
14
+ - CPU-bound tasks like image decoding happen in a thread pool, without blocking the async executor.
15
+ - Buffer protocol integration for zero-copy data sharing between Rust and Python.
16
+ - Lightweight with no GDAL dependency.
17
+ - Integration with [obstore] for efficient data access on object stores.
18
+ - Full type hinting for all operations.
19
+ - Broad decompression support: Deflate, LZW, JPEG, JPEG2000, WebP, ZSTD.
20
+
21
+ **Anti-Features** (features explicitly not in scope):
22
+
23
+ - No pixel resampling.
24
+ - No warping/reprojection.
25
+
26
+ Resampling and warping bring significant additional complexity and are out of scope for this library.
27
+
28
+ [obstore]: https://developmentseed.org/obstore/latest/
29
+ [obspec]: https://developmentseed.org/obspec/latest/
30
+
31
+ ## Example
32
+
33
+ First create a "store", such as an [`S3Store`][S3Store], [`GCSStore`][GCSStore], [`AzureStore`][AzureStore], or [`LocalStore`][LocalStore] for reading data from AWS S3, Google Cloud, Azure Storage, or local files. Refer to [obstore] documentation for more information.
34
+
35
+ [S3Store]: https://developmentseed.org/obstore/latest/api/store/aws/#obstore.store.S3Store
36
+ [GCSStore]: https://developmentseed.org/obstore/latest/api/store/gcs/#obstore.store.GCSStore
37
+ [AzureStore]: https://developmentseed.org/obstore/latest/api/store/azure/#obstore.store.AzureStore
38
+ [LocalStore]: https://developmentseed.org/obstore/latest/api/store/local/#obstore.store.LocalStore
39
+
40
+ ```py
41
+ from obstore.store import S3Store
42
+
43
+ store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
44
+ path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/TCI.tif"
45
+ ```
46
+
47
+ Then open a `GeoTIFF`:
48
+
49
+ ```py
50
+ from async_geotiff import GeoTIFF
51
+
52
+ geotiff = await GeoTIFF.open(path, store=store)
53
+ ```
54
+
55
+ On the `GeoTIFF` instance you have metadata about the image, such as its affine transform and Coordinate Reference System:
56
+
57
+ ```py
58
+ geotiff.transform
59
+ # Affine(10.0, 0.0, 300000.0,
60
+ # 0.0, -10.0, 4100040.0)
61
+
62
+ geotiff.crs
63
+ # <Projected CRS: EPSG:32612>
64
+ # Name: WGS 84 / UTM zone 12N
65
+ ```
66
+
67
+ For a COG, you can access the overviews, or reduced resolution versions, of the image:
68
+
69
+ ```py
70
+ # Overviews are ordered from finest to coarsest resolution
71
+ # In this case, access the second-coarsest resolution version of the image
72
+ overview = geotiff.overviews[-2]
73
+ ```
74
+
75
+ Then we can read data from the image. This loads a 512-pixel square from the
76
+ upper-left corner of the selected overview.
77
+
78
+ ```py
79
+ from async_geotiff import Window
80
+
81
+ window = Window(col_off=0, row_off=0, width=512, height=512)
82
+ array = await overview.read(window=window)
83
+ ```
84
+
85
+ This `Array` instance has `data`, `mask`, and some other metadata about the fetched array data.
86
+
87
+ Plot, using [`rasterio.plot.show`](https://rasterio.readthedocs.io/en/stable/api/rasterio.plot.html#rasterio.plot.show) (requires `matplotlib`):
88
+
89
+ ```py
90
+ import rasterio.plot
91
+
92
+ rasterio.plot.show(array.data)
93
+ ```
94
+
95
+ ![](assets/sentinel_2_plot.jpg)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-geotiff"
3
- version = "0.1.0-beta.4"
3
+ version = "0.1.0-beta.5"
4
4
  description = "Async GeoTIFF reader for Python"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Kyle Barron", email = "kyle@developmentseed.org" }]
@@ -22,7 +22,7 @@ classifiers = [
22
22
  keywords = ["geotiff", "tiff", "async", "cog", "raster", "gis"]
23
23
  dependencies = [
24
24
  "affine>=2.4.0",
25
- "async-tiff>=0.5.0-beta.1",
25
+ "async-tiff>=0.5.0-beta.3",
26
26
  "numpy>=2.0",
27
27
  "pyproj>=3.3.0",
28
28
  ]
@@ -43,6 +43,7 @@ dev = [
43
43
  "build>=1.4.0",
44
44
  "ipykernel>=7.1.0",
45
45
  "jsonschema>=4.26.0",
46
+ "matplotlib>=3.10.8",
46
47
  "morecantile>=7.0.2",
47
48
  "obstore>=0.8.2",
48
49
  "pydantic>=2.12.5",
@@ -52,6 +53,8 @@ dev = [
52
53
  "types-jsonschema>=4.26.0.20260109",
53
54
  ]
54
55
  docs = [
56
+ # Workaround for https://github.com/mkdocs/mkdocs/issues/4032
57
+ "click<8.3",
55
58
  "mkdocs-material[imaging]>=9.5.49",
56
59
  "mkdocs>=1.6.1",
57
60
  "mkdocstrings[python]>=1.0",
@@ -76,6 +79,7 @@ module = [
76
79
  # https://github.com/rasterio/affine/issues/135
77
80
  "affine.*",
78
81
  "async_tiff.store.*",
82
+ "rasterio.*",
79
83
  ]
80
84
  ignore_missing_imports = true
81
85
 
@@ -92,9 +96,10 @@ ignore = [
92
96
 
93
97
  [tool.ruff.lint.per-file-ignores]
94
98
  "tests/*" = [
95
- "ANN001", # annotation in function argument
96
- "ANN201", # return type annotation
97
- "S101", # assert
98
- "SLF001", # private member access
99
- "D", # docstring
99
+ "ANN001", # annotation in function argument
100
+ "ANN201", # return type annotation
101
+ "PLR2004", # Magic value used in comparison
102
+ "S101", # assert
103
+ "SLF001", # private member access
104
+ "D", # docstring
100
105
  ]
@@ -3,9 +3,20 @@
3
3
  [cogeo]: https://cogeo.org/
4
4
  """
5
5
 
6
+ from . import exceptions
6
7
  from ._array import Array
7
8
  from ._geotiff import GeoTIFF
8
9
  from ._overview import Overview
10
+ from ._tile import Tile
9
11
  from ._version import __version__
12
+ from ._windows import Window
10
13
 
11
- __all__ = ["Array", "GeoTIFF", "Overview", "__version__"]
14
+ __all__ = [
15
+ "Array",
16
+ "GeoTIFF",
17
+ "Overview",
18
+ "Tile",
19
+ "Window",
20
+ "__version__",
21
+ "exceptions",
22
+ ]
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Self
5
5
 
6
6
  import numpy as np
7
7
  from async_tiff.enums import PlanarConfiguration
8
+ from numpy.ma import MaskedArray
8
9
 
9
10
  from async_geotiff._transform import TransformMixin
10
11
 
@@ -43,8 +44,11 @@ class Array(TransformMixin):
43
44
  crs: CRS
44
45
  """The coordinate reference system of the array."""
45
46
 
47
+ nodata: float | None = None
48
+ """The nodata value for the array, if any."""
49
+
46
50
  @classmethod
47
- def _create(
51
+ def _create( # noqa: PLR0913
48
52
  cls,
49
53
  *,
50
54
  data: AsyncTiffArray,
@@ -52,22 +56,12 @@ class Array(TransformMixin):
52
56
  planar_configuration: PlanarConfiguration,
53
57
  transform: Affine,
54
58
  crs: CRS,
59
+ nodata: float | None = None,
55
60
  ) -> Self:
56
61
  """Create an Array from async_tiff data.
57
62
 
58
63
  Handles axis reordering to ensure data is always in (bands, height, width)
59
64
  order, matching rasterio's convention.
60
-
61
- Args:
62
- data: The decoded tile data from async_tiff.
63
- mask: The decoded mask data from async_tiff, if any.
64
- planar_configuration: The planar configuration of the source IFD.
65
- transform: The affine transform for this tile.
66
- crs: The coordinate reference system.
67
-
68
- Returns:
69
- An Array with data in (bands, height, width) order.
70
-
71
65
  """
72
66
  data_arr = np.asarray(data, copy=False)
73
67
  if mask is not None:
@@ -99,4 +93,29 @@ class Array(TransformMixin):
99
93
  count=count,
100
94
  transform=transform,
101
95
  crs=crs,
96
+ nodata=nodata,
102
97
  )
98
+
99
+ def as_masked(self) -> MaskedArray:
100
+ """Return the data as a masked array using the Array mask or nodata value.
101
+
102
+ !!! warning
103
+ In a numpy [`MaskedArray`][numpy.ma.MaskedArray], `True`
104
+ indicates invalid (masked) data and `False` indicates valid data.
105
+
106
+ This is the inverse convention of a GeoTIFF's mask. The boolean array
107
+ [`Array.mask`][async_geotiff.Array.mask] uses `True` for valid data and
108
+ `False` for invalid data.
109
+
110
+ Returns:
111
+ A masked array with the same shape as `data`, where invalid data
112
+ (as indicated by the mask) is masked out.
113
+
114
+ """
115
+ if self.mask is not None:
116
+ return MaskedArray(self.data, mask=~self.mask)
117
+
118
+ if self.nodata is not None:
119
+ return np.ma.masked_equal(self.data, self.nodata)
120
+
121
+ return MaskedArray(self.data)
@@ -5,27 +5,26 @@ from typing import TYPE_CHECKING, Protocol
5
5
 
6
6
  from affine import Affine
7
7
 
8
- from async_geotiff import Array
8
+ from async_geotiff._array import Array
9
+ from async_geotiff._tile import Tile
9
10
  from async_geotiff._transform import HasTransform
10
11
 
11
12
  if TYPE_CHECKING:
12
- from async_tiff import TIFF
13
+ from async_tiff import TIFF, ImageFileDirectory
13
14
  from async_tiff import Array as AsyncTiffArray
14
15
  from pyproj import CRS
15
16
 
16
- from async_geotiff._ifd import IFDReference
17
-
18
17
 
19
18
  class HasTiffReference(HasTransform, Protocol):
20
19
  """Protocol for objects that hold a TIFF reference and can request tiles."""
21
20
 
22
21
  @property
23
- def _ifd(self) -> IFDReference:
22
+ def _ifd(self) -> ImageFileDirectory:
24
23
  """The data IFD for this image (index, IFD)."""
25
24
  ...
26
25
 
27
26
  @property
28
- def _mask_ifd(self) -> IFDReference | None:
27
+ def _mask_ifd(self) -> ImageFileDirectory | None:
29
28
  """The mask IFD for this image (index, IFD), if any."""
30
29
  ...
31
30
 
@@ -49,6 +48,11 @@ class HasTiffReference(HasTransform, Protocol):
49
48
  """The width of tiles in pixels."""
50
49
  ...
51
50
 
51
+ @property
52
+ def nodata(self) -> int | float | None:
53
+ """The nodata value for the image, if any."""
54
+ ...
55
+
52
56
 
53
57
  class FetchTileMixin:
54
58
  """Mixin for fetching tiles from a GeoTIFF.
@@ -60,13 +64,12 @@ class FetchTileMixin:
60
64
  self: HasTiffReference,
61
65
  x: int,
62
66
  y: int,
63
- ) -> Array:
64
- tile_fut = self._tiff.fetch_tile(x, y, self._ifd.index)
67
+ ) -> Tile:
68
+ tile_fut = self._ifd.fetch_tile(x, y)
65
69
 
66
70
  mask_data: AsyncTiffArray | None = None
67
71
  if self._mask_ifd is not None:
68
- mask_ifd_index = self._mask_ifd.index
69
- mask_fut = self._tiff.fetch_tile(x, y, mask_ifd_index)
72
+ mask_fut = self._mask_ifd.fetch_tile(x, y)
70
73
  tile, mask = await asyncio.gather(tile_fut, mask_fut)
71
74
  tile_data, mask_data = await asyncio.gather(tile.decode(), mask.decode())
72
75
  else:
@@ -78,19 +81,26 @@ class FetchTileMixin:
78
81
  y * self.tile_height,
79
82
  )
80
83
 
81
- return Array._create( # noqa: SLF001
84
+ array = Array._create( # noqa: SLF001
82
85
  data=tile_data,
83
86
  mask=mask_data,
84
- planar_configuration=self._ifd.ifd.planar_configuration,
87
+ planar_configuration=self._ifd.planar_configuration,
85
88
  crs=self.crs,
86
89
  transform=tile_transform,
90
+ nodata=self.nodata,
91
+ )
92
+ return Tile(
93
+ x=x,
94
+ y=y,
95
+ _ifd=self._ifd,
96
+ array=array,
87
97
  )
88
98
 
89
99
  async def fetch_tiles(
90
100
  self: HasTiffReference,
91
101
  xs: list[int],
92
102
  ys: list[int],
93
- ) -> list[Array]:
103
+ ) -> list[Tile]:
94
104
  """Fetch multiple tiles from this overview.
95
105
 
96
106
  Args:
@@ -98,12 +108,11 @@ class FetchTileMixin:
98
108
  ys: The y coordinates of the tiles.
99
109
 
100
110
  """
101
- tiles_fut = self._tiff.fetch_tiles(xs, ys, self._ifd.index)
111
+ tiles_fut = self._ifd.fetch_tiles(xs, ys)
102
112
 
103
113
  decoded_masks: list[AsyncTiffArray | None] = [None] * len(xs)
104
114
  if self._mask_ifd is not None:
105
- mask_ifd_index = self._mask_ifd.index
106
- masks_fut = self._tiff.fetch_tiles(xs, ys, mask_ifd_index)
115
+ masks_fut = self._mask_ifd.fetch_tiles(xs, ys)
107
116
  tiles, masks = await asyncio.gather(tiles_fut, masks_fut)
108
117
 
109
118
  decoded_tile_futs = [tile.decode() for tile in tiles]
@@ -114,7 +123,7 @@ class FetchTileMixin:
114
123
  tiles = await tiles_fut
115
124
  decoded_tiles = await asyncio.gather(*[tile.decode() for tile in tiles])
116
125
 
117
- arrays: list[Array] = []
126
+ final_tiles: list[Tile] = []
118
127
  for x, y, tile_data, mask_data in zip(
119
128
  xs,
120
129
  ys,
@@ -129,10 +138,17 @@ class FetchTileMixin:
129
138
  array = Array._create( # noqa: SLF001
130
139
  data=tile_data,
131
140
  mask=mask_data,
132
- planar_configuration=self._ifd.ifd.planar_configuration,
141
+ planar_configuration=self._ifd.planar_configuration,
133
142
  crs=self.crs,
134
143
  transform=tile_transform,
144
+ nodata=self.nodata,
145
+ )
146
+ tile = Tile(
147
+ x=x,
148
+ y=y,
149
+ _ifd=self._ifd,
150
+ array=array,
135
151
  )
136
- arrays.append(array)
152
+ final_tiles.append(tile)
137
153
 
138
- return arrays
154
+ return final_tiles
@@ -10,9 +10,10 @@ from async_tiff.enums import PhotometricInterpretation
10
10
 
11
11
  from async_geotiff._crs import crs_from_geo_keys
12
12
  from async_geotiff._fetch import FetchTileMixin
13
- from async_geotiff._ifd import IFDReference
14
13
  from async_geotiff._overview import Overview
14
+ from async_geotiff._read import ReadMixin
15
15
  from async_geotiff._transform import TransformMixin
16
+ from async_geotiff.colormap import Colormap
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from async_tiff import GeoKeyDirectory, ImageFileDirectory, ObspecInput
@@ -23,7 +24,7 @@ if TYPE_CHECKING:
23
24
 
24
25
 
25
26
  @dataclass(frozen=True, init=False, kw_only=True, repr=False)
26
- class GeoTIFF(FetchTileMixin, TransformMixin):
27
+ class GeoTIFF(ReadMixin, FetchTileMixin, TransformMixin):
27
28
  """A class representing a GeoTIFF image."""
28
29
 
29
30
  _crs: CRS | None = None
@@ -37,16 +38,14 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
37
38
  """The underlying async-tiff TIFF instance that we wrap.
38
39
  """
39
40
 
40
- _primary_ifd: IFDReference = field(init=False)
41
+ _primary_ifd: ImageFileDirectory = field(init=False)
41
42
  """The primary (first) IFD of the GeoTIFF.
42
43
 
43
44
  Some tags, like most geo tags, only exist on the primary IFD.
44
45
  """
45
46
 
46
- _mask_ifd: IFDReference | None = None
47
+ _mask_ifd: ImageFileDirectory | None = None
47
48
  """The mask IFD of the full-resolution GeoTIFF, if any.
48
-
49
- (positional index of the IFD in the TIFF file, IFD object)
50
49
  """
51
50
 
52
51
  _gkd: GeoKeyDirectory = field(init=False)
@@ -58,7 +57,7 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
58
57
  """
59
58
 
60
59
  @property
61
- def _ifd(self) -> IFDReference:
60
+ def _ifd(self) -> ImageFileDirectory:
62
61
  """An alias for the primary IFD to satisfy _fetch protocol."""
63
62
  return self._primary_ifd
64
63
 
@@ -76,20 +75,20 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
76
75
 
77
76
  # We use object.__setattr__ because the dataclass is frozen
78
77
  object.__setattr__(self, "_tiff", tiff)
79
- object.__setattr__(self, "_primary_ifd", IFDReference(index=0, ifd=first_ifd))
78
+ object.__setattr__(self, "_primary_ifd", first_ifd)
80
79
  object.__setattr__(self, "_gkd", gkd)
81
80
 
82
81
  # Separate data IFDs and mask IFDs (skip the primary IFD at index 0)
83
82
  # Data IFDs are indexed by (width, height) for matching with masks
84
- data_ifds: dict[tuple[int, int], IFDReference] = {}
85
- mask_ifds: dict[tuple[int, int], IFDReference] = {}
83
+ data_ifds: dict[tuple[int, int], ImageFileDirectory] = {}
84
+ mask_ifds: dict[tuple[int, int], ImageFileDirectory] = {}
86
85
 
87
- for idx, ifd in enumerate(tiff.ifds[1:], start=1):
86
+ for ifd in tiff.ifds[1:]:
88
87
  dims = (ifd.image_width, ifd.image_height)
89
88
  if is_mask_ifd(ifd):
90
- mask_ifds[dims] = IFDReference(index=idx, ifd=ifd)
89
+ mask_ifds[dims] = ifd
91
90
  else:
92
- data_ifds[dims] = IFDReference(index=idx, ifd=ifd)
91
+ data_ifds[dims] = ifd
93
92
 
94
93
  # Find and set the mask for the primary IFD (matches primary dimensions)
95
94
  if primary_mask_ifd := mask_ifds.get(
@@ -192,23 +191,18 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
192
191
  # https://github.com/developmentseed/async-geotiff/issues/12
193
192
  raise NotImplementedError
194
193
 
195
- def colormap(self, bidx: int) -> dict[int, tuple[int, int, int]]:
196
- """Return a dict containing the colormap for a band.
197
-
198
- Args:
199
- bidx: The 1-based index of the band whose colormap will be returned.
194
+ @property
195
+ def colormap(self) -> Colormap | None:
196
+ """Return the Colormap stored in the file, if any.
200
197
 
201
198
  Returns:
202
- Mapping of color index value (starting at 0) to RGBA color as a
203
- 4-element tuple.
204
-
205
- Raises:
206
- ValueError: If no colormap is found for the specified band (NULL color
207
- table).
208
- IndexError: If no band exists for the provided index.
199
+ A Colormap instance if the dataset has a colormap, else None.
209
200
 
210
201
  """
211
- raise NotImplementedError
202
+ if upstream_colormap := self._primary_ifd.colormap:
203
+ return Colormap(_cmap=upstream_colormap, _nodata=self.nodata)
204
+
205
+ return None
212
206
 
213
207
  @property
214
208
  def compression(self) -> Compression:
@@ -247,14 +241,14 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
247
241
  @property
248
242
  def height(self) -> int:
249
243
  """The height (number of rows) of the full image."""
250
- return self._primary_ifd.ifd.image_height
244
+ return self._primary_ifd.image_height
251
245
 
252
246
  def indexes(self) -> list[int]:
253
247
  """Return the 1-based indexes of each band in the dataset.
254
248
 
255
249
  For a 3-band dataset, this property will be [1, 2, 3].
256
250
  """
257
- return list(range(1, self._primary_ifd.ifd.samples_per_pixel + 1))
251
+ return list(range(1, self._primary_ifd.samples_per_pixel + 1))
258
252
 
259
253
  @property
260
254
  def interleaving(self) -> Interleaving:
@@ -271,7 +265,7 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
271
265
  @property
272
266
  def nodata(self) -> float | None:
273
267
  """The dataset's single nodata value."""
274
- nodata = self._primary_ifd.ifd.gdal_nodata
268
+ nodata = self._primary_ifd.gdal_nodata
275
269
  if nodata is None:
276
270
  return None
277
271
 
@@ -279,7 +273,14 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
279
273
 
280
274
  @property
281
275
  def overviews(self) -> list[Overview]:
282
- """A list of overview levels for the dataset."""
276
+ """A list of overview levels for the dataset.
277
+
278
+ Overviews are reduced-resolution versions of the main image used for faster
279
+ rendering at lower zoom levels.
280
+
281
+ This list of overviews is ordered from finest to coarsest resolution. The first
282
+ element of the list is the highest-resolution after the base image.
283
+ """
283
284
  return self._overviews
284
285
 
285
286
  @property
@@ -303,12 +304,12 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
303
304
  @property
304
305
  def tile_height(self) -> int:
305
306
  """The height in pixels per tile of the image."""
306
- return self._primary_ifd.ifd.tile_height or self.height
307
+ return self._primary_ifd.tile_height or self.height
307
308
 
308
309
  @property
309
310
  def tile_width(self) -> int:
310
311
  """The width in pixels per tile of the image."""
311
- return self._primary_ifd.ifd.tile_width or self.width
312
+ return self._primary_ifd.tile_width or self.width
312
313
 
313
314
  @property
314
315
  def transform(self) -> Affine:
@@ -317,8 +318,8 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
317
318
  This transform maps pixel row/column coordinates to coordinates in the dataset's
318
319
  CRS.
319
320
  """
320
- if (tie_points := self._primary_ifd.ifd.model_tiepoint) and (
321
- model_scale := self._primary_ifd.ifd.model_pixel_scale
321
+ if (tie_points := self._primary_ifd.model_tiepoint) and (
322
+ model_scale := self._primary_ifd.model_pixel_scale
322
323
  ):
323
324
  x_origin = tie_points[3]
324
325
  y_origin = tie_points[4]
@@ -327,7 +328,7 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
327
328
 
328
329
  return Affine(x_resolution, 0, x_origin, 0, y_resolution, y_origin)
329
330
 
330
- if model_transformation := self._primary_ifd.ifd.model_transformation:
331
+ if model_transformation := self._primary_ifd.model_transformation:
331
332
  # ModelTransformation is a 4x4 matrix in row-major order
332
333
  # [0 1 2 3 ] [a b 0 c]
333
334
  # [4 5 6 7 ] = [d e 0 f]
@@ -358,7 +359,7 @@ class GeoTIFF(FetchTileMixin, TransformMixin):
358
359
  @property
359
360
  def width(self) -> int:
360
361
  """The width (number of columns) of the full image."""
361
- return self._primary_ifd.ifd.image_width
362
+ return self._primary_ifd.image_width
362
363
 
363
364
 
364
365
  def has_geokeys(ifd: ImageFileDirectory) -> bool:
@@ -6,20 +6,20 @@ from typing import TYPE_CHECKING
6
6
  from affine import Affine
7
7
 
8
8
  from async_geotiff._fetch import FetchTileMixin
9
+ from async_geotiff._read import ReadMixin
9
10
  from async_geotiff._transform import TransformMixin
10
11
 
11
12
  if TYPE_CHECKING:
12
- from async_tiff import TIFF, GeoKeyDirectory
13
+ from async_tiff import TIFF, GeoKeyDirectory, ImageFileDirectory
13
14
  from pyproj.crs import CRS
14
15
 
15
16
  from async_geotiff import GeoTIFF
16
- from async_geotiff._ifd import IFDReference
17
17
 
18
18
  # ruff: noqa: SLF001
19
19
 
20
20
 
21
21
  @dataclass(init=False, frozen=True, kw_only=True, eq=False, repr=False)
22
- class Overview(FetchTileMixin, TransformMixin):
22
+ class Overview(ReadMixin, FetchTileMixin, TransformMixin):
23
23
  """An overview level of a Cloud-Optimized GeoTIFF image."""
24
24
 
25
25
  _geotiff: GeoTIFF
@@ -30,13 +30,13 @@ class Overview(FetchTileMixin, TransformMixin):
30
30
  """The GeoKeyDirectory of the primary IFD.
31
31
  """
32
32
 
33
- _ifd: IFDReference
33
+ _ifd: ImageFileDirectory
34
34
  """The IFD for this overview level.
35
35
 
36
36
  (positional index of the IFD in the TIFF file, IFD object)
37
37
  """
38
38
 
39
- _mask_ifd: IFDReference | None
39
+ _mask_ifd: ImageFileDirectory | None
40
40
  """The IFD for the mask associated with this overview level, if any.
41
41
 
42
42
  (positional index of the IFD in the TIFF file, IFD object)
@@ -48,8 +48,8 @@ class Overview(FetchTileMixin, TransformMixin):
48
48
  *,
49
49
  geotiff: GeoTIFF,
50
50
  gkd: GeoKeyDirectory,
51
- ifd: IFDReference,
52
- mask_ifd: IFDReference | None,
51
+ ifd: ImageFileDirectory,
52
+ mask_ifd: ImageFileDirectory | None,
53
53
  ) -> Overview:
54
54
  instance = cls.__new__(cls)
55
55
 
@@ -74,17 +74,22 @@ class Overview(FetchTileMixin, TransformMixin):
74
74
  @property
75
75
  def height(self) -> int:
76
76
  """The height of the overview in pixels."""
77
- return self._ifd.ifd.image_height
77
+ return self._ifd.image_height
78
+
79
+ @property
80
+ def nodata(self) -> int | float | None:
81
+ """The nodata value for the overview, if any."""
82
+ return self._geotiff.nodata
78
83
 
79
84
  @property
80
85
  def tile_height(self) -> int:
81
86
  """The height in pixels per tile of the overview."""
82
- return self._ifd.ifd.tile_height or self.height
87
+ return self._ifd.tile_height or self.height
83
88
 
84
89
  @property
85
90
  def tile_width(self) -> int:
86
91
  """The width in pixels per tile of the overview."""
87
- return self._ifd.ifd.tile_width or self.width
92
+ return self._ifd.tile_width or self.width
88
93
 
89
94
  @property
90
95
  def transform(self) -> Affine: # type: ignore[override]
@@ -96,9 +101,9 @@ class Overview(FetchTileMixin, TransformMixin):
96
101
  """
97
102
  full_transform = self._geotiff.transform
98
103
 
99
- overview_width = self._ifd.ifd.image_width
104
+ overview_width = self._ifd.image_width
100
105
  full_width = self._geotiff.width
101
- overview_height = self._ifd.ifd.image_height
106
+ overview_height = self._ifd.image_height
102
107
  full_height = self._geotiff.height
103
108
 
104
109
  scale_x = full_width / overview_width
@@ -109,4 +114,4 @@ class Overview(FetchTileMixin, TransformMixin):
109
114
  @property
110
115
  def width(self) -> int:
111
116
  """The width of the overview in pixels."""
112
- return self._ifd.ifd.image_width
117
+ return self._ifd.image_width
@@ -0,0 +1,185 @@
1
+ """Higher-level read utilities for cross-tile operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ import numpy as np
8
+ from affine import Affine
9
+
10
+ from async_geotiff._array import Array
11
+ from async_geotiff._fetch import HasTiffReference
12
+ from async_geotiff._windows import Window
13
+ from async_geotiff.exceptions import WindowError
14
+
15
+ if TYPE_CHECKING:
16
+ from numpy.typing import NDArray
17
+
18
+ from async_geotiff._tile import Tile
19
+
20
+
21
+ class CanFetchTiles(HasTiffReference, Protocol):
22
+ """Protocol for objects that can fetch tiles."""
23
+
24
+ @property
25
+ def height(self) -> int:
26
+ """The height of the image in pixels."""
27
+ ...
28
+
29
+ @property
30
+ def width(self) -> int:
31
+ """The width of the image in pixels."""
32
+ ...
33
+
34
+ async def fetch_tiles(
35
+ self,
36
+ xs: list[int],
37
+ ys: list[int],
38
+ ) -> list[Tile]: ...
39
+
40
+
41
+ class ReadMixin:
42
+ async def read(
43
+ self: CanFetchTiles,
44
+ *,
45
+ window: Window | None = None,
46
+ ) -> Array:
47
+ """Read pixel data for a window region.
48
+
49
+ This method fetches all tiles that intersect the given window and
50
+ stitches them together, returning only the pixels within the window.
51
+
52
+ Args:
53
+ window: A Window object defining the pixel region to read.
54
+ If None, the entire image is read.
55
+
56
+ Returns:
57
+ An Array containing the pixel data for the requested window.
58
+
59
+ Raises:
60
+ WindowError: If the window extends outside the image bounds.
61
+
62
+ """
63
+ return await read(self, window=window)
64
+
65
+
66
+ async def read(
67
+ self: CanFetchTiles,
68
+ *,
69
+ window: Window | None = None,
70
+ ) -> Array:
71
+ if isinstance(window, Window):
72
+ win = window
73
+ else:
74
+ win = Window(col_off=0, row_off=0, width=self.width, height=self.height)
75
+
76
+ # Most validation occurred in construction of Window; here we just check against
77
+ # image size
78
+ if win.col_off + win.width > self.width or win.row_off + win.height > self.height:
79
+ raise WindowError(
80
+ f"Window extends outside image bounds.\n"
81
+ f"Window: cols={win.col_off}:{win.col_off + win.width}, "
82
+ f"rows={win.row_off}:{win.row_off + win.height}.\n"
83
+ f"Image size: {self.height}x{self.width}",
84
+ )
85
+
86
+ # Calculate which tiles we need to fetch
87
+ tile_x_start = win.col_off // self.tile_width
88
+ tile_x_stop = (win.col_off + win.width - 1) // self.tile_width + 1
89
+ tile_y_start = win.row_off // self.tile_height
90
+ tile_y_stop = (win.row_off + win.height - 1) // self.tile_height + 1
91
+
92
+ # Build list of tile coordinates
93
+ xs: list[int] = []
94
+ ys: list[int] = []
95
+ for tx in range(tile_x_start, tile_x_stop):
96
+ for ty in range(tile_y_start, tile_y_stop):
97
+ xs.append(tx)
98
+ ys.append(ty)
99
+
100
+ tiles = await self.fetch_tiles(xs, ys)
101
+
102
+ num_bands = tiles[0].array.count
103
+ dtype = tiles[0].array.data.dtype
104
+
105
+ # Create output array and mask array
106
+ output_data = np.empty((num_bands, win.height, win.width), dtype=dtype)
107
+ output_mask: NDArray[np.bool_] | None = None
108
+ if self._mask_ifd is not None:
109
+ output_mask = np.ones((win.height, win.width), dtype=np.bool_)
110
+
111
+ assemble_tiles(
112
+ tiles=tiles,
113
+ window=win,
114
+ tile_width=self.tile_width,
115
+ tile_height=self.tile_height,
116
+ output_data=output_data,
117
+ output_mask=output_mask,
118
+ )
119
+
120
+ window_transform = self.transform * Affine.translation(
121
+ win.col_off,
122
+ win.row_off,
123
+ )
124
+
125
+ return Array(
126
+ data=output_data,
127
+ mask=output_mask,
128
+ width=win.width,
129
+ height=win.height,
130
+ count=num_bands,
131
+ transform=window_transform,
132
+ crs=self.crs,
133
+ )
134
+
135
+
136
+ def assemble_tiles( # noqa: PLR0913
137
+ *,
138
+ tiles: list[Tile],
139
+ window: Window,
140
+ tile_width: int,
141
+ tile_height: int,
142
+ output_data: NDArray[np.generic],
143
+ output_mask: NDArray[np.bool_] | None,
144
+ ) -> None:
145
+ """Assemble multiple tiles into output arrays.
146
+
147
+ This function copies data from tiles into the appropriate positions
148
+ in the output arrays, handling partial tiles at window boundaries.
149
+ """
150
+ for tile in tiles:
151
+ # Create a window for this tile's position in image coordinates
152
+ tile_window = Window(
153
+ col_off=tile.x * tile_width,
154
+ row_off=tile.y * tile_height,
155
+ width=tile.array.width,
156
+ height=tile.array.height,
157
+ )
158
+
159
+ # Calculate the intersection between tile and target window
160
+ overlap = window.intersection(tile_window)
161
+
162
+ # Calculate source slice within the tile
163
+ src_col_start = overlap.col_off - tile_window.col_off
164
+ src_col_stop = src_col_start + overlap.width
165
+ src_row_start = overlap.row_off - tile_window.row_off
166
+ src_row_stop = src_row_start + overlap.height
167
+
168
+ # Calculate destination slice within the output
169
+ dst_col_start = overlap.col_off - window.col_off
170
+ dst_col_stop = dst_col_start + overlap.width
171
+ dst_row_start = overlap.row_off - window.row_off
172
+ dst_row_stop = dst_row_start + overlap.height
173
+
174
+ # Copy data and mask if present
175
+ output_data[
176
+ :,
177
+ dst_row_start:dst_row_stop,
178
+ dst_col_start:dst_col_stop,
179
+ ] = tile.array.data[:, src_row_start:src_row_stop, src_col_start:src_col_stop]
180
+
181
+ if output_mask is not None and tile.array.mask is not None:
182
+ output_mask[
183
+ dst_row_start:dst_row_stop,
184
+ dst_col_start:dst_col_stop,
185
+ ] = tile.array.mask[src_row_start:src_row_stop, src_col_start:src_col_stop]
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from async_tiff import ImageFileDirectory
8
+
9
+ from async_geotiff._array import Array
10
+
11
+
12
+ @dataclass(frozen=True, kw_only=True, eq=False)
13
+ class Tile:
14
+ """A tile from a GeoTIFF, containing array data and grid position."""
15
+
16
+ x: int
17
+ """The tile column index in the GeoTIFF or overview."""
18
+
19
+ y: int
20
+ """The tile row index in the GeoTIFF or overview."""
21
+
22
+ _ifd: ImageFileDirectory
23
+ """A reference to the IFD this tile belongs to."""
24
+
25
+ array: Array
26
+ """The array data for this tile."""
@@ -0,0 +1,76 @@
1
+ """Window utilities for defining rectangular subsets of rasters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from async_geotiff.exceptions import WindowError
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Window:
12
+ """A rectangular subset of a raster.
13
+
14
+ Windows define pixel regions using column/row offsets and dimensions.
15
+ This class is similar to rasterio's [Window][rasterio.windows.Window] but supports
16
+ integer offsets and ranges only.
17
+ """
18
+
19
+ col_off: int
20
+ """The column offset (x position of the left edge)."""
21
+
22
+ row_off: int
23
+ """The row offset (y position of the top edge)."""
24
+
25
+ width: int
26
+ """The width in pixels (number of columns)."""
27
+
28
+ height: int
29
+ """The height in pixels (number of rows)."""
30
+
31
+ def __post_init__(self) -> None:
32
+ """Validate window dimensions."""
33
+ if self.col_off < 0 or self.row_off < 0:
34
+ raise WindowError(
35
+ f"Window start indices must be non-negative, "
36
+ f"got col_off={self.col_off}, row_off={self.row_off}",
37
+ )
38
+
39
+ if self.width <= 0:
40
+ raise WindowError(f"Window width must be positive, got {self.width}")
41
+
42
+ if self.height <= 0:
43
+ raise WindowError(f"Window height must be positive, got {self.height}")
44
+
45
+ def __repr__(self) -> str:
46
+ """Return a nicely formatted representation string."""
47
+ return (
48
+ f"async_geotiff.Window(col_off={self.col_off}, row_off={self.row_off}, "
49
+ f"width={self.width}, height={self.height})"
50
+ )
51
+
52
+ def intersection(self, other: Window) -> Window:
53
+ """Compute the intersection with another window.
54
+
55
+ Args:
56
+ other: Another Window object.
57
+
58
+ Returns:
59
+ A new Window representing the overlapping region.
60
+
61
+ Raises:
62
+ WindowError: If windows do not intersect.
63
+
64
+ """
65
+ col_off = max(self.col_off, other.col_off)
66
+ row_off = max(self.row_off, other.row_off)
67
+ col_stop = min(self.col_off + self.width, other.col_off + other.width)
68
+ row_stop = min(self.row_off + self.height, other.row_off + other.height)
69
+
70
+ width = col_stop - col_off
71
+ height = row_stop - row_off
72
+
73
+ if width <= 0 or height <= 0:
74
+ raise WindowError(f"Windows do not intersect: {self} and {other}")
75
+
76
+ return Window(col_off=col_off, row_off=row_off, width=width, height=height)
@@ -0,0 +1,104 @@
1
+ """High-level Colormap class for GeoTIFF colormaps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+
10
+ if TYPE_CHECKING:
11
+ from async_tiff import Colormap as AsyncTiffColormap
12
+ from numpy.typing import NDArray
13
+
14
+
15
+ @dataclass(frozen=True, kw_only=True, eq=False)
16
+ class Colormap:
17
+ """A representation of a GeoTIFF colormap.
18
+
19
+ GeoTIFF colormaps
20
+ """
21
+
22
+ _cmap: AsyncTiffColormap
23
+ """The colormap data held in Rust, accessible via the buffer protocol.
24
+
25
+ Has shape `(N, 3)` and is of data type uint16.
26
+ """
27
+
28
+ _nodata: int | float | None
29
+ """The nodata value from gdal_nodata, if set."""
30
+
31
+ def as_array(self, *, dtype: type[np.uint8 | np.uint16] = np.uint8) -> NDArray:
32
+ """Return the colormap as a NumPy array with shape (N, 3) and dtype uint16.
33
+
34
+ Each row corresponds to a color entry in the colormap, with columns
35
+ representing the Red, Green, and Blue components respectively.
36
+
37
+ This is the most efficient way to access and apply the colormap data.
38
+
39
+ ```py
40
+ geotiff = await GeoTIFF.open(...)
41
+ array = await geotiff.fetch_tile(0, 0)
42
+
43
+ colormap = geotiff.colormap
44
+ colormap_array = colormap.as_array()
45
+
46
+ rgb_data = colormap_array[array.data[0]]
47
+ # A 3D array with shape (height, width, 3)
48
+ ```
49
+
50
+ Returns:
51
+ A NumPy array representation of the colormap.
52
+
53
+ """
54
+ cmap_array = np.asarray(self._cmap)
55
+ if dtype == np.uint8:
56
+ return (cmap_array >> 8).astype(np.uint8)
57
+ if dtype == np.uint16:
58
+ return cmap_array
59
+ raise ValueError("dtype must be either np.uint8 or np.uint16.")
60
+
61
+ def as_dict(
62
+ self,
63
+ *,
64
+ dtype: type[np.uint8 | np.uint16] = np.uint8,
65
+ ) -> dict[int, tuple[int, int, int]]:
66
+ """Return the colormap as a dictionary mapping indices to RGB tuples.
67
+
68
+ Returns:
69
+ A dictionary where keys are indices and values are tuples of
70
+ (Red, Green, Blue) components.
71
+
72
+ """
73
+ cmap_array = self.as_array(dtype=dtype)
74
+ return {
75
+ int(idx): (int(r), int(g), int(b))
76
+ for idx, (r, g, b) in enumerate(cmap_array)
77
+ }
78
+
79
+ def as_rasterio(self) -> dict[int, tuple[int, int, int, int]]:
80
+ """Return the colormap as a mapping to 8-bit RGBA colors.
81
+
82
+ This returns a colormap in the same format as rasterio's
83
+ [`DatasetReader.colormap`][rasterio.io.DatasetReader.colormap] method.
84
+
85
+ This is the same as
86
+ [`Colormap.as_dict`][async_geotiff.colormap.Colormap.as_dict] with:
87
+
88
+ - `dtype` set to `np.uint8`
89
+ - an added alpha channel set to 255, **except** for the nodata value, if
90
+ defined, which has an alpha of 0.
91
+
92
+ Returns:
93
+ Mapping of color index value (starting at 0) to RGBA color as a 4-element
94
+ tuple.
95
+
96
+ """
97
+ cmap_array = self.as_array(dtype=np.uint8)
98
+ cmap_dict: dict[int, tuple[int, int, int, int]] = {}
99
+
100
+ for idx, (r, g, b) in enumerate(cmap_array):
101
+ alpha = 255 if self._nodata is None or idx != self._nodata else 0
102
+ cmap_dict[int(idx)] = (int(r), int(g), int(b), alpha)
103
+
104
+ return cmap_dict
@@ -0,0 +1,5 @@
1
+ """Exceptions for async_geotiff package."""
2
+
3
+
4
+ class WindowError(Exception):
5
+ """Exception raised for window-related errors."""
@@ -47,8 +47,8 @@ def generate_tms(
47
47
  bounds = geotiff.bounds
48
48
  crs = geotiff.crs
49
49
  tr = geotiff.transform
50
- blockxsize = geotiff._primary_ifd.ifd.tile_width # noqa: SLF001
51
- blockysize = geotiff._primary_ifd.ifd.tile_height # noqa: SLF001
50
+ blockxsize = geotiff._primary_ifd.tile_width # noqa: SLF001
51
+ blockysize = geotiff._primary_ifd.tile_height # noqa: SLF001
52
52
 
53
53
  if blockxsize is None or blockysize is None:
54
54
  raise ValueError("GeoTIFF must be tiled to generate a TMS.")
@@ -63,8 +63,8 @@ def generate_tms(
63
63
 
64
64
  for idx, overview in enumerate(reversed(geotiff.overviews)):
65
65
  overview_tr = overview.transform
66
- blockxsize = overview._ifd.ifd.tile_width # noqa: SLF001
67
- blockysize = overview._ifd.ifd.tile_height # noqa: SLF001
66
+ blockxsize = overview._ifd.tile_width # noqa: SLF001
67
+ blockysize = overview._ifd.tile_height # noqa: SLF001
68
68
 
69
69
  if blockxsize is None or blockysize is None:
70
70
  raise ValueError("GeoTIFF overviews must be tiled to generate a TMS.")
@@ -1,53 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: async-geotiff
3
- Version: 0.1.0b4
4
- Summary: Async GeoTIFF reader for Python
5
- Keywords: geotiff,tiff,async,cog,raster,gis
6
- Author: Kyle Barron
7
- Author-email: Kyle Barron <kyle@developmentseed.org>
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Programming Language :: Python :: 3.14
18
- Classifier: Topic :: Scientific/Engineering :: GIS
19
- Classifier: Typing :: Typed
20
- Requires-Dist: affine>=2.4.0
21
- Requires-Dist: async-tiff>=0.5.0b1
22
- Requires-Dist: numpy>=2.0
23
- Requires-Dist: pyproj>=3.3.0
24
- Requires-Dist: morecantile>=7.0,<8.0 ; extra == 'morecantile'
25
- Requires-Python: >=3.11
26
- Project-URL: Changelog, https://github.com/developmentseed/async-geotiff/blob/main/CHANGELOG.md
27
- Project-URL: Documentation, https://developmentseed.github.io/async-geotiff/
28
- Project-URL: Homepage, https://github.com/developmentseed/async-geotiff
29
- Project-URL: Issues, https://github.com/developmentseed/async-geotiff/issues
30
- Project-URL: Repository, https://github.com/developmentseed/async-geotiff
31
- Provides-Extra: morecantile
32
- Description-Content-Type: text/markdown
33
-
34
- # async-geotiff
35
-
36
- Async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping [`async-tiff`][async-tiff].
37
-
38
- [async-tiff]: https://github.com/developmentseed/async-tiff
39
- [cogeo]: https://cogeo.org/
40
-
41
- ## Project Goals:
42
-
43
- - Support only for GeoTIFF and Cloud-Optimized GeoTIFF (COG) formats
44
- - Support for reading only, no writing support
45
- - Full type hinting.
46
- - API similar to rasterio where possible.
47
- - We won't support the full rasterio API, but we'll try to when it's possible to implement rasterio APIs with straightforward maintenance requirements.
48
- - For methods where we do intentionally try to match with rasterio, the tests should match against rasterio.
49
- - Initially, we'll try to support a core set of GeoTIFF formats. Obscure GeoTIFF files may not be supported.
50
-
51
- ## References
52
-
53
- - aiocogeo: https://github.com/geospatial-jeff/aiocogeo
@@ -1,20 +0,0 @@
1
- # async-geotiff
2
-
3
- Async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping [`async-tiff`][async-tiff].
4
-
5
- [async-tiff]: https://github.com/developmentseed/async-tiff
6
- [cogeo]: https://cogeo.org/
7
-
8
- ## Project Goals:
9
-
10
- - Support only for GeoTIFF and Cloud-Optimized GeoTIFF (COG) formats
11
- - Support for reading only, no writing support
12
- - Full type hinting.
13
- - API similar to rasterio where possible.
14
- - We won't support the full rasterio API, but we'll try to when it's possible to implement rasterio APIs with straightforward maintenance requirements.
15
- - For methods where we do intentionally try to match with rasterio, the tests should match against rasterio.
16
- - Initially, we'll try to support a core set of GeoTIFF formats. Obscure GeoTIFF files may not be supported.
17
-
18
- ## References
19
-
20
- - aiocogeo: https://github.com/geospatial-jeff/aiocogeo
@@ -1,18 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from async_tiff import ImageFileDirectory
8
-
9
-
10
- @dataclass(frozen=True, kw_only=True, repr=False)
11
- class IFDReference:
12
- """A reference to an Image File Directory (IFD) in a TIFF file."""
13
-
14
- index: int
15
- """The positional index of the IFD in the TIFF file."""
16
-
17
- ifd: ImageFileDirectory
18
- """The IFD object itself."""
File without changes