Metadata-Version: 2.4
Name: cog-tiles
Version: 0.1.2
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Dist: numpy>=1.22
Summary: Web map tiles and stitched rasters from tile-aligned Cloud Optimized GeoTIFFs, local or in S3, GCS, Azure or HTTP
Keywords: cog,geotiff,tiles,webmercator,gis,raster
Home-Page: https://martin.maplibre.org/
License-Expression: MIT OR Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/maplibre/martin/tree/main/cog-tiles-py
Project-URL: Repository, https://github.com/maplibre/martin

# cog-tiles for Python

Web map tiles and stitched rasters from tile-aligned Cloud Optimized GeoTIFFs (COGs), local or in
S3, GCS, Azure or over HTTP, fetching only the bytes each tile needs.
Python bindings for the Rust [`cog-tiles`](https://crates.io/crates/cog-tiles) crate used by
[Martin](https://github.com/maplibre/martin).

```python
import cog_tiles

cog = cog_tiles.open("s3://bucket/ortho.tif", options={"aws_region": "us-west-2"})
cog.min_zoom, cog.max_zoom, cog.tile_size, cog.format, cog.bounds

# One encoded tile, ready to serve or store (JPEG/WebP as stored, others as PNG).
png = cog.tile(18, 41936, 101296)

# Every tile in an area, fetched 64 at a time in the background, in arrival order.
for z, x, y, data in cog.tiles(bbox=(-122.42, 37.77, -122.41, 37.78), zoom=range(16, 19)):
    ...

# An area stitched into one (height, width, bands) uint8 array, decoded on all cores.
raster = cog.read((-122.42, 37.77, -122.41, 37.78), zoom=18)
raster.data.shape     # (1311, 1166, 3)
raster.transform      # GDAL geotransform in EPSG:3857
```

![What tile, tiles and read return](https://raw.githubusercontent.com/maplibre/martin/main/cog-tiles-py/docs/images/tile-tiles-read.svg)

Many files, each covering one tile at a file zoom and named after it:

```python
mosaic = cog_tiles.open_mosaic("s3://bucket/ortho/{quadkey}.tif", file_zoom=12)
raster = mosaic.read(bbox, zoom=18)
```

Templates use `{quadkey}`, or `{x}` and `{y}` with an optional `{z}` (`{-y}` counts rows from the
bottom, as in TMS). One file is found by listing the location when it is opened; the others are
opened on first use and cached.

Files in Web Mercator or a UTM zone, listed by a `gdaltindex` tile index and reprojected onto
Web Mercator tiles:

```python
# gdaltindex -f GPKG -t_srs EPSG:4326 naip.gpkg *.tif
naip = cog_tiles.open_tile_index("naip.gpkg", min_zoom=12, nodata=0)
naip.bounds  # the union of the files' footprints
png = naip.tile(14, 2619, 6334)
```

The whole index is read when it is opened. Each tile is painted from the files that touch it, in the
index's row order, so a pixel takes the first file's data; it is a PNG with an alpha band that is
transparent where no file has data. `bands` picks the bands served, counted from 1 (`[1, 2, 3]` by
default, or one band for gray). Locations may be paths relative to the index, URLs, or GDAL
`/vsis3/`, `/vsigs/`, `/vsiaz/` and `/vsicurl/` paths, read with `options` as in `open`.

Or the items a STAC API finds for each tile:

```python
naip = cog_tiles.open_stac(
    "https://earth-search.aws.element84.com/v1", "naip", "image", min_zoom=13,
    datetime="2020-01-01T00:00:00Z/..", query={"naip:state": {"eq": "ca"}}, nodata=0,
    options={"aws_request_payer": "true"},
)
```

One search covers a tile at `search_zoom` (`min_zoom` by default) and serves every tile below it;
results are cached for `search_expiry` seconds. Items are painted newest first (`order="oldest"`
for the other way), each pixel from the first item with data for it. `bands` selects bands by
number or `eo:bands` name, `["red", "green", "blue"]` by default.

With `asyncio`, e.g. in a tile server, every call that waits has an `_async` twin:

```python
cog = await cog_tiles.open_async("s3://bucket/ortho.tif")
png = await cog.tile_async(18, 41936, 101296)
raster = await cog.read_async(bbox, zoom=18)
async for z, x, y, data in cog.tiles(bbox, zoom=18):
    ...
```

The fetches run on background threads, so awaited calls neither block the event loop nor hold a
thread each.

## Tile format

`format`, `quality` and `background` re-encode every tile, on `open` and `open_mosaic` alike:

```python
cog = cog_tiles.open("s3://bucket/ortho.tif", format="webp", quality=80)
cog = cog_tiles.open("s3://bucket/ortho.tif", format="webp", quality="lossless")
cog = cog_tiles.open("s3://bucket/ortho.tif", format="jpeg", quality=90, background="black")
```

| `format` | `quality`                                | transparency                                                   |
|----------|------------------------------------------|----------------------------------------------------------------|
| `png`    | -                                        | kept                                                           |
| `webp`   | 1 to 100 or `"lossless"`, 80 when unset  | kept exactly, even lossy                                       |
| `jpeg`   | 1 to 100, 80 when unset                  | painted over `background`: a CSS color or `(r, g, b)`, white when unset |

Without `format`, JPEG and WebP tiles are returned as stored and others as PNG. With it, every tile
is decoded and encoded again, also JPEG and WebP tiles, so lossy tiles lose detail again.
`tile_pixels` and `read` return the decoded pixels either way.

## What it reads

COGs on the `WebMercatorQuad` grid, as `gdal_translate -of COG -co TILING_SCHEME=GoogleMapsCompatible`
writes them: EPSG:3857, every image (and overview) at a zoom level's resolution and aligned to its
tiles, 8-bit samples. Each tile is one internal tile, so nothing is resampled. Other files raise
`UnsupportedCogError` saying why.

Uncompressed striped files are split into 256 px tiles.
Pages that are half the size of the previous page are used as overviews.
In a mosaic, square files without GeoTIFF tags are placed on the tile in their file name,
e.g. `open_mosaic("s3://bucket/{quadkey}_rgb.tif", 16)`.

## Performance

* All I/O and decoding runs in Rust with the GIL released; `read` and `tiles` keep `concurrency`
  requests in flight (64 by default) and decode on every core.
* Wheels cover CPython 3.11 and later, including free-threaded CPython from 3.15t on.
* Headers are read once when opening: one request for a typical file, or one per IFD if IFDs come after
  the pixel data. Each tile is one range read. Tiles in the same row of a striped file share one read.
* `tile_pixels` and `read` hand their buffer to NumPy without copying.
* Ctrl-C interrupts any call. Cancelling an `_async` call or dropping a `tiles()` iterator stops its
  pending fetches.

## Details

* `bbox` is `(west, south, east, north)` in degrees, or `(min_x, min_y, max_x, max_y)` in meters
  with `crs="EPSG:3857"`. A raster covers the whole pixels the bbox touches; `raster.bounds` is
  the snapped extent. Pixels without data are 0, and a bbox without any data returns `None`.
* `read` refuses rasters over `max_pixels` (16384 x 16384 by default).
* `Cog`, `Mosaic`, `TileIndex` and `Stac` objects pickle by reopening, so they work in `multiprocessing` and PyTorch
  `DataLoader` workers. Forked processes get their own I/O threads.
* Object store credentials come from the usual environment variables (`AWS_*`, `GOOGLE_*`,
  `AZURE_*`); `options` overrides them with
  [`object_store` keys](https://docs.rs/object_store/latest/object_store/aws/enum.AmazonS3ConfigKey.html),
  e.g. `{"aws_skip_signature": "true"}` for public buckets.
* Errors: `FileNotFoundError`, `ValueError` for invalid arguments, `UnsupportedCogError` for files
  that cannot be read as tiles, `CogTilesError` for everything else.

## Development

```bash
cd cog-tiles-py
uv venv && uv pip install maturin pytest numpy
uv run maturin develop --release
uv run pytest
```

`cog_tiles.pyi` is generated from the bindings by `just sync-cog-py-stubs`; autofix.ci regenerates it on pull requests.

