UNPKG

geotiff

Version:

GeoTIFF image decoding in JavaScript

800 lines (731 loc) 29.1 kB
/** @module geotiff */ import GeoTIFFImage from './geotiffimage.js'; import DataView64 from './dataview64.js'; import DataSlice from './dataslice.js'; import Pool from './pool.js'; import { makeRemoteSource, makeCustomSource } from './source/remote.js'; import { makeBufferSource } from './source/arraybuffer.js'; import { makeFileReaderSource } from './source/filereader.js'; import { makeFileSource } from './source/file.js'; import { BaseClient, BaseResponse } from './source/client/base.js'; import { ImageFileDirectoryParser } from './imagefiledirectory.js'; import { fieldTypes, getFieldTypeSize, registerTag } from './globals.js'; import { writeGeotiff } from './geotiffwriter.js'; import * as globals from './globals.js'; import * as rgb from './rgb.js'; import { getDecoder, addDecoder } from './compression/index.js'; import { setLogger } from './logging.js'; /** @import { BaseSource } from './source/basesource.js' */ export { globals }; export { registerTag }; export { rgb }; export { default as BaseDecoder } from './compression/basedecoder.js'; export { getDecoder, addDecoder }; export { setLogger }; export { ImageFileDirectory } from './imagefiledirectory.js'; /** * @typedef {Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | Float32Array | Float64Array} * TypedArray */ /** * @typedef {{ height:number, width: number }} Dimensions */ /** * The autogenerated docs are a little confusing here. The effective type is: * * `TypedArray & { height: number; width: number}` * @typedef {TypedArray & Dimensions} TypedArrayWithDimensions */ /** * The autogenerated docs are a little confusing here. The effective type is: * * `TypedArray[] & { height: number; width: number}` * @typedef {TypedArray[] & Dimensions} TypedArrayArrayWithDimensions */ /** * @typedef {Object} GeotiffWriterMetadata * @property {number | number[]} [ImageWidth] * @property {number | number[]} [ImageLength] * @property {number} [width] * @property {number} [height] * @property {number | number[]} [BitsPerSample] * @property {number | number[]} [Compression] * @property {number | number[]} [PlanarConfiguration] * @property {number | number[]} [ExtraSamples] * @property {number | number[]} [PhotometricInterpretation] * @property {number | number[]} [SamplesPerPixel] * @property {number | number[]} [StripByteCounts] * @property {number[]} [ModelPixelScale] * @property {number[]} [ModelTransformation] * @property {number[]} [ModelTiepoint] * @property {number[]} [GeoKeyDirectory] * @property {string} [GeoAsciiParams] * @property {number[]} [GeoDoubleParams] * @property {number | number[]} [Orientation] * @property {number | number[]} [ResolutionUnit] * @property {number | number[]} [XPosition] * @property {number | number[]} [YPosition] * @property {number | number[]} [RowsPerStrip] * @property {number[]} [SampleFormat] * @property {number | number[]} [TileWidth] * @property {number | number[]} [TileLength] * @property {number[]} [TileOffsets] * @property {number[]} [TileByteCounts] * @property {string} [GDAL_NODATA] * @property {number | number[]} [GeographicTypeGeoKey] * @property {number | number[]} [ProjectedCSTypeGeoKey] * @property {string} [GeogCitationGeoKey] * @property {string} [GTCitationGeoKey] * @property {number | number[]} [GTModelTypeGeoKey] * @property {number | number[]} [GTRasterTypeGeoKey] */ /** * The autogenerated docs are a little confusing here. The effective type is: * * `(TypedArray | TypedArray[]) & { height: number; width: number}` * @typedef {TypedArrayWithDimensions | TypedArrayArrayWithDimensions} ReadRasterResult */ /** * @typedef {Object} DecoderWorker * Use the {@link Pool.bindParameters} method to get a decoder worker for * a specific compression and its parameters. * * @property {(buffer: ArrayBufferLike) => Promise<ArrayBufferLike>} decode * A function that takes a compressed buffer and returns a promise resolving to the decoded buffer. */ /** * @typedef {Object} ReadRastersOptions * @property {Array<number>} [window] the subset to read data from in pixels. Whole window if not specified. * @property {Array<number>} [samples] the selection of samples to read from. Default is all samples. * All samples if not specified. * @property {Pool|null} [pool=null] The optional decoder pool to use. * @property {number} [width] The desired width of the output. When the width is not the * same as the images, resampling will be performed. * @property {number} [height] The desired height of the output. When the width is not the * same as the images, resampling will be performed. * @property {string} [resampleMethod='nearest'] The desired resampling method. * @property {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @property {number|number[]} [fillValue] The value to use for parts of the image * outside of the images extent. When multiple samples are requested and `interleave` is * `false`, an array of fill values can be passed. * @property {boolean|true|false} [interleave] whether the data shall be read * in one single array or separate arrays. */ /** * @typedef {Object} ReadRGBOptions * @property {Array<number>} [window] the subset to read data from in pixels. Whole window if not specified. * @property {Pool|null} [pool=null] The optional decoder pool to use. * @property {number} [width] The desired width of the output. When the width is no the * same as the images, resampling will be performed. * @property {number} [height] The desired height of the output. When the width is no the * same as the images, resampling will be performed. * @property {string} [resampleMethod='nearest'] The desired resampling method. * @property {boolean} [enableAlpha=false] Enable reading alpha channel if present. * @property {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @property {boolean|true|false} [interleave] whether the data shall be read * in one single array or separate arrays. */ /** * @typedef {Object} BlockedSourceOptions * @property {number} [blockSize] Block size for a BlockedSource. * @property {number} [cacheSize=100] The number of blocks to cache. */ /** * @typedef {Object} RemoteSourceOptions * @property {Record<string, string>} [headers={}] Additional headers to add to each request * @property {number} [maxRanges=0] Maximum number of ranges to request in a single HTTP request. 0 means no multi-range requests. * @property {boolean} [allowFullFile=false] Whether to allow full file responses when requesting ranges * @property {boolean} [forceXHR=false] When the Fetch API would be used, force using XMLHttpRequest instead. */ /** * @overload * @param {DataSlice} dataSlice * @param {0x0002} fieldType * @param {number} count * @param {number} offset * @returns {string} */ /** * @param {DataSlice} dataSlice * @param {import('./globals.js').FieldType} fieldType * @param {number} count * @param {number} offset * @returns {TypedArray|Array<number>|string} */ function getValues(dataSlice, fieldType, count, offset) { /** @type {TypedArray|Array<number>|null} */ let values = null; let readMethod = null; const fieldTypeLength = getFieldTypeSize(fieldType); switch (fieldType) { case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.UNDEFINED: values = new Uint8Array(count); readMethod = dataSlice.readUint8; break; case fieldTypes.SBYTE: values = new Int8Array(count); readMethod = dataSlice.readInt8; break; case fieldTypes.SHORT: values = new Uint16Array(count); readMethod = dataSlice.readUint16; break; case fieldTypes.SSHORT: values = new Int16Array(count); readMethod = dataSlice.readInt16; break; case fieldTypes.LONG: case fieldTypes.IFD: values = new Uint32Array(count); readMethod = dataSlice.readUint32; break; case fieldTypes.SLONG: values = new Int32Array(count); readMethod = dataSlice.readInt32; break; case fieldTypes.LONG8: case fieldTypes.IFD8: values = new Array(count); readMethod = dataSlice.readUint64; break; case fieldTypes.SLONG8: values = new Array(count); readMethod = dataSlice.readInt64; break; case fieldTypes.RATIONAL: values = new Uint32Array(count * 2); readMethod = dataSlice.readUint32; break; case fieldTypes.SRATIONAL: values = new Int32Array(count * 2); readMethod = dataSlice.readInt32; break; case fieldTypes.FLOAT: values = new Float32Array(count); readMethod = dataSlice.readFloat32; break; case fieldTypes.DOUBLE: values = new Float64Array(count); readMethod = dataSlice.readFloat64; break; default: // will throw below } if (values === null || readMethod === null) { throw new RangeError(`Invalid field type: ${fieldType}`); } // normal fields if (!(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) { for (let i = 0; i < count; ++i) { values[i] = readMethod.call( dataSlice, offset + (i * fieldTypeLength), ); } } else { // RATIONAL or SRATIONAL for (let i = 0; i < count; i += 2) { values[i] = readMethod.call( dataSlice, offset + (i * fieldTypeLength), ); values[i + 1] = readMethod.call( dataSlice, offset + ((i * fieldTypeLength) + 4), ); } } if (fieldType === fieldTypes.ASCII) { return new TextDecoder('utf-8').decode(/** @type {Uint8Array} */ (values)); } return values; } /** * Error class for cases when an IFD index was requested, that does not exist * in the file. */ class GeoTIFFImageIndexError extends Error { /** * @param {number} index */ constructor(index) { super(`No image at index ${index}`); this.index = index; } } class GeoTIFFBase { /** * @param {number} [_index=0] the index of the image to return. * @returns {Promise<GeoTIFFImage>} the image at the given index */ async getImage(_index = 0) { throw new Error('Not implemented'); } /** * @returns {Promise<number>} the number of internal subfile images */ async getImageCount() { throw new Error('Not implemented'); } /** * @typedef {Object} ReadRastersWindowOptions * @property {number} [resX] desired Y resolution (world units per pixel) * @property {number} [resY] desired X resolution (world units per pixel) * @property {Array<number>} [bbox] the subset to read data from in * geographical coordinates. Whole image if not specified. */ /** * (experimental) Reads raster data from the best fitting image. This function uses * the image with the lowest resolution that is still a higher resolution than the * requested resolution. * When specified, the `bbox` option is translated to the `window` option and the * `resX` and `resY` to `width` and `height` respectively. * Then, the [readRasters]{@link GeoTIFFImage#readRasters} method of the selected * image is called and the result returned. * @see GeoTIFFImage.readRasters * @param {ReadRastersOptions & ReadRastersWindowOptions} options optional parameters * @returns {Promise<ReadRasterResult>} the decoded array(s), with `height` and `width`, as a promise */ async readRasters(options = {}) { const { window: imageWindow, width, height } = options; let { resX, resY, bbox } = options; const firstImage = await this.getImage(); let usedImage = firstImage; const imageCount = await this.getImageCount(); const imgBBox = firstImage.getBoundingBox(); if (imageWindow && bbox) { throw new Error('Both "bbox" and "window" passed.'); } // if width/height is passed, transform it to resolution if (width || height) { // if we have an image window (pixel coordinates), transform it to a BBox // using the origin/resolution of the first image. if (imageWindow) { const [oX, oY] = firstImage.getOrigin(); const [rX, rY] = firstImage.getResolution(); bbox = [ oX + (imageWindow[0] * rX), oY + (imageWindow[1] * rY), oX + (imageWindow[2] * rX), oY + (imageWindow[3] * rY), ]; } // if we have a bbox (or calculated one) const usedBBox = bbox || imgBBox; if (width) { if (resX) { throw new Error('Both width and resX passed'); } resX = (usedBBox[2] - usedBBox[0]) / width; } if (height) { if (resY) { throw new Error('Both width and resY passed'); } resY = (usedBBox[3] - usedBBox[1]) / height; } } // if resolution is set or calculated, try to get the image with the worst acceptable resolution if (resX || resY) { const allImages = []; for (let i = 0; i < imageCount; ++i) { const image = await this.getImage(i); const subfileType = image.fileDirectory.getValue('SubfileType'); const newSubfileType = image.fileDirectory.getValue('NewSubfileType'); if (i === 0 || subfileType === 2 || (newSubfileType || 0) & 1) { allImages.push(image); } } allImages.sort((a, b) => a.getWidth() - b.getWidth()); for (let i = 0; i < allImages.length; ++i) { const image = allImages[i]; const imgResX = (imgBBox[2] - imgBBox[0]) / image.getWidth(); const imgResY = (imgBBox[3] - imgBBox[1]) / image.getHeight(); usedImage = image; if ((resX && resX > imgResX) || (resY && resY > imgResY)) { break; } } } let wnd = imageWindow; if (bbox) { const [oX, oY] = firstImage.getOrigin(); const [imageResX, imageResY] = usedImage.getResolution(firstImage); wnd = [ Math.round((bbox[0] - oX) / imageResX), Math.round((bbox[1] - oY) / imageResY), Math.round((bbox[2] - oX) / imageResX), Math.round((bbox[3] - oY) / imageResY), ]; wnd = [ Math.min(wnd[0], wnd[2]), Math.min(wnd[1], wnd[3]), Math.max(wnd[0], wnd[2]), Math.max(wnd[1], wnd[3]), ]; } return usedImage.readRasters({ ...options, window: wnd }); } } /** * @typedef {Object} GeoTIFFOptions * @property {boolean} [cache=false] whether or not decoded tiles shall be cached. */ /** * The abstraction for a whole GeoTIFF file. */ class GeoTIFF extends GeoTIFFBase { /** * @constructor * @param {BaseSource} source The datasource to read from. * @param {boolean} littleEndian Whether the image uses little endian. * @param {boolean} bigTiff Whether the image uses bigTIFF conventions. * @param {number} firstIFDOffset The numeric byte-offset from the start of the image * to the first IFD. * @param {GeoTIFFOptions} [options] further options. */ constructor(source, littleEndian, bigTiff, firstIFDOffset, options = {}) { super(); this.source = source; this.parser = new ImageFileDirectoryParser(source, littleEndian, bigTiff, false); this.littleEndian = littleEndian; this.bigTiff = bigTiff; this.firstIFDOffset = firstIFDOffset; this.cache = options.cache || false; /** @type {Array<Promise<import('./imagefiledirectory.js').ImageFileDirectory> | undefined>} */ this.ifdRequests = []; /** @type {Record<string, unknown>|null} */ this.ghostValues = null; } /** * @param {number} offset * @param {number} [size] * @returns {Promise<DataSlice>} */ async getSlice(offset, size) { const fallbackSize = this.bigTiff ? 4048 : 1024; return new DataSlice( (await this.source.fetch([{ offset, length: typeof size !== 'undefined' ? size : fallbackSize, }]))[0], offset, this.littleEndian, this.bigTiff, ); } /** * @param {number} index * @return {Promise<import('./imagefiledirectory.js').ImageFileDirectory>} */ async requestIFD(index) { // see if we already have that IFD index requested. if (this.ifdRequests[index]) { // attach to an already requested IFD return this.ifdRequests[index]; } else if (index === 0) { // special case for index 0 this.ifdRequests[index] = this.parser.parseFileDirectoryAt(this.firstIFDOffset); return this.ifdRequests[index]; } else if (!this.ifdRequests[index - 1]) { // if the previous IFD was not yet loaded, load that one first // this is the recursive call. try { this.ifdRequests[index - 1] = this.requestIFD(index - 1); } catch (e) { // if the previous one already was an index error, rethrow // with the current index if (e instanceof GeoTIFFImageIndexError) { throw new GeoTIFFImageIndexError(index); } // rethrow anything else throw e; } } // if the previous IFD was loaded, we can finally fetch the one we are interested in. // we need to wrap this in an IIFE, otherwise this.ifdRequests[index] would be delayed this.ifdRequests[index] = (async () => { const previousPromise = this.ifdRequests[index - 1]; if (!previousPromise) { throw new Error('Previous IFD request missing'); } const previousIfd = await previousPromise; if (previousIfd.nextIFDByteOffset === 0) { throw new GeoTIFFImageIndexError(index); } return this.parser.parseFileDirectoryAt(previousIfd.nextIFDByteOffset); })(); return this.ifdRequests[index]; } /** * Get the n-th internal subfile of an image. By default, the first is returned. * * @param {number} [index=0] the index of the image to return. * @returns {Promise<GeoTIFFImage>} the image at the given index */ async getImage(index = 0) { return new GeoTIFFImage( await this.requestIFD(index), this.littleEndian, this.cache, this.source, ); } /** * Returns the count of the internal subfiles. * * @returns {Promise<number>} the number of internal subfile images */ async getImageCount() { let index = 0; // loop until we run out of IFDs let hasNext = true; while (hasNext) { try { await this.requestIFD(index); ++index; } catch (e) { if (e instanceof GeoTIFFImageIndexError) { hasNext = false; } else { throw e; } } } return index; } /** * Get the values of the COG ghost area as a parsed map. * See https://gdal.org/drivers/raster/cog.html#header-ghost-area for reference * @returns {Promise<Record<string, unknown>|null>} the parsed ghost area or null, if no such area was found */ async getGhostValues() { const offset = this.bigTiff ? 16 : 8; if (this.ghostValues !== null) { return this.ghostValues; } const detectionString = 'GDAL_STRUCTURAL_METADATA_SIZE='; const heuristicAreaSize = detectionString.length + 100; let slice = await this.getSlice(offset, heuristicAreaSize); if (detectionString === getValues(slice, fieldTypes.ASCII, detectionString.length, offset)) { const valuesString = getValues(slice, fieldTypes.ASCII, heuristicAreaSize, offset); const firstLine = valuesString.split('\n')[0]; const metadataSize = Number(firstLine.split('=')[1].split(' ')[0]) + firstLine.length; if (metadataSize > heuristicAreaSize) { slice = await this.getSlice(offset, metadataSize); } const fullString = getValues(slice, fieldTypes.ASCII, metadataSize, offset); /** @type {Record<string, unknown>} */ const ghostValues = {}; fullString .split('\n') .filter((line) => line.length > 0) .map((line) => line.split('=')) .forEach(([key, value]) => { ghostValues[key] = value; }); this.ghostValues = ghostValues; } return this.ghostValues; } /** * Parse a (Geo)TIFF file from the given source. * * @param {BaseSource} source The source of data to parse from. * @param {GeoTIFFOptions} [options] Additional options. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted */ static async fromSource(source, options, signal) { const headerData = (await source.fetch([{ offset: 0, length: 1024 }], signal))[0]; const dataView = new DataView64(headerData); const BOM = dataView.getUint16(0, false); let littleEndian; if (BOM === 0x4949) { littleEndian = true; } else if (BOM === 0x4D4D) { littleEndian = false; } else { throw new TypeError('Invalid byte order value.'); } const magicNumber = dataView.getUint16(2, littleEndian); let bigTiff; if (magicNumber === 42) { bigTiff = false; } else if (magicNumber === 43) { bigTiff = true; const offsetByteSize = dataView.getUint16(4, littleEndian); if (offsetByteSize !== 8) { throw new Error('Unsupported offset byte-size.'); } } else { throw new TypeError('Invalid magic number.'); } const firstIFDOffset = bigTiff ? dataView.getUint64(8, littleEndian) : dataView.getUint32(4, littleEndian); return new GeoTIFF(source, littleEndian, bigTiff, firstIFDOffset, options); } /** * Closes the underlying file buffer * N.B. After the GeoTIFF has been completely processed it needs * to be closed but only if it has been constructed from a file. */ close() { if (typeof this.source.close === 'function') { return this.source.close(); } return false; } } export { GeoTIFF }; export default GeoTIFF; /** * Wrapper for GeoTIFF files that have external overviews. * @augments GeoTIFFBase */ class MultiGeoTIFF extends GeoTIFFBase { /** * Construct a new MultiGeoTIFF from a main and several overview files. * @param {GeoTIFF} mainFile The main GeoTIFF file. * @param {GeoTIFF[]} overviewFiles An array of overview files. */ constructor(mainFile, overviewFiles) { super(); this.mainFile = mainFile; this.overviewFiles = overviewFiles; this.imageFiles = [mainFile].concat(overviewFiles); this.fileDirectoriesPerFile = null; this.fileDirectoriesPerFileParsing = null; this.imageCount = null; } async parseFileDirectoriesPerFile() { const requests = [this.mainFile.parser.parseFileDirectoryAt(this.mainFile.firstIFDOffset)] .concat(this.overviewFiles.map((file) => file.parser.parseFileDirectoryAt(file.firstIFDOffset))); this.fileDirectoriesPerFile = await Promise.all(requests); return this.fileDirectoriesPerFile; } /** * Get the n-th internal subfile of an image. By default, the first is returned. * * @param {number} [index=0] the index of the image to return. * @returns {Promise<GeoTIFFImage>} the image at the given index */ async getImage(index = 0) { // Initialize this.imageCounts if not yet done await this.getImageCount(); if (!this.imageCounts) { throw new Error('Image counts not available'); } await this.parseFileDirectoriesPerFile(); let visited = 0; let relativeIndex = 0; for (let i = 0; i < this.imageFiles.length; i++) { const imageFile = this.imageFiles[i]; for (let ii = 0; ii < this.imageCounts[i]; ii++) { if (index === visited) { return new GeoTIFFImage( await imageFile.requestIFD(relativeIndex), imageFile.littleEndian, imageFile.cache, imageFile.source, ); } visited++; relativeIndex++; } relativeIndex = 0; } throw new RangeError('Invalid image index'); } /** * Returns the count of the internal subfiles. * * @returns {Promise<number>} the number of internal subfile images */ async getImageCount() { if (this.imageCount !== null) { return this.imageCount; } const requests = [this.mainFile.getImageCount()] .concat(this.overviewFiles.map((file) => file.getImageCount())); this.imageCounts = await Promise.all(requests); this.imageCount = this.imageCounts.reduce((count, ifds) => count + ifds, 0); return this.imageCount; } } export { MultiGeoTIFF }; /** * Creates a new GeoTIFF from a remote URL. * @param {string} url The URL to access the image from * @param {RemoteSourceOptions} [options] Additional options to pass to the source. * See {@link makeRemoteSource} for details. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file. */ export async function fromUrl(url, options = {}, signal) { return GeoTIFF.fromSource(makeRemoteSource(url, options), undefined, signal); } /** * Creates a new GeoTIFF from a custom {@link BaseClient}. * @param {BaseClient} client The client. * @param {RemoteSourceOptions} [options] Additional options to pass to the source. * See {@link makeRemoteSource} for details. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file. */ export async function fromCustomClient(client, options = {}, signal) { return GeoTIFF.fromSource(makeCustomSource(client, options), undefined, signal); } /** * Construct a new GeoTIFF from an * [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}. * @param {ArrayBuffer} arrayBuffer The data to read the file from. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file. */ export async function fromArrayBuffer(arrayBuffer, signal) { return GeoTIFF.fromSource(makeBufferSource(arrayBuffer), undefined, signal); } /** * Construct a GeoTIFF from a local file path. This uses the node * [filesystem API]{@link https://nodejs.org/api/fs.html} and is * not available on browsers. * * N.B. After the GeoTIFF has been completely processed it needs * to be closed but only if it has been constructed from a file. * @param {string} path The file path to read from. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file. */ export async function fromFile(path, signal) { return GeoTIFF.fromSource(makeFileSource(path), undefined, signal); } /** * Construct a GeoTIFF from an HTML * [Blob]{@link https://developer.mozilla.org/en-US/docs/Web/API/Blob} or * [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File} * object. * @param {Blob|File} blob The Blob or File object to read from. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @returns {Promise<GeoTIFF>} The resulting GeoTIFF file. */ export async function fromBlob(blob, signal) { return GeoTIFF.fromSource(makeFileReaderSource(blob), undefined, signal); } /** * Construct a MultiGeoTIFF from the given URLs. * @param {string} mainUrl The URL for the main file. * @param {string[]} overviewUrls An array of URLs for the overview images. * @param {RemoteSourceOptions} [options] Additional options to pass to the source. * See [makeRemoteSource]{@link module:source.makeRemoteSource} * for details. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is * to be aborted * @returns {Promise<MultiGeoTIFF>} The resulting MultiGeoTIFF file. */ export async function fromUrls(mainUrl, overviewUrls = [], options = {}, signal) { const mainFile = await GeoTIFF.fromSource(makeRemoteSource(mainUrl, options), undefined, signal); const overviewFiles = await Promise.all( overviewUrls.map((url) => GeoTIFF.fromSource(makeRemoteSource(url, options), undefined, signal)), ); return new MultiGeoTIFF(mainFile, overviewFiles); } /** * Main creating function for GeoTIFF files. * @param {Array<number>|Array<Array<Array<number>>>|TypedArray} values The pixel values to write. * Can be a flat array of all pixels or a 3-dimensional array of shape `[band][row][column]`. * @param {GeotiffWriterMetadata} metadata * @returns {ArrayBuffer} */ export function writeArrayBuffer(values, metadata) { return writeGeotiff(values, metadata); } export { Pool }; export { GeoTIFFImage }; export { BaseClient, BaseResponse };