ja-geotiff
Version:
GeoTIFF image decoding in JavaScript
887 lines (813 loc) • 27.9 kB
JavaScript
/** @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 {
fieldTypes,
fieldTagNames,
arrayFields,
geoKeyNames,
} 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"
export { globals }
export { rgb }
export { default as BaseDecoder } from "./compression/basedecoder.js"
export { getDecoder, addDecoder }
export { setLogger }
/**
* @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
*/
/**
* The autogenerated docs are a little confusing here. The effective type is:
*
* `(TypedArray | TypedArray[]) & { height: number; width: number}`
* @typedef {TypedArrayWithDimensions | TypedArrayArrayWithDimensions} ReadRasterResult
*/
function getFieldTypeLength(fieldType) {
switch (fieldType) {
case fieldTypes.BYTE:
case fieldTypes.ASCII:
case fieldTypes.SBYTE:
case fieldTypes.UNDEFINED:
return 1
case fieldTypes.SHORT:
case fieldTypes.SSHORT:
return 2
case fieldTypes.LONG:
case fieldTypes.SLONG:
case fieldTypes.FLOAT:
case fieldTypes.IFD:
return 4
case fieldTypes.RATIONAL:
case fieldTypes.SRATIONAL:
case fieldTypes.DOUBLE:
case fieldTypes.LONG8:
case fieldTypes.SLONG8:
case fieldTypes.IFD8:
return 8
default:
throw new RangeError(`Invalid field type: ${fieldType}`)
}
}
function parseGeoKeyDirectory(fileDirectory) {
const rawGeoKeyDirectory = fileDirectory.GeoKeyDirectory
if (!rawGeoKeyDirectory) {
return null
}
const geoKeyDirectory = {}
for (let i = 4; i <= rawGeoKeyDirectory[3] * 4; i += 4) {
const key = geoKeyNames[rawGeoKeyDirectory[i]]
const location = rawGeoKeyDirectory[i + 1]
? fieldTagNames[rawGeoKeyDirectory[i + 1]]
: null
const count = rawGeoKeyDirectory[i + 2]
const offset = rawGeoKeyDirectory[i + 3]
let value = null
if (!location) {
value = offset
} else {
value = fileDirectory[location]
if (typeof value === "undefined" || value === null) {
throw new Error(`Could not get value of geoKey '${key}'.`)
} else if (typeof value === "string") {
value = value.substring(offset, offset + count - 1)
} else if (value.subarray) {
value = value.subarray(offset, offset + count)
if (count === 1) {
value = value[0]
}
}
}
geoKeyDirectory[key] = value
}
return geoKeyDirectory
}
function getValues(dataSlice, fieldType, count, offset) {
let values = null
let readMethod = null
const fieldTypeLength = getFieldTypeLength(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:
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(values)
}
return values
}
/**
* Data class to store the parsed file directory (+ its raw form), geo key directory and
* offset to the next IFD
*/
class ImageFileDirectory {
/**
* Create an ImageFileDirectory.
* @param {object} fileDirectory the file directory, mapping tag names to values
* @param {Map} rawFileDirectory the raw file directory, mapping tag IDs to values
* @param {object} geoKeyDirectory the geo key directory, mapping geo key names to values
* @param {number} nextIFDByteOffset the byte offset to the next IFD
*/
constructor(
fileDirectory,
rawFileDirectory,
geoKeyDirectory,
nextIFDByteOffset
) {
this.fileDirectory = fileDirectory
this.rawFileDirectory = rawFileDirectory
this.geoKeyDirectory = geoKeyDirectory
this.nextIFDByteOffset = nextIFDByteOffset
}
}
/**
* Error class for cases when an IFD index was requested, that does not exist
* in the file.
*/
class GeoTIFFImageIndexError extends Error {
constructor(index) {
super(`No image at index ${index}`)
this.index = index
}
}
class GeoTIFFBase {
/**
* (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 {import('./geotiffimage').ReadRasterOptions} [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: subfileType, NewSubfileType: newSubfileType } =
image.fileDirectory
if (i === 0 || subfileType === 2 || newSubfileType & 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.
* @augments GeoTIFFBase
*/
class GeoTIFF extends GeoTIFFBase {
/**
* @constructor
* @param {*} 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.littleEndian = littleEndian
this.bigTiff = bigTiff
this.firstIFDOffset = firstIFDOffset
this.cache = options.cache || false
this.ifdRequests = []
this.ghostValues = null
}
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
)
}
/**
* Instructs to parse an image file directory at the given file offset.
* As there is no way to ensure that a location is indeed the start of an IFD,
* this function must be called with caution (e.g only using the IFD offsets from
* the headers or other IFDs).
* @param {number} offset the offset to parse the IFD at
* @returns {Promise<ImageFileDirectory>} the parsed IFD
*/
async parseFileDirectoryAt(offset) {
const entrySize = this.bigTiff ? 20 : 12
const offsetSize = this.bigTiff ? 8 : 2
let dataSlice = await this.getSlice(offset)
const numDirEntries = this.bigTiff
? dataSlice.readUint64(offset)
: dataSlice.readUint16(offset)
// if the slice does not cover the whole IFD, request a bigger slice, where the
// whole IFD fits: num of entries + n x tag length + offset to next IFD
const byteSize = numDirEntries * entrySize + (this.bigTiff ? 16 : 6)
if (!dataSlice.covers(offset, byteSize)) {
dataSlice = await this.getSlice(offset, byteSize)
}
const fileDirectory = {}
const rawFileDirectory = new Map()
// loop over the IFD and create a file directory object
let i = offset + (this.bigTiff ? 8 : 2)
for (
let entryCount = 0;
entryCount < numDirEntries;
i += entrySize, ++entryCount
) {
const fieldTag = dataSlice.readUint16(i)
const fieldType = dataSlice.readUint16(i + 2)
const typeCount = this.bigTiff
? dataSlice.readUint64(i + 4)
: dataSlice.readUint32(i + 4)
let fieldValues
let value
const fieldTypeLength = getFieldTypeLength(fieldType)
const valueOffset = i + (this.bigTiff ? 12 : 8)
// check whether the value is directly encoded in the tag or refers to a
// different external byte range
if (fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4)) {
fieldValues = getValues(dataSlice, fieldType, typeCount, valueOffset)
} else {
// resolve the reference to the actual byte range
const actualOffset = dataSlice.readOffset(valueOffset)
const length = getFieldTypeLength(fieldType) * typeCount
// check, whether we actually cover the referenced byte range; if not,
// request a new slice of bytes to read from it
if (dataSlice.covers(actualOffset, length)) {
fieldValues = getValues(dataSlice, fieldType, typeCount, actualOffset)
} else {
const fieldDataSlice = await this.getSlice(actualOffset, length)
fieldValues = getValues(
fieldDataSlice,
fieldType,
typeCount,
actualOffset
)
}
}
// unpack single values from the array
if (
typeCount === 1 &&
arrayFields.indexOf(fieldTag) === -1 &&
!(
fieldType === fieldTypes.RATIONAL ||
fieldType === fieldTypes.SRATIONAL
)
) {
value = fieldValues[0]
} else {
value = fieldValues
}
// write the tags value to the file directory
const tagName = fieldTagNames[fieldTag]
if (tagName) {
fileDirectory[tagName] = value
}
rawFileDirectory.set(fieldTag, value)
}
const geoKeyDirectory = parseGeoKeyDirectory(fileDirectory)
const nextIFDByteOffset = dataSlice.readOffset(
offset + offsetSize + entrySize * numDirEntries
)
return new ImageFileDirectory(
fileDirectory,
rawFileDirectory,
geoKeyDirectory,
nextIFDByteOffset
)
}
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.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 previousIfd = await this.ifdRequests[index - 1]
if (previousIfd.nextIFDByteOffset === 0) {
throw new GeoTIFFImageIndexError(index)
}
return this.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) {
const ifd = await this.requestIFD(index)
return new GeoTIFFImage(
ifd.fileDirectory,
ifd.geoKeyDirectory,
this.dataView,
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<Object>} the parsed ghost area or null, if no such area was found
*/
async getGhostValues() {
const offset = this.bigTiff ? 16 : 8
if (this.ghostValues) {
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
)
this.ghostValues = {}
fullString
.split("\n")
.filter((line) => line.length > 0)
.map((line) => line.split("="))
.forEach(([key, value]) => {
this.ghostValues[key] = value
})
}
return this.ghostValues
}
/**
* Parse a (Geo)TIFF file from the given source.
*
* @param {*} 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, 0)
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.parseFileDirectoryAt(this.mainFile.firstIFDOffset),
].concat(
this.overviewFiles.map((file) =>
file.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) {
await this.getImageCount()
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) {
const ifd = await imageFile.requestIFD(relativeIndex)
return new GeoTIFFImage(
ifd.fileDirectory,
ifd.geoKeyDirectory,
imageFile.dataView,
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 | string[]} url The URL to access the image from
* @param {object} [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), signal)
}
/**
* Creates a new GeoTIFF from a custom {@link BaseClient}.
* @param {BaseClient} client The client.
* @param {object} [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), 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), 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), 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), 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 {Object} [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),
signal
)
const overviewFiles = await Promise.all(
overviewUrls.map((url) =>
GeoTIFF.fromSource(makeRemoteSource(url, options))
)
)
return new MultiGeoTIFF(mainFile, overviewFiles)
}
/**
* Main creating function for GeoTIFF files.
* @param {(Array)} array of pixel values
* @returns {metadata} metadata
*/
export function writeArrayBuffer(values, metadata) {
return writeGeotiff(values, metadata)
}
export { Pool }
export { GeoTIFFImage }
export { BaseClient, BaseResponse }