From 2a6a5cf27119b1c44157b8e6e51fc547337ed279 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 15:14:43 +0200 Subject: [PATCH 1/9] Parse DOUBLE GeoKeys as well as ASCII - not only GeogCitationGeoKey --- src/geotiffwriter.js | 40 ++++++++++++++++++++++++++++++++++++---- src/globals.js | 2 ++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/geotiffwriter.js b/src/geotiffwriter.js index 9f542b0e..93d64791 100644 --- a/src/geotiffwriter.js +++ b/src/geotiffwriter.js @@ -446,6 +446,28 @@ export function writeGeotiff(data, metadata) { } } + // Spec http://geotiff.maptools.org/spec/geotiff2.4.html + // Build GeoDoubleParams the same way as GeoAsciiParams (collect DOUBLE values) + if (!metadata.GeoDoubleParams) { + const geoDoubleParams = []; + geoKeys.forEach((name) => { + const code = Number(name2code[name]); + const tagType = fieldTagTypes[code]; + const key = metadata[name] + if (tagType === 'DOUBLE' && key !== undefined) { + // Accept either a single numeric value or an array of numbers + if (Array.isArray(key)) { + key.forEach((v) => geoDoubleParams.push(Number(v))); + } else { + geoDoubleParams.push(Number(key)); + } + } + }); + if (geoDoubleParams.length > 0) { + metadata.GeoDoubleParams = geoDoubleParams; + } + } + if (!metadata.GeoKeyDirectory) { const NumberOfKeys = geoKeys.length; @@ -461,10 +483,20 @@ export function writeGeotiff(data, metadata) { Count = 1; TIFFTagLocation = 0; valueOffset = metadata[geoKey]; - } else if (geoKey === 'GeogCitationGeoKey') { - Count = metadata.GeoAsciiParams.length; - TIFFTagLocation = Number(name2code.GeoAsciiParams); - valueOffset = 0; + } else if (fieldTagTypes[KeyID] === 'ASCII') { + const asciiValue = `${metadata[geoKey].toString()}\u0000`; + Count = asciiValue.length; + TIFFTagLocation = Number(name2code.GeoAsciiParams); // 34737 + valueOffset = metadata.GeoAsciiParams.indexOf(asciiValue); + } else if (fieldTagTypes[KeyID] === 'DOUBLE') { + TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 + if (Array.isArray(metadata[geoKey])) { + Count = metadata[geoKey].length; + valueOffset = metadata.GeoDoubleParams.indexOf(metadata[geoKey][0]); + } else { + Count = 1; + valueOffset = metadata.GeoDoubleParams.indexOf(Number(metadata[geoKey])); + } } else { console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`); } diff --git a/src/globals.js b/src/globals.js index 677d5f98..57d1bda9 100644 --- a/src/globals.js +++ b/src/globals.js @@ -166,6 +166,8 @@ export const fieldTagTypes = { 2049: 'ASCII', 2052: 'SHORT', 2054: 'SHORT', + 2057: 'DOUBLE', + 2059: 'DOUBLE', 2060: 'SHORT', 3072: 'SHORT', 3073: 'ASCII', From 332043ce32c7e9a6216f2364365da7ae67a9bd5e Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 15:33:33 +0200 Subject: [PATCH 2/9] rework for simple pass to get all offsets and values for ascii/double --- src/geotiffwriter.js | 81 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/src/geotiffwriter.js b/src/geotiffwriter.js index 93d64791..30cb766b 100644 --- a/src/geotiffwriter.js +++ b/src/geotiffwriter.js @@ -432,13 +432,23 @@ export function writeGeotiff(data, metadata) { .filter((key) => endsWith(key, 'GeoKey')) .sort((a, b) => name2code[a] - name2code[b]); + // Build ASCII and DOUBLE tags, GeoAsciiParamsTag and GeoDoubleParamsTag + // Spec http://geotiff.maptools.org/spec/geotiff2.4.html + + // GeoAsciiParams and geoAsciiOffsets + const geoAsciiOffsets = {} if (!metadata.GeoAsciiParams) { let geoAsciiParams = ''; + let currentAsciiOffset = 0; geoKeys.forEach((name) => { const code = Number(name2code[name]); const tagType = fieldTagTypes[code]; - if (tagType === 'ASCII') { - geoAsciiParams += `${metadata[name].toString()}\u0000`; + const val = metadata[name] + if (tagType === 'ASCII' && val !== undefined) { + const s = `${val.toString()}\u0000`; + geoAsciiOffsets[name] = currentAsciiOffset; + geoAsciiParams += s; + currentAsciiOffset += s.length; // count in bytes (chars) } }); if (geoAsciiParams.length > 0) { @@ -446,20 +456,25 @@ export function writeGeotiff(data, metadata) { } } - // Spec http://geotiff.maptools.org/spec/geotiff2.4.html - // Build GeoDoubleParams the same way as GeoAsciiParams (collect DOUBLE values) + // GeoDoubleParamsTag and geoDoubleOffsets + const geoDoubleOffsets = {}; if (!metadata.GeoDoubleParams) { const geoDoubleParams = []; + let currentDoubleIndex = 0; geoKeys.forEach((name) => { const code = Number(name2code[name]); const tagType = fieldTagTypes[code]; - const key = metadata[name] - if (tagType === 'DOUBLE' && key !== undefined) { - // Accept either a single numeric value or an array of numbers - if (Array.isArray(key)) { - key.forEach((v) => geoDoubleParams.push(Number(v))); + const val = metadata[name]; + if (tagType === 'DOUBLE' && val !== undefined) { + geoDoubleOffsets[name] = currentDoubleIndex; + if (Array.isArray(val)) { + val.forEach((v) => { + geoDoubleParams.push(Number(v)); + currentDoubleIndex++; + }); } else { - geoDoubleParams.push(Number(key)); + geoDoubleParams.push(Number(val)); + currentDoubleIndex++; } } }); @@ -468,6 +483,52 @@ export function writeGeotiff(data, metadata) { } } + // Build GeoKeyDirectory for resolved records, handling SHORT as well as ASCII and DOUBLE + if (!metadata.GeoKeyDirectory) { + // Number of keys will be populated on finish + const GeoKeyDirectory = [1, 1, 0, 0]; + let validKeys = 0; + + geoKeys.forEach((geoKey) => { + const KeyID = Number(name2code[geoKey]); + const tagType = fieldTagTypes[KeyID]; + let Count, TIFFTagLocation, valueOffset; + + if (tagType === 'SHORT') { + Count = 1; + TIFFTagLocation = 0; + valueOffset = metadata[geoKey]; + } else if (tagType === 'ASCII') { + if (geoAsciiOffsets?.[geoKey] === undefined) return; + TIFFTagLocation = Number(name2code.GeoAsciiParams); // 34737 + valueOffset = geoAsciiOffsets[geoKey]; + Count = (`${metadata[geoKey].toString()}\u0000`).length; + } else if (tagType === 'DOUBLE') { + if (geoAsciiOffsets?.[geoKey] === undefined) return; + TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 + valueOffset = geoDoubleOffsets[geoKey]; + const val = metadata[geoKey]; + Count = Array.isArray(val) ? val.length : 1; + } else { + console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`); + return; + } + // Push to GeoKeyDirectory + GeoKeyDirectory.push(KeyID); + GeoKeyDirectory.push(TIFFTagLocation); + GeoKeyDirectory.push(Count); + GeoKeyDirectory.push(valueOffset); + validKeys++; + }); + + GeoKeyDirectory[3] = validKeys; + metadata.GeoKeyDirectory = GeoKeyDirectory; + } + + + +// END + if (!metadata.GeoKeyDirectory) { const NumberOfKeys = geoKeys.length; From f9011a77c0d44436913ce81037aa38fe2bdfbd00 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 15:42:13 +0200 Subject: [PATCH 3/9] touch-ups, using toArray for simpler handling of arrays and simple double vals --- src/geotiffwriter.js | 58 +++++--------------------------------------- 1 file changed, 6 insertions(+), 52 deletions(-) diff --git a/src/geotiffwriter.js b/src/geotiffwriter.js index 30cb766b..ea3671e9 100644 --- a/src/geotiffwriter.js +++ b/src/geotiffwriter.js @@ -457,6 +457,8 @@ export function writeGeotiff(data, metadata) { } // GeoDoubleParamsTag and geoDoubleOffsets + // Helper to handle the same way simple moni-values and arrays + const toArray = (input) => Array.isArray(input) ? input : [input]; const geoDoubleOffsets = {}; if (!metadata.GeoDoubleParams) { const geoDoubleParams = []; @@ -467,15 +469,10 @@ export function writeGeotiff(data, metadata) { const val = metadata[name]; if (tagType === 'DOUBLE' && val !== undefined) { geoDoubleOffsets[name] = currentDoubleIndex; - if (Array.isArray(val)) { - val.forEach((v) => { - geoDoubleParams.push(Number(v)); - currentDoubleIndex++; - }); - } else { - geoDoubleParams.push(Number(val)); + toArray(val).forEach((v) => { + geoDoubleParams.push(Number(v)); currentDoubleIndex++; - } + }); } }); if (geoDoubleParams.length > 0) { @@ -504,7 +501,7 @@ export function writeGeotiff(data, metadata) { valueOffset = geoAsciiOffsets[geoKey]; Count = (`${metadata[geoKey].toString()}\u0000`).length; } else if (tagType === 'DOUBLE') { - if (geoAsciiOffsets?.[geoKey] === undefined) return; + if (geoDoubleOffsets?.[geoKey] === undefined) return; TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 valueOffset = geoDoubleOffsets[geoKey]; const val = metadata[geoKey]; @@ -525,49 +522,6 @@ export function writeGeotiff(data, metadata) { metadata.GeoKeyDirectory = GeoKeyDirectory; } - - -// END - - if (!metadata.GeoKeyDirectory) { - const NumberOfKeys = geoKeys.length; - - const GeoKeyDirectory = [1, 1, 0, NumberOfKeys]; - geoKeys.forEach((geoKey) => { - const KeyID = Number(name2code[geoKey]); - GeoKeyDirectory.push(KeyID); - - let Count; - let TIFFTagLocation; - let valueOffset; - if (fieldTagTypes[KeyID] === 'SHORT') { - Count = 1; - TIFFTagLocation = 0; - valueOffset = metadata[geoKey]; - } else if (fieldTagTypes[KeyID] === 'ASCII') { - const asciiValue = `${metadata[geoKey].toString()}\u0000`; - Count = asciiValue.length; - TIFFTagLocation = Number(name2code.GeoAsciiParams); // 34737 - valueOffset = metadata.GeoAsciiParams.indexOf(asciiValue); - } else if (fieldTagTypes[KeyID] === 'DOUBLE') { - TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 - if (Array.isArray(metadata[geoKey])) { - Count = metadata[geoKey].length; - valueOffset = metadata.GeoDoubleParams.indexOf(metadata[geoKey][0]); - } else { - Count = 1; - valueOffset = metadata.GeoDoubleParams.indexOf(Number(metadata[geoKey])); - } - } else { - console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`); - } - GeoKeyDirectory.push(TIFFTagLocation); - GeoKeyDirectory.push(Count); - GeoKeyDirectory.push(valueOffset); - }); - metadata.GeoKeyDirectory = GeoKeyDirectory; - } - // delete GeoKeys from metadata, because stored in GeoKeyDirectory tag for (const geoKey of geoKeys) { if (metadata.hasOwnProperty(geoKey)) { From 547c30444a02432fabe3e55b6ea21ed7b8ffcf75 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 16:24:09 +0200 Subject: [PATCH 4/9] Add tests for DOUBLE, ASCII GeoKeys to geotiffwriter --- test/geotiffwriter.spec.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/geotiffwriter.spec.js diff --git a/test/geotiffwriter.spec.js b/test/geotiffwriter.spec.js new file mode 100644 index 00000000..c03a73ca --- /dev/null +++ b/test/geotiffwriter.spec.js @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import { fromArrayBuffer, writeArrayBuffer } from '../src/geotiff.js'; + +it('should write and read back GeoDoubleParams keys (EPSG:4326)', async () => { + const width = 2; + const height = 2; + const data = new Float32Array(width * height).fill(1); + + const metadata = { + width, + height, + GeographicTypeGeoKey: 4326, + GTModelTypeGeoKey: 2, + GeogSemiMajorAxisGeoKey: 6378137.0, // DOUBLE + GeogInvFlatteningGeoKey: 298.257223563, // DOUBLE + GeogCitationGeoKey: 'WGS 84', // ASCII + PCSCitationGeoKey: 'test-ascii', // ASCII + }; + + const buffer = await writeArrayBuffer(data, metadata); + const tiff = await fromArrayBuffer(buffer); + const image = await tiff.getImage(); + + const geoKeys = image.getGeoKeys(); + expect(geoKeys.GeogSemiMajorAxisGeoKey).to.be.closeTo(6378137.0, 0.000001); + expect(geoKeys.GeogInvFlatteningGeoKey).to.be.closeTo(298.257223563, 0.000001); + expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84'); + expect(geoKeys.PCSCitationGeoKey).to.equal('test-ascii'); +}); From ef867a808af0b89f33261b60c719d91b93567a0c Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 16:33:23 +0200 Subject: [PATCH 5/9] fix typo redefining toArray --- src/geotiffwriter.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/geotiffwriter.js b/src/geotiffwriter.js index ea3671e9..78a9fbb7 100644 --- a/src/geotiffwriter.js +++ b/src/geotiffwriter.js @@ -436,14 +436,14 @@ export function writeGeotiff(data, metadata) { // Spec http://geotiff.maptools.org/spec/geotiff2.4.html // GeoAsciiParams and geoAsciiOffsets - const geoAsciiOffsets = {} + const geoAsciiOffsets = {}; if (!metadata.GeoAsciiParams) { let geoAsciiParams = ''; let currentAsciiOffset = 0; geoKeys.forEach((name) => { const code = Number(name2code[name]); const tagType = fieldTagTypes[code]; - const val = metadata[name] + const val = metadata[name]; if (tagType === 'ASCII' && val !== undefined) { const s = `${val.toString()}\u0000`; geoAsciiOffsets[name] = currentAsciiOffset; @@ -458,8 +458,7 @@ export function writeGeotiff(data, metadata) { // GeoDoubleParamsTag and geoDoubleOffsets // Helper to handle the same way simple moni-values and arrays - const toArray = (input) => Array.isArray(input) ? input : [input]; - const geoDoubleOffsets = {}; + const geoDoubleOffsets = {}; if (!metadata.GeoDoubleParams) { const geoDoubleParams = []; let currentDoubleIndex = 0; @@ -483,25 +482,30 @@ export function writeGeotiff(data, metadata) { // Build GeoKeyDirectory for resolved records, handling SHORT as well as ASCII and DOUBLE if (!metadata.GeoKeyDirectory) { // Number of keys will be populated on finish - const GeoKeyDirectory = [1, 1, 0, 0]; + const GeoKeyDirectory = [1, 1, 0, 0]; let validKeys = 0; geoKeys.forEach((geoKey) => { const KeyID = Number(name2code[geoKey]); const tagType = fieldTagTypes[KeyID]; - let Count, TIFFTagLocation, valueOffset; + let Count; let TIFFTagLocation; let + valueOffset; if (tagType === 'SHORT') { Count = 1; TIFFTagLocation = 0; valueOffset = metadata[geoKey]; } else if (tagType === 'ASCII') { - if (geoAsciiOffsets?.[geoKey] === undefined) return; + if (geoAsciiOffsets?.[geoKey] === undefined) { + return; + } TIFFTagLocation = Number(name2code.GeoAsciiParams); // 34737 valueOffset = geoAsciiOffsets[geoKey]; Count = (`${metadata[geoKey].toString()}\u0000`).length; } else if (tagType === 'DOUBLE') { - if (geoDoubleOffsets?.[geoKey] === undefined) return; + if (geoDoubleOffsets?.[geoKey] === undefined) { + return; + } TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 valueOffset = geoDoubleOffsets[geoKey]; const val = metadata[geoKey]; From 8cd3ad159efcd5605d50542c4916b2e825ec2132 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 21:40:33 +0200 Subject: [PATCH 6/9] Move geotiffwriter spec test GeoAsciiParams/GeoDoubleParams into geotiff tests --- test/geotiff.spec.js | 27 +++++++++++++++++++++++++++ test/geotiffwriter.spec.js | 29 ----------------------------- 2 files changed, 27 insertions(+), 29 deletions(-) delete mode 100644 test/geotiffwriter.spec.js diff --git a/test/geotiff.spec.js b/test/geotiff.spec.js index 327462d8..f2f9b631 100644 --- a/test/geotiff.spec.js +++ b/test/geotiff.spec.js @@ -1287,6 +1287,33 @@ describe('writeTests', () => { expect(normalize(fileDirectory.StripByteCounts)).to.equal(normalize(metadata.StripByteCounts)); expect(fileDirectory.GDAL_NODATA).to.equal('0\u0000'); }); + + it('should write and read back GeoAsciiParams/GeoDoubleParams keys', async () => { + const width = 2; + const height = 2; + const data = new Float32Array(width * height).fill(1); + + const metadata = { + width, + height, + GeographicTypeGeoKey: 4326, + GTModelTypeGeoKey: 2, + GeogSemiMajorAxisGeoKey: 6378137.0, // DOUBLE + GeogInvFlatteningGeoKey: 298.257223563, // DOUBLE + GeogCitationGeoKey: 'WGS 84', // ASCII + PCSCitationGeoKey: 'test-ascii', // ASCII + }; + + const buffer = await writeArrayBuffer(data, metadata); + const tiff = await fromArrayBuffer(buffer); + const image = await tiff.getImage(); + + const geoKeys = image.getGeoKeys(); + expect(geoKeys.GeogSemiMajorAxisGeoKey).to.be.closeTo(6378137.0, 0.000001); + expect(geoKeys.GeogInvFlatteningGeoKey).to.be.closeTo(298.257223563, 0.000001); + expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84'); + expect(geoKeys.PCSCitationGeoKey).to.equal('test-ascii'); + }); }); describe('BlockedSource Test', () => { diff --git a/test/geotiffwriter.spec.js b/test/geotiffwriter.spec.js deleted file mode 100644 index c03a73ca..00000000 --- a/test/geotiffwriter.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import { expect } from 'chai'; -import { fromArrayBuffer, writeArrayBuffer } from '../src/geotiff.js'; - -it('should write and read back GeoDoubleParams keys (EPSG:4326)', async () => { - const width = 2; - const height = 2; - const data = new Float32Array(width * height).fill(1); - - const metadata = { - width, - height, - GeographicTypeGeoKey: 4326, - GTModelTypeGeoKey: 2, - GeogSemiMajorAxisGeoKey: 6378137.0, // DOUBLE - GeogInvFlatteningGeoKey: 298.257223563, // DOUBLE - GeogCitationGeoKey: 'WGS 84', // ASCII - PCSCitationGeoKey: 'test-ascii', // ASCII - }; - - const buffer = await writeArrayBuffer(data, metadata); - const tiff = await fromArrayBuffer(buffer); - const image = await tiff.getImage(); - - const geoKeys = image.getGeoKeys(); - expect(geoKeys.GeogSemiMajorAxisGeoKey).to.be.closeTo(6378137.0, 0.000001); - expect(geoKeys.GeogInvFlatteningGeoKey).to.be.closeTo(298.257223563, 0.000001); - expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84'); - expect(geoKeys.PCSCitationGeoKey).to.equal('test-ascii'); -}); From 667b0566ed00d6df45bb6376040f5f407ccd5bd0 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 21:41:07 +0200 Subject: [PATCH 7/9] Do a single for-loop pass to append to corresponding directory for SHORT/ASCII/DOUBLE --- src/geotiffwriter.js | 110 +++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/src/geotiffwriter.js b/src/geotiffwriter.js index 78a9fbb7..f6af9b16 100644 --- a/src/geotiffwriter.js +++ b/src/geotiffwriter.js @@ -432,101 +432,75 @@ export function writeGeotiff(data, metadata) { .filter((key) => endsWith(key, 'GeoKey')) .sort((a, b) => name2code[a] - name2code[b]); - // Build ASCII and DOUBLE tags, GeoAsciiParamsTag and GeoDoubleParamsTag + // If not provided, build GeoKeyDirectory as well as GeoAsciiParamsTag and GeoDoubleParamsTag + // if GeoAsciiParams/GeoDoubleParams were passed in, we assume offsets are already correct // Spec http://geotiff.maptools.org/spec/geotiff2.4.html - - // GeoAsciiParams and geoAsciiOffsets - const geoAsciiOffsets = {}; - if (!metadata.GeoAsciiParams) { - let geoAsciiParams = ''; - let currentAsciiOffset = 0; - geoKeys.forEach((name) => { - const code = Number(name2code[name]); - const tagType = fieldTagTypes[code]; - const val = metadata[name]; - if (tagType === 'ASCII' && val !== undefined) { - const s = `${val.toString()}\u0000`; - geoAsciiOffsets[name] = currentAsciiOffset; - geoAsciiParams += s; - currentAsciiOffset += s.length; // count in bytes (chars) - } - }); - if (geoAsciiParams.length > 0) { - metadata.GeoAsciiParams = geoAsciiParams; - } - } - - // GeoDoubleParamsTag and geoDoubleOffsets - // Helper to handle the same way simple moni-values and arrays - const geoDoubleOffsets = {}; - if (!metadata.GeoDoubleParams) { - const geoDoubleParams = []; - let currentDoubleIndex = 0; - geoKeys.forEach((name) => { - const code = Number(name2code[name]); - const tagType = fieldTagTypes[code]; - const val = metadata[name]; - if (tagType === 'DOUBLE' && val !== undefined) { - geoDoubleOffsets[name] = currentDoubleIndex; - toArray(val).forEach((v) => { - geoDoubleParams.push(Number(v)); - currentDoubleIndex++; - }); - } - }); - if (geoDoubleParams.length > 0) { - metadata.GeoDoubleParams = geoDoubleParams; - } - } - - // Build GeoKeyDirectory for resolved records, handling SHORT as well as ASCII and DOUBLE if (!metadata.GeoKeyDirectory) { - // Number of keys will be populated on finish + // Only build ASCII / DOUBLE params if not provided + let geoAsciiParams = metadata.GeoAsciiParams || ''; + let currentAsciiOffset = geoAsciiParams.length; + let geoDoubleParams = metadata.GeoDoubleParams || []; + let currentDoubleIndex = geoDoubleParams.length; + + // Since geoKeys already sorted and filtered, do a single pass to append to corresponding directory for SHORT/ASCII/DOUBLE const GeoKeyDirectory = [1, 1, 0, 0]; let validKeys = 0; - geoKeys.forEach((geoKey) => { const KeyID = Number(name2code[geoKey]); const tagType = fieldTagTypes[KeyID]; - let Count; let TIFFTagLocation; let - valueOffset; + const val = metadata[geoKey]; + if (val === undefined) return; + let Count, TIFFTagLocation, valueOffset; if (tagType === 'SHORT') { Count = 1; TIFFTagLocation = 0; - valueOffset = metadata[geoKey]; + valueOffset = val; } else if (tagType === 'ASCII') { - if (geoAsciiOffsets?.[geoKey] === undefined) { + if (!metadata.GeoAsciiParams) { + const valStr = `${val.toString()}\u0000`; + TIFFTagLocation = Number(name2code.GeoAsciiParams); // 34737 + valueOffset = currentAsciiOffset; + Count = valStr.length; + geoAsciiParams += valStr; + currentAsciiOffset += valStr.length; + } else { return; } - TIFFTagLocation = Number(name2code.GeoAsciiParams); // 34737 - valueOffset = geoAsciiOffsets[geoKey]; - Count = (`${metadata[geoKey].toString()}\u0000`).length; } else if (tagType === 'DOUBLE') { - if (geoDoubleOffsets?.[geoKey] === undefined) { + if (!metadata.GeoDoubleParams) { + const arr = toArray(val); + TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 + valueOffset = currentDoubleIndex; + Count = arr.length; + arr.forEach((v) => { + geoDoubleParams.push(Number(v)); + currentDoubleIndex++; + }); + } else { return; } - TIFFTagLocation = Number(name2code.GeoDoubleParams); // 34736 - valueOffset = geoDoubleOffsets[geoKey]; - const val = metadata[geoKey]; - Count = Array.isArray(val) ? val.length : 1; } else { - console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`); + console.warn(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`); return; } - // Push to GeoKeyDirectory - GeoKeyDirectory.push(KeyID); - GeoKeyDirectory.push(TIFFTagLocation); - GeoKeyDirectory.push(Count); - GeoKeyDirectory.push(valueOffset); + + GeoKeyDirectory.push(KeyID, TIFFTagLocation, Count, valueOffset); validKeys++; }); + // Write GeoKeyDirectory, GeoAsciiParams, GeoDoubleParams GeoKeyDirectory[3] = validKeys; metadata.GeoKeyDirectory = GeoKeyDirectory; + if (!metadata.GeoAsciiParams && geoAsciiParams.length > 0) { + metadata.GeoAsciiParams = geoAsciiParams; + } + if (!metadata.GeoDoubleParams && geoDoubleParams.length > 0) { + metadata.GeoDoubleParams = geoDoubleParams; + } } - // delete GeoKeys from metadata, because stored in GeoKeyDirectory tag + // cleanup original GeoKeys metadata, because stored in GeoKeyDirectory tag for (const geoKey of geoKeys) { if (metadata.hasOwnProperty(geoKey)) { delete metadata[geoKey]; From ab0852f2b7c4634e871d882d3caccfddcab4c5a8 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Tue, 9 Sep 2025 21:50:21 +0200 Subject: [PATCH 8/9] Fix linting, tests passing --- src/geotiffwriter.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/geotiffwriter.js b/src/geotiffwriter.js index f6af9b16..0030373d 100644 --- a/src/geotiffwriter.js +++ b/src/geotiffwriter.js @@ -439,7 +439,7 @@ export function writeGeotiff(data, metadata) { // Only build ASCII / DOUBLE params if not provided let geoAsciiParams = metadata.GeoAsciiParams || ''; let currentAsciiOffset = geoAsciiParams.length; - let geoDoubleParams = metadata.GeoDoubleParams || []; + const geoDoubleParams = metadata.GeoDoubleParams || []; let currentDoubleIndex = geoDoubleParams.length; // Since geoKeys already sorted and filtered, do a single pass to append to corresponding directory for SHORT/ASCII/DOUBLE @@ -449,9 +449,13 @@ export function writeGeotiff(data, metadata) { const KeyID = Number(name2code[geoKey]); const tagType = fieldTagTypes[KeyID]; const val = metadata[geoKey]; - if (val === undefined) return; + if (val === undefined) { + return; + } - let Count, TIFFTagLocation, valueOffset; + let Count; + let TIFFTagLocation; + let valueOffset; if (tagType === 'SHORT') { Count = 1; TIFFTagLocation = 0; From a14b2e818b1875daa590e648fca57f15d558f6a8 Mon Sep 17 00:00:00 2001 From: Jonathan Chemla Date: Mon, 22 Sep 2025 09:00:18 +0200 Subject: [PATCH 9/9] change write test from closeTo to equal --- test/geotiff.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/geotiff.spec.js b/test/geotiff.spec.js index f2f9b631..1e72fc29 100644 --- a/test/geotiff.spec.js +++ b/test/geotiff.spec.js @@ -1309,8 +1309,8 @@ describe('writeTests', () => { const image = await tiff.getImage(); const geoKeys = image.getGeoKeys(); - expect(geoKeys.GeogSemiMajorAxisGeoKey).to.be.closeTo(6378137.0, 0.000001); - expect(geoKeys.GeogInvFlatteningGeoKey).to.be.closeTo(298.257223563, 0.000001); + expect(geoKeys.GeogSemiMajorAxisGeoKey).to.equal(6378137.0); + expect(geoKeys.GeogInvFlatteningGeoKey).to.equal(298.257223563); expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84'); expect(geoKeys.PCSCitationGeoKey).to.equal('test-ascii'); });