OpenDiachronicMaps/geotiffGesture/geotiffJS/test/geotiff.spec.js

1181 lines
42 KiB
JavaScript

/* eslint-disable no-unused-expressions */
import isNode from 'detect-node';
import { expect } from 'chai';
import http from 'http';
import serveStatic from 'serve-static';
import finalhandler from 'finalhandler';
import AbortController from 'node-abort-controller';
import { dirname } from 'path';
import { GeoTIFF, fromArrayBuffer, writeArrayBuffer, fromUrls, Pool } from '../dist-module/geotiff.js';
import { makeFetchSource } from '../dist-module/source/remote.js';
import { makeFileSource } from '../dist-module/source/file.js';
import { BlockedSource } from '../dist-module/source/blockedsource.js';
import { chunk, toArray, toArrayRecursively, range } from '../dist-module/utils.js';
import DataSlice from '../dist-module/dataslice.js';
import DataView64 from '../dist-module/dataview64.js';
const __dirname = dirname(new URL(import.meta.url).pathname);
// Set up a node server to make tiffs available at localhost:3000/test/data, and a worker pool
let server = null;
let pool = null;
before(async () => {
const serve = serveStatic(__dirname);
server = http.createServer((req, res) => {
serve(req, res, finalhandler(req, res));
});
server.listen(3000);
pool = new Pool();
});
after(async () => {
server.close();
pool.destroy();
});
function createSource(filename) {
if (isNode) {
return makeFileSource(`test/data/${filename}`);
}
return makeFetchSource(`test/data/${filename}`);
}
async function performTiffTests(tiff, width, height, sampleCount, type) {
const image = await tiff.getImage();
expect(image).to.be.ok;
expect(image.getWidth()).to.equal(width);
expect(image.getHeight()).to.equal(height);
expect(image.getSamplesPerPixel()).to.equal(sampleCount);
expect(image.getGeoKeys().GeographicTypeGeoKey).to.equal(4326);
expect(image.getGeoKeys().GeogAngularUnitsGeoKey).to.equal(9102);
const allData = await image.readRasters({ window: [200, 200, 210, 210] });
const brData = await image.readRasters({ window: [width - 10, height - 10, width, height] });
const data = await image.readRasters({ window: [200, 200, 210, 210], samples: [5] });
expect(allData).to.have.length(sampleCount);
expect(allData[0]).to.be.an.instanceof(type);
expect(brData).to.have.length(sampleCount);
expect(brData[0]).to.be.an.instanceof(type);
expect(data[0]).to.deep.equal(allData[5]);
}
async function performRGBTest(tiff, options, comparisonRaster, maxDiff) {
const image = await tiff.getImage();
const rgbRaster = await image.readRGB(options);
const comp = await comparisonRaster;
expect(rgbRaster).to.have.lengthOf(comp.length);
const diff = new Float32Array(rgbRaster);
for (let i = 0; i < rgbRaster.length; ++i) {
diff[i] = Math.abs(comp[i] - rgbRaster[i]);
}
expect(Math.max.apply(null, diff)).to.be.at.most(maxDiff);
}
function normalize(input) {
return JSON.stringify(toArrayRecursively(input));
}
function getMockMetaData(height, width) {
return {
ImageWidth: width, // only necessary if values aren't multi-dimensional
ImageLength: height, // only necessary if values aren't multi-dimensional
BitsPerSample: [8],
Compression: 1, // no compression
PhotometricInterpretation: 2,
StripOffsets: [1054],
SamplesPerPixel: 1,
RowsPerStrip: [height],
StripByteCounts: [width * height],
PlanarConfiguration: 1,
SampleFormat: [1],
ModelPixelScale: [0.031355, 0.031355, 0],
ModelTiepoint: [0, 0, 0, 11.331755000000001, 46.268645, 0],
GeoKeyDirectory: [1, 1, 0, 5, 1024, 0, 1, 2, 1025, 0, 1, 1, 2048, 0, 1, 4326, 2049, 34737, 7, 0, 2054, 0, 1, 9102],
GeoAsciiParams: 'WGS 84',
GTModelTypeGeoKey: 2,
GTRasterTypeGeoKey: 1,
GeographicTypeGeoKey: 4326,
GeogCitationGeoKey: 'WGS 84',
GDAL_NODATA: '0',
};
}
describe('GeoTIFF - external overviews', () => {
it('Can load', async () => {
const tiff = await fromUrls(
'http://localhost:3000/data/overviews_external.tiff',
['http://localhost:3000/data/overviews_external.tiff.ovr'],
);
const count = await tiff.getImageCount();
expect(count).to.equal(5);
const image1 = await tiff.getImage(0);
expect(image1.fileDirectory.ImageWidth).to.equal(539);
const image2 = await tiff.getImage(1);
expect(image2.fileDirectory.ImageWidth).to.equal(270);
const image3 = await tiff.getImage(2);
expect(image3.fileDirectory.ImageWidth).to.equal(135);
const image4 = await tiff.getImage(3);
expect(image4.fileDirectory.ImageWidth).to.equal(68);
const image5 = await tiff.getImage(4);
expect(image5.fileDirectory.ImageWidth).to.equal(34);
});
});
describe('GeoTIFF', () => {
it('geotiff.js module available', () => {
expect(GeoTIFF).to.be.ok;
});
it('should work on stripped tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should close the GeoTIFF without errors', async () => {
const tiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
expect(await tiff.close()).to.be.undefined;
});
it('should work on tiled tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('tiled.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on band interleaved tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('interleave.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
// TODO: currently only the interleave.tiff is used, make own
it('should work on band interleaved tiled tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('interleave.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on LZW compressed images', async () => {
const tiff = await GeoTIFF.fromSource(createSource('lzw.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on deflate compressed images', async () => {
const tiff = await GeoTIFF.fromSource(createSource('deflate.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on deflate compressed images with predictor', async () => {
const tiff = await GeoTIFF.fromSource(createSource('deflate_predictor.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on deflate compressed images with predictor and big strips', async () => {
const tiff = await GeoTIFF.fromSource(createSource('deflate_predictor_big_strips.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on tiled deflate compressed images with predictor', async () => {
const tiff = await GeoTIFF.fromSource(createSource('deflate_predictor_tiled.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on band interleaved, lzw compressed, and tiled tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('tiledplanarlzw.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on Int32 tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('int32.tiff'));
await performTiffTests(tiff, 539, 448, 15, Int32Array);
});
it('should work on UInt32 tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('uint32.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint32Array);
});
it('should work on Float32 tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float32.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float32Array);
});
it('should work on Float64 tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float64.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float64Array);
});
it('should work on Float64 and lzw compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float64lzw.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float64Array);
}).timeout(4000);
it('should work on packbit compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('packbits.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work with BigTIFFs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('bigtiff.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work with NASAs LZW compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('nasa_raster.tiff'));
const image = await tiff.getImage();
await image.readRasters();
});
it('should work on LERC compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('lerc.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on band interleaved LERC compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('lerc_interleave.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on LERC deflate compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('lerc_deflate.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on LERC Zstandard compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('lerc_zstd.tiff'));
await performTiffTests(tiff, 539, 448, 15, Uint16Array);
});
it('should work on Float32 and LERC compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float32lerc.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float32Array);
});
it('should work on Float32 and band interleaved LERC compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float32lerc_interleave.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float32Array);
});
it('should work on Float32 and LERC deflate compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float32lerc_deflate.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float32Array);
});
it('should work on Float32 and LERC Zstandard compressed tiffs', async () => {
const tiff = await GeoTIFF.fromSource(createSource('float32lerc_zstd.tiff'));
await performTiffTests(tiff, 539, 448, 15, Float32Array);
});
it('should work with worker pool', async () => {
const testPool = new Pool();
const tiff = await GeoTIFF.fromSource(createSource('nasa_raster.tiff'));
const image = await tiff.getImage();
await image.readRasters({ pool: testPool });
testPool.destroy();
});
it('should work with LZW compressed tiffs that have an EOI Code after a CLEAR code', async () => {
const tiff = await GeoTIFF.fromSource(createSource('lzw_clear_eoi/lzw.tiff'));
const image = await tiff.getImage();
await image.readRasters();
});
});
describe('n-bit uint tests', () => {
const wnd = [100, 100, 150, 150];
for (const n of [10, 11, 12, 13, 14, 15]) {
it(`should correctly read ${n}-bit datasets pixel interleaved`, async () => {
const truncValue = (1 << n) - 1;
const origTiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const origData = await (await origTiff.getImage()).readRasters({ window: wnd, samples: [0] });
const testTiff = await GeoTIFF.fromSource(createSource(`n_bit_${n}.tiff`));
const testImage = await testTiff.getImage();
const testData = await testImage.readRasters({ window: wnd, samples: [0] });
expect(testImage.getBitsPerSample()).to.equal(n);
for (let s = 0; s < origData.length; ++s) {
const origSample = origData[s];
const testSample = testData[s];
for (let i = 0; i < origSample.length; ++i) {
expect(testSample[i]).to.equal(Math.min(origSample[i], truncValue));
}
}
});
it(`should correctly read ${n}-bit datasets band interleaved`, async () => {
const truncValue = (1 << n) - 1;
const origTiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const origData = await (await origTiff.getImage()).readRasters({ window: wnd, samples: [0] });
const testTiff = await GeoTIFF.fromSource(createSource(`n_bit_interleave_${n}.tiff`));
const testImage = await testTiff.getImage();
const testData = await testImage.readRasters({ window: wnd, samples: [0] });
expect(testImage.getBitsPerSample()).to.equal(n);
for (let s = 0; s < origData.length; ++s) {
const origSample = origData[s];
const testSample = testData[s];
for (let i = 0; i < origSample.length; ++i) {
expect(testSample[i]).to.equal(Math.min(origSample[i], truncValue));
}
}
});
it(`should correctly read ${n}-bit tiled datasets`, async () => {
const truncValue = (1 << n) - 1;
const origTiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const origData = await (await origTiff.getImage()).readRasters({ window: wnd, samples: [0] });
const testTiff = await GeoTIFF.fromSource(createSource(`n_bit_tiled_${n}.tiff`));
const testImage = await testTiff.getImage();
const testData = await testImage.readRasters({ window: wnd, samples: [0] });
expect(testImage.getBitsPerSample()).to.equal(n);
for (let s = 0; s < origData.length; ++s) {
const origSample = origData[s];
const testSample = testData[s];
for (let i = 0; i < origSample.length; ++i) {
expect(testSample[i]).to.equal(Math.min(origSample[i], truncValue));
}
}
});
}
it('should correctly read 16-bit float pixel interleaved datasets', async () => {
const origTiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const origData = await (await origTiff.getImage()).readRasters({ window: wnd, samples: [0] });
const testTiff = await GeoTIFF.fromSource(createSource('float_n_bit_16.tiff'));
const testImage = await testTiff.getImage();
const testData = await testImage.readRasters({ window: wnd, samples: [0] });
expect(testImage.getBitsPerSample()).to.equal(16);
for (let s = 0; s < origData.length; ++s) {
const origSample = origData[s];
const testSample = testData[s];
for (let i = 0; i < origSample.length; ++i) {
expect(Math.abs(testSample[i] - origSample[i])).to.be.lessThan(100);
}
}
});
it('should correctly read 16-bit float band interleaved datasets', async () => {
const origTiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const origData = await (await origTiff.getImage()).readRasters({ window: wnd, samples: [0] });
const testTiff = await GeoTIFF.fromSource(createSource('float_n_bit_interleave_16.tiff'));
const testImage = await testTiff.getImage();
const testData = await testImage.readRasters({ window: wnd, samples: [0] });
expect(testImage.getBitsPerSample()).to.equal(16);
for (let s = 0; s < origData.length; ++s) {
const origSample = origData[s];
const testSample = testData[s];
for (let i = 0; i < origSample.length; ++i) {
expect(Math.abs(testSample[i] - origSample[i])).to.be.lessThan(100);
}
}
});
it('should correctly read 16-bit float band tiled datasets', async () => {
const origTiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const origData = await (await origTiff.getImage()).readRasters({ window: wnd, samples: [0] });
const testTiff = await GeoTIFF.fromSource(createSource('float_n_bit_tiled_16.tiff'));
const testImage = await testTiff.getImage();
const testData = await testImage.readRasters({ window: wnd, samples: [0] });
expect(testImage.getBitsPerSample()).to.equal(16);
for (let s = 0; s < origData.length; ++s) {
const origSample = origData[s];
const testSample = testData[s];
for (let i = 0; i < origSample.length; ++i) {
expect(Math.abs(testSample[i] - origSample[i])).to.be.lessThan(100);
}
}
});
});
describe('ifdRequestTests', () => {
const offsets = [8, 2712, 4394];
const source = 'multi-channel.ome.tif';
it('requesting first image only parses first IFD', async () => {
const tiff = await GeoTIFF.fromSource(createSource(source));
await tiff.getImage(0);
expect(tiff.ifdRequests.length).to.equal(1);
});
it('requesting last image only parses all IFDs', async () => {
const tiff = await GeoTIFF.fromSource(createSource(source));
await tiff.getImage(2);
// the image has 3 panes, so 2 is the index of the third image
expect(tiff.ifdRequests.length).to.equal(3);
});
it('requesting third image after manually parsing second yiels 2 ifdRequests', async () => {
const tiff = await GeoTIFF.fromSource(createSource(source));
const index = 1;
tiff.ifdRequests[index] = tiff.parseFileDirectoryAt(offsets[index]);
await tiff.getImage(index + 1);
// first image slot is empty so we filter out the Promises, of which there are two
expect(
tiff.ifdRequests.filter((ifdRequest) => ifdRequest instanceof Promise).length,
).to.equal(2);
});
it('should be able to manually set ifdRequests and readRasters', async () => {
const tiff = await GeoTIFF.fromSource(createSource(source));
tiff.ifdRequests = offsets.map((offset) => tiff.parseFileDirectoryAt(offset));
tiff.ifdRequests.forEach(async (_, i) => {
const image = await tiff.getImage(i);
image.readRasters();
});
});
});
describe('RGB-tests', () => {
const options = { window: [250, 250, 300, 300], interleave: true };
const comparisonRaster = (async () => {
const tiff = await GeoTIFF.fromSource(createSource('rgb.tiff'));
const image = await tiff.getImage();
return image.readRasters(options);
})();
// TODO: disabled, as in CI environment such images are not similar enough
// it('should work with CMYK files', async () => {
// const tiff = await GeoTIFF.fromSource(createSource('cmyk.tif'));
// await performRGBTest(tiff, options, comparisonRaster, 1);
// });
it('should work with YCbCr files', async () => {
const tiff = await GeoTIFF.fromSource(createSource('ycbcr.tif'));
await performRGBTest(tiff, options, comparisonRaster, 27);
});
it('should work with paletted files', async () => {
const tiff = await GeoTIFF.fromSource(createSource('rgb_paletted.tiff'));
await performRGBTest(tiff, options, comparisonRaster, 15);
});
it('should read into non-interleaved arrays if requested', async () => {
const tiff = await GeoTIFF.fromSource(createSource('rgb.tiff'));
const image = await tiff.getImage();
const data = await image.readRGB({ ...options, interleave: false });
expect(data).to.have.lengthOf(3);
expect(data[0]).to.have.lengthOf(50 * 50);
expect(data[1]).to.have.lengthOf(50 * 50);
expect(data[2]).to.have.lengthOf(50 * 50);
});
});
describe('Abort signal', () => {
const source = 'multi-channel.ome.tif';
it('Abort signal on readRasters throws exception', async () => {
const tiff = await GeoTIFF.fromSource(createSource(source));
const image = await tiff.getImage(0);
const abortController = new AbortController();
const { signal } = abortController;
abortController.abort();
try {
await image.readRasters({ signal });
} catch (e) {
expect(e.name).to.equal('AbortError');
}
});
it('Abort signal on readRGB returns fill value array', async () => {
const tiff = await GeoTIFF.fromSource(createSource('rgb_paletted.tiff'));
const image = await tiff.getImage();
const abortController = new AbortController();
const { signal } = abortController;
abortController.abort();
try {
await image.readRGB({ signal });
} catch (e) {
expect(e.name).to.equal('AbortError');
}
});
});
describe('RGBA-tests', () => {
const options = { window: [250, 250, 300, 300], interleave: true };
const comparisonRaster = (async () => {
const tiff = await GeoTIFF.fromSource(createSource('RGBA.tiff'));
const image = await tiff.getImage();
return image.readRasters(options);
})();
options.enableAlpha = true;
// TODO: disabled, as in CI environment such images are not similar enough
// it('should work with CMYK files', async () => {
// const tiff = await GeoTIFF.fromSource(createSource('cmyk.tif'));
// await performRGBTest(tiff, options, comparisonRaster, 1);
// });
it('should work with RGBA files', async () => {
const tiff = await GeoTIFF.fromSource(createSource('RGBA.tiff'));
await performRGBTest(tiff, options, comparisonRaster, 3);
});
});
describe('Geo metadata tests', async () => {
it('should be able to get the origin and offset of images using tie points and scale', async () => {
const tiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const image = await tiff.getImage();
expect(image.getResolution()).to.be.an('array');
expect(image.getOrigin()).to.be.an('array');
expect(image.getBoundingBox()).to.be.an('array');
});
it('should be able to get the origin and offset of images using model transformation', async () => {
const tiff = await GeoTIFF.fromSource(createSource('stripped.tiff'));
const image = await tiff.getImage();
expect(image.getResolution()).to.be.an('array');
expect(image.getOrigin()).to.be.an('array');
expect(image.getBoundingBox()).to.be.an('array');
expect(image.getGeoKeys()).to.have.property('GeographicTypeGeoKey');
expect(image.getGeoKeys().GeographicTypeGeoKey).to.equal(4326);
});
it('should be able to get the bounding box of skewed images', async () => {
const tiff = await GeoTIFF.fromSource(createSource('umbra_mount_yasur.tiff'));
const image = await tiff.getImage();
expect(image.getBoundingBox()).to.be.an('array');
expect(image.getBoundingBox()).to.be.deep.equal([336494.9320674397, 7839364.913043569, 337934.4836350695, 7840804.464611199]);
});
});
describe('GDAL_METADATA tests', async () => {
it('should parse stats for specific sample', async () => {
const tiff = await GeoTIFF.fromSource(
createSource('abetow-ERD2018-EBIRD_SCIENCE-20191109-a5cf4cb2_hr_2018_abundance_median.tiff'),
);
const image = await tiff.getImage();
const metadata = await image.getGDALMetadata(10);
expect(metadata).to.deep.equal({
STATISTICS_MAXIMUM: '7.2544522285461',
STATISTICS_MEAN: 'nan',
STATISTICS_MINIMUM: '0',
STATISTICS_STDDEV: 'nan',
});
});
it('should parse stats for single-band GeoTIFF', async () => {
const tiff = await GeoTIFF.fromSource(createSource('nt_20201024_f18_nrt_s.tif'));
const image = await tiff.getImage();
expect(await image.getGDALMetadata(), {}); // no top-level-stats
expect(await image.getGDALMetadata(0)).to.deep.equal({
STATISTICS_MAXIMUM: '100',
STATISTICS_MEAN: '28.560288669249',
STATISTICS_MINIMUM: '0',
STATISTICS_STDDEV: '39.349526064368',
});
});
it('should parse layer type', async () => {
const tiff = await GeoTIFF.fromSource(createSource('eu_pasture.tiff'));
const image = await tiff.getImage();
const metadata = await image.getGDALMetadata(0);
expect(metadata).to.deep.equal({
LAYER_TYPE: 'athematic',
});
});
it('should parse color interpretation', async () => {
const tiff = await GeoTIFF.fromSource(createSource('utm.tif'));
const image = await tiff.getImage();
const metadata = await image.getGDALMetadata(0);
expect(metadata).to.deep.equal({
COLORINTERP: 'Palette',
});
});
it('should parse stats for another single-band GeoTIFF', async () => {
const tiff = await GeoTIFF.fromSource(createSource('vestfold.tif'));
const image = await tiff.getImage();
const metadata = await image.getGDALMetadata(0);
expect(metadata).to.deep.equal({
STATISTICS_MAXIMUM: '332.6073328654',
STATISTICS_MEAN: '83.638959236148',
STATISTICS_MINIMUM: '18.103807449341',
STATISTICS_STDDEV: '69.590554367352',
});
});
it('should parse creation times', async () => {
const tiff = await GeoTIFF.fromSource(createSource('wind_direction.tif'));
const image = await tiff.getImage();
const metadata = await image.getGDALMetadata(0);
expect(metadata).to.deep.equal({
creationTime: '1497289465',
creationTimeString: '2017-06-12T17:44:25.466257Z',
name: 'Wind_Dir_SFC',
});
});
it('should parse top-level metadata when no sample is specified', async () => {
const tiff = await GeoTIFF.fromSource(createSource('wind_direction.tif'));
const image = await tiff.getImage();
const metadata = await image.getGDALMetadata();
expect(metadata).to.deep.equal({
DATUM: 'WGS84',
});
});
});
describe('COG tests', async () => {
it('should parse the header ghost area when present', async () => {
const tiff = await GeoTIFF.fromSource(createSource('cog.tiff'));
const ghostValues = await tiff.getGhostValues();
expect(ghostValues).to.deep.equal({
GDAL_STRUCTURAL_METADATA_SIZE: '000140 bytes',
LAYOUT: 'IFDS_BEFORE_DATA',
BLOCK_ORDER: 'ROW_MAJOR',
BLOCK_LEADER: 'SIZE_AS_UINT4',
BLOCK_TRAILER: 'LAST_4_BYTES_REPEATED',
KNOWN_INCOMPATIBLE_EDITION: 'NO',
});
});
it('should return null, when no ghost area is present', async () => {
const tiff = await GeoTIFF.fromSource(createSource('initial.tiff'));
const ghostValues = await tiff.getGhostValues();
expect(ghostValues).to.be.null;
});
});
describe('fillValue', async () => {
it('should fill pixels outside the image area (to the left and above)', async () => {
const tiff = await GeoTIFF.fromSource(createSource('cog.tiff'));
const image = await tiff.getImage(0);
const data = await image.readRasters({ window: [-1, -1, 0, 0], fillValue: 42 });
expect(data).to.have.lengthOf(15);
for (const band of data) {
expect(band).to.have.lengthOf(1);
expect(band).to.deep.equal(new Uint16Array([42]));
}
});
it('should fill pixels outside the image area (to the right and below)', async () => {
const tiff = await GeoTIFF.fromSource(createSource('cog.tiff'));
const image = await tiff.getImage(0);
const data = await image.readRasters({ window: [512, 512, 513, 513], fillValue: 42 });
expect(data).to.have.lengthOf(15);
for (const band of data) {
expect(band).to.have.lengthOf(1);
expect(band).to.deep.equal(new Uint16Array([42]));
}
});
it('should fill areas in overview tiles outside the image extent (left, with worker pool)', async () => {
const tiff = await GeoTIFF.fromSource(createSource('cog.tiff'));
const image = await tiff.getImage(1);
const data = await image.readRasters({ window: [269, 0, 270, 1], fillValue: 42, pool });
expect(data).to.have.lengthOf(15);
for (const band of data) {
expect(band).to.have.lengthOf(1);
expect(band).to.deep.equal(new Uint16Array([42]));
}
}).timeout(10000);
it('should fill areas in overview tiles outside the image extent (below, with worker pool)', async () => {
const tiff = await GeoTIFF.fromSource(createSource('cog.tiff'));
const image = await tiff.getImage(1);
const data = await image.readRasters({ window: [0, 224, 1, 225], fillValue: 42, pool });
expect(data).to.have.lengthOf(15);
for (const band of data) {
expect(band).to.have.lengthOf(1);
expect(band).to.deep.equal(new Uint16Array([42]));
}
}).timeout(10000);
});
describe('64 bit tests', () => {
it('DataView64 uint64 tests', () => {
const littleEndianBytes = new Uint8Array([
// ((2 ** 53) - 1)
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0x1f,
0x00,
// 2 ** 64 - 1
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
]);
const littleEndianView = new DataView64(littleEndianBytes.buffer);
const bigEndianBytes = new Uint8Array([
// ((2 ** 53) - 1)
// left
0x00,
0x1f,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
// 2 ** 64 - 1
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
]);
const bigEndianView = new DataView64(bigEndianBytes.buffer);
const readLittleEndianBytes = littleEndianView.getUint64(0, true);
const readBigEndianBytes = bigEndianView.getUint64(0, false);
expect(readLittleEndianBytes).to.equal((2 ** 53) - 1);
expect(readBigEndianBytes).to.equal((2 ** 53) - 1);
expect(() => {
littleEndianView.getUint64(8, true);
}).to.throw();
expect(() => {
bigEndianView.getUint64(8, false);
}).to.throw();
});
it('DataView64 negative int64 tests', () => {
const littleEndianBytes = new Uint8Array([
// -(2 ** 32 - 1)
// left
0x01,
0x00,
0x00,
0x00,
// right
0x00,
0x00,
0xf0,
0xff,
]);
const littleEndianView = new DataView64(littleEndianBytes.buffer);
const bigEndianBytes = new Uint8Array([
// -(2 ** 32 - 1)
// left
0xff,
0xf0,
0x00,
0x00,
// right
0x00,
0x00,
0x00,
0x01,
]);
const bigEndianView = new DataView64(bigEndianBytes.buffer);
const readLittleEndianBytes = littleEndianView.getInt64(0, true);
const readBigEndianBytes = bigEndianView.getInt64(0, false);
expect(readLittleEndianBytes).to.equal(-((2 ** 52) - 1));
expect(readBigEndianBytes).to.equal(-((2 ** 52) - 1));
});
it('DataView64 positive int64 tests', () => {
const littleEndianBytes = new Uint8Array([
// ((2 ** 52) - 1)
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0x0f,
0x00,
]);
const littleEndianView = new DataView64(littleEndianBytes.buffer);
const bigEndianBytes = new Uint8Array([
// ((2 ** 52) - 1)
// left
0x00,
0x0f,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
]);
const bigEndianView = new DataView64(bigEndianBytes.buffer);
const readLittleEndianBytes = littleEndianView.getInt64(0, true);
const readBigEndianBytes = bigEndianView.getInt64(0, false);
expect(readLittleEndianBytes).to.equal((2 ** 52) - 1);
expect(readBigEndianBytes).to.equal((2 ** 52) - 1);
});
it('DataSlice positive int64 tests', () => {
const littleEndianBytes = new Uint8Array([
// ((2 ** 52) - 1)
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0x0f,
0x00,
]);
const littleEndianSlice = new DataSlice(
littleEndianBytes.buffer,
0,
true,
true,
);
const bigEndianBytes = new Uint8Array([
// ((2 ** 52) - 1)
// left
0x00,
0x0f,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
]);
const bigEndianSlice = new DataSlice(bigEndianBytes.buffer, 0, false, true);
const readLittleEndianBytes = littleEndianSlice.readInt64(0, true);
const readBigEndianBytes = bigEndianSlice.readInt64(0, false);
expect(readLittleEndianBytes).to.equal((2 ** 52) - 1);
expect(readBigEndianBytes).to.equal((2 ** 52) - 1);
});
it('DataSlice negative int64 tests', () => {
const littleEndianBytes = new Uint8Array([
// -(2 ** 32 - 1)
// left
0x01,
0x00,
0x00,
0x00,
// right
0x00,
0x00,
0xf0,
0xff,
]);
const littleEndianSlice = new DataSlice(
littleEndianBytes.buffer,
0,
true,
true,
);
const bigEndianBytes = new Uint8Array([
// -(2 ** 32 - 1)
// left
0xff,
0xf0,
0x00,
0x00,
// right
0x00,
0x00,
0x00,
0x01,
]);
const bigEndianSlice = new DataSlice(bigEndianBytes.buffer, 0, false, true);
const readLittleEndianBytes = littleEndianSlice.readInt64(0, true);
const readBigEndianBytes = bigEndianSlice.readInt64(0, false);
expect(readLittleEndianBytes).to.equal(-((2 ** 52) - 1));
expect(readBigEndianBytes).to.equal(-((2 ** 52) - 1));
});
it('DataSlice uint64 bit tests', () => {
const littleEndianBytes = new Uint8Array([
// ((2 ** 53) - 1)
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0x1f,
0x00,
// 2 ** 64 - 1
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
]);
const littleEndianSlice = new DataSlice(
littleEndianBytes.buffer,
0,
true,
true,
);
const bigEndianBytes = new Uint8Array([
// ((2 ** 53) - 1)
// left
0x00,
0x1f,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
// 2 ** 64 - 1
// left
0xff,
0xff,
0xff,
0xff,
// right
0xff,
0xff,
0xff,
0xff,
]);
const bigEndianSlice = new DataSlice(bigEndianBytes.buffer, 0, false, true);
const readLittleEndianBytes = littleEndianSlice.readOffset(0);
const readBigEndianBytes = bigEndianSlice.readOffset(0);
expect(readLittleEndianBytes).to.equal((2 ** 53) - 1);
expect(readBigEndianBytes).to.equal((2 ** 53) - 1);
expect(() => {
littleEndianSlice.readOffset(8);
}).to.throw();
expect(() => {
bigEndianSlice.readOffset(8);
}).to.throw();
});
});
describe('writeTests', () => {
it('should write pixel values and metadata with sensible defaults', async () => {
const originalValues = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const metadata = {
height: 3,
width: 3,
};
const newGeoTiffAsBinaryData = await writeArrayBuffer(originalValues, metadata);
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData);
const image = await newGeoTiff.getImage();
const rasters = await image.readRasters();
const newValues = toArrayRecursively(rasters[0]);
expect(
JSON.stringify(newValues.slice(0, -1)),
).to.equal(JSON.stringify(originalValues.slice(0, -1)));
const geoKeys = image.getGeoKeys();
expect(geoKeys).to.be.an('object');
expect(geoKeys.GTModelTypeGeoKey).to.equal(2);
expect(geoKeys.GeographicTypeGeoKey).to.equal(4326);
expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84');
const { fileDirectory } = image;
expect(normalize(fileDirectory.BitsPerSample)).to.equal(normalize([8]));
expect(fileDirectory.Compression).to.equal(1);
expect(fileDirectory.GeoAsciiParams).to.equal('WGS 84\u0000');
expect(fileDirectory.ImageLength).to.equal(3);
expect(fileDirectory.ImageWidth).to.equal(3);
expect(normalize(fileDirectory.ModelPixelScale)).to.equal(normalize(metadata.ModelPixelScale));
expect(normalize(fileDirectory.ModelTiepoint)).to.equal(normalize(metadata.ModelTiepoint));
expect(fileDirectory.PhotometricInterpretation).to.equal(1);
expect(fileDirectory.PlanarConfiguration).to.equal(1);
expect(normalize(fileDirectory.StripOffsets)).to.equal('[1000]'); // hardcoded at 2000 now rather than calculated
expect(normalize(fileDirectory.SampleFormat)).to.equal(normalize([1]));
expect(fileDirectory.SamplesPerPixel).to.equal(1);
expect(normalize(fileDirectory.RowsPerStrip)).to.equal(normalize(3));
expect(normalize(fileDirectory.StripByteCounts)).to.equal(normalize(metadata.StripByteCounts));
});
it('should write rgb data with sensible defaults', async () => {
const originalRed = [
[255, 255, 255],
[0, 0, 0],
[0, 0, 0],
];
const originalGreen = [
[0, 0, 0],
[255, 255, 255],
[0, 0, 0],
];
const originalBlue = [
[0, 0, 0],
[0, 0, 0],
[255, 255, 255],
];
const originalValues = [originalRed, originalGreen, originalBlue];
const metadata = {
height: 3,
width: 3,
};
const newGeoTiffAsBinaryData = await writeArrayBuffer(originalValues, metadata);
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData);
const image = await newGeoTiff.getImage();
const newValues = await image.readRasters();
const red = chunk(newValues[0], 3);
const green = chunk(newValues[1], 3);
const blue = chunk(newValues[2], 3);
expect(normalize(red)).to.equal(normalize(originalRed));
expect(normalize(green)).to.equal(normalize(originalGreen));
expect(normalize(blue)).to.equal(normalize(originalBlue));
const geoKeys = image.getGeoKeys();
expect(geoKeys).to.be.an('object');
expect(geoKeys.GTModelTypeGeoKey).to.equal(2);
expect(geoKeys.GeographicTypeGeoKey).to.equal(4326);
expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84');
const { fileDirectory } = image;
expect(normalize(fileDirectory.BitsPerSample)).to.equal(normalize([8, 8, 8]));
expect(fileDirectory.Compression).to.equal(1);
expect(fileDirectory.GeoAsciiParams).to.equal('WGS 84\u0000');
expect(fileDirectory.ImageLength).to.equal(3);
expect(fileDirectory.ImageWidth).to.equal(3);
expect(normalize(fileDirectory.ModelPixelScale)).to.equal(normalize(metadata.ModelPixelScale));
expect(normalize(fileDirectory.ModelTiepoint)).to.equal(normalize(metadata.ModelTiepoint));
expect(fileDirectory.PhotometricInterpretation).to.equal(2);
expect(fileDirectory.PlanarConfiguration).to.equal(1);
expect(normalize(fileDirectory.StripOffsets)).to.equal('[1000]'); // hardcoded at 2000 now rather than calculated
expect(normalize(fileDirectory.SampleFormat)).to.equal(normalize([1, 1, 1]));
expect(fileDirectory.SamplesPerPixel).to.equal(3);
expect(normalize(fileDirectory.RowsPerStrip)).to.equal(normalize(3));
expect(toArray(fileDirectory.StripByteCounts).toString()).to.equal('27');
});
it('should write flattened pixel values', async () => {
const originalValues = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const height = 3;
const width = 3;
const metadata = getMockMetaData(height, width);
const newGeoTiffAsBinaryData = await writeArrayBuffer(originalValues, metadata);
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData);
const image = await newGeoTiff.getImage();
const rasters = await image.readRasters();
const newValues = toArrayRecursively(rasters[0]);
expect(
JSON.stringify(newValues.slice(0, -1)),
).to.equal(JSON.stringify(originalValues.slice(0, -1)));
});
it('should write pixel values in two dimensions', async () => {
const originalValues = [
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
],
];
const height = 3;
const width = 3;
const metadata = getMockMetaData(height, width);
const newGeoTiffAsBinaryData = await writeArrayBuffer(originalValues, metadata);
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData);
const image = await newGeoTiff.getImage();
const newValues = await image.readRasters();
const newValuesReshaped = toArray(newValues).map((band) => {
return chunk(band, width);
});
expect(
JSON.stringify(newValuesReshaped.slice(0, -1)),
).to.equal(JSON.stringify(originalValues.slice(0, -1)));
});
it('should write metadata correctly', async () => {
const height = 12;
const width = 12;
const originalValues = range(height * width);
const metadata = getMockMetaData(height, width);
const newGeoTiffAsBinaryData = await writeArrayBuffer(originalValues, metadata);
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData);
const image = await newGeoTiff.getImage();
const rasters = await image.readRasters();
const newValues = toArrayRecursively(rasters[0]);
expect(
JSON.stringify(newValues.slice(0, -1)),
).to.equal(JSON.stringify(originalValues.slice(0, -1)));
const { fileDirectory } = image;
expect(normalize(fileDirectory.BitsPerSample)).to.equal(normalize([8]));
expect(fileDirectory.Compression).to.equal(1);
expect(fileDirectory.GeoAsciiParams).to.equal('WGS 84\u0000');
expect(fileDirectory.ImageLength).to.equal(height);
expect(fileDirectory.ImageWidth).to.equal(width);
expect(normalize(fileDirectory.ModelPixelScale)).to.equal(normalize(metadata.ModelPixelScale));
expect(normalize(fileDirectory.ModelTiepoint)).to.equal(normalize(metadata.ModelTiepoint));
expect(fileDirectory.PhotometricInterpretation).to.equal(2);
expect(fileDirectory.PlanarConfiguration).to.equal(1);
expect(normalize(fileDirectory.StripOffsets)).to.equal('[1000]'); // hardcoded at 2000 now rather than calculated
expect(normalize(fileDirectory.SampleFormat)).to.equal(normalize([1]));
expect(fileDirectory.SamplesPerPixel).to.equal(1);
expect(normalize(fileDirectory.RowsPerStrip)).to.equal(normalize(height));
expect(normalize(fileDirectory.StripByteCounts)).to.equal(normalize(metadata.StripByteCounts));
expect(fileDirectory.GDAL_NODATA).to.equal('0\u0000');
});
});
describe('BlockedSource Test', () => {
const blockedSource = new BlockedSource(null, { blockSize: 2 });
it('Groups only contiguous blocks as one group', () => {
const groups = blockedSource.groupBlocks([2, 0, 1, 3]);
expect(groups.length).to.equal(1);
const [group] = groups;
expect(group.blockIds.length).to.equal(4);
expect(group.offset).to.equal(0);
expect(group.length).to.equal(8);
});
it('Groups two non-contiguous blocks as two groups', () => {
const groups = blockedSource.groupBlocks([0, 1, 7, 2, 8, 3]);
expect(groups.length).to.equal(2);
const [group1, group2] = groups;
expect(group1.blockIds.length).to.equal(4);
expect(group1.blockIds).to.deep.equal([0, 1, 2, 3]);
expect(group1.offset).to.equal(0);
expect(group1.length).to.equal(8);
expect(group2.blockIds.length).to.equal(2);
expect(group2.offset).to.equal(14);
expect(group2.length).to.equal(4);
expect(group2.blockIds).to.deep.equal([7, 8]);
});
});