@developmentseed/geotiff

中文文档
0.10.0发布于 2026/11/10MITTypeScriptES Module构建来源已验证近 30 天下载 2.3万

High-level GeoTIFF reading library.

原始使用文档
文档目录15 节
  1. Features
  2. Full TypeScript
  3. No unnecessary data copies
  4. Easy access to COG tiles
  5. Convenient access to overviews
  6. Read any pixel window
  7. Affine transformation handling
  8. Automatic Nodata Mask handling
  9. Easy CRS handling
  10. Configurable Web Worker pool for image decoding
  11. Dynamically load compressions as needed
  12. Bring your own source
  13. Nearly-identical API to Python async-geotiff
  14. Reading over HTTP
  15. Why not build on top of geotiff.js?

@developmentseed/geotiff

Fast, high-level GeoTIFF reader written in TypeScript for the browser, wrapping @cogeotiff/core.

  • Easy access to COG tiles and reduced-resolution overviews.
  • Automatic nodata mask handling.
  • Image decoding off the main thread.
  • Supported compressions:
    • Deflate, LERC, LERC+Deflate, LERC+ZSTD, LZW, JPEG (browser-only), WebP (browser-only), ZSTD
    • Support for user-defined decompression algorithms.

Features

Full TypeScript

The entire project — both public APIs and internal code — is strongly and exhaustively typed in TypeScript.

Image data fetches return a strongly typed object. With type narrowing we can infer whether data is pixel-interleaved or band-separate:

import { GeoTIFF } from "@developmentseed/geotiff";

const geotiff = await GeoTIFF.fromUrl(url);
const tile = await geotiff.fetchTile(10, 10);
const { array } = tile;
if ( array.layout === "pixel-interleaved" ) {
    // array is narrowed to type RasterArrayPixelInterleaved

    // TypedArray interleaved pixel data in a single array
    const data = array.data; // RasterTypedArray
} else if ( array.layout === "band-separate" ) {
    // array is narrowed to type RasterArrayBandSeparate

    // TypedArray data per raster band
    const bands = array.bands; // RasterTypedArray[]
}

No unnecessary data copies

Image fetching APIs always return data in their original layout, ensuring data copies only happen with the user's explicit consent.

Easy access to COG tiles

Use GeoTIFF.fetchTile to load Tile instances.

import { GeoTIFF } from "@developmentseed/geotiff";

// RGB Sentinel-2 image over NYC
const url = "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/18/T/WL/2026/1/S2B_18TWL_20260101_0_L2A/TCI.tif"
const geotiff = await GeoTIFF.fromUrl(url);

// Read full-resolution tile from bottom right of image.
// tile ordering starts at top left.
const tile = await geotiff.fetchTile(10, 10);
const {
    x, // 10
    y, // 10
} = tile;

// tile.array holds data & relevant metadata
const {
    count, // 3 (RGB)
    height, // 1024
    width, // 1024
    transform, // [ 10, 0, 602380, 0, -10, 4497620 ]
    crs, // 32618 (EPSG code)
    mask, // null
    nodata, // 0
} = tile.array;

// Pixel data is in `data` or `bands`, depending on the layout
if (tile.array.layout === "pixel-interleaved") {
    const { data } = tile.array; // Uint8Array(3145728) [49,  51,  46,  42, ...]
}

Convenient access to overviews

The .overviews attribute on GeoTIFF contains an array of reduced-resolution overviews, ordered from highest to lowest resolution. Use these to fetch tiles at your desired resolution.

const geotiff = await GeoTIFF.fromUrl("https://example.com/image.tif");
// Read from the first overview
// (the next-highest resolution after the full-resolution image)
const overview = geotiff.overviews[0];
// Read top-left tile of the overview
const tile = await overview.fetchTile(0, 0);

Read any pixel window

Use GeoTIFF.read to read a rectangle of pixels without handling tiles yourself. It fetches every tile that the window touches and stitches them into one RasterArray covering exactly the window. Omit window to read the whole image. Overviews have the same method.

const geotiff = await GeoTIFF.fromUrl("https://example.com/image.tif");

// Read a 512x512 window, starting at column 1024 and row 2048
const array = await geotiff.read({
  window: { colOff: 1024, rowOff: 2048, width: 512, height: 512 },
});

// Read a whole overview
const overview = geotiff.overviews[0];
const overviewArray = await overview.read();

A window that doesn't fit inside the image throws a WindowError.

Affine transformation handling

Integrates with @developmentseed/affine for easy handling of image transforms, associating pixel positions to spatial coordinates.

All loaded RasterArray instances contain an Affine, tracking the relative image transform of that specific image window.

Automatic Nodata Mask handling

With a library like geotiff.js or the underlying @cogeotiff/core, you have to do extra work to keep track of which of the internal images represent data versus masks. We automatically handle nodata values and mask arrays.

Easy CRS handling

GeoTIFFs can be defined either by an integer EPSG code or by a user-defined CRS.

For integer EPSG codes, the GeoTIFF.crs method returns the integer directly, allowing downstream applications to decide how to resolve the EPSG code into WKT or PROJJSON formats (e.g. by querying https://epsg.io).

For user-defined CRSes, we automatically parse the CRS into a PROJJSON object, which can be passed directly into proj4js. This is natively implemented without a large cache of PROJ data. This avoids the need for a large additional dependency like geotiff-geokeys-to-proj4, which would otherwise add 1.5MB to your bundle.

If you have an image where the CRS fails to parse, please create an issue.

Configurable Web Worker pool for image decoding

A DecoderPool decodes image data in Web Workers, off the main thread. Pass one to fetchTile, fetchTiles or read:

import { defaultDecoderPool, GeoTIFF } from "@developmentseed/geotiff";

const geotiff = await GeoTIFF.fromUrl("https://example.com/image.tif");
const tile = await geotiff.fetchTile(0, 0, { pool: defaultDecoderPool() });

Without a pool, tiles are decoded on the main thread.

defaultDecoderPool() returns a single shared pool of navigator.hardwareConcurrency built-in workers. To set the number of workers or use your own worker script, pass size and createWorker to the DecoderPool constructor. A DecoderPool without createWorker has no workers, so it decodes on the main thread too.

If you bundle with Vite, set worker.format to "es". The decoder worker uses top-level await and dynamic imports, which Vite's default iife worker format doesn't support, so vite build fails without it:

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  worker: { format: "es" },
});

Dynamically load compressions as needed

Instead of bundling support for all compressions out of the box, dynamically load the decompressors as required.

Until you try to load an image compressed with, say, LERC, you don't pay for the bundle size of the dependency.

You can also override the built-in decoders with your own by using DECODER_REGISTRY. For example, to use a custom zstd decoder:

import { Compression } from "@cogeotiff/core";
import { DECODER_REGISTRY } from "@developmentseed/geotiff";

DECODER_REGISTRY.set(Compression.Zstd, () =>
  import("your-zstd-module").then((m) => m.decode),
);

A decoder is a function that takes an ArrayBuffer and a DecoderMetadata object and returns a Promise of either the decompressed bytes as an ArrayBuffer or the decoded pixels. See Decoder for the full type definition.

Bring your own source

GeoTIFF.open takes any chunkd Source, so a file on disk, in memory, or behind a custom transport can be opened by composing one; the chunkd packages are separate installs. Give the header source chunking and caching, since cogeotiff reads offset-table entries one at a time.

import { SourceCache, SourceChunk } from "@chunkd/middleware";
import { SourceView } from "@chunkd/source";
import { SourceFile } from "@chunkd/source-file";
import { GeoTIFF } from "@developmentseed/geotiff";

const file = new SourceFile("/data/scene.tif");
const headerSource = new SourceView(file, [
  new SourceChunk({ size: 64 * 1024 }),
  new SourceCache({ size: 16 * 1024 * 1024 }),
]);
const geotiff = await GeoTIFF.open({ headerSource, dataSource: file });
const tile = await geotiff.fetchTile(0, 0);

For HTTP, use GeoTIFF.fromUrl (next section): it reads through a RangeReader, which merges, streams and caches reads in a way a chunkd stack cannot.

Nearly-identical API to Python async-geotiff

The TypeScript API is nearly identical to our Python project async-geotiff.

This is extremely useful for us as we build visualization projects for both Python and the browser.

Reading over HTTP

GeoTIFF.fromUrl reads through a RangeReader: the reads cogeotiff makes for the header (IFDs, then one offset-table entry per tile) and the reads for tile bytes are merged into few streamed range requests, each tile resolves as its bytes arrive, and everything received is cached, so a pan back over tiles that were already downloaded costs no request.

import { GeoTIFF } from "@developmentseed/geotiff";
import { PerOriginSemaphore, RangeReader } from "@developmentseed/range-reader";

// One reader per page: files that share it share its cache and its batching
// window. (The deck.gl layers do this for you.)
const reader = new RangeReader();
const concurrencyLimiter = new PerOriginSemaphore({ maxRequests: 6 });

const tiff = await GeoTIFF.fromUrl("https://example.com/scene.tif", {
  reader,
  concurrencyLimiter,
  // Headers, credentials and mode for every request of this file.
  requestInit: { headers: { authorization: "Bearer ..." } },
});

// Reads issued together are merged: these four tiles become one or two
// range requests, not four.
const tiles = await tiff.fetchTiles([
  [0, 0],
  [1, 0],
  [0, 1],
  [1, 1],
]);

Tune the reader, not the file: maxGap, maxOverfetchBytes, maxRequestBytes, minMetadataReadBytes and the cache budgets are RangeReader options (the former chunkSize and cacheSize options of fromUrl are minMetadataReadBytes and cache.metadataBytes there). reader.stats counts requests, bytes and cache hits. Without a concurrencyLimiter, every merged request goes out at once; the deck.gl layers pass a shared per-origin limiter, and so should a page that opens many files itself. A file replaced in place while open fails its next read with an ETag conflict error, as it did through chunkd, because the parsed layout no longer applies; open it again.

Why not build on top of geotiff.js?

The initial implementation of deck.gl-raster used geotiff.js, and geotiff.js was great for quickly getting started. But there's a few reasons why this project switched to @cogeotiff/core.

  • Fully typed: @cogeotiff/core is fully typed in expressive TypeScript, making it much more enjoyable to build on top of. Even the low-level
  • @cogeotiff/core implements a bunch of optimizations, like reading and utilizing the GDAL "ghost header" out of the box. In contrast, geotiff.js can parse but won't automatically use the ghost values.
  • Project scope: geotiff.js has a lot of code unrelated to the needs of deck.gl-raster. All we need here is really efficient access to individual tiles from the COG. geotiff.js has a bunch of features we don't need: resampling, tile-merging, conversion to RGB, overview choice based on a target resolution, or writing GeoTIFFs.
  • Complexity: this is subjective. Overall geotiff.js seems to be... fine. But there are various parts of geotiff.js that give me pause. Vendoring a full 1000-line JPEG decoder? Perhaps this is because they need to support JPEG decoding in Node as well (points to differences in project scopes), but it doesn't give me confidence that I could fix a problem there if I had to.
  • Confidence to build on top of: this is subjective, but geotiff.js doesn't feel focused, like the way that @cogeotiff/core is very focused on its targeted, narrow API.
  • JSDoc is hard to read and contribute to: this is very subjective, but I find it much harder to read and contribute to geotiff.js code written with JSDoc instead of pure TypeScript.
  • Code quality: there are various parts of geotiff.js with code just... commented out. And the function has no documentation. Why is it normalizing? The needsNormalization function also has no documentation, and is hard to understand what the equality is checking because it doesn't use TypeScript-standard enums, which would make the code itself readable.

Overall, geotiff.js seems like a fine library, and it was useful to get started with to prove my proof of concept quickly. But if I'm building an entire stack on top of a COG reader, I need to have a huge amount of confidence on what I'm building on, and @cogeotiff/core gives that confidence.

版本与发布记录

@developmentseed/geotiff 共发布 23 个版本,首次发布于 2026/02/21;过去 12 个月发布了 23 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.10.0latest2026/11/102 天前
0.10.0-beta.4beta2026/11/075 天前
0.10.0-beta.32026/11/066 天前
0.10.0-beta.22026/10/3012 天前
0.9.02026/10/2419 天前
0.9.0-beta.32026/10/2320 天前

关于 @developmentseed/geotiff 的常见问题

@developmentseed/geotiff 是什么?

High-level GeoTIFF reading library.

如何安装 @developmentseed/geotiff?

在项目目录运行 npm install @developmentseed/geotiff,或使用 pnpm add / yarn add / bun add。

@developmentseed/geotiff 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@developmentseed/geotiff 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@developmentseed/geotiff 可以商用吗?

@developmentseed/geotiff 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@developmentseed/geotiff 的下载量有多少?

根据 npm 官方下载统计,@developmentseed/geotiff 在 2026/10/12 – 2026/11/10 这 30 天内被下载 22,596 次,日均约 753 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@developmentseed/geotiff 还在维护吗?

最新版本 0.10.0 发布于 2026/11/10(2 天前);至今共发布 23 个版本,其中过去 12 个月发布了 23 个;首次发布于 2026/02/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。