Skip to content

Command Line

terrio <COMMAND>

  identify        Identify dataset formats from bounded headers
  check           Read dataset content and report decoding failures
  info            Summarize a raster, vector dataset or point cloud
  convert         Convert a dataset to another format, optionally reprojecting it
  pipeline        Chain raster/vector operations or load/save GDALG definitions
  concat          Concatenate vector layers and datasets (like gdal vector concat and ogrmerge.py)
  partition       Split a vector dataset into files by field values (like gdal vector partition)
  set-field-type  Change field types with GDAL's value conversions (like gdal vector set-field-type)
  make-point      Point geometries from coordinate fields (like gdal vector make-point)
  create          Create a vector dataset from a schema or template (like gdal vector create)
  export-schema   Print a vector dataset's OGR_SCHEMA JSON (like gdal vector export-schema)
  rename-layer    Rename vector layers or normalize their names (like gdal vector rename-layer)
  edit            Assign vector CRS, geometry declarations or metadata (like gdal vector edit)
  update          Update or append vector features by key or FID (like gdal vector update)
  translate       Subset, resize, rescale or change the type of a raster (like gdal_translate)
  warp            Reproject and resample rasters (like gdalwarp)
  raster-index    Create raster footprint indexes
  vector-index    Create vector layer-envelope indexes
  gti-create      Create GTI-compatible catalogs (raster-index options)
  catalog         Read an indexed raster mosaic from GeoPackage or FlatGeobuf footprints
  mosaic          Mosaic rasters into one, or into a virtual raster (like gdalbuildvrt and gdal_merge)
  rasterize       Burn vector geometries into a raster
  polygonize      Turn a raster band into polygons of connected pixels with the same value
  centroid        One planar centroid per feature
  singlepart      Split multipart geometries and collections into individual features
  vertices        Extract vertices as points with source attributes and vertex indices
  buffer          Planar buffers with round caps and joins
  overlay         Polygon intersection, union, difference and symmetric difference
  dissolve        Merge polygon features, optionally grouped by attributes
  is-valid        Report geometry validity and a reason per feature
  make-valid      Repair invalid geometry
  clean           Snap, deduplicate, close rings and repair geometry
  simplify        Topology-preserving simplification (like gdal vector simplify)
  segmentize      Limit segment lengths by adding vertices (like gdal vector segmentize)
  convex-hull     Convex hull per feature (like gdal vector convex-hull)
  concave-hull    Concave hull per feature (like gdal vector concave-hull)
  swap-xy         Swap X and Y coordinates (like gdal vector swap-xy)
  set-geom-type   Change geometry types and dimensions (like gdal vector set-geom-type)
  combine         Collect geometries into collections by groups (like gdal vector combine)
  sort            Spatial sort along a Hilbert curve or an STR tree (like gdal vector sort)
  footprint       Exact valid-data footprint of a raster
  sample          Sample multiple raster bands at vector points
  contour         Contour lines at interval or fixed elevation levels
  zonal-stats     Per-polygon statistics across multiple raster bands
  sql             Read-only SELECT queries over vector layers
  dem             Terrain analysis: hillshade, slope, aspect, color relief, TRI, TPI, roughness (like gdaldem)
  calc            Raster algebra: band math in NumPy syntax over one or more rasters (like gdal_calc.py)
  proximity       Distance from every pixel to the nearest target pixel (like gdal_proximity.py)
  pansharpen      Weighted Brovey pansharpening of panchromatic and spectral bands
  compare     Compare two rasters and report the differences (like gdal raster compare, gdalcompare.py)
  pixel-info  Pixel values at positions (like gdal raster pixel-info, gdallocationinfo)
  xyz         Raster pixels as XYZ text (like gdal2xyz.py)

  viewshed        Single-observer visibility or minimum visible height (like gdal_viewshed)
  sieve           Merge raster areas smaller than a threshold into their largest neighbour (like gdal_sieve.py)
  blend           Compose Byte images with ten operators, alpha and opacity (like gdal raster blend)
  unscale         Apply band scales and offsets to get physical values (like gdal raster unscale)
  nodata-to-alpha Replace nodata values with an alpha band (like gdal raster nodata-to-alpha)
  reclassify      Map values or ranges of values to new values (like gdal raster reclassify)
  as-features     One vector feature per pixel, with band values as fields (like gdal raster as-features)
  neighbors       Focal statistics and convolution kernels (like gdal raster neighbors)
  clean-collar    Set noisy nearly black/white collars to exact black/white (like gdal raster clean-collar)
  nearblack       The same, with nearblack's two-pass defaults (like nearblack)
  enhance         Contrast enhancement by lookup tables: equalization and LUT files (like gdalenhance)
  crs             Describe a coordinate reference system, or transform coordinates between two
  formats         List the formats terrio reads and writes
  grid            Grid a point cloud into a raster (like PDAL's writers.gdal)
  filter          PDAL's point cloud filters (smrf, pmf, csf, elm, hag_nn, crop, expression...)
  vsi             List, copy, delete, move and sync files on local, cloud, HTTP and archive locations (like gdal vsi)
  mcp             Serve these commands to AI agents over the Model Context Protocol

Install it with curl -LsSf https://terrio.dev/install.sh | sh (on Windows, irm https://terrio.dev/install.ps1 | iex in PowerShell), pip install terrio or cargo install terrio-cli; see Installation.

Every input can be a local path, a URL, a cloud object or a zip archive member; see Remote Data. Outputs are local, except those of terrio vsi. Run terrio <COMMAND> --help for the full option list.

The commands that take any number of inputs (mosaic, warp, concat and filter merge) read them from a file too, as GDAL's --input @filelist.txt does. pansharpen does not: its inputs have fixed roles, the panchromatic band first, and spectral inputs may carry ,band=N. @FILE stands for the paths listed in FILE, one per line, with relative paths taken from the working directory. Unlike GDAL 3.13, which opens each line as written and fails on a blank line, terrio skips blank lines, trims whitespace around paths and ignores a byte-order mark at the start of the file. A list must be a regular file of at most 64 MiB. As in GDAL, every input beginning with @ is read as a list: name a file whose name begins with @ as ./@name.tif. The MCP tools refuse @FILE, since errors naming listed paths would let a client read a text file back line by line; MCP clients pass the inputs as arguments instead. Lists mix with paths given directly:

terrio mosaic @filelist.txt naip.vrt
terrio warp @filelist.txt extra.tif out.tif --t-srs EPSG:3857

Geometry operations

terrio centroid countries.gpkg centroids.gpkg
terrio singlepart countries.gpkg parts.gpkg --layer countries
terrio vertices roads.gpkg nodes.gpkg

All three preserve the input attributes and CRS. centroid (alias centroids) computes a 2D, planar centroid: area-weighted for polygons with holes subtracted, length-weighted for lines, and equally weighted for points. Collections use the highest non-degenerate dimension. Empty and null geometries yield null centroids, retaining the feature. The calculation uses the input coordinate units; reproject geographic data first when a centroid in linear units is needed.

singlepart (alias multipart-to-singlepart) recursively splits multipart geometries and collections, retaining Z/M and assigning new sequential feature IDs from 1. Null geometries and empty single geometries are retained; empty multipart geometries yield no features.

vertices (alias extract-vertices) retains Z/M and adds source_fid (text, preserving all 64-bit IDs), part_index, ring_index and vertex_index. Indices start at 0; polygon exteriors are ring 0, while non-polygons have a null ring index. Parts follow recursive traversal order. The repeated closing vertex of each polygon ring is included, including when the format reader omits it internally. Null and empty geometries yield no vertices. Field name collisions are resolved case-insensitively with _2, _3, etc. The result is held in memory, with a separate copy of the source attributes for each vertex.

Vector inputs accept repeated --layer/-l. These commands, footprint and sample accept -f FORMAT, --nln NAME (one output layer), --append and --overwrite. As with convert, the default writes a new dataset, replacing an existing file; --overwrite replaces only the named layers of a GeoPackage, preserving its other layers. Rust operations retain source feature IDs except when generating new features. Output formats can assign new dataset-specific IDs when writing; GeoPackage outputs normally number them from 1.

Vector topology

terrio buffer roads_utm.gpkg buffers.gpkg --distance 100 --quad-segs 30
terrio overlay parcels.gpkg zones.gpkg intersections.gpkg --operation intersection
terrio overlay parcels.gpkg zones.gpkg remainder.gpkg --operation difference
terrio dissolve counties.gpkg states.gpkg --field STATEFP
terrio is-valid polygons.gpkg validity.gpkg
terrio make-valid polygons.gpkg repaired.gpkg
terrio clean roads.gpkg cleaned.gpkg --snap-grid 0.01

These operations use planar XY coordinates in the source CRS. Buffer distances and cleanup spacings are in metres for metre-based CRSs, and degrees for geographic CRSs; reproject with convert --t-srs first when a linear distance is needed. They use pure Rust and require no GDAL or GEOS installation. Inputs and outputs are held in memory. All accept the vector layer selection and output options described above, including append and overwrite.

buffer retains attributes, source IDs and null geometries. Other outputs are XY MultiPolygons with round caps and joins; --quad-segs controls segments per quadrant (1–4096, default 30). Negative distances erode polygons; points and lines then have empty buffers. Non-finite distances and invalid geometries fail; run make-valid first when needed.

overlay accepts Polygon/MultiPolygon features in a common CRS; both inputs may also have unknown CRS. It does not reproject automatically. --operation is intersection (default), union, difference or symmetric-difference (alias xor). --layer selects left layers; --overlay-layer selects the single right layer when its dataset has several. Output fields have a_ and b_ prefixes, followed by text a_source_fid/b_source_fid fields preserving 64-bit source IDs. Fields of an absent side are null. Collisions receive _2, _3, etc. Null and empty geometries contribute no area; touching lines and points are omitted. Results have sequential IDs from 1. Overlaps within an input retain source feature multiplicity; dissolve the input first when disjoint coverage is required.

layer-algebra OPERATION input method output supports intersection, union, sym-difference, identity, update, clip and erase for valid polygon, line, point, multipart and collection inputs in matching CRS. It constructs XY output; Z/M ordinates are dropped. Overlapping polygon components in a collection are queried as their union. Untouched single-part collections keep their outer type. Inputs are materialized. --layer selects input layers; --method-layer chooses one method layer. Existing output requires explicit --overwrite or --append; failed preflight preserves output files.

Intersection, union, symmetric difference and identity include both attribute schemas with default input_ and method_ prefixes. Clip, erase and update use the input schema without a default prefix. --input-prefix and --method-prefix override these defaults. Prefix collisions receive numeric suffixes. Update copies method values whose original names match the output field names; adding an input prefix can therefore leave method values null. Null input geometry is skipped; update also retains typed-empty method geometry. IDs are sequential from one, with source multiplicity retained. The general output geometry type is inferred from results rather than preserving an unknown input declaration.

--keep-lower-dimension-geometries true|false defaults to true. When false, intersection, identity and union omit a pair intersection whose dimension is below the smaller input geometry dimension. A mixed collection is retained when its maximum dimension qualifies. Clip always retains lower-dimensional results; erase and update subtract full method geometry. Symmetric difference subtracts both sides regardless of this option. Individual point cuts do not remove line length; line/line crossings node the remaining linework.

Rust exposes ops::layer_algebra::{layer_algebra, LayerAlgebraOperation, LayerAlgebraOptions}. Python terrio.layer_algebra(src, method, operation, *, layer=None, method_layer=None, keep_lower_dimension_geometries=True, input_prefix=None, method_prefix=None) returns a Layer. JavaScript layerAlgebra(src, method, operation, {layer, methodLayer, keepLowerDimensionGeometries, inputPrefix, methodPrefix}) returns a Dataset. Each binding requires one layer per input unless selected. CLI registration supplies the MCP tool. Field selection, output type forcing and on-the-fly reprojection are outside this operation's option subset; prepare inputs with convert when needed.

dissolve also accepts only valid Polygon/MultiPolygon features. Without --field it merges the whole layer; repeat or comma-separate --field (alias --by) to group by typed attribute values. Null values form a group; non-finite numeric group values are rejected. Only grouping fields and feature_count are retained. Null geometries count toward a group but add no area; an all-null group stays null. Groups follow first-seen order with sequential IDs from 1; an empty layer produces no features. Output topology is XY MultiPolygon.

is-valid (alias validity) appends collision-safe Boolean is_valid and text valid_reason fields, preserving geometry, Z/M and attributes. Null geometry has null status/reason; empty geometry is valid. Rings are logically closed even if their driver omits the repeated closing vertex. Reasons describe the first detected problem; their wording differs from GEOS. Non-finite XY coordinates are reported as invalid.

make-valid preserves already-valid geometry, including Z/M. Invalid polygons use an odd-even triangulated fill, which can change Polygon to MultiPolygon; rebuilt topology is XY. Collapsed area parts are dropped, rather than returned as lines or points; a fully collapsed polygon becomes an empty MultiPolygon. Degenerate lines become empty lines, and collections are repaired recursively. Attributes, null features and source IDs are retained. Non-finite XY or an unsuccessful repair returns an error.

clean removes consecutive XY duplicates, closes rings, drops empty/collapsed parts and repairs invalid polygons. --snap-grid is a finite nonnegative spacing in CRS units: 0 (default) disables snapping, otherwise XY coordinates round to the nearest multiple, with ties away from zero. The first retained duplicate supplies Z/M; polygon repair discards Z/M when it rebuilds topology. A fully collapsed feature becomes null, retaining its attributes and source ID. Non-finite XY and coordinate overflow fail. Output formats may assign their own feature IDs on writing.

Vector geometry commands

terrio simplify counties.gpkg simple.gpkg --tolerance 500
terrio segmentize borders.gpkg dense.gpkg --max-length 0.1
terrio convex-hull parcels.gpkg hulls.gpkg
terrio concave-hull places_by_country.gpkg hulls.gpkg --ratio 0.3 --allow-holes
terrio swap-xy latlon.gpkg lonlat.gpkg
terrio set-geom-type roads.gpkg roads_multi.gpkg --multi --dim XYZ
terrio combine places.gpkg places_by_country.gpkg --group-by adm0_a3
terrio sort buildings.gpkg buildings_sorted.fgb --method hilbert
terrio convert counties.shp simple.gpkg --simplify 0.01

These are pure-Rust equivalents of GDAL 3.13's gdal vector commands of the same names; no GDAL or GEOS installation is needed. Distances are in the input CRS units (degrees for geographic CRSs). All accept the vector layer selection and output options described above. The per-feature commands (simplify, segmentize, convex-hull, concave-hull, swap-xy, set-geom-type) keep attributes, feature IDs and null geometries, and stream a chunk of features at a time when the input and output formats allow it, as convert does; combine and sort read whole layers, as GDAL does. The vector-geom conformance suite (scripts/compare_vector_geom_with_gdal.py) compares them with GDAL and GEOS on real data.

simplify --tolerance ports JTS 1.20.0's TopologyPreservingSimplifier, with adaptations checked against GDAL's GEOS-backed OGRGeometry::SimplifyPreserveTopology: Douglas-Peucker simplification of every line and ring, where a section is flattened only when the new segment crosses no other segment of the feature and moves no other part (a hole, say) to its other side. Rings keep at least four points and their start point may move. Simplification is per feature, so shared boundaries between features may separate. Outputs are vertex-for-vertex identical to gdal vector simplify on the conformance data. Z is kept and M dropped, as GDAL's GEOS conversion does. convert --simplify TOLERANCE (ogr2ogr -simplify) applies the same operation before reprojection and keeps a geometry whose simplification fails.

segmentize --max-length is OGR's segmentize: a segment of length L gets floor(L / max - 0.01) evenly spaced points (none when L is within a relative 1e-5 of the maximum). Lines are processed from the end with the larger X (then Y), so a line and its reverse get the same points, and added points copy the Z/M of the segment start in that direction (they are not interpolated). Ring closing segments are divided too. convert --segmentize is ogr2ogr -segmentize; it cannot be combined with --simplify. Segmentization allows at most ten million output vertices per feature across its lines, rings and collection members. It checks each component against the remaining budget before allocating its output. This limit does not cap a whole layer's total output.

convex-hull ports JTS's Graham scan: a polygon starting at the lowest, then leftmost, vertex, clockwise; a line string for collinear input and a point for one distinct vertex. The layer's type becomes unknown. concave-hull --ratio R (0 most concave, 1 convex) ports JTS's ConcaveHull for points and lines (erosion of the Delaunay triangulation of the vertices, by longest border edge) and ConcaveHullOfPolygons for polygons (erosion of the constrained Delaunay triangulation of the space between them, united with them); --allow-holes allows holes and --tight makes polygon hulls follow the polygons' outer boundaries. The point and constrained polygon triangulations also port JTS, retaining construction and traversal order with compatibility adaptations tested against executable GEOS 3.14.1. Hull regressions include co-circular grids and multipart polygons; tolerances and source revisions are recorded in the provenance record. As GDAL 3.13, polygon and multipolygon layers keep their declared type and hulls are forced to it, which can join several polygons into one invalid polygon in a layer declared POLYGON; other layers' type becomes unknown.

set-geom-type ports OGR's forceTo for linear geometries: --geometry-type TYPE (POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION or GEOMETRY, with an optional Z, M, ZM or 25D suffix), or a combination of --multi/--single, --linear and --dim XY|XYZ|XYM|XYZM (added Z and M values are 0). Conversions that cannot be made leave the geometry type as it is (OGR may still apply the target's dimensions); --skip drops those features. --layer-only changes only the declared layer type, --feature-only only the geometries. terrio has no curve geometries: --curve and curve or surface types are refused, and --linear changes nothing. terrio's writers store no M values (they remain in Python and JavaScript layers), and the declared Z/M of a layer follows its geometries.

combine collects each group's geometries into one feature: without --group-by the whole layer (an empty layer produces no features, as in GDAL), otherwise one feature per combination of the fields' values (repeat or comma-separate). Groups are ordered by their values as OGR formats them as text, nulls as empty text, as GDAL orders them. Output fields are the grouping fields; --add-extra-fields sometimes-identical also copies the fields identical within at least one group (null for the groups where they differ) and always-identical those identical within every group. Layers of single-part types give MultiPoint, MultiLineString or MultiPolygon features, others GeometryCollections; parts of multi-part inputs are added one by one unless --keep-nested adds them whole (the output then becomes a GeometryCollection). A geometry that does not fit the layer's collection type is an error, as in GDAL. Null and empty geometries add nothing (a group of them gets an empty collection); when any geometry has Z, all outputs do (0 where missing). Feature IDs are sequential from 1.

sort --method hilbert (default) orders features by GDAL's Hilbert code of the centre of each bounding box on a 65535 × 65535 grid over the layer's extent; --method strtree by the leaf order of JTS's sort-tile-recursive packing with nodes of 10 (slices by box centre X, each sorted by box centre Y). Null and empty geometries go last. terrio's sorts are stable, GDAL's are not: features whose keys tie may come in another order. Writing a sorted layer to FlatGeobuf re-sorts it for the spatial index.

Not supported: GDAL's --active-layer, --active-geometry and --geometry-field (terrio layers have one geometry), --use-tempfile for sort, and the --skip-errors, --upsert and creation-option flags of gdal vector writes.

footprint

terrio footprint image.tif footprint.gpkg
terrio footprint image.tif footprint.gpkg --band 1,2 --intersection
terrio footprint image.tif footprint.gpkg --mask valid.tif
terrio footprint image.tif footprint.shp --singlepart --attribute survey=2026 --attribute "label=North field"

Writes one multipolygon of all finite, non-nodata pixels, preserving holes, disconnected areas, the full affine geotransform (including rotated/south-up grids) and the source CRS. Boundaries follow exact pixel edges, without overviews or simplification. Pixels connect through their sides (4-connectedness); regions touching only at a corner stay separate polygon members, as in GDAL's default footprint. An entirely invalid raster produces an empty layer. By default, any band may be valid; --intersection requires all selected bands to be valid. --band selects bands from 1; omitted, it uses all bands.

The output includes a text location field containing the input path. Local paths are stored exactly as supplied; URL sources (inputs containing ://, including GDAL virtual-path prefixes) omit user information, query strings and fragments. Use --location-field NAME to rename it or --no-location to omit it. Repeated --attribute NAME=VALUE options add text fields to every feature; values may be empty or contain =. Field names must be nonempty, have no surrounding whitespace, and be unique ignoring case, including the location field. Output format limits still apply (Shapefile field names are limited to 10 characters and text values are truncated to at most 254 bytes, at a UTF-8 boundary); use GeoPackage to retain long names or values.

--singlepart writes one polygon feature per disconnected region, keeping holes, CRS and all attributes on each feature. Without it, the output remains a single multipolygon.

--mask further restricts the footprint to finite, nonzero, non-nodata pixels of the mask's first band; its grid and CRS must match the input. Alpha bands and external mask files are not used as implicit masks. Complex bands and geolocation arrays are unsupported.

sample

terrio sample sites.gpkg samples.gpkg --raster dem.tif --raster rain.tif
terrio sample sites.gpkg samples.gpkg --raster image.tif --band 1,2,3 --prefix image
terrio point-sample sites.gpkg samples.gpkg --raster dem.tif --raster rain.tif \
  --t-srs EPSG:3857 --decimal-places 2

sample (alias point-sample) is the multi-raster point sampling tool. It samples the pixel containing each point, adding a Float field for each raster/band pair. Repeat --raster for multiple rasters; --band selects the same bands (from 1, default 1) from each. Fields are named FILE_STEM_bN, or PREFIX_bN when one --prefix is supplied per raster in the same order. Name collisions get _2, _3, etc.; existing attributes remain intact.

Each raster may have a different resolution, affine grid or CRS. Coordinates are transformed for sampling while the output retains the original point geometries, Z/M, attributes and CRS by default. --t-srs CRS reprojects the output points after sampling; it requires a declared input CRS. Output reprojection failure is an error. --decimal-places N rounds newly sampled values to 0–15 decimal places, with halfway values rounded away from zero. Existing attributes are unchanged. Values remain numeric floats; output formats control trailing-zero display. Rounding uses binary64 scaling, so decimal values very close to a halfway boundary may fall on either side because of floating-point representation. Coordinates retain full precision. Nodata, non-finite pixels, out-of-bounds locations and null geometries yield null values. Points that cannot be projected into a raster's CRS yield null samples for that raster too; the CLI prints one warning per affected output field with the failure count. Errors constructing the CRS transformation still fail the command. Non-point geometries are rejected; use singlepart for multipart points first. Missing CRSs are accepted only when both the points and the raster lack a CRS, using coordinates directly. Complex bands and geolocation arrays are unsupported. Rasters are loaded into memory, and sample values are represented as 64-bit floats (integers above 2⁵³ may lose precision).

contour

terrio contour dem.tif contours.gpkg --interval 10
terrio contour dem.tif contours.gpkg --levels 100,150,200 --3d
terrio contour dem.tif contours.gpkg --interval 20 --offset 5 --levels 64.5

contour (alias contours) extracts line strings with ID and ELEV attributes; --field/-a renames ELEV. Supply a positive --interval/-i, fixed --levels, or both. Interval levels are offset + k * interval; explicit levels are sorted and deduplicated. The elevation field must be nonempty and different from ID. --band/-b selects a band from 1 (default 1). --3d puts the elevation in every vertex's Z coordinate.

Interpolation follows GDAL's marching squares, including its exact-level perturbation, fixed saddle connectivity and nodata-boundary subdivision. Boundaries extend from pixel centres to pixel edges. The source CRS and full affine geotransform are retained; feature ordering and line direction can differ from GDAL. Declared nodata values and non-finite pixels are excluded. --nodata VALUE overrides the nodata value; --ignore-nodata includes declared nodata as data. These two options cannot be combined. Non-finite pixels are always excluded.

The raster and generated segments are held in memory. At most 100,000 levels are allowed. This command creates contour lines; contour polygons are not provided. Complex bands and geolocation arrays are unsupported. Standard vector output options apply.

zonal-stats

terrio zonal-stats counties.gpkg stats.gpkg --raster elevation.tif --raster rain.tif
terrio zonal-stats counties.gpkg stats.gpkg --raster image.tif --band 1,2 --prefix image \
  --stats count,min,max,mean,sum,stddev,median --all-touched
terrio zonal-stats counties.gpkg stats.gpkg --raster dem.tif \
  --t-srs EPSG:3857 --decimal-places 2

zonal-stats (alias zonal) adds statistics independently for each polygon or multipolygon. Repeat --raster for multiple rasters and use --band for comma-separated bands from each. Default statistics are count,min,max,mean,sum,stddev; median is available explicitly. Standard deviation uses the population definition. Fields are FILE_STEM_bN_STATISTIC, or PREFIX_bN_STATISTIC when one --prefix is supplied per raster. Name collisions get _2, _3, etc. count is an integer; the other statistics use 64-bit floats.

By default, a pixel contributes when its centre lies in the zone, following GDAL's rasterization boundary rules. --all-touched includes every touched pixel. Holes are excluded. Overlapping zones are evaluated independently, so a pixel can contribute to multiple zones. Declared nodata and non-finite values are excluded; alpha bands and external masks are not applied implicitly. With no valid contributing pixels, including null/empty geometries and zones outside the raster, count is 0 and every other statistic is null. Source geometry, Z/M, attributes and CRS remain unchanged by default. All non-null geometries must be polygons or multipolygons.

--t-srs CRS reprojects output zones after statistics are computed, so it does not change pixel membership or the analytical grid. The input must declare a CRS, and output reprojection failure is an error. --decimal-places N rounds only newly calculated floating statistics to 0–15 decimal places (halfway values away from zero); integer counts and original attributes are unchanged. This controls numeric values, not coordinate precision or trailing-zero display. Rounding uses the same binary64 scaling as sample; decimal values near halfway boundaries are subject to floating-point representation. For point inputs, use sample/point-sample, which accepts multiple rasters in one command.

Rasters may use different resolutions, CRSs, rotated grids or south-up grids. Boundaries are transformed for each raster; a boundary transformation failure is an error. Missing CRSs are accepted only when both the zones and the raster have no CRS. Reprojection transforms stored vertices without densifying curved edges. The rasters are loaded into memory; masks cover only each zone's intersecting window and are shared by requested bands of the same raster. Median needs extra memory for a zone's contributing values. Integers above 2⁵³ can lose precision. Complex bands and geolocation arrays are unsupported. Standard vector input/output options apply.

sql

terrio sql countries.gpkg selected.gpkg --query \
  "SELECT ADMIN, POP_EST AS population FROM countries WHERE POP_EST > 10000000 ORDER BY POP_EST DESC LIMIT 10"

Runs the read-only OGR SQL subset. FROM must name an input layer, matched case-insensitively; --layer can restrict the available layers. Supports computed columns, aliases, single-field DISTINCT, COUNT/SUM/AVG/MIN/MAX, and attribute LEFT JOIN within the input dataset, plus WHERE, one primary ORDER BY field and LIMIT.

terrio sql countries.gpkg population.gpkg --query \
  "SELECT ADMIN, POP_EST/1000000.0 AS millions FROM countries"
terrio sql countries.gpkg summary.gpkg --query \
  "SELECT COUNT(*) AS countries, SUM(POP_EST) AS population FROM countries"

Computed columns and joins retain primary geometries, CRS and feature IDs. DISTINCT and summaries produce attribute-only results. Output drivers apply their normal file-ID conventions. String predicates (comparisons, IN and BETWEEN) now ignore ASCII case to match current OGRSQL; ORDER BY and MIN/MAX retain case-sensitive ordering. Output field names must be unique, ignoring case. The convert --sql path retains its legacy single-layer fallback for FROM and allows duplicate output field names.

Vector datasets

These commands are GDAL 3.13's gdal vector concat, partition, set-field-type, make-point, create, export-schema, rename-layer, edit and update. Their results are compared with GDAL in the vector-dataset conformance suite (see Testing).

concat

terrio concat countries.gpkg us_counties.gpkg merged.gpkg                  # one layer per layer name
terrio concat a.shp b.shp c.shp all.gpkg --mode single --source-layer-field-name source
terrio concat *.gpkg stacked.gpkg --mode stack --output-layer "{DS_BASENAME}_{LAYER_NAME}"
terrio concat a.gpkg b.geojson out.gpkg --mode single --field-strategy intersection --dst-crs EPSG:3857

--mode merge-per-layer-name (the default) makes one output layer per distinct layer name, stack one per input layer (named by the --output-layer template, {AUTO_NAME} by default: {DS_BASENAME}_{LAYER_NAME}, or the layer name when both are the same), and single one layer (merged by default). Templates also take {DS_NAME}, {DS_INDEX} and {LAYER_INDEX} (from 0). Output layers are ordered by name, as GDAL orders them. The generated source-layer field must have an unused name; collisions with source fields are rejected before creating an output (GDAL's writer may instead rename a colliding field). --field-strategy union (the default) keeps every field, matched by exact name; intersection the fields every layer has, ignoring case. Fields of the same name merge into a type holding both (reals over integers, 64-bit over 32-bit integers, strings otherwise). Union fields such as Name and name remain distinct; formats requiring case-insensitive unique column names may reject that schema (GDAL may rename columns when writing). --source-layer-field-name/--source-layer-field-content add a first field naming each feature's source (source_ds_lyr and {AUTO_NAME} by default). Each output layer has the geometry type and CRS of its first source layer; features of layers in another CRS are reprojected to it, or to --dst-crs. -s/--src-crs overrides the inputs' CRS. -l selects input layers; --append/--overwrite update an existing GeoPackage as elsewhere. GDAL's --upsert, --skip-errors, --if, --oo, --co and --lco are not supported.

partition

terrio partition counties.gpkg out --field STATE                         # out/counties/STATE=TN/part_0000000001.gpkg
terrio partition places.shp out --field country --field city --feature-limit 1000 -f GPKG
terrio partition places.shp flat --field featurecla --scheme flat --pattern "{FIELD_VALUE}_%03d" -f FlatGeobuf
terrio partition big.parquet out --max-file-size 500MB                     # parts of an estimated size

--scheme hive (the default) writes layer/field=value/.../part_%010d.ext directories, with null values as __HIVE_DEFAULT_PARTITION__; flat writes the files in the output directory, named {LAYER_NAME}_{FIELD_VALUE}_%010d (several values joined by _, nulls as __NULL__, empty strings as __EMPTY__). --pattern sets the name, with {LAYER_NAME}, {FIELD_VALUE} and one %d, %5d or %05d part number. Names and values are percent-encoded as GDAL encodes them. Fields must be String or Integer fields; OGR_GEOMETRY partitions on the geometry type (as GDAL 3.13 does). --feature-limit and --max-file-size split partitions into numbered parts; the size estimate begins at 64 KiB plus the bytes each feature adds, and the minimum size limit is 1 MiB. Without --field, they split the layer alone (GDAL 3.13). --omit-partitioned-field leaves the partitioning fields out (automatic for Hive GeoParquet). The format defaults to the input's; files take GDAL's extension of the format (.json for GeoJSON). An existing output is refused unless --overwrite (only when it looks like a partitioned dataset) or --append (new parts are numbered after the existing ones; GDAL also fills the last existing part while it is below the limits) is given. GDAL's --skip-errors, --co and --lco are not supported, nor GDAL's Parquet _metadata file.

Partition output directories carry a .terrio-partition ownership marker. Overwrite validates options and every descendant entry before removal, refuses a directory containing the path input, and requires a matching marker; unmarked directories must be removed explicitly. Flat output accepts only the selected format's files and sidecars. Empty and dot-only names are escaped. The Rust partition_to_dir API receives materialized layers, so callers must keep any source files outside an overwrite target; path-based CLI/Python calls check that relation. Partition and concat currently materialize feature copies; large datasets can require roughly input plus output memory.

set-field-type

terrio set-field-type countries.gpkg out.gpkg --field-name POP_EST --dst-field-type Integer64
terrio set-field-type countries.gpkg out.gpkg --src-field-type Real --dst-field-type String
terrio set-field-type places.csv out.gpkg --field-name visited --dst-field-type Date --active-layer places

--dst-field-type (alias --field-type) is a type (Integer, Integer64, Real, String, Date, Time, DateTime, Binary) or a subtype (Boolean, Int16, Float32, JSON, UUID) implying its type; --field-name names the field, or --src-field-type changes every field of a type. Values are converted as GDAL converts them: strings by their numeric prefix ("12abc" is 12, "abc" 0, with warnings), reals truncated and clamped to 32 or 64 bits, 64-bit integers clamped to 32 bits, booleans from true/yes/on/numbers, dates and date-times reformatted (dates become YYYY/MM/DD strings), text parsed as dates or times. Features whose value cannot be converted (a date or blob to a number, a date to a blob) are left out, with a warning, as in GDAL. Widths and precisions are kept. terrio's data model has no separate Int16, Float32 or UUID storage: Int16 values are clamped to 16 bits in 64-bit integers, Float32 values are doubles, UUIDs text, times text (HH:MM:SS[.sss]), and an Integer field changed to Boolean holds 0 or 1 where GDAL keeps the integer. List types are not supported.

Numeric-prefix conversion of unusually long text values currently involves repeated parsing and can be expensive.

make-point

terrio make-point trace.csv points.gpkg --x longitude --y latitude --dst-crs EPSG:4326
terrio make-point stations.gpkg points.gpkg --x easting --y northing --z elevation -l stations

Replaces the geometries of the first layer (or -l) with points read from numeric or text fields (text must hold a number, surrounding spaces allowed; null numbers are 0, as in GDAL), keeping every field. --z and --m add Z and M. --dst-crs assigns a CRS without reprojecting; without it the points have none, as in GDAL. Unlike GDAL, Integer64 fields are accepted. terrio's GeoPackage writer writes XY and XYZ geometries, so M values are kept in memory (Rust, Python and JavaScript) but not in GeoPackages.

Text coordinates accept NaN and infinity as GDAL 3.13.3 does; use finite coordinates for valid extents and spatial indexes. GDAL can report an extent-update error for infinite GeoPackage coordinates even while its command exits successfully.

create

terrio create sites.gpkg --output-layer sites --geometry-type POINT --crs EPSG:4326 \
  --field "name:STRING(80)" --field visits:INTEGER --field total:INTEGER64 --fid site_id
terrio create empty.gpkg --like countries.gpkg --layer countries --output-layer countries_new
terrio create copy.gpkg --schema countries.schema.json
terrio create sites.gpkg --update --output-layer more --geometry-type LINESTRING --crs EPSG:4326

Creates a dataset of empty layers from an OGR_SCHEMA document (--schema, a file or JSON text, such as terrio export-schema writes), from the layers of a template dataset (--like, -l choosing layers), or from --geometry-type, --crs, --field NAME:TYPE[(WIDTH[,PRECISION])] and --fid. --output-layer names a single layer. --update adds the layers to an existing GeoPackage (a layer of the same name is refused); --overwrite replaces an existing dataset. --fid names the FID column of new GeoPackages. Field types are mapped onto terrio's (see set-field-type); 32-bit INTEGER(10) fields keep no width of 10 or more, which marks 64-bit integers. Z and M suffixes of geometry types are accepted but not recorded in empty layers (a warning says so). Curve, surface, TIN and list types, --geometry-field and GDAL's --oo are not supported.

Vector schema/template creation orders fields by name, matching GDAL 3.13; explicit --field arguments keep their order.

export-schema

terrio export-schema countries.gpkg
terrio export-schema natural_earth.gpkg --layer ne_50m_admin_0_countries -o countries.json

Prints the dataset's OGR_SCHEMA (GDAL's ogr/data/ogr_fields_override.schema.json), as gdal vector export-schema does: per layer its name, "schemaType": "Full", geometry fields (name, camel-case type with Z/M, nullability, coordinateSystem as {"authid": "EPSG:4326"}, or {"wkt": ...} without an authority code, null without a CRS), the FID column and the fields (name, type, subtype, width, precision, nullability, unique constraint, default). Local GeoPackages are described from their table declarations (exact column types, NOT NULL, UNIQUE and defaults); other formats from the layers read (integer fields are Integer64 when 10 or more digits wide or holding 64-bit values, as GDAL's DBF reader and GeoJSON reader type them).

rename-layer

terrio rename-layer data.gpkg --input-layer roads --output-layer transport
terrio rename-layer source.gpkg renamed.gpkg --ascii --lower-case

Renames a chosen layer or normalizes every layer name. --ascii, --lower-case, --filename-compatible, --reserved-characters, --replacement-character and --max-length control normalization; colliding results receive unique suffixes. A selected rename retains the other input layers; name collisions are rejected clearly. GDAL's CLI --input-layer selection can instead produce only the selected layer. Without an output, local GeoPackages are changed in place, including their indexes, triggers and metadata references. Other formats require an output.

edit

terrio edit data.gpkg --crs EPSG:4326 --metadata SOURCE=survey
terrio edit data.gpkg edited.gpkg --active-layer roads --geometry-type LINESTRING

Assigns CRS without reprojecting, changes declared geometry type (including Z/M), or sets/removes dataset and layer metadata. Metadata edits and Z/M declarations require GeoPackage outputs; other output formats reject those options. --active-layer selects one layer, matching exact names first and otherwise ignoring ASCII case. Default CRS and geometry edits skip attribute-only tables. --crs none removes a CRS. --unset-fid requires an output. Geometries are not converted by a declaration change. An output copy retains default-domain GeoPackage metadata; named domains are not copied, matching GDAL 3.13.3.

In-place GeoPackage edits materialize the database and replace it with a unique temporary file beside the resolved input. Mode bits and symlink targets are preserved; hard links are detached, and ownership, ACLs and extended attributes are not preserved. Exclude concurrent writers: journal/WAL checks are not transaction locks. Atomic replacement is not a power-loss durability guarantee. Output creation and metadata application are separate steps, so a failed output edit can leave a file.

update

terrio update incoming.gpkg existing.gpkg --key code --mode merge

Matches features by FID, or by repeated --key fields of matching types. --input-layer and --output-layer select layers. merge updates matched features and appends unmatched ones; update-only and append-only restrict that behavior. NULL keys match nothing, so merge and append-only append them, while update-only skips them. A key matching several output features matches none. Shared fields and geometry are updated without reprojection; output-only fields are retained and input-only fields are not added.

GeoPackages use the in-place mechanism above. Other formats stage new files then replace the originals. Multi-file replacements are not atomic across sidecars and a failure may leave a partial replacement. Successful Shapefile replacements remove obsolete QIX/SBN/SBX indexes and optional PRJ/CPG sidecars not produced by the new write.

dataset-copy, dataset-rename, dataset-delete, dataset-files

terrio dataset-files scene.tif --json
terrio dataset-copy scene.tif backup/scene.tif
terrio dataset-rename countries.shp countries-new.shp
terrio dataset-delete obsolete.tif obsolete.shp

These commands manage encoded datasets and their companion files without conversion. The destination is an exact dataset name. Existing destinations and companion collisions require --overwrite; replacing a dataset removes obsolete companions. --input-format (alias --format) restricts the source driver. --json returns the driver, affected files and copied bytes. dataset-files lists owned files and a directory flag.

Shapefile and MapInfo components, raw raster headers, world files, RPCs, PAM metadata, overviews and masks are included when present. Raster-only companions are not assigned to vector files. Zarr, Arc/Info grids and vector directory datasets (including whole MVT pyramids) are handled as trees. A recognized Zarr or Arc/Info grid directory owns its complete subtree: copying, renaming or deleting it includes every entry, even user-added files. Store unrelated files outside these dataset roots. A Shapefile, MapInfo or CSV directory is accepted only when every entry belongs to that driver's datasets; directories containing unrelated files or subdirectories are refused. Select individual dataset files in a mixed directory. Unrelated siblings of individual dataset files are retained. TIFF world files are used only without an embedded transform. CSV .prj files are preserved: GDAL 3.13.3 uses these CRSs but omits them from GetFileList and loses them during dataset copy. Basename companions such as .prj and .hdr can be shared by distinct recognized primary files. Rename, deletion and replacement refuse such a companion while another same-basename owner remains. A batch deletion may include all owners; copying may read a shared companion without changing its original.

Directory recognition parses complete Zarr marker and MVT metadata JSON, checking fields at the top level rather than matching text in a fixed prefix. Unknown fields are skipped while streaming. Large metadata files are read in full; tile payloads are not decoded for inventory.

VRT copies and renames copy referenced assets with regular corresponding basenames, confined by default to the original source directory tree. Native links and all entries of referenced directory datasets are checked before mutation. Remote URL paths with percent-encoded slashes are refused by default: server handling of those bytes cannot establish directory confinement. Trusted callers can use the same explicit external-reference opt-in for such paths. Non-raw references must identify a supported dataset; arbitrary files are accepted only as explicit raw-band data. Virtual keys with dot segments are refused by default. Use --allow-external-references only for trusted VRTs that need assets outside that tree, such as a local VRT referencing an HTTP raster. The same option is allow_external_references in Rust/Python and allowExternalReferences in JavaScript. MCP exposes the same opt-in: a client enabling it is explicitly trusting the VRT references and permitting access beyond the source tree. MCP clients must not enable it for untrusted VRTs. Source references are rewritten to the new names. Rename moves the owned definition but preserves original dependencies so other datasets can keep using them; copied payloads count toward reported bytes, including for same-filesystem VRT renames. VRT/GML bytes are preserved when no reference needs rebasing. GML schema locations and Idrisi/ERS header references are also updated. Text header rewrites preserve opaque non-reference bytes and CRLF/LF line endings; file-reference values themselves must be UTF-8. Irregular basenames are refused before mutation, as in GDAL. VRT graph traversal reuses completed inventories and permits at most 128 nested definitions; definitions are held in memory. GML definitions are also read and parsed in full when preparing a transfer, even when no reference changes. Original bytes, decoded text and a serialized output buffer can coexist, so peak memory can be several times the definition size and can expand with text encoding; there is no fixed memory bound for these definitions. Shared-companion safety checks scan the parent directory for each input with companions; large batches in one directory can incur quadratic preflight work. A file used both as file-bearing metadata and raw VRT pixels is refused if rewriting would change those pixels. VRT deletion removes the definition and its owned companions, retaining referenced rasters. VRT source filenames preserve trailing whitespace and leading whitespace encoded with character references or CDATA, matching GDAL. Rewritten references encode leading whitespace so it remains part of the filename. Native raster paths also retain edge whitespace rather than silently opening a different file. Obsolete companion links, including dangling links, are staged and unlinked during overwrite without following them; destinations receiving new content continue to refuse symbolic links.

Dataset file management refuses MVT trees containing entries outside metadata.json and numeric z/x/y.pbf or z/x/y.mvt tile paths. Relative GML schema references must belong to the inventoried file set so they can be rebased; external absolute URIs remain unchanged.

Windows native destinations with case-equivalent names are refused during preflight. Installation also checks actual destination entries on every platform, rolling back if the filesystem aliases two components instead of silently replacing one.

All inputs and destinations are checked before staging files. Source aliases, overlapping directory trees, protected roots, read-only mutations and file/directory collisions are refused. Failed staging or installation attempts restore the original paths; failed rollback returns an error identifying recovery files. After installation commits, cleanup failures return success with leftover staged paths in recovery_files. A failed stat skips deletion and reports that path, since guessing whether a cloud object is a directory could delete a broader prefix. The CLI reports these leftovers on stderr as well as in JSON. An empty list means cleanup completed. Batch deletion includes the shared cleanup list in each report. An existing destination must have a successful inventory, including with overwrite; an unknown or corrupt destination is refused. Source and destination trees must not be changed concurrently. Native renames preserve symlink text, so relative links must remain meaningful at their destination; links cannot be renamed across filesystems. Overlapping input directory/component sets are refused because they cannot be staged independently. Multiple file or object operations do not provide a filesystem-wide or object-store transaction.

Local and VSI locations follow terrio vsi: memory and writable cloud stores support mutation; HTTP and archive members are copy sources only. WebAssembly operates on /vsimem/ datasets and archive members, with no native files or signed cloud writes. Owned-file queries and deletion do not open VRT references, which may be missing. Archive TIFF tag discovery uses a 16 KiB member cache. Stored ZIP members are read by range; compressed members stream and discard preceding bytes when seeking. Partial metadata inventories do not validate the whole-member CRC; complete copies still use the normal archive extractor and its integrity checks. MCP exposes all four commands; only dataset-files is marked read-only. JSON and text reports redact URL credentials. Byte counts exclude backup/rollback work; native same-filesystem and memory file moves copy no payload bytes.

Destructive operations refuse a same-stem sibling whose inventory fails: its possible companion ownership cannot be established safely. Repair or move that sibling first.

identify

terrio identify scene.tif countries.fgb --json
terrio identify ./data --recursive --report-failures --json
terrio identify raw.dat --deep

Identifies formats using at most the first 16 KiB of each file, without decoding pixels or features. Local files, individual HTTP/cloud objects and local Zarr or Arc/Info grid directories are supported. HTTP sends one prefix GET plus any redirect or retry GETs, and stops reading after 16 KiB even when the server ignores Range; transport buffering can receive additional bytes. Identification is not an integrity check: a damaged file can retain a recognizable signature. Only formats enabled in the build are reported.

Inputs retain their order; directory children are sorted. --recursive descends into local directories and stops at recognized dataset directories. --force-recursive also scans inside recognized directories. Nested directory symlinks are skipped. Remote directory traversal is not supported. --report-failures includes unknown formats and I/O errors; otherwise they are omitted. These reported failures do not change the command's success exit status, matching GDAL dataset identification.

--deep permits existing raster/vector readers as a fallback for formats requiring companion files, such as an ENVI data file. The report marks those results with deep: true. A fallback reader can materialize formats without lazy metadata support. Archives require this fallback, except .kmz files: the ZIP header alone does not identify KMZ; that case also requires the filename hint. Signatures beyond the prefix can be missed. SQLite containers use .gpkg/.mbtiles filename hints as GDAL does, including SQLite files whose application ID is unset; this does not validate their tables. Text formats may need filename hints; raw SRTM HGT tiles require their tile filename and total size. Generic format names (JP2, KML, HDF5) can differ from GDAL's installed driver implementation names.

JSON is an array of records with name, driver, kinds, multidimensional and deep; failures have name and error instead of driver metadata. kinds describes the formats supported by a container, rather than verifying which tables it holds. URL credential parameters are redacted from display names. MCP exposes identify as a read-only tool with the same options. Recursive scans enumerate the requested local tree without an entry or depth cap; scope the input directory accordingly.

Python exposes terrio.identify(path_or_paths, recursive=False, force_recursive=False, deep=False, report_failures=False). Uploaded immutable bytes accept filename=... instead of native scan options. Python always returns a list of named records, including for a single upload. JavaScript exposes identify(uint8Array, filename?) or identify({bytes, filename}), returning format metadata or null. Only the first 16 KiB of JavaScript input is copied into WASM. Uploaded bytes do not use the deeper native reader fallback.

check

terrio check scene.tif --json
terrio check scene.tif --overviews --masks --json
terrio check countries.gpkg
terrio check scientific.nc

Reads every exposed base-resolution raster band, vector layer or numeric scientific array. Successful checks exit with status 0 and report content counts; reader errors exit with status 1. JSON reports contain raster_samples, raster_windows, mask_samples, layers, features, arrays, array_elements and points. Errors use the usual CLI diagnostic instead of a success report. MCP exposes check as a read-only tool.

The default matches gdal dataset check's base-band scope. --overviews also reads the exposed raster pyramid; --masks reads per-dataset masks at every checked level. These options do not request generated alpha/nodata masks. GeoPackage roots include every exposed feature layer and raster table; explicit subdataset names check the selected raster. Scientific roots include coordinates, scalar arrays and nested groups; unsupported array types fail explicitly. Point-cloud builds also read all exposed point records.

Lazy raster readers use two-dimensional value windows of about 8 MiB, aligned to source blocks when they fit. The window planner uses the base level block shape for overviews too; an overview with a different block shape can decode a tile more than once. Very wide images do not require a whole tile row in memory. Requested masks add up to one byte per window pixel on top of the value window budget, plus any codec working buffers. A compressed block larger than this budget can be decoded once per window that intersects it: a 16-row strip that only fits eight rows per window can require two decodes. Scientific arrays use their storage chunk shape when its decoded values fit the same budget; unknown or oversized chunks use bounded hyperslabs and may also be decoded repeatedly. Stream-capable vector readers use batches. Input storage, codec working buffers and readers without lazy support retain their existing memory behavior. Checking all content can require reading the entire source, including remote data. Identification alone remains a separate bounded-header operation.

A successful check establishes that the reader reported no errors, not that the file matches its original bytes. Valid-valued mutations and corruption tolerated by a driver can pass. GDAL 3.13.3 and Terrio both accept the malformed GeoPackage geometry-blob fixture in the conformance suite; the damaged compressed TIFF and truncated GeoJSON fixtures fail in both.

Python exposes terrio.check_dataset(path_or_bytes, filename=None, overviews=False, masks=False), returning counts or raising a reader exception. filename is an upload hint only. JavaScript exposes checkDataset(uint8Array, filename, {overviews, masks}); it owns the full upload for decoding. Native scientific handles also expose MultidimDataset.check() in Python and JavaScript, including JavaScript Zarr stores opened in memory.

info

terrio info INPUT [--json] [--stats] [--hist] [-l LAYER]... [--oo KEY=VALUE]...

Prints the driver, size, bands, data types, nodata, geotransform, CRS and extent of a raster, or the layers, geometry types, feature counts, CRS, extent and fields of a vector dataset. --stats computes band minimum, maximum, mean and standard deviation over valid pixels. Remote GeoTIFFs are summarized from their header alone, and their overviews are listed. The subdatasets of netCDF, HDF5 and GeoPackage files are listed as gdalinfo lists them (SUBDATASET_N_NAME and SUBDATASET_N_DESC).

--oo KEY=VALUE opens a vector dataset with an open option, as convert --oo does: for example the HEC-RAS options that join stored results to a layer (terrio info plan.p03.hdf -l mesh_cells --oo "CELL_SUMMARIES=Maximum Water Surface"; see HEC-RAS).

--hist computes each band's histogram as gdalinfo -hist does, and prints it as it does (256 buckets from -0.5 to 255.5: and the counts; a histogram object of count, min, max and buckets in the JSON): 256 buckets over -0.5 to 255.5 for Byte bands and -128.5 to 127.5 for Int8 bands; for the other types over GDAL's approximate minimum and maximum (statistics stored in the file's metadata or .aux.xml sidecar, the format's own range, the smallest overview of more than 2500 pixels, or a sample of the blocks), widened by half a bucket on each side; with --stats, over the exact minimum and maximum. Nodata and NaN pixels are not counted, values outside the range fall in the first or last bucket, and a histogram GDAL stored in the .aux.xml sidecar is used as it is. The results are identical to gdalinfo -hist's for GeoTIFFs (strips, tiles, overviews) and the formats GDAL reads by scanline.

convert

terrio convert INPUT OUTPUT [-f FORMAT] [options]
Option Applies to Meaning
-f, --format, --of both Output driver short name (GTiff, COG, GPKG, ESRI Shapefile...)
--t-srs CRS both Reproject
--compress, --tile-size, --bigtiff rasters GeoTIFF/COG creation options
--srcwin X Y W H, --overview N rasters Read a window or an overview level
-l, --layer vectors Only these layers
--where EXPR vectors OGR SQL attribute filter
--spat XMIN YMIN XMAX YMAX, --spat-srs vectors Spatial filter
--select FIELDS, --limit N vectors Fields to keep, number of features
--sql STATEMENT vectors SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT
--nln NAME, --nlt TYPE vectors New layer name, geometry type (PROMOTE_TO_MULTI...)
--explodecollections vectors One feature per part
--s-srs, --a-srs vectors Override or assign the CRS
--append, --overwrite vectors, rasters (--append) Add to or replace layers of an existing GeoPackage or vector tile archive; add a raster to a PMTiles or MBTiles archive of image tiles
--oo, --lco KEY=VALUE vectors Open and layer creation options (RFC7946=YES...)
--co KEY=VALUE rasters Creation options (Zarr; netCDF: FORMAT=NC4, COMPRESS=DEFLATE, ZLEVEL, CHUNKING; PMTiles and MBTiles: TILE_FORMAT=WEBP...)
--minzoom N, --maxzoom N both Zoom levels of PMTiles, MBTiles and MVT outputs

GeoTIFF/COG WebP output accepts --compress WEBP --co WEBP_LOSSLESS=YES or --co WEBP_LEVEL=0..100 for lossy quality. Supported inputs are Byte RGB/RGBA; alpha is retained losslessly. QUALITY aliases WebP quality (0–100) or JPEG quality (1–100). JPEG_QUALITY always accepts 1–100. For compatibility, quality keys are validated even when an unrelated codec ignores them. Invalid WEBP_LOSSLESS values are rejected; accepted booleans are YES/NO, TRUE/FALSE, ON/OFF, and 1/0.

Tile archives (PMTiles, MBTiles, MVT) take --oo ZOOM_LEVEL=N and --oo CLIP=NO as inputs; see Formats. HEC-RAS geometry and plan files take the HECRAS options (CELL_SUMMARIES, CELL_SERIES with TIME_STEP, XS_RESULTS, ...; see HEC-RAS):

terrio convert BaldEagleDamBrk.p03.hdf cells.gpkg -l mesh_cells \
  --oo "CELL_SUMMARIES=Maximum Water Surface" --oo "CELL_SERIES=Water Surface" --oo TIME_STEP=12

Point clouds (LAS, LAZ, COPC, EPT, PLY, E57) convert to LAS, LAZ, COPC, PLY, E57, an EPT dataset (an output named ept.json, --ept-data-type laszip|binary|zstandard) or a vector layer of points, with --bounds, --z-range, --class, --returns, --max-level, --resolution, --max-points, --t-srs, --a-srs, --point-format, --las-version, --scale, --offset and --dims; see Point Clouds.

Vector layers convert as streams, a chunk of features at a time, when the input is a local GeoPackage, FlatGeobuf, GeoParquet, Shapefile, GeoJSON, GeoJSONSeq, CSV or OpenStreetMap PBF file, the output a GeoPackage, FlatGeobuf, GeoParquet, Shapefile, GeoJSON, GeoJSONSeq or CSV file other than the input, neither --sql, --append nor --overwrite is given, and --lco gives only CSV and GeoJSON options: memory stays bounded whatever the size of the layer (a 15-million-polygon, 3.8 GB GeoPackage converts to GeoParquet in 192 MB, as ogr2ogr does in 169 MB). Several layers go to a GeoPackage one after another; the other formats hold one layer, chosen with -l. OpenStreetMap PBF files stream when sorted by type then id, as osmium, Geofabrik and the planet files are: only the nodes (about 6 bytes each) and the ways relations use are kept (Colorado's 385 MB extract converts to a GeoPackage in 19 s and 1.5 GB, where ogr2ogr takes 44 s and 0.45 GB). The other conversions read the layers whole. See Formats.

The --where language supports comparisons, AND/OR/NOT, parentheses, IN, LIKE/ILIKE, IS [NOT] NULL, BETWEEN, arithmetic, || and FID, with OGR SQL's null semantics.

translate

terrio translate INPUT OUTPUT [options]

--srcwin, --projwin (+ --projwin-srs), -b (repeatable), --ot, --scale [SRC_MIN SRC_MAX [DST_MIN DST_MAX]], --exponent, --outsize W H (pixels or %), --tr, -r, --a-nodata, --a-srs, --a-ullr, and the output format options. Type conversion rounds and clamps as GDAL does.

warp

terrio warp INPUT... OUTPUT [options]

--t-srs, --s-srs, --tr, --te (+ --te-srs), --ts, --tap, -r (near, bilinear, cubic, cubicspline, lanczos, average, rms, mode, min, max, med, q1, q3, sum), --srcnodata, --dstnodata, --ot, --cutline (+ --crop-to-cutline), --et, --order, --tps, --rpc, --to and --wo. Several inputs are mosaicked into one output. The default output grid follows GDAL's GDALSuggestedWarpOutput2; --dst-transform X0 A B Y0 D E with --ts sets any affine output grid instead, rotated ones included (GDAL's warper API, which gdalwarp does not expose). Rotated and south-up inputs are warped through their geotransform; a GeoTIFF georeferenced by GCPs is warped with GDAL's GCP transformers: a homography from 4 or 5 GCPs, otherwise a polynomial (of order 2 from 6 GCPs, else 1, or --order 1|2|3), or thin plate splines (--tps), giving gdalwarp's pixels.

Images with RPCs (the GeoTIFF RPC tag, or a _RPC.TXT or .RPB sidecar) are warped with GDAL's RPC transformer when they have neither a geotransform nor GCPs, or with --rpc (gdalwarp -rpc). --to KEY=VALUE passes its options as gdalwarp -to does: RPC_HEIGHT, RPC_HEIGHT_SCALE, RPC_DEM (a DEM giving the ground height, read whole), RPC_DEMINTERPOLATION (near, bilinear, cubic), RPC_DEM_MISSING_VALUE, RPC_DEM_SRS, RPC_DEM_APPLY_VDATUM_SHIFT, RPC_PIXEL_ERROR_THRESHOLD and RPC_MAX_ITERATIONS. As gdalwarp for a source with RPCs, 5 more source pixels are read around each chunk, an RPC DEM makes the transformations exact (--et 0) and samples every edge point, and a GeoTIFF output is warped only where the source's footprint falls. --wo SOURCE_EXTRA=n, --wo SAMPLE_STEPS=ALL and --wo SKIP_NOSOURCE=NO set those warp options (other warp options are ignored, with a warning). On ICEYE images with a Copernicus DEM, the output grids and pixels follow gdalwarp. --overview (--ovr) selects AUTO (the default), AUTO-n (n levels finer), NONE (full resolution), or a stored level numbered from 0. The target grid is always planned from full-resolution metadata before choosing source overviews. Downsampling remote COGs therefore fetches coarse tiles instead of full-resolution pixels. An unavailable explicit level falls back to the last available level, as GDAL does.

terrio warp scene.tif scene_4326.tif --rpc --to RPC_HEIGHT=120
terrio warp scene.tif scene_utm.tif --rpc --to RPC_DEM=dem.tif --t-srs EPSG:32720 -r bilinear

Warp source overviews

terrio warp large-cog.tif preview.tif --ts 256 256 --overview AUTO
terrio warp large-cog.tif finer.tif --ts 256 256 --overview AUTO-1
terrio warp large-cog.tif full.tif --ts 256 256 --overview NONE
terrio warp large-cog.tif level.tif --overview 1

Overview selection changes which stored pixels are resampled; the final grid remains based on the full-resolution source. Use NONE when the source pyramids were built with a filter unsuitable for the analysis. Mosaics select a level independently for each source. GCP/RPC transformations retain their full-resolution coordinate meaning.

Forced GCP overviews can give slightly different automatically suggested bounds than GDAL, which refits GCPs at the selected resolution. Specify --te and --ts for an identical target grid; Terrio keeps its full-resolution grid while selecting source pixels.

Swaths from netCDF and HDF-EOS use their longitude/latitude geolocation arrays automatically when no GCP/RPC method is supplied. --geoloc (or --to METHOD=GEOLOC_ARRAY) requires arrays on every source. Pixel/line offsets, separate sampling steps and RFC 4's PIXEL_CENTER or TOP_LEFT_CORNER conventions are honored. The center shift is half a geolocation-array step, including subsampled arrays. Coordinates interpolate bilinearly; an indexed inverse maps output pixels through the same resampling kernels as other warps.

terrio warp 'NETCDF:"swath.nc":/PRODUCT/qa_value' swath.tif --geoloc --t-srs EPSG:4326 --tr 0.05 0.05

Coordinate nodata, NaNs, infinities and invalid geographic latitudes invalidate the neighboring cells; no interpolation bridges those holes. Adjacent geographic longitudes are unwrapped across the antimeridian; use canonical longitude extents on either side for GDAL comparisons. Arrays must have matching dimensions of at least 2 by 2 and positive steps. Cutlines with geolocation arrays are currently unsupported. Automatic selection preserves an existing affine cutline workflow when the dataset also has a usable affine transform; explicitly selecting geolocation arrays still rejects cutlines. Coordinate arrays and their cell index are held in memory. The index stores only cell envelopes and array positions; its size is linear in the number of valid cells, in addition to the two coordinate arrays. The inverse solves bilinear cells directly; GDAL's inverse backmap can differ by a few boundary pixels, particularly with sparse arrays or invalid nodes. The conformance suite reports those mask differences explicitly. Specify --te and --ts to compare identical grids.

mosaic

terrio mosaic INPUT... OUTPUT [options]
terrio mosaic @filelist.txt OUTPUT [options]    # inputs listed in a file, one per line

--resolution highest|lowest|average|user, --tr, --te, --tap, --separate, -b, --nodata, --srcnodata, --dstnodata, -r, --t-srs. A .vrt output writes a GDAL VRT that references the inputs; other outputs hold the mosaicked pixels.

rasterize

terrio rasterize INPUT OUTPUT (--burn VALUE | -a FIELD) (--tr X Y | --ts W H | --like RASTER) [options]

-l, --sql, --where, --te, --tap, --ot, --init, --a-nodata, --at (all touched), --add, --a-srs. Points, lines and polygons are burned with GDAL's algorithms, pixel for pixel.

polygonize

terrio polygonize INPUT OUTPUT [options]

-b, --field NAME (default DN), --nln, --8 (8-connectedness), --nomask, --mask RASTER, -f, --append, --overwrite. Polygons have holes; exteriors are counter-clockwise and holes clockwise.

dem

terrio dem MODE INPUT [COLOR_FILE] OUTPUT [options]

MODE is hillshade, slope, aspect, color-relief (which takes a color file), TRI, TPI or roughness. Options: -b, --compute-edges, --alg Horn|ZevenbergenThorne (hillshade, slope, aspect) or --alg Riley|Wilson (TRI); for hillshade -z, -s (111120 for a geographic DEM in metres), --xscale/--yscale, --az, --alt, --combined, --multidirectional, --igor; -p (slope in percent); --trigonometric, --zero-for-flat (aspect); --alpha, --exact-color-entry, --nearest-color-entry (color relief). Without -s, geographic DEMs are scaled from the ellipsoid at their centre latitude, as gdaldem does since GDAL 3.11. Color files are GDAL's: value R G B [A] or value colorname [A], where the value may be nv (the nodata value) or a percentage of the band's range; GMT .cpt files work too.

The DEM is read and the output written a strip of rows at a time, so neither is held whole (for a GeoTIFF or COG output; other formats gather the output). --max-memory MIB sets about how much memory the strips in flight may take, and so their height, like gdalwarp -wm; by default, strips hold about 64 MiB of input values (for a Float32 DEM, about 300 to 550 MiB in flight). The values do not depend on it. Below one row of the input's blocks, each strip decodes the blocks it touches again: slower, with the same values. Percentages in a color file are of the band's range from the .aux.xml statistics when there are some, otherwise from a first pass over the band, a strip at a time within the same budget (so the DEM is read twice). An output that is also the input is read whole first. --max-memory is refused where it cannot apply: for a VRT output and for an output that is also the input.

--kernel-version 3.12|3.13 selects terrain arithmetic (default 3.12). The 3.13 profile supports slope and standard hillshade, including both gradient algorithms. Other products and hillshade variants require 3.12. Use the version matching the GDAL reference; transcendental and SIMD rounding can vary by platform.

A color relief written as a VRT (-f VRT, --of VRT, or a .vrt output) is a virtual raster of lookup tables over the DEM, as gdaldem color-relief -of VRT writes it: three Byte bands (four with --alpha), each a ComplexSource of the DEM's band with a LUT built from the color file (the midpoints between entries with --nearest-color-entry, zero around them with --exact-color-entry). The lookup tables are those gdaldem writes, so GDAL reads the same pixels from both, and terrio reads such VRTs (gdaldem's too) as GDAL reads them.

pansharpen

terrio pansharpen pan.tif rgb.tif sharpened.tif
terrio pansharpen pan.tif red.tif green.tif blue.tif sharpened.tif --weight 0.6 --weight 0.3 --weight 0.1
terrio pansharpen pan.tif rgbnir.tif rgb.tif --band 1 --band 2 --band 3 --bitdepth 12 --nodata 0

GDAL-compatible weighted Brovey: spectral * pan / sum(weight * spectral) at the pan resolution. Inputs must have the same CRS (or all omit it) and north-up geotransforms. Spectral inputs may be multiband files or PATH,band=N; --band selects/reorders outputs in their flattened input order. Every spectral input contributes intensity, including unselected output bands. Weights default to equal weights and are used as provided, without normalization.

--resampling selects nearest, bilinear, cubic (default), cubicspline, lanczos or average. Direct reads resample in the spectral native type; temporary upsampling first converts spectral samples to the pan working type. Brovey works in the pan type for Byte/UInt16 and Float64 otherwise. Intermediate integer results are rounded/clipped even with --ot Float64. --bitdepth clamps Byte/UInt16 pan samples/results to 2^N-1; other pan types ignore it, as in GDAL. NBITS metadata is not inferred.

--spat-adjust union (default) pads unequal footprints with zero at their native resolutions, intersection crops them, and none preserves the pan footprint. Extents align to the pan pixel grid with GDAL's one-spectral-pixel tolerance. --nodata VALUE sets common nodata for resampling and sharpening; none disables common nodata processing. By default GDAL 3.12's common value is inferred from the selected pan band; spectral declarations do not override it. When common nodata processing is disabled, GDAL's temporary upsampling buffer treats numeric source nodata as ordinary sample values, and Terrio follows that behavior. Like GDAL, footprint padding/cropping drops the source nodata declaration; use an explicit --nodata to preserve common nodata processing with adjusted extents. A nodata contributor or zero weighted intensity produces nodata in every output band when enabled; otherwise zero intensity produces zero. Path and virtual reads follow GDAL's mask behavior: direct resampling honors explicit dataset masks; temporary upsampling buffers, footprint-adjustment VRTs and common-nodata override wrappers drop those masks. Nearest sampling copies pixels without applying masks. Zero padding and nodata are separate: use --nodata 0 when padded margins should be missing. An output ending in .vrt (or -f VRT) writes a GDAL-compatible VRTPansharpenedDataset, opening only source headers. Sources are stored as absolute paths or URLs; moving the inputs requires updating those references. Declared VRT block sizes must be positive integers and are treated as hints: Terrio uses fixed 512×512 internal processing blocks. Opening the description and reading a window fetches the matching pan pixels and spectral pixels with interpolation margins. GeoTIFF/COG outputs stream row strips with bounded interpolation buffers; other output formats may gather the output in memory. Inputs remain separate files needed by the VRT. Source formats without a lazy window reader may load their pixels at open. As in GDAL, nodata metadata retains the supplied number even when the working sentinel is rounded/clamped to an integer type. Rotated grids, cross-CRS alignment and mixed output data types within a VRT are unsupported. The command is also an MCP tool.

viewshed

terrio viewshed dem_utm.tif visible.tif --ox 500000 --oy 4400000 --oz 2 --tz 0 --md 5000
terrio viewshed dem_utm.tif height.tif --ox 500000 --oy 4400000 --om GROUND --cc 0

Single-observer viewshed using GDAL's default edge recurrence. --om NORMAL produces Byte visibility (255 visible, 0 hidden); DEM produces the minimum visible elevation and GROUND the minimum visible height above the DEM surface, both Float64. Height modes ignore --tz. --md is maximum distance, zero for unlimited, and crops the output rectangle. Its corner pixels outside the radius receive --ov (default 0). --band selects a source band.

The DEM must be north-up in a projected CRS. Heights use DEM units and maximum distance uses projected CRS units. Observer coordinates are converted to pixels by truncating toward zero, as in GDAL. Outside observers start at an unobstructed nearest edge. Their height is absolute when X is outside; when only Y is outside, GDAL samples elevation on the nearest row. For physically consistent curvature, use horizontal and vertical units in metres: GDAL's curvature formula uses the ellipsoid diameter in metres even for other projected units. --cc is the curvature coefficient, equal to 1 minus the atmospheric refraction coefficient: 0 gives a flat Earth, 1 disables refraction. The default is 0.85714 for Earth ellipsoids, 1 for other bodies.

Like GDAL, numeric nodata values are treated as elevations and masks are ignored; this can produce misleading occlusion. Nonfinite elevations in the processed rectangle produce an error. Fill or crop missing elevations before use. Output has no nodata marker. Cumulative visibility is not supported. The command is also an MCP tool.

Terrio applies the radius mask deterministically. GDAL 3.12 can race its left/right row jobs for an outside observer, occasionally leaving an out-of-range boundary pixel visible. The conformance suite checks Terrio's mask independently and compares in-radius GDAL pixels for that case; all other viewshed cases compare every pixel.

compare

terrio compare reference.tif output.tif            # exit status: the number of differences
terrio compare reference.tif output.tif --skip-binary --json
terrio compare golden.tif new.tif --skip-all-optional

Compares two rasters as gdal raster compare (and gdalcompare.py) do, and prints GDAL's report: one line per difference, worded and ordered as GDAL's, and exits with the number of lines (capped at 255). The first raster is the reference. Identical files have no difference and nothing else is compared; otherwise the report covers the file sizes and bytes, the subdataset count, CRS, geotransform (relative tolerance 1e-10), band count and size, the dataset's default, RPC and GEOLOCATION metadata, then per band its size, description, data type, nodata value, color interpretation, pixels, overviews (each compared as a band), mask flags and per-dataset mask (compared as a band), and band metadata; then each subdataset. Metadata includes .aux.xml (PAM) items, GeoTIFF TIFFTAG_* items and RPC tags, as GDAL reports them. Differing pixels add two lines per band: 1: pixels differing: N and 1: maximum pixel value difference: X, computed in the union of both types (GDAL's GDALDataTypeUnion); a NaN in one band only differs, NaNs in both do not.

--skip-all-optional, --skip-binary, --skip-crs, --skip-geotransform, --skip-overview, --skip-metadata, --skip-rpc, --skip-geolocation and --skip-subdataset skip what GDAL's options skip; sizes, band counts, descriptions, types, nodata, color interpretations and pixels are always compared. --json prints {difference_count, differences: [{kind, band, subdataset, message}], bands: [{band, data_type, pixels, pixels_differing, max_abs_diff}]}, with every band, overview and mask compared, including those without differences.

Rasters are read lazily: strips of rows by window (of every band at once), overviews from their own levels, and the binary comparison compares sizes first and then reads a megabyte at a time. Comparing two copies of a 51 MB USGS GeoTIFF served over HTTP with --skip-binary --skip-overview fetched 76.5 MB in 10 requests; the binary comparison of two identical copies read both files (103 MB). The MCP tool reports query statuses as results, not execution failures. On the command line, status 1 can mean either one difference or an execution error; status 2 can mean two differences or a command-line usage error. Check stderr and the report for errors. pixel-info similarly uses status 1 for off-raster positions. --skip-all-optional takes precedence over individual --skip-* flags.

Compared with GDAL 3.13.3 in the raster-query conformance suite: 20 of 21 cases give the same lines and exit status. One known mask-count reporting difference is described below.

Differences: CRS mismatch lines print Terrio's WKT where GDAL prints WKT2_2019; GDAL's metadata value line swaps the two values (Reference ... has value <input's>), Terrio names them in reference, input order. Not supported: complex bands, --input-format/--open-option, pipelines, and GDAL's per-dataset comparison of more than 10 pixel-interleaved bands (whose lines read Band 1: ...); RPC comes from GeoTIFF tags only (not _RPC.TXT). Terrio opens a multi-variable netCDF/HDF5 container as its first variable, so the comparison of two containers differs from GDAL's empty container datasets; compare their subdatasets.

With GDAL 3.13.3, the internal-mask comparison reports 1,863,354 differing pixels per band on the Landsat fixture, while direct GDAL mask reads and Terrio both count 2,568,634. Terrio retains the direct pixel count. The full 100-case rerun has 96 identical cases, three within the documented numeric tolerances, and this one mask-report difference.

validate-cog

terrio validate-cog dem.tif
terrio validate-cog https://example.com/dem.tif --strict --json

Checks that a GeoTIFF is a Cloud Optimized GeoTIFF with the checks of rio-cogeo's cog_validate, the rasterio port of GDAL's validate_cloud_optimized_geotiff.py, and the same error and warning messages: a tiled full-resolution image and tiled overviews above 512 pixels, overviews of increasing decimation, IFDs before the image data (the main one within the first 300 bytes), image data from the smallest overview to the full resolution, no .ovr sidecar and no KNOWN_INCOMPATIBLE_EDITION=YES. A large image without overviews is a warning; --strict makes warnings errors. Only headers and chunk offsets are read, so a remote file costs a few range requests. --json adds the IFD and first-block offsets, the overview factors, GDAL's structural metadata and layout notes (mask IFDs after image data, which do not affect validity). The exit status is 1 when the file is not a valid COG, as with GDAL's script. The command is also a read-only MCP tool.

--profile gdal selects GDAL 3.13's structural rules: exact main-IFD placement following the optional structural metadata, source-width tiling threshold above 1024, and overviews ordered by their dimensions. --full-check auto|yes|no checks row-major block order, little-endian size leaders, repeated four-byte trailers and interleaved masks when the file declares those optimizations. Auto checks local inputs fully and keeps HTTP/S3 checks to headers and offset arrays; yes explicitly fetches block boundary bytes remotely. No pixels are decoded. --full-check yes requires the GDAL profile. The default profile retains the existing rio-cogeo behavior.

GDAL COG full checking examines blocks in the main TIFF and its internal overviews. External .ovr sidecars make the report invalid and receive header checks only. Explicit full checks of URLs can make several boundary requests per stored block; the cost grows with the number of blocks, so automatic mode leaves them off. Remote full checks stop with an error after 10,000 boundary read calls (excluding header/table reads and configured HTTP retries); download larger files to validate their full layout locally. Full block validation stops at 1,000 findings and reports the limit; remaining blocks need not be checked once the file is already invalid. Full GDAL checks limit total parsed GDAL metadata to 4 MiB per file and XML nesting to 128 levels. Header-only checks and the default rio-cogeo profile ignore this metadata tag.

terrio validate-cog dem.tif --profile gdal --full-check yes --json

COG validation reads GDAL_METADATA only when full GDAL checks run. Full checks reject metadata XML parse errors and malformed attributes. Local files and byte uploads have no shared total boundary-read budget; work scales with the stored block arrays. Other VSI inputs use the conservative remote policy: automatic full checks are off, and explicit full checks use the 10,000-read budget. This includes ZIP members backed by local archives: the input is classified by its VSI location, even when resolution extracts it to a temporary file. This keeps archive validation policy independent of where the archive is stored.

pixel-info

-o - writes text formats to standard output; binary/vector formats require a file path.

terrio pixel-info dem.tif 100 200                          # column, line; GeoJSON
terrio pixel-info --position-crs WGS84 -r bilinear dem.tif -105.5 39.5
echo "-105.5 39.5 site A" | terrio pixel-info -f text --position-crs EPSG:4326 dem.tif
terrio pixel-info --position-dataset sites.gpkg -f csv dem.tif
terrio pixel-info --position-dataset sites.gpkg dem.tif -o values.gpkg

Pixel values at positions, as gdal raster pixel-info and gdallocationinfo (locationinfo is an alias). Positions are pairs of arguments (X Y or X,Y), -p X,Y, lines of standard input (an X Y pair, then any text, reported as extra content; GDAL's tokenizing, quotes kept together) or the points of a vector dataset with --position-dataset (GDAL 3.13; centroids of other geometries, -l for the layer, --include-field NAME|ALL|NONE). --position-crs is pixel (column and line, the default), dataset (the raster's CRS) or any CRS, X being the longitude or easting; with --position-dataset it defaults to the layer's CRS. -b selects bands, --overview N (from 0) queries an overview with full-resolution positions, and -r nearest|bilinear|cubic|cubicspline interpolates as GDAL's GDALRasterInterpolateAtPoint: pixel centres at half-integers, kernels shifted inside at edges, no value when a bilinear kernel touches nodata, nodata skipped by cubic kernels. Values are reported raw and with the band's scale and offset applied.

-f selects the output: geojson (the default: gdal raster pixel-info's feature collection, points in the raster's CRS when it has an EPSG code, else in WGS 84; --promote-pixel-value-to-z puts the value as Z), csv (GDAL 3.13's columns geom_x, geom_y, the layer's fields or extra_content, column, line, band_N_raw_value, band_N_unscaled_value, numbers as %.15g), text (gdallocationinfo's report), value (gdallocationinfo -valonly, with --field-sep) and json (a list of {input, extra, column, line, xy, bands: [{band, value, unscaled}]}). With text and value, the exit status is 1 when a position is off the raster, as gdallocationinfo's. -o writes the GeoJSON, CSV or a vector layer of points (any vector format) to a file. Vector-file output records missing/off-raster samples as null and returns normal write success.

Only the 64 x 64 pixel blocks around the positions are read: two cubic queries of a 51 MB USGS GeoTIFF served over HTTP fetched 1.8 MB in 3 requests.

Compared with GDAL 3.13.3 (gdal raster pixel-info and gdallocationinfo) in the raster-query conformance suite: of 67 cases, 64 identical (GeoJSON and CSV values parsed; text and -valonly byte for byte) and 3 within tolerance: positions in WGS 84 on a custom Albers raster (column, line and geometry within 4e-14 relative, from Terrio's projection arithmetic) and one scale/offset product one ULP from GDAL's build. Formatting differences: GeoJSON whitespace (values are identical); CSV follows GDAL 3.13 (3.12's columns were input_x, input_y, extra_input with 17 significant digits). Not supported: complex bands, VRT LocationInfo (source files), GDAL pipelines, -xml, -lifonly and -E.

Pixel-info edge cases follow GDAL 3.13.3: a position layer without a CRS uses pixel coordinates; interpolated integer bands truncate GeoJSON raw_value while retaining fractional unscaled_value. Copied attributes can overwrite GeoJSON column, line, input_coordinate and extra_content, and CSV can repeat those names. Rename those input fields or select them out to retain computed properties. Vector-file output rejects field names colliding with computed columns rather than writing an ambiguous schema.

The diagnostic json format uses JSON null for non-finite numbers; GDAL-compatible GeoJSON can emit its non-standard NaN/Infinity tokens. Text prints non-finite locations as nan/inf, not as pixel zero. A source-CRS transformation failure aborts the query, matching GDAL 3.13.3; it is distinct from an ordinary off-raster position.

xyz

terrio xyz dem.tif dem.xyz
terrio xyz landsat.tif --allbands --csv --skipnodata > landsat.csv
terrio xyz dem.tif --srcwin 100 200 50 50 --skip 2 --dstnodata -9999

Writes a line per pixel with the X and Y of its centre and the band values, as gdal2xyz.py: coordinates as %.10g when the raster's origin and extent are within 180 units, %.3f otherwise, values as %d when the first band is Int32 or UInt32, else %g, converted to the first band's type. -b (band 1 by default; repeat or comma-separate), --allbands, --skip N or --skip X,Y, --srcwin XOFF YOFF XSIZE YSIZE, --skipnodata, --srcnodata and --dstnodata (one value, or one per band), --csv and --delimiter. Output goes to the file given, or standard output. The raster is read and the text written a strip of rows at a time.

Compared with GDAL 3.13.3's gdal2xyz.py in the raster-query conformance suite: 11 of 11 text cases are byte-identical; an additional mixed-type non-finite integer nodata case is rejected by both tools before sample data is written. Difference: --skipnodata and --dstnodata match NaN nodata values, which gdal2xyz.py never matches (so it keeps those lines). gdal2xyz.py's -skip takes no value on its 3.12 command line; Terrio's --skip N is its function's skip. Not supported: returning NumPy arrays (use Raster.read).

XYZ rejects non-finite source nodata when the first selected band's output type is integer, as GDAL's NumPy conversion does.

calc

terrio calc -A INPUT [--A_band N] [-B INPUT ...] --calc EXPR [--calc EXPR...] OUTPUT [options]

The arguments of gdal_calc.py work as they are (-A file --A_band 2, --outfile), and inputs can also be named freely with --input NAME=PATH and --input-band NAME=N. Expressions use NumPy syntax: + - * / // % **, comparisons, & | ^ ~, and/or/not, and functions (where, clip, minimum, maximum, min/max of several values, abs, sqrt, exp, log, log10, trigonometric functions, floor, ceil, round, isnan, logical_and..., also written numpy.where or np.where), A.astype(float32) and the constants pi, e, nan, inf. Nothing else is evaluated. Values follow NumPy's typing rules, as in gdal_calc.py: integer arithmetic wraps around (A + B of two Byte bands is Byte), / gives float64, and Python numbers adapt to the array's type. A repeated --calc gives one band each. Options: --type, --NoDataValue (or none), --hideNoData, --extent union|intersect|ignore|fail, --projwin, --allBands NAME, --projectionCheck, and the output format options. Any input pixel equal to its nodata value gives an output nodata pixel.

proximity

terrio proximity INPUT OUTPUT [--values 1,2] [--distunits PIXEL|GEO] [--maxdist N] [options]

--srcband, --nodata (the value of pixels beyond --maxdist, 65535 by default), --use-input-nodata, --fixed-buf-val, --ot (Float32 by default). Targets are the non-zero pixels unless --values lists them.

sieve

terrio sieve INPUT OUTPUT [--st N] [-4|-8] [--nomask | --mask RASTER]

Areas of connected pixels with the same value smaller than --st pixels (2 by default) take the value of their largest neighbour. The band's nodata pixels are left out unless --nomask.

neighbors

terrio neighbors INPUT OUTPUT --kernel KERNEL [--kernel ...] [--method METHOD ...] [--size N]
                 [-b BAND] [--ot TYPE] [--nodata VALUE|none] [output options]
terrio neighbors dem.tif max5.tif --kernel equal --size 5 --method max
terrio neighbors image.tif edges.tif --kernel gaussian --kernel edge2 -b 2 --ot Float32

Focal statistics, as GDAL 3.12's gdal raster neighbors. Kernels: u and v (horizontal and vertical derivatives), edge1, edge2, sharpen, gaussian (3x3 or 5x5), unsharp-masking (5x5), equal (any odd size from 3 to 99), or a matrix written row by row, [[0,1,0],[1,1,1],[0,1,0]]. Explicit matrices accept any positive odd square size, as GDAL 3.13.3 does; the 99 limit applies to --size, not matrix dimensions. Filtering work grows with pixels times coefficients, and strip margins grow with the kernel radius. --method combines the weighted neighbours: sum, mean, min, max, stddev, median or mode; give one for every kernel or one per kernel. The default is sum for kernels whose coefficients sum to zero (u, v, edge1, edge2, zero-sum matrices) and mean otherwise. Each input band (or the -b band) gives one output band per kernel. The output type defaults to Float64 and the output nodata to the input band's.

Values follow GDAL's VRTKernelFilteredSource exactly: inputs are read as Float32, nodata and NaN neighbours (with a nodata value) and zero coefficients are skipped, finite nodata centres keep the input nodata value, even when --nodata changes the output tag or removes it with none. NaN nodata centres are interpolated, matching GDAL 3.13.3; pixels past the raster's edges repeat the edge pixels, and results are Float32 values converted to the output type. mode breaks ties for the first value to reach the highest count, in row order. The input is read and GeoTIFF/COG outputs are written a strip of rows at a time, with the kernel's margin rows. The real-data comparisons with GDAL are identical (scripts/compare_focal_with_gdal.py). GDAL's GDALG/VRT pipeline output and --append are not supported.

clean-collar and nearblack

terrio clean-collar INPUT [OUTPUT] [--update] [--color black|white|R,G,B ...] [--white]
                    [--color-threshold N] [--pixel-distance N] [--add-alpha | --add-mask]
                    [--algorithm floodfill|twopasses] [output options]
terrio clean-collar scene.tif clean.tif --add-alpha
terrio nearblack scene.tif clean.tif --nb 0 --near 10 --color 0,0,0 --color 255,255,255
terrio clean-collar --update scene.tif

Sets the noisy, nearly black (or white, or --color) collar of an image to exact black (or white, with --white), as GDAL's gdal raster clean-collar and nearblack do. Pixels within --color-threshold (-near, 15) of a color in every non-alpha band are collar pixels; the search inwards goes past up to --pixel-distance (-nb, 2) non-collar pixels. --add-alpha adds an alpha band (transparent in the collar) and --add-mask a per-dataset mask (GeoTIFF: internal). clean-collar runs a flood fill from the edges by default and retains the input's alpha/mask representation; requested or retained alpha/mask processing regenerates opacity from the collar rather than intersecting it with the old alpha/mask. nearblack runs nearblack's two passes (top-down then bottom-up, each scanning lines from both sides) by default and adds alpha or masks only when asked. Both algorithms are ports of GDAL's, in GDAL's scan order, and agree pixel for pixel with GDAL on the real Sentinel-2 and Landsat 7 comparisons. Output is a new Byte raster with the input's georeferencing and no nodata value; values are read as Byte (rounded and clamped).

--update (or an output equal to the input) updates a local GeoTIFF in place: its bands and type are kept, with --add-alpha its last alpha band is updated (it must have one), and an existing mask is kept unless mask processing regenerates it from the collar. The result is written next to the input, then replaces it, so a failure leaves the input as it was; remote files, other formats and read-only files are refused. The file is rewritten by Terrio's writer: overviews are dropped, and compression and tiling are those of the output options. GDAL 3.12's in-place flood fill of a raster with an alpha band writes stale alpha values on every modified line (2,715,128 alpha values of the Landsat 7 case become 0); Terrio's alpha band matches GDAL's output to a new file instead. Images are processed in memory as Byte values (one byte per band and pixel); the flood fill visits lines out of order. New-file output holds an interleaved working image and a planar output image at once (about two Byte output buffers, plus input data and any mask); this keeps the GDAL scan order and contiguous scanlines. In-place updates follow input symlinks and retain mode bits. Replacement detaches hard links and does not preserve ownership, ACLs or extended attributes.

nearblack --update and clean-collar --update follow GDAL's Byte processing: non-Byte samples across the image are rounded/clamped to 0–255 before being written back to the original data type. Terrio emits a warning for these inputs. For RGBA sources, nearblack --color black compares four bands unless --add-alpha or --add-mask selects alpha/mask processing; clean-collar keeps existing alpha. With mask processing on RGBA, new-file output drops the alpha band, while in-place output retains it, matching GDAL 3.13.3.

enhance

terrio enhance INPUT OUTPUT --equalize
terrio enhance INPUT --equalize --config lut.txt     # write the tables
terrio enhance INPUT OUTPUT --config lut.txt          # apply tables
terrio enhance INPUT OUTPUT --src-scale 0 4000 [--lut-bins 256] [--ot Byte]

Contrast enhancement by lookup tables, as GDAL's gdalenhance. --equalize computes histogram-equalization tables from each band's default histogram (gdalinfo -hist's: stored histograms and GDAL's approximate minimum and maximum included), without its first and last buckets. Without an output, the tables are written to --config (or printed) in gdalenhance's format: a line per band, 1:Band -0.5:ScaleMin 255.5:ScaleMax and the 256 entries. Without --equalize, --config reads such a file. A pixel's bin is (value - ScaleMin) * bins / (ScaleMax - ScaleMin), truncated and clamped; nodata pixels keep their value after conversion to the output type. NaN nodata uses the first LUT bin, matching GDAL 3.13.3. The output keeps the georeferencing, metadata, band descriptions and color interpretations, and the nodata value of bands of the output type; it is read and written in strips (equalization reads the bands whole for their histograms). Equalization, LUT files and LUT input are identical to gdalenhance on the real comparisons. Terrio extensions: --src-scale MIN MAX (a linear stretch over the bins, without tables; gdalenhance parses -src_scale but ignores it), --lut-bins (256 in GDAL) and --ot other than Byte (gdalenhance refuses it). Byte output casts LUT results modulo 256, matching GDAL (for example, a table value of 300 becomes 44). Byte nodata also truncates and wraps through a signed 32-bit integer (for example, -9999 becomes 241); values outside that intermediate range and infinities become zero. This defines the behavior observed in the GDAL 3.13.3 reference build; C++ out-of-range conversion can vary by build. Non-Byte outputs preserve exact source nodata before output conversion, including Float64 precision. Select a wider --ot to retain larger LUT values. Unlike gdalenhance, Terrio does not save the histograms it computes in an .aux.xml sidecar.

crs

terrio crs CRS [--wkt] [--projjson]
terrio crs SRC --to DST [--s_coord_epoch YEAR] [--t_coord_epoch YEAR] X Y [X Y ...]
echo "X Y [Z [T]]" | terrio crs SRC --to DST [--s_coord_epoch YEAR] [--t_coord_epoch YEAR]

As gdaltransform: lines of standard input with a height transform it too, with a time (a decimal year) use it for time-dependent transformations, and a point that cannot be transformed prints transformation failed.. Datum shift grids and geoid models are used when they can be found (see Coordinate Systems). --projjson prints only the CRS as PROJJSON, so that terrio crs CRS --projjson > crs.json writes a PROJJSON file (see Writing PROJJSON); it cannot be combined with --to or --wkt.

formats

terrio formats [--json]

grid

terrio grid INPUT OUTPUT --tr RES [--stat min,max,mean,idw,count,stdev|all] [--dim z]
            [--radius R] [--power P] [--window-size N] [--bin] [--te XMIN YMIN XMAX YMAX]
            [--class ...] [--returns ...] [--bounds ...]

Grids a point cloud into a raster as PDAL's writers.gdal does: a DSM with --stat max, a DTM with --class 2 --stat idw, a density map with --stat count --bin. See Point Clouds. For vector points, see vector-grid.

vector-grid

terrio vector-grid invdist stations.gpkg temp.tif --zfield temp --power 2 --radius 50000 --max-points 12
terrio vector-grid linear samples.gpkg dem.tif --extent 636000 848930 637180 849500 --resolution 5 5
terrio vector-grid count places.shp density.tif --zfield pop_max --radius 3 --ot UInt16
terrio vector-grid "invdistnn:radius=4:max_points=8" places.shp idw.tif --zfield pop_max --where "adm0name = 'Peru'"

Grids scattered vector points into a raster, as GDAL's gdal vector grid and gdal_grid do. It is a separate command from grid, which grids point clouds the way PDAL's writers.gdal does. Every vertex of every geometry is a point (lines' and polygons' too), with the value of --zfield or its Z (0 for 2D points); --zoffset is added, then --zmultiply multiplies. The algorithm is the first argument, or a GDAL algorithm string (invdist:power=3:radius1=500:radius2=300) whose parameters the options override:

Algorithm GDAL Parameters
invdist (default) inverse distance to a power --power (2), --smoothing, --radius or --radius1/--radius2, --angle, --min-points, --max-points (with a radius), per-quadrant limits (then invdistnn)
invdistnn inverse distance of the nearest points --power, --smoothing, --radius (1), --min-points, --max-points (12), per-quadrant limits
average moving average ellipse, --angle, --min-points, per-quadrant limits, --max-points (with them)
nearest nearest neighbour ellipse, --angle
linear linear in the Delaunay triangulation --radius: the nearest point outside it within this distance; 0 for the nodata value; by default any distance
minimum, maximum, range, count, average-distance, average-distance-points data metrics ellipse, --angle, --min-points, per-quadrant limits (not average-distance-points)

The per-quadrant limits are --min-points-per-quadrant and --max-points-per-quadrant. --nodata (0 by default) fills nodes without enough points; when it is given (here or in the string), it is declared as the bands' nodata value, as gdal_grid does. Nodes are the cell centres. The grid is --extent (by default the layer's, before --where and --bbox, as Shapefile and GeoPackage report it), --size (256 × 256 by default) or --resolution (with --extent: (|max - min| + res / 2) / res cells, the cell size then the extent over that). --layer grids several layers into a band each; --sql grids a query result; --bbox keeps the points strictly inside a rectangle (-clipsrc). --ot sets the type (Float64 by default), --crs the CRS (by default the layer's; points are not reprojected).

NaN geometry Z values are skipped; NaN values supplied through --zfield can propagate into the result, matching GDAL 3.13.3. Input layers requested by name must all exist. Native path-based GeoPackage selections are case-insensitive; in-memory and JavaScript byte/Dataset selections use exact names. Validating a file-stem alias can reread the source to confirm that it originally has only one layer; use the actual layer name to avoid that extra read on large inputs.

Without a search radius, invdist visits all input points for each output cell; its work grows with cells times points. Output allocation is bounded, but runtime is not bounded by a separate work budget. Use a smaller grid or an appropriate search radius for large inputs.

average-distance-points evaluates every pair in each node's search neighborhood; work is quadratic in that neighborhood's point count. Float32 and other typed outputs are converted from an f64 working grid, so both buffers can coexist (about 768 MiB for f64 plus Float32 at the 2^26-cell limit, before inputs and indexes). Limit the search radius for dense inputs.

The algorithms are ported from GDAL 3.13's gdalgrid.cpp: the search ellipse and its rotation, GDAL's switch from scanning every point to searching its quadtree above 100 points, and the order the quadtree returns points in (which decides ties and the rounding of sums) are reproduced. With one zero ellipse semi-axis, the scan accepts an unbounded line through the node while the quadtree search clips its candidates to a square of half-width equal to the other semi-axis; Terrio preserves this GDAL 3.13.3 behavior across the threshold. Unbounded average, minimum, maximum, range and count compute their constant value once per output block. Many cases agree exactly; linear triangulation and SIMD arithmetic have the tolerances and known differences reported in Testing. Known differences:

  • GDAL computes the default invdist (power 2, no smoothing, no radius) with AVX or SSE, in single precision with approximate reciprocals, unless GDAL_USE_AVX=NO and GDAL_USE_SSE=NO; terrio computes in double precision, as GDAL's scalar code.
  • Grids are materialized in memory and limited to 67,108,864 cells across all bands (512 MiB of Float64 values before conversion); allocation failure returns an error.
  • linear triangulates with spade rather than qhull. The triangles of scattered points are the same, but their vertex order (and so the last bits of the interpolation) can differ, and the ties of co-circular points (a regular grid's squares) are broken differently.
  • GDAL 3.12 scanned every point for a rotated ellipse (and refused rotation with per-quadrant limits); 3.13, as terrio, searches its quadtree: sums can differ in their last bits from 3.12.
  • Like GDAL 3.13.3, linear skips triangles whose barycentric determinant has absolute value below 1e-5. Small geographic triangles can therefore produce nodata with radius 0; use a suitable projected CRS for those inputs. Coordinates outside the triangulator's supported numeric range are rejected. Collinear points have no triangulation: Terrio returns nodata, while GDAL 3.13.3 reports a triangulation failure and leaves its initialized nodata output. The default nearest fallback does not fill that case.
  • Zero-width or zero-height extents are rejected before allocating; GDAL can emit a noninvertible transform for these inputs. Reversed x bounds retain negative pixel width, matching GDAL.
  • With --sql, gdal_grid writes no CRS; terrio keeps the query result's.
  • -clipsrc with a geometry or a datasource, -spat and -co other than terrio's raster output options are not supported.

Search-axis signs follow GDAL: for more than 100 points, negative semi-axes can use a different candidate path from positive axes and yield a different result. They are not normalized to magnitudes.

lineref

terrio lineref create --lines path.shp --points mstones.shp --position-field pos --step 1000 parts.gpkg
terrio lineref get-pos --parts parts.gpkg -x -1.4345 -y 51.9497
terrio lineref get-coord --parts parts.gpkg -m 15977.724709
terrio lineref get-subline --parts parts.gpkg --begin 13300 --end 17400 subline.gpkg

Linear referencing, as GDAL's ogrlineref (-create, -get_pos, -get_coord, -get_subline). create cuts the route (the first feature of --lines, or with --line-field and --point-field each route with its reference points; --id-field, default uniq_uid, receives the route identifier) into parts --step positions long, from reference points (milestones) carrying their positions in --position-field. Points farther than 10 units (0.00008983153 degrees for a geographic route) from the route are ignored, the route is reversed when it runs against the points, and the parts before the first and after the last point are measured in the UTM zone of their middle for a geographic route. Each part has its first and last positions (beg, end) and its length per position unit (scale).

get-pos prints the position of a point on the nearest part, get-coord the x,y,z of a position on each part strictly spanning it (both with six decimals, as ogrlineref -quiet; --json for full precision), and get-subline writes the route between two positions. As ogrlineref, positions are rounded to six decimals in its part lookups. --layer-name names the output layer (by default the file's stem); --lines-layer, --points-layer and --parts-layer choose input layers. Like GDAL, an initial route stretch before the first retained picket can produce a zero position span and a non-finite scale; avoid such inputs when using the resulting parts for position lookup. With exactly two milestones at the route endpoints, creation produces no parts, matching GDAL 3.13.3; include an intermediate milestone when using this case. Creation is limited to 100,000 pickets to bound memory use; increase the step for longer routes. Subline extraction rejects duplicate start positions rather than silently mixing routes. With route identifiers, the first feature for each identifier is used even if its geometry is missing; a later line does not replace it, matching GDAL 3.13.3. Only single-LineString parts contribute to get-subline; missing geometries are skipped as in GDAL, so validate externally edited parts to avoid gaps. Coordinate lookups interpret null scale as zero, as GDAL 3.13.3 does; zero and negative scales clamp to the part's first vertex. Position lookups currently copy each part's vertices, and text-to-number conversion tries numeric prefixes; repeated lookups and unusually long numeric strings can be expensive.

For repeated queries, open parts once as a Python VectorDataset/Layer or JavaScript Dataset. Path, byte and GeoJSON inputs are read/parsed anew on each call. Lookup edge behavior follows OGR: positive-infinite scale clamps coordinates to the last vertex and contributes zero position offset; null scale is zero. Malformed single-vertex parts can produce OGR's -1 projection value. Use normally generated parts for meaningful reference positions.

Grouped route failures are reported as per-route warnings; as with ogrlineref, creation can succeed with an empty output if every selected route fails.

mcp

terrio mcp

Serves the other commands to AI agents (Claude Code, Claude Desktop, Cursor, VS Code) as a Model Context Protocol server over standard input and output. See AI Agents.

vsi

terrio vsi list -lR s3://bucket/prefix                 # gdal vsi list (ls)
terrio vsi list -R --depth 2 -f json --tree /vsizip/data.zip
terrio vsi copy -r data/ s3://bucket/prefix            # gdal vsi copy (cp)
terrio vsi copy /vsizip//vsicurl/https://host/a.zip/a.shp .
terrio vsi sync -r --strategy ETag data/ s3://bucket/prefix   # gdal vsi sync
terrio vsi move s3://bucket/old s3://bucket/new        # gdal vsi move (mv, ren, rename)
terrio vsi delete -r s3://bucket/prefix                # gdal vsi delete (rm, rmdir, del)
terrio vsi sozip create -j big.gpkg big.gpkg.zip         # gdal vsi sozip (see below)

File management across locations, as GDAL's gdal vsi commands do on its virtual file systems:

Location Forms list read write, delete
Local files dir/file yes yes yes
In memory /vsimem/dir/file yes yes yes (within one process: Python, JavaScript)
Amazon S3, S3-compatible services s3://bucket/key, /vsis3/ yes yes yes, signed (see Remote Data)
Google Cloud Storage gs://bucket/key, /vsigs/ public buckets yes no: use its S3-compatible API (s3:// with AWS_S3_ENDPOINT=https://storage.googleapis.com and HMAC keys)
Azure Blob Storage az://container/blob, /vsiaz/ yes yes with a SAS token (AZURE_STORAGE_SAS_TOKEN)
HTTP(S) https://..., /vsicurl/ from an HTML index page (Apache, nginx) yes no
Zip archives /vsizip/a.zip/dir, also .kmz, .shz; nested in remote locations yes yes no
Tar archives /vsitar/a.tar, .tar.gz, .tgz yes yes no

list prints a directory's entries (a file lists as itself), directories before what they hold and siblings by name; -l adds permissions, size and date as ls -l (and gdal vsi list -l) print them, -R recurses (--depth N levels), --abs prints absolute paths, -f json prints GDAL's JSON (names, or with -l objects with name, type, size, last_modification_date, permissions, and etag for objects), --tree nests it. An object store with unlimited depth is listed recursively in one paged listing (1,000 keys per request on S3). Finite depths use delimiter listings only down to the requested levels, including JavaScript listUrl. Remote zip archives are listed by range requests: the archive's end and central directory, about 32 KB for Natural Earth's 4.9 MB ne_10m_admin_0_countries.zip; tar archives by seeking from header to header. For an archive URL with a query string, braces keep the query attached to the archive, for example /vsizip/{/vsicurl/https://host/data.zip?sig=TOKEN}/dir/member.tif.

copy SRC DST copies a file to DST, or into it when DST is an existing directory (or ends with /; a local destination directory named this way must already exist); a directory needs -r and is copied as DST, or into an existing DST as DST/<name>, as GDAL does. --skip-errors goes on past the files that fail, reporting them. Copies within S3 are server-side (x-amz-copy-source), uploads are single PUTs (up to 5 GiB), and other remote copies go through a temporary file. Local files are written to a temporary file renamed into place, so a failed copy leaves an existing target as it was.

sync SRC DST copies the files that are missing or out of date in DST and deletes nothing; a source directory ending with / is synchronized into DST, without one as DST/<name> (rsync's convention, as GDAL's). --strategy timestamp (the default) copies when the sizes differ or the source is newer; ETag compares entity tags, or a local file's MD5 with an S3 object's (single-part uploads; multipart entity tags fall back to timestamp); overwrite always copies.

move SRC DST renames on one local file system; elsewhere it copies, then deletes the source only once all of it was copied. delete PATH deletes a file; -r deletes a directory and what it holds.

Safety: delete and move refuse an empty path, a file system root, the home directory, the current directory or one of its parents, the root of /vsimem/, a whole bucket or container, and archive and HTTP locations. Recursive deletion never follows symbolic links (the links are deleted), checks that every directory it enters is below the path given, and on object stores deletes only the keys below prefix/ (not prefix2/...). Like GDAL 3.13.3, recursive object-store deletion preserves an exact object named prefix, even when it coexists with prefix/ children; delete that exact object without -r. Recursive deletion of a lone object fails because there is no directory prefix to remove. copy, sync and move refuse to copy a directory into itself. Local recursive removal uses the standard library's symlink-race-protected removal on Linux, macOS and Windows. Counting makes a discovery pass before the removal pass; deleted reflects that discovery count, which requires an unchanged tree to be exact.

-q prints nothing, --json prints what was done (copied, bytes, unchanged, deleted, errors), --stats the HTTP requests and bytes received.

VSI moves replace existing destination files, as GDAL does, and refuse existing destination directories. Local filesystem renames preserve native links and names; copy-delete moves between stores or filesystems refuse symbolic links and non-UTF-8 entries before copying. Keep the source unchanged during a copy-delete move.

Recursive extraction and copy reject parent traversal, Windows drive-style child names from archives or remote/memory listings (on all platforms for portable paths), and symlinks escaping the output root. Local-to-local copies on Unix preserve legal colons and backslashes in file names. Tar and tar.gz copies and sync stream payloads in one pass after discovery. ZIP central directories are indexed once per operation with a bounded read buffer. Local file writes sync the temporary file before renaming it into place; many small files therefore pay one file sync each. Containment checks resolve the root and existing target ancestors per entry, retaining current filesystem-state checks at that cost. File sync does not include a directory sync and is not a complete power-loss durability guarantee. Tar listings expose regular files and directories; copying a tar link or other unsupported entry fails, or records an error with --skip-errors. Untimed memory targets are compared by content during sync. Local atomic writes follow destination symlinks and preserve mode bits, while replacement detaches hard links and does not preserve ownership, ACLs or extended attributes.

HTTP directory listings reject index pages larger than 4 MiB and stop with an error at the object-listing page budget. Binary HTTP responses are not read as index pages. Recursive copy/sync refuses nested directory symlinks rather than silently creating an empty directory; --skip-errors records the refusal in its error report.

Bucket/container roots are synthetic directories across S3, GCS and Azure URL forms; stat does not verify their existence or access. Listing performs that verification. Recursive deletes stop at the first failure (on S3, after the first failing batch of up to 1,000 keys, all of which were attempted) and can leave a partially deleted tree or prefix; the error does not include a partial deletion count. Inspect the remaining entries before retrying. A plain HTTP URL is first requested for one byte when it is copied, synchronized or described without a trailing /: a response typed other than HTML is a file, so a copy uses two requests (that probe and the payload GET), also from servers that ignore Range. An HTML or untyped response, or a missing one, is then probed as an index page (a bounded 4096-byte sniff of URL/), so an untyped file copy uses three requests; a directory named without its / costs that one extra request. Listings, and URLs ending with /, fetch the index page first. On a server that has both an object dir typed other than HTML and an index page at dir/ (possible with object-store-backed HTTP), dir names the object, as in GDAL; dir/ names the directory. A server may send additional bytes before the sniff connection closes; --stats counts bytes consumed by Terrio, not packets on the wire. The requests of one command share connections (and TLS sessions) to a host, and a remote archive's members are read through one reader and its block cache. Credential-bearing URL queries are redacted in diagnostics and listing names. VSI mutation/listing operations require complete object-store listings; capped read-only subdirectory discovery retains its existing warning and partial results for readers such as Zarr.

Recursive object-store listings include directories implied by keys; GDAL omits those implied directory entries. --skip-errors continues copying but exits nonzero when any files fail. Signed S3 uploads of local files up to 8 MiB read them once, into memory, to calculate their SHA-256 and send them, with the payload hash signed. Larger local files sent over HTTPS are signed with x-amz-content-sha256: UNSIGNED-PAYLOAD, as the AWS SDKs sign S3 uploads over HTTPS, and read once, streaming the body (a retried, redirected or re-routed request sends it again): the signature then covers the request's headers but not its body, which TLS protects in transit (no extra checksum header is sent, since one would need another pass over the file). Without a payload hash, the service cannot tell that a file changed while it was sent, so its size and modification time are compared before and after the upload, which fails if they differ. This is best effort: a same-size change within the file system's timestamp resolution is not detected. An S3-compatible service that refuses UNSIGNED-PAYLOAD rejects these uploads; send them over HTTP to keep the payload signed, only over a trusted connection: plain HTTP does not encrypt the request body or headers, including x-amz-security-token for temporary credentials. Over plain HTTP (AWS_HTTPS=NO, or an http:// AWS_S3_ENDPOINT; an endpoint given with https:// keeps HTTPS whatever AWS_HTTPS says), larger files keep a signed payload hash and are read once to calculate it and once to send the body (twice the file size in local reads). Uploads staged through a temporary file (from remote, in-memory or archive sources) are hashed as they are staged, read once and signed over their payload. ETag sync can add a separate MD5 pass. Recursive S3 deletion sends up to 1,000 keys per DeleteObjects request, directory markers only once the objects are deleted, reporting the first key the service could not delete; a service that answers that it does not implement it (405, 501, or 400 NotImplemented) is sent one DELETE per key, as are keys with control characters, which XML cannot carry, between the batches around them. Azure blobs are deleted one request each.

Differences from GDAL (checked with 3.13.3): entries are sorted (GDAL prints local directories in the order the file system returns them); dates are UTC (a zip member's date is its stored DOS time, as GDAL prints it); HTTP index names stay percent-encoded, as GDAL keeps them. Not supported: sync -j/--num-threads (requests are sequential), multipart uploads (objects over 5 GiB), Azure shared-key authentication, writing to Google Cloud Storage's own API, empty directory creation on destinations that cannot represent them (local directories are supported; S3 uses marker keys), progress bars, and the virtual file systems /vsigzip/, /vsi7z/, /vsirar/, /vsiswift/, /vsioss/, /vsiadls/, /vsihdfs/ and /vsiwebhdfs/.

Byte copies and copy-delete moves refuse pipes, sockets and devices. Recursive copies with --skip-errors report those entries and continue with regular files. ZIP member names retain valid UTF-8; unflagged non-UTF-8 legacy names are decoded as CP437 into Unicode. GDAL can instead emit raw non-UTF-8 bytes for those names.

vsi sozip

terrio vsi sozip create -j roads.gpkg dem.tif data.zip        # gdal vsi sozip create
terrio vsi sozip create -r --enable-sozip yes --sozip-chunk-size 64K dir/ data.zip
terrio vsi sozip optimize plain.zip seek.zip                  # gdal vsi sozip optimize
terrio vsi sozip list /vsicurl/https://host/data.zip          # gdal vsi sozip list
terrio vsi sozip validate -v data.zip                         # gdal vsi sozip validate

Seek-Optimized ZIP (SOZip index version 1): a Deflate member compressed in independent chunks, with a hidden index (.name.sozip.idx) of their offsets, so that readers can start decompressing anywhere. create adds files (directories with -r; -j drops their directories) to a new archive, or to an existing one unless --overwrite; --enable-sozip auto (the default) seek-optimizes files of at least --sozip-min-file-size (1 MB), yes every file larger than a chunk (--sozip-chunk-size, 32 KB), no none; --content-type stores GDAL's KV property. Inputs can be any location vsi reads; outputs are local or /vsimem/. optimize rewrites an archive's members. list prints GDAL's table (--json for JSON); validate decompresses every member and checks its CRC-32, each index's header against its member and its offsets, and that every chunk decompresses on its own; it exits with status 1 when the archive is not valid.

GeoTIFFs in a zip archive are read in place when stored or seek-optimized: terrio info and window reads of /vsizip/archive.zip/dem.tif (or of a remote archive, by range requests) read only the chunks they need instead of extracting the archive. Other members, and other formats, are extracted to a temporary directory first. ZIP readers reopen independently for parallel TIFF decoding while sharing remote range caches. Native ZIP-member raster exports stream strips and treat the containing archive as an input when checking overwrite aliases. Random reads check requested chunk structure; they do not read the whole member to check its CRC-32. Use vsi sozip validate for the complete checksum and independent-chunk checks.

GDAL 3.13.3's sozip optimize also adds the input archive itself to its output (a bug); terrio does not. Native creation and optimization stream to an atomic temporary output; compressed payload spools spill to disk after 64 MiB even when the source size was unknown. Central records and writer chunk offsets remain in memory. /vsimem/ output size bounds its memory use. Validation streams every member, and index tables are checked with a fixed buffer and read by offset when needed. Lazy readers retain a 64 KiB decoded window and decoder state, including for large chunk sizes. list performs index header/offset checks; only validate checks every independent compressed chunk. Optimization preserves member names verbatim, including unsafe paths present in an input archive; extraction enforces its own path checks. Create-only options (recursive, junk_paths, content_type) are rejected by optimize. Not supported: sozip create progress bars and multithreaded compression.

Create accepts existing empty placeholders as new archives. Append copies every payload byte before the old central directory into the replacement, then rewrites central records: adding a small file costs a full archive rewrite. The central records and prefix come from one retained input handle/snapshot; detected native file changes abort publication, and in-memory append uses a version-checked replacement. Unix appends hold an advisory exclusive archive lock through flush, sync and publication; other append attempts fail while that lock is held. Writers that ignore advisory locks and non-Unix native writers are not serialized. A destination that was initially absent is published only if it remains absent, including in-memory destinations. Recursive creation enumerates inputs before opening the temporary output and excludes an existing output from its own input tree. Remote member names omit URL userinfo, queries and fragments. Auto chunking uses streamed length even without a size hint. Its provisional full-flush boundaries reset the compression dictionary even for members below the indexing threshold. On the real 11,146,182-byte Natural Earth places DBF, with a threshold above its size, Auto produced 529,185 compressed bytes versus 482,152 with --enable-sozip no (9.75% larger); payload bytes agree. Use no to avoid that tradeoff. Writers retain at most 1,048,576 chunk offsets (8 MiB) per member; exceeding this budget fails before adding an offset. Use a larger chunk size or disable indexing for larger members.

list intentionally checks every index offset: its index-table work is eight bytes per additional chunk (plus a 32-byte header), so remote listings can fetch whole offset tables. A local HTTP listing of the 48,788,679-byte USGS archive transferred 29,895 bytes in two requests, including its 12,600-byte index (32 KiB chunks). Random member opens check the header and requested chunks to keep window reads lazy; a malformed requested chunk raises an error. validate performs the complete index, independent-decoding and checksum checks. Decoder state and buffers are reused between chunks; the offset cache mutex is released before archive I/O. TIFF prefetch hints are not yet coalesced across ZIP chunks, so remote windows use on-demand range reads. Native parallel member reopens reject changed archive identity, size or timestamps without reparsing offsets against a different archive. When keeping paths, literal backslashes in Unix/remote names are preserved; --junk-paths flattens either separator. Optimize preserves directory KV properties.

fill-nodata

terrio fill-nodata dem.tif filled.tif --max-distance 100 --smoothing-iterations 1
terrio fill-nodata image.tif filled.tif --band 2 --mask validity.tif --interpolation nearest

Fills holes in one band using GDAL's four-quadrant inverse-distance (inv_dist, the default) or nearest interpolation. The output contains the selected band, retaining its type, nodata and georeferencing. Valid source pixels are preserved exactly. The mask's first band is zero at holes and nonzero at donor pixels; without --mask, the source's internal/sidecar validity mask is used, otherwise its nodata value. Mask dimensions must match the input; mask georeferencing is ignored. --max-distance is in pixels (100 by default, 0 for unlimited search). --smoothing-iterations applies 3×3 mean-filter passes only to filled pixels, leaving the first/last rows unchanged as GDAL does. Inputs are processed in memory; interpolation uses Float32 samples, including for Float64 rasters. For nearest interpolation, GDAL's search fringe can write zero where the closest quadrant candidate is less than one pixel beyond the distance limit; Terrio follows that behavior. Complex bands are unsupported. Standard raster output options (--format, --compress, --co, etc.) apply.

unscale

terrio unscale packed.tif physical.tif
terrio unscale packed.tif physical.tif --ot Float64

As gdal raster unscale (gdal_translate -unscale): each band becomes value * scale + offset with its own scale and offset (GeoTIFF band scales and offsets, netCDF scale_factor and add_offset; bands without them are converted unchanged). --ot is the output type, Float32 by default (Float64 for Float64 inputs). Nodata pixels keep the band's nodata value, which the output declares unchanged, and the output has no scale or offset. Values are computed and rounded as GDAL's virtual rasters compute them: working pixels are Float32 unless the input or output is a 32- or 64-bit integer, or the output is Float64. A Float64 input with --ot Float32, Byte or Int16 still uses Float32 working pixels, matching GDAL 3.13.3. Byte outputs round half up. Complex bands are not supported. As in GDAL 3.13.3, if the output type cannot represent the original nodata value, pixels are clamped to that type while the original nodata declaration is retained (for example, nodata -9999 becomes a zero pixel in Byte output, still tagged -9999). Choose an output type that can hold the nodata value when those pixels must remain invalid. The standard raster output options apply.

nodata-to-alpha

terrio nodata-to-alpha landsat.tif landsat_rgba.tif
terrio nodata-to-alpha image.tif out.tif --nodata 0 --nodata 0 --nodata 0

As gdal raster nodata-to-alpha: the bands are copied without nodata values and followed by an alpha band of their type, 0 where the first band's mask is 0 and 255 elsewhere (Int8 alpha saturates at 127, as GDAL converts it). The mask is the first band's nodata value; one --nodata overrides the value used for the first-band mask; other bands do not participate in that mask. One --nodata per band (nan, inf and -inf are accepted) makes a pixel transparent only where every band has its value, as GDAL's NODATA_VALUES. Without explicit --nodata, a raster whose first band has no nodata value, or that already ends with an alpha band (two or four bands), is copied unchanged. Explicit nodata adds a new alpha band even to RGBA input, matching GDAL 3.13.3. Copied bands retain their scales and offsets, including driver-recorded packing. Internal and .msk masks are not used. The result is an owned raster held in memory. An unchanged result also clones the input's pixel buffer; this path does not stream file conversion.

reclassify

terrio reclassify nlcd.tif groups.tif -m "11=1;[21,25)=2;[41,44)=4;DEFAULT=0"
terrio reclassify dem.tif zones.tif -m "NO_DATA=0;[0,1500)=1;[1500,inf)=2" --ot Byte
terrio reclassify nlcd.tif groups.tif -m @mapping.txt

As gdal raster reclassify, with GDAL's mapping syntax: from=to entries separated by ;. from is a value, an interval ([a,b), (a,b], (-inf,b], [a,inf)...), NO_DATA (the band's nodata value), NAN or DEFAULT (any other value); to is a value, NO_DATA or PASS_THROUGH (the value unchanged). Intervals must not overlap. @file reads one entry per line; leading spaces and # comments are removed. A value that no entry maps, or an output value the output type cannot represent exactly, is an error. --ot is the output type (by default the input's). The output keeps each band's nodata value; like GDAL's, it has no color table, band descriptions or scales. Like GDAL 3.13.3, NO_DATA intervals with NaN nodata can fail overlap validation and do not match NaN pixels: NO_DATA=0;DEFAULT=PASS_THROUGH leaves NaN unchanged. Use NAN as the source mapping key for NaN pixels. As with unscale, retained nodata pixels are clamped when the output type cannot hold the original nodata declaration; choose a suitable output type to preserve their validity.

as-features

terrio as-features dem.tif pixels.gpkg --geometry-type polygon --include-xy --include-row-col
terrio as-features landsat.tif pixels.csv --skip-nodata -b 3 -b 2

As gdal raster as-features: one feature per pixel, row by row, in a layer named pixels (--output-layer). Fields: CENTER_X and CENTER_Y (pixel centres, --include-xy), ROW and COL (--include-row-col), then BAND_<n> (Float) per band. --geometry-type is none (an attribute table, the default), point (pixel centres) or polygon (pixel squares, in the raster's CRS). --skip-nodata leaves out the pixels whose first reported band has its nodata value after conversion to the source type; nearby Float32 values remain valid. Repeated band selections are reported once, in their first-selected order. -b selects bands (GDAL 3.13.3 accepts -b but reports every band). --append and --overwrite update an existing output, as for polygonize. The features are built in memory; an estimated expansion above 512 MiB is refused before generating features. Crop large inputs first. GDAL's --update, --overwrite-layer and layer creation options are not supported.

Native multidimensional arrays

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

catalog

terrio catalog tiles.gti.gpkg mosaic.tif --cache-size 32
terrio catalog tiles.fgb window.tif --window 100 200 256 256 --stats
terrio info GTI:tiles.gpkg --json

GTI-compatible GeoPackage/FlatGeobuf indexes contain polygon footprints and a string location field naming each source. Relative tile paths resolve against the index directory; absolute paths, URLs and supported VSI references select those sources directly. GTI XML IndexDataset likewise accepts a relative or absolute index path. GTI: forces catalog interpretation; .gti.gpkg, .gti.fgb and GTI XML .gti files open through ordinary raster APIs too. Indexes are local files; referenced tiles may be remote COGs.

The catalog retains one index reader and parsed schema, queries it spatially for each window, opens only selected sources and keeps a bounded LRU cache (default 32 datasets). Later sources in composition order overwrite earlier ones; --sort-field FIELD sorts ascending before composition, and --sort-descending reverses it. Source nodata, dataset masks and zero alpha are transparent; TIFF/PNG alpha is detected from the source, and --alpha-band or catalog color-interpretation metadata identifies it otherwise. Nonzero alpha copies source values without blending. Mixed source CRSs are reprojected into the index CRS.

--layer, --location-field, --sort-field, --tr X Y, --te XMIN YMIN XMAX YMAX, --srs, --band-count, --ot, --nodata, --alpha-band, --cache-size and -r override catalog characteristics. GTI layer metadata and XML supply their equivalents; missing resolution or band count requires inspecting one tile. GeoTIFF/COG pixels stay lazy; other formats follow their existing readers. Complete metadata opens zero tiles at startup. When a tile is inspected, its type and optional nodata/alpha can also be inferred. Otherwise, output type defaults to Byte and output nodata/alpha comes from catalog metadata or explicit options. Source nodata/alpha always controls tile validity when pixels are requested. --stats with --window reports persistent index reader opens/queries, logical source opens, cache hits, selected features, source pixels (excluding separate mask samples), warped destination samples and peak cached datasets. Each tile is warped only over its intersection with the request. Band-subset reads (including translate -b) load and warp only requested bands plus source alpha. Generating output alpha needs validity from all source data bands; a separate dataset-mask request likewise computes coverage from every data band, without allocating a final pixel mosaic. Untagged extra source bands are ignored rather than treated as alpha. Whole mosaics write in strips. Window buffers use fallible allocation so capacity errors return an error.

The supported GTI subset includes IndexDataset, IndexLayer, LocationField, sort order, SRS, resolution, extent, band count/type/nodata and alpha color interpretation. The XSIZE/YSIZE/GEOTRANSFORM metadata triplet (XML XSize/YSize/GeoTransform) also defines a north-up grid. Sizes must be positive integers; incomplete triplets and rotated output transforms are rejected. Tile sources must provide affine geotransforms; pre-warp GCP/RPC or swath sources, or wrap them in warped VRTs. Index and output CRSs must match; use warp on the catalog to change the output CRS. Mosaic bands share one data type and nodata value; per-band nodata/type differences, nested GTI catalogs, GTI SQL/filters, explicit GTI overviews, color-table expansion and rotated GTI output grids are not implemented. Compositing uses f64; Int64/UInt64 values beyond 2^53 may lose precision. Mark a TIFF alpha band on export with --co ALPHA=YES when needed.

Catalogs supplied with lazy raster handles retain their storage and read only selected source windows. Sharing a handle serializes access to its reader and preserves masks, alpha and overviews. Supplied handles remain alive with the catalog; the cache limit bounds selected reader handles, not the caller-owned source map. Native supplied paths open only when selected and close on eviction; encoded files open on first selection.

Catalog exports enumerate nested VRT references before creating their destination, including supplied aliases, masks, dataset containers and archive files. Inventory errors and cyclic references fail before opening the output. A destination within a referenced directory store also triggers a completed read before writing. Supported container selectors are NETCDF:, HDF5:, ZARR: and GPKG:; other driver-style selectors report an unknown inventory instead of claiming they read no files. Materialize such a tile first when its provider cannot enumerate its inputs. An encoded tile that previously failed to open also blocks whole-catalog export, even when other windows remain readable; materialize a successful window first to export only that data. Literal local filenames resembling selectors, such as DATA:1.tif, must use an explicit filesystem path (./DATA:1.tif or an absolute path) to disambiguate them from an unknown driver selector. Remote VRTs require fetching their XML descriptions during export inventory; ordinary remote raster pixels stay unread. Duplicate locations and shared nested VRTs are inventoried once per export traversal; its metadata cache is released when the traversal ends.

Catalog mask caching retains at most 1 MiB of coverage. Larger mask requests return their buffer directly and release the cache instead of retaining and cloning it.

Serialized virtual rasters

terrio warp dem.tif projected.vrt --t-srs EPSG:3857 --ts 512 512
terrio convert projected.vrt crop.tif --srcwin 32 48 128 128

A .vrt warp destination (or --format VRT) writes a GDAL-compatible virtual warp without reading the source pixels. Source references are absolute, so the file can be written in a different directory. GDAL can read the result. Reading a VRT resolves relative sources against its original location, including URLs and archive members, and reads only windows needed for the requested output.

Serialized affine/reprojection, polynomial GCP, TPS, homography and RPC transformers are supported. Warped reads use the declared VRT block boundaries, preserving GDAL's approximation and resampling behavior for cropped requests. Band mappings may reorder or repeat source bands; nodata is retained per band, including UNIFIED_SRC_NODATA=YES, NO and PARTIAL. GeoTIFF file output uses a .aux.xml sidecar when nodata differs between bands or a Float32 nodata value needs its original double precision; keep that sidecar with the TIFF. A standalone TIFF byte buffer has only the shared embedded nodata tag. Raster creation options (compression, tiles, masks, overviews and metadata overrides) are rejected for a serialized warp; set the warp grid through the warp options. GDAL 3.12's generic sum polygon path can lose overlap area on pixel boundaries, including when a redundant identity CRS transformer disables its affine fast path. Terrio preserves conservative affine overlap sums; its results match GDAL's affine path within one Float32 ULP, and can differ from that generic clipping behavior. Serialized cutlines, alpha warps and geolocation transformers are currently reported as unsupported; use a materialized warp for those supported by the normal warper. Custom INIT_DEST values are also unsupported.

VRTDerivedRasterBand supports the scalar built-ins sum, diff, mul, div, mean, min, max, sqrt, real, imag, conj, mod, inv, intensity, log10, dB, dB2amp, dB2pow, pow and exp, with numeric arguments, source transfer types and applicable nodata behavior. Complex-valued derived buffers, external pixel functions and expression languages are unsupported.

A 1×1 unnormalized VRT kernel is applied once, matching GDAL 3.13.3 (coefficient 2 maps sample 3 to 6).

KernelFilteredSource supports odd full/separable kernels, normalization and min, max, stddev, median and mode. Kernels read the necessary halo and replicate source edges as GDAL does. The 3–99 limit applies to --size; explicit matrices can be larger odd squares, matching GDAL. Their work per pixel grows with the number of coefficients. Explicit dataset masks, SourceBand mask,N and ComplexSource with UseMaskBand are read lazily.

Mixed native band types use a common numeric buffer selected with GDAL-style DataTypeUnion promotion, preserving Float64 precision. Plain and derived bands round to their declared type before entering that buffer. A selected lazy band retains its native type. Warped VRTs convert source windows and compute shared blocks in the XML WorkingDataType; if omitted, it is inferred from source, destination and nodata types. The working block converts directly to the requested common buffer, as GDAL's warped dataset RasterIO does. This can retain fractional working values for an integer-declared band in a multiband read; reading that band alone converts to its native integer type. Python GDAL's default ReadAsArray chooses Float32 for any mixed dataset; use its explicit buf_type argument to compare a promoted Float64 buffer.

VRT references use the caller’s normal file and network access. Supplied source maps override matching names; unresolved names are opened by the usual drivers. For heterogeneous plain/derived bands, a shared buffer uses the common numeric type needed by its requested bands. Each band's declared rounding is retained; a leading Byte band does not truncate later floating-point bands.

The Rust VirtualRaster API parses and serializes these descriptions. Its into_lazy source map also permits filesystem-free reads in WebAssembly.

Warped VRT block evaluation is limited to 8,388,608 samples across all bands per declared block. Larger blocks return an unsupported error, including for small subwindow reads, to keep temporary memory bounded while preserving GDAL block semantics.

Coordinate operation safeguards and warp kernel versions

crs and convert accept repeated --ct-opt KEY=VALUE; warp accepts the same coordinate operation controls through its existing --to KEY=VALUE option:

terrio crs EPSG:4610 --to EPSG:4326 110 35 --ct-opt ALLOW_BALLPARK=NO
terrio convert input.shp output.geojson --t-srs EPSG:4326 --ct-opt ONLY_BEST=YES
terrio warp input.tif output.tif --t-srs EPSG:2056 --to ONLY_BEST=YES

ALLOW_BALLPARK=NO excludes approximate datum operations; a CRS pair with no remaining operation fails. ONLY_BEST=YES requires the best registered operation for each point assuming all datum grids were available; it fails if that operation needs a missing grid. Ordinary out-of-domain coordinates remain invalid pixels in a warp. ONLY_BEST alone still permits a ballpark when no better operation exists. Defaults preserve fallback behavior. Warnings report ballparks between different ellipsoids, missing best-operation grids (sampled at the first coordinate with default options, with TERRIO_NETWORK=ON / TERRIO_GRID_DIR guidance), and operations used outside their area of use. Parallel work aggregates warnings, which appear once on stderr and in MCP tool result text. Heights follow GDAL too: between CRSs with heights, keeping a vertical CRS's heights as they are (PROJ's ballpark vertical transformation) fails with ALLOW_BALLPARK=NO, and with ONLY_BEST=YES when a geoid model or vertical transformation exists but does not apply (its grid is missing, or the point is outside it). With default options, heights not converted because a geoid model covering the point did not apply (its grid is missing, could not be read or downloaded, or has no value at the point) are reported by a warning, as a missing horizontal grid is. PROMOTE_TO_3D=YES takes the heights of 2D CRSs as ellipsoidal heights, so that datum transformations change them, as GDAL does after PromoteTo3D on both CRSs (terrio crs EPSG:28992 --to EPSG:4979 --dim 3 85000 447000 0 --ct-opt PROMOTE_TO_3D=YES). Point-cloud reprojection currently rejects --ct-opt because it does not support these controls.

GDAL 3.13 changed warp kernel scales and the bilinear/cubic downsampling threshold. Use warp --kernel-version 3.13 to reproduce that behavior. The default 3.12 retains Terrio's earlier results and the repository's GDAL 3.12 conformance baseline. Both modes keep lazy window/overview reads. Versioned kernels and explicit coordinate-operation policies currently require materialized output rather than serialized warped VRTs.

GeoTIFF and COG translation and warping preserve AREA_OR_POINT=Point, including point tie points and rotated affine transforms, without moving pixel corners.

Writing a PixelIsPoint GeoTIFF requires a CRS. A write with an unknown CRS is rejected before creating the file, avoiding a half-pixel origin shift without a corresponding raster-type GeoKey.

Remote listings limit each HTML index to 4 MiB and each object-store XML page to 16 MiB; larger pages fail explicitly.

Recursive copy destinations must remain within their destination tree, including ordinary local copies: directory symlinks that escape it are refused. Same-device directory moves rename the directory without copying payloads and report zero copied files/bytes. Plain HTTP source discovery currently probes the directory-index URL before the file range request; a file can therefore cost an extra GET, usually a 404.

Raster comparison reads TIFFTAG items, embedded RPC tags and PAM sidecar metadata from local filesystem inputs. HTTP and archive comparisons use the reader's metadata and do not add those local-file extras. Comparing a local file to its remote/archive copy can therefore report metadata differences; use the applicable metadata/RPC skip options for a pixel-focused comparison across those locations.

Raster management

The vector commands create, edit and update retain their existing names. Use create-raster, edit-raster and update-raster for raster management:

terrio create-raster filled.tif --size 256,256 --ot Float32 --burn 7 --crs EPSG:3857 --bbox 0,0,256,256
terrio create-raster template.tif --like dem.tif --copy-metadata --copy-overviews
terrio edit-raster filled.tif --nodata -9999 --metadata 'SOURCE=A & B' --stats
terrio edit-raster filled.tif --t-srs EPSG:4326 --error-threshold 0.1
terrio overviews filled.tif 2 4 --resampling average
terrio overviews filled.tif --refresh --bbox 0,0,128,128
terrio update-raster replacement.tif filled.tif --resampling bilinear
terrio overviews filled.tif --clean

Overview methods are nearest, average, min, max and mode. Existing unrequested levels are retained. --external manages the supplied filename's .ovr sidecar, including when that filename is a symlink. Explicit compression applies to requested rebuilt levels. Updates retain destination pixels outside valid source coverage and refresh affected overviews unless --no-update-overviews is set. Georeferencing moves change the affine transform and CRS without resampling; --t-srs conflicts with other edits, and requires --error-threshold to write. A raster without a geotransform of its own (unreferenced, or georeferenced by ground control points) cannot be moved. With --like, a template without a geotransform gives its GCPs, or no georeferencing, instead of a unit-pixel grid.

In-place operations require writable little-endian GeoTIFFs. Updating a COG requires --ignore-cog-layout-break and can invalidate its optimized layout. Edits use a temporary file and rename; a failure before replacement preserves the original. A tag of a field type the TIFF specification does not define is refused rather than dropped from the rewritten file. Updating external overviews occurs after the base TIFF commits, so an error there can leave stale overviews. Update working buffers span full-width rows intersecting the update window; memory can be large for wide rasters.

Raster update requires equal source/destination band counts and treats alpha as an ordinary band; automatic RGB-to-RGBA alpha synthesis is not supported. Existing per-dataset masks remain unchanged, as in the GDAL 3.13.3 update probes. Valid values can saturate to integer type bounds or collide with destination nodata, also matching those probes. Average updates between identical affine pixel grids use direct copying when their origin offsets are integer to within 1e-7 pixels; fractional offsets retain area-based resampling. Min/max overview methods are Terrio extensions; GDAL 3.13.3 gdaladdo refuses them.

Raster in-place edits copy the whole TIFF, including for metadata-only changes, and need free disk space for that copy. Symlink targets and mode bits are preserved; replacement detaches hard links and does not preserve ownership, ACLs or extended attributes. The containing directory must be writable. The TIFF is committed before its histogram or external-overview sidecar: a later sidecar error can leave the TIFF updated with an unchanged sidecar.

Building or wholly refreshing overviews reads the full-resolution sources the levels need (the image, its mask, or both) once, forward, in full-width strips of whole blocks (about 64 MB of values at a time, and at least a row of blocks, which on very wide tiled rasters is larger). Every level computed from full resolution is computed in that pass: in GDAL's chunks of rows (GDALRegenerateOverviewsEx, 5 to 10 MB of values per band), or in strips holding every source window (GDALRegenerateOverviewsMultiBand's schedule), with the pixels of a whole read. A level computed from the previous one reads that level, held in memory. Memory is therefore bounded by a read strip, a chunk and the levels: the first level (a quarter of the pixels for factor 2) and the one being computed, or every level at once (about a third of the pixels for 2, 4, 8...) when GDAL computes each from full resolution. That is the case for nearest overviews, and overviews of rasters with a mask, which GDAL computes band by band: planar images, and uncompressed external overviews of images without an alpha band. A striped TIFF is read in whole strips, so a raster stored as a single strip is read whole. On a 16384x16384 Float32 DEM, five internal levels peaked at 478 MiB instead of 4.1 GiB. Partial refreshes (--refresh --bbox, --like) read the full-width rows over their region, and the whole image once when the region spans its height. Overview and raster-update arithmetic uses Float64 and warns for Int64/UInt64: values above 2^53 may lose precision, including nearest/mode overviews. Raster update holds the warped window and the full width of the destination rows it rewrites in memory.

blend

terrio blend COLOR OVERLAY OUTPUT --operator multiply --opacity 75 composes same-sized Byte images by pixel position. CLI/MCP blend, Rust ops::blend::blend, Python terrio.blend(src, overlay, dst=None, operator=..., opacity=...) and JavaScript blend(src, overlay, {operator, opacity}) share the implementation.

Operators are src-over (default), hsv-value, multiply, screen, overlay, hard-light, darken, lighten, color-dodge and color-burn. Opacity is an integer percentage from 0 to 100 (default 100). One/two bands mean gray/gray-alpha; three/four mean RGB/RGBA, independently of color interpretation tags. GeoTIFF and PNG indexed images expand their palettes to RGBA. HSV requires RGB/RGBA color input; darken/lighten require equal color channel counts. Other types and complex images are refused. No alignment or reprojection is performed: prepare matching inputs with warp first. Transparency uses alpha; nodata and separate masks do not affect composition, matching GDAL. Palette expansion uses palette transparency and makes the palette nodata entry transparent.

Output band count follows the color input, except multiply/screen/overlay/hard-light swap inputs when the overlay has more bands, as GDAL does. Output CRS and transform follow that effective color input. Outputs have color interpretations but no nodata, packing scales or descriptive metadata. Inputs and output are materialized in memory; this command does not offer GDAL's lazy GDALG pipeline output or append-as-subdataset mode. Standard raster format/compression/creation options apply.

GDAL 3.13.3 has unsafe optimized paths when a non-swapping operator receives more overlay bands than color bands, and treats gray-alpha channels inconsistently in src-over. Terrio uses explicit gray/alpha semantics for these inputs. Conformance expands those reference inputs to RGBA and selects the corresponding output channels; it does not compare against GDAL's corrupted or misinterpreted output. GDAL's HSV fast path can repeat the first alpha row; Terrio retains the complete base alpha. Reference source-over and HSV reads use individual bands of a lazy GDALG pipeline to avoid those fast paths. The other operators use whole-dataset reference kernels: GDAL 3.13.3's generic per-band kernels process only the first sample.

Palette conversion

terrio rgb-to-palette INPUT OUTPUT quantizes Byte RGB imagery with GDAL's median-cut palette generation and integer error diffusion. --color-count 2..256 (default 256), --bit-depth 5|8 (default 5), --no-dither, --color-map FILE and --output-nodata INDEX control generation/mapping. Color count applies only to generated palettes; when --color-map supplies a table, that table determines the color count and --color-count is ignored. A color map can be an indexed raster, indexed GDAL/GRASS text, RGB GMT CPT (with a # COLOR_MODEL = RGB header) or QGIS QML/QLR file. RGB color interpretations select the channels when present; otherwise the first three bands are used. Shared RGB nodata excludes pixels from palette generation. If no shared RGB nodata value exists, dataset masks (or alpha validity) select valid pixels. This precedence follows GDAL; masks do not add exclusions when shared RGB nodata exists. Explicit Python/JavaScript masks use this same precedence. Indexed CPT tables retain declared segment endpoints; interior indices are padding, matching GDAL palette conversion. DEM color relief interpolates ramps separately. UTF-8 BOM-prefixed QGIS XML is recognized without a QML filename hint. GDAL also tests vertically diffused RGB values for nodata, so a valid near-black pixel can become nodata after dithering. An automatically generated palette reserves index zero for invalid pixels; a supplied map uses invalid-pixel handling only when --output-nodata is given, following GDAL. Reserving a nodata index inserts a table slot, shifts later entries and drops the old last entry if the table would exceed 256 entries; this also follows GDAL for supplied palettes. A nodata index beyond the table pads it with transparent black, and valid pixels may map to those padding entries, as in GDAL's virtual output. GeoTIFF normalizes their alpha to opaque unless the entry is the declared nodata index.

terrio palette-to-rgb INPUT OUTPUT [--rgba] expands indexed pixels to three or four Byte bands, and terrio attach-palette PALETTE INPUT OUTPUT replaces a Byte/UInt16 raster's table without changing its indices. Expansion/attachment preserve dataset masks in returned handles and GeoTIFF/COG outputs; other formats omit separate masks. Source nodata becomes transparent on RGBA expansion; unmapped indices become black in RGB and transparent black in RGBA. GeoTIFF stores RGB table entries and nodata transparency; arbitrary per-entry alpha requires a format such as PNG.

All three commands accept raster output options and require --overwrite to replace an existing output. Inputs and their local symlink/hard-link aliases are protected from replacement. These operations currently materialize the full input/output. They do not append subdatasets, execute GDALG pipelines or support non-Byte RGB quantization. QGIS XML alpha values are honored explicitly, rather than reproducing GDAL 3.13.3's XML alpha parsing bug.

Rust exposes ops::palette, Python exposes rgb_to_palette, palette_to_rgb, attach_palette, read_color_table, and JavaScript exposes rgbToPalette, paletteToRgb, attachPalette, readColorTable. Python accepts paths and Raster objects; JavaScript accepts Dataset objects/uploaded files and color-file text or RGBA arrays. CLI commands are also available through MCP.

Raster/vector indexes and GTI creation

terrio raster-index tiles/ catalog.gpkg --recursive --filename-filter '*.tif' --absolute-path
terrio gti-create tiles/ catalog.fgb --filename-filter '*.tif' --absolute-path \
  --output-crs EPSG:4326 --resolution 0.001,0.001 --band-count 1 --ot Float32 \
  --nodata=-9999 --color-interpretation gray
terrio vector-index roads.gpkg counties.geojson index.gpkg --accept-different-schemas \
  --source-crs-field-name source_crs --output-crs EPSG:4326

raster-index writes affine corners when retaining the source CRS, and the suggested warped-grid envelope when changing CRS; gti-create uses the same operation with optional GTI virtual-raster metadata. vector-index writes one layer-envelope polygon with filename,layer_index locations; --dataset-name-only omits the suffix. --source-layer-name and zero-based --source-layer-index are repeatable and select their union across all inputs without renumbering layers. A selector absent from one input is allowed; absence from every input is an error. Different vector schemas/CRSs are skipped with warnings unless accepted with the corresponding flags; an explicit --output-crs reprojects corners. Empty vector layers are skipped.

Inputs can be files, local directories, explicitly slash-terminated VSI directories, or @FILE lists. Directory scanning supports --recursive and repeatable case-sensitive --filename-filter patterns (* and ?). --absolute-path stores absolute local paths. Otherwise, relative native path inputs are resolved relative to the written index directory, even when it differs from the working directory. In-memory source mappings keep their explicit stored locations verbatim; supply locations appropriate for the eventual catalog directory. Remote locations keep their query parameters, which can include access credentials; a warning is emitted without printing them. Use credential-free references and environment credentials when sharing catalogs. Signed-source open/list errors also omit query-bearing driver diagnostics. Raster --skip-errors skips unreadable inputs with a warning. Invalid options and typed-metadata conversion failures remain errors. Scanning without a filename filter is strict, as in GDAL: unrelated files and sidecars can fail the run. Use a filter or raster --skip-errors for mixed-content directories. Lazy-capable raster readers only inspect headers. Local GeoPackage/FlatGeobuf vector inputs use cached bounds when present; these cached bounds are trusted, so stale source extents must be refreshed before indexing. Other paths, including remote datasets, scan features in chunks where supported.

Both index commands support --location-name, --output-layer, --output-crs, --source-crs-field-name, --source-crs-format auto|WKT|EPSG|PROJ, --format and repeatable --metadata KEY=VALUE. Raster layer names default to the output basename; vector layer names default to tileindex. These defaults also apply to Python and JavaScript serialization; before a raster index is given an output filename, its in-memory layer uses the placeholder tileindex. Explicit names are retained. Without an explicit target CRS, raster indexes skip incompatible source CRSs unless a source-CRS field is requested; with that field, they retain original coordinates and warn, matching GDAL. Supply a target CRS for a usable mixed-CRS catalog. Raster reprojection indexes the suggested warped-grid envelope, as GDAL 3.13 does. Accepted mixed-CRS vector inputs retain original coordinates and warn; request a source-CRS field to retain their identity, or an output CRS to reproject. Consumers of mixed native-coordinate footprints must honor the per-feature CRS; the layer CRS alone does not describe every footprint. Vector reprojection transforms envelope corners without densification. Both require known source CRSs. For nonlinear transformations, vector corner envelopes can underestimate curved edges and miss features in spatial queries. Keep the source CRS, or construct densified footprints, when full edge coverage is required.

Raster pixel-size filters use sqrt(envelope_area / source_pixel_count) after reprojection, including rotated rasters. The area uses the suggested warped-grid envelope, while the pixel count remains the original source width times height. Repeatable --fetch-metadata KEY,FIELD,TYPE copies header metadata as String, Integer or Real; the reserved GDAL key {PIXEL_SIZE} selects the computed size. Missing metadata becomes null; invalid numeric metadata is rejected. Integer {PIXEL_SIZE} fields truncate toward zero, matching GDAL. Non-finite Real metadata follows the output driver's representation; GeoPackage stores NaN as null, as GDAL does. GTI options include --resolution x,y, --bbox xmin,ymin,xmax,ymax, --ot, --band-count, repeatable/comma-separated --nodata, --color-interpretation and --mask. Nodata/color lists must contain one value or one per output band, inferred from the first source when --band-count is omitted. With explicit GTI characteristics, included sources must provide at least that many bands; a smaller explicit output count selects the leading bands. Ordinary footprint inventories retain heterogeneous band counts with a warning, as GDAL does; they may require --band-count for GTI pixel reads. Palette-to-RGB band expansion is not performed by the index writer. GeoPackage/FlatGeobuf preserve GTI metadata; open them with GDAL as GTI:catalog.gpkg or GTI:catalog.fgb. For relative tile references in GDAL 3.13.3, use gdal raster info --if GTI catalog.gpkg (or gdal.OpenEx(..., allowed_drivers=["GTI"])): that version has a GTI: prefix resolution bug during pixel reads. Terrio resolves these references directly against the index directory. Custom raster location fields are recorded in LOCATION_FIELD.

This release creates new indexes and atomically replaces single-file outputs with --overwrite. Indexed source paths and aliases cannot be replaced, including files opened through Python mapping values whose stored labels differ from their paths. Unreadable inputs skipped by --skip-errors remain protected too. Shapefile outputs publish all components from staging and require a new basename even with --overwrite; .shp.zip writes an archive. GTI/custom metadata require GeoPackage or FlatGeobuf. Append/update, layer replacement, STAC-GeoParquet profiles, XML companion output and GDAL-specific creation options are outside this supported subset. Shapefile field width/type limits still apply. The three commands are also exposed as MCP tools.

Palette commands reject masked PMTiles/MBTiles outputs, including materialized results, because these writers cannot retain the separate dataset mask. GeoTIFF/COG preserve it.

Palette text/XML channels outside 0–255 are rejected. GDAL 3.13.3 wraps invalid RGB text channels; Terrio deliberately rejects those malformed inputs. Text Color-file XML is limited to 65,536 elements and 256 nesting levels; color files are limited to 10 MiB while streaming, including when their size is unavailable.

Parquet metadata files

terrio parquet-create-metadata part-0.parquet part-1.parquet --output _metadata --json creates a metadata-only file referencing the explicitly supplied partitions in input order. Inputs may repeat, including aliases of the same physical file: each occurrence contributes its row groups and rows again. Supply each partition once for a deduplicated catalog. --overwrite replaces an existing output; direct inputs and externally referenced column files are protected. The CLI command also supplies the parquet-create-metadata MCP tool.

Input schemas and Arrow field metadata must agree. GeoParquet coordinate systems, encodings and column definitions must agree; bounds and geometry types are merged. A missing bound omits that column's aggregate bound, and an unknown geometry type keeps the aggregate type unknown. Mixed 2D/3D bounds and antimeridian-wrapped bounds omit the optional aggregate bound rather than inventing Z values or an incorrect Cartesian extent. Other file metadata is retained only when identical across inputs; JSON reports omitted_metadata_keys, file/row-group counts, rows and encoded output bytes. Arrow schema metadata is regenerated to carry the merged GeoParquet metadata.

The operation reads headers and footers without decoding data pages and does not validate page integrity. Footer buffers grow with bytes received; complete footers and their decoded metadata are materialized, so memory scales with metadata size. Input and output must share a filesystem namespace (native, /vsimem/, or one writable S3/Azure namespace) so column paths remain relative. Query-bearing source URLs cannot be persisted as partition references. Archive inputs cannot target another filesystem. Native symbolic-link outputs are refused. This command does not discover partitions or change the existing partition command's output behavior.

Column file_path values are logical relative paths. URL transport paths are decoded once when deriving them and encoded once when resolving them; spaces and literal percent characters remain distinct. Object-store keys retain their logical spelling.

Parquet page indexes remain in the referenced partition files. The metadata file retains column/offset-index locations relative to each column's file_path, just as it retains the column data-page locations; it does not copy those payloads into _metadata.

GeoPackage repacking

terrio gpkg-repack survey.gpkg --json compacts a native GeoPackage in place. Repacking rejects malformed table key ordering and child keys outside their parent divider ranges, matching the stricter shared SQLite traversal used by validation. The gpkg-repack MCP tool accepts the same arguments. JSON reports original and compacted byte/page counts and the preserved SQLite page size.

Repacking rebuilds every B-tree from its encoded records, preserving rowids, SQL objects, application/user versions, text encodings, rasters and vector metadata. It supports expression, partial, descending and collated indexes, WITHOUT ROWID tables, overflow data, R-tree shadow tables, and auto-vacuum/incremental-vacuum pointer maps. Physical page layouts and final sizes can differ from SQLite VACUUM; the operation does not promise byte-identical files.

Native changes use SQLite-compatible exclusive locks and a synced rollback journal. All original data pages, including unchanged pages and pages removed by truncation, are journaled before a full-image write. SQLite's unused lock-byte page is excluded because its page number is a super-journal marker. An interrupted rewrite can be recovered by SQLite. The database pathname is checked against the locked handle before journal creation. Do not rename or replace an open database or its parent directory during a transaction. On Unix, directory sync errors abort before database writes; filesystems without directory fsync are unsupported for this operation. Windows flushes the journal and database file handles; directory entry durability follows the platform. In-memory replacement refuses a concurrent change to its source image, allowing the caller to retry. A database locked by another process or with a nonempty WAL sidecar is refused. POSIX locks are process-scoped; callers must close/checkpoint same-process SQLite connections before repacking and must not access the database concurrently through them. Native symbolic-link or hard-linked dataset filenames, linked/special sidecars, reserved/encrypted page bytes, remote and archive paths are refused. This is storage maintenance, not a GeoPackage specification validator.

Native in-place append/update also refuses hard-linked database targets: another alias may have its own WAL. It rejects linked or special journal/WAL/shared-memory sidecars before recovery; it never replays a journal through a symbolic link or hard-link alias. Native append/update follows filename symlinks to their target and checks that target's WAL.

Unix directory-sync requirements also apply to existing native SQLite in-place edits and recovery, including append/update. Unreadable directories and filesystems rejecting directory fsync can fail before page writes. A sync failure after journal deletion explicitly reports that changes committed or recovery completed but durability is unconfirmed; callers must not blindly retry an append.

Native repacking also accepts GeoPackage contents under opaque filenames such as snapshot.zip; explicit archive locations remain unsupported. On Unix, rollback journals inherit the database's permission bits, restricted further by the process umask. A nonempty WAL is refused before any rollback-journal recovery. Other format checks inspect the recovered header, so successful recovery can precede a format-specific refusal. If initial journal-directory synchronization fails, the unchanged database and synced rollback journal are retained for recovery after the filesystem issue is resolved; filesystems without directory synchronization are unsupported.

The rollback journal can approach the original file size, plus per-page record headers. Peak database-plus-journal disk usage can approach twice the original size; write I/O includes both the journal and replacement database image. Reserve disk space accordingly.

Repacking scans and rewrites the complete database. It owns one input image and builds one contiguous output image; encoded records/cells for the largest tree are also materialized. Memory depends on row shape/count and is not hard-capped. A local 2.01 GiB live-BLOB derivative used about 6.04 GiB peak RSS; its mostly deleted counterpart used about 2.06 GiB.

WebAssembly uses /vsimem/ paths or the bytes API. A bytes input must be a self-contained, recovered/checkpointed image; external journal/WAL files are not part of it.

Multidimensional mosaics

terrio mdmosaic INPUT... OUTPUT [--array PATH_OR_NAME] combines numeric scientific arrays on their indexing-variable coordinates. Repeat --array to select multiple arrays; by default, all arrays with at least two dimensions are selected. Sources can be netCDF/HDF5 files or local/remote Zarr stores. The output must be a new .nc (netCDF-4) file or .zarr (Zarr v2) store. The command is also exposed as the mdmosaic MCP tool.

Sources contributing to an array must agree on its full path, numeric type, nodata, packing scale/offset, units and dimension names/order. Dimensions that change across inputs need a finite, numeric, one-dimensional indexing variable. Unindexed dimensions, including CF bounds axes, are retained positionally and must keep the same size across inputs. Regular axes must share spacing and grid alignment; disjoint irregular ranges and axes made entirely of irregular singleton time slices are also supported. Last input wins overlaps, including nodata pixels. Gaps on regular grids, or missing combinations of coordinates across axes, are filled with _FillValue, falling back to a scalar CF missing_value, or raw zero if neither is declared. An array-valued missing_value is retained as metadata but does not select a unique gap fill value.

The shared Rust multidim::mosaic creates lazy array handles: it reads coordinate one-dimensional indexing variables to plan the mosaic but fetches data only for intersecting hyperslabs. Python mosaic_multidim(sources, dst=None, arrays=None) and JavaScript mosaicMultidim(sources, {arrays}) expose the same engine. Numeric values retain their native types, including Int64/UInt64 precision. Input handles may be closed after construction; mosaic views keep their sources alive. netCDF snapshots currently materialize arrays; Zarr snapshots iterate output chunks.

Selected arrays automatically retain their auxiliary coordinates, CF bounds and scalar CRS definitions. Auxiliary coordinates and bounds must resolve to the same array paths across contributors; equivalent scalar CRS aliases can use different paths when their metadata agrees. Conflicting definitions and missing dependencies are rejected. Extended grid-mapping coordinate tuples retain their declared order. Auxiliary payloads stay lazy. Reference attributes keep unambiguous local names and use explicit paths when merged groups would otherwise rebind them. GDAL 3.13.3 automatic coordinate discovery does not resolve absolute coordinate names; those cross-group arrays/attributes remain readable, and Terrio resolves them according to CF group scoping.

Terrio retains source group paths and metadata; GDAL places explicitly selected nested arrays at the root. It also retains integer coordinate types rather than GDAL's synthesized Float64 coordinates. Float32 indexing variables use their native precision when recognizing regular grids and placing tiles; gap cells are retained. Off-grid origins beyond that precision allowance are rejected; the allowance is capped at 0.001 grid cells. This can reject mathematically regular Float32 coordinates when large origins or wide tile separation amplify rounding beyond that cap (including 40 + index × 0.1 at 900-cell separation, also rejected by GDAL 3.13.3). Such inputs require higher-precision coordinates before mosaicking. Per-tile actual_range metadata on coordinates and data arrays may differ and is removed from the mosaic. Coordinate comment, history, long_name, source and references may differ; the first input supplies them. Other coordinate attributes, including validity limits, units and calendar, must match to preserve their interpretation. Source chunk shapes and other compatible descriptive metadata are inherited from the first input. Full-array reads and netCDF snapshots materialize the requested data; use selected reads or chunked Zarr output for large mosaics. Extended scalar CF grid_mapping clauses are retained; unresolved mapping references are rejected. Irregular axes are sorted with equal coordinates deduplicated in either direction.

Coordinate planning checks the actual in-memory element width before reading coordinates or allocating generated grids. Combined input coordinate storage per axis and each generated coordinate buffer are limited to 256 MiB. This is not a cap on data-array size: views remain lazy, Zarr writes iterate chunks, and netCDF snapshots materialize the requested data. String/compound arrays, serialized multidimensional VRT output, creation/open options, automatic glob/list-file expansion and replacement of existing outputs are outside this supported subset. Output replacement is refused before writing. Descending axes work directly in Terrio; comparisons normalize GDAL 3.13.3 inputs because its descending-axis VRT size/offset computation can produce invalid dimensions, then restore the coordinate order for comparison.

A singleton mixed with a regular multidimensional series must align to that series; off-grid mixed slices are rejected, as in GDAL 3.13.3. Data-array valid_range, valid_min, valid_max, missing_value, grid_mapping and _Unsigned must agree across contributing inputs, in addition to scale, offset, units and nodata. Numeric integer/float metadata compares by value.

An array present in only some inputs is retained; regions belonging to other inputs use that array's nodata value, or zero when it has none.

GeoPackage validation

terrio gpkg-validate data.gpkg --json
terrio gpkg-validate data.gpkg --full-check --strict
terrio gpkg-validate icu-created.gpkg --unicode-case

The pure Rust validator reports valid, complete, version, errors (a numeric core or named extension requirement and message), warnings and unsupported_checks. Exit status is 0 for a valid image, 1 for invalid data or strict-mode warnings, and 2 when unsupported checks prevent completing validation (even when another check has already proven the image invalid). Text summaries identify both invalidity and incompleteness when both apply. Execution errors, including missing inputs and refusals caused by live sidecars, exit with status 1 and return an error diagnostic rather than a JSON verdict. The MCP gpkg-validate tool is read-only: a completed query returns its structured verdict with isError=false; CLI verdict statuses are appended as diagnostic text. Execution failures still return isError=true.

Checks cover SQLite structure, foreign keys and indexes; GeoPackage core and extension metadata; geometry WKB; raster tile payloads and registered views. --full-check adds advisory attribute storage, range, length and date checks; --strict makes warnings invalidate the verdict. Stock ASCII case functions are the default; --unicode-case selects Unicode lower/upper/LIKE behavior for images created with ICU SQLite. NOCASE collation retains SQLite's ASCII behavior.

Native paths and /vsimem/ images must be closed/checkpointed. Validation reads the complete database image once, then makes multiple in-memory record passes for different rule groups. It refuses nonempty WAL or rollback-journal sidecars; it never checkpoints or modifies the file. Uploaded bytes are supported by the Rust/Python/JavaScript APIs. Filename requirement 3 applies only to path inputs.

Supported views include projections, filters, DISTINCT, ORDER/LIMIT/OFFSET, nested views, inner/cross/left/right/full/natural joins and compound SELECTs. Aggregates, CTEs, subqueries, window functions, extension loading and unimplemented functions yield an explicitly incomplete verdict. The image stays in memory; there is no configured input-size ceiling, so reserve memory for the entire file plus the retained validation state before validating large databases. Ordinary user rows stream, while metadata, integrity keys, DISTINCT/set values and sorted candidates are retained. Join views additionally retain all right-hand input rows, including geometry blobs, once per evaluation. Each join evaluation is limited to 1,000,000 work units, counting retained right-input rows and candidate row pairs across all join levels and nested views. A shared 128 MiB budget covers right-input caches, candidate rows, sorted rows, DISTINCT keys and compound-set working rows, including text/blob payloads. Key/set reservations are conservative and can exceed the eventual retained result size; compound reservations last until evaluation ends. A separate 1 GiB cumulative candidate-copy budget bounds repeated copying of large join payloads, and is charged before the candidate is constructed. Reservations are released on success or failure. Exceeding a budget yields an incomplete verdict; multi-way nested-loop combinations can otherwise grow as the product of row counts. Inputs are not rescanned for each outer row. Extension/metadata/R-tree/schema rules that have not evaluated a referenced view, and tile views not evaluated by the tile validator, are explicitly incomplete. Unsupported schema declarations are also reported as incomplete, rather than proven SQLite corruption. The same applies to CHECK/generated expressions outside the implemented expression subset, including JSON extraction operators. json_valid supports SQLite's 1,000-level JSON nesting limit without constructing a recursive JSON value. LIKE/GLOB matching reuses its state buffers and refuses estimated work above 10,000,000 units per call, yielding an incomplete verdict; this includes the cost of character-class ranges.

Checks are stricter than GDAL for some malformed geometry, image and metadata payloads. See validation details and known differences.

Vector clipping

terrio vector-clip clips points, lines and polygons to a bounding box, a polygon/multipolygon supplied as WKT or GeoJSON, or a dataset template:

terrio vector-clip roads.gpkg clipped.gpkg --bbox=-115.2,36.1,-115.1,36.2
terrio vector-clip places.fgb clipped.geojson --geometry 'POLYGON ((0 0,2 0,2 2,0 2,0 0))'
terrio vector-clip parcels.gpkg clipped.gpkg --like zones.gpkg --like-layer study_area
terrio vector-clip roads.gpkg clipped.gpkg --like dem.tif
terrio vector-clip parcels.gpkg clipped.gpkg --like zones.gpkg --like-where 'keep=1'

Supply exactly one of --bbox, --geometry or --like. Bbox and WKT coordinates use each input layer's CRS unless --bbox-crs or --geometry-crs is supplied; GeoJSON defaults to WGS84 or its embedded CRS. Only the clipping region is reprojected; input coordinates and CRS are retained. Region vertices are transformed without densification, so edges are straight in the input CRS after reprojection, as in GDAL vector clipping. --layer selects input layers. A vector template's polygons are unioned, with --like-layer, --like-where or --like-sql selecting/filtering its features. SQL uses Terrio's existing documented vector SELECT subset. A filter selecting no polygonal features, or a template containing only null geometries, produces an empty output with success status. Raster templates use rectangular bounds, including rotated-grid bounds; lazy raster pixels are not decoded.

Polygon holes and boundary points/lines are included. Null/empty intersections are omitted. Single geometry layers split multipart results and discard lower-dimensional contacts, as GDAL does. Multi geometry layers retain their declared type; unspecified/collection layers retain boundary-only intersections. Attributes, schema, layer name and CRS are retained, with output IDs sequential from 1. Points retain Z/M; line cuts interpolate source Z/M, while constructed polygon topology is XY. Invalid geometries are refused; repair them first with make-valid. Exact inclusion at a transformed boundary can differ from GDAL by a few floating-point units. Reprojection does not snap input points to the transformed boundary. If a polygon result cannot be reconstructed as valid geometry at floating precision, clipping returns an error rather than silently dropping features or accepting the coarser integer-grid result.

Outputs use the usual vector format inference, --format, --output-layer, --append and --overwrite options. Existing outputs are refused unless an explicit write mode is selected. This is a preflight existence check; the shared vector writers do not guarantee exclusive publication against a concurrent writer. GDAL-specific creation/open options, upsert and skip-errors are not separate options of this command.

Rust uses ops::vector_clip::{clip, ClipRegion, ClipTemplateOptions}. Python terrio.clip_vector(src, bbox=[...]) returns one Layer (choose layer= when needed); unlike the CLI and JavaScript, Python does not return several input layers in one call. geometry=, like=, crs=, like_layer=, like_where= and like_sql= provide the corresponding options. Python accepts vector/raster paths and in-memory data as templates. JavaScript clipVector(src, {bbox: [...]}) returns a Dataset, with geometry, like, crs, layers, likeLayer, likeWhere and likeSql; templates may be Datasets, uploaded bytes or vector GeoJSON.

Check polygon coverages

terrio check-coverage counties.gpkg invalid.gpkg reports invalid polygon boundary segments: overlaps, duplicated interiors and mismatched nodes on shared edges. --maximum-gap-width 10 additionally flags narrow gaps in input CRS units; zero (the default) disables gap detection. Gaps narrower than the threshold require sufficiently long, nearly parallel edges, following JTS/GEOS coverage validation. Use a projected CRS when the threshold should represent metres.

Each input polygon must individually be valid; repair it before checking the coverage. Polygon and multipolygon features are supported, including holes. Nulls and geometry collections are rejected. Gap searches clamp overflowing search bounds; unrepresentable projections return an explicit precision error. The operation loads the layer into memory and uses indexed edge and interior tests; it does not fix or simplify the coverage.

Output is a MultiLineString layer called invalid_edge, with CRS and original XY edge coordinates retained after consecutive XY repeats are normalized. Diagnostic lines are two-dimensional even for Z/M inputs, matching GDAL. Source attributes are omitted, and output IDs are sequential from 1, as in GDAL. --include-valid retains valid source features with empty line geometry, maintaining one-to-one correspondence. Multiple selected input layers are checked independently and named invalid_edge_{input_layer}; use --layer to select input layers or --output-layer to name a single result. The usual vector output format, append and overwrite options apply. This command also supplies the MCP tool. Terrio has one geometry field per layer; GDAL geometry-field selection, upsert, skip-errors, no-create-empty-layers, GDALG output and GDAL driver-specific options are outside this command's subset.

Rust exposes ops::coverage::{check_coverage, CoverageCheckOptions}. Python terrio.check_coverage(src, maximum_gap_width=0.0, include_valid=False, layer=None) returns one Layer, accepting the usual vector path, dataset, layer or GeoJSON inputs. JavaScript checkCoverage(src, {maximumGapWidth: 0, includeValid: false}) returns a Dataset and checks each input layer independently.

Simplify polygon coverages

terrio simplify-coverage counties.gpkg simplified.gpkg --tolerance 100 applies Visvalingam-Whyatt simplification to common coverage edges. The tolerance is the square root of the triangle-area threshold, in input CRS units. Shared nodes and matching boundaries remain consistent across adjacent features. Use a projected CRS for metre-based tolerances. --preserve-boundary retains exterior and hole boundaries, simplifying shared internal edges only.

Input polygons must individually be valid and form a valid coverage; run check-coverage first. The operation materializes each layer and does not repair invalid coverages. Polygon and MultiPolygon features, including empty polygons, are supported. Null and non-polygon active inputs are rejected. The feature, polygon-part and hole counts and Polygon/MultiPolygon kinds are retained. Rings can simplify to three distinct vertices (four coordinates with closure), matching GDAL 3.13.3 with GEOS 3.14.1. Attributes, names and CRS are retained. Output feature IDs are sequential from 1, as in GDAL's default writer.

--layer selects input layers. --active-layer NAME processes one selected layer and copies the other selected layers, including non-polygon data; output IDs are new in those layers too. An unknown active name is an error. Standard vector format, output-layer, append and overwrite options apply. GeoPackage output retains the input declaration, including Unknown, instead of inferring a concrete type from homogeneous features. GDAL geometry-field selection, upsert, skip-errors, GDALG output and driver-specific options are outside this command's subset. The command also supplies the MCP tool.

Rust exposes ops::coverage_simplify::{simplify_coverage, simplify_coverage_layers, CoverageSimplifyOptions}. Python terrio.simplify_coverage(src, tolerance, preserve_boundary=False, layer=None) returns one Layer. With active_layer="counties", it returns a VectorDataset, simplifying that layer and copying the other input layers. layer and active_layer are mutually exclusive. JavaScript simplifyCoverage(src, tolerance, {preserveBoundary: false, activeLayer: "counties"}) returns a Dataset, copying inactive layers. Inputs use the usual vector paths, datasets, layers and GeoJSON forms of their respective bindings.

For general GeoPackage writes, Rust VectorWriteOptions, Python Layer.write, Layer.to_bytes, VectorDataset.write and write_vector offer preserve_declared_geometry_type=True. JavaScript Dataset.toBytes accepts preserveDeclaredGeometryType: true. This option applies to GPKG output and retains base geometry declarations when creating/replacing layers; appending to an existing layer keeps its base geometry declaration, with dimension flags promoted to optional when needed to retain mixed layouts. The existing default infers the common feature type. The pure Rust GeoPackage encoders preserve M values by default, independent of this option, for native files, memory outputs and streams. Mixed measured and unmeasured geometries use optional M declarations, preserving their layouts. Streamed Z flags also reflect any coordinate with Z, matching the encoded payload even when the first coordinate is two-dimensional; absent ordinates are encoded as zero. This intentionally changes the default streaming and whole-layer output: later Z values previously dropped in that layout now survive, and vertices lacking Z receive 0.0. Missing M ordinates within a measured geometry likewise receive zero. This encoding rule applies independently of declaration preservation. Ordinary updates retain the in-place transaction path, with the existing atomic rewrite fallback when in-place editing is unsupported.

General CLI writers currently infer geometry declarations and do not expose a preserve_declared_geometry_type flag. simplify-coverage enables declaration preservation for its GeoPackage output. This is a scope limit of the general CLI/MCP writer options; the simplification operation itself preserves declarations across all public surfaces.

Retained simplified vertices keep their Z values; GEOS drops M values from active polygon layers. Inactive layers retain their coordinates, including Z and M in GeoPackage output. Unknown GeoPackage declarations allow optional Z. The shared Layer model does not store source required/optional dimensional declarations, so this option cannot reproduce those hints for empty layers or explicitly required Z/M declarations. This is a metadata limitation even when coordinates match.

Coverage simplification follows GDAL 3.13.3 / GEOS 3.14.1 and JTS when reassembling shared edges: exterior/hole vertex direction can change with input feature order. Ring direction is not preserved; polygon roles and topology are preserved. For example, reversing the two-feature shared-zigzag fixture yields signed exterior areas +8 and -8 in both Terrio and GDAL.

For simplifyCoverage, JavaScript layer selects a single output layer and is mutually exclusive with activeLayer, which copies inactive layers. Interior shared-span vertices take Z from the representative edge encountered first. Shared endpoints use the preceding output span's Z; the first ring vertex uses the following span's Z. Neighbors can therefore retain different endpoint heights. Per-feature heights on shared spans are not independently preserved.

With preserve_declared_geometry_type (JavaScript preserveDeclaredGeometryType), GeoPackage writers reject non-null features incompatible with a concrete declared type. Unknown declarations accept every geometry; GeometryCollection declarations also accept multipart subtypes.

Mixed 2D/3D features use optional Z by default in every pure-Rust GeoPackage whole-layer, memory and streaming writer, independently of the preserve option; append/update also promote the declaration when needed. Null geometries do not force Z or M to optional. A typed-empty geometry encoded with an XY WKB type still counts as 2D for this registration, unlike SQL NULL.

Clean polygon coverages

terrio clean-coverage input.gpkg output.gpkg snaps and nodes polygon boundaries, assigns overlaps to a neighboring source feature and optionally fills enclosed narrow gaps. This is a pure-Rust operation. Each polygon must individually be valid; use make-valid first for invalid polygons. Null and non-polygonal active features are rejected before output creation; inactive layers are copied without applying this restriction.

  • --snapping-distance D: planar distance in input CRS units. The default negative value selects extent diameter / 1e8; zero disables proximity snapping.
  • --merge-strategy longest-border|max-area|min-area|min-index: choose overlap ownership, breaking ties by input order. The default is longest-border. Area/border scores use already assigned faces, including earlier overlaps.
  • --maximum-gap-width W: merge enclosed gaps whose maximum inscribed diameter is at most W, into the neighbor with longest common boundary. Zero disables filling. Open inlets are retained. Holes are treated as enclosed gaps.
  • --active-layer NAME: clean one selected layer and copy inactive layers. The standard --layer, output format/name and append/overwrite options apply.

The whole selected layer is materialized. Planar topology uses XY; representative Z vertices are retained and intersection elevations are interpolated. Active-layer M values are dropped. Output attributes and CRS survive, with sequential IDs; removed/covered features retain empty Polygon geometry. Geographic coordinates use degrees: reproject before supplying distances in metres. This operation can alter boundaries and a merged narrow gap can introduce a spike. Ownership is classified against the original input polygons after snapping, matching JTS. General gap widths use a bounded inscribed-circle search with tolerance 0.0001 times the requested radius. If the iteration bound is exhausted without finding a larger radius, the gap is classified within the width. Near-threshold or pathological gaps can therefore be approximate. Triangles and convex quadrilaterals use the exact radius shortcut.

Rust exposes ops::coverage_clean::{clean_coverage, clean_coverage_layers, CoverageCleanOptions, CoverageMergeStrategy}. Python terrio.clean_coverage(src, snapping_distance=-1, merge_strategy="longest-border", maximum_gap_width=0, layer=None) returns a Layer; active_layer="name" returns a VectorDataset and is mutually exclusive with layer. JavaScript cleanCoverage(src, {snappingDistance, mergeStrategy, maximumGapWidth, layer, activeLayer}) returns a Dataset. The CLI also supplies the MCP tool. Without layer or activeLayer, CLI and JavaScript process every input layer, following their dataset return/output contracts. Python's default returns one Layer and requires layer= when its source has multiple layers. active_layer= (or activeLayer) cleans the named layer and copies the others on both bindings.

Coverage cleaning can have multiple ordering-dependent results for interacting overlaps. GDAL 3.13.3 / GEOS 3.14.1 itself changes longest-border ownership when only the starting vertices of unchanged polygon rings are rotated. Terrio uses a fixed graph traversal; per-feature ownership need not match one particular reference traversal. The comparer reports such differences explicitly: it checks exact cyclic-input reference alternatives, then independently replays greedy ownership on GDAL-polygonized input faces when necessary. Both results must have a legal complete merge order, preserve the input union, attributes, IDs and CRS, and pass GDAL coverage validation. Per-feature geometric equality is not claimed for these cases; Polygon/MultiPolygon types may follow the differing owners. The ordinary per-feature geometric tolerance is not widened.

Cleaning infers output geometry declarations from the result. A one-part result is Polygon, including when the input GeoPackage declares MultiPolygon. This reflects the topology operation's Polygon collapse; coverage simplification separately supports preserving its input declaration.

Separate-file raster tiles

terrio tile INPUT OUTPUT_DIR writes separate z/x/y.png tiles. The default WebMercatorQuad grid uses EPSG:3857; --tiling-scheme WorldCRS84Quad uses longitude/latitude with two columns at zoom zero, and --tiling-scheme raster retains a north-up source grid. --convention tms reverses filename rows while retaining the same pixel grids. --tile-size, --min-zoom, --max-zoom and --min-x/--max-x/--min-y/--max-y restrict generation. Coordinate restrictions use top-origin coordinates at the maximum zoom, including with TMS filenames.

terrio tile rgb.tif tiles --min-zoom 5 --max-zoom 8 --resampling nearest
terrio tile dem.tif dem-tiles --tiling-scheme raster --format GTiff --tile-size 512
terrio tile rgb.tif tiles --resume
terrio tile rgb.tif ignored --dry-run

PNG requires Byte gray/RGB/palette input, optionally with alpha; scale other inputs first, or select --format GTiff to retain their types and bands. PNG tiles get alpha for nodata, dataset masks and padded edges. Each color channel's nodata is excluded before interpolation; the pixel remains opaque when other channels are valid. GeoTIFF outputs without alpha retain dataset masks and exclude masked samples from resampling. Fully opaque tiles omit automatic alpha unless STACTA publication requires a uniform band count; --add-alpha retains it on every tile and --no-alpha removes it. --output-nodata sets invalid-pixel backgrounds, including the color beneath fully transparent pixels when alpha is enabled. With --no-alpha, invalid image pixels become the background; PNG has no separate validity mask. A gray PNG with explicit output nodata uses a tRNS transparency entry; every sample equal to that background value becomes transparent, including valid samples. Retain alpha with --add-alpha when valid data can equal the background value. RGB PNG does not use tRNS; retain alpha for RGB transparency. Like GDAL, plain GeoTIFF tiles without explicit output nodata use zero padding while retaining source nodata tags. --skip-blank omits fully transparent/nodata-only tiles, including without alpha (GDAL 3.13.3 can retain zero-filled nodata-only tiles with --no-alpha). GeoTIFF mask/blank processing uses a separate validity rendering pass. Non-nearest validity uses the maximum contributing source density, which can mark more edge pixels valid than a GDAL interpolating alpha warp; exact mask agreement is validated for nearest. It can read source windows again while retaining the same buffer budgets. PNG output contains image pixels only, without world files, CRS sidecars or web viewers. The --dry-run JSON includes crs and the grid for georeferencing; crs is an authority code or full definition, and is null for an unknown source CRS on a raster grid. GeoTIFF tiles embed their grids and CRS. Distinct per-band nodata requiring PAM sidecars is currently refused for GeoTIFF folder output. The same restriction covers a shared Float32 nodata value whose raw value cannot be represented as Float32 (for example -999.9), because retaining it also requires PAM. Use an exactly representable nodata value or translate it before folder tiling when the output retains a nodata tag. With alpha enabled, the explicit background may round to Float32 because validity is encoded by alpha and no nodata tag is written. Alpha uses 255 for full opacity for Byte, 32-bit integer and floating-point samples, 65535 for UInt16 and 32767 for Int16, following GDAL's warp alpha defaults. Floating alpha fractions are retained in typed GeoTIFF tiles; GDAL 3.13.3's tile writer can quantize such alpha samples to integers.

Maximum-zoom resampling defaults to cubic; --overview-resampling selects the method at lower zooms. Every zoom is rendered directly from the source (using its stored overviews when appropriate), rather than downsampling previously encoded tiles. This can differ from GDAL's generated-image pyramid. Image/alpha tiles support nearest, bilinear, cubic, cubicspline, lanczos and average; plain GeoTIFF tiles also support the warp engine's statistical methods. Interpolating kernels use premultiplied alpha; exact GDAL agreement is currently validated for nearest-neighbor tiling. Affine georeferencing is required; global schemes also require a known source CRS; image/alpha tiles refuse Int64/UInt64 input to avoid float conversion losses; additional GDAL matrix sets are not currently implemented. Publication companions and the separate retile layout are described below.

Rendering holds one output tile and bounded source windows. By default a decoded source window may use at most 64 MiB and the output buffer 256 MiB; requests exceeding those limits fail before the source window read. Decoder caches, already-loaded sources and PNG/GeoTIFF encoder buffers are additional. A plan exceeding --max-tiles (default one million) is rejected before rendering. --dry-run reports a metadata-only plan, including CRS, and validates format, resampling, alpha and output-nodata choices available from that metadata. The maximum tile count, source reads and encoding checks are enforced during generation.

Output directories must be new unless --resume is given. Existing files are never overwritten; resume trusts existing tiles without validating their contents or options. Symlinked descendant paths are refused. A failure can leave completed tiles and unfinished hidden temporary files. Each final filename is published atomically only after its contents are synced; resume ignores unfinished temporary files and fills the missing tiles. Use a fresh directory when changing generation options. The caller must control the output tree throughout generation; hostile concurrent replacement of its directories requires separate filesystem confinement. Atomic publication requires hard-link support on the output filesystem; unsupported filesystems return an I/O error. File contents are synced before publication; power-loss durability of directory entries is not guaranteed.

Directory writes require a native filesystem; WebAssembly callers use the incremental tile sink instead. The command is also the MCP tile tool. Rust callers use ops::raster_tile::{plan, generate, to_directory, FolderSink, TileOptions}. Python terrio.raster_tiles(source, output, **options) accepts a raster path or an open Raster, including lazy rasters. Options use snake_case; grid options are nested, for example grid={"tile_size": 128, "min_zoom": 0}. Pass dry_run=True for the plan or resume=True to fill missing files. JavaScript planRasterTiles(input, options) returns the plan and rasterTiles(input, options, onTile) calls the synchronous onTile(relativePath, Uint8Array) callback once per encoded tile. JavaScript options accept camelCase, including grid: {tileSize: 128, minZoom: 0}. Uploaded bytes and shared Dataset handles retain window reads. A thrown callback stops generation and is rethrown with its original JavaScript value; consumers are responsible for saving files and for any bytes retained by their callback.

For the raster tiling scheme, lower zooms floor-divide source dimensions by the decimation factor, with a minimum of one pixel, matching GDAL 3.13.3. When a dimension is at least that factor, trailing native rows/columns that do not fill a decimated pixel are outside that lower zoom. Maximum zoom covers the full source. Output edge tiles may include padding.

Publishing folder tiles

terrio tile input.tif published --webviewer all --kml \
  --url https://example.org/maps/published/ --title "Imagery"

--webviewer accepts none (default), all, leaflet, openlayers, mapml or stac. all selects the companions applicable to the chosen grid. Leaflet requires a global PNG grid; OpenLayers supports PNG grids, including raster grids with unequal X/Y resolution. MapML requires global XYZ tiles. STACTA (stacta.json) requires XYZ, a known CRS, square pixels and blank tiles retained, and can be reopened as a raster. Leaflet/OpenLayers pages fetch pinned public library distributions. Serve the generated directory over HTTP to view it in a browser.

--kml requires a known CRS and writes doc.kml plus tile overlays and child links for either XYZ or TMS. --url names the tile directory itself; no output basename is appended. Omit it for relative links. --copyright supplies plain text attribution. Titles and attribution are escaped for HTML/XML. --mapml-template FILE reads template contents; Python/JavaScript instead pass the contents as publication.mapml_template / publication.mapmlTemplate. Substitution is single-pass, so placeholders inside a title remain literal.

Python and JavaScript accept these options in a nested publication object, for example publication={"viewers": ["all"], "kml": True} in Python. The JavaScript callback receives companion files as well as encoded tiles. Unsupported explicitly requested companions fail during planning. Publication metadata is bounded to 16 MiB per file, strings have individual limits, and root KML links are limited to 20,000. Resume preserves existing companions as well as images; use a new directory after changing options.

retile

terrio retile west.tif east.tif -o retiled --tile-size 512 512 \
  --overlap 16 --levels 3 --resampling bilinear \
  --csv tiles.csv --tile-index tiles.gpkg
terrio retile input.tif -o retiled --row-directories --pyramid-only --levels 2

retile implements gdal_retile file layouts through the shared Rust engine and the MCP retile tool. Inputs must have north-up affine grids, matching CRS and band counts, and noncomplex samples. --source-srs assigns a CRS without reprojecting; use warp for reprojection. Later inputs overwrite earlier inputs, including nodata. Physical dataset masks are ignored, matching classic gdal_retile behavior.

Files use PREFIX_ROW_COLUMN.tif, with one-based, zero-padded row/column numbers from the top left. The prefix defaults to the first input's filename stem. Level zero is in the root; additional levels use 1/, 2/, etc. --row-directories instead writes LEVEL/ROW/PREFIX_ROW_COLUMN.tif, including level zero. Edge tiles are cropped. --overlap must be smaller than both tile dimensions. Each pyramid level floor-divides the original dimensions by its power of two; levels yielding zero width or height are rejected.

Pyramids sample the preceding emitted level, with interpolation clipped to the tile footprint. Stored input overviews are ignored. --pyramid-only starts with level one sampled from the original mosaic. Supported resamplers are nearest (default), bilinear, cubic, cubicspline and lanczos. --format accepts GTiff or COG; --output-type changes sample type, and repeatable --co NAME=VALUE supplies writer options. Same-type aligned base tiles preserve native UInt64 values. Source band scale/offset, descriptions, units and statistics are not copied; output color defaults, palette and nodata follow the retile policy.

--csv NAME.csv creates a per-level filename/extent table, with ; by default and --csv-delimiter for another single character. --tile-index accepts a .shp, .geojson or .gpkg basename; --tile-index-field defaults to location. Index filenames are relative to the level directory. Metadata is limited to 100,000 records per level and CSV files to 16 MiB. --max-tiles defaults to one million; --dry-run validates metadata and reports the layout without reading pixels. Directory publication and --resume follow the same rules as tile. Missing pyramid tiles read existing parent files, so resumed parents must match the expected dimensions, bands and type.

Rendering uses bounded source windows and a parent cache limited to 64 MiB and 256 tiles, in addition to one output tile and decoder/encoder buffers. Planning checks the output and direct base-copy buffers from metadata, using cropped edge dimensions. Resampled source and previous-level windows retain a 64 MiB per-window runtime limit; a dry-run does not preflight every adaptive resampling window. Reduce tile sizes if sampling exceeds that limit. Native directory output reads emitted parent files after eviction. Each cached parent is decoded from its emitted encoding, adding a decode pass so higher levels see the actual samples, including lossy writer effects. Write-only callback sinks regenerate evicted parents; callbacks can supply read-back to avoid this additional work. Inputs must remain unchanged throughout generation, including across callbacks. Write-only regeneration assumes stable source samples; supply parent read-back when emitted parent bytes must be reused independently of the source. No full input mosaic is materialized.

Rust exposes ops::raster_retile::{plan, generate, to_directory, RetileOptions, RetileSource}. Python terrio.retile(source_or_sources, output, **options) accepts paths or open rasters, including lazy readers; options use snake_case, with dry_run=True and resume=True as native controls. JavaScript exposes planRetile(inputs, options) and retile(inputs, options, onFile, readFile?). Inputs are uploaded bytes, shared datasets or arrays of them; options accept camelCase. Both callbacks are synchronous. onFile(path, Uint8Array) stores each image/index/CSV file; optional readFile(path) returns earlier bytes or null. Callback exceptions propagate unchanged. JavaScript does not implement native filesystem resume.

GDAL 3.13.3 real-data comparisons cover 28 Landsat/USGS DEM cases, including overlap, row directories, three recursive levels, all resamplers, COG and Byte output. Fourteen are exact. The other fourteen explicitly account for GDAL's six-decimal input-index origin rounding; bilinear DEM differences are bounded to one Float32 ULP (287 samples, maximum absolute delta 0.000244140625). The GDAL COG retile path incorrectly converts Float32 elevations through Byte; that defect is independently verified before using GDAL GTiff retile as the sample oracle. Native COGs preserve the elevations and pass full COG validation.

Pipelines

terrio pipeline composes raster and vector operations through the shared Rust terrio::ops::pipeline::Pipeline engine. Separate steps with !; quote SQL expressions and paths with spaces for your shell:

terrio pipeline read counties.geojson ! filter --where 'population > 10000' ! select name,population,_ogr_geometry_ ! write selected.gpkg
terrio pipeline read image.tif ! clip --bbox 2,2,8,8 ! select 1 ! resize --size 256,256 ! write subset.tif
terrio pipeline --save subset.gdalg.json read image.tif ! select 1
terrio pipeline --load subset.gdalg.json --output subset.tif

Global options (--load, --save, --output, --input-kind, --overwrite) precede the steps. The input kind can be auto, raster or vector. read must be first and write, when supplied, must be last. Supported intermediate steps are filter, select, clip, reproject, resize, set-type, buffer, simplify, segmentize, swap-xy, dissolve, as-features, rasterize and materialize. Unknown steps and options are rejected. set-type requires --output-data-type.

Step Supported arguments and options
read Input path or alias, optionally --input; repeatable vector --layer selectors
filter Vector --where expression and/or --bbox xmin,ymin,xmax,ymax spatial selection
select Comma-separated positional fields/bands, --fields, or repeatable --band; raster bands are one-based
clip Required --bbox xmin,ymin,xmax,ymax, in the current dataset CRS
reproject Required --output-crs; raster-only --size width,height, --resolution x,y, --resampling
resize Raster --size width,height (pixels or percentages) or --resolution x,y; optional --resampling
set-type Required raster --output-data-type
buffer Vector distance, positional or --distance; --quadrant-segments defaults to 8
simplify, segmentize Vector tolerance (--tolerance) or maximum length (--max-length), also accepted positionally
swap-xy, dissolve Vector operations with no additional options
as-features Raster --geometry-type, --skip-nodata, --include-xy, --include-row-col, --output-layer
rasterize Required --size or --resolution; --extent, repeatable --burn, --attribute-name, --all-touched, --output-data-type, --nodata
write Destination, positional or --output; --format, --overwrite, native-vector --append, vector --output-layer
materialize No options for an in-memory snapshot; optional --output with the write options above for a named checkpoint

--size and --resolution are mutually exclusive. Resampling names use the shared raster resamplers. Unknown options are rejected rather than forwarded to an external GDAL executable. Python and JavaScript use these same step strings and validation rules; JavaScript encodes named outputs to bytes instead of writing native files.

Raster reads, numeric band selection and north-up bounding-box clips retain lazy window and overview access. Vector layers and transformed raster results reside in memory. materialize takes an in-memory snapshot; add --output checkpoint.tif (or a vector destination) to request a persisted checkpoint. There are no implicit serialized intermediate datasets. Existing destinations require --overwrite; destination collisions, format availability and overwrite policy are checked before any checkpoint or final output is written.

.gdalg.json definitions use type: gdal_streamed_alg and command_line. Saving omits the final write and does not evaluate the source. CLI saves resolve input paths to absolute paths; imported relative paths resolve against the saved definition unless relative_paths_relative_to_this_file is false. Nested saved pipelines are limited to 16 levels. Definitions execute supported Terrio operations directly and never invoke a shell or external program.

This is a linear pipeline implementation. It does not expose every option of GDAL's individual steps. Raster bounding-box clips must fit inside a north-up source; use reprojection before resizing rotated grids. Complex raster band/crop views are unsupported. Mixed pipelines can use as-features and rasterize to change dataset family. Raster resize samples the pixel and validity planes independently; reprojection excludes masked pixels from interpolation.

Vector select includes only the listed fields. Include _ogr_geometry_ to retain geometry and its CRS; selecting attributes alone produces a nonspatial table.

Pipeline dissolve unions components within each feature, nodes intersecting lines and merges degree-two line chains; it preserves rows and attributes. The standalone terrio dissolve groups polygon features across a layer instead. Constructed dissolve geometry is XY. Reprojection uses the source validity mask when sampling but, like GDAL's default reproject step, does not emit a physical output mask. Resize retains a separately sampled validity plane.

Grid-changing and type-conversion steps clear derived STATISTICS_* metadata. Descriptive band metadata, scale, offset and units are retained. GDAL 3.13.3's clip/resize/type steps can omit a unit string; Terrio retains the source unit.

Python's input_kind keyword and JavaScript's inputKind option accept "auto", "raster", or "vector". These override a family prefix in the command string. Plain materialize steps interoperate in saved GDALG definitions. GDAL 3.13.3 requires its GDAL_ALGORITHM_ALLOW_WRITES_IN_STREAM=YES configuration option when opening a definition with a named checkpoint, because opening that stream writes the checkpoint file.

Native masked COG output currently reads the final raster and mask into memory before encoding; the COG row writer cannot stream that mask. This is separate from lazy input/view reads and from the streaming unmasked COG path. Byte exports also encode a complete in-memory dataset.

Vector output uses the native writer's feature-ID policy. GeoPackage output assigns sequential IDs when the schema has no explicit fid field; GDAL can retain source IDs instead. The pipeline conformance suite identifies this discrete writer-policy difference in its case details and checks the native sequence independently. Source identifiers needed as data should be retained in an ordinary attribute field. Writer option parity remains part of #288.

A saved definition used as a nested read input contributes its evaluated dataset; its checkpoint and write artifacts are discarded. Execute a definition directly with --load to request its own named checkpoints. Output preflight checks are completed before writing, but multi-file writes are not transactional: an encoding or I/O failure can leave earlier outputs in place. CLI runs without write, --output, or --save print a dataset summary without saving a file. Resize requires a north-up grid; reproject south-up, flipped or rotated grids first. Detached in-memory vector results report the GeoJSON memory-container driver; this value does not retain the source file driver's provenance.

relative_paths_relative_to_this_file applies to input paths only. Named checkpoint and final output paths resolve against the caller's working directory. Executing a definition directly with --load executes its write options, including explicit --overwrite; this is an instruction to run that definition, including its requested output writes. Nested read definitions continue to discard writes.

Each Python/JavaScript dataset property access creates an independent wrapper; vector features are copied on each access and at write/checkpoint snapshots. Keep and reuse one returned dataset when repeated access is needed. Native in-place raster output also snapshots the returned dataset and write artifact independently, which can read the source twice. High-degree line dissolve remains quadratic; its segment-neighborhood indexes are rebuilt per segment.

A standalone ! token is reserved as a step separator even when quoted; use ./! to name a file literally called !. Raster bounding boxes round to source pixel windows; a box that rounds to an empty window is rejected.

RPF table of contents

terrio rpftoc-create INPUT [OUTPUT] indexes existing CADRG frames recursively, without decoding compressed pixel components. OUTPUT defaults to INPUT/A.TOC. The command is also an MCP tool. It supports --scale, --producer-id (five printable ASCII bytes), --producer-name (27), --country-code (two uppercase letters), --classification U|R|C|S|T and --overwrite. The index classification must be at least as high as every indexed frame. Scale is inferred per series unless overridden; mismatched corners, intervals or zones fail validation. The JSON report includes frame/group counts, skipped non-frame candidate counts, metadata bytes read and output size. On Unix, new output follows the process umask; overwrites preserve the existing file mode. --overwrite replaces any existing file at OUTPUT, not only a TOC; input frames remain protected.

The output parent must be the input directory or an ancestor. This preserves safe relative frame paths, including when the TOC is placed above a nested frame tree. Symlinks and non-NITF 2.0/2.1 candidates are skipped; the latter are counted in skipped_files, along with previous TOC indexes other than the selected output. Malformed supported input NITF frames fail validation. Explicit overwrite may replace a malformed file at the selected output path; validated input frames remain protected. An output cannot replace an input frame. Frame path components must be UTF-8 and cannot contain :, backslashes or NUL bytes. Unrepresentable paths fail rather than being silently omitted. Components ending in dots or spaces, or reserved Windows device names such as CON, NUL, COM1 and LPT1 (including extensions), are rejected to follow Windows filename conventions. Duplicate paths or grid cells within one scale/zone are rejected. File opens refuse symlinks and verify the inventoried file identity before reading metadata. Validation finishes before atomic publication; an existing TOC is preserved on errors. CADRG NITF 2.0/2.1 metadata and northern/southern ARC/polar grids are supported; CIB, other RPF products and TRE overflow chains are unsupported. A recognized NITF candidate that is unsupported or malformed aborts the run before publication; it is not counted as a skipped non-NITF file. Declared NITF file size must match the actual size, so truncation after metadata is detected without reading pixels.

Unreadable directories fail the whole scan rather than publishing an incomplete inventory. Select a CADRG frame tree, rather than an entire drive root.

Native creation requires a stable local directory tree: callers must synchronize concurrent changes to input and output directories. File identity checks detect replaced frames; they do not make pathname traversal or publication a confinement boundary against other local writers. Atomic overwrite preserves Unix mode bits; it creates a replacement file and does not preserve ownership, extended ACLs or other extended attributes. Applications requiring those properties should manage publication of rpftoc_bytes themselves. Limits include 100,000 candidate frames (an inspection safety cap, not serializable capacity), 200,000 directory entries, 64 directory levels, 1 MiB NITF headers, 64 KiB location tables and a 99,999-byte encoded TRE payload. Large indexes must be partitioned by the caller. Historical polar nominal interval fields are normalized from the scale and verified frame geometry.

The nominal scale catalogue retains fractional scales such as WI at 3.5M. GDAL 3.13.3 truncates that catalogue value to 3M, producing incorrect index georeferencing; comparisons use its explicit --scale 3500000 reference instead.