Roadmap
terrio reads, writes, converts and reprojects the geospatial formats most workflows use (rasters, vector data and point clouds), in pure Rust, so that it builds anywhere Rust does, WebAssembly included. It does not try to replace all of GDAL: the long tail of formats stays reachable through an optional GDAL backend.
Principles
- Pure Rust by default. Every default feature builds for
wasm32-unknown-unknown; anything that needs C sits behind an opt-in feature. - Reuse the ecosystem. The format codecs and projections come from whitebox_next_gen's pure-Rust
crates (
wbraster,wbvector,wbprojection), plustifffor GeoTIFF decoding. Where a codec is wrong or missing, terrio supplies its own driver and reports the problem upstream. - One data model across formats, so a driver is a small amount of code and conversions are free.
- Read cloud data in place: range requests, lazy tiles and chunks, no full download.
- Correctness is measured against GDAL. Every driver is tested on real-world files and its output compared with GDAL's.
Non-goals
- Matching GDAL's C API or its option names one to one.
- Proprietary formats that need vendor SDKs (MrSID, ECW, FileGDB raster).
- Database drivers (PostGIS, Oracle, SQL Server) in the core crate.
0.1: Foundations
- Data model:
Dataset, rasterBandwith block access, vectorLayerwithFeature, field schemas and geometry (ongeo-types), and a driver registry with format detection. - Virtual file system: local files, HTTP(S) with range requests, S3 (with SigV4 signing), Google Cloud Storage and Azure; zip archives.
- Coordinate reference systems: EPSG codes, WKT and PROJ strings in and out; transformations that match GDAL's (units, axis orders, prime meridians and datum transformations) for every EPSG geographic and projected CRS. Done, and since: ESRI codes under the ESRI authority, WKT 1 and WKT 2 read by terrio's own reader and identified by their datum, rotated-pole and geostationary grids, PROJ's world projections, datum transformations between CRSs chosen as PROJ lists them, and every PROJ projection checked against PROJ as a PROJ string and as WKT.
- GeoJSON and GeoTIFF read and write, so the model is proven on one vector and one raster format.
-
terrio infofor any supported dataset.
0.2: Core formats
- Raster: Cloud Optimized GeoTIFF (overviews, internal tiling, all common compressions), ASCII grids.
- Vector: Shapefile (with
.prj,.cpgand zipped), FlatGeobuf (with its spatial index), GeoParquet 1.1 (with bounding-box filtering), CSV with point or WKT columns. - Attribute and spatial filters pushed down to the drivers that support them.
-
terrio convertbetween any two supported formats.
0.3: Processing
- Warp: reprojection and resampling (nearest, bilinear, cubic, average, mode), with output extent and resolution control.
- Translate: subset by window or bounds, change data type, scale, add overviews, write COGs.
- Rasterize vectors and polygonize rasters.
- Mosaics: a virtual dataset (an equivalent of GDAL's VRT) over many rasters.
-
terrio warp,terrio translate,terrio rasterize,terrio polygonize. - Terrain analysis and raster algebra, matching GDAL pixel for pixel:
terrio dem(gdaldem: hillshade, slope, aspect, color relief, TRI, TPI, roughness),terrio calc(gdal_calc.py, with NumPy's typing rules),terrio proximity(gdal_proximity.py) andterrio sieve(gdal_sieve.py).
0.4: More formats
- GeoPackage, vector and raster tiles. Done: terrio reads and writes the SQLite files itself
(feature and attribute tables, R-tree spatial indexes, tile pyramids with PNG or JPEG tiles,
gridded coverages for Int16, UInt16 and Float32 rasters, files holding both), from files or
bytes; spatial filters through the R-tree; existing files updated (features appended, layers
replaced or added, other tables kept), checked against
ogr2ograndvalidate_gpkg.py; files updated in place as SQLite updates them (b-tree page inserts and splits, R*-tree inserts and node splits, freed pages on the freelist and reused, only the changed pages written, in a transaction protected by a rollback journal SQLite plays back after a crash, under SQLite's locks), checked withPRAGMA integrity_check,rtreecheck(),validate_gpkg.pyand terrio killed mid-commit; appending 100,000 points to a 2-million-point file takes 2.7 s (ogr2ogr -append: 6.7 s). - Zarr 2 and 3, and GeoZarr conventions. Done: GeoZarr's
spatial:,proj:andmultiscalesconventions read (multiscales levels as overviews, read by window) and written (GEOREFERENCING_CONVENTION=SPATIAL_PROJ, overviews laid out as GDAL lays them out), and CF grid mappings written as rioxarray writes them; remote stores (HTTP(S), S3, Google Cloud Storage, Azure, and GDAL's/vsicurl/-style names) read in place, Zarr 2 and 3 with consolidated metadata, the v3 sharding codec (shard indexes, then only the inner chunks needed), float16 and thescale_offset/cast_valuecodecs, windows fetching only the chunks they touch in parallel, GeoZarr multiscales as overviews; identical to GDAL's Zarr driver and xarray on EOPF Sentinel-2 (v2 and sharded GeoZarr v3), NOAA HRRR on S3, CMIP6 on GCS and Daymet on Azure (scripts/compare_remote_zarr.py).- Listing remote groups without consolidated metadata.
- Arrays of more than three dimensions.
- KML/KMZ, GPX, GML, TopoJSON.
- PMTiles and MBTiles archives. Done: raster tiles (PNG, JPEG, WebP) read as rasters with their
zoom levels as overviews, and vector tiles (MVT) as layers, as GDAL's MBTiles, PMTiles and MVT
drivers read them; PMTiles read in place over HTTP; rasters written as tile pyramids and vector
layers as MVT tiles (archives or directories), as GDAL writes them; invalid polygons repaired
as GDAL's MVT writer repairs them with GEOS; AVIF tiles; existing archives updated (rasters
and vector layers appended). Checked against GDAL (
scripts/compare_tiles_with_gdal.py). - STAC items and collections as datasets. Done: items, item collections, collections and STAC
API searches (local or remote, following
nextlinks) as footprint layers read as GDAL reads STAC GeoJSON; assets opened asSTAC:"item":asset(COGs in place); mosaics of an asset over items as GDAL's STACIT driver builds them (STACIT:"search":asset=red), read by window.- POST pagination.
- STACTA (tiled assets).
- Signed URLs (Planetary Computer).
0.5: Bindings and the GDAL backend
- Python bindings (PyO3), with NumPy arrays for rasters and Arrow for vector layers. Done:
open, lazy window and overview reads (remote COGs included),Raster.from_array, vector layers as GeoJSON, WKB and GeoDataFrames, Arrow through the PyCapsule interface (pyarrow, GeoPandas, Polars, GDAL), and every operation, with arrays checked against GDAL's Python bindings; type stubs checked withmypy --strictandstubtest. - A rasterio-compatible Python API,
terrio.rio(import terrio.rio as rasterio). Done: datasets read lazily by window (remote COGs included) and written by window, with rasterio's profiles, tags, band metadata, masks, overviews and resampled reads;crs,transform,windows,features,warp,merge,mask,vrt.WarpedVRT,io.MemoryFile,env,plot; every function compared with rasterio on synthetic and real files, and rasterio's documentation examples run unchanged but for the import. - An npm package built from the WebAssembly target. Done: datasets opened from bytes (GeoTIFF, ASCII grid and the text and FlatGeobuf vector formats), bands as typed arrays, GeoJSON out, conversion to bytes, CRS transforms, zipped Shapefile and MapInfo and GeoPackage from memory, Arrow IPC streams.
- Datasets opened from and written to bytes (
terrio::memory). - An optional
gdalfeature that opens anything GDAL can when it is installed, through the same data model. Done: fallback inopen,open_raster,open_vectorandlist_layers, aGDAL:prefix and--driver gdal, GDAL dataset names and subdatasets, rasters and vector layers written with any GDAL driver (ERDAS Imagine, OpenFileGDB, DXF...), and bytes terrio can't read opened through GDAL's/vsimem/. - Arrow and GeoArrow import and export for vector layers.
- The command line without a Rust toolchain:
pip install terrioinstalls theterriocommand, standalone binaries for Linux, macOS and Windows are attached to each release (cargo binstall terrio-cli), and one-line installers download them (curl -LsSf https://terrio.opengeos.org/install.sh | sh,install.ps1on Windows). - An MCP server (
terrio mcp): AI agents run the commands as tools, each described by its--help.
1.0: Stable
- A stable public API, documented with examples. Done: every public item documented
(
#![warn(missing_docs)], rustdoc warnings denied in CI), a tour in the crate documentation, examples on the entry points, the API decisions written down (thewbraster/wbvectordata model,#[non_exhaustive]enums, options built withDefault), docs.rs metadata, and the crate packaging verified (cargo publish --dry-run); released: terrio 0.1.0 and 0.1.1 on crates.io and npm (terrio-js), 0.1.1 on PyPI. - A conformance suite of real-world files per format, with results compared against GDAL. Done:
scripts/conformance.pyruns every comparison (summaries and pixels, vector and raster round trips, windows, operations, terrain and algebra, vector operations, point clouds, CRS accuracy) and writes the report published asdocs/conformance.md; CI runs it. - Benchmarks against GDAL for reading, writing, warping and converting.
- A minimum supported Rust version policy. Done: Rust 1.90, raised only when needed and never to a release less than six months old, checked in CI.
Point clouds
- LAS 1.0 to 1.4 (point formats 0 to 10, extra bytes, VLRs and EVLRs, WKT and GeoKey CRSs),
LAZ (LASzip, including its first unchunked compressor) and COPC, read and written; COPC files,
local or remote, read in place by bounds, octree level, point spacing or point budget, with HTTP
range requests; filters by bounds, classes and returns; reprojection; gridding to rasters as PDAL's
writers.gdaldoes (min, max, mean, IDW, count, standard deviation, window filling); conversion to and from vector point layers;terrio info,terrio convertandterrio grid; Python (NumPy) and JavaScript (typed arrays) bindings. Every value is compared with laspy, and headers, COPC queries, grids and reprojections with PDAL (scripts/compare_pointclouds.py). - EPT read (LASzip, binary and Zstandard tiles; local, HTTP and S3, in place, by bounds,
resolution and depth, as PDAL's
readers.ept, checked on a USGS 3DEP EPT of 11.5 billion points); PLY (ASCII and binary) and E57 read and written, with PDAL's dimension names and values; reads and writes of LAS, LAZ, COPC and PLY in chunks for files larger than memory (terrio convert,info --stats,grid,filter range; COPC octrees built out of core), verified on a 3 GB file of 101.7 million points in at most 251 MB; waveform data packets (point formats 4, 5, 9 and 10, in the file or a.wdpfile) read and kept on write; PDAL'sfilters.smrf,filters.outlier,filters.range,filters.sample,filters.voxeldownsizeandfilters.voxelcenternearestneighbor, identical to PDAL's on USGS 3DEP lidar (terrio filter); gridding by TIN (linear) and nearest-neighbour interpolation asgdal_griddoes (terrio grid --method). - EPT writing (LASzip, binary and Zstandard tiles, the COPC octree built out of core, read by
PDAL's
readers.eptas it reads the source, checked on 4.07 million points of USGS 3DEP lidar); E57 written in chunks (packet by packet, 59 MB for 4.07 million points); PDAL'sfilters.pmf,filters.csf,filters.elm,filters.hag_nn,filters.hag_delaunay,filters.crop(boxes, polygons, circles and spheres),filters.decimation,filters.expressionandfilters.assign(PDAL's expression language),filters.merge,filters.splitter,filters.chipper,filters.normalandfilters.cluster, identical to PDAL's on autzen and USGS 3DEP lidar (terrio filter, Python and JavaScript);filters.reprojectionisterrio convert --t-srs. - PDAL's other filters (
filters.dbscan,filters.covariancefeatures,filters.lof,filters.dem,filters.colorization,filters.overlayand the long tail). - EPT datasets updated in place (appending points, as Entwine's
--subsetand merge do).
Bindings: Python and WebAssembly
Python:
- A rasterio-compatible API,
terrio.rio(import terrio.rio as rasterio): datasets opened for reading, writing and update with profiles, windowed and incremental writes, masks,windows,transform,features,warp,merge,mask,WarpedVRTandMemoryFile, checked against rasterio on the same scripts. Done, and its known gaps closed: rotated and south-up rasters read, written (ModelTransformationTag) and warped as GDAL does, in the core raster model too (RasterDataset::affine); GDAL's internal TIFF masks read and written (write_masksaved to the file); GCPs read and written (GeoTIFF tie points), with GDAL's GCP transformers in the warper and intransform.xy/rowcol(a homography from 4 or 5 GCPs, polynomials of order 1 to 3, thin plate splines;gdalwarp -order/-tpspixels interrio warptoo); RPCs read (the GeoTIFF tag,_RPC.TXTand.RPBsidecars) and written; per-band nodata from.aux.xmlsidecars;rasterio.shutilandrasterio.stack; writers larger than memory kept in a mapped temporary file and written to GeoTIFF in rows of blocks (terrio.GeoTiffWriter, Rust'sGeoTiffWriter).- RPC transformers.
-
.msksidecars. - A
riocommand line.
- xarray integration like rioxarray: a
terriobackend engine with lazy windowed reads (remote COGs read tile by tile, dask chunks), and an accessor with rioxarray's methods (reproject,reproject_match,clip,clip_box,pad_box,to_raster,merge_arrays). Done:terrio.xarray, an xarray backend (engine="terrio") andopen_rasterioreading lazily by window (remote COGs fetch only the tiles needed; dask chunks read in parallel, thread-safe and picklable), with rioxarray's coordinates,spatial_refgrid mapping, attributes and encoding (masked, mask_and_scale, overviews, band_as_variable); the.terrioaccessor with rioxarray's.riomethods (CRS, transform, bounds, nodata,isel_window,clip_box,clip,pad_box,reproject,reproject_match,interpolate_na,to_rasterto GeoTIFF, COG and GeoZarr),merge_arraysandmerge_datasets, and an opt-in.rioalias; compared with rioxarray on synthetic and real files (257 checks, none differing).- Subdatasets, GCPs and RPCs.
- Wheels with the GDAL backend as an optional extra, for the formats terrio doesn't read itself.
Done: the Python package's
gdalfeature, built from source against an installed GDAL (maturin build --features gdal).- Prebuilt
terrio-gdalwheels bundling GDAL (vcpkg and cibuildwheel, as rasterio's), or a conda-forge package linkinglibgdal-core(see docs/gdal.md).
- Prebuilt
- Streaming vector reads and writes, so layers larger than memory can be converted and filtered.
- GeoPackage updates in place (appends without rewriting the file). Done:
write_vector(..., append=True)andconvert(..., overwrite=True)change the file in place, as GDAL does through SQLite, with a rollback journal. - Windowed decoding of JPEG 2000 and JPEG XL, so Sentinel-2 bands are read by window as COGs are.
Done: JPEG 2000 read by window and resolution level (OpenJPEG's decode area and reduce factor,
tiles decoded in parallel) through
LazyRaster,terrio convert --srcwin/--overview, Python's lazy reads andterrio.rio, with GDAL's overviews and block sizes; a 1024 x 1024 window of a 10980 x 10980 Sentinel-2 band takes 0.05 s and 81 MB where the whole band takes 0.5 s and 1.7 GB, and fetches 3.4 MB of the 26 MB file over HTTP; JPEG XL GeoTIFFs decoded tile by tile. Every window and overview is identical to GDAL's. - Wheels for more platforms (musl aarch64, Windows ARM64) and for Pyodide, to run in the browser. Done: musllinux aarch64 and Windows ARM64 wheels; a Pyodide 0.29 wheel (Python 3.13) attached to each GitHub release, tested in Pyodide under Node.js in the release workflow.
WebAssembly (the terrio-js npm package):
- The operations:
warpwith all its options,translate,rasterize,polygonize,dem,calc,mosaic,proximityandsieve, as the command line and Python offer them. - Remote data read lazily with HTTP range requests (
fetch), as native builds do: COGs by window and overview, PMTiles, COPC and FlatGeobuf by bounding box, GeoParquet by row group. - More formats in memory: Zarr and GeoZarr, netCDF and HDF5, point clouds (LAS, LAZ, COPC), and writing LERC, JPEG XL and JPEG GeoTIFFs.
- terrio's own FlatGeobuf reader in place of the format library's, which overflowed on large
files in 32-bit WebAssembly. Done: terrio's driver parses the files, with 64-bit offsets (the
format library supplies only the schema types and the writer's spatial index); an 867 MB file
of 2 million lines opens in Node.js in 3.3 s. A layer opened whole takes, with the file's
bytes, about four times the file's size for lines and five for polygons, so a 1 GB file of
polygons needs the memory64 item below, or
openUrlto read it by bounding box (with a spatial index; a file without one is still read whole). - Multithreaded decoding and writing with WebAssembly threads, where the page allows them.
- A smaller download: optional formats split out or loaded on demand, and size-optimized builds.
- Datasets larger than 4 GB, with the 64-bit memory proposal (memory64) where runtimes support it.
Later
- CRS: datum shift grids (NTv2, GeoTIFF grids), vertical datums, time-dependent transformations
between dynamic datums (coordinate epochs). Done: PROJ's GeoTIFF grids (PROJ-data), NTv2 and GTX
read in pure Rust and interpolated as PROJ does (
hgridshift,vgridshift, NADCON5's biquadraticgridshift,xyzgridshift,deformation), looked up inTERRIO_GRID_DIR,PROJ_DATAand PROJ's user directory, or downloaded from the PROJ CDN when network access is on (or registered from bytes in WebAssembly); the transformations GDAL picks when the grids are available, else those without grids; geoid models for compound and geographic 3D CRSs (EPSG:4326+5773); time-dependent Helmert transformations with coordinate epochs as GDAL sets them (--s_coord_epoch,--t_coord_epoch,TransformOptions, Python and JavaScript). Checked against GDAL with the same grids: every grid ofscripts/proj_grids.txtto the micrometre, and every EPSG CRS's reference points (scripts/compare_crs_grids_with_gdal.py,epsg_accuracy_grids).- PROJ's chains through several intermediate datums.
- Vertical-to-vertical transformations (VERTCON, offsets).
- Moving coordinates between epochs (point motion operations).
- Reading remote grids by range requests rather than whole.
- NetCDF and HDF5 in pure Rust. Done: classic and netCDF-4 reading, with variables in groups
below the root (
NETCDF:"file.nc":/group/var) listed asgdalinfolists them; classic (CDF-1/2/5) and netCDF-4 writing (FORMAT=NC4/NC4C, chunked, deflate and shuffle, CF georeferencing, as GDAL writes them; terrio's own HDF5 writer); HDF5 rasters as GDAL's HDF5 driver reads them (HDF5:"file.h5"://group/dataset, metadata, user blocks), checked on JPSS and Sentinel-5P products; HDF-EOS5 grids and swaths, COSMO-SkyMed and ODIM georeferencing, netCDF geolocation arrays and complex (compound) datasets, as GDAL 3.12 reads them.- Writing groups.
- JPEG 2000 and other compressed raster formats. Done: JPEG 2000 (JP2 and J2K, GeoJP2 and
GMLJP2 georeferencing) is read and written in WebAssembly too, with the same OpenJPEG port as
native builds, and the npm package reads and writes it; GeoTIFFs compressed with LERC (alone,
with Deflate or with Zstandard), JPEG XL and LZMA are read, natively and in WebAssembly; JPEG
(GeoTIFF, GeoPackage and tile archive tiles,
.jpgfiles) is decoded exactly as libjpeg decodes it for GDAL; ZSTD GeoTIFFs are written in WebAssembly. Every case is compared with GDAL on Sentinel-2 bands, GDAL's JPEG 2000 test files and GDAL-written GeoTIFFs. JPEG 2000 files are read by window and resolution level as GDAL reads them (their resolution levels as overviews, only the tiles, precincts and code blocks of a window decoded, tiles in parallel, locally or with HTTP range requests), and JPEG XL GeoTIFFs tile by tile; every window and overview is identical to GDAL's. - Band histograms as
gdalinfo -histcomputes them (terrio info --hist, identical to GDAL's on GeoTIFFs and scanline formats); color relief written as a VRT (terrio dem color-relief -f VRT, lookup tables identical togdaldem's). Done.
Decisions
- The core API is synchronous and reads datasets into memory; remote files are downloaded first.
GeoTIFFs, local or remote, can instead be opened lazily (
LazyRaster) and read by window and overview level, decoding only the tiles needed and fetching them with HTTP range requests. Terrain analysis, warping and mosaics read their inputs that way and write GeoTIFF outputs a strip of rows at a time (ops::dem::dem_to_path,ops::warp::warp_to_path,ops::mosaic::mosaic_to_path, which the command line uses), with the values of the in-memory operations. - HTTP goes through
ureq(with rustls) and S3 requests are signed by terrio itself, rather than throughobject_store, to keep the API synchronous and the dependencies small. - GeoPackage is read and written by terrio's own SQLite file reader and writer (pure Rust): no C dependency, and files written in one go.
- terrio carries its own EPSG table (
crates/terrio/data/epsg.tsv.zst, about 220 KB, generated from GDAL and PROJ byscripts/gen_epsg_table.py): the PROJ strings of the CRSs, their WKT and the datum transformations GDAL lists for them, so that terrio picks the same transformation as GDAL for each point. The projection formulas are PROJ's, ported to Rust;wbprojectionremains the fallback for projections EPSG CRSs do not use. WKT 1 and the geographic and projected CRSs of WKT 2 are read by terrio's own reader. - NetCDF-4 and HDF5 are read with the pure-Rust
netcdf-readerandhdf5-readercrates, and netCDF-4 is written by terrio's own small HDF5 writer (superblock 2, version 2 object headers, version 1 chunk B-trees, dimension scales), which the netCDF and HDF5 libraries read: no HDF5 write crate exists in pure Rust. - Point clouds use the
lazcrate for LASzip, and terrio's own LAS and COPC code rather than thelascrate (whose header reader keeps only the first EVLR) orwblidar(which reads COPC points in hierarchy order and cannot read LASzip's unchunked compressor). Point clouds are notDatasets: they have their own reader (pointcloud::open_points) and columnarPointCloud.