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.
- async_geotiff-0.1.0b5/PKG-INFO +128 -0
- async_geotiff-0.1.0b5/README.md +95 -0
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/pyproject.toml +12 -7
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/__init__.py +12 -1
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_array.py +31 -12
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_fetch.py +36 -20
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_geotiff.py +37 -36
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_overview.py +18 -13
- async_geotiff-0.1.0b5/src/async_geotiff/_read.py +185 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_tile.py +26 -0
- async_geotiff-0.1.0b5/src/async_geotiff/_windows.py +76 -0
- async_geotiff-0.1.0b5/src/async_geotiff/colormap.py +104 -0
- async_geotiff-0.1.0b5/src/async_geotiff/exceptions.py +5 -0
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/tms.py +4 -4
- async_geotiff-0.1.0b4/PKG-INFO +0 -53
- async_geotiff-0.1.0b4/README.md +0 -20
- async_geotiff-0.1.0b4/src/async_geotiff/_ifd.py +0 -18
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/LICENSE +0 -0
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_crs.py +0 -0
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_transform.py +0 -0
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/_version.py +0 -0
- {async_geotiff-0.1.0b4 → async_geotiff-0.1.0b5}/src/async_geotiff/enums.py +0 -0
- {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
|
+

|
|
@@ -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
|
+

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "async-geotiff"
|
|
3
|
-
version = "0.1.0-beta.
|
|
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.
|
|
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",
|
|
96
|
-
"ANN201",
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
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__ = [
|
|
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) ->
|
|
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) ->
|
|
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
|
-
) ->
|
|
64
|
-
tile_fut = self.
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
array = Array._create( # noqa: SLF001
|
|
82
85
|
data=tile_data,
|
|
83
86
|
mask=mask_data,
|
|
84
|
-
planar_configuration=self._ifd.
|
|
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[
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
152
|
+
final_tiles.append(tile)
|
|
137
153
|
|
|
138
|
-
return
|
|
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:
|
|
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:
|
|
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) ->
|
|
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",
|
|
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],
|
|
85
|
-
mask_ifds: dict[tuple[int, int],
|
|
83
|
+
data_ifds: dict[tuple[int, int], ImageFileDirectory] = {}
|
|
84
|
+
mask_ifds: dict[tuple[int, int], ImageFileDirectory] = {}
|
|
86
85
|
|
|
87
|
-
for
|
|
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] =
|
|
89
|
+
mask_ifds[dims] = ifd
|
|
91
90
|
else:
|
|
92
|
-
data_ifds[dims] =
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
321
|
-
model_scale := self._primary_ifd.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
52
|
-
mask_ifd:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
104
|
+
overview_width = self._ifd.image_width
|
|
100
105
|
full_width = self._geotiff.width
|
|
101
|
-
overview_height = self._ifd.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
51
|
-
blockysize = geotiff._primary_ifd.
|
|
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.
|
|
67
|
-
blockysize = overview._ifd.
|
|
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.")
|
async_geotiff-0.1.0b4/PKG-INFO
DELETED
|
@@ -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
|
async_geotiff-0.1.0b4/README.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|