Skip to content

JavaScript

terrio, pure-Rust geospatial data I/O, compiled to WebAssembly for browsers and Node.js. Datasets are opened from bytes, with a file name telling the format, and written back to bytes. Try it without installing anything on the Playground.

import init, { open, fromGeoJSON, rasterFromArray, transformPoints, describeCrs } from "terrio-js";

await init(); // in Node.js: await init({ module_or_path: fs.readFileSync(wasmPath) })

const bytes = new Uint8Array(await (await fetch("dem.tif")).arrayBuffer());
const dem = open(bytes, "dem.tif");
dem.info();                       // the summary `terrio info --json` prints
dem.width, dem.height, dem.count, dem.dtype, dem.crs, dem.transform, dem.nodata
const band = dem.readBand(1);     // Float32Array, Uint8Array... row after row
const tile = dem.readBand(1, [0, 0, 256, 256]);   // a window [col_off, row_off, width, height]
const cog = dem.toBytes("dem_cog.tif", { format: "COG", compress: "DEFLATE" });

const roads = open(geojsonBytes, "roads.geojson");
roads.toGeoJSON();                // a FeatureCollection object
const fgb = roads.toBytes("roads.fgb", { tSrs: "EPSG:3857", where: "lanes > 2" });
const ipc = roads.toArrowIPC(0, { geometryEncoding: "geoarrow" }); // an Arrow IPC stream
const table = tableFromIPC(ipc);  // Apache Arrow JS; the geometry column is GeoArrow
const back = fromArrowIPC(tableToIPC(table), "roads");

const [xs, ys] = transformPoints([-122.4], [37.8], "EPSG:4326", "EPSG:32610");
// Datum shift and geoid grids from their bytes (https://cdn.proj.org), heights and epochs.
registerGrid("us_nga_egm96_15.tif", new Uint8Array(await (await fetch(gridUrl)).arrayBuffer()));
const [lon, lat, h] = transformPoints([-100], [40], "EPSG:4979", "EPSG:4326+5773", [100]);
transformPoints([135], [-25], "EPSG:7912", "EPSG:7843", [0], { sCoordEpoch: 2010.0 });

In WebAssembly, these formats are read from and written to memory:

Read Write
Raster GeoTIFF/COG (LZW, Deflate, PackBits, ZSTD, LZMA, JPEG, WebP, LERC, JPEG XL), JPEG 2000 (JP2 and J2K), GRIB 1 and 2 (read band by band), netCDF, HDF5, Zarr and GeoZarr (zipped, or with openZarr), Esri ASCII grid, GeoPackage, PMTiles and MBTiles (read by window, zoom levels as overviews) GeoTIFF, COG (lossless compressions, ZSTD included), JPEG 2000, Esri ASCII grid, GeoPackage, PMTiles, MBTiles
Vector GeoJSON, TopoJSON, FlatGeobuf*, GeoPackage, CSV, GeoParquet, KML, KMZ, GML, GPX, OSM (PBF and XML), PMTiles, MBTiles, MVT tiles, zip archives of these, and Shapefile and MapInfo (MIF and TAB) in zip archives GeoJSON, TopoJSON, FlatGeobuf, GeoPackage, KML, KMZ (kmzResources packs icon images), GML, GPX, PMTiles, MBTiles (minzoom, maxzoom options)

* FlatGeobuf files are read by terrio's own driver, with 64-bit offsets. open decodes a layer whole, within WebAssembly's 4 GB of memory; with the file's bytes, that takes about four times the file's size for lines and five for polygons (3.2 GB and 5.4 GB in native builds): an 867 MB file of 2 million OpenStreetMap lines opens in about 3 seconds in Node.js, and a 1 GB file of 2.3 million polygons runs out of memory, but in the memory64 module (see below). openUrl reads files with a spatial index by bounding box, decoding only the features found; a file without one is still read whole.

HEC-RAS geometry and plan HDF files (the full module) open as vector layers with open, and with openVector(bytes, filename, { layers, openOptions }) to join the results HEC-RAS stored (openOptions: { CELL_SUMMARIES: "Maximum Water Surface" }; see HEC-RAS). The whole file is read in memory: opening briefly takes about twice its size, so the 32-bit module reads plan files up to somewhat under 2 GB, and the memory64 module larger ones.

JPEG 2000 files decode with the codec of GDAL's JP2OpenJPEG driver (a Rust port of OpenJPEG), with their GeoJP2 or GMLJP2 georeferencing: a Sentinel-2 band (T10SEG_..._B04.jp2, 10980 x 10980) opens in about 6 seconds in Node.js. Written with a nodata value, a JPEG 2000 comes back as a zip archive of the .jp2 file and the .aux.xml sidecar that holds the value, as GDAL writes it; open reads that archive back.

A smaller download: terrio-js/core

The package holds two WebAssembly modules with the same API. terrio-js has every format above; terrio-js/core leaves out the heaviest ones (GeoParquet and Arrow IPC, netCDF and HDF5, point clouds, JPEG 2000, GRIB, Zarr, AVIF tiles and OpenStreetMap) and keeps GeoTIFF/COG with all its compressions, GeoJSON and the other text formats, FlatGeobuf, GeoPackage, Shapefile, PMTiles and MBTiles, the CRS transformations and the operations, at about half the size:

Module .wasm gzip Brotli
terrio-js 16.9 MB 5.9 MB 4.3 MB
terrio-js/core 9.2 MB 3.2 MB 2.4 MB
import init, { open, features } from "terrio-js/core";

await init();
features(); // { build: "terrio-js/core", features: ["flatgeobuf", "lerc", "lzma", "sqlite", "tiles"] }

A format the module leaves out fails with a message that says so (built without the `jp2` feature: terrio-js/core leaves it out, the full module (terrio-js) has it). A page that needs those formats only now and then can start with the core module and load the full one when it meets them, with a dynamic import("terrio-js"): datasets belong to the module that opened them, so a dataset is passed only to the functions of its own module.

Both modules are built for size: whole-program LTO, the codecs optimized for speed and the rest for size, and wasm-opt -Oz. The full module is 21% smaller than before (20.6 MB, 6.7 MB gzipped) and decodes and writes as fast.

Threads: terrio-js/load and terrio-js/threads

A third module, terrio-js/threads, has every format and a thread pool of Web Workers (or Node.js worker threads) over shared memory: GeoTIFF tiles and strips, JPEG 2000 tiles and Zarr chunks decode in parallel, and GeoTIFF/COG tiles, overviews and Zarr chunks are compressed in parallel, as natively. terrio-js/load picks it where WebAssembly threads run (Node.js 22 or later, and pages that are cross-origin isolated, served with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp), starts its pool, and loads the single-threaded module everywhere else:

import { load } from "terrio-js/load";

const terrio = await load();      // or load({ threads: 4 }), load({ threads: false }), load({ core: true })
terrio.features();                // { build: "terrio-js/threads", threads: 8, ... } or { build: "terrio-js", threads: 1, ... }
const dem = terrio.open(bytes, "dem.tif");

The threaded module can also be imported and started by hand: import init, { initThreadPool } from "terrio-js/threads", then await init() and await initThreadPool(navigator.hardwareConcurrency). Its results are those of the single-threaded module, byte for byte. With 16 threads, against one (a Xeon w7-3445, best of three):

Node.js 26 Chromium Firefox
3612 x 3612 Float32 LZW GeoTIFF, decoded 542 → 92 ms 580 → 91 ms 622 → 113 ms
The same, written as a DEFLATE COG with overviews 2467 → 302 ms 2460 → 306 ms 2680 → 340 ms
10980 x 10980 Sentinel-2 JPEG 2000 band, decoded 6.8 → 0.9 s 6.6 → 0.9 s 6.7 → 0.9 s

Work on vector features (parsing and writing GeoJSON, FlatGeobuf, GeoPackage...) stays on the calling thread in WebAssembly: its many small allocations contend on WebAssembly's allocator, one lock for all threads, and ran several times slower on more threads.

Datasets larger than 4 GB: terrio-js/memory64

WebAssembly modules have 32-bit memories, at most 4 GB. The terrio-js/memory64 module (every format, single-threaded) is built for WebAssembly's 64-bit memories (memory64), up to 16 GB, the limit of V8. load({ memory64: true }) loads it where the runtime has memory64 (Node.js 24 and later, Chrome and Edge 133, Firefox 134), and the usual 32-bit module elsewhere (Safari), which onFallback reports and features().build tells:

import { load, memory64Supported } from "terrio-js/load";

const terrio = await load({ memory64: true });
const ds = terrio.open(bytes, "polygons.fgb");   // a 1.27 GB FlatGeobuf of 289,890 polygons
terrio.features().memory;                        // 8 GB of memory: beyond a 32-bit module's reach

That FlatGeobuf, 290 thousand US county polygons, opens in 4 seconds in Node.js with 7.5 GB of module memory, where the 32-bit modules run out of memory. It is 7 to 17% slower than the 32-bit module (64-bit addresses and bounds checks), and larger: 25.4 MB, 6.4 MB gzipped (4.4 MB Brotli). Use it for the datasets that need it. It is single-threaded, and readBand windows are numbers of pixels below 2^32, as in the other modules.

It is built with the parts of the toolchain still in progress: wasm64-unknown-unknown is a tier 3 Rust target without a prebuilt standard library (platform support), built on nightly with -Z build-std; its SIMD intrinsics, which zlib-rs uses, are unstable (rust-lang/rust#90599); wasm-pack builds for wasm32 only, so js/build.mjs runs cargo and wasm-bindgen (which supports wasm64) itself; and the wasm-opt wasm-pack downloads (binaryen 117) cannot read 64-bit tables, so the module is not passed through it. Three crates treat WebAssembly as wasm32 only, and the build patches copies of them (target_arch = "wasm32" read as target_family = "wasm"): tar, which wbraster depends on, acadrust and arrow-buffer; getrandom, which seeds ahash's hash maps, has no wasm64 backend and is given one (crypto.getRandomValues).

Building

node js/build.mjs                         # the npm package in js/pkg: all modules, built for size
node --test js/tests/package.test.mjs     # (threads and memory64 need the nightly js/build.mjs names)
TERRIO_JS_LOAD=../pkg/load.js node --test js/tests/node.test.mjs   # the suite on the threaded module
TERRIO_JS_LOAD=../pkg/load.js TERRIO_MEMORY64=1 TERRIO_LARGE_TESTS=1 node --test js/tests/node.test.mjs  # memory64, > 4 GB
node js/tests/browser.mjs ../terrio-data  # in headless browsers (Playwright), timings included
wasm-pack build js --release --target nodejs --out-dir pkg-node --out-name terrio
node --test js/tests/node.test.mjs        # TERRIO_TEST_DATA=... adds real files checked against GDAL
# terrio-js/core, and the tests of the formats it has:
wasm-pack build js --release --target nodejs --out-dir pkg-node/core --out-name terrio --no-pack -- --no-default-features --features core
TERRIO_JS_PKG=../pkg-node/core/terrio.js node --test js/tests/node.test.mjs
node js/bench/bench.mjs ../terrio-data    # timings of decoding and writing real files

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

openCatalog(index, sources, options) opens an index Dataset, vector file bytes or GeoJSON, with a map of location strings to raster datasets or file bytes:

const catalog = terrio.openCatalog(tileIndex, { "left.tif": left, "right.tif": right }, {
  tr: [30, 30], sortField: "priority", cacheSize: 16,
});
const pixels = catalog.readBand(1, [100, 200, 256, 256]);

The index uses an in-memory R-tree in WASM; only selected sources enter the bounded cache. Supplied lazy datasets retain window/overview reads, and encoded files are opened only when selected. Existing dataset buffers/readers remain shared with the caller; freeing the original Dataset does not close the catalog's handle. Locations are keys in the supplied source map; WASM does not open filesystem paths or URLs. Options are locationField, sortField, sortDescending, tr, te, srs, bands, ot, nodata, alphaBand, cacheSize and resampling. Source masks/nodata and zero alpha are transparent; tiles in other CRSs are reprojected into the index CRS. See CLI catalogs for the supported GTI subset.

Pansharpening

const sharp = terrio.pansharpen(pan, rgbnir, { bands: [1, 2, 3], bitdepth: 12, nodata: 0 });
const rgb = terrio.pansharpen(pan, [red, green, blue], { weights: [0.6, 0.3, 0.1] });
const sources = { "pan.tif": panBytes, "rgb.tif": spectralBytes };
const xml = terrio.pansharpenVrt("pan.tif", ["rgb.tif"], sources, { bands: [1, 2, 3] });
const virtual = terrio.openVrt(xml, sources);
const window = virtual.readBand(1, [100, 200, 64, 64]);

Inputs can be datasets or raster bytes. All spectral inputs contribute to intensity; bands selects/reorders output bands. See pansharpen for working types, interpolation, nodata, bit depth and extent handling.

Focal statistics, collars and enhancement

const max5 = terrio.neighbors(dem, { kernel: "equal", size: 5, method: "max" });
const edges = terrio.neighbors(image, { kernel: ["gaussian", "edge2"], band: 2, ot: "Float32" });
const clean = terrio.cleanCollar(scene, { addAlpha: true });          // flood fill
const nb = terrio.nearblack(scene, { colors: ["0,0,0", "255,255,255"], pixelDistance: 0 });
const lut = terrio.enhanceLuts(image, {});                            // gdalenhance's LUT file text
const eq = terrio.enhance(image, { equalize: true });
const again = terrio.enhance(image, { config: lut });

nearblack and cleanCollar accept colors as a single string or an array of strings.

These are terrio neighbors, clean-collar, nearblack and enhance (see neighbors, clean-collar and enhance), on rasters in memory. Datasets hold no mask: addMask is refused, and an input's mask is not kept (use addAlpha). There is no in-place update; LUT files are passed and returned as text.

SQL queries

sql(src, query) returns a Dataset using the shared OGR SQL subset. Inputs may be a Dataset, file bytes, { bytes, filename }, or GeoJSON.

const result = sql(countries, "SELECT ADMIN, POP_EST/1000000.0 AS millions FROM countries");
const summary = sql(countries, "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(src, tolerance), segmentize(src, maxLength), convexHull(src), concaveHull(src, ratio, { allowHoles, tight }), swapXy(src), setGeomType(src, { geometryType, multi, single, linear, dim, layerOnly, featureOnly, skip }), combine(src, { groupBy, keepNested, addExtraFields }) and sort(src, { method }) run the vector geometry commands on every layer of a Dataset, file bytes or GeoJSON, and return a Dataset.

const hulls = concaveHull(combine(places, { groupBy: ["adm0_a3"] }), 0.3, { allowHoles: true });
const simple = simplify(counties, 0.01);
const sorted = sort(simple, { method: "hilbert" });

Vector datasets

const merged = terrio.concat([countries, { bytes: usBytes, filename: "us.gpkg" }], { mode: "single", sourceLayerFieldName: "source" });
const parts = terrio.partition(counties, { fields: ["STATE"], format: "GPKG" }); // [{ path, bytes }]
const typed = terrio.setFieldType(countries, "Integer64", { fieldName: "POP_EST" });
const points = terrio.makePoint(trace, "longitude", "latitude", { crs: "EPSG:4326" });
const schema = terrio.exportSchema(countries); // the OGR_SCHEMA object
const empty = terrio.createVector("empty.gpkg", { schema }); // the new file's bytes

These are terrio concat, partition, set-field-type, make-point, export-schema and create (GDAL's gdal vector commands of the same names), in memory: partition returns the parts' relative paths and bytes, and createVector the bytes of the dataset, rather than writing files. exportSchema describes the layers read (GeoPackage column declarations are read only from files, by the command line, Python and Rust). See Vector datasets for the options and GDAL's rules.

Renaming and updating vector datasets

renameLayer(src, options), editVector(src, options) and updateVector(src, dst, options) return a new Dataset, preserving their inputs. Serialize the result with toBytes to persist it.

const renamed = terrio.renameLayer(countries, {ascii: true, lowerCase: true});
const assigned = terrio.editVector(renamed, {crs: "EPSG:4326"});
const updated = terrio.updateVector(changes, renamed, {key: ["name"], mode: "merge"});

Rename accepts inputLayer, outputLayer, ascii, lowerCase, filenameCompatible, reservedCharacters, replacementCharacter, maxLength. Edit accepts activeLayer, crs ("none" removes it), geometryType, unsetFid. CRS assignment does not transform coordinates. Dataset metadata edits require the native path API because the memory model does not retain dataset metadata. Update accepts inputLayer, outputLayer, key, mode (merge, update-only, append-only); it matches feature IDs when key is omitted.

Viewshed

const visible = terrio.viewshed(dem, 500000, 4400000, { oz: 2, tz: 0, md: 5000 });
const height = terrio.viewshed(dem, 500000, 4400000, { om: "GROUND", cc: 0 });

The input can be a dataset or raster bytes. om selects NORMAL visibility, DEM minimum visible elevation, or GROUND minimum height above the DEM. cc is 1 minus refraction. See viewshed for units, curvature, radius cropping and nodata behavior.

Vector gridding and linear referencing

const grid = terrio.vectorGrid(points, { algorithm: "invdistnn", zfield: "temp", radius: 50000, size: [200, 200] });
const parts = terrio.linerefCreate(route, stones, { positionField: "pos", step: 1000 });
const position = terrio.linerefPosition(parts, -1.4345, 51.9497);
const [[x, y, z]] = terrio.linerefCoordinates(parts, 15977.724709);
const sub = terrio.linerefSubline(parts, 13300, 17400);

Inputs are datasets, vector bytes or GeoJSON. vectorGrid options are camelCase versions of the Python keywords (minPointsPerQuadrant...). See vector-grid and lineref.

Linear reference lookups accept {layer: "parts"} to select a source layer. linerefSubline also accepts layerName to name its output layer.

Reuse a Dataset, such as terrio.open(bytes, "parts.gpkg"), for repeated lookups; serialized bytes and GeoJSON are parsed on each call.

Compare, pixel info and XYZ

const report = terrio.compare(reference, output, { skipBinary: true });
console.log(report.difference_count, report.bands[0].pixels_differing, report.bands[0].max_abs_diff);
const values = terrio.pixelInfo(dem, [[-105.5, 39.5, "site A"]], { positionCrs: "EPSG:4326", resampling: "bilinear" });
const fromPoints = terrio.pixelInfo(dem, sitesGeoJSON, { format: "json" });
const text = terrio.xyz(dem, { srcwin: [0, 0, 10, 10], skipNodata: true });

Rasters are Datasets (read by window) or file bytes. compare makes the binary comparison when both inputs are bytes; GeoTIFF TIFFTAG_* items and RPC tags, .aux.xml metadata and subdatasets need files and are not compared in WebAssembly. pixelInfo takes [x, y]/[x, y, text] positions, or a vector dataset, file bytes or GeoJSON of points, and returns GeoJSON (an object), json (an array), or csv, text and value strings. xyz returns the text. See compare, pixel-info and xyz.

pixelInfo(src, positions, {format: "value", fieldSep: "|"}) selects the separator between band values. For vector file bytes, pass the GeoJSON result to fromGeoJSON and serialize that Dataset with toBytes.

xyz returns the complete text as one JavaScript string. Use srcwin/skip for large XYZ exports. Native Rust XYZ writers stream the output. Byte-based compare copies each input once and reuses that buffer for binary comparison and lazy reads.

Pixel-wise operations

const physical = terrio.unscale(packed, { ot: "Float64" });
const rgba = terrio.nodataToAlpha(landsat, { nodata: 0 });
const groups = terrio.reclassify(nlcd, "11=1;[21,25)=2;DEFAULT=0", { ot: "Byte" });
const fromFile = terrio.reclassify(nlcd, "", { mappingFile: await (await fetch("map.txt")).text() });
const pixels = terrio.asFeatures(dem, { geometryType: "polygon", includeXy: true, skipNodata: true });

Inputs are datasets or raster bytes; asFeatures returns a vector dataset (toGeoJSON, toBytes). WebAssembly cannot read local files, so a mapping file's text is passed as mappingFile instead of @file. See unscale, nodata-to-alpha, reclassify and as-features.

For nodataToAlpha, non-finite overrides use the strings "nan", "inf" or "-inf" (as a scalar or in the nodata array). Raw JavaScript non-finite numbers and explicit null are rejected before JSON serialization, so they cannot silently become a default override.

Geolocation-array warping

Pass longitude and latitude rasters explicitly when array datasets cannot be reopened from paths in WebAssembly. x and y accept a Dataset, file bytes, or { bytes, filename }:

const output = terrio.warp(swath, {
  tSrs: "EPSG:4326",
  geolocation: { x: longitude, y: latitude, pixelStep: 2, lineStep: 2, pixelCenter: true },
});

xBand and yBand select the 1-based coordinate bands (default 1). Optional srs defaults to EPSG:4326, pixelOffset/lineOffset to 0, and steps to 1. pixelCenter shifts by half an array step. geoloc: true requires geolocation metadata; the explicit geolocation option supplies arrays for the first source. The operation remains pure Rust in full and core WASM builds. See CLI warping for invalid coordinates, antimeridian handling and boundary differences from GDAL.

Raster management

createRaster(like, options) returns a raster Dataset. Options include size, bandCount, ot, crs, bbox, nodata, unsetNodata, burn, metadata and copyMetadata. Encode it with toBytes("output.tif"); addOverviews(bytes, options), refreshOverviews(bytes, options) and deleteOverviews(bytes, options) return updated GeoTIFF bytes. External overview files are unavailable for byte inputs.

editRaster(bytes, options) changes CRS, geotransform, bbox, GCPs, nodata, metadata or statistics without resampling. Options use camelCase; GCPs are [pixel, line, x, y, z] arrays. Histogram storage needs a sidecar and is refused. moveRaster(bytes, {tSrs, sSrs, errorThreshold, ignoreCogLayoutBreak}) returns {bytes, report}; report contains geotransform, corners, errors, maxError and updated. Without errorThreshold it reports the proposed affine change without applying it. updateRaster(source, destinationBytes, options) returns updated GeoTIFF bytes and supports resampling, clipping geometry and overview refresh. Byte inputs/results are materialized; large-file editing should use native APIs. refreshOverviews takes explicit bbox extents only: there is no like option, so compute a template's extent from its geotransform. createRaster returns an in-memory dataset, so the file-only copyOverviews option of the CLI and Python is unavailable; add overviews to the encoded bytes with addOverviews.

File management

terrio.vsiWrite("/vsimem/data/a.zip", zipBytes);
terrio.vsiList("/vsizip//vsimem/data/a.zip", { recursive: true });  // [{name, type, size, ...}]
const shp = terrio.vsiRead("/vsizip//vsimem/data/a.zip/roads.shp");
terrio.vsiCopy("/vsimem/data", "/vsimem/backup", { recursive: true });
terrio.vsiSync("/vsimem/data/", "/vsimem/mirror", { recursive: true, strategy: "overwrite" });
terrio.vsiMove("/vsimem/backup", "/vsimem/old");
terrio.vsiDelete("/vsimem/old", { recursive: true });
// Remote listings through fetch: public S3 buckets, Google Cloud Storage, Azure (SAS URLs) and
// HTML index pages. Returns {entries, requests, bytes}.
const { entries } = await terrio.listUrl("s3://bucket/prefix", { recursive: true });

The vsi commands support the in-memory file system (/vsimem/) and the zip archives in it, and listUrl provides remote listings. WebAssembly has no local file system (local paths fail) and no request signing: tar archives, local files, signed S3 requests and writes to object stores are available in the native CLI, Rust and Python only. /vsimem/ files carry no dates, so vsiSync compares content when an in-memory endpoint lacks a timestamp. In browsers, a store must allow the page's origin (CORS) for listUrl.

HTML index responses use a readable byte stream, a 4 KiB sniff prefix and a 4 MiB body limit, matching the native path. Binary/ordinary web content is refused; custom fetch implementations must return a standard Response. Object-store XML pagination is unchanged.

VSI listing bindings return structured flat entries. Nested tree presentation (--tree) is currently available through the CLI; JSON serialization flags apply to CLI output.

validateCog(bytes, {profile: "gdal", fullCheck: true}) checks uploaded TIFF structure without decoding pixels and returns is_valid, errors, warnings, ifd_offsets, data_offsets, overview_factors, structural_metadata and layout_notes. The default profile is rio-cogeo; gdal selects GDAL 3.13 rules with full leader/trailer, row-order and interleaved-mask checks enabled for uploaded bytes unless fullCheck: false is supplied. External sidecars are unavailable for a standalone byte buffer.

GeoPackage maintenance

gpkgRepack(path) compacts an uploaded /vsimem/ GeoPackage in place and returns bytes_before, bytes_after, page_size, pages_before and pages_after. gpkgRepackBytes(bytes) returns a compacted Uint8Array copy of a self-contained image. Read the in-memory result with vsiRead(path). Encoded records, indexes, rowids and schema SQL are retained. See supported scope.

Multidimensional mosaics

mosaicMultidim(sources, {arrays}) builds a lazy coordinate-aligned MultidimDataset from dataset handles or uploaded scientific inputs. arrays selects numeric arrays by path or unambiguous name. Source handles can be freed after construction; mosaic views keep the source data alive. See Multidimensional mosaics for alignment, priority, Int64/UInt64 handling and snapshot formats.

Seek-optimized ZIP archives

terrio.sozipCreate(["/vsimem/roads.gpkg", "/vsimem/dem.tif"], "/vsimem/data.zip",
  { junkPaths: true, enable: "yes", chunkSize: 65536 });
terrio.sozipList("/vsimem/data.zip");
terrio.sozipValidate("/vsimem/data.zip");
terrio.sozipOptimize("/vsimem/data.zip", "/vsimem/optimized.zip");

These synchronous functions operate on uploaded /vsimem/ files in WebAssembly. Creation and optimization return added, seek_optimized and bytes counts; validation returns valid, sozip_files, messages and errors. See the supported options and limitations.

Dataset file management

datasetFiles(path, {format}) returns the owned file set. datasetCopy(source, destination, {format, overwrite, allowExternalReferences}) and datasetRename(...) return affected paths and copied bytes. datasetDelete(pathOrArray, {format}) preflights the whole batch and returns one report per input. These APIs manage /vsimem/ datasets; archive members may be copied out. HTTP/cloud mutation is unavailable in WebAssembly. VRT references stay in the source directory tree by default; enable allowExternalReferences only for trusted external assets. VRT deletion preserves referenced rasters. Reports contain recovery_files; leftovers indicate a committed operation whose staged cleanup needs attention. The CLI file-management section documents companion discovery, overwrite, staging and recovery behavior shared by these APIs.

parquetCreateMetadata(inputs, output, {overwrite: false}) creates a metadata-only Parquet file from an array of uploaded /vsimem/ partition paths. It returns files, row_groups, rows, bytes and omitted_metadata_keys. Read the resulting bytes with vsiRead(output). Schemas and CRS must agree; GeoParquet bounds/types are merged and Arrow schema metadata is regenerated. Direct and referenced input partitions are protected from overwrite. See CLI supported scope; no data pages are validated.

Indexed color conversion

rgbToPalette(source, {colorCount: 256, bitDepth: 5, dither: true, colorMap, outputNodata, mask}) returns an indexed Byte Dataset. Sources accept Dataset objects or uploaded raster bytes (Uint8Array or {bytes, filename}). colorMap accepts a palette Dataset/upload, color-file text, {text, filename}, or RGBA integer arrays; mask accepts a flat Uint8Array/array of validity bytes. paletteToRgb(source, {rgba: true}) expands to RGBA (RGB by default), and attachPalette(source, colorMap) changes only the table. readColorTable(source, filename) reads integer RGBA entries from these same palette sources. Operations materialize the input/output. See Palette conversion for formats and validity rules.

Raster toBytes preserves dataset masks for GeoTIFF/COG without tSrs. Other formats omit separate dataset masks. Reprojecting a raster with a separate dataset mask is refused explicitly; use an alpha band instead.

Color-file text passed to palette APIs as a Uint8Array needs a filename hint or {bytes, filename: "colors.txt"}. Plain strings and {text, filename} also accept text tables. Tile-archive serialization rejects any raster with a dataset mask, including materialized palette results; use an explicit alpha band.

GeoPackage validation

const report = terrio.validateGpkg(gpkgBytes, { fullCheck: true, strict: false });
terrio.vsiWrite("/vsimem/data.gpkg", gpkgBytes);
const fromPath = terrio.validateGpkgPath("/vsimem/data.gpkg", { unicodeCase: false });

Both functions return valid, complete, version, errors, warnings and unsupported_checks. WebAssembly path inputs use /vsimem/; the bytes API accepts uploaded images directly. See the supported rules and limitations.

Separate raster tiles

planRasterTiles(src, options) returns the grid and tile count without reading pixels and includes the destination crs (null when unknown). It validates metadata-based rendering options. rasterTiles(src, options, callback) sends each encoded tile to callback(name, bytes) and returns generation counts. Options include tilingScheme (raster, WebMercatorQuad, WorldCRS84Quad), grid (for example {tileSize: 128, minZoom: 0, maxZoom: 2}), format (PNG or GTiff), resampling, overviewResampling, alpha, outputNodata, skipBlank and maxTiles. The caller stores the emitted bytes. See the CLI tiling reference for supported output and transparency policies.