diff --git a/package-lock.json b/package-lock.json index fea4717d..569f59e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@basemaps/geo": "^6.44.0", "@chunkd/fs": "^10.0.9", "@chunkd/source-aws-v3": "^10.1.3", - "@cogeotiff/core": "^8.1.1", + "@cogeotiff/core": "^9.0.3", "@linzjs/geojson": "^6.43.0", "@octokit/core": "^5.0.0", "@octokit/plugin-rest-endpoint-methods": "^10.1.1", @@ -1079,9 +1079,9 @@ } }, "node_modules/@cogeotiff/core": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@cogeotiff/core/-/core-8.1.1.tgz", - "integrity": "sha512-VYC3XTdsonVJq8TEpkBWgq2RCec5mw+b1cgiKOUOm1z3ZiQ/MS4jbD9wlbeqKs7F6QuX5J6otatEGv7SPzFpTw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@cogeotiff/core/-/core-9.0.3.tgz", + "integrity": "sha512-/1amOpBg+O916kYpB0FK6F8PFnVBCB3DamfzJDWgO7lDOBh5SNOZP95xvGAE98q4/fPylBL2Q/CxvzAy8c9TCg==", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } diff --git a/package.json b/package.json index 4d6b73a4..c969d09d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@basemaps/geo": "^6.44.0", "@chunkd/fs": "^10.0.9", "@chunkd/source-aws-v3": "^10.1.3", - "@cogeotiff/core": "^8.1.1", + "@cogeotiff/core": "^9.0.3", "@linzjs/geojson": "^6.43.0", "@octokit/core": "^5.0.0", "@octokit/plugin-rest-endpoint-methods": "^10.1.1", diff --git a/src/commands/common.ts b/src/commands/common.ts index 58ebc226..5c01845b 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -1,5 +1,5 @@ import { fsa } from '@chunkd/fs'; -import { CogTiff } from '@cogeotiff/core'; +import { Tiff } from '@cogeotiff/core'; import { boolean, flag, option, optional, string } from 'cmd-ts'; import pLimit from 'p-limit'; import { fileURLToPath, pathToFileURL } from 'url'; @@ -91,10 +91,10 @@ const TiffQueue = pLimit(25); * @param loc location to load the tiff from * @returns Initialized tiff */ -export function createTiff(loc: string): Promise { +export function createTiff(loc: string): Promise { const source = fsa.source(loc); - const tiff = new CogTiff({ + const tiff = new Tiff({ url: tryParseUrl(loc), fetch: (offset, length): Promise => { /** Limit fetches concurrency see {@link TiffQueue} **/ diff --git a/src/commands/path/path.generate.ts b/src/commands/path/path.generate.ts index 8d321baa..dfdecbb1 100644 --- a/src/commands/path/path.generate.ts +++ b/src/commands/path/path.generate.ts @@ -1,6 +1,6 @@ import { Epsg } from '@basemaps/geo'; import { fsa } from '@chunkd/fs'; -import { CogTiff } from '@cogeotiff/core'; +import { Tiff } from '@cogeotiff/core'; import { command, option, positional, string } from 'cmd-ts'; import { StacCollection, StacItem } from 'stac-ts'; @@ -154,9 +154,9 @@ export function formatDate(collection: StacCollection): string { * @async * @param {string} source * @param {StacCollection} collection - * @returns {Promise} + * @returns {Promise} */ -export async function loadFirstTiff(source: string, collection: StacCollection): Promise { +export async function loadFirstTiff(source: string, collection: StacCollection): Promise { const itemLink = collection.links.find((f) => f.rel === 'item')?.href; if (itemLink == null) throw new Error(`No items in collection from ${source}.`); const itemPath = new URL(itemLink, source).href; @@ -170,7 +170,7 @@ export async function loadFirstTiff(source: string, collection: StacCollection): return tiff; } -export function extractGsd(tiff: CogTiff): number { +export function extractGsd(tiff: Tiff): number { const gsd = tiff.images[0]?.resolution[0]; if (gsd == null) { throw new Error(`Missing resolution tiff tag: ${tiff.source.url}`); @@ -178,7 +178,7 @@ export function extractGsd(tiff: CogTiff): number { return gsd; } -export function extractEpsg(tiff: CogTiff): number { +export function extractEpsg(tiff: Tiff): number { const epsg = tiff.images[0]?.epsg; if (epsg == null) { throw new Error(`Missing epsg tiff tag: ${tiff.source.url}`); diff --git a/src/commands/tileindex-validate/__test__/data/16b.tiff b/src/commands/tileindex-validate/__test__/data/16b.tiff new file mode 100644 index 00000000..ee27c44a Binary files /dev/null and b/src/commands/tileindex-validate/__test__/data/16b.tiff differ diff --git a/src/commands/tileindex-validate/__test__/data/8b.tiff b/src/commands/tileindex-validate/__test__/data/8b.tiff new file mode 100644 index 00000000..247a49b9 Binary files /dev/null and b/src/commands/tileindex-validate/__test__/data/8b.tiff differ diff --git a/src/commands/tileindex-validate/__test__/tileindex.validate.data.ts b/src/commands/tileindex-validate/__test__/tileindex.validate.data.ts index c26951b0..07363ae4 100644 --- a/src/commands/tileindex-validate/__test__/tileindex.validate.data.ts +++ b/src/commands/tileindex-validate/__test__/tileindex.validate.data.ts @@ -1,4 +1,4 @@ -import { CogTiff, CogTiffImage, Size } from '@cogeotiff/core'; +import { Size, Tiff, TiffImage } from '@cogeotiff/core'; import { MapSheet } from '../../../utils/mapsheet.js'; @@ -16,12 +16,12 @@ const DefaultTiffImage = { isGeoLocated: true, }; -export interface FakeCogTiffImage extends CogTiffImage { +export interface FakeCogTiffImage extends TiffImage { epsg: number; origin: [number, number, number]; } -export class FakeCogTiff extends CogTiff { +export class FakeCogTiff extends Tiff { override images: [FakeCogTiffImage, ...FakeCogTiffImage[]]; constructor( diff --git a/src/commands/tileindex-validate/__test__/tileindex.validate.test.ts b/src/commands/tileindex-validate/__test__/tileindex.validate.test.ts index be9a4f36..464198dc 100644 --- a/src/commands/tileindex-validate/__test__/tileindex.validate.test.ts +++ b/src/commands/tileindex-validate/__test__/tileindex.validate.test.ts @@ -7,6 +7,7 @@ import { FeatureCollection } from 'geojson'; import { MapSheetData } from '../../../utils/__test__/mapsheet.data.js'; import { GridSize, MapSheet } from '../../../utils/mapsheet.js'; +import { createTiff } from '../../common.js'; import { commandTileIndexValidate, extractTiffLocations, @@ -14,6 +15,7 @@ import { GridSizeFromString, groupByTileName, TiffLoader, + validate8BitsTiff, } from '../tileindex.validate.js'; import { FakeCogTiff } from './tileindex.validate.data.js'; @@ -276,3 +278,17 @@ describe('GridSizeFromString', () => { ); }); }); + +describe('is8BitsTiff', () => { + it('should be a 8 bits TIFF', async () => { + const testTiff = await createTiff('./src/commands/tileindex-validate/__test__/data/8b.tiff'); + await assert.doesNotReject(validate8BitsTiff(testTiff)); + }); + it('should not be a 8 bits TIFF', async () => { + const testTiff = await createTiff('./src/commands/tileindex-validate/__test__/data/16b.tiff'); + await assert.rejects(validate8BitsTiff(testTiff), { + name: 'Error', + message: `${testTiff.source.url} is not a 8 bits TIFF`, + }); + }); +}); diff --git a/src/commands/tileindex-validate/tileindex.validate.ts b/src/commands/tileindex-validate/tileindex.validate.ts index c7c8b073..574ffaa5 100644 --- a/src/commands/tileindex-validate/tileindex.validate.ts +++ b/src/commands/tileindex-validate/tileindex.validate.ts @@ -1,6 +1,6 @@ import { Bounds, Projection } from '@basemaps/geo'; import { fsa } from '@chunkd/fs'; -import { CogTiff, Size } from '@cogeotiff/core'; +import { Size, Tiff, TiffTag } from '@cogeotiff/core'; import { boolean, command, flag, number, option, optional, restPositionals, string, Type } from 'cmd-ts'; import { CliInfo } from '../../cli.info.js'; @@ -25,7 +25,7 @@ export const TiffLoader = { * @param args filter the tiffs * @returns Initialized tiff */ - async load(locations: string[], args?: FileFilter): Promise { + async load(locations: string[], args?: FileFilter): Promise { const files = await getFiles(locations, args); const tiffLocations = files.flat().filter(isTiff); if (tiffLocations.length === 0) throw new Error('No Files found'); @@ -46,6 +46,8 @@ export const TiffLoader = { for (const prom of promises) { // All the errors are logged above so just throw the first error if (prom.status === 'rejected') throw new Error('Tiff loading failed: ' + String(prom.reason)); + // We are processing only 8 bits Tiff for now + await validate8BitsTiff(prom.value); output.push(prom.value); } return output; @@ -286,7 +288,7 @@ export interface TiffLocation { * @returns {TiffLocation[]} */ export async function extractTiffLocations( - tiffs: CogTiff[], + tiffs: Tiff[], gridSize: GridSize, forceSourceEpsg?: number, ): Promise { @@ -416,3 +418,22 @@ export function getTileName(x: number, y: number, gridSize: GridSize): string { const tileId = `${`${tileY}`.padStart(nbDigits, '0')}${`${tileX}`.padStart(nbDigits, '0')}`; return `${sheetCode}_${gridSize}_${tileId}`; } + +/** + * Validate if a TIFF contains only 8 bits bands. + * + * @param tiff + */ +export async function validate8BitsTiff(tiff: Tiff): Promise { + const baseImage = tiff.images[0]; + if (baseImage === undefined) throw new Error(`Can't get base image for ${tiff.source.url}`); + + const bitsPerSample = await baseImage.fetch(TiffTag.BitsPerSample); + if (bitsPerSample == null) { + throw new Error(`Failed to extract band information from ${tiff.source.url}`); + } + + if (!bitsPerSample.every((currentNumberBits) => currentNumberBits === 8)) { + throw new Error(`${tiff.source.url} is not a 8 bits TIFF`); + } +} diff --git a/src/utils/__test__/geotiff.test.ts b/src/utils/__test__/geotiff.test.ts index aff2fbbf..9ffd4525 100644 --- a/src/utils/__test__/geotiff.test.ts +++ b/src/utils/__test__/geotiff.test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'node:test'; import { fsa } from '@chunkd/fs'; import { FsMemory } from '@chunkd/source-memory'; -import { CogTiff, CogTiffImage, Source } from '@cogeotiff/core'; +import { Source, Tiff, TiffImage } from '@cogeotiff/core'; import { createTiff } from '../../commands/common.js'; import { findBoundingBox, parseTfw, PixelIsPoint } from '../geotiff.js'; @@ -77,7 +77,7 @@ describe('geotiff', () => { const fakeSource: Source = { url: url, fetch: async () => new ArrayBuffer(1) }; it('should not parse a tiff with no information ', async () => { // tiff with no location information and no TFW - await assert.rejects(() => findBoundingBox({ source: fakeSource, images: [] } as unknown as CogTiff)); + await assert.rejects(() => findBoundingBox({ source: fakeSource, images: [] } as unknown as Tiff)); }); it('should parse a tiff with TFW', async () => { @@ -86,8 +86,8 @@ describe('geotiff', () => { // tiff with no location information and no TFW const bbox = await findBoundingBox({ source: fakeSource, - images: [{ size: { width: 3200, height: 4800 } }] as unknown as CogTiffImage, - } as unknown as CogTiff); + images: [{ size: { width: 3200, height: 4800 } }] as unknown as TiffImage, + } as unknown as Tiff); assert.deepEqual(bbox, [1460800, 5079120, 1461040, 5079480]); await fsa.delete('memory://BX20_500_023098.tfw'); }); @@ -104,9 +104,9 @@ describe('geotiff', () => { valueGeo(): number { return 1; // PixelIsArea }, - } as unknown as CogTiffImage, + } as unknown as TiffImage, ], - } as unknown as CogTiff); + } as unknown as Tiff); assert.deepEqual(bbox, [1460800, 5079120, 1461040, 5079480]); }); it('should parse with pixel offset', async () => { @@ -121,9 +121,9 @@ describe('geotiff', () => { valueGeo(): number { return PixelIsPoint; }, - } as unknown as CogTiffImage, + } as unknown as TiffImage, ], - } as unknown as CogTiff); + } as unknown as Tiff); assert.deepEqual(bbox, [1460800, 5079120, 1461040, 5079480]); }); }); diff --git a/src/utils/geotiff.ts b/src/utils/geotiff.ts index cac74f1d..5ade834c 100644 --- a/src/utils/geotiff.ts +++ b/src/utils/geotiff.ts @@ -1,5 +1,5 @@ import { fsa } from '@chunkd/fs'; -import { CogTiff, TiffTagGeo } from '@cogeotiff/core'; +import { Tiff, TiffTagGeo } from '@cogeotiff/core'; import { urlToString } from '../commands/common.js'; @@ -52,7 +52,7 @@ export const PixelIsPoint = 2; * * @returns [minX, minY, maxX, maxY] bounding box */ -export async function findBoundingBox(tiff: CogTiff): Promise<[number, number, number, number]> { +export async function findBoundingBox(tiff: Tiff): Promise<[number, number, number, number]> { const img = tiff.images[0]; if (img == null) throw new Error(`Failed to find bounding box/origin - no images found in file: ${tiff.source.url}`); const size = img.size;