Python
Python bindings for terrio: read, write, convert and
reproject geospatial rasters and vector datasets in Rust, without installing GDAL. Wheels need
only NumPy (and the small affine package).
pip install terrio
The quickstart notebook runs these functions on real data, in Google Colab or locally.
Coming from rasterio? import terrio.rio as rasterio runs typical rasterio scripts unchanged: see
Coming from rasterio.
Working with xarray? terrio.xarray.open_rasterio (or xr.open_dataset(path, engine="terrio"))
opens rasters lazily as DataArrays, as rioxarray does, and the .terrio accessor has rioxarray's
.rio methods (clip, reproject, to_raster...): see xarray.
Rasters
import terrio
dem = terrio.open_raster("dem.tif")
dem.width, dem.height, dem.count, dem.dtype # dtype is a NumPy name: "float32", "uint8"...
dem.crs, dem.transform, dem.bounds, dem.nodata
elevation = dem.read(1) # numpy array (rows, cols) of the raster's own dtype
everything = dem.read() # (bands, rows, cols)
# GeoTIFFs and COGs, local or remote, can be read by window: only the tiles a window needs are
# decoded, and fetched with HTTP range requests.
cog = terrio.open_raster("https://example.com/cog.tif", lazy=True)
cog.overviews # [(width, height), ...] of the overviews, largest first
tile = cog.read(1, window=(1024, 1024, 512, 512)) # (col_off, row_off, width, height)
thumb = cog.read(overview=2)
# PMTiles and MBTiles raster tiles too: zoom levels are overviews (PMTiles read in place).
tiles = terrio.open_raster("https://example.com/tiles.pmtiles", lazy=True)
# JPEG 2000 (Sentinel-2 bands) too: resolution levels are overviews, windows decode their tiles.
b04 = terrio.open_raster("T10SEG_20240630T185919_B04.jp2", lazy=True)
patch = b04.read(1, window=(2000, 6000, 1024, 1024))
mask = terrio.open_raster("masked.tif", lazy=True).read_mask() # a GeoTIFF's internal mask, or None
# A GeoTIFF larger than memory, written in rows (band after band, from the top).
w = terrio.GeoTiffWriter("big.tif", 100000, 80000, 1, "uint16", transform, crs="EPSG:32610",
compress="DEFLATE", tile_size=512, mask=True)
for rows, valid in row_blocks: # (1, n, 100000) uint16 and (n, 100000) uint8 arrays
w.write(rows, valid)
w.close()
out = terrio.Raster.from_array(elevation * 3.28084, dem.transform, crs=dem.crs, nodata=dem.nodata)
out.write("dem_feet.tif", compress="DEFLATE")
out.write("dem_feet_cog.tif", driver="COG")
out.write("dem.zarr", options={"FORMAT": "ZARR_V3", "GEOREFERENCING_CONVENTION": "SPATIAL_PROJ"}) # GeoZarr
# STAC: a mosaic of an asset over a search's items (or an item's asset, 'STAC:"item.json":red'), read by window.
red = terrio.open_raster('STACIT:"https://earth-search.aws.element84.com/v1/search?collections=sentinel-2-l2a&bbox=-105.05,40.5,-104.95,40.6&datetime=2023-07-05":asset=red', lazy=True)
terrio.open_raster("landsat.tif").write("landsat.pmtiles") # Byte rasters as tiles (or .mbtiles)
Vector data
ds = terrio.open_vector("countries.gpkg") # or .shp, .geojson, .fgb, .parquet, .kml, .zip...
layer = ds.layers[0] # also ds["countries"]
layer.name, layer.crs, layer.geometry_type, layer.fields, len(layer), layer.bounds
collection = layer.to_geojson() # a GeoJSON FeatureCollection dict
wkb = layer.to_wkb() # one WKB geometry per feature
columns = layer.columns() # attribute values by field
gdf = layer.to_geopandas() # with geopandas installed
terrio.write_vector(layer.to_crs("EPSG:3857"), "countries_3857.fgb")
terrio.write_vector(collection, "countries.geojson") # GeoJSON dicts, GeoDataFrames, Arrow...
# GeoPackages (and GeoParquet files) are read by bounding box through their spatial index.
europe = terrio.open_vector("counties.gpkg", bbox=(-10, 35, 30, 60))
# Existing GeoPackages are updated: features appended to a layer (new ids, R-tree, extent and
# feature count updated, as GDAL does), layers replaced or added, other tables kept.
terrio.write_vector(layer, "world.gpkg", append=True)
terrio.convert("more.shp", "world.gpkg", layer_name="countries", overwrite=True)
Driver open options go in options, by their GDAL names, as terrio convert --oo takes them.
HEC-RAS geometry and plan HDF files (the package is built with the hecras feature) read as
vector layers this way, with the results HEC-RAS stored joined by open options
(see HEC-RAS):
plan = terrio.open_vector(
"BaldEagleDamBrk.p03.hdf",
layers=["mesh_cells", "cross_sections"],
options={"CELL_SUMMARIES": "Maximum Water Surface", "XS_RESULTS": "Water Surface", "TIME_STEP": "12"},
)
terrio.info("BaldEagleDamBrk.p03.hdf", layers=["mesh_cells"], options={"CELL_ATTRIBUTES": "MINIMUM_ELEVATION"})
The file is changed in place, as SQLite changes it: only the pages that change are written (new rows, R-tree nodes, the tables describing the layer), in a transaction protected by a rollback journal, so appending to a large GeoPackage costs about as much as the features appended, and a process stopped in the middle leaves the file as it was (SQLite, GDAL and terrio play the journal back).
Streams
Layers larger than memory are read a chunk of features at a time with terrio.open_stream
(GeoPackage, FlatGeobuf, GeoParquet, Shapefile, GeoJSON, GeoJSONSeq and CSV; other formats are
read whole and handed out in chunks), and terrio.convert streams vector conversions between
those formats (all but GeoPackage as outputs), as the command line does:
stream = terrio.open_stream("buildings.gpkg", "buildings", batch_size=100_000)
stream.name, stream.crs, stream.fields, stream.geometry_type
for chunk in stream: # terrio.Layer chunks of at most 100,000 features
gdf = chunk.to_geopandas()
for feature in terrio.iter_features("buildings.fgb"): # GeoJSON-like dicts, one at a time
print(feature["properties"])
# Arrow record batches, read as the consumer asks for them.
reader = pa.RecordBatchReader.from_stream(terrio.open_stream("buildings.parquet"))
for batch in reader:
...
terrio.convert("buildings.gpkg", "buildings.parquet", where="height > 10") # in bounded memory
bbox=(min_x, min_y, max_x, max_y) reads only the features a GeoPackage's R-tree or a
GeoParquet file's bounding boxes find. The Arrow stream has the schema Layer.to_arrow() gives
(WKB geometries); a layer with date fields is read twice, once to choose their Arrow types.
Arrow
Layers convert to and from Apache Arrow through the Arrow PyCapsule interface, so pyarrow, GeoPandas, Polars, DuckDB and GDAL exchange them without copying through Python and terrio needs none of them:
table = layer.to_arrow() # a terrio.ArrowTable
pyarrow_table = pa.table(table) # or table.to_pyarrow(); pa.table(layer) works too
gdf = geopandas.GeoDataFrame.from_arrow(table) # geometries and CRS included
df = polars.DataFrame(table)
native = layer.to_arrow(geometry_encoding="geoarrow") # geoarrow.multipolygon...
interleaved = layer.to_arrow(geometry_encoding="geoarrow", coordinates="interleaved")
layer = terrio.from_arrow(pyarrow_table) # also GeoDataFrame.to_arrow(), Polars, record batches
layer = terrio.from_arrow(gdal_layer) # an osgeo.ogr.Layer exports its Arrow stream
terrio.write_vector(pyarrow_table, "out.gpkg", layer_name="countries")
The attributes become typed columns (integers as int64, reals as double, bool, string,
binary, dates as date32, date-times as millisecond timestamps, UTC when they have time zones)
and the geometries a GeoArrow column: WKB (geoarrow.wkb) by default,
which holds any geometry, or the native encodings for layers of one geometry type (a type and its
multi-part form are written as the multi-part form). The CRS is in the column's metadata, as
PROJJSON. On import, the geometry column is found by its GeoArrow metadata (WKB, WKT or native),
GDAL's ogc.wkb, GeoParquet geo metadata (as pyarrow.parquet keeps it), or a binary column
named geometry or wkb_geometry; struct columns are flattened and lists become JSON, as in GDAL.
scripts/compare_arrow_with_gdal.py checks both directions on real files against GDAL's Arrow
streams (ogr.Layer.GetArrowStreamAsPyArrow, ogr.Layer.WriteArrow), pyarrow and GeoPandas:
geometries are WKB-identical and attribute values equal.
Operations
The operations take the command-line options as keyword arguments. Inputs are paths or objects in memory; outputs are returned, and also written when a destination is given.
terrio.info("dem.tif", stats=True) # the same dict as `terrio info --json`
terrio.convert("countries.shp", "europe.gpkg", where="CONTINENT = 'Europe'", select=["NAME"])
terrio.convert("https://example.com/cog.tif", "window.tif", srcwin=(0, 0, 512, 512))
terrio.translate("dem.tif", "small.tif", outsize=("25%", "25%"), resampling="average")
terrio.warp("dem.tif", "dem_4326.tif", t_srs="EPSG:4326", resampling="bilinear")
terrio.mosaic(["n40w106.tif", "n40w107.tif"], "mosaic.tif")
terrio.rasterize("counties.gpkg", "pop.tif", attribute="POPULATION", tr=(0.01, 0.01), ot="Int32")
polygons = terrio.polygonize("landcover.tif", connectedness=8)
shade = terrio.dem("dem.tif", "hillshade", z=2, multidirectional=True) # like gdaldem
terrio.dem("dem.tif", "color-relief", "relief.tif", color_file="colors.txt", alpha=True) # by strips, lazy result
ndvi = terrio.calc("(B - A * 1.0) / (B + A * 1.0)", A="landsat.tif", A_band=3, B="landsat.tif", B_band=4,
type="Float32") # like gdal_calc.py, with NumPy's typing rules
dist = terrio.proximity("roads.tif", values=[1], distunits="GEO")
clean = terrio.sieve("landcover.tif", threshold=10, connectedness=8)
max5 = terrio.neighbors("dem.tif", kernel="equal", size=5, method="max") # like gdal raster neighbors
edges = terrio.neighbors("image.tif", kernel=["gaussian", "edge2"], band=2, ot="Float32")
collar = terrio.clean_collar("scene.tif", "clean.tif", add_alpha=True) # like gdal raster clean-collar
masked = terrio.nearblack("scene.tif", "nb.tif", colors=["0,0,0"], add_mask=True) # like nearblack
text = terrio.enhance_luts("image.tif", config="lut.txt") # gdalenhance -equalize -config lut.txt
eq = terrio.enhance("image.tif", "eq.tif", equalize=True) # or config="lut.txt", src_scale=(0, 4000)
xs, ys = terrio.transform_points([699960], [3900000], "EPSG:32610", "EPSG:4326")
xs, ys, hs = terrio.transform_points([-100], [40], "EPSG:4979", "EPSG:4326+5773", [100]) # EGM96
xs, ys, hs = terrio.transform_points([135], [-25], "EPSG:7912", "EPSG:7843", [0], s_coord_epoch=2010.0)
terrio.set_grid_search_paths(["/data/proj-grids"]) # where datum shift grids are looked up
data = open("dem.tif", "rb").read()
raster = terrio.open_bytes(data, "dem.tif") # from bytes; to bytes with raster.to_bytes("x.tif")
clean_collar and nearblack return a Byte Raster; a mask (add_mask, or an input's mask
kept by clean_collar) needs dst, which holds it, and the returned Raster is then read from
dst (has_mask, read_mask). In-place updates are a command-line feature
(terrio clean-collar --update). See neighbors,
clean-collar and enhance for the
options, GDAL's arithmetic and the known differences.
Pansharpening
sharp = terrio.pansharpen("pan.tif", "rgbnir.tif", bands=[1, 2, 3], bitdepth=12, nodata=0)
sharp = terrio.pansharpen("pan.tif", ["red.tif", "green.tif", "blue.tif"], weights=[0.6, 0.3, 0.1])
virtual = terrio.pansharpen("pan.tif", ["red.tif", "green.tif", "blue.tif"], "sharp.vrt")
window = virtual.read(window=(100, 200, 64, 64)) # pixel windows read on demand
All spectral inputs contribute to intensity; bands selects/reorders output bands from
the flattened input sequence. See pansharpen for interpolation,
working types, nodata, bit depth and extent handling.
Viewshed
visible = terrio.viewshed("dem_utm.tif", 500000, 4400000, oz=2, tz=0, md=5000)
height = terrio.viewshed("dem_utm.tif", 500000, 4400000, om="GROUND", cc=0)
om="NORMAL" returns visibility; DEM and GROUND return minimum visible elevation
or height above the DEM. cc is 1 minus refraction. See viewshed for
units, curvature, radius cropping and input nodata behavior.
Vector gridding and linear referencing
temp = terrio.vector_grid("stations.gpkg", "temp.tif", algorithm="invdist", zfield="temp",
power=2, radius=50000, max_points=12)
dem = terrio.vector_grid("samples.gpkg", algorithm="linear", extent=(636000, 848930, 637180, 849500),
resolution=(5, 5))
parts = terrio.lineref_create("path.shp", "mstones.shp", "pos", 1000, "parts.gpkg")
pos = terrio.lineref_position(parts, -1.4345, 51.9497)
(x, y, z), = terrio.lineref_coordinates(parts, 15977.724709)
sub = terrio.lineref_subline(parts, 13300, 17400)
vector_grid takes the algorithm (a name or a GDAL string) and its parameters as keywords
(power, smoothing, radius, radius1, radius2, angle, min_points, max_points,
min_points_per_quadrant, max_points_per_quadrant, nodata), with zfield, zoffset,
zmultiply, layers, sql, where, bbox, extent, size, resolution, ot and crs.
See vector-grid and lineref.
Repeated lookups should reuse terrio.open_vector(parts_path) or a Layer. Passing a path,
serialized bytes or GeoJSON each time reopens/reparses the input.
Compare, pixel info and XYZ
report = terrio.compare("reference.tif", "output.tif", skip_binary=True)
print(report["difference_count"], [d["message"] for d in report["differences"]])
print(report["bands"][0]["pixels_differing"], report["bands"][0]["max_abs_diff"])
values = terrio.pixel_info("dem.tif", [(-105.5, 39.5, "site A")], position_crs="EPSG:4326", resampling="bilinear")
print(values["features"][0]["properties"]["bands"][0]["raw_value"])
print(terrio.pixel_info("dem.tif", position_dataset="sites.gpkg", format="csv"))
terrio.xyz("dem.tif", "dem.xyz", skip_nodata=True) # number of lines written
text = terrio.xyz("dem.tif", srcwin=(0, 0, 10, 10))
compare takes paths or Rasters (two paths for the binary, RPC and subdataset comparisons)
and returns terrio compare --json's report. pixel_info returns GeoJSON (a dict), json (a
list), or csv, text and value strings; xyz returns the line count written to dst, or
the text. See compare, pixel-info and xyz
for options, GDAL agreement and differences.
pixel_info(..., format="value", field_sep="|") selects the separator between
band values. pixel_info(..., dst="samples.gpkg", output_format="GPKG") also
writes a vector layer of sample points and returns the requested report as usual.
Missing/off-raster samples are retained as null values. Input paths cannot be used
as the output. JavaScript hosts can serialize the GeoJSON result through a Dataset
when they need file bytes; direct local file paths are a native API feature.
Vector datasets
merged = terrio.concat(["countries.gpkg", "us_counties.gpkg"], "merged.gpkg", mode="single",
source_layer_field_name="source", dst_crs="EPSG:3857")
files = terrio.partition("counties.gpkg", "out", ["STATE"], feature_limit=1000) # paths written
typed = terrio.set_field_type("countries.gpkg", "Integer64", field_name="POP_EST")
points = terrio.make_point("trace.csv", "longitude", "latitude", crs="EPSG:4326") # a Layer
schema = terrio.export_schema("countries.gpkg") # the OGR_SCHEMA dict
terrio.create_vector("empty.gpkg", schema=schema)
terrio.create_vector("sites.gpkg", output_layer="sites", geometry_type="POINT", crs="EPSG:4326",
fields=["name:STRING(80)", "visits:INTEGER"])
These are terrio concat, partition, set-field-type, make-point, export-schema and
create (GDAL's gdal vector commands of the same names); sources are paths or vector data in
memory (VectorDataset, Layer, lists of layers, GeoJSON). Conversion warnings are issued as
UserWarnings. See Vector datasets for the options, GDAL's rules
and the differences. Filesystem partition is unavailable in Pyodide;
the remaining imported APIs remain available.
Renaming and updating vector datasets
rename_layer, edit_vector and update_vector accept the same vector sources as
concat. They return a new VectorDataset for memory inputs. For local files,
rename_layer(path, in_place=True, ...) and edit_vector(path, in_place=True, ...)
change a GeoPackage; update_vector(source, destination_path, ...) updates an
existing destination. Read-only files and active SQLite journals are refused.
renamed = terrio.rename_layer(countries, ascii=True, lower_case=True)
assigned = terrio.edit_vector(renamed, crs="EPSG:4326")
updated = terrio.update_vector(changes, renamed, key=["name"], mode="merge")
terrio.edit_vector("countries.gpkg", in_place=True, metadata=["SOURCE=survey"])
Rename options mirror the CLI: input_layer, output_layer, ascii, lower_case,
filename_compatible, reserved_characters, replacement_character, max_length.
Edit supports active_layer, crs ("none" removes it), geometry_type,
unset_fid, dataset/layer metadata and unset_metadata lists. Metadata edits
require file paths because the memory model does not retain dataset metadata.
Assigning a CRS changes its declaration without transforming coordinates.
Update supports input_layer, output_layer, key and mode (merge,
update-only, append-only); missing layer selections in multilayer datasets fail.
File edits materialize the dataset and require space for a neighboring temporary
copy. See the CLI documentation for atomicity and metadata preservation limits.
The vector dataset copy operations concat, rename_layer and edit_vector refuse an
existing destination path. Choose a new output path; use in_place=True for supported
GeoPackage edits.
Pixel-wise operations
physical = terrio.unscale("packed.tif", ot="Float64") # value * scale + offset
rgba = terrio.nodata_to_alpha("landsat.tif", "rgba.tif") # nodata to an alpha band
groups = terrio.reclassify("nlcd.tif", "11=1;[21,25)=2;DEFAULT=0", ot="Byte")
groups = terrio.reclassify("nlcd.tif", "@mapping.txt") # one entry per line
pixels = terrio.as_features("dem.tif", "pixels.gpkg", geometry_type="polygon", include_xy=True)
These follow GDAL's gdal raster unscale, nodata-to-alpha, reclassify and as-features;
see unscale, nodata-to-alpha,
reclassify and as-features.
File management
terrio.vsi_list("s3://bucket/prefix", recursive=True) # gdal vsi list
terrio.vsi_list("/vsizip/data.zip", recursive=True, depth=2)
terrio.vsi_copy("data", "s3://bucket/prefix", recursive=True) # gdal vsi copy
terrio.vsi_sync("data/", "s3://bucket/prefix", recursive=True, strategy="ETag")
terrio.vsi_move("s3://bucket/old", "s3://bucket/new")
terrio.vsi_delete("s3://bucket/prefix", recursive=True)
terrio.vsi_write("/vsimem/a.txt", b"bytes"); terrio.vsi_read("/vsimem/a.txt")
terrio.vsi_stat("gs://bucket/key") # None when nothing is there
The terrio vsi commands as functions: entries are dictionaries (name, type, size,
modified in seconds since the epoch, last_modification_date, permissions, etag), and the
copy, sync, move and delete functions return a report (copied, bytes, unchanged,
deleted, errors). /vsimem/ files last as long as the process. Locations, semantics and the
refusals of dangerous deletions are those of vsi.
Seek-optimized ZIP archives
terrio.sozip_create(["roads.gpkg", "dem.tif"], "data.zip", junk_paths=True)
terrio.sozip_list("data.zip") # dictionaries with names, sizes and SOZip chunk sizes
terrio.sozip_validate("data.zip") # valid, sozip_files, messages, errors
terrio.sozip_optimize("plain.zip", "seek.zip", enable="yes", chunk_size=65536)
The four functions share the CLI options and limitations.
Creation and optimization return added, seek_optimized and bytes counts.
Native outputs are replaced only after the entire operation succeeds.
COG validation
ok, errors, warnings = terrio.validate_cog("dem.tif") # rio_cogeo.cog_validate's result
terrio.validate_cog("https://example.com/dem.tif", strict=True) # warnings fail too
The checks and messages of rio_cogeo.cogeo.cog_validate, read from the file's structure without
GDAL, for local files (a str or os.PathLike), URLs and S3 objects (range requests). See
Rust.
Typing
The package ships type stubs (py.typed), checked with mypy --strict and stubtest:
cd python && mypy --strict tests/typing/usage.py
cd "$(mktemp -d)" && python -m mypy.stubtest terrio # outside the source tree
Errors raise terrio.TerrioError subclasses, which also derive from the matching built-in
exceptions: NotFoundError is a FileNotFoundError, TerrioIOError an OSError, and
InvalidArgumentError, CRSError, UnsupportedError and InvalidDataError are ValueErrors.
Building from source
cd python
uv venv && uv pip install maturin numpy pytest
uv run maturin develop --release
uv run pytest
Warp source overviews
lazy = terrio.open_raster("large-cog.tif", lazy=True)
preview = terrio.warp(lazy, ts=(256, 256)) # AUTO by default
finer = terrio.warp(lazy, ts=(256, 256), overview="AUTO-1")
full = terrio.warp(lazy, ts=(256, 256), overview="NONE")
level = terrio.warp(lazy, overview=1) # levels start at 0
Paths, lazy Raster objects, and mixed/repeated inputs in mosaics retain their
stored overviews. Selection precedes pixel reads and does not alter the output grid
computed from the original metadata. Only the source resolution changes. Loaded rasters
have no stored overviews: a requested numeric level warns and uses full resolution.
Calls with loaded inputs reuse their existing pixels, including repeated references.
File GCPs/RPCs of the first path are retained in mixed path/Raster calls.
SQL queries
terrio.sql(src, query, layers=None) returns a Layer. Inputs may be a vector path,
VectorDataset, Layer, list of layers, or GeoJSON. It implements the shared
OGR SQL subset, including computed columns, DISTINCT, summaries and attribute joins.
result = terrio.sql("countries.gpkg", "SELECT ADMIN, POP_EST/1000000.0 AS millions FROM countries")
summary = terrio.sql("countries.gpkg", "SELECT COUNT(*) AS n, SUM(POP_EST) AS population FROM countries")
Raw GeoJSON inputs use the layer name layer; use Layer.from_geojson(..., name=...)
(Python) or fromGeoJSON(src, name) (JavaScript) to assign another name.
Vector geometry operations
simplify, segmentize, convex_hull, concave_hull, swap_xy, set_geom_type, combine
and sort run the vector geometry commands (GDAL 3.13's
gdal vector commands of those names) and return a Layer. Inputs may be a vector path,
VectorDataset, Layer, list of layers, or GeoJSON, with a single layer, or the one layer=
names. convert(..., simplify=TOLERANCE) and convert(..., segmentize=MAX_LENGTH) are
ogr2ogr's -simplify and -segmentize.
groups = terrio.combine("places.gpkg", group_by=["adm0_a3"])
hulls = terrio.concave_hull(groups, 0.3, allow_holes=True)
simple = terrio.simplify("counties.gpkg", 0.01)
dense = terrio.segmentize(simple, 0.1)
multi = terrio.set_geom_type("roads.gpkg", multi=True, dim="XYZ")
ordered = terrio.sort(multi, method="strtree")
Geolocation-array warping
terrio.warp(path, geoloc=True, t_srs="EPSG:4326") requires the longitude/latitude arrays
already exposed by netCDF/HDF-EOS drivers. Arrays are also used automatically when present.
For coordinate rasters held separately or in memory, supply the first source's arrays directly:
out = terrio.warp(source, t_srs="EPSG:4326", geolocation={
"x": longitude_raster, "y": latitude_raster,
"pixel_step": 2, "line_step": 2, "pixel_center": True,
})
x and y each accept a raster or a path. x_band and y_band select their 1-based
coordinate bands (default 1). Optional srs defaults to EPSG:4326;
pixel_offset/line_offset default to 0 and steps to 1. The center shift is half an array
step. Invalid coordinate cells remain empty. See CLI warping for the supported
subset and comparison behavior at swath boundaries.
Fill nodata
filled = terrio.fill_nodata("dem.tif", "filled.tif", max_distance=100,
smoothing_iterations=1)
nearest = terrio.fill_nodata("image.tif", band=2, mask="validity.tif",
interpolation="nearest")
Inputs and masks may also be terrio.Raster objects. The result is a new, single-band
raster, retaining the selected band's type, nodata and georeferencing. The mask is zero
at holes and nonzero at donors; without it, the source mask or nodata defines holes.
Distances are in pixels; zero searches the whole image. Smoothing affects only filled
pixels and preserves the first/last rows. Valid source samples remain unchanged.
Native multidimensional arrays
See Native multidimensional arrays for scientific groups, named dimensions, lazy coordinate arrays, time/depth slicing, transposition and netCDF/Zarr numeric snapshots.
Indexed raster catalogs
catalog = terrio.open_catalog("tiles.gti.gpkg", cache_size=16, sort_field="priority")
window = catalog.read(1, window=(100, 200, 256, 256))
Catalogs select features through the GeoPackage/FlatGeobuf spatial index and cache a bounded
number of source datasets. terrio.open_raster("GTI:tiles.gpkg", lazy=True) also works with
catalog defaults. For in-memory indexes and named source rasters:
catalog = terrio.open_catalog(index_layer, {"left.tif": left, "right.tif": right},
tr=(30, 30), sort_field="priority", cache_size=16)
The source mapping accepts Raster objects and paths, including PathLike values. Paths
are opened only when selected and close on cache eviction. Supplied lazy rasters retain their
window/overview reads; loaded buffers and existing readers remain shared with the caller.
Replacing or deleting the original Python object does not replace the catalog's handle.
Options include layer, location_field, sort_field, sort_descending, tr, te, srs,
bands, ot, nodata, alpha_band, cache_size and resampling. Missing grid characteristics
come from index metadata or one tile header. Dataset masks, source nodata and zero alpha are
transparent. See CLI catalogs for the supported GTI subset.
Serialized VRTs and uploaded sources
virtual = terrio.warp("dem.tif", "projected.vrt", t_srs="EPSG:3857", ts=(512, 512))
window = virtual.read(1, window=(32, 48, 128, 128))
uploaded = terrio.open_vrt(xml, {"dem.tif": image_bytes})
open_vrt accepts source names mapped to file bytes, paths or PathLike objects;
source windows remain lazy. open_raster(path, lazy=True) also opens VRT files
lazily. The serialized VRT support lists the
supported transformers, built-in functions, kernels and masks.
VSI listing bindings return structured flat entries. Nested tree presentation (--tree)
is currently available through the CLI; JSON serialization flags apply to CLI output.
Raster management
create_raster(dst=None, *, like=None, size=None, band_count=None, ot=None,
crs=None, bbox=None, nodata=None, burn=None, metadata=None, ...) creates an
in-memory raster or a file. File creation accepts writer options, overwrite,
copy_metadata and copy_overviews. edit_raster(path, ...) changes CRS, transform,
GCPs, nodata, metadata, statistics and histograms in place. move_raster(path,
t_srs, s_srs=None, error_threshold=None, ...) reports georeferencing changes,
and applies them only when the affine error is below the supplied threshold.
Both operations preserve pixel values.
add_overviews, delete_overviews and refresh_overviews manage internal or
external GeoTIFF levels. update_raster(src, dst, ...) warps valid source pixels
into an existing file and returns its changed window, or None for no overlap.
Their keyword options follow the CLI, including compression, partial refresh,
clipping geometry and overview updates. Local writable little-endian GeoTIFFs
are required for in-place changes; external overview updates can fail after the
base update commits. Use create-raster, edit-raster and update-raster when
calling the corresponding CLI commands.
COG validation also accepts uploaded TIFF bytes. Use
terrio.validate_cog(data, profile="gdal", full_check=True) for GDAL 3.13
structural rules and compressed-block leader, trailer, order and mask checks.
The result remains (valid, errors, warnings); the default rio-cogeo profile
preserves existing behavior. Automatic full checking applies to local paths
and bytes, and stays off for URLs.
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.
The GDAL profile limits total parsed GDAL metadata to 4 MiB per file and XML nesting
to 128 levels. The default rio-cogeo profile ignores this metadata tag.
GeoPackage maintenance
gpkg_repack(path) compacts a native or /vsimem/ GeoPackage in place and returns
bytes_before, bytes_after, page_size, pages_before and pages_after.
gpkg_repack_bytes(data) returns a compacted bytes copy of a self-contained GeoPackage,
leaving the input unchanged. Both preserve encoded table/index records and schema SQL.
See supported scope and storage safety.
Multidimensional mosaics
terrio.mosaic_multidim(sources, dst=None, *, arrays=None) builds a lazy
coordinate-aligned mosaic. Each source can be a MultidimDataset, a scientific
path/bytes, or a Zarr key/value dictionary. Read coordinate-aligned views through
output.array(name), or write a new netCDF/Zarr snapshot with dst or write.
Source handles stay alive through the mosaic. See
Multidimensional mosaics for alignment,
priority, type preservation and supported scope.
Dataset file management
terrio.dataset_files(path, *, format=None) returns the driver, owned files and a
directory flag. dataset_copy(source, destination, *, format=None, overwrite=False, allow_external_references=False)
and dataset_rename(source, destination, *, format=None, overwrite=False, allow_external_references=False) manage the complete file set and return affected paths
and copied bytes. Paths may be strings or os.PathLike values, including VSI paths.
dataset_delete(path_or_iterable, *, format=None) preflights all inputs and returns a
list of reports. VRT references stay in the source directory tree by default;
allow_external_references=True permits trusted external assets. Deleting a VRT
preserves referenced rasters. Reports include recovery_files: a nonempty list
means the operation committed but cleanup left staged files. I/O releases the GIL.
See the CLI file-management section for staging, rollback, overwrite and format rules.
parquet_create_metadata(inputs, output, *, overwrite=False) accepts a sequence of string
or pathlib partition paths and returns a dictionary containing files, row_groups,
rows, bytes and omitted_metadata_keys. It writes a metadata-only file with relative
partition references, merged GeoParquet bounds/types and regenerated Arrow schema metadata.
Inputs, including referenced column files, are protected from overwrite. Schemas and CRS
must agree; data pages are not validated. See CLI supported scope.
Indexed color conversion
terrio.rgb_to_palette(src, dst=None, *, color_count=256, bit_depth=5,
dither=True, color_map=None, output_nodata=None, mask=None) returns an indexed
Byte Raster. src accepts a path or Raster; color_map accepts a palette
raster/path, encoded color-file bytes, or a sequence of RGBA integer entries.
mask is a two-dimensional uint8 validity array. palette_to_rgb(src, dst=None,
rgba=False) expands to RGB/RGBA; attach_palette(src, color_map, dst=None)
changes only the table. read_color_table(source, filename=None) returns integer
RGBA lists; filename supplies the format hint for encoded bytes.
The three conversion functions accept output format/compression/tile options
and overwrite=False. Existing outputs need overwrite=True; source path
aliases are protected. Operations materialize the input/output. See
Palette conversion for validity rules, formats and
GDAL differences.
Python Raster.write and to_bytes retain dataset masks for GeoTIFF/COG
outputs. Other writers omit separate dataset masks, as before; explicit masks
passed to unsupported writers are rejected. Any raster with a dataset mask, including materialized results, is refused for PMTiles/MBTiles writes and their byte serialization,
since that streaming path supports alpha bands rather than separate masks.
Normal raster writes materialize the full raster and its mask; remote inputs
therefore fetch the selected image blocks and mask blocks.
GeoPackage validation
report = terrio.validate_gpkg("data.gpkg", full_check=True, strict=False)
report_from_bytes = terrio.validate_gpkg(gpkg_bytes, unicode_case=False)
print(report["valid"], report["complete"], report["errors"])
Returns a dictionary with valid, complete, version, errors, warnings and
unsupported_checks. Paths accept native or /vsimem/ files; uploaded bytes are
also supported. The CLI reference documents the
supported rules, SQL subset, case options, memory use and checkpoint requirement.