UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

376 lines 15.3 kB
// https://docs.ogc.org/is/19-008r4/19-008r4.html#_requirements_class_tiff import { ARRAY_FIELDS, FIELD_TAG_NAMES, FIELD_TYPES, GEO_KEY_NAMES } from './constants'; /** * GeoTIFF Header Reader */ export class GeoTIFFHeaderReader { reader; #littleEndian = true; #bigTiff = false; imageDirectories = []; /** @param reader - the geotiff reader to parse data from */ constructor(reader) { this.reader = reader; this.#readheader(); } /** @returns - the number of images in the GeoTIFF */ get length() { return this.imageDirectories.length; } /** @returns - the littleEndian flag */ get littleEndian() { return this.#littleEndian; } /** @returns - the bigTIFF flag */ get bigTiff() { return this.#bigTiff; } /** parses the header data to begin parsing the GeoTIFF */ #readheader() { const { reader } = this; // pull the endianess from the header const BOM = reader.getUint16(0, false); if (BOM === 0x4949) { this.#littleEndian = true; } else if (BOM === 0x4d4d) { this.#littleEndian = false; } else { throw new TypeError('Invalid byte order value.'); } const magicNumber = reader.getUint16(2, this.littleEndian); if (magicNumber === 42) { this.#bigTiff = false; } else if (magicNumber === 43) { this.#bigTiff = true; const offsetByteSize = reader.getUint16(4, this.littleEndian); if (offsetByteSize !== 8) { throw new Error('Unsupported offset byte-size.'); } } else { throw new TypeError('Invalid magic number.'); } const firstIFDOffset = this.bigTiff ? Number(reader.getBigUint64(8, this.littleEndian)) : reader.getUint32(4, this.littleEndian); this.#getImageMetadata(firstIFDOffset); } /** * Reads the value of the tag at the given offset (16 bits if not bigTIFF) * @param offset - the offset to read the tag from * @returns - the value of the tag */ #readTag(offset) { const { reader, bigTiff, littleEndian } = this; return bigTiff ? Number(reader.getBigUint64(offset, littleEndian)) : reader.getUint16(offset, littleEndian); } /** * Reads the value of the tag at the given offset (32 bits if not bigTIFF) * @param offset - the offset to read the tag from * @returns - the value of the tag */ #readOffset(offset) { const { reader, bigTiff, littleEndian } = this; return bigTiff ? Number(reader.getBigUint64(offset, littleEndian)) : reader.getUint32(offset, littleEndian); } /** * 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 firstOffset - the offset to begin parsing the IFDs (Image File Directory) at. */ #getImageMetadata(firstOffset) { const { reader, bigTiff, littleEndian } = this; const entrySize = bigTiff ? 20 : 12; const offsetSize = bigTiff ? 8 : 2; let offset = firstOffset; let ifdOffset = firstOffset; while (ifdOffset !== 0) { const ifd = {}; const numDirEntries = this.#readTag(offset); let i = offset + offsetSize; let geokeyDirOffset = undefined; let prevTag = 0; for (let entryCount = 0; entryCount < numDirEntries; i += entrySize, entryCount++) { const fieldTag = reader.getUint16(i, littleEndian); if (fieldTag < prevTag) throw new Error(`Invalid IFD, ${fieldTag} < ${prevTag}`); prevTag = fieldTag; if (fieldTag === 33550) { // PixelScaleTag ifd.pixelScale = this.#getPixelScale(i); } else if (fieldTag === 33922) { // TiepointTag ifd.tiepoint = this.#getTiepoint(i); } else if (fieldTag === 34735) { // GeoKeyDirectory - map to use after all keys are cached. geokeyDirOffset = i; } else { const { key, value } = this.#getKeyValue(fieldTag, i); // @ts-expect-error - its ok to set the key-value pair. ifd[key] = value; } // NOTE: Technically geotiffs support column encoding of double and ascii values. Seems like it's not common enough to use though // else if (fieldTag === 34736) { // // location of DoubleValues // } else if (fieldTag === 34737) { // // location of ASCIIValues // } } // Validate it has a TransformationTag or a TiepointTag before storing if (geokeyDirOffset === undefined) console.info('No GeoKeyDirectory found. May contain errors'); else ifd.geoKeyDirectory = this.#getGeoKeyDirectory(geokeyDirOffset, ifd); if (ifd.tiepoint === undefined && ifd.ModelTransformation === undefined) console.info('No ModelTiepoint or ModelTransformation found. May contain errors'); if (Object.keys(ifd).length > 0) this.imageDirectories.push(ifd); else break; // increment offset and check for the next IFD // 814 offset += offsetSize + entrySize * numDirEntries; ifdOffset = this.#readTag(offset); offset += offsetSize; } } /** * Get the pixel scale from the GeoKeyDirectory * @param offset - the offset to begin parsing the IFDs (GeoKeyDirectory) at. * @returns the parsed GeoKeyDirectory */ #getPixelScale(offset) { const { reader, littleEndian, bigTiff } = this; const fieldType = reader.getUint16(offset + 2, littleEndian); if (fieldType !== 12) throw new Error(`Invalid GeoKeyDirectory type ${reader.getUint16(offset + 2)}`); const numKeys = this.#readOffset(offset + 4); if (numKeys !== 3) throw new Error(`Invalid GeoKeyDirectory numKeys ${numKeys}`); const valueOffset = this.#readOffset(offset + (bigTiff ? 12 : 8)); const xscale = reader.getFloat64(valueOffset, littleEndian); const yscale = reader.getFloat64(valueOffset + 8, littleEndian); const zscale = reader.getFloat64(valueOffset + 16, littleEndian); return [xscale, yscale, zscale]; } /** * https://docs.ogc.org/is/19-008r4/19-008r4.html#_geokey_directory_test * @param offset - the offset to begin parsing the IFDs (GeoKeyDirectory) at. * @param fileDir - the parsed ImageFileDirectory thus far * @returns the parsed GeoKeyDirectory */ #getGeoKeyDirectory(offset, fileDir) { const { reader, bigTiff } = this; const numKeys = this.#readOffset(offset + 4); const valueOffset = this.#readOffset(offset + (bigTiff ? 12 : 8)); const rawGeoKeys = new Uint16Array(reader.slice(valueOffset, valueOffset + numKeys * 2).buffer); const geoKeyDirectory = parseRawGeoKeys(rawGeoKeys, fileDir); // Validate that there is a GTModelType GeoKey in the GeoKey Directory if (geoKeyDirectory.GTModelTypeGeoKey === undefined) { throw new Error(`Missing "GTModelTypeGeoKey" in GeoKeyDirectory`); } return geoKeyDirectory; } /** * @param offset - the offset to begin parsing the IFDs (TiepointTag) at. * @returns the parsed Tiepoint */ #getTiepoint(offset) { const { reader, bigTiff, littleEndian } = this; // Validate that Bytes 2-3 = 12 (Double) const fieldType = reader.getUint16(offset + 2, littleEndian); if (fieldType !== 12) throw new Error(`Invalid TiepointTag type ${fieldType}`); // get size to the value in Bytes 4-7 const count = this.#readOffset(offset + 4); // Set TagValue to the value in Bytes 8-11 const valueOffset = this.#readOffset(offset + (bigTiff ? 12 : 8)); const tiepoint = []; for (let i = 0; i < count; i++) { tiepoint.push(reader.getFloat64(valueOffset + i * 8, littleEndian)); } return tiepoint; } /** * @param fieldTag - the tag to read * @param offset - the current offset in the IFD header data * @returns the parsed key value */ #getKeyValue(fieldTag, offset) { const { reader, littleEndian } = this; const fieldType = reader.getUint16(offset + 2, littleEndian); const typeCount = this.#readOffset(offset + 4); const fieldTypeLength = getFieldTypeLength(fieldType); const valueOffset = offset + (this.bigTiff ? 12 : 8); const actualOffset = fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4) ? valueOffset : this.#readOffset(valueOffset); const value = this.#getValue(fieldTag, fieldType, typeCount, actualOffset); // write the tags value to the file directly return { key: FIELD_TAG_NAMES[fieldTag], value, }; } /** * @param fieldTag - the tag to read * @param fieldType - the field type * @param typeCount - the number of values * @param valueOffset - the value offset * @returns - the parsed value */ #getValue(fieldTag, fieldType, typeCount, valueOffset) { const { reader, littleEndian } = this; const res = []; // console.log('GET VALUE', fieldTag, fieldType, typeCount, valueOffset); if (fieldType === FIELD_TYPES.ASCII) { return reader.parseString(valueOffset, typeCount); } else if (fieldType === FIELD_TYPES.BYTE || fieldType === FIELD_TYPES.UNDEFINED) { for (let i = 0; i < typeCount; i++) res.push(reader.getUint8(valueOffset + i)); } else if (fieldType === FIELD_TYPES.SBYTE) { for (let i = 0; i < typeCount; i++) res.push(reader.getInt8(valueOffset + i)); } else if (fieldType === FIELD_TYPES.SHORT) { for (let i = 0; i < typeCount; i++) res.push(reader.getUint16(valueOffset + i * 2, littleEndian)); } else if (fieldType === FIELD_TYPES.SSHORT) { for (let i = 0; i < typeCount; i++) res.push(reader.getInt16(valueOffset + i * 2, littleEndian)); } else if (fieldType === FIELD_TYPES.LONG) { for (let i = 0; i < typeCount; i++) res.push(reader.getUint32(valueOffset + i * 4, littleEndian)); } else if (fieldType === FIELD_TYPES.SLONG) { for (let i = 0; i < typeCount; i++) res.push(reader.getInt32(valueOffset + i * 4, littleEndian)); } else if (fieldType === FIELD_TYPES.FLOAT) { for (let i = 0; i < typeCount; i++) res.push(reader.getFloat32(valueOffset + i * 4, littleEndian)); } else if (fieldType === FIELD_TYPES.RATIONAL) { typeCount *= 2; for (let i = 0; i < typeCount; i += 2) { res.push(reader.getUint32(valueOffset + i * 4, littleEndian)); res.push(reader.getUint32(valueOffset + i * 4 + 4, littleEndian)); } } else if (fieldType === FIELD_TYPES.SRATIONAL) { typeCount *= 2; for (let i = 0; i < typeCount; i += 2) { res.push(reader.getInt32(valueOffset + i * 4, littleEndian)); res.push(reader.getInt32(valueOffset + i * 4 + 4, littleEndian)); } } else if (fieldType === FIELD_TYPES.DOUBLE) { for (let i = 0; i < typeCount; i++) res.push(reader.getFloat64(valueOffset + i * 8, littleEndian)); } else if (fieldType === FIELD_TYPES.LONG8) { for (let i = 0; i < typeCount; i++) res.push(Number(reader.getBigUint64(valueOffset + i * 8, littleEndian))); } else if (fieldType === FIELD_TYPES.SLONG8) { for (let i = 0; i < typeCount; i++) res.push(Number(reader.getBigInt64(valueOffset + i * 8, littleEndian))); } // unpack single values from the array if (typeCount === 1 && ARRAY_FIELDS.indexOf(fieldTag) === -1 && !(fieldType === FIELD_TYPES.RATIONAL || fieldType === FIELD_TYPES.SRATIONAL)) { return res[0]; } else { return res; } } } /** * Get the field type length * @param fieldType - the field type * @returns - the field type length */ function getFieldTypeLength(fieldType) { switch (fieldType) { case FIELD_TYPES.BYTE: case FIELD_TYPES.ASCII: case FIELD_TYPES.SBYTE: case FIELD_TYPES.UNDEFINED: return 1; case FIELD_TYPES.SHORT: case FIELD_TYPES.SSHORT: return 2; case FIELD_TYPES.LONG: case FIELD_TYPES.SLONG: case FIELD_TYPES.FLOAT: case FIELD_TYPES.IFD: return 4; case FIELD_TYPES.RATIONAL: case FIELD_TYPES.SRATIONAL: case FIELD_TYPES.DOUBLE: case FIELD_TYPES.LONG8: case FIELD_TYPES.SLONG8: case FIELD_TYPES.IFD8: return 8; default: throw new RangeError(`Invalid field type: ${fieldType}`); } } /** * Parse the raw geo keys * @param rawGeoKeys - the raw geo keys * @param fileDir - the image file directory * @returns - the parsed geo keys */ function parseRawGeoKeys(rawGeoKeys, fileDir) { const geoKeyDirectory = {}; for (let i = 4; i <= rawGeoKeys[3] * 4; i += 4) { const geoKey = rawGeoKeys[i]; const key = GEO_KEY_NAMES[geoKey]; const location = rawGeoKeys[i + 1] !== 0 ? FIELD_TAG_NAMES[rawGeoKeys[i + 1]] : null; const count = rawGeoKeys[i + 2]; const offset = rawGeoKeys[i + 3]; let value = null; if (location === null) { value = offset; } else { value = fileDir[location]; if (typeof value === 'undefined' || value === null) { throw new Error(`Could not get value of geoKey '${key}' at location '${location}'.`); } else if (typeof value === 'string') { value = value.substring(offset, offset + count - 1); } else if (Array.isArray(value)) { value = value.slice(offset, offset + count); if (count === 1) value = value[0]; } } // @ts-expect-error - value assignment is ok here geoKeyDirectory[key] = value; } return geoKeyDirectory; } //# sourceMappingURL=header.js.map