crossbind
GitHub

GeoTIFF

v1.7.4Geospatial

GeoTIFF 1.7.4, geoTIFF metadata and coordinate-system support, packaged by crossbind as @crossbind/port-geotiff and one package per target. Only a variant that is actually on npm beta is listed as published.

npm install @crossbind/port-geotiff-wasm@beta
LIVE · 3 APPS · RUNS IN THIS TAB

libgeotiff in your browser: where a GeoTIFF is, and GeoTIFFs that GDAL places

libgeotiff is the GeoTIFF library GDAL's GTiff driver is built on. These apps run libgeotiff 1.7.4 with libtiff and PROJ, compiled by crossbind: they name any GeoTIFF's coordinate system and draw its footprint on a map, turn a picture and a bounding box into a compressed GeoTIFF that GDAL places where you said, and read heights and degrees off an elevation model. Most of the first run's download is PROJ's data, 10.2 MB, chiefly the EPSG database proj.db, which libgeotiff reads to name coordinate systems. The first run downloads 16.2 MB of WebAssembly once; every app on this page shares it, and nothing is uploaded.

APP 01

Where is this GeoTIFF?

Drop a satellite scene, an elevation tile or a drone map and see its coordinate system by name, its corners in degrees and its footprint on the map, with listgeo's full report. geotiff.js, the most used JavaScript GeoTIFF reader, returns the raw GeoKeys; libgeotiff names the system they describe through PROJ's EPSG database.

SAMPLES

The samples are written by the module: blank pixels, real placements. Your own file stays in this tab: it is mounted into the module's in-memory filesystem, and libtiff reads its directories and tags, not its pixels. Degrees are on the file's own datum, OSGB36 for the London sample. For drawing GeoTIFF pixels on a web map, geotiff.js is the lighter choice.

Open a sample or a GeoTIFF of your own to see where it is.
SHOW THE CODE
src/support/geotiff_app.h
// src/support/geotiff_app.h (excerpt)
Tiff tif = openTiff(path, "r"); // XTIFFOpen: libtiff with the GeoTIFF tags
Keys keys = openKeys(tif.get()); // GTIFNewEx reads the GeoKeys
GTIFDefn defn;
const bool defined = GTIFGetDefn(keys.get(), &defn) != 0; // EPSG code -> full definition
GTIFGetPCSInfo(defn.PCS, &name, nullptr, nullptr, nullptr); // its name, from proj.db
 
double x = columns[i]; // a corner, in pixels
double y = rows[i];
if (!GTIFImageToPCS(keys, &x, &y)) return "null"; // to map coordinates
return GTIFProj4ToLatLong(&defn, count, x, y) != 0; // to degrees, on the file's datum
 
GTIFPrint(keys, appendText, &text); // the report listgeo prints
GTIFPrintDefnEx(keys, defn, stream);
main.js
const m = await initNative();
const [path] = await m.autoMountFiles([file], await m.getRandomPath('/memfs')); // from <input type=file>
const info = JSON.parse(await m.GeoTiffInspector.inspect(path));
 
// For the Istanbul sample:
// info.epsg: 32635, info.name: 'WGS 84 / UTM zone 35N'
// info.corners[0].lonLat: [28.198970, 41.545594], the upper-left corner in degrees
// info.outline: 64 points around the footprint; info.listgeo: listgeo's report
APP 02

Georeference any picture

Give a scanned map, a screenshot or a render its place on Earth: a longitude and latitude box, or a corner and a pixel size in any projected EPSG system. The module writes it as a GeoTIFF in five compressions that GDAL places where you said, and reads it back with libgeotiff to show it. geotiff.js's writer stores pixels uncompressed.

PICTURE

Written as EPSG:4326, WGS 84 longitude and latitude. The box is the picture's outer edge: GTRasterTypeGeoKey says each pixel covers an area.

Choose a picture, or keep the sample, then write the GeoTIFFs.
SHOW THE CODE
src/support/geotiff_app.h
// src/support/geotiff_app.h (excerpt)
bool projected = GTIFGetPCSInfo(epsg, &name, nullptr, nullptr, nullptr) != 0; // or GTIFGetGCSInfo
TIFFSetField(tif.get(), TIFFTAG_COMPRESSION, codec);
const double tiepoint[6] = {0, 0, 0, originX, originY, 0};
const double scale[3] = {pixelWidth, pixelHeight, 0};
TIFFSetField(tif.get(), TIFFTAG_GEOTIEPOINTS, 6, tiepoint);
TIFFSetField(tif.get(), TIFFTAG_GEOPIXELSCALE, 3, scale);
 
GTIFKeySet(keys.get(), GTModelTypeGeoKey, TYPE_SHORT, 1, projected ? ModelTypeProjected : ModelTypeGeographic);
GTIFKeySet(keys.get(), GTRasterTypeGeoKey, TYPE_SHORT, 1, RasterPixelIsArea);
GTIFKeySet(keys.get(), ProjectedCSTypeGeoKey, TYPE_SHORT, 1, epsg); // GeographicTypeGeoKey for 4326
if (!GTIFWriteKeys(keys.get())) fail("libgeotiff could not write the GeoKeys");
main.js
const m = await initNative();
const dir = await m.getRandomPath('/memfs');
await m.FS.writeFile(`${dir}/picture.rgba`, rgba); // RGBA pixels, as a canvas gives them
const [west, south, east, north] = [28.6, 40.8, 29.4, 41.3];
const written = JSON.parse(await m.Georeferencer.write(
`${dir}/picture.rgba`, width, height, 4326, west, north,
(east - west) / width, (north - south) / height, 'deflate', 85, `${dir}/picture.tif`));
// written.bytes: 101434 for the 384 x 256 sample, 295368 uncompressed
const tif = await m.getFileBytes(`${dir}/picture.tif`);
APP 03

Read heights and degrees off an elevation model

Open an elevation GeoTIFF and move over its relief: libgeotiff turns the position under the cursor into longitude and latitude, next to the height stored there. Then save the model again, losslessly or with LERC within an error you choose, and compare the sizes. A cloud-optimized file is read from the smallest overview that still fills the view.

The sample is written by the module: generated heights on 512 × 512 cells of 30 m, placed in UTM zone 33N. Your own file stays in this tab. Band 1 is read, integers or floats, and GDAL's no-data value is left out. A file larger than 512 pixels a side is sampled to fit, nearest pixel, and the heights shown are those samples.

Open the sample, or an elevation GeoTIFF of your own, then move over it.
SHOW THE CODE
src/support/dem_file.h
// src/support/dem_file.h (excerpt)
// The smallest overview that still fills the grid, then band 1 block by block
TIFFGetField(tif.get(), TIFFTAG_SUBFILETYPE, &type);
if (!(type & FILETYPE_REDUCEDIMAGE) || (type & FILETYPE_MASK)) continue;
TIFFReadEncodedTile(tif, TIFFComputeTile(tif, left, top, 0, 0), buffer.data(), size);
 
// The position under the cursor, in full-resolution pixels, to degrees
if (!GTIFImageToPCS(keys.get(), &x, &y)) throw std::runtime_error("no tiepoint and pixel scale to place the pixels");
const bool degrees = toLonLat(defn, 1, &lon, &lat);
 
// Saved again as a float GeoTIFF in 256 x 256 tiles
TIFFSetField(tif.get(), TIFFTAG_COMPRESSION, codec.compression);
if (codec.compression == COMPRESSION_LERC) TIFFSetField(tif.get(), TIFFTAG_LERC_MAXZERROR, codec.maxZError);
main.js
const m = await initNative();
const dir = await m.getRandomPath('/memfs');
await m.DemProbe.writeSample(`${dir}/dem.tif`); // or a DEM mounted with autoMountFiles
const probe = await new m.DemProbe(`${dir}/dem.tif`, 512);
const info = JSON.parse(await probe.info()); // size, type, range, no-data value, EPSG
await probe.writeGrid(`${dir}/grid.f32`); // at most 512 x 512 heights, row by row
const heights = new Float32Array((await m.getFileBytes(`${dir}/grid.f32`)).slice().buffer);
const spot = JSON.parse(await probe.at(100.5, 200.5)); // the centre of pixel 100, 200
// spot.lonLat: [13.854264880, 47.073645579]; heights[200 * 512 + 100]: 734.90625
const saved = JSON.parse(await probe.encode('lerc', 0.1, `${dir}/dem-lerc.tif`));
// saved.bytes: 283298 of 1048576 raw; saved.maxError: 0.100006103515625

Usage

The calls most GeoTIFF code makes, each a small C++ header crossbind binds and the JavaScript that uses it. Every example runs here in WebAssembly and prints what the site build checked; the same headers and calls work on Android and iOS.

Each example also has a JavaScript only tab: the same task with no C++ file, calling GeoTIFF's own headers from @crossbind/port-geotiff directly. 4 of 5 work that way; the other says what stops it.

Find where a GeoTIFF is

The question most GeoTIFF code answers: GTIFNew reads the GeoKeys, GTIFGetDefn turns them into a coordinate system, GTIFImageToPCS places pixels on the map and GTIFProj4ToLatLong turns map coordinates into degrees.

src/native/geotiff_locator.h
#pragma once
 
#include <geo_normalize.h>
#include <geotiff.h>
#include <geovalues.h>
#include <tiffio.hxx>
#include <xtiffio.h>
 
#include <cstdint>
#include <cstdio>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
 
// Where is a GeoTIFF? Reads the coordinate system from its GeoKeys, puts two corners in map
// coordinates and turns them into longitude and latitude. Files travel as bytes, one per character,
// and libtiff's C++ stream API reads and writes them in memory.
class GeoTiffLocator {
public:
static std::string version() { return LIBGEOTIFF_STRING_VERSION; }
 
// A blank 8-bit image in the projected system `epsg`, its upper-left corner at (x, y), with
// square pixels `pixelSize` map units wide.
static std::u16string write(int epsg, int width, int height, double x, double y, double pixelSize) {
std::ostringstream out;
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("sample.tif", &out), XTIFFClose);
if (!tif) throw std::runtime_error("libtiff could not start a file");
TIFFSetField(tif.get(), TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif.get(), TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif.get(), TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(tif.get(), TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tif.get(), TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(tif.get(), TIFFTAG_ROWSPERSTRIP, height);
const double tiepoint[6] = {0, 0, 0, x, y, 0}; // pixel (0, 0) sits at map (x, y)
const double scale[3] = {pixelSize, pixelSize, 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::string row(width, '\0');
for (int y = 0; y < height; y += 1) {
if (TIFFWriteScanline(tif.get(), row.data(), y, 0) < 0) throw std::runtime_error("libtiff could not write a row");
}
tif.reset();
const std::string bytes = out.str();
return std::u16string(bytes.begin(), bytes.end());
}
 
// JSON: the EPSG code and name, the size in pixels, and the upper-left and lower-right corners
// in map units and in degrees.
static std::string locate(const std::u16string& bytes) {
std::istringstream in(std::string(bytes.begin(), bytes.end()));
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("input.tif", &in), XTIFFClose);
if (!tif) throw std::runtime_error("not a TIFF file");
Keys keys(GTIFNew(tif.get()), GTIFFree);
GTIFDefn defn;
if (!keys || !GTIFGetDefn(keys.get(), &defn)) throw std::runtime_error("no coordinate system in the GeoKeys");
 
uint32_t width = 0;
uint32_t height = 0;
TIFFGetField(tif.get(), TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif.get(), TIFFTAG_IMAGELENGTH, &height);
double x[2] = {0, static_cast<double>(width)};
double y[2] = {0, static_cast<double>(height)};
for (int i = 0; i < 2; i += 1) {
if (!GTIFImageToPCS(keys.get(), &x[i], &y[i])) throw std::runtime_error("no tiepoint and pixel scale to place the pixels");
}
double lon[2] = {x[0], x[1]};
double lat[2] = {y[0], y[1]};
if (defn.Model == ModelTypeProjected && !GTIFProj4ToLatLong(&defn, 2, lon, lat)) throw std::runtime_error("PROJ could not unproject the corners");
 
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 = name ? name : "unnamed";
GTIFFreeMemory(name);
 
char json[640];
std::snprintf(json, sizeof json,
"{\"epsg\":%d,\"name\":\"%s\",\"width\":%u,\"height\":%u,\"upperLeft\":[%.17g,%.17g],\"lowerRight\":[%.17g,%.17g],"
"\"upperLeftLonLat\":[%.17g,%.17g],\"lowerRightLonLat\":[%.17g,%.17g]}",
projected ? defn.PCS : defn.GCS, crs.c_str(), width, height, x[0], y[0], x[1], y[1], lon[0], lat[0], lon[1], lat[1]);
return json;
}
 
private:
using Tiff = std::unique_ptr<TIFF, void (*)(TIFF*)>;
using Keys = std::unique_ptr<GTIF, void (*)(GTIF*)>;
};
main.js
import { initNative, GeoTiffLocator } from './native/geotiff_locator.h';
 
await initNative();
// 100 x 100 pixels of 30 m in WGS 84 / UTM zone 33N, the upper-left corner at 500000, 4650000
const tiff = await GeoTiffLocator.write(32633, 100, 100, 500000, 4650000, 30);
const where = JSON.parse(await GeoTiffLocator.locate(tiff));
console.log(`libgeotiff ${await GeoTiffLocator.version()}, ${tiff.length} B: EPSG:${where.epsg} ${where.name}, ${where.width} x ${where.height} pixels`);
const degrees = ([lon, lat]) => `${lon.toFixed(6)} E, ${lat.toFixed(6)} N`;
console.log(`upper left ${where.upperLeft.join(', ')} = ${degrees(where.upperLeftLonLat)}`);
console.log(`lower right ${where.lowerRight.join(', ')} = ${degrees(where.lowerRightLonLat)}`);
PRINTSfirst run downloads 16.2 MB
libgeotiff 1.7.4, 10262 B: EPSG:32633 WGS 84 / UTM zone 33N, 100 x 100 pixels
upper left 500000, 4650000 = 15.000000 E, 42.002015 N
lower right 503000, 4647000 = 15.036210 E, 41.974990 N

Write a compressed GeoTIFF

Georeferencing is two TIFF tags and a few GeoKeys: TIFFTAG_GEOTIEPOINTS and TIFFTAG_GEOPIXELSCALE place the pixels, GTIFKeySet and GTIFWriteKeys name the coordinate system. GTIFPrint then prints the result the way listgeo does.

src/native/geotiff_writer.h
#pragma once
 
#include <geotiff.h>
#include <geovalues.h>
#include <tiffio.hxx>
#include <xtiffio.h>
 
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
 
// Writes 8-bit grey pixels as a Deflate-compressed GeoTIFF in longitude and latitude (WGS 84), and
// prints any GeoTIFF's GeoKeys and georeferencing tags the way the listgeo tool does.
class GeoTiffWriter {
public:
// `pixels` holds width x height bytes, row by row from the top. The image covers west to east
// and south to north, in degrees.
static std::u16string write(const std::u16string& pixels, int width, int height, double west, double south, double east, double north,
const std::string& citation) {
if (width < 1 || height < 1 || pixels.size() != static_cast<size_t>(width) * height) throw std::invalid_argument("pixels must hold width * height bytes");
std::ostringstream out;
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("grey.tif", &out), XTIFFClose);
if (!tif) throw std::runtime_error("libtiff could not start a file");
TIFFSetField(tif.get(), TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif.get(), TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif.get(), TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(tif.get(), TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tif.get(), TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(tif.get(), TIFFTAG_COMPRESSION, COMPRESSION_ADOBE_DEFLATE);
TIFFSetField(tif.get(), TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
TIFFSetField(tif.get(), TIFFTAG_ROWSPERSTRIP, 16);
const double tiepoint[6] = {0, 0, 0, west, north, 0};
const double scale[3] = {(east - west) / width, (north - south) / height, 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, ModelTypeGeographic);
GTIFKeySet(keys.get(), GTRasterTypeGeoKey, TYPE_SHORT, 1, RasterPixelIsArea);
GTIFKeySet(keys.get(), GTCitationGeoKey, TYPE_ASCII, 0, citation.c_str());
GTIFKeySet(keys.get(), GeographicTypeGeoKey, TYPE_SHORT, 1, GCS_WGS_84);
GTIFKeySet(keys.get(), GeogAngularUnitsGeoKey, TYPE_SHORT, 1, Angular_Degree);
GTIFWriteKeys(keys.get());
keys.reset();
 
std::string row(width, '\0');
for (int y = 0; y < height; y += 1) {
for (int x = 0; x < width; x += 1) row[x] = static_cast<char>(pixels[static_cast<size_t>(y) * width + x]);
if (TIFFWriteScanline(tif.get(), row.data(), y, 0) < 0) throw std::runtime_error("libtiff could not write a row");
}
tif.reset();
const std::string bytes = out.str();
return std::u16string(bytes.begin(), bytes.end());
}
 
// GTIFPrint: the key and tag dump listgeo starts with, collected into a string.
static std::string print(const std::u16string& tiff) {
std::istringstream in(std::string(tiff.begin(), tiff.end()));
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("input.tif", &in), XTIFFClose);
if (!tif) throw std::runtime_error("not a TIFF file");
Keys keys(GTIFNew(tif.get()), GTIFFree);
if (!keys) throw std::runtime_error("libgeotiff could not read the GeoKeys");
std::string text;
GTIFPrint(keys.get(), append, &text);
return text;
}
 
private:
using Tiff = std::unique_ptr<TIFF, void (*)(TIFF*)>;
using Keys = std::unique_ptr<GTIF, void (*)(GTIF*)>;
 
static int append(char* message, void* text) {
static_cast<std::string*>(text)->append(message);
return 1;
}
};
main.js
import { initNative, GeoTiffWriter } from './native/geotiff_writer.h';
 
await initNative();
const [width, height] = [256, 128];
let seed = 11;
const noise = () => (seed = (seed * 48271) % 2147483647) % 24;
const relief = (x, y) => 60 + 2 * Math.abs((x % 64) - 32) + 2 * Math.abs((y % 48) - 24) + noise(); // ridges and noise
const pixels = Array.from({ length: width * height }, (_, i) => String.fromCharCode(relief(i % width, Math.floor(i / width)))).join('');
 
// 10 W to 30 E and 35 N to 60 N, in WGS 84 degrees
const tiff = await GeoTiffWriter.write(pixels, width, height, -10, 35, 30, 60, 'Europe, synthetic relief');
console.log(`${width} x ${height} pixels: ${pixels.length} B, ${tiff.length} B as a Deflate GeoTIFF`);
for (const line of (await GeoTiffWriter.print(tiff)).trimEnd().split('\n')) console.log(line.trimEnd());
PRINTSfirst run downloads 16.2 MB
256 x 128 pixels: 32768 B, 23246 B as a Deflate GeoTIFF
Geotiff_Information:
   Version: 1
   Key_Revision: 1.0
   Tagged_Information:
      ModelTiepointTag (2,3):
         0                 0                 0
         -10               60                0
      ModelPixelScaleTag (1,3):
         0.15625           0.1953125         0
      End_Of_Tags.
   Keyed_Information:
      GTModelTypeGeoKey (Short,1): ModelTypeGeographic
      GTRasterTypeGeoKey (Short,1): RasterPixelIsArea
      GTCitationGeoKey (Ascii,25): "Europe, synthetic relief"
      GeographicTypeGeoKey (Short,1): GCS_WGS_84
      GeogAngularUnitsGeoKey (Short,1): Angular_Degree
      End_Of_Keys.
   End_Of_Geotiff.

Read the GeoKeys, the tiepoint and the pixel scale

GTIFDirectoryInfo counts the keys, GTIFKeyInfo and GTIFKeyGet read each one with its type, and GTIFValueNameEx names its value. The tiepoint and the pixel scale are TIFF tags, read with TIFFGetField.

src/native/geokey_reader.h
#pragma once
 
#include <geotiff.h>
#include <tiffio.hxx>
#include <xtiffio.h>
 
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
 
// Reads a GeoTIFF's GeoKeys one at a time, as GDAL and most readers do: GTIFKeyInfo says whether a
// key is set and what type it has, GTIFKeyGet copies its value out. The tiepoint and the pixel
// scale are plain TIFF tags next to the keys.
class GeoKeyReader {
public:
// JSON: the key directory's version, every common key the file sets, and the two tags.
static std::string read(const std::u16string& tiff) {
std::istringstream in(std::string(tiff.begin(), tiff.end()));
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("input.tif", &in), XTIFFClose);
if (!tif) throw std::runtime_error("not a TIFF file");
Keys gtif(GTIFNew(tif.get()), GTIFFree);
if (!gtif) throw std::runtime_error("libgeotiff could not read the GeoKeys");
 
int versions[3] = {0, 0, 0}; // directory version, key revision, minor revision
int count = 0;
GTIFDirectoryInfo(gtif.get(), versions, &count);
char head[96];
std::snprintf(head, sizeof head, "{\"version\":%d,\"revision\":\"%d.%d\",\"count\":%d,\"keys\":[", versions[0], versions[1], versions[2], count);
std::string json = head;
const geokey_t common[] = {GTModelTypeGeoKey, GTRasterTypeGeoKey, GTCitationGeoKey, GeographicTypeGeoKey,
GeogCitationGeoKey, GeogAngularUnitsGeoKey, ProjectedCSTypeGeoKey, PCSCitationGeoKey,
ProjLinearUnitsGeoKey, VerticalCSTypeGeoKey, VerticalUnitsGeoKey};
for (const geokey_t key : common) {
const std::string entry = describe(gtif.get(), key);
if (entry.empty()) continue;
if (json.back() != '[') json += ',';
json += entry;
}
json += "],\"tiepoint\":" + doubles(tif.get(), TIFFTAG_GEOTIEPOINTS) + ",\"pixelScale\":" + doubles(tif.get(), TIFFTAG_GEOPIXELSCALE) + "}";
return json;
}
 
private:
using Tiff = std::unique_ptr<TIFF, void (*)(TIFF*)>;
using Keys = std::unique_ptr<GTIF, void (*)(GTIF*)>;
 
// One key as JSON, or an empty string when the file does not set it. A SHORT key also gets the
// name of its value, from libgeotiff's tables or PROJ's database.
static std::string describe(GTIF* gtif, geokey_t key) {
int size = 0;
tagtype_t type = TYPE_UNKNOWN;
const int count = GTIFKeyInfo(gtif, key, &size, &type);
if (count == 0) return "";
std::string entry = "{\"key\":\"" + std::string(GTIFKeyName(key)) + "\",\"type\":\"" + GTIFTypeName(type) + "\",\"value\":";
if (type == TYPE_ASCII) {
std::string text(count + 1, '\0');
GTIFKeyGetASCII(gtif, key, text.data(), count + 1);
text.resize(std::strlen(text.c_str()));
entry += quoted(text);
} else if (type == TYPE_SHORT) {
unsigned short value = 0;
GTIFKeyGetSHORT(gtif, key, &value, 0, 1);
entry += std::to_string(value) + ",\"name\":" + quoted(GTIFValueNameEx(gtif, key, value));
} else {
double value = 0;
GTIFKeyGetDOUBLE(gtif, key, &value, 0, 1);
entry += number(value);
}
return entry + "}";
}
 
// A TIFF tag of doubles as a JSON array; libgeotiff registers the GeoTIFF tags with a count.
static std::string doubles(TIFF* tif, uint32_t tag) {
uint16_t count = 0;
double* values = nullptr;
if (!TIFFGetField(tif, tag, &count, &values)) return "null";
std::string json = "[";
for (uint16_t i = 0; i < count; i += 1) json += (i ? "," : "") + number(values[i]);
return json + "]";
}
 
static std::string number(double value) {
char text[32];
std::snprintf(text, sizeof text, "%.17g", value);
return text;
}
 
static std::string quoted(const std::string& text) {
std::string out = "\"";
for (const char c : text) {
if (c == '"' || c == '\\') out += '\\';
out += static_cast<unsigned char>(c) < 0x20 ? ' ' : c;
}
return out + "\"";
}
};
main.js
import { initNative, GeoKeyReader } from './native/geokey_reader.h';
import { GeoTiffWriter } from './native/geotiff_writer.h';
 
await initNative();
const pixels = String.fromCharCode(...Array.from({ length: 64 * 32 }, (_, i) => (i * 7) % 256));
const tiff = await GeoTiffWriter.write(pixels, 64, 32, -10, 35, 30, 60, 'Europe'); // the writer from the previous example
 
const read = JSON.parse(await GeoKeyReader.read(tiff));
console.log(`GeoTIFF ${read.version}, key revision ${read.revision}, ${read.count} keys`);
for (const key of read.keys) console.log(`${key.key} (${key.type}): ${JSON.stringify(key.value)}${key.name ? ` = ${key.name}` : ''}`);
const [column, row, , x, y] = read.tiepoint;
console.log(`tiepoint: pixel ${column}, ${row} is at ${x}, ${y}; pixel scale: ${read.pixelScale[0]} x ${read.pixelScale[1]}`);
PRINTSfirst run downloads 16.2 MB
GeoTIFF 1, key revision 1.0, 5 keys
GTModelTypeGeoKey (Short): 2 = ModelTypeGeographic
GTRasterTypeGeoKey (Short): 1 = RasterPixelIsArea
GTCitationGeoKey (Ascii): "Europe"
GeographicTypeGeoKey (Short): 4326 = GCS_WGS_84
GeogAngularUnitsGeoKey (Short): 9102 = Angular_Degree
tiepoint: pixel 0, 0 is at -10, 60; pixel scale: 0.625 x 0.78125

Expand an EPSG code into a full definition

GTIFGetDefn normalises the GeoKeys through PROJ's EPSG database: from ProjectedCSTypeGeoKey alone it recovers the projection method and parameters, the datum, the ellipsoid and the unit. GTIFGetProj4Defn writes it as a PROJ string, with the scale factor rounded to six decimals.

src/native/crs_definition.h
#pragma once
 
#include <geo_normalize.h>
#include <geotiff.h>
#include <geovalues.h>
#include <tiffio.hxx>
#include <xtiffio.h>
 
#include <cstdio>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
 
// A file often carries a single EPSG code. GTIFGetDefn expands it through PROJ's database into the
// whole definition: projection method and parameters, datum, ellipsoid, prime meridian and unit.
class CrsDefinition {
public:
// JSON: the normalised definition, with EPSG names, and the PROJ string libgeotiff builds from it.
static std::string describe(const std::u16string& tiff) {
std::istringstream in(std::string(tiff.begin(), tiff.end()));
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("input.tif", &in), XTIFFClose);
if (!tif) throw std::runtime_error("not a TIFF file");
Keys gtif(GTIFNew(tif.get()), GTIFFree);
GTIFDefn defn;
if (!gtif || !GTIFGetDefn(gtif.get(), &defn)) throw std::runtime_error("no coordinate system in the GeoKeys");
 
std::string json = "{\"model\":" + quoted(GTIFValueNameEx(gtif.get(), GTModelTypeGeoKey, defn.Model));
if (defn.Model == ModelTypeProjected) {
json += ",\"pcs\":" + code(defn.PCS, pcsName(defn.PCS));
json += ",\"projection\":" + code(defn.ProjCode, projectionName(defn.ProjCode));
json += ",\"method\":" + quoted(GTIFValueNameEx(gtif.get(), ProjCoordTransGeoKey, defn.CTProjection));
json += ",\"parameters\":[";
for (int i = 0; i < defn.nParms; i += 1) {
if (defn.ProjParmId[i] == 0) continue; // a slot the method does not use
if (json.back() != '[') json += ',';
json += "[" + quoted(GTIFKeyName(static_cast<geokey_t>(defn.ProjParmId[i]))) + "," + number(defn.ProjParm[i]) + "]";
}
json += "]";
}
char* name = nullptr;
GTIFGetGCSInfo(defn.GCS, &name, nullptr, nullptr, nullptr);
json += ",\"gcs\":" + code(defn.GCS, taken(name));
GTIFGetDatumInfo(defn.Datum, &name, nullptr);
json += ",\"datum\":" + code(defn.Datum, taken(name));
GTIFGetEllipsoidInfo(defn.Ellipsoid, &name, nullptr, nullptr);
json += ",\"ellipsoid\":" + code(defn.Ellipsoid, taken(name)) + ",\"axes\":[" + number(defn.SemiMajor) + "," + number(defn.SemiMinor) + "]";
GTIFGetPMInfo(defn.PM, &name, nullptr);
json += ",\"primeMeridian\":" + code(defn.PM, taken(name));
GTIFGetUOMLengthInfo(defn.UOMLength, &name, nullptr);
json += ",\"unit\":" + code(defn.UOMLength, taken(name)) + ",\"metres\":" + number(defn.UOMLengthInMeters);
char* proj = GTIFGetProj4Defn(&defn);
json += ",\"proj\":" + quoted(taken(proj)) + "}";
return json;
}
 
private:
using Tiff = std::unique_ptr<TIFF, void (*)(TIFF*)>;
using Keys = std::unique_ptr<GTIF, void (*)(GTIF*)>;
 
static std::string pcsName(int pcs) {
char* name = nullptr;
GTIFGetPCSInfo(pcs, &name, nullptr, nullptr, nullptr);
return taken(name);
}
 
static std::string projectionName(int projection) {
char* name = nullptr;
GTIFGetProjTRFInfo(projection, &name, nullptr, nullptr);
return taken(name);
}
 
// The GTIFGet...Info functions hand over strings the caller frees.
static std::string taken(char*& text) {
const std::string value = text ? text : "";
GTIFFreeMemory(text);
text = nullptr;
return value;
}
 
static std::string code(int value, const std::string& name) { return "[" + std::to_string(value) + "," + quoted(name) + "]"; }
 
static std::string number(double value) {
char text[32];
std::snprintf(text, sizeof text, "%.17g", value);
return text;
}
 
static std::string quoted(const std::string& text) {
std::string out = "\"";
for (const char c : text) {
if (c == '"' || c == '\\') out += '\\';
out += static_cast<unsigned char>(c) < 0x20 ? ' ' : c;
}
return out + "\"";
}
};
main.js
import { initNative, CrsDefinition } from './native/crs_definition.h';
import { GeoTiffLocator } from './native/geotiff_locator.h';
 
await initNative();
// A file that only says ProjectedCSTypeGeoKey = 27700, written by the first example's class
const tiff = await GeoTiffLocator.write(27700, 100, 100, 529000, 181000, 10);
const crs = JSON.parse(await CrsDefinition.describe(tiff));
console.log(`${crs.model} EPSG:${crs.pcs[0]} ${crs.pcs[1]}`);
console.log(`projection ${crs.projection[0]} ${crs.projection[1]}, ${crs.method}`);
for (const [name, value] of crs.parameters) console.log(` ${name} ${value}`);
console.log(`geographic ${crs.gcs.join(' ')}, datum ${crs.datum.join(' ')}`);
console.log(`ellipsoid ${crs.ellipsoid.join(' ')}: ${crs.axes.map((axis) => axis.toFixed(3)).join(' m, ')} m`);
console.log(`prime meridian ${crs.primeMeridian.join(' ')}, unit ${crs.unit.join(' ')} (${crs.metres} m)`);
console.log(crs.proj.trim());
PRINTSfirst run downloads 16.2 MB
ModelTypeProjected EPSG:27700 OSGB36 / British National Grid
projection 19916 British National Grid, CT_TransverseMercator
  ProjNatOriginLatGeoKey 49
  ProjNatOriginLongGeoKey -2
  ProjScaleAtNatOriginGeoKey 0.9996012717
  ProjFalseEastingGeoKey 400000
  ProjFalseNorthingGeoKey -100000
geographic 4277 OSGB36, datum 6277 Ordnance Survey of Great Britain 1936
ellipsoid 7001 Airy 1830: 6377563.396 m, 6356256.909 m
prime meridian 8901 Greenwich, unit 9001 metre (1 m)
+proj=tmerc +lat_0=49.000000000 +lon_0=-2.000000000 +k=0.999601 +x_0=400000.000 +y_0=-100000.000 +a=6377563.396 +b=6356256.909 +units=m

Convert between pixels and longitude, latitude

GTIFProj4FromLatLong and GTIFPCSToImage find the pixel under a longitude and latitude; GTIFImageToPCS and GTIFProj4ToLatLong go back from a pixel. The class keeps the file open between questions.

src/native/pixel_position.h
#pragma once
 
#include <geo_normalize.h>
#include <geotiff.h>
#include <geovalues.h>
#include <tiffio.hxx>
#include <xtiffio.h>
 
#include <cstdio>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
 
// Opens a GeoTIFF once and converts positions both ways: pixel to map coordinates to longitude and
// latitude, and back. Pixel (0, 0) is the upper-left corner of the first pixel, so the centre of
// pixel (column, row) is (column + 0.5, row + 0.5).
class PixelPosition {
public:
explicit PixelPosition(const std::u16string& tiff) : in(std::string(tiff.begin(), tiff.end())), tif(open(in)), keys(GTIFNew(tif.get()), GTIFFree) {
if (!keys || !GTIFGetDefn(keys.get(), &defn)) throw std::runtime_error("no coordinate system in the GeoKeys");
}
 
// JSON {"map": [x, y], "lonLat": [lon, lat]} for a position in pixels.
std::string lonLat(double column, double row) {
double x = column;
double y = row;
if (!GTIFImageToPCS(keys.get(), &x, &y)) throw std::runtime_error("no tiepoint and pixel scale to place the pixels");
double lon = x;
double lat = y;
if (defn.Model == ModelTypeProjected && !GTIFProj4ToLatLong(&defn, 1, &lon, &lat)) throw std::runtime_error("PROJ could not unproject the point");
return json("lonLat", x, y, lon, lat);
}
 
// JSON {"map": [x, y], "pixel": [column, row]} for a longitude and latitude.
std::string pixel(double lon, double lat) {
double x = lon;
double y = lat;
if (defn.Model == ModelTypeProjected && !GTIFProj4FromLatLong(&defn, 1, &x, &y)) throw std::runtime_error("PROJ could not project the point");
double column = x;
double row = y;
if (!GTIFPCSToImage(keys.get(), &column, &row)) throw std::runtime_error("no tiepoint and pixel scale to place the pixels");
return json("pixel", x, y, column, row);
}
 
private:
using Tiff = std::unique_ptr<TIFF, void (*)(TIFF*)>;
using Keys = std::unique_ptr<GTIF, void (*)(GTIF*)>;
 
static Tiff open(std::istringstream& in) {
XTIFFInitialize();
Tiff tif(TIFFStreamOpen("input.tif", &in), XTIFFClose);
if (!tif) throw std::runtime_error("not a TIFF file");
return tif;
}
 
static std::string json(const char* name, double x, double y, double a, double b) {
char text[160];
std::snprintf(text, sizeof text, "{\"map\":[%.17g,%.17g],\"%s\":[%.17g,%.17g]}", x, y, name, a, b);
return text;
}
 
std::istringstream in; // libtiff reads from it for as long as the file is open
Tiff tif;
Keys keys;
GTIFDefn defn;
};
main.js
import { initNative, PixelPosition } from './native/pixel_position.h';
import { GeoTiffLocator } from './native/geotiff_locator.h';
 
await initNative();
// 300 x 300 pixels of 100 m over Istanbul in WGS 84 / UTM zone 35N, written by the first example's class
const tiff = await GeoTiffLocator.write(32635, 300, 300, 650000, 4560000, 100);
const position = await new PixelPosition(tiff);
 
const tower = JSON.parse(await position.pixel(28.974167, 41.025833)); // Galata Tower
console.log(`Galata Tower: ${tower.map.map((v) => v.toFixed(2)).join(', ')} m, pixel ${tower.pixel.map((v) => v.toFixed(3)).join(', ')}`);
const [column, row] = tower.pixel.map(Math.floor);
const centre = JSON.parse(await position.lonLat(column + 0.5, row + 0.5));
console.log(`centre of pixel ${column}, ${row}: ${centre.map.join(', ')} m, ${centre.lonLat.map((v) => v.toFixed(6)).join(', ')}`);
PRINTSfirst run downloads 16.2 MB
Galata Tower: 665970.23, 4543502.03 m, pixel 159.702, 164.980
centre of pixel 159, 164: 665950, 4543550 m, 28.973939, 41.026269

Add it to your project

One package per platform: install the ones you build for and list each in crossbind.config.js; crossbind compiles only the one that matches the build target. Your C++ goes in src/native, next to the headers it binds. Libraries explains the whole flow.

shell
npm install @crossbind/port-geotiff-wasm@beta
crossbind.config.js
import geotiffWasm from '@crossbind/port-geotiff-wasm/crossbind.config.js';
 
export default {
dependencies: [geotiffWasm],
paths: { config: import.meta.url },
};

Platforms

PlatformRuns inBuildsPage
WebAssemblybrowsers, Node.js and edge runtimeswasm32, single-threaded and multi-threadedGeoTIFF for WebAssembly
AndroidReact Native apps on Androidarm64-v8a devices and the x86_64 emulatorGeoTIFF for Android
iOSReact Native apps on iOSarm64 devices and simulatorsGeoTIFF for iOS
macOSnative Node.js addons and Electron on macOSarm64 and x64, macOS 11 or laterGeoTIFF for macOS
Linuxnative Node.js addons on Linuxx64 and arm64, glibc 2.28 or laterGeoTIFF for Linux
Windowsnative Node.js addons on Windowsx64 and arm64, Windows 10 or laterGeoTIFF for Windows
WASIcommand-line programs under wasmtimewasm32-wasip3, single-threadedGeoTIFF for WASI

Packages

TargetPackagenpm `beta`
Meta package@crossbind/port-geotiff2.0.0-beta.62
Web and Node.js@crossbind/port-geotiff-wasm2.0.0-beta.62
WASI library@crossbind/port-geotiff-wasi2.0.0-beta.62
WASI commands@crossbind/port-geotiff-standalone-wasinot published
Android@crossbind/port-geotiff-android2.0.0-beta.62
iOS@crossbind/port-geotiff-ios2.0.0-beta.62
macOS@crossbind/port-geotiff-darwin2.0.0-beta.62
Linux@crossbind/port-geotiff-linux2.0.0-beta.62
Linux (musl)@crossbind/port-geotiff-linuxmuslnot published
Windows@crossbind/port-geotiff-win322.0.0-beta.62

Licence

  • npm license field of @crossbind/port-geotiff: MIT.
  • The licence files that ship with the package, and the port recipe, are in the port directory.

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