whitebox-wasm
v0.4.0
Published
Pure-Rust geospatial toolkit (raster, vector, LiDAR, projections) compiled to WebAssembly
Maintainers
Readme
whitebox-wasm
Pure-Rust geospatial toolkit compiled to WebAssembly. No GDAL, no PROJ, no native libraries, no server. Work with raster, vector, and LiDAR data entirely in the browser, Node, Deno, or any Wasm host:
- Raster - read/write GeoTIFF / BigTIFF / COG, stats, range-request streaming
- Projections - full EPSG + user-defined CRS to WGS84 lon/lat
- Vector - read GeoJSON, TopoJSON, GML, GPX, KML, FlatGeobuf, GeoPackage, KMZ -> GeoJSON, with reprojection
- LiDAR - read LAS / LAZ / PLY point clouds (xyz, classification, intensity)
- Analysis - convex hull, Moran's I spatial autocorrelation
- Tools - the full WhiteboxTools suite (733 tools) via
whitebox-wasm/tools
This wraps wbgeotiff, the shared GeoTIFF engine from the original
whitebox_next_gen project
by John Lindsay (Whitebox Geospatial Inc.) - the next-generation, pure-Rust
rewrite of WhiteboxTools - and exposes a tiny WebAssembly API. The entire codec
stack (Deflate, LZW, PackBits, JPEG, WebP, JPEG-XL, PNG predictors, BigTIFF,
tiling) is pure Rust with zero C dependencies, so the published module imports
nothing from the host beyond its own linear memory.
Install
npm install whitebox-wasmUsage (browser / Deno / Node >= 20, ESM)
import init, { geotiff_info, GeoTiffReader, CogBuilder } from "whitebox-wasm";
await init(); // in Node, pass the .wasm bytes to init()
const bytes = new Uint8Array(await (await fetch("dem.tif")).arrayBuffer());
// Metadata only - O(header) memory, works on multi-GB rasters:
console.log(JSON.parse(geotiff_info(bytes)));
// { ok:true, width, height, bands, epsg, nodata, bits_per_sample,
// sample_format, compression, tiled, bigtiff }
// Parse once, read many times:
const tif = new GeoTiffReader(bytes);
console.log(tif.width, tif.height, tif.bands, tif.epsg, tif.nodata);
console.log(Array.from(tif.geo_transform())); // [ox, px, 0, oy, 0, -py]
console.log(Array.from(tif.bounding_box())); // [minx, miny, maxx, maxy]
const band0 = tif.read_band_f64(0); // Float64Array
console.log(JSON.parse(tif.stats_json()));
// Encode a Cloud Optimized GeoTIFF (also a valid plain GeoTIFF):
const cb = new CogBuilder(width, height, 1);
cb.set_epsg(32610);
cb.set_origin(500000, 4000000, 30); // x_min, y_max, pixel size
cb.set_compression("deflate");
cb.set_nodata(-9999);
const cogBytes = cb.write_f64(new Float64Array(pixels)); // Uint8ArrayIn Node the web target needs the wasm bytes handed to init():
import { readFileSync } from "node:fs";
import init, { geotiff_info } from "whitebox-wasm";
await init({ module_or_path: readFileSync("node_modules/whitebox-wasm/whitebox_wasm_bg.wasm") });API
Convenience functions
| Function | Returns |
|---|---|
| geotiff_info(bytes) | JSON metadata, header-only (incl. bbox, center, center_lonlat) |
| geotiff_stats(bytes) | JSON band-0 stats {valid, min, max, mean, ...} |
| geotiff_read_band_f64(bytes, band) | Float64Array of band pixels |
| version() | crate version string |
GeoTiffReader (parse once, read many)
new GeoTiffReader(bytes), then:
- Properties:
width,height,bands,bits_per_sample,sample_format,compression,is_bigtiff,epsg,nodata geo_transform()->[x_origin, pixel_width, row_rot, y_origin, col_rot, pixel_height](empty if none)bounding_box()->[min_x, min_y, max_x, max_y]in the dataset CRS (empty if not georeferenced)center()->[x, y]center in the dataset CRScenter_lonlat()->[lon, lat]WGS84 degrees;bounds_lonlat()->[min_lon, min_lat, max_lon, max_lat]WGS84 (any EPSG via the bundled pure-Rust projection engine; also handles user-defined projections like NLCD's Albers; empty if not georeferenced)value_transform()->[scale, offset](GDAL scale/offset; empty if none)info_json(),stats_json()-> JSON stringsread_band_f64(band)/read_all_f64()->Float64Array(any on-disk type converted)read_band_bytes(band)-> rawUint8Array- Native typed reads (require matching on-disk type):
read_band_u8/i8/u16/i16/u32/i32/f32
CogBuilder (write Cloud Optimized GeoTIFFs)
new CogBuilder(width, height, bands), configure, then write_u8 / write_f32 / write_f64(data) -> Uint8Array:
set_epsg(code),set_nodata(v),set_compression("none|lzw|deflate|packbits|webp|jpeg|jpegxl")set_geo_transform([6 values])orset_origin(x_min, y_max, pixel_size)set_tile_size(px),set_bigtiff(bool),set_overview_levels([2,4,8])
Output is a tiled COG with overviews and GDAL ghost metadata - readable by GDAL, rasterio, QGIS, and GeoTiffReader.
CogStream (read remote COGs via HTTP range requests)
Read a window or overview out of a large COG without downloading the whole file. The wasm parses the header and reports byte ranges; your JS does the range requests:
import init, { CogStream } from "whitebox-wasm";
await init();
const url = "https://example.com/big-cog.tif";
const range = (a, b) => fetch(url, { headers: { Range: `bytes=${a}-${b}` } })
.then(r => r.arrayBuffer()).then(b => new Uint8Array(b));
const stream = new CogStream(await range(0, 65535)); // header prefix
const lv = JSON.parse(stream.levels_json())[0]; // level 0 = full res
const tiles = JSON.parse(stream.tiles_for_window(0, 1200, 800, 256, 256));
for (const t of tiles) {
const bytes = await range(t.offset, t.offset + t.length - 1); // one tile
const px = stream.decode_tile_f64(0, bytes); // Float64Array
// place px (tile_width x tile_height) into your output window...
}new CogStream(headerBytes)- parse the IFD chain + tile index (throws if the prefix is too short; fetch more and retry).num_levels,epsg,nodata,geo_transform(),levels_json()bounding_box(),center(),center_lonlat(),bounds_lonlat()(same semantics asGeoTiffReader)tiles_for_window(level, x, y, w, h)-> JSON[{col,row,offset,length}]tile_range(level, col, row)->[offset, length]decode_tile_f64(level, tileBytes)->Float64Array(one decoded tile)
Use a higher level (overview) for zoomed-out views. See
examples/cog-stream.mjs for a full window read
that fetches only the tiles it needs (about 13% of a 5.7 MiB file for a 256x256
window). Requires a tiled COG on a server that supports HTTP range requests.
JSON-returning functions report failures as {"ok":false,"error":"..."}; class methods throw on error.
Vector
import init, { vector_to_geojson, vector_info, vector_to_geojson_reproject } from "whitebox-wasm";
await init();
const bytes = new Uint8Array(await (await fetch("data.fgb")).arrayBuffer());
const geojson = JSON.parse(vector_to_geojson(bytes, "flatgeobuf"));
const meta = JSON.parse(vector_info(bytes, "flatgeobuf")); // { features, geometry, epsg, fields, bbox }
const wgs84 = vector_to_geojson_reproject(bytes, "flatgeobuf", 4326, 0); // dst, src(0=auto)vector_formats()-> supported formats (geojson, topojson, gml, gpx, kml, flatgeobuf, geopackage, kmz)vector_to_geojson(data, format)-> GeoJSON stringvector_to_geojson_reproject(data, format, dst_epsg, src_epsg)-> reprojected GeoJSON (src_epsg=0uses the layer CRS, or 4326)vector_info(data, format)-> JSON{name, features, geometry, epsg, fields, bbox}
LiDAR
import init, { lidar_info, lidar_read_xyz } from "whitebox-wasm";
await init();
const las = new Uint8Array(await (await fetch("cloud.laz")).arrayBuffer());
const meta = JSON.parse(lidar_info(las, "laz")); // { points, epsg, point_format, bounds }
const xyz = lidar_read_xyz(las, "laz"); // Float64Array [x0,y0,z0, x1,y1,z1, ...]lidar_formats(),lidar_info(data, format)(header-only count/bounds for LAS/LAZ)lidar_read_xyz->Float64Array;lidar_read_classification->Uint8Array;lidar_read_intensity->Uint16Array
Analysis
convex_hull(points_xy)-> hull ringFloat64Array(input[x0,y0,x1,y1,...])morans_i(points_xy, values, distance_threshold)-> JSON global spatial autocorrelation{morans_i, expected, variance, z_score, p_value, n}
Tools (the full WhiteboxTools suite)
The whitebox-wasm/tools subpath runs the complete WhiteboxTools algorithm
suite (733 tools - slope, filters, hydrology, geomorphometry, vector ops,
...). The tools are path-based, so they run through an in-memory WASI filesystem
(bundled whitebox-cli.wasm); raster outputs are Cloud Optimized GeoTIFFs.
import { runTool, listTools } from "whitebox-wasm/tools";
console.log((await listTools()).length); // 733
// raster: slope -> a COG
const { files } = await runTool("slope", {
args: ["--input=/work/dem.tif", "--output=/work/slope.tif", "--units=degrees"],
input: { "dem.tif": demBytes }, // Uint8Array, placed under /work
});
const slopeCog = files["slope.tif"]; // Uint8Array (tiled, Deflate, overviews)
// vector: convex hull -> GeoJSON
const hull = await runTool("minimum_convex_hull", {
args: ["--input=/work/in.geojson", "--output=/work/hull.geojson"],
input: { "in.geojson": geojsonBytes },
});listTools()->string[]of tool idsrunTool(id, { args, input })->{ exitCode, stdout, files }(files= outputs the tool wrote)initTools(source?)-> compile the runner; in Node pass the wasm bytes (browsers/bundlers omit it)
Needs the @bjorn3/browser_wasi_shim peer (declared as a dependency). The
whitebox-cli.wasm (~5 MB gzipped) is only fetched the first time you call a
tool, so the rest of the library stays lightweight.
Limits
WebAssembly is 32-bit, so linear memory is capped at ~4 GiB. geotiff_info is
header-only and unaffected, but reads/writes that materialize a full raster are
bounded by that ceiling (a national 1-billion-pixel raster cannot be fully
decoded in-browser). For such data, read metadata only or process server-side.
Links
- Source, live demo, and issues: https://github.com/opengeos/whitebox-wasm
- Live browser demo: https://opengeos.org/whitebox-wasm/
License
Dual-licensed under MIT or Apache-2.0, at your option. Includes the vendored
wbgeotiff, wbprojection, wbvector, wblidar, wbtopology, wbspatialstats, and wbhdf crates (Copyright John Lindsay, Whitebox
Geospatial Inc.), used under the same dual license.
