Skip to content
Draft
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
8 changes: 8 additions & 0 deletions packages/deck.gl-geotiff/src/geotiff/render-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ function createUnormPipeline(
throw new Error("Band-separate images not yet implemented.");
}

if (array.layout === "image-bitmap") {
return {
texture: array.data,
height: array.height,
width: array.width,
};
}

const textureFormat = inferTextureFormat(
// Add one sample for added alpha channel
numSamples,
Expand Down
41 changes: 21 additions & 20 deletions packages/geotiff/src/array.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Affine } from "@developmentseed/affine";
import type { ProjJson } from "./crs.js";
import type {
DecodedBandInterleaved,
DecodedPixelInterleaved,
DecodedPixels,
} from "./decode.js";

/** Typed arrays supported for raster sample storage. */
export type RasterTypedArray =
Expand Down Expand Up @@ -44,30 +49,13 @@ type RasterArrayBase = {
};

/** Raster stored in one typed array per band (band-major / planar). */
export type BandRasterArray = RasterArrayBase & {
layout: "band-separate";
/**
* One typed array per band, each length = width * height.
*
* This is the preferred representation when uploading one texture per band.
*/
bands: RasterTypedArray[];
};
export type BandRasterArray = RasterArrayBase & DecodedBandInterleaved;

/** Raster stored in one pixel-interleaved typed array. */
export type PixelRasterArray = RasterArrayBase & {
layout: "pixel-interleaved";
/**
* Pixel-interleaved raster data:
* [p00_band0, p00_band1, ..., p01_band0, ...]
*
* Length = width * height * count.
*/
data: RasterTypedArray;
};
export type PixelRasterArray = RasterArrayBase & DecodedPixelInterleaved;

/** Decoded raster data from a GeoTIFF region. */
export type RasterArray = BandRasterArray | PixelRasterArray;
export type RasterArray = RasterArrayBase & DecodedPixels;

/** Options for packing band data to a 4-channel pixel-interleaved array. */
export type PackBandsToRGBAOptions = {
Expand All @@ -87,6 +75,10 @@ export function toBandSeparate(array: RasterArray): BandRasterArray {
return array;
}

if (array.layout === "image-bitmap") {
throw new Error("Not implemented; should probably remove this helper fn");
}

const sampleCount = array.width * array.height;
const bands: RasterTypedArray[] = new Array(array.count);
const Ctor = array.data.constructor as new (
Expand Down Expand Up @@ -128,6 +120,10 @@ export function toPixelInterleaved(
return array;
}

if (array.layout === "image-bitmap") {
throw new Error("Not implemented; should probably remove this helper fn");
}

const Ctor = (
array.layout === "pixel-interleaved"
? array.data.constructor
Expand Down Expand Up @@ -257,6 +253,11 @@ function validateRasterShape(array: RasterArray): void {
return;
}

if (array.layout === "image-bitmap") {
// Validated in ImageBitmap construction
return;
}

const expectedDataLength = sampleCount * array.count;
if (array.data.length !== expectedDataLength) {
throw new Error(
Expand Down
39 changes: 6 additions & 33 deletions packages/geotiff/src/codecs/canvas.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,14 @@
import type { DecodedPixels, DecoderMetadata } from "../decode.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 { width, height } = canvas;
const imageData = ctx.getImageData(0, 0, width, height);
const rgba = imageData.data;

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

if (samplesPerPixel === 3) {
const pixelCount = width * 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}`);
const { clippedWidth, clippedHeight, width, height } = metadata;
const needsClip = clippedWidth !== width || clippedHeight !== height;
const imageBitmap = needsClip
? await createImageBitmap(blob, 0, 0, clippedWidth, clippedHeight)
: await createImageBitmap(blob);
return { layout: "image-bitmap", data: imageBitmap };
}
58 changes: 56 additions & 2 deletions packages/geotiff/src/decode.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
import type { PlanarConfiguration, Predictor } from "@cogeotiff/core";

Check failure on line 1 in packages/geotiff/src/decode.ts

View workflow job for this annotation

GitHub Actions / Lint

format

File content differs from formatting output
import { Compression, SampleFormat } from "@cogeotiff/core";
import type { RasterTypedArray } from "./array.js";
import { decode as decodeViaCanvas } from "./codecs/canvas.js";
import { applyPredictor } from "./codecs/predictor.js";

/** Raster data stored in an ImageBitmap.
*
* This is a common result type for image codecs like JPEG or WebP that are
* decoded via a canvas.
*/
export type DecodedImageBitmap = {
layout: "image-bitmap";
/** A pixel-interleaved ImageBitmap. */
data: ImageBitmap;
};

/** Raster stored in one pixel-interleaved typed array. */
export type DecodedPixelInterleaved = {
layout: "pixel-interleaved";
/**
* Pixel-interleaved raster data:
* [p00_band0, p00_band1, ..., p01_band0, ...]
*
* Length = width * height * count.
*/
data: RasterTypedArray;
};

/** Raster stored in one typed array per band (band-major / planar). */
export type DecodedBandInterleaved = {
layout: "band-separate";
/**
* One typed array per band, each length = width * height.
*
* This is the preferred representation when uploading one texture per band.
*/
bands: RasterTypedArray[];
};

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

/** Metadata from the TIFF IFD, passed to decoders that need it. */
export type DecoderMetadata = {
sampleFormat: SampleFormat;
bitsPerSample: number;
samplesPerPixel: number;
/** Full encoded tile width in pixels. */
width: number;
/** Full encoded tile height in pixels. */
height: number;
/**
* Clipped width for edge tiles when boundless=false.
*
* Equals `width` for interior tiles.
*
* Image-bitmap decoders use this to produce a pre-clipped bitmap.
*/
clippedWidth: number;
/**
*
* Clipped height for edge tiles when boundless=false.
*
* Equals `height` for interior tiles.
*
* Image-bitmap decoders use this to produce a pre-clipped bitmap.
*/
clippedHeight: number;
predictor: Predictor;
planarConfiguration: PlanarConfiguration;
};
Expand Down
23 changes: 20 additions & 3 deletions packages/geotiff/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,33 @@ export async function fetchTile(

const samplesPerPixel = self.image.value(TiffTag.SamplesPerPixel) ?? 1;

const { width: clippedWidth, height: clippedHeight } =
self.image.getTileBounds(x, y);
const clip =
boundless === false &&
(clippedWidth !== self.tileWidth || clippedHeight !== self.tileHeight);

const decoderMetadata = {
sampleFormat,
bitsPerSample,
samplesPerPixel,
width: self.tileWidth,
height: self.tileHeight,
clippedWidth: clip ? clippedWidth : self.tileWidth,
clippedHeight: clip ? clippedHeight : self.tileHeight,
predictor,
planarConfiguration,
};
const decodedPixels = await decodeTile(tile, decoderMetadata, pool);

const outWidth = clip ? clippedWidth : self.tileWidth;
const outHeight = clip ? clippedHeight : self.tileHeight;

const array: RasterArray = {
...decodedPixels,
count: samplesPerPixel,
height: self.tileHeight,
width: self.tileWidth,
height: outHeight,
width: outWidth,
mask: null,
transform: tileTransform,
crs: self.crs,
Expand All @@ -98,7 +109,7 @@ export async function fetchTile(
return {
x,
y,
array: boundless === false ? clipToImageBounds(self, x, y, array) : array,
array: clip ? clipToImageBounds(self, x, y, array) : array,
};
}

Expand Down Expand Up @@ -324,6 +335,12 @@ function clipToImageBounds(
return array;
}

if (array.layout === "image-bitmap") {
// We pre-clip the bitmap during decoding in `canvas.ts`, so this should
// never happen
return array;
}

if (array.layout === "pixel-interleaved") {
const { count, data } = array;
const Ctor = data.constructor as new (n: number) => typeof data;
Expand Down
5 changes: 5 additions & 0 deletions packages/geotiff/src/pool/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ export type WorkerErrorResponse = {

/** Collect the transferable ArrayBuffers from a DecodedPixels. */
export function collectTransferables(pixels: DecodedPixels): Transferable[] {
if (pixels.layout === "image-bitmap") {
return [pixels.data];
}

if (pixels.layout === "pixel-interleaved") {
return [pixels.data.buffer];
}

return pixels.bands.map((b) => b.buffer);
}

Expand Down
Loading