diff --git a/biome.json b/biome.json index b50dbf5d..bd18ddf4 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", "assist": { "actions": { "source": { diff --git a/fixtures/geotiff-test-data b/fixtures/geotiff-test-data index 4f268831..ee231aa8 160000 --- a/fixtures/geotiff-test-data +++ b/fixtures/geotiff-test-data @@ -1 +1 @@ -Subproject commit 4f2688310cbd9fccdd1c677ed8057b829ce99cb2 +Subproject commit ee231aa8e99a433987c40da96806d446d59437c4 diff --git a/packages/geotiff/README.md b/packages/geotiff/README.md index 295ee24a..d84c8bc3 100644 --- a/packages/geotiff/README.md +++ b/packages/geotiff/README.md @@ -75,6 +75,19 @@ Until you try to load an image compressed with, say, [LERC], you don't pay for t [LERC]: https://github.com/Esri/lerc +You can also override the built-in decoders with your own by using `registry`. For example, to use a custom zstd decoder: + +```ts +import { Compression } from "@cogeotiff/core"; +import { registry } from "@developmentseed/geotiff"; + +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`. See [decode.ts](./src/decode.ts) for the full type definitions. + ### Full user control over caching and chunking There are a lot of great utilities in [`chunkd`](https://github.com/blacha/chunkd) that work out of the box here. diff --git a/packages/geotiff/package.json b/packages/geotiff/package.json index c1989468..7f87c5b5 100644 --- a/packages/geotiff/package.json +++ b/packages/geotiff/package.json @@ -55,6 +55,7 @@ "@developmentseed/affine": "workspace:^", "@developmentseed/lzw-tiff-decoder": "^0.2.2", "@developmentseed/morecantile": "workspace:^", + "fzstd": "^0.1.1", "uuid": "^13.0.0" } } diff --git a/packages/geotiff/src/codecs/zstd.ts b/packages/geotiff/src/codecs/zstd.ts new file mode 100644 index 00000000..b5ab4c18 --- /dev/null +++ b/packages/geotiff/src/codecs/zstd.ts @@ -0,0 +1,6 @@ +import { decompress } from "fzstd"; + +export async function decode(bytes: ArrayBuffer): Promise { + const result = decompress(new Uint8Array(bytes)); + return result.buffer as ArrayBuffer; +} diff --git a/packages/geotiff/src/decode.ts b/packages/geotiff/src/decode.ts index c92a1493..8420e307 100644 --- a/packages/geotiff/src/decode.ts +++ b/packages/geotiff/src/decode.ts @@ -46,9 +46,9 @@ registry.set(Compression.DeflateOther, () => registry.set(Compression.Lzw, () => import("./codecs/lzw.js").then((m) => m.decode), ); -// registry.set(Compression.Zstd, () => -// import("../codecs/zstd.js").then((m) => m.decode), -// ); +registry.set(Compression.Zstd, () => + import("./codecs/zstd.js").then((m) => m.decode), +); // registry.set(Compression.Lzma, () => // import("../codecs/lzma.js").then((m) => m.decode), // ); diff --git a/packages/geotiff/tests/decode.test.ts b/packages/geotiff/tests/decode.test.ts index 8d62d635..6cb3e024 100644 --- a/packages/geotiff/tests/decode.test.ts +++ b/packages/geotiff/tests/decode.test.ts @@ -1,3 +1,4 @@ +import { PlanarConfiguration } from "@cogeotiff/core"; import { describe, expect, it } from "vitest"; import { decode } from "../src/decode.js"; import { loadGeoTIFF } from "./helpers.js"; @@ -38,6 +39,42 @@ describe("decode", () => { } }); + it("can decompress zstd-compressed tile data", async () => { + const tiff = await loadGeoTIFF("int8_3band_zstd_block64", "rasterio"); + const image = tiff.tiff.images[0]!; + const tile = await image.getTile(0, 0); + expect(tile).not.toBeNull(); + + const { bitsPerSample, sampleFormat, predictor, planarConfiguration } = + tiff.cachedTags; + const { width, height } = image.tileSize; + + // This fixture is band-separate, so each raw tile contains a single band. + const tileSamplesPerPixel = + planarConfiguration === PlanarConfiguration.Separate + ? 1 + : tiff.cachedTags.samplesPerPixel; + + const result = await decode(tile!.bytes, tile!.compression, { + sampleFormat: sampleFormat[0]!, + bitsPerSample: bitsPerSample[0]!, + samplesPerPixel: tileSamplesPerPixel, + width, + height, + predictor, + planarConfiguration, + }); + + const bytesPerSample = bitsPerSample[0]! / 8; + const expectedBytes = width * height * tileSamplesPerPixel * bytesPerSample; + + expect(result.layout).toBe("pixel-interleaved"); + if (result.layout === "pixel-interleaved") { + expect(result.data).toBeInstanceOf(Int8Array); + expect(result.data.byteLength).toBe(expectedBytes); + } + }); + it("can decompress lerc-compressed tile data", async () => { const tiff = await loadGeoTIFF("float32_1band_lerc_block32", "rasterio"); const image = tiff.tiff.images[0]!; diff --git a/packages/geotiff/tests/integration.test.ts b/packages/geotiff/tests/integration.test.ts index 5cbf91b9..6f4a7b70 100644 --- a/packages/geotiff/tests/integration.test.ts +++ b/packages/geotiff/tests/integration.test.ts @@ -5,8 +5,8 @@ * JavaScript, so we use it as a ground truth for pixel values, dimensions, and * georeferencing. * - * Fixtures that require unsupported codecs (WebP, JPEG, LZW, LZMA, JXL, - * zstd) are intentionally omitted here. + * Fixtures that require unsupported codecs (WebP, JPEG, LZW, LZMA, JXL) + * are intentionally omitted here. */ import type { GeoTIFFImage, GeoTIFF as GeotiffJs } from "geotiff"; @@ -99,8 +99,9 @@ describe("integration vs geotiff.js", () => { const tile = await ours.fetchTile(0, 0); const oursBandSep = toBandSeparate(tile.array); - // readRasters returns band-separate by default - const refData = await refImage.readRasters({ window: [0, 0, tw, th] }); + const refData = await refImage.readRasters({ + window: [0, 0, tw, th], + }); expect(oursBandSep.bands.length).toBe(ours.count); expect(refData.length).toBe(ours.count); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c62c2090..8ef758a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -406,6 +406,9 @@ importers: '@developmentseed/morecantile': specifier: workspace:^ version: link:../morecantile + fzstd: + specifier: ^0.1.1 + version: 0.1.1 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -1798,6 +1801,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fzstd@0.1.1: + resolution: {integrity: sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3962,6 +3968,8 @@ snapshots: fsevents@2.3.3: optional: true + fzstd@0.1.1: {} + gensync@1.0.0-beta.2: {} geotiff-geokeys-to-proj4@2024.4.13: {}