crossbind
GitHub

GeoTIFF for WASI

v1.7.4WASI

GeoTIFF 1.7.4 for command-line programs under wasmtime, precompiled for wasm32-wasip3, single-threaded as @crossbind/port-geotiff-wasi.

npm install @crossbind/port-geotiff-wasi@beta

Install

shell
npm install @crossbind/port-geotiff-wasi@beta crossbind@beta

A GeoTIFF command-line tool

WASI has no JavaScript bindings: the program is a main() that links libgeotiff, libtiff and PROJ, built into one .wasm and run with wasmtime. It writes the first usage example's file, byte for byte, then says where it is. GTIFGetDefn looks EPSG codes up in PROJ's proj.db, which the build copies to dist/data; the last command mounts that folder and sets PROJ_DATA.

crossbind.config.js
import geotiffWasi from '@crossbind/port-geotiff-wasi/crossbind.config.js';
 
export default {
general: { name: 'geotiff-tool' },
dependencies: [geotiffWasi],
// PROJ's data folder is copied from the build directory to dist/data, so the two must differ.
paths: { config: import.meta.url, output: 'dist' },
};
src/native/main.cpp
// geotiff-tool: writes a small GeoTIFF, and says where any GeoTIFF is: its size, its coordinate
// system and its corners in map units and in degrees. GTIFGetDefn reads PROJ's proj.db, so the
// commands mount the data folder the build puts next to the .wasm and point PROJ_DATA at it.
#include <geo_normalize.h>
#include <geotiff.h>
#include <geovalues.h>
#include <xtiffio.h>
 
#include <sys/stat.h>
 
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <string>
#include <vector>
 
using Tiff = std::unique_ptr<TIFF, void (*)(TIFF*)>;
using Keys = std::unique_ptr<GTIF, void (*)(GTIF*)>;
 
static std::string taken(char* text) {
const std::string value = text ? text : "unknown";
GTIFFreeMemory(text);
return value;
}
 
// A blank 100 x 100 8-bit image in the projected system `epsg`, upper-left corner at (x, y).
static int write(const char* path, int epsg, double x, double y, double pixel) {
Tiff tif(XTIFFOpen(path, "w"), XTIFFClose);
if (!tif) return std::fprintf(stderr, "cannot create %s\n", path), 1;
const int side = 100;
TIFFSetField(tif.get(), TIFFTAG_IMAGEWIDTH, side);
TIFFSetField(tif.get(), TIFFTAG_IMAGELENGTH, side);
TIFFSetField(tif.get(), TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(tif.get(), TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tif.get(), TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(tif.get(), TIFFTAG_ROWSPERSTRIP, side);
const double tiepoint[6] = {0, 0, 0, x, y, 0};
const double scale[3] = {pixel, pixel, 0};
TIFFSetField(tif.get(), TIFFTAG_GEOTIEPOINTS, 6, tiepoint);
TIFFSetField(tif.get(), TIFFTAG_GEOPIXELSCALE, 3, scale);
Keys keys(GTIFNew(tif.get()), GTIFFree);
GTIFKeySet(keys.get(), GTModelTypeGeoKey, TYPE_SHORT, 1, ModelTypeProjected);
GTIFKeySet(keys.get(), GTRasterTypeGeoKey, TYPE_SHORT, 1, RasterPixelIsArea);
GTIFKeySet(keys.get(), ProjectedCSTypeGeoKey, TYPE_SHORT, 1, epsg);
GTIFWriteKeys(keys.get());
keys.reset();
std::vector<unsigned char> row(side, 0);
for (int line = 0; line < side; line += 1) {
if (TIFFWriteScanline(tif.get(), row.data(), line, 0) < 0) return std::fprintf(stderr, "cannot write %s\n", path), 1;
}
tif.reset();
struct stat info;
stat(path, &info);
std::printf("libgeotiff %s write: %s, %d x %d pixels in EPSG:%d, %lld B\n", LIBGEOTIFF_STRING_VERSION, path, side, side, epsg, static_cast<long long>(info.st_size));
return 0;
}
 
static int describe(const char* path) {
Tiff tif(XTIFFOpen(path, "r"), XTIFFClose);
if (!tif) return std::fprintf(stderr, "%s is not a TIFF file\n", path), 1;
Keys keys(GTIFNew(tif.get()), GTIFFree);
GTIFDefn defn;
if (!keys || !GTIFGetDefn(keys.get(), &defn)) return std::fprintf(stderr, "%s has no GeoTIFF coordinate system\n", path), 1;
uint32_t width = 0;
uint32_t height = 0;
uint16_t bits = 8;
uint16_t bands = 1;
TIFFGetField(tif.get(), TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif.get(), TIFFTAG_IMAGELENGTH, &height);
TIFFGetFieldDefaulted(tif.get(), TIFFTAG_BITSPERSAMPLE, &bits);
TIFFGetFieldDefaulted(tif.get(), TIFFTAG_SAMPLESPERPIXEL, &bands);
std::printf("%s: %u x %u pixels, %u band%s, %u bits per sample\n", path, width, height, bands, bands == 1 ? "" : "s", bits);
 
const bool projected = defn.Model == ModelTypeProjected;
char* name = nullptr;
if (projected) GTIFGetPCSInfo(defn.PCS, &name, nullptr, nullptr, nullptr);
else GTIFGetGCSInfo(defn.GCS, &name, nullptr, nullptr, nullptr);
const std::string crs = taken(name);
GTIFGetDatumInfo(defn.Datum, &name, nullptr);
const std::string datum = taken(name);
std::printf("EPSG:%d %s, datum %s\n", projected ? defn.PCS : defn.GCS, crs.c_str(), datum.c_str());
 
const char* labels[2] = {"upper left", "lower right"};
for (int corner = 0; corner < 2; corner += 1) {
double x = corner ? width : 0;
double y = corner ? height : 0;
if (!GTIFImageToPCS(keys.get(), &x, &y)) return std::fprintf(stderr, "%s has no tiepoint and pixel scale\n", path), 1;
double lon = x;
double lat = y;
if (projected && !GTIFProj4ToLatLong(&defn, 1, &lon, &lat)) return std::fprintf(stderr, "PROJ could not unproject %s\n", labels[corner]), 1;
std::printf("%s %.3f %.3f = lon %.6f, lat %.6f\n", labels[corner], x, y, lon, lat);
}
return 0;
}
 
int main(int argc, char** argv) {
const std::string command = argc > 1 ? argv[1] : "";
if (command == "write" && argc == 7) return write(argv[2], std::atoi(argv[3]), std::atof(argv[4]), std::atof(argv[5]), std::atof(argv[6]));
if (command == "info" && argc == 3) return describe(argv[2]);
std::fprintf(stderr, "usage: geotiff-tool write <file.tif> <epsg> <x> <y> <pixel size>\n geotiff-tool info <file.tif>\n");
return 2;
}
shell
npx crossbind build -p wasi -b release
wasmtime run --dir=. dist/geotiff-tool-wasi-wasm32-st-release.wasm write utm33.tif 32633 500000 4650000 30
wasmtime run --dir=. --dir=dist/data::/data --env PROJ_DATA=/data/proj dist/geotiff-tool-wasi-wasm32-st-release.wasm info utm33.tif
output
libgeotiff 1.7.4 write: utm33.tif, 100 x 100 pixels in EPSG:32633, 10262 B
utm33.tif: 100 x 100 pixels, 1 band, 8 bits per sample
EPSG:32633 WGS 84 / UTM zone 33N, datum World Geodetic System 1984
upper left 500000.000 4650000.000 = lon 15.000000, lat 42.002015
lower right 503000.000 4647000.000 = lon 15.036210, lat 41.974990

Command-line tools

3 upstream commands ship as npm executables built for wasm32-wasip3. They need wasmtime on PATH and no compiler - see WASI commands.

shell
npm install --global @crossbind/port-geotiff-bin-wasi@beta
applygeo-wasi --help
  • applygeo-wasi
  • geotifcp-wasi
  • listgeo-wasi

What is different on WASI

  • There are no JavaScript bindings: src/native provides main(int, char**), and the build is a single .wasm.
  • Files come from the host through --dir preopens; --dir=. gives the program the current directory.
  • crossbind build -p wasi -b release writes the program to .crossbind/build/<name>-wasi-wasm32-st-release.wasm.
  • WASI 0.3's wasi:cli/exit carries success or failure only, so any non-zero return from main reaches the shell as exit code 1.
  • The build needs wasi-sdk 34 or newer (WASI_SDK_PATH) or the crossbind Docker image; running needs wasmtime 47 or newer. See WASI.

Other platforms

Facts on this page come from the port manifests in the repository and from what npm served on beta when the site was built. See the Libraries guide for the full consumer flow.

MORE LIBRARIES
cURLExpatGDALGEOSiconvLERClibjpeg-turbolibTIFFOpenSSLPROJSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close