# geotiff.js: reading GeoTIFF rasters in a browser without a server

> geotiff.js is a pure JavaScript library that parses GeoTIFF headers and decodes raster data, with support for tiled and stripped images, several compressions and a configurable worker pool. It suits teams that want to visualise geospatial rasters client side, and it deliberately stops short of offering a higher level API like GDAL.

**geotiffjs/geotiff.js** — geotiff.js is a small library to parse TIFF files for visualization or analysis. It is written in pure JavaScript, and is usable in both the browser and node.js applications.

- Repository: https://github.com/geotiffjs/geotiff.js
- Website: https://geotiffjs.github.io/
- Stars: 1,056 · Forks: 217
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/geotiffjs-geotiff-js

## What the library does and deliberately does not do

geotiff.js is described as a small library to parse TIFF files for visualization or analysis, written in pure JavaScript and usable in both the browser and node.js applications. The first line of the project description is the scope: read geospatial metadata and raw array data from a wide variety of different GeoTIFF file types.

The important sentence comes later, and it is a design statement rather than a caveat. Geotiff gives you access to all GeoTIFF metadata, but does not offer any one specific higher level API, such as GDAL, for things like transforms or data extraction. However, you can write your own higher level API using this library, given your specific dataset needs.

So this is a decoder, not a geospatial platform. There is no reprojection, no warping, no vector layer, no tiling scheme generation, no overviews pyramid builder. You get the bytes and the tags, and you assemble the analysis yourself.

That is a defensible boundary. A library that tried to be GDAL in JavaScript would carry a lot of opinion about what a map is. geotiff.js stays at the level where the format is the complexity, and leaves the domain decisions to the application.

The repository layout is consistent with a focused library. There is a `src/` directory, a `test/` directory, two rollup configuration files, `rollup.config.js` and `rollup.worker.config.js`, two tsconfig files, a `typedoc.json` for API documentation generation, and a `COMMUNITY.md` at the top level. The separate worker rollup config is the first hint that parallel decoding is a first class concern rather than an afterthought.

## Coverage across data types and compressions

The feature list is long enough to be worth reading in full, because the gaps in a codec are what break a project later.

On the parsing side, TIFFs can come from remote sources via `fetch` or XHR, from a local `ArrayBuffer`, and from the filesystem, using `FileReader` in browsers and the filesystem functions in node. The headers of all possible TIFF files are parsed, and there is rudimentary extraction of geospatial metadata.

Raster reading covers stripped images, tiled images, band interleaved images and pixel interleaved images. Those are the four storage layouts in the TIFF specification, and supporting all four is what lets the library open files produced by different software.

The data types are (U)Int8, 16 and 32, UInt1 to 31 with some drawbacks as the documentation puts it, and Float16, 32 and 64. The Float16 support is notable, because half precision is common in elevation and bathymetry data and it is awkward to handle without a dedicated library. The dependency list shows `@petamoriken/float16` for exactly that.

The enabled compressions are no compression, Packbits, LZW, Deflate with floating point or horizontal predictor support, JPEG, LERC with additional Deflate compression support, Zstandard, and image formats supported via the browser, such as WebP. The dependencies map onto that list, with `lerc` for LERC, `pako` for Deflate, `zstddec` for Zstandard and `parse-headers` for the header parsing.

On top of the format handling there are quality of life features: automatic selection of overview level to read from, subsetting via an image window or bounding box and selected bands, reading of samples into separate arrays or a single pixel-interleaved array, a configurable tile and strip cache, a configurable pool of workers to increase decoding efficiency, and utility functions for geospatial parameters such as bounding box, origin and resolution.

The two entries with the most caveats are the ones to note. Limited bigTIFF support means files over the classic 4GB limit are only partly handled, and the UInt1 to 31 range comes with stated drawbacks.

## Reading an elevation value, step by step

The example in the documentation resolves GPS coordinates to elevation in a GeoTIFF that encodes WGS-84 compliant geo data, and it is worth reading closely because it shows what the library asks of you.

The imports are from the package entry point:

```js
import { fromUrl, fromArrayBuffer, fromBlob  } from "geotiff";
```

Loading the file and the first image is two lines, and the comment notes that by default the first image is read:

```js
const tiff = await fromArrayBuffer(...);
const image = await tiff.getImage();
```

The georeferencing then comes out of the file directory. Two tags matter, `ModelPixelScale` and `ModelTiepoint`, and the code has to negate the y component because WGS-84 tiles have a flipped y:

```js
const s = image.fileDirectory.getValue('ModelPixelScale');
const t = image.fileDirectory.getValue('ModelTiepoint');
let [sx, sy, sz] = s;
let [px, py, k, gx, gy, gz] = t;
sy = -sy; // WGS-84 tiles have a "flipped" y component
```

From there the code builds the two affine matrices by hand, one for pixel to GPS and one for GPS to pixel, and then locates a pixel by interpolating within the bounding box:

```js
const [gx1, gy1, gx2, gy2] = image.getBoundingBox();
const lat = lerp(gy1, gy2, Math.random());
const long = lerp(gx1, gx2, Math.random());
```

The final step reads the rasters and indexes the value:

```js
const rasters = await image.readRasters();
const { width, [0]: raster } = rasters;
const elevation = raster[x + y * width];
```

Two things stand out. The transform matrix arithmetic is written by the application, not provided by the library, which is the higher level API the documentation says you have to build yourself. And the `sy = -sy` line is the kind of detail that a codec cannot abstract away, because the sign convention is a property of the coordinate system rather than of the file format.

The library is also the basis for larger examples, and the documentation points at a slice view using Cesium.js from the TAMP project and at contour generation using d3-contour, which is the natural pairing since the raster comes from here and the rendering comes from d3.

## Version 3 broke the API to make tag reading lazy

The migration guide from version 2 to version 3 is the most useful document in the repository for anyone with existing code, and the reason for the changes is performance.

Version 3.0 introduces significant performance improvements through deferred tag reading, but includes breaking changes to some APIs. The mechanism is that the `fileDirectory` object has been replaced with an `ImageFileDirectory` class that supports deferred loading, so a tag is only parsed when you actually ask for it.

For most users the required changes are small. The high-level APIs such as `getImage()` and `readRasters()` remain compatible. You need to add `await` to `getTiePoints()` and `getGDALMetadata()`, and change `image.geoKeys` into `image.getGeoKeys()`. Advanced users who access `fileDirectory` properties directly need to migrate to the new methods.

The synchronous access path is `getValue()`:

```javascript
const image = await tiff.getImage();
const s = image.fileDirectory.getValue('ModelPixelScale');
const t = image.fileDirectory.getValue('ModelTiepoint');
const width = image.fileDirectory.getValue('ImageWidth');
const compression = image.fileDirectory.getValue('Compression');
```

And there is a warning attached to it that is worth internalising, because it defines the boundary between the two access styles. `getValue()` throws an error if the tag is deferred.

So the design is deliberate. A synchronous getter gives you the value immediately if it has already been loaded, and refuses to block if it has not. To read a tag you have not touched before, you need the asynchronous path, which means awaiting. The consequence for a user is that a call which looks like a property access in version 2 can now be an await point, and code that read many tags in a loop may need restructuring rather than a one word change.

The release history shows this was a busy period. Version 3.0.4 was tagged on 2026-03-02, 3.0.5 on 2026-03-11 and a 3.1.0 beta on 2026-03-30, and the manifest already carries version 3.1.0.

## One package, three builds, and a worker pool

The package is published as `geotiff`, and the manifest shows a distribution layout that is worth understanding because it explains how the same library serves a browser and a server.

The package is an ES module, and there are three built outputs. The `main` field points at `dist-node/geotiff.js` for CommonJS consumers, the `module` field points at `dist-module/geotiff.js`, and the `jsdelivr` and `unpkg` fields both point at `dist-browser/geotiff.js` for a script tag or a CDN. The `exports` map resolves the import condition to the module build, require to the node build, and browser to the browser build. The types are in `dist-module/geotiff.d.ts`.

So the same source is compiled three ways, and the resolution rules pick the right one. The published `files` list is `src`, `dist-module`, `dist-node` and `dist-browser`, which means the sources ship too, which is a nice touch for anyone who wants to read the implementation or patch it.

The Node requirement is `>=10.19`, which is old and generous, and the language is JavaScript rather than TypeScript, though the repository has a `tsconfig.json` and a `tsconfig.build.json`, so the sources are likely typed and compiled.

The worker support is the other part of the layout worth noting. The feature list mentions a configurable pool of workers to increase decoding efficiency, the `web-worker` dependency provides the worker shim, and there is a separate `rollup.worker.config.js` so the worker bundle is built independently of the main bundle. Decoding a compressed raster is CPU bound and embarrassingly parallel, and doing it on the main thread blocks scrolling, so the pool is what makes a large image usable in a browser.

The remaining dependencies are small and specific. `quick-lru` is the cache behind the configurable tile and strip cache, `xml-utils` is for the metadata in the GDAL metadata format, and `lerc`, `pako` and `zstddec` are the three decompressors that need real implementations.

## Where geotiff.js stops and a geospatial stack begins

The limits of this library are the limits of its scope, and the documentation is upfront about them.

The first limit is bigTIFF. The feature list says limited bigTIFF support, which means very large files, past the 4GB limit of the classic TIFF format, are only partly handled. If your dataset is a set of multi-gigabyte rasters, that is the first thing to verify.

The second limit is the missing higher level API, which is a limit by design but still a limit in practice. There is no reprojection, so if your browser needs to show the raster in Web Mercator and the file is in another projection, the transformation is yours to write. There is no tiling for display, so a large image for a web map needs an overview or tiling strategy of your own, although the automatic selection of overview level to read from helps when the file already carries overviews.

The third is the metadata quality. The feature list says rudimentary extraction of geospatial metadata, and the migration guide shows a `getGDALMetadata()` call, which is the mechanism for the GDAL specific metadata that many real files carry. The word rudimentary is doing work there, and anyone who needs full fidelity of the projection definition should check the tags directly rather than trusting a convenience getter.

The alternative approach is GDAL itself, either through a server or through a compiled WebAssembly build of it. The difference in approach is that GDAL offers the higher level API that geotiff.js deliberately omits, at the cost of a much larger dependency. A server-side GDAL does the work on a machine with disk and RAM and returns a rendered tile.

So the choice is between a small decoder you embed and a large engine you call. If the application is a map that needs a proper tile pyramid, GDAL or a service is the answer. If the application needs to read a handful of values out of a file the user dropped onto the page, geotiff.js is the lighter and more honest choice.

On maintenance, the last push was on 2026-09-13, the repository is not archived, the licence is MIT, and the documentation is generated with typedoc and published at geotiffjs.github.io. The feature list also mentions automated testing via PhantomJS, which is a detail from an earlier era of the project.

## Conclusion

geotiff.js fits a browser application that needs to display or sample raster data without a server round trip, since the decoding runs in JavaScript and in a worker pool, and since the compression and data type coverage is wide enough for real satellite and aerial imagery. Skip it if you want a ready made geospatial stack, because the library offers no higher level API for transforms or data extraction and you are expected to write that layer yourself. Before you upgrade from version 2, read the migration guide, because deferred tag reading in 3.0 changed `geoKeys` into a method and made `getTiePoints()` and `getGDALMetadata()` asynchronous. The last push was on 2026-09-13, with version 3.0.5 in March 2026 and a 3.1.0 beta in the same month.

## FAQ

### What does geotiff.js do and what does it leave out?

It reads geospatial metadata and raw raster arrays from GeoTIFF files in the browser and in node, and it deliberately offers no one specific higher level API for transforms or data extraction, so you write that layer yourself.

### Which TIFF compressions and data types does geotiff.js support?

Compressions include no compression, Packbits, LZW, Deflate with floating point or horizontal predictor support, JPEG, LERC with additional Deflate support, Zstandard, and browser supported image formats such as WebP. Data types cover (U)Int8, 16 and 32, UInt1 to 31 with some drawbacks, and Float16, 32 and 64.

### What changed in geotiff.js version 3?

Version 3.0 added deferred tag reading, which is a significant performance improvement, but broke some APIs. The `geoKeys` property became `getGeoKeys()`, `getTiePoints()` and `getGDALMetadata()` became async, and `fileDirectory` properties moved to `ImageFileDirectory` methods such as `getValue()`.

### How do I read raster values from a GeoTIFF in geotiff.js?

Load the file with `fromUrl`, `fromArrayBuffer` or `fromBlob`, call `await tiff.getImage()` for the first image, then `await image.readRasters()` and index the returned array by `x + y * width` for the pixel you want.

### Does geotiff.js work in node as well as the browser?

Yes. It is written in pure JavaScript and is usable in both browser and node applications, with files read from the filesystem using `FileReader` in browsers and the filesystem functions in node, and the package publishes separate node, module and browser builds.

## Sources

- [geotiffjs/geotiff.js on GitHub](https://github.com/geotiffjs/geotiff.js)
- [License: MIT](https://github.com/geotiffjs/geotiff.js/blob/master/LICENSE)
- [Project website](https://geotiffjs.github.io/)
- [README](https://github.com/geotiffjs/geotiff.js/blob/master/README.md)
- [Releases](https://github.com/geotiffjs/geotiff.js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/geotiffjs-geotiff-js
