Skip to content
87 changes: 58 additions & 29 deletions src/geotiffwriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,50 +432,79 @@ export function writeGeotiff(data, metadata) {
.filter((key) => endsWith(key, 'GeoKey'))
.sort((a, b) => name2code[a] - name2code[b]);

if (!metadata.GeoAsciiParams) {
let geoAsciiParams = '';
geoKeys.forEach((name) => {
const code = Number(name2code[name]);
const tagType = fieldTagTypes[code];
if (tagType === 'ASCII') {
geoAsciiParams += `${metadata[name].toString()}\u0000`;
}
});
if (geoAsciiParams.length > 0) {
metadata.GeoAsciiParams = geoAsciiParams;
}
}

// 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
if (!metadata.GeoKeyDirectory) {
const NumberOfKeys = geoKeys.length;

const GeoKeyDirectory = [1, 1, 0, NumberOfKeys];
// Only build ASCII / DOUBLE params if not provided
let geoAsciiParams = metadata.GeoAsciiParams || '';
let currentAsciiOffset = geoAsciiParams.length;
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
const GeoKeyDirectory = [1, 1, 0, 0];
let validKeys = 0;
geoKeys.forEach((geoKey) => {
const KeyID = Number(name2code[geoKey]);
GeoKeyDirectory.push(KeyID);
const tagType = fieldTagTypes[KeyID];
const val = metadata[geoKey];
if (val === undefined) {
return;
}

let Count;
let TIFFTagLocation;
let valueOffset;
if (fieldTagTypes[KeyID] === 'SHORT') {
if (tagType === 'SHORT') {
Count = 1;
TIFFTagLocation = 0;
valueOffset = metadata[geoKey];
} else if (geoKey === 'GeogCitationGeoKey') {
Count = metadata.GeoAsciiParams.length;
TIFFTagLocation = Number(name2code.GeoAsciiParams);
valueOffset = 0;
valueOffset = val;
} else if (tagType === 'ASCII') {
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;
}
} else if (tagType === 'DOUBLE') {
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;
}
} else {
console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`);
console.warn(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`);
return;
}
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];
Expand Down
2 changes: 2 additions & 0 deletions src/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export const fieldTagTypes = {
2049: 'ASCII',
2052: 'SHORT',
2054: 'SHORT',
2057: 'DOUBLE',
2059: 'DOUBLE',
2060: 'SHORT',
3072: 'SHORT',
3073: 'ASCII',
Expand Down
27 changes: 27 additions & 0 deletions test/geotiff.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.equal(6378137.0);
expect(geoKeys.GeogInvFlatteningGeoKey).to.equal(298.257223563);
expect(geoKeys.GeogCitationGeoKey).to.equal('WGS 84');
expect(geoKeys.PCSCitationGeoKey).to.equal('test-ascii');
});
});

describe('BlockedSource Test', () => {
Expand Down