Skip to content

Fully materializing an ImageFileDirectory to an object #513

Description

@kylebarron

In geotiff v3 there's a new ImageFileDirectory class defined here

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;
}
}

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:

  • A helper to list all the keys defined by this IFD, so that users don't need to think about and look into actualizedFields, deferredFields, and deferredArrays.
  • A helper to (asynchronously) materialize the full IFD, for users who prefer the old behavior and don't need deferred tag loading.

Also, I think toObject is a horrible footgun as it exists now, because it only checks actualizedFields and 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement or new feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions