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.
Completed: Practical GDAL feature parity
All nine items in #238 are complete as of 2026-10-24. The gap analysis records the supported scope and validation criteria. These additions build on the operations and drivers below.
- Fill nodata: inverse-distance and nearest interpolation, validity masks, maximum pixel distance and smoothing, preserving valid source pixels. Done in #239, through Rust, CLI/MCP, Python and JavaScript.
- Overview-aware warping: explicit source overview selection, automatic selection for downsampling and a full-resolution override, reducing remote COG reads. Implemented in #242, through Rust, CLI/MCP, Python and JavaScript.
- Geolocation-array warping: reproject swaths using the longitude/latitude arrays already exposed by the scientific-format drivers, including subsampled arrays and pixel-center conventions. Done in #241; subsampled/invalid-node swaths retain documented GDAL boundary-mask differences.
- Richer VRT interoperability: serialized warped VRTs, then selected built-in derived bands and kernel-filtered sources, with lazy window reads and GDAL-compatible output. Rust, CLI/MCP, Python and WebAssembly; supported subsets are documented in the CLI guide.
- Richer vector SQL: computed SELECT columns, DISTINCT, summary aggregates and limited attribute joins, with the supported OGR SQL subset documented explicitly. Done in #245, with real-data OGR SQL comparisons.
- Viewshed: single-observer visibility and minimum visible height, observer/target heights, maximum distance and curvature/refraction controls, with explicit input nodata behavior. Done in #240, through Rust, CLI/MCP, Python and JavaScript; projected DEM comparisons are registered in conformance.
- Create new raster/vector footprint indexes and replace single-file outputs across Rust, CLI/MCP, Python and JavaScript, with GTI metadata in GeoPackage/FlatGeobuf, discovery/filtering and source-CRS fields. Done in #347. Append/update, STAC profiles and XML companions remain separate future extensions.
- Indexed raster catalogs: GTI-compatible GeoPackage/FlatGeobuf tile indexes, spatial source selection, compositing order, mixed-CRS sources and a bounded source cache. Done in #249, with nested VRT export protection and conformance reporting completed in #255.
- Pansharpening: weighted Brovey processing, spectral weights, resampling, nodata, bit depth and extent handling. Materialized output shipped in #247, with lazy GDAL-compatible VRT output and window reads in #250.
- Native multidimensional API: groups, named dimensions, coordinate arrays, lazy time/depth slicing and transposition across netCDF/HDF5/Zarr, retaining the existing raster views. Done in #252; the supported numeric/raw-CF subset is documented in the multidimensional guide.
The completed scope preserves pure-Rust/default WebAssembly builds, registers real-data comparisons in the GDAL conformance workflow, and exposes applicable functionality through Rust, CLI, Python, JavaScript and MCP. Supported subsets and numerical tolerances remain documented; additional feature scope belongs in separate tracking issues.
Additional GDAL command coverage
#289 tracks unified GDAL command equivalents and their documented subsets. It remains open at the maintainer's request; additional option and output depth is tracked in #288. Command-group progress:
-
Palette conversion: RGB median cut, supplied palettes, RGB/RGBA expansion and attachment across Rust, CLI/MCP, Python and JavaScript; real Landsat/OPERA GDAL 3.13.3 comparisons registered as
palette. Landed in #372. -
COG validation: GDAL 3.13 structural rules and full compressed-block checks, uploaded TIFF bytes through Rust/Python/JavaScript, and CLI/MCP options. Landed in #374, with real-data conformance and measured remote header/full-check I/O.
-
Seek-optimized ZIP archives: create, append, list, optimize and validate through Rust, CLI/MCP, Python and JavaScript, with lazy native/HTTP GeoTIFF member reads. Landed in #404; 34 GDAL 3.13.3 VSI comparisons include nine SOZip archive/window checks and measured HTTP I/O.
-
Multidimensional mosaics: lazy coordinate-aligned numeric arrays across Rust, CLI/MCP, Python and JavaScript, with netCDF/Zarr output and real NOAA wind comparisons registered as
mdmosaic. Landed in #373. -
Separate-file raster tiles: XYZ/TMS PNG and typed GeoTIFF output through Rust, CLI/MCP, Python and JavaScript, with bounded source windows and non-overwriting resume. Real Landsat/USGS comparisons are registered as
raster-tiles; direct-source lower zooms differ from GDAL's generated-image pyramid. Retile layouts, recursive pyramids, CSV/vector indexes and Leaflet/OpenLayers, MapML, STACTA and KML publication helpers are implemented in #440, with registeredretileandtile-publicationcomparisons against GDAL 3.13.3. -
Vector clipping: points, lines, polygons and collections, with bbox, geometry, filtered vector/raster templates and region CRS transforms through Rust, CLI/MCP, Python and JavaScript. Landed in #419, with 27 GDAL 3.13.3 comparisons and measured remote template I/O.
-
Polygon coverage validation: indexed overlap, shared-node and narrow-gap checks through Rust, CLI/MCP, Python and JavaScript. Landed in #420, with 24 exact GDAL 3.13.3 comparisons, including real counties and dimensional inputs.
-
Raster/vector pipelines: linear processing chains with read, write and materialize steps, saved GDALG definitions and deferred named outputs through Rust, CLI/MCP, Python and JavaScript. Landed in #428, with 45 registered GDAL 3.13.3 comparisons covering pipelines and direct metadata regressions.
-
Coverage simplification: shared-edge topology preservation, boundary options and active-layer copying across Rust, CLI/MCP, Python and JavaScript. Landed in #424, with 48 exact GDAL 3.13.3 simplification comparisons and documented dimensional metadata behavior.
-
Layer algebra: intersection, union, symmetric difference, identity, update, clip and erase for polygon, line, point and collection inputs across Rust, CLI/MCP, Python and JavaScript. Landed in #427.
-
Coverage cleaning: snapping, progressive overlap assignment and enclosed-gap filling across Rust, CLI/MCP, Python and JavaScript. Landed in #426, with 236 GDAL 3.13.3 comparisons and documented, independently checked ordering-dependent results.
-
Vector geometry operations: simplify, segmentize, convex and concave hulls, swap XY, geometry-type conversion, grouped combine and spatial sort. Landed in #306, through Rust, CLI/MCP, Python and JavaScript, with GDAL/GEOS comparisons and documented tolerances.
-
Vector gridding and linear referencing: every
gdal_gridalgorithm (search ellipses, quadtree order, per-quadrant limits) asterrio vector-grid, andogrlinerefasterrio lineref. Landed in #305, through Rust, CLI/MCP, Python and JavaScript, with real-data comparisons registered in conformance. -
GeoPackage repacking: encoded-record B-tree rebuilds, preserved rowids and SQL objects, auto-vacuum maps and journaled native replacement through Rust, CLI/MCP, Python and JavaScript. Landed in #384; real-data comparisons are registered as
gpkg-repack. -
GeoPackage validation: SQLite integrity, core and extension metadata, geometries, raster tiles and supported registered views through Rust, CLI/MCP, Python and JavaScript. Landed in #403, with explicit incomplete reports for unsupported SQL; 50 GDAL 3.13.3 validation attempts (34 compared, 16 not compared because GDAL refuses the malformed inputs), registered as
gpkg-validate. -
Dataset identification: bounded 16 KiB headers, ordered input batches, recursive local discovery and explicit companion-file reader fallback through Rust, CLI/MCP, Python and JavaScript. Landed in #375; real-data GDAL comparisons are registered as
identify. -
Dataset file management: encoded copies, renames and deletion of owned companions, with staged replacement and source alias protection through Rust, CLI/MCP, Python and WebAssembly. Landed in #382; real Landsat and Natural Earth comparisons are registered as
dataset-files. -
Parquet metadata aggregation: explicit compatible partitions, relative column paths, merged GeoParquet metadata and overwrite guards through Rust, CLI/MCP, Python and JavaScript. Landed in #382, including #383; real-data and measured S3 range-read comparisons are registered as
parquet-metadata. -
Dataset content checks: all exposed raster bands, vector layers, numeric scientific arrays and point records, with optional raster overviews and masks. Rust, CLI/MCP, Python and JavaScript share the checks. Landed in #378; conformance is registered as
dataset-check.
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
-
Raster query commands: compare, pixel-info and XYZ export through Rust, CLI/MCP, Python and JavaScript, with windowed reads, native streaming, GDAL 3.13.3 comparisons and documented differences. #303, part of the #289 command tracker.
-
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). - Raster blending (
blend): ten GDAL composition operators, opacity and palette/alpha inputs - Raster pixel operations (
unscale,nodata-to-alpha,reclassify,as-features) across Rust, CLI/MCP, Python and WebAssembly. Landed in #299. - Focal statistics and kernels (
neighbors), collar cleaning (clean-collar/nearblack) and LUT contrast enhancement (enhance) across Rust, CLI/MCP, Python and WebAssembly (#300). - Geometry type conversion, simplify/segmentize, concave and convex hulls, grouped combine, spatial sorting and coordinate swapping across the public APIs (#306).
- Centroids, multipart splitting, vertex extraction, exact valid-data raster footprints and point sampling across multiple rasters, through the Rust API, CLI and MCP.
- Polygonization preserves exterior rings and holes on south-up and rotated grids, and classifies rings correctly for small pixels at large coordinate origins.
- Metadata-only CADRG RPF table-of-contents creation through Rust, CLI/MCP, Python and WebAssembly, with bounded frame reads, polar grids and atomic native output (#446). Part of #289.
- Python and WebAssembly contour, footprint, buffer, grouped dissolve, geometry validity and repair functions, using the shared Rust engines.
- Python and WebAssembly zonal statistics and recursive multipart splitting, using the shared Rust engines with explicit eager-raster and dimensional limits (#447).
- Contour lines with interval/fixed levels, nodata handling and optional elevations in Z; multi-raster zonal statistics with GDAL's centre/all-touched pixel rules and per-raster CRS transformations; a standalone read-only SQL command using the existing query engine.
- Planar buffers, polygon overlay/dissolve, geometry validity/repair and explicit cleanup tolerances through the Rust API, CLI and MCP, completing the commands from #211; tracked in #227.
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); remote groups without consolidated metadata listed through the object store's listing (S3 and S3-compatible, Google Cloud Storage, Azure), and a group's arrays listed as GDAL's subdatasets, identical togdalinfoon an OME-Zarr image of EBI's S3 and on CMIP6 on GCS; arrays of more than three dimensions read with their other dimensions as bands, as GDAL reads them, identical to GDAL on CMIP6's 4D air temperature on GCS (37,620 bands,DIM_*metadata included). - 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
by an independent tile-grid implementation, checked against GDAL/GEOS with documented
compatibility limits; 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;POSTsearches and pages (method=POST,nextlinks withbody,mergeandheaders), and Planetary Computer's assets signed with its SAS tokens, mosaics identical to GDAL's STACIT on the items pystac-client andplanetary_computerreturn.- STACTA (tiled assets). Done: STAC items of the tiled-assets extension read as one raster
as GDAL's STACTA driver reads them (
item.json,STACTA:"item.json":asset:tms, local or remote): the most detailed tile matrix over its limits, the others as overviews (cut to its extent as GDAL cuts them), windows reading only the tiles they touch, bands fromraster:bandsor a first tile, properties as metadata, GDAL's subdatasets, missing tiles as nodata withGDAL_STACTA_SKIP_MISSING_METATILE; summaries, whole rasters, windows and overviews identical to GDAL's on its STACTA test data and ongdal raster tilepyramids, local and over HTTP (scripts/compare_stac_geozarr.py stacta).
- STACTA (tiled assets). Done: STAC items of the tiled-assets extension read as one raster
as GDAL's STACTA driver reads them (
- GRIB 1 and 2, read only. Done: a pure-Rust driver read as GDAL's GRIB driver reads files (a
Float64 band per message or field, GDAL's
GRIB_*band metadata and descriptions from its tables and degrib's naming rules, its grids, earth shapes and 0-360 rewrapping, units normalized, nodata where bitmaps mask points), GRIB2 packings decoded as g2clib decodes them (simple, complex, spatial differencing, IEEE, JPEG 2000 with the OpenJPEG port, PNG, and CCSDS with the pure-Rustrust-aecdecoder), GRIB1 simple packing; local files, bytes, and remote files read message by message (one 16 KiB block per message for its headers, no sidecar; servers that ignore range requests refused), every allocation a header sizes fallible; in WebAssembly too. Identical to GDAL, metadata and pixels, on files GDAL writes in each of its encodings and on NOAA's GFS, GEFS, HRRR and MRMS products (scripts/compare_grib_with_gdal.py).
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.dev/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
filters.dbscan,filters.covariancefeatures,filters.eigenvalues,filters.approximatecoplanar,filters.estimaterank,filters.lof,filters.nndistance,filters.radialdensity,filters.reciprocity,filters.miniball,filters.zsmooth,filters.skewnessbalancing,filters.head,filters.tail,filters.sort,filters.locate,filters.iqr,filters.mad,filters.dem,filters.colorization,filters.overlayandfilters.hexbin(boundaries on hexer's hexagons; not its H3 grids), identical to PDAL's on autzen (nanoflann's neighbours, sorted radius results and Eigen's eigensolver as PDAL runs them; in radius results of more than 16 points, points at exactly the same distance may be ordered differently, see Point clouds;terrio filter, Python and JavaScript). - PDAL's long tail of filters (
filters.optimalneighborhood,filters.neighborclassifier,filters.radiusassign,filters.hag_dem,filters.voxelcentroidnearestneighbor,filters.farthestpointsampling,filters.transformation,filters.ferry,filters.returns,filters.groupby,filters.planefit,filters.litree,filters.delaunay,filters.h3and others). - EPT datasets updated in place: points appended (
terrio convert --append,append_points, Python'sappend_points), placed in the dataset's octree within its cube, leaves split as the writer splits them, the hierarchy (its pages kept),ept.jsonand the source list updated, staged beside the dataset and moved in withept.jsonlast, in bounded memory; terrio's datasets (LASzip, binary and Zstandard tiles) and Entwine's; datasets built with the same--ept-boundsmerged by appending one to the other. Checked on autzen (10.65 million points in three parts): every point once, reads by bounds identical to a build from all the points at once, read by PDAL'sreaders.ept(and itsoriginoption).
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); GDAL's.msksidecars read (any local format, overviews included) and written (GDAL_TIFF_INTERNAL_MASK=NO, formats without internal masks, kept onr+), the same files as rasterio's on Copernicus DEM tiles.- RPC transformers. Done: GDAL's RPC transformer (
terrio::ops::rpc): the polynomials, the iterative inverse,RPC_HEIGHT,RPC_HEIGHT_SCALE,RPC_DEMwith its interpolations, missing value and DEM CRS,RPC_PIXEL_ERROR_THRESHOLD,RPC_MAX_ITERATIONS, and its rows sampled whole from a WGS 84 DEM; in the warper, asgdalwarp -rpc -to ...(terrio warp --rpc --to, with gdalwarp'sSOURCE_EXTRA,SAMPLE_STEPS=ALLand output window for RPCs) and Python'swarp(rpc=, to=); rasterio'sRPCTransformer,xy/rowcolandTransformMethod.rpcswithrpc_options,reproject(rpcs=),calculate_default_transform(rpcs=)andWarpedVRTby GCPs or RPCs; rioxarray'sreprojectthrough RPCs. Coordinates identical to rasterio's and pixels to rasterio's and gdalwarp's on ICEYE images with a Copernicus DEM and on synthetic RPCs with DEMs (WGS 84 and UTM, nodata holes). - A
riocommand line. Done:terrio-rio(andpython -m terrio.rio), rasterio'sriocommands (info,bounds,blocks,shapes,clip,convert,merge,overview,warp,sample,transform,mask,rasterize,stack,edit-info,env,gcps,calcwith its snuggs expressions,create,rm,insp) with their options, defaults and outputs, run withterrio.rioand compared with rasterio'srioon synthetic rasters and on Copernicus DEM and Sentinel-2 COGs.
- RPC transformers. Done: GDAL's RPC transformer (
- 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 (group=,variable=), GCPs and RPCs as rioxarray opens, stores, writes and (GCPs) reprojects them, identical to rioxarray on an ESA CCI soil moisture netCDF and ICEYE GCP and RPC images. - 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). - 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.
Done: a threaded module,
terrio-js/threads(WebAssembly atomics, a rayon pool of Web Workers or Node.js worker threads,initThreadPool(n)), whichterrio-js/loadpicks in Node.js and on cross-origin isolated pages, the single-threaded module elsewhere. GeoTIFF tiles, JPEG 2000 tiles and Zarr chunks decode in parallel, and GeoTIFF/COG and Zarr writes compress in parallel, to the same bytes; with 16 threads, a 3612 x 3612 DEM decodes in 92 ms instead of 542 and writes as a DEFLATE COG in 302 ms instead of 2.5 s, and a Sentinel-2 JPEG 2000 band decodes in 0.9 s instead of 6.8, in Node.js, Chromium and Firefox alike (tested in CI in all three). Vector work stays on one thread, as WebAssembly's allocator makes small parallel allocations slower. - A smaller download: optional formats split out or loaded on demand, and size-optimized builds.
Done: the package holds a second module,
terrio-js/core, without GeoParquet and Arrow, netCDF/HDF5, point clouds, JPEG 2000, Zarr, AVIF and OSM: 8.9 MB (3.0 MB gzipped, 2.3 MB with Brotli), against 16.4 MB (5.6 MB, 4.1 MB) for the full one, which a page can import when it needs those formats; formats left out say which module has them. Both are built with whole-program LTO, the codecs optimized for speed and the rest for size, and wasm-opt -Oz: the full module went from 20.6 MB (6.7 MB gzipped) to 16.4 MB, and decodes GeoTIFFs and JPEG 2000 and writes COGs as fast in Node.js. - Datasets larger than 4 GB, with the 64-bit memory proposal (memory64) where runtimes support
it. Done: a memory64 module,
terrio-js/memory64(every format, up to 16 GB), whichload({ memory64: true })picks where the runtime has 64-bit memories (Node.js 24+, Chrome 133+, Firefox 134+; Safari falls back): a 1.27 GB FlatGeobuf of 289,890 polygons, which runs the 32-bit modules out of memory, opens in 4 s in Node.js with 7.5 GB of memory, and a 2 GB raster is written and read back. Built on nightly Rust (wasm64-unknown-unknown, tier 3,-Z build-std) with cargo and wasm-bindgen, wasm-pack and its wasm-opt not supporting it yet, and with build-time patches of three crates that treat WebAssembly as wasm32 only (tar,zstd-sys,arrow-buffer); single-threaded.
Later
- HEC-RAS geometry and plan HDF files as vector layers (the
HECRASdriver,hecrasfeature). Done: the 2D mesh cells, cell centres, faces and perimeters, the 1D cross sections, rivers, structures and storage areas, boundary-condition lines, reference lines and points, through theras-commander-fast-hdfengine, with the stored results joined by open options (summaries, static mesh arrays, time-step values, series extremes, cross-section, structure, boundary and reference series); the stored projection text as the CRS, never a guessed EPSG code; refusals as errors. Recognized by content, so HDF5 rasters are unchanged. CLI (info --oo,convert --oo), MCP, Python (options=) and JavaScript (openVector, in memory). Tested on HEC-RAS 7.0.1 files against h5py, bit for bit (scripts/compare_hecras_with_h5py.py, thehecrasconformance suite); older versions best effort. - HEC-RAS files read in place over HTTP and S3 by range requests: needs an engine byte-source
interface (plan the ranges from the HDF5 metadata, fetch them, then decode, as
sparse::SparseFilereads). - One HDF5 reader for terrio and the HEC-RAS engine: both on crates.io
hdf5-reader0.10.1. - 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); CDN grids read in place by range requests, as PROJ reads them (four EGM2008 heights fetch 0.85 MB of 80 MB), identical to PROJ's values and togdalwarpwithPROJ_NETWORK=ON(or downloaded whole withTERRIO_GRID_DOWNLOAD=YES); transformations between vertical CRSs as PROJ lists them (offsets, offsets and slopes, VERTCON and other grids, chained through an intermediate vertical CRS, depths included), PROJ's heights on 727 of 732 points over 604 pairs (scripts/compare_vertical_with_proj.py); point motion operations between two epochs of a datum (the Canada velocity grids of NAD83(CSRS), NKG_RF17vel), PROJ's coordinates to 4 nm.- PROJ's chains through several intermediate datums. Done: between CRSs whose datums
no transformation links directly, the transformations PROJ's database registers
(concatenations of several among them included, NKG's too) chained through
intermediate CRSs and datums as
proj_create_operationsfinds them: through an intermediate CRS between the geographic and geocentric CRSs of the two datums in PROJ's order, then through an intermediate datum whatever its CRSs (when none is found, or only through PROJ's WGS 84 null transformations, over too small an area or time-dependent ones), without those another found replaces or whose grids are missing; sorted, pruned and picked per point as PROJ does (0 m ballparks between prime meridians, unknown areas, merged translations, heights through the chain, reversed for projected targets). The EPSG table carries the registered transformations and geodetic CRSs (EandRrows, 320 to 352 KB). Over 31,127 pairs of EPSG geographic CRSs and 1,204 projected ones (scripts/compare_chains_with_proj.py), GDAL's coordinates within 1 cm for 31,037 and 1,179 of them before, for all but the 2 projected pairs PROJ links by transformations between projected CRSs after; with grids, 30,993 and 1,173 before, 31,126 and 1,200 after.
- PROJ's chains through several intermediate datums. Done: between CRSs whose datums
no transformation links directly, the transformations PROJ's database registers
(concatenations of several among them included, NKG's too) chained through
intermediate CRSs and datums as
- 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; rasters written into netCDF-4 groups (Rust, Python, JavaScript, andterrio convertwithGROUPandAPPEND_SUBDATASET=YES), read back identically by GDAL's classic and multidimensional APIs, netCDF4 and xarray on Copernicus DEM tiles and their terrain. - 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.
CAD vector interchange (#262)
- Native DWG and ASCII/binary DXF reading and DWG/ASCII DXF writing through acadrust, exposed by the existing Rust, CLI/MCP, Python and full JavaScript/WASM vector APIs. Common linework, curves, text, faces, polygon hatches and inserted blocks are supported; output includes horizontal polygon holes. Drawing coordinates have no inferred CRS. See supported entities and limits.
- Additional CAD entities (splines, dimensions, meshes, ACIS solids) and acadrust's optional STL, COLLADA, OBJ, glTF/GLB and FBX importers.
Standalone CRS crate (#268)
- Extract
terrio-crsin this workspace with its tables, standalone tests and attribution, keepingterrio::crscompatible. Share grid configuration and decoded caches; keep network transport optional and lazy. Landed in #274. - Measure and reduce the standalone dependency footprint, retaining Terrio's
existing projection coverage, and publish the crate with release wiring.
Landed in #276;
terrio-crs0.1.0 is published with the release workflow's trusted publisher configured. - The portable table decoder also affects native Terrio, including CLI and Python: measured first CRS use increases from about 10.6 ms to 17.9–18.1 ms. Repeated calls remain about 3.4–3.7 µs. This initialization tradeoff removes the duplicate table-decoder dependency; TIFF codecs remain supported. See the measurement report.
- Geocentric Cartesian XYZ endpoints, EPSG/PROJ/WKT definitions, datum and projected composition, and Rust/CLI/MCP/Python/JavaScript/WASM validation against pinned and live GDAL/PROJ references (#269, #286).
- PROJJSON v0.7 parsing for static 2D geographic, projected and bound CRSs, including units, GIS axis normalization and explicit Helmert operations. Rust/CLI/MCP/Python/JavaScript/WASM validation is tracked in #270.
- PROJJSON v0.7 writing (
SpatialRef::to_projjson) of geographic, geocentric, projected, vertical, compound, bound and IAU planetary CRSs, refusing what it would describe wrongly: Rust, CLI and MCP (terrio crs --projjson), Python (CRS.to_projjson()) and JavaScript (crsToProjjson), compared with live PROJ/GDAL byscripts/conformance.py. - IAU and additional projection capabilities remain scoped separately in #271–#272.
ndarray interoperability (#298)
The optional ndarray feature (off by default): raster pixels as ndarray arrays, without
copying, converting or reordering them.
- Borrowed and mutable views of a raster's pixels (
RasterArrayExt):(bands, rows, cols)arrays and single bands, and mutable band views that do not overlap, for parallel updates. - Owned transfers between datasets and
Array3/Array2(RasterDataset::into_array3,from_array3and their two-dimensional forms), the metadata kept in a header. - Lazy reads: windows moved into arrays, masks read as
Array2<u8>(LazyRaster::read_mask_array2). - Out of scope: complex bands as
Complex<T>(they are viewed as their component bands),multidim::Buffer, and decoding into caller-provided buffers.
Analysis-ready floating-point reads (#422)
Windows read as f32 or f64 values, NaN where GDAL's mask band marks pixels invalid.
-
LazyRaster::read_window_f32andread_window_f64(FloatReadOptions: bands, overview and optional unscaling with each band's scale and offset or a netCDF variable's packing), and theirRasterDatasetcounterparts: per-dataset masks (internal,.msk, overviews') in place of nodata values, which are compared in each band's own type as GDAL's nodata mask band compares them. Identical to GDAL 3.13.2 on 38 cases (tests/data/float_reads), and to rasterio on the 35 its GDAL opens. - With the
ndarrayfeature, ownedArray3andArray2arrays, bands first or last (BandLayout), written in that layout as the pixels are converted. - Out of scope: alpha bands as masks (GDAL's
GMF_ALPHA), the Python and JavaScript bindings (Python'sterrio.rioreads masked arrays already), and writing NaN back as nodata values.
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-crs/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.