diff --git a/packages/geotiff/src/colorinterp.ts b/packages/geotiff/src/colorinterp.ts new file mode 100644 index 00000000..b0c5b87a --- /dev/null +++ b/packages/geotiff/src/colorinterp.ts @@ -0,0 +1,67 @@ +import { Photometric } from "@cogeotiff/core"; +import { ExtraSample } from "./ifd.js"; + +export enum ColorInterp { + UNDEFINED = "undefined", + GRAY = "gray", + RED = "red", + GREEN = "green", + BLUE = "blue", + ALPHA = "alpha", + PALETTE = "palette", + CYAN = "cyan", + MAGENTA = "magenta", + YELLOW = "yellow", + BLACK = "black", + Y = "Y", + Cb = "Cb", + Cr = "Cr", +} + +export function inferColorInterpretation({ + count, + photometric, + extraSamples, +}: { + count: number; + photometric: Photometric | null; + extraSamples: ExtraSample[] | null; +}): ColorInterp[] { + switch (photometric) { + case null: + return Array(count).fill(ColorInterp.UNDEFINED); + + case Photometric.MinIsBlack: + return Array(count).fill(ColorInterp.GRAY); + + case Photometric.Rgb: { + if (count < 3) { + throw new Error( + "RGB photometric interpretation with fewer than 3 bands is not supported.", + ); + } + if (count === 3) { + return [ColorInterp.RED, ColorInterp.GREEN, ColorInterp.BLUE]; + } + // count >= 4: map extra samples + const extras = (extraSamples ?? []).map((sample) => + sample === ExtraSample.UnassociatedAlpha ? ColorInterp.ALPHA : ColorInterp.UNDEFINED, + ); + return [ColorInterp.RED, ColorInterp.GREEN, ColorInterp.BLUE, ...extras]; + } + + case Photometric.Palette: + return [ColorInterp.PALETTE]; + + case Photometric.Separated: + return [ColorInterp.CYAN, ColorInterp.MAGENTA, ColorInterp.YELLOW, ColorInterp.BLACK]; + + case Photometric.Ycbcr: + return [ColorInterp.Y, ColorInterp.Cb, ColorInterp.Cr]; + + default: + throw new Error( + `Color interpretation not implemented for photometric: ${photometric}`, + ); + } +} diff --git a/packages/geotiff/src/geotiff.ts b/packages/geotiff/src/geotiff.ts index 96d4725c..70ff2d35 100644 --- a/packages/geotiff/src/geotiff.ts +++ b/packages/geotiff/src/geotiff.ts @@ -10,6 +10,8 @@ import { crsFromGeoKeys } from "./crs.js"; import { fetchTile } from "./fetch.js"; import type { BandStatistics, GDALMetadata } from "./gdal-metadata.js"; import { parseGDALMetadata } from "./gdal-metadata.js"; +import type { ColorInterp } from "./colorinterp.js"; +import { inferColorInterpretation } from "./colorinterp.js"; import type { CachedTags, GeoKeyDirectory } from "./ifd.js"; import { extractGeoKeyDirectory, prefetchTags } from "./ifd.js"; import { Overview } from "./overview.js"; @@ -330,6 +332,15 @@ export class GeoTIFF { return this.image.value(TiffTag.SamplesPerPixel) ?? 1; } + /** The color interpretation of each band in index order. */ + get colorInterp(): ColorInterp[] { + return inferColorInterpretation({ + count: this.count, + photometric: this.cachedTags.photometric, + extraSamples: this.cachedTags.extraSamples, + }); + } + /** Bounding box [minX, minY, maxX, maxY] in the CRS. */ get bbox(): [number, number, number, number] { return this.image.bbox; diff --git a/packages/geotiff/src/ifd.ts b/packages/geotiff/src/ifd.ts index c572f775..d1a8b49c 100644 --- a/packages/geotiff/src/ifd.ts +++ b/packages/geotiff/src/ifd.ts @@ -1,11 +1,28 @@ import type { TiffImage, TiffTagGeoType, TiffTagType } from "@cogeotiff/core"; import { Predictor, SampleFormat, TiffTag, TiffTagGeo } from "@cogeotiff/core"; +/** + * Description of extra components. + * + * Specifies that each pixel has N extra components whose interpretation is + * defined by one of the values listed below. When this field is used, the + * SamplesPerPixel field has a value greater than the PhotometricInterpretation + * field suggests. + * + * @see https://web.archive.org/web/20240329145321/https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html + */ +export enum ExtraSample { + Unspecified = 0, + AssociatedAlpha = 1, + UnassociatedAlpha = 2, +} + /** Subset of TIFF tags that we pre-fetch for easier visualization. */ export interface CachedTags { bitsPerSample: Uint16Array; colorMap?: Uint16Array; // TiffTagType[TiffTag.ColorMap]; compression: TiffTagType[TiffTag.Compression]; + extraSamples: [ExtraSample] | null; gdalMetadata: TiffTagType[TiffTag.GdalMetadata] | null; lercParameters: TiffTagType[TiffTag.LercParameters] | null; modelTiepoint: TiffTagType[TiffTag.ModelTiePoint] | null; @@ -33,6 +50,7 @@ export async function prefetchTags(image: TiffImage): Promise { const [ bitsPerSample, colorMap, + extraSamples, gdalNoData, gdalMetadata, lercParameters, @@ -49,6 +67,7 @@ export async function prefetchTags(image: TiffImage): Promise { ] = await Promise.all([ image.fetch(TiffTag.BitsPerSample), image.fetch(TiffTag.ColorMap), + image.fetch(TiffTag.ExtraSamples), image.fetch(TiffTag.GdalNoData), image.fetch(TiffTag.GdalMetadata), image.fetch(TiffTag.LercParameters), @@ -91,6 +110,7 @@ export async function prefetchTags(image: TiffImage): Promise { bitsPerSample: new Uint16Array(bitsPerSample), colorMap: colorMap ? new Uint16Array(colorMap as number[]) : undefined, compression, + extraSamples: (extraSamples as [ExtraSample] | null) ?? null, gdalMetadata, lercParameters, modelTiepoint,