crossbind
GitHub

GeoTIFF for Android

v1.7.4Android

GeoTIFF 1.7.4 for React Native apps on Android, precompiled for arm64-v8a devices and the x86_64 emulator as @crossbind/port-geotiff-android.

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

Install

shell
npm install @crossbind/plugin-react-native@beta @crossbind/plugin-react-native-ios-helper@beta @crossbind/port-geotiff-android@beta
npm install --save-dev @crossbind/plugin-metro@beta
crossbind.config.mjs
import geotiffAndroid from '@crossbind/port-geotiff-android/crossbind.config.js';
 
export default {
dependencies: [geotiffAndroid],
paths: { config: import.meta.url },
};
metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const CrossbindMetroPlugin = require('@crossbind/plugin-metro');
 
const defaultConfig = getDefaultConfig(__dirname);
 
const config = {
...CrossbindMetroPlugin(defaultConfig),
};
 
module.exports = mergeConfig(defaultConfig, config);

The whole flow, including Expo, is in the React Native playbook.

Usage

The examples the WebAssembly page runs, as Android compiles them: the same headers and the same calls. They are checked on the WebAssembly build.

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)}`);
PRINTS
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());
PRINTS
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]}`);
PRINTS
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());
PRINTS
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(', ')}`);
PRINTS
Galata Tower: 665970.23, 4543502.03 m, pixel 159.702, 164.980
centre of pixel 159, 164: 665950, 4543550 m, 28.973939, 41.026269

What is different on Android

  • The React Native plugin compiles your headers with the library inside Gradle's native build, so npm run android builds everything.
  • Named imports from ./native/<header>.h work as on the web: await initNative() once, then call the classes.
  • There is no m.FS and no /memfs: files live in the app's own storage, and your C++ takes their paths.
  • No Worker and no COOP or COEP: runtime: 'mt' uses pthreads directly.

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