Skip to content

Rust Library

[dependencies]
terrio = { git = "https://github.com/opengeos/terrio" }

The API reference, with an example on each entry point, is on docs.rs once terrio is published; until then, cargo doc -p terrio --open builds it. The crate documentation opens with a tour of the API.

Opening datasets

```rust,no_run use terrio::Dataset;

let ds = terrio::open("countries.gpkg")?; match &ds { Dataset::Raster(r) => println!("{} x {}", r.raster.cols, r.raster.rows), Dataset::Vector(v) => println!("{} layers", v.layers.len()), } println!("{}", terrio::info::info(&ds, &Default::default()).to_text());

Ok::<(), terrio::Error>(())

- `terrio::open(path)` detects the format and returns a raster or a vector dataset.
- `terrio::open_raster(path)` returns a `RasterDataset`: a `wbraster::Raster` (band-major typed
  pixels, georeferencing, CRS) with its driver and whether nodata is declared.
- `terrio::open_vector(path, &VectorOpenOptions)` returns a `VectorDataset` of `wbvector::Layer`s.
- `terrio::list_layers(path)` lists layers without reading features where the format allows.
- `terrio::open_raster_lazy(path)` opens a GeoTIFF or COG by its header; `read_window` decodes only
  the tiles a window touches, from full resolution or an overview.
- `terrio::memory` opens datasets from bytes and writes them to bytes.

`RasterDataset::band_nodata`, when nonempty, overrides the shared
`has_nodata` / `raster.nodata` fields, including explicit `None` entries. Read
nodata with `band_nodata(band)` (one-based) or `nodata()` (band 1). To replace it
with one shared value, clear the per-band vector and update both shared fields.

## Writing

```rust,no_run
let ds = terrio::open_raster("dem.tif")?;
let options = terrio::RasterWriteOptions {
    driver: Some(terrio::formats::RasterDriver::Cog),
    compression: Some("DEFLATE".into()),
    ..Default::default()
};
terrio::write_raster_dataset(&ds, "dem_cog.tif", &options)?;
# Ok::<(), terrio::Error>(())

RasterWriteOptions::overview_resampling takes GDAL's names, nearest, average, rms, bilinear, cubic, cubicspline, lanczos, mode and gauss, plus terrio's min and max for GeoTIFF overviews. GeoTIFF overviews default to nearest. COGs follow GDAL's COG driver: the OVERVIEW_RESAMPLING and RESAMPLING creation options come first, and the default is cubic (nearest for paletted and complex rasters). COG overviews have GDAL's sizes, down to one that fits in a tile, and GDAL's pixels: each level is computed from the previous one, as GDALRegenerateOverviewsMultiBand computes it, with nodata, the alpha band (ALPHA=YES) and per-dataset masks weighting the samples as in GDAL, and the masks given overviews of their own. Row-streamed COG output computes the same pixels, whatever the input row chunk size, without retaining the whole source image.

Native WebP TIFF output supports pixel-interleaved Byte RGB and RGBA. WEBP_LOSSLESS=YES preserves all samples, including transparent colors; WEBP_LEVEL=0..100 controls lossy quality (default 75). Lossy RGBA retains alpha losslessly, using an uncompressed ALPH chunk; files can be larger than libwebp's compressed-alpha output. Lossless RGBA preserves RGB under transparent pixels, which differs from libwebp/GDAL defaults that may discard hidden colors. Compressed TIFF/WebP bytes are not claimed identical across encoders. The same codecs and creation options are available in WebAssembly.

write_vector(&layers, path, &VectorWriteOptions) writes layers; GeoPackage holds several. ops::vector_filter::write_vector_with_mode appends to or overwrites the layers of an existing output; GeoPackages are updated keeping their other tables.

Streams

terrio::stream reads and writes vector layers a chunk of features at a time, for layers larger than memory: open_layers gives a FeatureReader per layer (an iterator of features, or chunks with read_chunk), FeatureWriter writes chunks, ArrowStream gives Arrow record batches (an arrow_array::RecordBatchReader), and convert combines them with the ogr2ogr-style options (ops::vector_filter::Translator applies them chunk by chunk), as terrio convert does.

```rust,no_run use terrio::stream::{ConvertOptions, open_layers};

let mut readers = open_layers("buildings.gpkg", &Default::default(), 65536)?; while let Some(chunk) = readers[0].read_chunk(10_000)? { println!("{} features", chunk.features.len()); } terrio::stream::convert("buildings.gpkg", "buildings.fgb", &ConvertOptions::default())?;

Ok::<(), terrio::Error>(())

Writers whose headers or column types depend on the values (CSV, Shapefile, FlatGeobuf,
GeoParquet) take a `LayerProfile` of all the features, which `convert` computes in a first pass
over the input, so that a streamed file is identical to the one `write_vector` writes.

## Arrow

`terrio::arrow` converts layers to and from Arrow record batches with a GeoArrow geometry column
(WKB or native encodings, the CRS as PROJJSON), and reads and writes Arrow IPC streams. The
`arrow-array` and `arrow-schema` versions are re-exported as `terrio::arrow::arrow_array` and
`terrio::arrow::arrow_schema`.

```rust,no_run
use terrio::arrow::{ArrowWriteOptions, GeometryEncoding, layer_from_arrow, layer_to_arrow};

let layer = terrio::open_vector("countries.gpkg", &Default::default())?.layers.remove(0);
let options = ArrowWriteOptions { geometry_encoding: GeometryEncoding::GeoArrow, ..Default::default() };
let table = layer_to_arrow(&layer, &options)?; // schema and record batches
let back = layer_from_arrow(&table.schema, &table.batches, "countries")?;
# Ok::<(), terrio::Error>(())

ndarray

With the ndarray feature (features = ["ndarray"], off by default), raster pixels are ndarray arrays, without copying them. A raster's buffer holds its bands one after the other, each row after row from the top: the standard layout of an array of shape (bands, rows, cols). The crate is re-exported as terrio::ndarray.

```rust,no_run use terrio::{RasterArrayExt, RasterDataset};

let mut dem = terrio::open_raster("dem.tif")?; let pixels = dem.raster.view3::()?; // an ArrayView3 of the raster's buffer for mut band in dem.raster.split_bands_mut::()? { band.mapv_inplace(|z| z * 3.28084); // with rayon: .into_par_iter() on the vector } let (array, header) = dem.into_array3::()?; // an owned Array3: the same buffer let dem = RasterDataset::from_array3(header, array)?; // and back, with the metadata

Ok::<(), terrio::Error>(())

- `RasterArrayExt` views a raster's pixels (`view3`, `view3_mut`) or a band of them (`band_view2`,
  `band_view2_mut`), borrowed or mutable, and splits the bands into mutable views that do not
  overlap (`split_bands_mut`), for threads.
- `RasterDataset::into_array3` (`into_array2` for one band) moves the pixel buffer into an owned
  array and returns the rest of the dataset, a header without pixels; `RasterDataset::from_array3`
  (`from_array2`) moves an array's buffer into a header.
- `LazyRaster::read_window` returns a dataset, which `into_array3` moves into an array, and
  `LazyRaster::read_mask_array2` reads a window's mask (255 for valid pixels, 0 for invalid ones)
  as an `Array2<u8>`.
- `LazyRaster::read_window_array3` and `RasterDataset::read_window_array3` read a window as an
  owned array of `f32` or `f64` values (`FloatElement`), NaN where pixels are invalid (see
  [Floating-point windows](#floating-point-windows)), bands first or last (`BandLayout`):
  `(bands, rows, cols)` or `(rows, cols, bands)`, written in that layout as the pixels are
  converted. `read_window_array2` reads one band as `(rows, cols)`. Bands first, `Float32` pixels
  read as `f32` (`Float64` as `f64`) are converted where they are read, without a second buffer.

```rust,no_run
use terrio::{BandLayout, FloatReadOptions, Window};

let mut image = terrio::open_raster_lazy("https://example.com/cog.tif")?;
let window = Window::new(0, 0, 256, 256);
let pixels = image.read_window_array3::<f32>(window, &FloatReadOptions::default(), BandLayout::BandsLast)?;
assert_eq!(pixels.dim(), (256, 256, image.band_count())); // rows, columns, bands
# Ok::<(), terrio::Error>(())

Apart from these floating-point reads, which convert pixels as they read them, nothing is copied, converted, reordered or reallocated, so what would need it fails instead, giving its input back exactly (ArrayError): the type asked for must be the pixels' (f32 for Float32) and the raster's declared data type, and an owned array must be in standard layout and own exactly its elements. An array refused is given back with the same buffer, first element and strides; its axes of length 1 must therefore have strides it can be rebuilt with (those of construction, slicing, insert_axis or permuted axes), others being refused before the array is taken apart. Nodata pixels keep the nodata value (no NaN is substituted), masks stay separate arrays, and complex datasets are their stored component bands: each complex band's real parts, then its imaginary parts. Bands are numbered from 0, as along the arrays' first axis. Reading and decoding still allocate: the guarantee covers the buffers once read.

Reading windows lazily

```rust,no_run let mut cog = terrio::open_raster_lazy("s3://bucket/image.tif")?; println!("{} x {}, overviews {:?}", cog.width(), cog.height(), cog.overviews()); let options = terrio::WindowOptions { bands: vec![1], overview: Some(0) }; let window = cog.read_window(terrio::Window::new(0, 0, 512, 512), &options)?; // a RasterDataset

Ok::<(), terrio::Error>(())

`terrio::validate_cog(path, strict)` checks a Cloud Optimized GeoTIFF, local or remote (read with
range requests, headers and chunk offsets only), with the checks and messages of rio-cogeo's
`cog_validate` (GDAL's `validate_cloud_optimized_geotiff.py`): tiled full resolution and overviews
above 512 pixels, overviews sorted, IFDs first (the main one within 300 bytes), image data from
the smallest overview to the full resolution, no `.ovr` sidecar, no
`KNOWN_INCOMPATIBLE_EDITION=YES`, and a warning for a large image without overviews. Masks are
also checked against GDAL's COG layout (`layout_notes`, which do not affect validity). `strict` makes warnings errors.

```rust,no_run
let v = terrio::validate_cog("https://example.com/dem.tif", false)?;
assert!(v.is_valid, "{:?}", v.errors);
# Ok::<(), terrio::Error>(())

Floating-point windows

LazyRaster::read_window_f32 and read_window_f64 read a window as f32 or f64 values, band after band ((bands, rows, cols)), NaN where pixels are invalid: the values of GDAL's ReadAsArray into a floating-point buffer, with the pixels its GetMaskBand masks set to NaN. FloatReadOptions selects the bands and the overview, as WindowOptions does, and with unscale applies each band's scale and offset (value * scale + offset, the band's properties or a netCDF variable's scale_factor and add_offset), which GDAL and rasterio leave to the caller. RasterDataset::read_window_f32 and read_window_f64 read a dataset in memory alike.

```rust,no_run use terrio::{FloatReadOptions, Window};

let mut sst = terrio::open_raster_lazy("sst.nc")?; let options = FloatReadOptions { bands: vec![1], unscale: true, ..Default::default() }; let celsius = sst.read_window_f32(Window::new(0, 0, 360, 180), &options)?; // NaN over land

Ok::<(), terrio::Error>(())

Pixels are invalid as GDAL's mask band decides, alpha bands aside (they are read as bands):

- where the per-dataset mask of the level read is 0 (`LazyRaster::read_mask`: an internal mask or
  a `.msk` sidecar; an overview's own mask, else the full-resolution mask sampled by nearest
  neighbour). Such a mask replaces the nodata values, as in GDAL and rasterio: a pixel holding the
  nodata value that the mask marks valid is a number.
- otherwise where a pixel is its band's nodata value (`RasterDataset::band_nodata`), compared in
  the band's own type before any conversion, as GDAL's `GDALNoDataMaskBand` compares it:
  `Float32` and `Float64` values within GDAL's relative tolerance (`ARE_REAL_EQUAL`, a `Float32`
  nodata value rounded to `f32`), NaN for a NaN nodata value, integers equal to the nodata value
  cast to their type (truncated: 1.5 is 1 for a `Byte` band). A value the type cannot hold
  (-9999 for `Byte`, 1e39 for `Float32`) masks nothing.

Pixels already NaN stay NaN and infinities are kept. Valid pixels are rounded once to the nearest
`f32` or `f64`, as GDAL converts them; `f32` holds integers exactly up to 2^24 and `f64` up to
2^53, beyond which `UInt32`, `Int32`, `UInt64` and `Int64` values are rounded, but the nodata
comparison happens before: an `Int32` -2147483646 stays valid next to a nodata value of
-2147483647, although both read as -2147483648 in `f32`. Unscaled values are computed in `f64`,
then rounded to `f32` once. Complex bands are refused.

## Bytes in memory

```rust,no_run
let bytes = std::fs::read("countries.geojson")?;
let ds = terrio::memory::open_vector_bytes(bytes, "countries.geojson", &Default::default())?;
let fgb: Vec<u8> = terrio::memory::write_vector_bytes(&ds.layers, "countries.fgb", &Default::default())?;
# Ok::<(), terrio::Error>(())

Operations

terrio::ops holds the operations behind the commands: translate, warp, mosaic and vrt for rasters; vector_filter (with the sql parser), rasterize and polygonize; dem, calc (with its expression parser and NumPy-typed evaluator), proximity and sieve. Each takes an options struct mirroring the command-line options.

ops::neighbors::{neighbors, neighbors_to_path} computes focal statistics (gdal raster neighbors) from NeighborsOptions (kernels, NeighborMethods, size, band, output type and NeighborsNodata); the _to_path form reads a LazyRaster and writes GeoTIFF/COG strips. ops::nearblack::{nearblack, nearblack_in_place} cleans collars (nearblack, gdal raster clean-collar) from NearblackOptions (colors, white, near, max_non_black, alpha, mask and CollarAlgorithm), returning a Collar (the Byte raster and its mask); keep_alpha_or_mask applies clean-collar's defaults. ops::enhance computes equalization_luts (or _from_path, with GDAL's stored histograms), reads and writes LUT files with parse_luts/format_luts, and applies BandLuts with enhance or enhance_to_path (gdalenhance).

ops::geometry::{centroids, singleparts, vertices} transforms vector layers while preserving their CRS and attributes. ops::footprint::footprint extracts an exact raster valid-data footprint with FootprintOptions (bands, intersection, optional mask, singlepart, and attributes: Vec<(String, String)> for text fields copied to every feature). The Rust operation does not infer a source path from an in-memory raster; supply a location attribute explicitly when needed. Field names must be nonempty, have no surrounding whitespace, and be unique ignoring case. ops::sample::sample accepts a point layer and a slice of SampleInput values, each naming a raster, a band and an output field. It transforms points into each raster's CRS for sampling, retains their original geometries, and records missing samples as nulls. sample_with_options additionally accepts SampleOptions { dst_srs, decimal_places } to reproject output geometries after sampling and round only newly sampled numeric fields. sample keeps its existing behavior. These operations are available in the core crate, including WebAssembly builds, without additional features.

ops::contour::contour accepts a raster and ContourOptions (interval/fixed levels, band, nodata override and optional Z coordinates), returning a line layer with IDs and elevations. ops::zonal::zonal_stats accepts a polygon layer, ZonalInput raster bands and ZonalOptions (statistics, pixel inclusion, optional dst_srs and decimal_places); the result retains the source features and adds statistic fields. Output reprojection occurs after analysis, and rounding (0–15 decimal places, ties away from zero after binary64 scaling) leaves source attributes and counts intact. Zonal statistics reuse a cropped membership mask across bands of the same raster. ops::sql::query runs the documented OGR SQL subset, including computed columns, DISTINCT, summaries and attribute joins, with strict input-layer lookup. These operations are also available in the core crate without extra features.

ops::topology provides buffer (BufferOptions), overlay (OverlayOperation), dissolve (group field names), validity, make_valid and clean (CleanOptions). These pure-Rust operations are included in WebAssembly builds without extra features. Distances and snap grids use planar source CRS units, including degrees. Overlay and dissolve accept polygons only; overlay requires matching CRSs. Buffer/overlay/dissolve construct XY geometry; validity and repair of already-valid features preserve Z/M. See Vector topology for null, attribute, ID and collapsed-part handling.

```rust,no_run use terrio::ops::topology::{self, BufferOptions, CleanOptions, OverlayOperation};

let ds = terrio::open_vector("parcels_utm.gpkg", &Default::default())?; let layer = &ds.layers[0]; let repaired = topology::make_valid(layer)?; let buffers = topology::buffer(&repaired, &BufferOptions { distance: 100.0, ..Default::default() })?; let dissolved = topology::dissolve(&buffers, &["zone".into()])?; let overlaps = topology::overlay(&repaired, &dissolved, OverlayOperation::Intersection)?; let report = topology::validity(&overlaps); let cleaned = topology::clean(&repaired, &CleanOptions { snap_grid: 0.01 })?;

Ok::<(), terrio::Error>(())

The commands are also available through the Python package's `terrio` CLI and MCP. Dedicated
Python and JavaScript operation bindings are not yet provided for these additions.

`ops::vector_geom` holds the equivalents of GDAL 3.13's `gdal vector simplify`, `segmentize`,
`convex-hull`, `concave-hull`, `swap-xy`, `set-geom-type`, `combine` and `sort` (see
[Vector geometry commands](cli.md#vector-geometry-commands)). The per-feature operations are
`GeometryOp` values (`Simplify`, `Segmentize`, `ConvexHull`, `ConcaveHull(ConcaveHullOptions)`,
`SwapXy`, `SetGeomType(SetGeomTypeOptions)`), applied to a layer with `vector_geom::apply`, or
to layers read and written as streams through `VectorTranslateOptions::geometry_op` (which is
also `ogr2ogr`'s `-simplify`/`-segmentize`). `vector_geom::combine` (`CombineOptions`) and
`vector_geom::sort` (`SortMethod`) take whole layers; `vector_geom::force_to` is OGR's `forceTo`.
The single-geometry algorithms are `ops::simplify::{simplify_preserve_topology, segmentize}`
and `ops::hull::{convex_hull, concave_hull, concave_hull_of_polygons}`. They are pure Rust and
included in WebAssembly builds.

```rust,no_run
use terrio::ops::vector_geom::{self, CombineOptions, ConcaveHullOptions, GeometryOp, SortMethod};

let ds = terrio::open_vector("places.gpkg", &Default::default())?;
let groups = vector_geom::combine(&ds.layers[0], &CombineOptions { group_by: vec!["adm0_a3".into()], ..Default::default() })?;
let hulls = vector_geom::apply(&groups, &GeometryOp::ConcaveHull(ConcaveHullOptions { ratio: 0.3, ..Default::default() }))?;
let simple = vector_geom::apply(&hulls, &GeometryOp::Simplify { tolerance: 0.01 })?;
terrio::write_vector(&[vector_geom::sort(&simple, SortMethod::Hilbert)], "hulls.gpkg", &Default::default())?;
# Ok::<(), terrio::Error>(())

```rust,no_run use terrio::ops::{Resampling, translate, warp};

let dem = terrio::open_raster("dem.tif")?; let o = warp::WarpOptions { dst_srs: Some(terrio::crs::SpatialRef::parse("EPSG:32613")?), resampling: Resampling::Bilinear, ..Default::default() }; let utm = warp::warp(&[&dem], &o)?; let o = translate::TranslateOptions { src_win: Some((0.0, 0.0, 512.0, 512.0)), ..Default::default() }; let corner = translate::translate(&utm, &o)?;

Ok::<(), terrio::Error>(())

### Pansharpening

```rust
use terrio::ops::pansharpen::{pansharpen, PansharpenOptions, SpectralBand};
let pan = terrio::open_raster("pan.tif")?;
let rgb = terrio::open_raster("rgb.tif")?;
let spectral: Vec<_> = (1..=3).map(|band| SpectralBand { dataset: &rgb, band }).collect();
let sharp = pansharpen(&pan, &spectral, &PansharpenOptions::default())?;
use terrio::ops::pansharpen::vrt::PansharpenVrt;
let description = PansharpenVrt::from_paths("pan.tif", &[("rgb.tif".into(), 1)], &Default::default())?;
description.write("sharp.vrt")?; // source headers only
let virtual_raster = terrio::open_raster_lazy("sharp.vrt")?;
# Ok::<(), terrio::Error>(())

Each SpectralBand can reference a separate raster. See pansharpen for working types, interpolation, nodata, bit depth and extent handling.

Viewshed

use terrio::ops::viewshed::{viewshed, ViewshedOptions};
let dem = terrio::open_raster("dem_utm.tif")?;
let visible = viewshed(&dem, &ViewshedOptions {
    observer_x: 500000.0, observer_y: 4400000.0, max_distance: 5000.0,
    ..Default::default()
})?;
# Ok::<(), terrio::Error>(())

ViewshedMode::Dem and Ground return minimum visible elevation or height above the DEM. See viewshed for units, curvature, radius cropping and nodata behavior.

Vector gridding and linear referencing

use terrio::ops::vector_grid::{vector_grid, GridAlgorithm, VectorGridOptions};
use terrio::ops::lineref::{create_reference, position, LineRefOptions};
let points = terrio::open_vector("stations.gpkg", &Default::default())?;
let grid = vector_grid(&points.layers, &VectorGridOptions {
    algorithm: GridAlgorithm::parse("invdist:power=2:radius=50000:max_points=12")?,
    z_field: Some("temp".into()),
    size: Some((200, 200)),
    ..Default::default()
})?;
let route = terrio::open_vector("path.shp", &Default::default())?;
let stones = terrio::open_vector("mstones.shp", &Default::default())?;
let parts = create_reference(&route.layers[0], &stones.layers[0], &LineRefOptions {
    position_field: "pos".into(), step: 1000.0, ..Default::default()
})?;
let pos = position(&parts, -1.4345, 51.9497)?;
# Ok::<(), terrio::Error>(())

grid_points grids coordinate arrays directly; coordinates and subline are the other lookups. See vector-grid and lineref.

Compare, pixel info and XYZ

```rust,no_run use terrio::ops::compare::{compare, CompareOptions}; use terrio::ops::pixel_info::{pixel_info, PixelInfoOptions, Position, PositionCrs}; use terrio::ops::xyz::{write_xyz, XyzOptions};

let report = compare("reference.tif", "output.tif", &CompareOptions { skip_binary: true, ..Default::default() })?; print!("{}", report.to_text()); // gdal raster compare's lines println!("{} difference(s)", report.difference_count());

let mut dem = terrio::open_raster_lazy("dem.tif")?; let options = PixelInfoOptions { position_crs: Some(PositionCrs::parse("EPSG:4326")?), ..Default::default() }; let info = pixel_info(&mut dem, &[Position::new(-105.5, 39.5)], &options)?; print!("{}", info.to_text()); // gdallocationinfo's report write_xyz(&mut dem, &mut std::io::stdout(), &XyzOptions::default())?;

Ok::<(), terrio::Error>(())

`compare_rasters` compares two open `LazyRaster`s (without the binary, RPC and subdataset
comparisons), `pixel_info_layer` queries at a layer's points, and `PixelInfo` prints GeoJSON,
CSV, text and `-valonly` output or builds a point layer. See [compare](cli.md#compare),
[pixel-info](cli.md#pixel-info) and [xyz](cli.md#xyz).

### Vector datasets

```rust,no_run
use terrio::ops::concat::{concat, ConcatMode, ConcatOptions, ConcatSource};
use terrio::ops::partition::{partition_to_dir, PartitionOptions, PartitionWriteMode};
use terrio::ops::vector_schema::{export_schema_path, set_field_type, FieldSelector, OgrType};

let ds = terrio::open_vector("countries.gpkg", &Default::default())?;
let sources = vec![ConcatSource { name: "countries.gpkg".into(), layers: ds.layers.clone() }];
let merged = concat(&sources, &ConcatOptions { mode: ConcatMode::Single, ..Default::default() })?;
let typed = set_field_type(&merged[0], &FieldSelector::Name("POP_EST".into()), OgrType::parse("Integer64")?)?;
let options = PartitionOptions { fields: vec!["CONTINENT".into()], ..Default::default() };
let write = terrio::VectorWriteOptions { driver: Some(terrio::formats::VectorDriver::GPKG), ..Default::default() };
partition_to_dir(&[typed], "out", &options, &write, PartitionWriteMode::Create)?;
println!("{}", export_schema_path("countries.gpkg", &[])?.to_json()); // OGR_SCHEMA
# Ok::<(), terrio::Error>(())

ops::concat, ops::partition (partition plans the parts in memory, partition_to_dir writes them), ops::vector_schema (set_field_type, export_schema, export_schema_path, layer_from_schema, create, parse_field_spec, parse_geometry_type, OgrSchema) and ops::make_point are terrio concat, partition, set-field-type, export-schema, create and make-point; see Vector datasets.

Pixel-wise operations

use terrio::ops::as_features::{as_features, AsFeaturesOptions, PixelGeometry};
use terrio::ops::pixel::{nodata_to_alpha, reclassify, unscale, ReclassifyOptions};
let packed = terrio::open_raster("packed.tif")?;
let physical = unscale(&packed, &Default::default())?; // value * scale + offset, Float32
let rgba = nodata_to_alpha(&terrio::open_raster("landsat.tif")?, &Default::default())?;
let nlcd = terrio::open_raster("nlcd.tif")?;
let groups = reclassify(&nlcd, &ReclassifyOptions { mapping: "11=1;[21,25)=2;DEFAULT=0".into(), output_type: None })?;
let pixels = as_features(&nlcd, &AsFeaturesOptions { geometry: PixelGeometry::Polygon, ..Default::default() })?;
# Ok::<(), terrio::Error>(())

terrio::ops::pixel::Reclassifier parses and applies GDAL's mapping syntax on its own, and mapping_from_file_text reads mapping files. See unscale, nodata-to-alpha, reclassify and as-features.

File management

```rust,no_run use terrio::vsi::{self, CopyOptions, ListOptions, SyncOptions, SyncStrategy};

for e in vsi::list("s3://bucket/prefix", &ListOptions { recursive: true, ..Default::default() })? { println!("{} {:?} {:?}", e.name, e.kind, e.size); } vsi::copy("data", "s3://bucket/prefix", &CopyOptions { recursive: true, ..Default::default() })?; let report = vsi::sync("data/", "s3://bucket/prefix", &SyncOptions { recursive: true, strategy: SyncStrategy::ETag })?; println!("{} copied, {} up to date", report.copied, report.unchanged); vsi::rename("s3://bucket/old", "s3://bucket/new")?; vsi::delete("s3://bucket/prefix", true)?;

Ok::<(), terrio::Error>(())

`terrio::vsi` lists, copies, deletes, moves and synchronizes files across local, `/vsimem/`,
S3, Google Cloud Storage, Azure, HTTP and zip/tar locations (see [vsi](cli.md#vsi)).
`vsi::RemoteLister` plans a remote listing without doing I/O, for hosts that send the requests
themselves (WebAssembly's `fetch`).

### Seek-optimized ZIP archives

```rust,no_run
use terrio::vsi::sozip::{self, SozipOptions};
let options = SozipOptions { junk_paths: true, ..Default::default() };
sozip::create(&["roads.gpkg", "dem.tif"], "data.zip", &options)?;
let members = sozip::list("data.zip")?;
let validation = sozip::validate("data.zip")?;
sozip::optimize("plain.zip", "optimized.zip", &SozipOptions::default())?;
# Ok::<(), terrio::Error>(())

Native outputs use atomic replacement on success. Existing archives are appended to by create; optimize requires overwrite to replace an output. See the supported options and limitations.

Coordinate systems

```rust,no_run use terrio::crs::{SpatialRef, Transformer};

let src = SpatialRef::parse("EPSG:4326")?; let dst = SpatialRef::parse("EPSG:32613")?; let (x, y) = Transformer::new(&src, &dst)?.transform(-105.5, 39.5)?;

Ok::<(), terrio::Error>(())

## Point clouds

`terrio::pointcloud` reads LAS, LAZ and COPC into columns, writes them and grids them into
rasters: see [Point Clouds](point-clouds.md).

## Features

Every driver is on by default. A program that needs only part of terrio turns the default
features off and names the ones it uses, for a smaller dependency tree, faster builds and a
smaller WebAssembly module. A COG writer, say:

```toml
terrio = { version = "0.3", default-features = false, features = ["geotiff"] }

The core, always built, is GeoTIFF and COG (deflate, LZW, PackBits, ZSTD, WebP, JPEG and JPEG XL), the raw and ASCII rasters (AAIGrid, EHdr, ENVI, ERS, HFA, RST, SAGA, GSBG, XYZ...), PNG, JPEG, VRT, STAC, GeoJSON, TopoJSON, Shapefile, KML/KMZ, GML, GPX, CSV, MapInfo, the CRS engine, the operations and the streams. The other drivers are features:

Feature Default What it adds
geotiff on GeoTIFF and COG: always built, the feature exists so that features = ["geotiff"] says what a build needs.
parquet on GeoParquet, the arrow module and Arrow streams of vector layers (arrow, parquet).
remote on URLs, s3://, gs:// and az:// objects, range reads of remote COGs, COPC, PMTiles and Zarr, and datum grids from the PROJ CDN (native builds only).
netcdf-hdf5 on NetCDF, HDF5 and HDF-EOS reading, netCDF writing.
sqlite on GeoPackage (vector and raster tiles) and SQLite/SpatiaLite.
tiles on MBTiles, PMTiles and MVT, raster and vector (the tiles module's archives; needs sqlite).
avif on AVIF tiles of tile archives (needs tiles).
zarr on Zarr v2 and v3.
jp2 on JPEG 2000.
grib on GRIB1 and GRIB2 reading and GRIB2 writing (pure Rust, CCSDS packing with rust-aec; JPEG 2000 packing needs jp2).
flatgeobuf on FlatGeobuf.
osm on OpenStreetMap PBF and XML.
pointcloud on LAS, LAZ, COPC, EPT, PLY and E57 (the pointcloud module).
lerc on LERC-compressed GeoTIFFs, read and written.
lzma on LZMA-compressed GeoTIFFs.
hecras on HEC-RAS geometry and plan HDF files as vector layers, read only (the HECRAS driver and the hecras module, through the ras-commander-fast-hdf engine; needs netcdf-hdf5). See HEC-RAS.
gdal off The optional GDAL backend: formats terrio has no driver for, through GDAL (needs GDAL installed). See the GDAL backend.
ndarray off Raster pixels as ndarray arrays, without copying them: views, owned transfers and the masks of lazy reads, with the crate re-exported as terrio::ndarray; floating-point windows as owned arrays, bands first or last. See ndarray.

The drivers stay in the API whatever the features (formats::RasterDriver, formats::VectorDriver): those a build leaves out are listed as neither read nor written, and opening or writing their files fails with "built without the zarr feature". Modules and types that belong to one feature (terrio::arrow, terrio::pointcloud, terrio::http, terrio::s3, tiles::TileArchive, terrio::ndarray, RasterArrayExt, BandLayout) exist only with it.

The geotiff build still pulls the JPEG XL decoder and encoder (jxl-oxide, zune-jpegxl): wbraster, terrio's raster data model, depends on them through its GeoTIFF reader.

API stability

terrio is heading for a stable 1.0 API. What to rely on, and the decisions behind it:

  • The data model is wbraster's and wbvector's. Rasters are wbraster::Rasters and vector layers wbvector::Layers, re-exported by terrio (terrio::Raster, terrio::Layer, terrio::raster::RasterData, terrio::vector::Coord...). Wrapping them in terrio types would copy every pixel and coordinate through the bindings and the operations for no gain, so terrio exposes them as they are: they are part of terrio's API, and a breaking release of either crate that terrio adopts is a breaking release of terrio. Use them through terrio's re-exports rather than depending on those crates yourself, so that the versions always match.
  • Options structs (RasterWriteOptions, WarpOptions, PointReadOptions...) all implement Default: build them with ..Default::default(). New options are added as new fields with a default that keeps the old behaviour, which is not considered a breaking change.
  • Enums that grow (Error, formats::RasterDriver, formats::VectorDriver, formats::Kind) are #[non_exhaustive]: match them with a wildcard arm.
  • Errors carry a message for display; Error::Raster and Error::Vector wrap the format libraries' errors.
  • Minimum supported Rust version: 1.90, raised only when needed, never to a release less than six months old (see Contributing).

WebAssembly

The library builds for wasm32-unknown-unknown, with any features; network access and temporary files are native only. No C compiler is needed, except for the parquet feature, whose Zstandard codec is the zstd C library (it needs clang for WebAssembly); Zstandard GeoTIFFs are decoded in pure Rust there. JPEG 2000 (the jp2 feature) works there too, with two settings in the program that is built: the global allocator terrio::NullSafeAllocator (the OpenJPEG port releases null pointers, which the WebAssembly allocator does not tolerate), and the link flag -C link-arg=--allow-undefined for wasm32-unknown-unknown, as in the repository's .cargo/config.toml (the port is built as a C library too, whose C allocator calls terrio defines):

#[global_allocator]
static ALLOCATOR: terrio::NullSafeAllocator = terrio::NullSafeAllocator;

API documentation: docs.rs/terrio, or cargo doc -p terrio --open in the repository.

Native multidimensional arrays

See Native multidimensional arrays for scientific groups, named dimensions, lazy coordinate arrays, time/depth slicing, transposition and netCDF/Zarr numeric snapshots.

Indexed color conversion

terrio::ops::palette exposes rgb_to_palette with PaletteOptions, palette_to_rgb, attach_palette and their lazy-source variants. The lazy variants retain dataset validity masks, while the conversions materialize the pixels. parse_color_table, color_table_from_bytes and native read_color_table accept indexed text/QGIS tables or raster palettes. See Palette conversion for channel selection, quantization settings, supported formats and GDAL differences.

GeoPackage validation

```rust,no_run use terrio::{GpkgValidationOptions, validate_gpkg, validate_gpkg_bytes}; let options = GpkgValidationOptions { full_check: true, ..Default::default() }; let report = validate_gpkg("data.gpkg", &options)?; let from_bytes = validate_gpkg_bytes(&std::fs::read("data.gpkg")?, &options)?; assert!(report.is_complete && from_bytes.is_complete);

Ok::<(), terrio::Error>(())

```

GpkgValidation exposes validity, completeness, version, requirement-numbered errors, warnings and unsupported checks. It serializes validity/completeness as valid/complete. The sqlite feature is required. See the supported checks and limitations.