Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/geotiff/src/codecs/canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { DecodedPixels, DecoderMetadata } from "../decode/api.js";

// TODO: in the future, have an API that returns an ImageBitmap directly from
// the decoder, to avoid copying pixel data from GPU -> CPU memory
// Then deck.gl could use the ImageBitmap directly as a texture source without
// copying again from CPU -> GPU memory
// https://github.com/developmentseed/deck.gl-raster/issues/228
export async function decode(
bytes: ArrayBuffer,
metadata: DecoderMetadata,
): Promise<DecodedPixels> {
const blob = new Blob([bytes]);
const imageBitmap = await createImageBitmap(blob);

const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
const ctx = canvas.getContext("2d")!;
ctx.drawImage(imageBitmap, 0, 0);
imageBitmap.close();

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const rgba = imageData.data;

const samplesPerPixel = metadata.samplesPerPixel;
if (samplesPerPixel === 4) {
return { layout: "pixel-interleaved", data: rgba };
}

if (samplesPerPixel === 3) {
const pixelCount = imageBitmap.width * imageBitmap.height;
const rgb = new Uint8ClampedArray(pixelCount * 3);
for (let i = 0, j = 0; i < rgb.length; i += 3, j += 4) {
rgb[i] = rgba[j]!;
rgb[i + 1] = rgba[j + 1]!;
rgb[i + 2] = rgba[j + 2]!;
}
return { layout: "pixel-interleaved", data: rgb };
}

throw new Error(`Unsupported SamplesPerPixel for JPEG: ${samplesPerPixel}`);
}
37 changes: 17 additions & 20 deletions packages/geotiff/src/decode/api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { Compression, SampleFormat } from "@cogeotiff/core";
import type { RasterTypedArray } from "../array.js";
import { decode as decodeViaCanvas } from "../codecs/canvas.js";

/** The result of a decoding process */
export type DecodedPixels =
| { layout: "pixel-interleaved"; data: RasterTypedArray }
| { layout: "band-separate"; bands: RasterTypedArray[] };

/** Metadata from the TIFF IFD, passed to decoders that need it. */
export type DecoderMetadata = {
sampleFormat: SampleFormat;
bitsPerSample: number;
samplesPerPixel: number;
};

/**
* A decoder returns either:
* - An ArrayBuffer of raw decompressed bytes (byte-level codecs like deflate, zstd)
* - A DecodedPixels with typed pixel data (image codecs like LERC, JPEG)
*/
export type Decoder = (
bytes: ArrayBuffer,
metadata: DecoderMetadata,
) => Promise<ArrayBuffer | DecodedPixels>;

async function decodeUncompressed(bytes: ArrayBuffer): Promise<ArrayBuffer> {
Expand All @@ -34,18 +43,12 @@ registry.set(Compression.DeflateOther, () =>
// registry.set(Compression.Lzma, () =>
// import("../codecs/lzma.js").then((m) => m.decode),
// );
// registry.set(Compression.Webp, () =>
// import("../codecs/webp.js").then((m) => m.decode),
// );
// registry.set(Compression.Jp2000, () =>
// import("../codecs/jp2000.js").then((m) => m.decode),
// );
// registry.set(Compression.Jpeg, () =>
// import("../codecs/jpeg.js").then((m) => m.decode),
// );
// registry.set(Compression.Jpeg6, () =>
// import("../codecs/jpeg.js").then((m) => m.decode),
// );
registry.set(Compression.Jpeg, () => Promise.resolve(decodeViaCanvas));
registry.set(Compression.Jpeg6, () => Promise.resolve(decodeViaCanvas));
registry.set(Compression.Webp, () => Promise.resolve(decodeViaCanvas));
registry.set(Compression.Lerc, () =>
import("../codecs/lerc.js").then((m) => m.decode),
);
Expand All @@ -56,26 +59,20 @@ registry.set(Compression.Lerc, () =>
export async function decode(
bytes: ArrayBuffer,
compression: Compression,
{
sampleFormat,
bitsPerSample,
}: {
sampleFormat: SampleFormat;
bitsPerSample: number;
},
metadata: DecoderMetadata,
): Promise<DecodedPixels> {
const loader = registry.get(compression);
if (!loader) {
throw new Error(`Unsupported compression: ${compression}`);
}

const decoder = await loader();
const result = await decoder(bytes);
const result = await decoder(bytes, metadata);

if (result instanceof ArrayBuffer) {
return {
layout: "pixel-interleaved",
data: toTypedArray(result, sampleFormat, bitsPerSample),
data: toTypedArray(result, metadata),
};
}

Expand All @@ -89,9 +86,9 @@ export async function decode(
*/
function toTypedArray(
buffer: ArrayBuffer,
sampleFormat: SampleFormat,
bitsPerSample: number,
metadata: Pick<DecoderMetadata, "sampleFormat" | "bitsPerSample">,
): RasterTypedArray {
const { sampleFormat, bitsPerSample } = metadata;
switch (sampleFormat) {
case SampleFormat.Uint:
switch (bitsPerSample) {
Expand Down
11 changes: 6 additions & 5 deletions packages/geotiff/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,19 @@ export async function fetchTile(
translation(x * self.tileWidth, y * self.tileHeight),
);

// https://github.com/blacha/cogeotiff/pull/1394
const samplesPerPixel =
(self.image.value(TiffTag.SamplesPerPixel) as number) ?? 1;

const decodedPixels = await decode(bytes, compression, {
sampleFormat,
bitsPerSample,
samplesPerPixel,
});

if (decodedPixels.layout === "band-separate") {
}

const array = {
...decodedPixels,
// https://github.com/blacha/cogeotiff/pull/1394
count: self.image.value(TiffTag.SamplesPerPixel) as number,
count: samplesPerPixel,
height: self.tileHeight,
width: self.tileWidth,
mask: null,
Expand Down
2 changes: 1 addition & 1 deletion packages/geotiff/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type { RasterArray } from "./array.js";
export type { DecodedPixels, Decoder } from "./decode/api.js";
export type { DecodedPixels, Decoder, DecoderMetadata } from "./decode/api.js";
export { decode, registry } from "./decode/api.js";
export { GeoTIFF } from "./geotiff.js";
export { Overview } from "./overview.js";
Expand Down
6 changes: 4 additions & 2 deletions packages/geotiff/tests/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ describe("decode", () => {
const sampleFormat =
(image.value(TiffTag.SampleFormat) as SampleFormat[] | null)?.[0] ??
SampleFormat.Uint;
const samplesPerPixel = image.value(TiffTag.SamplesPerPixel) as number;

const result = await decode(tile!.bytes, tile!.compression, {
sampleFormat,
bitsPerSample,
samplesPerPixel,
});

const { width, height } = image.tileSize;
const samplesPerPixel = image.value(TiffTag.SamplesPerPixel) as number;
const bytesPerSample = bitsPerSample / 8;
const expectedBytes = width * height * samplesPerPixel * bytesPerSample;

Expand All @@ -42,14 +43,15 @@ describe("decode", () => {
const sampleFormat =
(image.value(TiffTag.SampleFormat) as SampleFormat[] | null)?.[0] ??
SampleFormat.Uint;
const samplesPerPixel = image.value(TiffTag.SamplesPerPixel) as number;

const result = await decode(tile!.bytes, tile!.compression, {
sampleFormat,
bitsPerSample,
samplesPerPixel,
});

const { width, height } = image.tileSize;
const samplesPerPixel = image.value(TiffTag.SamplesPerPixel) as number;
const bytesPerSample = bitsPerSample / 8;
const expectedBytesPerBand = width * height * bytesPerSample;

Expand Down