|
export class ImageFileDirectory { |
|
/** |
|
* Create an ImageFileDirectory. |
|
* @param {Map} actualizedFields the file directory, mapping tag names to values |
|
* @param {Map} deferredFields the deferred fields, mapping tag names to async functions |
|
* @param {Map} deferredArrays the deferred arrays, mapping tag names to DeferredArray objects |
|
* @param {number} nextIFDByteOffset the byte offset to the next IFD |
|
*/ |
|
constructor(actualizedFields, deferredFields, deferredArrays, nextIFDByteOffset) { |
|
this.actualizedFields = actualizedFields; |
|
this.deferredFields = deferredFields; |
|
this.deferredFieldsBeingResolved = new Map(); |
|
this.deferredArrays = deferredArrays; |
|
this.nextIFDByteOffset = nextIFDByteOffset; |
|
} |
|
|
|
/** |
|
* @param {number|string} tagIdentifier The field tag ID or name |
|
* @returns {boolean} whether the field exists (actualized or deferred) |
|
*/ |
|
hasTag(tagIdentifier) { |
|
const tag = resolveTag(tagIdentifier); |
|
return this.actualizedFields.has(tag) || this.deferredFields.has(tag) || this.deferredArrays.has(tag); |
|
} |
|
|
|
/** |
|
* Synchronously retrieves the value for a given tag. If it is deferred, an error is thrown. |
|
* @param {number|string} tagIdentifier The field tag ID or name |
|
* @returns the field value, or undefined if it does not exist |
|
* @throws {Error} If the tag is deferred and requires asynchronous loading |
|
*/ |
|
getValue(tagIdentifier) { |
|
const tag = resolveTag(tagIdentifier); |
|
|
|
if (this.deferredFields.has(tag) || this.deferredArrays.has(tag)) { |
|
const tagDef = tagDefinitions[tag]; |
|
const tagName = tagDef?.name || `Tag${tag}`; |
|
throw new Error( |
|
`Field '${tagName}' (${tag}) is deferred. Use loadValue() to load it asynchronously.`, |
|
); |
|
} |
|
|
|
if (!this.actualizedFields.has(tag)) { |
|
return undefined; |
|
} |
|
|
|
return this.actualizedFields.get(tag); |
|
} |
|
|
|
/** |
|
* Retrieves the value for a given tag. If it is deferred, it will be loaded first. |
|
* @param {number|string} tagIdentifier The field tag ID or name |
|
* @returns the field value, or undefined if it does not exist |
|
*/ |
|
async loadValue(tagIdentifier) { |
|
const tag = resolveTag(tagIdentifier); |
|
if (this.actualizedFields.has(tag)) { |
|
return this.actualizedFields.get(tag); |
|
} |
|
if (this.deferredFieldsBeingResolved.has(tag)) { |
|
return this.deferredFieldsBeingResolved.get(tag); |
|
} |
|
if (this.deferredFields.has(tag)) { |
|
const loaderFn = this.deferredFields.get(tag); |
|
this.deferredFields.delete(tag); |
|
|
|
// Set promise BEFORE starting async work to prevent race conditions |
|
const valuePromise = (async () => { |
|
try { |
|
const value = await loaderFn(); |
|
this.actualizedFields.set(tag, value); |
|
return value; |
|
} finally { |
|
this.deferredFieldsBeingResolved.delete(tag); |
|
} |
|
})(); |
|
|
|
this.deferredFieldsBeingResolved.set(tag, valuePromise); |
|
return valuePromise; |
|
} |
|
if (this.deferredArrays.has(tag)) { |
|
const deferredArray = this.deferredArrays.get(tag); |
|
return deferredArray.loadAll(); |
|
} |
|
|
|
return undefined; |
|
} |
|
|
|
/** |
|
* Retrieves the value at a given index for a tag that is an array. If it is deferred, it will be loaded first. |
|
* @param {number|string} tagIdentifier The field tag ID or name |
|
* @param {number} index The index within the array |
|
* @returns the field value at the given index, or undefined if it does not exist |
|
*/ |
|
async loadValueIndexed(tagIdentifier, index) { |
|
const tag = resolveTag(tagIdentifier); |
|
if (this.actualizedFields.has(tag)) { |
|
const value = this.actualizedFields.get(tag); |
|
return value[index]; |
|
} else if (this.deferredArrays.has(tag)) { |
|
const deferredArray = this.deferredArrays.get(tag); |
|
return deferredArray.get(index); |
|
} else if (this.hasTag(tag)) { |
|
return (await this.loadValue(tag))[index]; |
|
} |
|
return undefined; |
|
} |
|
|
|
/** |
|
* Parses the GeoTIFF GeoKeyDirectory tag into a structured object. |
|
* The GeoKeyDirectory is a special TIFF tag that contains geographic metadata |
|
* in a key-value format as defined by the GeoTIFF specification. |
|
* @returns {Record<import('./globals.js').GeoKeyName, *>|null} Parsed geo key directory |
|
* mapping key names to values, or null if not present |
|
* @throws {Error} If a referenced geo key value cannot be retrieved |
|
*/ |
|
parseGeoKeyDirectory() { |
|
const rawGeoKeyDirectory = this.getValue('GeoKeyDirectory'); |
|
if (!rawGeoKeyDirectory) { |
|
return null; |
|
} |
|
|
|
/** @type {Record<import('./globals.js').GeoKeyName, *>} */ |
|
const geoKeyDirectory = {}; |
|
for (let i = 4; i <= rawGeoKeyDirectory[3] * 4; i += 4) { |
|
const key = geoKeyNames[rawGeoKeyDirectory[i]]; |
|
const location = rawGeoKeyDirectory[i + 1] || null; |
|
const count = rawGeoKeyDirectory[i + 2]; |
|
const offset = rawGeoKeyDirectory[i + 3]; |
|
|
|
let value = null; |
|
if (!location) { |
|
value = offset; |
|
} else { |
|
value = this.getValue(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; |
|
} |
|
|
|
toObject() { |
|
const obj = {}; |
|
for (const [tag, value] of this.actualizedFields.entries()) { |
|
const tagDefinition = tagDefinitions[tag]; |
|
const tagName = tagDefinition ? tagDefinition.name : `Tag${tag}`; |
|
obj[tagName] = value; |
|
} |
|
return obj; |
|
} |
|
} |
In geotiff v3 there's a new ImageFileDirectory class defined here
geotiff.js/src/imagefiledirectory.js
Lines 254 to 414 in 903125b
It seems entirely not straightforward to materialize the IFD to a normal object, because there's no iterator of keys provided by the IFD. Instead you're forced to already know what IFD keys you want to look at, in order to use
loadValue.There should be:
actualizedFields,deferredFields, anddeferredArrays.Also, I think
toObjectis a horrible footgun as it exists now, because it only checksactualizedFieldsand will silently omit any keys that have not yet been loaded, while not telling the user that it's missing those keys.As an aside, if I may, v3 feels kinda rushed and half-baked for a library as mature as geotiff.js. It looks like it was less than 24 hours from when #484 was merged to when 3.0 was released. There wasn't any time for downstream users to test any betas or release candidates before v3. I had seen that #484 was open, but didn't know the state/whether it was still in progress, and would've loved to give feedback before the actual release of v3.