async-geotiff
中文文档 | English | Documentation | API reference
Pure-Rust async crate that reads cloud-hosted Cloud-Optimized GeoTIFFs (COGs) and serves them as XYZ map tiles with Raster Style 3.0 inputs.
No GDAL. All raster IO goes through async-tiff + object_store.
Reprojection through proj (libproj system dep, feature-gated). Affine math
through raffine (the author's own Rust port of Python's affine).
Colormaps via colorous. Resampling via
fast_image_resize. PNG encoding via image.
Quick start
Run these example-server commands from a Git checkout.
# Or omit the URL to browse local data/*.tif files with the dataset picker.
# then open one of:
# http://127.0.0.1:8080/maplibre
# http://127.0.0.1:8080/openlayers
# http://127.0.0.1:8080/leaflet
The example server binds to 127.0.0.1:8080 by default. To expose it on a
network interface, set ASYNC_GEOTIFF_BIND, for example:
ASYNC_GEOTIFF_BIND=0.0.0.0:8080
Raster Style 3.0 inputs
Uses raster-style = "=0.4.0". Rust entrypoints are TileStyle::from_json,
from_raster_query, and from_raster_style; all three viewers default to Q3.
New inputs declare version=3.0&type=...; omitted stretch uses fixed [0,255].
HTTP accepts complete Q3 documents only. Unsupported protocol capabilities fail explicitly.
See the migration guide and supported subset.
Band math expressions
renderer.expression uses one-based bN band references, parentheses,
arithmetic operators, and unary minus. Gray/pseudocolor accept one expression;
RGB accepts three semicolon-separated expressions. Expressions and bidx are
mutually exclusive. Referenced NoData and non-finite results become transparent.
Library usage
[]
= "0.13.0"
Default features enable mosaics and general reprojection through system
libproj. For a build without native PROJ, use
async-geotiff = { version = "0.13.0", default-features = false, features = ["mosaic"] }.
See the library guide
for a complete application, point queries and source consistency.
HTTP serving lives only in examples/serve.rs; the library itself does not
require axum. Downstream applications can use CogReader, Tiler, or
MosaicTiler directly to read COGs and produce tiles. When reading multiple
sources, reuse one Arc<BlockCache> and give each reader a distinct
source_id so all sources share one decoded-block memory budget.
Optional STAC demo
With the existing stac feature enabled, startup environment variables can
connect /mosaic/tiles/... to a fixed STAC API, collection, and COG asset key:
ASYNC_GEOTIFF_STAC_URL=https://earth-search.aws.element84.com/v1 \
ASYNC_GEOTIFF_STAC_COLLECTION=sentinel-2-l2a \
ASYNC_GEOTIFF_STAC_ASSET_KEY=visual \
All three variables are required and are mutually exclusive with
ASYNC_GEOTIFF_MOSAIC. They are read only at process startup; HTTP query
parameters cannot choose the STAC endpoint, collection, or asset key. STAC
assets still use the default HTTP access policy, which rejects loopback and
link-local destinations so the demo cannot become a client-controlled public
proxy.
STAC discovery follows same-origin GET/POST pagination in server order and
rejects incomplete results when limits are exceeded: 1 MiB per response,
8 MiB total responses, 128 pages and 8 MiB of asset metadata. The asset count
uses MosaicConfig::max_assets_per_tile; setting it to zero disables only
that count limit. Direct source calls can pass DiscoveryOptions to
assets_for_tile_with_options. Custom MosaicSource implementations should
override this method and statistics_assets_with_options to apply limits
before allocating; their compatibility defaults validate returned assets.
StacSource::new keeps its existing signature and disables automatic proxy
discovery. For authentication, explicit proxies or custom HTTP timeouts, use
StacSource::with_http_client_builder(api_url, builder, collections, asset_key)?,
passing a configured reqwest 0.11 ClientBuilder before calling build().
The source preserves those settings and replaces the redirect policy: at most
ten same-origin hops; cross-origin redirects are rejected before credentials
are sent. Transport errors omit URL credentials and query strings.
The old with_http_client constructor now returns an error because a built
client's redirect policy cannot be overridden. with_client(stac_client::Client, ...) remains deprecated and fails at discovery. Carry authentication into the
builder's default headers when migrating.
Styles and migration
All styles use Raster Style 3.0 JSON/Q3:
let style = from_json?;
let bytes = tiler.tile.await?;
For mosaics, declare the Mosaic 1.0.0 schema in extensions and add raster:mosaic to the same document and call mosaic.tile(coord, &style).
Five selection methods, both merge stages, and rank channels are supported.
MosaicJSON/STAC supplies assets separately.
This is a breaking migration: from_query, from_pairs, mutable style fields,
the old Stretch, ColourDefinition, Colour, and lut_from_stops APIs,
and the separate mosaic selection argument are removed. The default style is gray band 1 with fixed 0–255 mapping;
provide a linear range for high-bit-depth data. Styles support PNG and lossless
WebP (with the webp feature). Unknown fields, old parameters, and unimplemented
capabilities return errors.
See the styling guide for fields, migration, and capabilities, and tile API for request paths and encoding.
Supported TileMatrixSets
WebMercatorQuad(EPSG:3857) — default for slippy XYZ clientsWorldCRS84Quad(EPSG:4326)
TMS supported pixel types
Source COG can be U8 / U16 / I16 / U32 / I32 / F32 / F64. Internally unified to F64 at the read/warp seam.
Build requirements
- Rust ≥ 1.92, matching
Cargo.toml'srust-version. - For the default
projfeature:libproj9.x on the system (brew install projon macOS;apt install libproj-dev libsqlite3-dev libtiff-dev clangon Debian/Ubuntu). Build without it viacargo build --no-default-features.
Architecture
Example HTTP request (axum, `examples/serve.rs`)
├─ TMS.tile_bbox(z, x, y) tile-grid
├─ CogReader.read_window async-tiff + object_store
│ └─ decoded native blocks from a shared BlockCache (one byte budget);
│ misses fetched coalesced + decoded in semaphore-limited spawn_blocking
├─ warp::warp (if src_crs ≠ dst_crs) proj coarse-grid + bilinear
│ └─ runs in semaphore-limited spawn_blocking (Proj is !Send + !Sync)
├─ render pipeline: ndarray + raster-style
│ ├─ rescale per band
│ ├─ Gamma / opacity
│ └─ Raster Style gray / RGB / native colormaps
└─ encode PNG / lossless WebP image (+ image-webp)
Out-of-range tiles return Ok(None) (the example server replies HTTP 204);
Lossless WebP output requires the webp cargo feature.
Module layout:
src/
error.rs AgError + AgResult
bounds.rs Bbox + GeoTransform (wraps raffine::Affine)
crs.rs Crs identifier
pixel.rs PixelData + ImageData
tms.rs WebMercatorQuad + WorldCRS84Quad
io/
cog.rs CogReader open/metadata/block-fetch orchestration
block_cache.rs shared decoded-block cache (BlockKey/BlockCache, one budget)
cache.rs CacheConfig (block-cache byte budget + optional TTL)
decode.rs semaphore-limited single-block decode spawn_blocking helper
window.rs source tile-range and stitch helpers
warp/ Projector + Resampling + coarse-grid warp
render/ colormap, expression, encode, pipeline
style/ Raster Style JSON/Q3 validation and render plans
tiler.rs orchestrator
examples/
serve.rs demo entrypoint and viewer wiring
serve/server.rs axum routes for the demo server
maplibre.html MapLibre viewer
openlayers.html OpenLayers viewer
leaflet.html Leaflet viewer
The library crate does not expose an axum server module; HTTP serving lives in the example so downstream library users do not inherit web-framework dependencies.
CPU-heavy decode/stitch and warp/render work is dispatched with
spawn_blocking behind async Semaphore limits. Use
CogReader::builder() (e.g. .max_decode_tasks(n), .block_cache(cache),
then .open_http(url) / .open_object_store(store, key) / .open(reader))
to tune decode concurrency, and Tiler::with_config with
async_geotiff::tiler::TilerConfig to tune warp/render concurrency. Defaults
use the host's available parallelism and clamp zero to one permit.
Decoded native tile blocks are cached in a shared Arc<BlockCache> under one
byte budget (default 512 MB). ReaderConfig carries a caller-supplied
source_id (the cache key prefix) and the block_cache; pass the same
block_cache with distinct source_ids to several readers (e.g. every
dataset in a server, or every asset in a mosaic) to bound their combined decode
memory to a single budget instead of one cache per reader. MosaicTiler does
this automatically via its ReaderPool.
Performance notes
See production operation for cache sizing and the local workload guide for reproducible measurements. Historical machine-specific benchmark reports are retained in Git history rather than shipped with the source release.
- Cache capacity is the strongest lever. 512 MB holds a typical browsing viewport for large RGB COGs; use 1024 MB for multi-dataset servers or wide pan/zoom sessions.
- Compare ZSTD and DEFLATE with your imagery. Compression, tile size and storage latency affect decode cost; measure representative requests before choosing a COG creation profile.
MAX_TILE_TASKSshould stay at its default (available_parallelism()). Pinning it lower inflates warp/encode P95 at high concurrency via scheduling queuing. Only override after profiling confirms CPU contention at the default level.- Concurrent in-flight single-flight. When multiple requests race for the same uncached block, only the first fetches and decodes it; the rest wait on the result channel. If the owner is canceled (client disconnect), waiters transparently retry with a fresh claim rather than surfacing a 500 error.
Observability
The library emits tracing only — it takes no
OpenTelemetry (or other metrics-backend) dependency, so your application owns
the export pipeline. Two integration points:
Spans → traces. Tiler::tile and CogReader::read_window are
#[tracing::instrument]-ed (fields: source id, tile coords, level, format).
Bridge them to OpenTelemetry with a tracing-opentelemetry layer:
use *;
registry
.with
.with // your OTLP tracer
.init;
Cache metrics → gauges/counters. You construct the shared Arc<BlockCache>,
so poll BlockCache::stats() periodically and feed it to your metrics backend:
let s = block_cache.stats; // BlockCacheStats { hits, misses, decoded, fetched, entries, resident_bytes }
// hit ratio = hits / (hits + misses)
// fetch amplification = fetched / decoded (1.0 = no duplicate cold fetches)
// memory pressure = resident_bytes / configured budget
Boundary metrics you can't read from the library (request rate, latency
percentiles, the HTTP-204 "no tile" rate) are best recorded where you call
Tiler::tile / MosaicTiler::tile — match on Ok(Some) / Ok(None) / Err
and record OTel histograms/counters there.
License
MIT; see LICENSE-MIT.