Skip to content

Commit dcb9154

Browse files
Merge pull request #463 from geotiffjs/feature/write-typedarrays
2 parents 8618f8a + 06d4385 commit dcb9154

File tree

4 files changed

+166
-11
lines changed

4 files changed

+166
-11
lines changed

src/geotiffwriter.js

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
You can view that here:
55
https://github.com/photopea/UTIF.js/blob/master/LICENSE
66
*/
7-
import { fieldTagNames, fieldTagTypes, fieldTypeNames, geoKeyNames } from './globals.js';
8-
import { assign, endsWith, forEach, invert, times } from './utils.js';
7+
import { fieldTags, fieldTagNames, fieldTagTypes, fieldTypeNames, geoKeyNames } from './globals.js';
8+
import { assign, endsWith, forEach, invert, times, typeMap,
9+
isTypedUintArray, isTypedIntArray, isTypedFloatArray } from './utils.js';
910

1011
const tagName2Code = invert(fieldTagNames);
1112
const geoKeyName2Code = invert(geoKeyNames);
@@ -251,17 +252,50 @@ const encodeImage = (values, width, height, metadata) => {
251252
}
252253

253254
const prfx = new Uint8Array(encodeIfds([ifd]));
255+
const samplesPerPixel = ifd[fieldTags.SamplesPerPixel];
254256

255-
const img = new Uint8Array(values);
257+
const dataType = values.constructor.name;
258+
const TypedArray = typeMap[dataType];
256259

257-
const samplesPerPixel = ifd[277];
260+
// default for Float64
261+
let elementSize = 8;
262+
if (TypedArray) {
263+
elementSize = TypedArray.BYTES_PER_ELEMENT;
264+
}
265+
266+
const data = new Uint8Array(numBytesInIfd + (values.length * elementSize * samplesPerPixel));
258267

259-
const data = new Uint8Array(numBytesInIfd + (width * height * samplesPerPixel));
260268
times(prfx.length, (i) => {
261269
data[i] = prfx[i];
262270
});
263-
forEach(img, (value, i) => {
264-
data[numBytesInIfd + i] = value;
271+
272+
forEach(values, (value, i) => {
273+
if (!TypedArray) {
274+
data[numBytesInIfd + i] = value;
275+
return;
276+
}
277+
278+
const buffer = new ArrayBuffer(elementSize);
279+
const view = new DataView(buffer);
280+
281+
if (dataType === 'Float64Array') {
282+
view.setFloat64(0, value, false);
283+
} else if (dataType === 'Float32Array') {
284+
view.setFloat32(0, value, false);
285+
} else if (dataType === 'Uint32Array') {
286+
view.setUint32(0, value, false);
287+
} else if (dataType === 'Uint16Array') {
288+
view.setUint16(0, value, false);
289+
} else if (dataType === 'Uint8Array') {
290+
view.setUint8(0, value);
291+
}
292+
293+
const typedArray = new Uint8Array(view.buffer);
294+
const idx = numBytesInIfd + (i * elementSize);
295+
296+
for (let j = 0; j < elementSize; j++) {
297+
data[idx + j] = typedArray[j];
298+
}
265299
});
266300

267301
return data.buffer;
@@ -328,7 +362,11 @@ export function writeGeotiff(data, metadata) {
328362
// consult https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml
329363

330364
if (!metadata.BitsPerSample) {
331-
metadata.BitsPerSample = times(numBands, () => 8);
365+
let bitsPerSample = 8;
366+
if (ArrayBuffer.isView(flattenedValues)) {
367+
bitsPerSample = 8 * flattenedValues.BYTES_PER_ELEMENT;
368+
}
369+
metadata.BitsPerSample = times(numBands, () => bitsPerSample);
332370
}
333371

334372
metadataDefaults.forEach((tag) => {
@@ -352,7 +390,15 @@ export function writeGeotiff(data, metadata) {
352390

353391
if (!metadata.StripByteCounts) {
354392
// we are only writing one strip
355-
metadata.StripByteCounts = [numBands * height * width];
393+
394+
// default for Float64
395+
let elementSize = 8;
396+
397+
if (ArrayBuffer.isView(flattenedValues)) {
398+
elementSize = flattenedValues.BYTES_PER_ELEMENT;
399+
}
400+
401+
metadata.StripByteCounts = [numBands * elementSize * height * width];
356402
}
357403

358404
if (!metadata.ModelPixelScale) {
@@ -361,7 +407,17 @@ export function writeGeotiff(data, metadata) {
361407
}
362408

363409
if (!metadata.SampleFormat) {
364-
metadata.SampleFormat = times(numBands, () => 1);
410+
let sampleFormat = 1;
411+
if (isTypedFloatArray(flattenedValues)) {
412+
sampleFormat = 3;
413+
}
414+
if (isTypedIntArray(flattenedValues)) {
415+
sampleFormat = 2;
416+
}
417+
if (isTypedUintArray(flattenedValues)) {
418+
sampleFormat = 1;
419+
}
420+
metadata.SampleFormat = times(numBands, () => sampleFormat);
365421
}
366422

367423
// if didn't pass in projection information, assume the popular 4326 "geographic projection"

src/globals.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,18 @@ export const fieldTagTypes = {
161161
514: 'LONG',
162162
1024: 'SHORT',
163163
1025: 'SHORT',
164+
1026: 'ASCII',
164165
2048: 'SHORT',
165166
2049: 'ASCII',
167+
2052: 'SHORT',
168+
2054: 'SHORT',
169+
2060: 'SHORT',
166170
3072: 'SHORT',
167171
3073: 'ASCII',
172+
3076: 'SHORT',
173+
4096: 'SHORT',
174+
4097: 'ASCII',
175+
4099: 'SHORT',
168176
33432: 'ASCII',
169177
33550: 'DOUBLE',
170178
33922: 'DOUBLE',

src/utils.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,41 @@ export class CustomAggregateError extends Error {
156156
}
157157

158158
export const AggregateError = CustomAggregateError;
159+
160+
export function isTypedFloatArray(input) {
161+
if (ArrayBuffer.isView(input)) {
162+
const ctr = input.constructor;
163+
if (ctr === Float32Array || ctr === Float64Array) {
164+
return true;
165+
}
166+
}
167+
return false;
168+
}
169+
170+
export function isTypedIntArray(input) {
171+
if (ArrayBuffer.isView(input)) {
172+
const ctr = input.constructor;
173+
if (ctr === Int8Array || ctr === Int16Array || ctr === Int32Array) {
174+
return true;
175+
}
176+
}
177+
return false;
178+
}
179+
180+
export function isTypedUintArray(input) {
181+
if (ArrayBuffer.isView(input)) {
182+
const ctr = input.constructor;
183+
if (ctr === Uint8Array || ctr === Uint16Array || ctr === Uint32Array || ctr === Uint8ClampedArray) {
184+
return true;
185+
}
186+
}
187+
return false;
188+
}
189+
190+
export const typeMap = {
191+
Float64Array,
192+
Float32Array,
193+
Uint32Array,
194+
Uint16Array,
195+
Uint8Array,
196+
};

test/geotiff.spec.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@ function normalize(input) {
7878
return JSON.stringify(toArrayRecursively(input));
7979
}
8080

81+
function generateTestDataArray(min, max, length, onlyWholeNumbers) {
82+
const data = [];
83+
84+
for (let i = 0; i < length; i++) {
85+
let randomValue = (Math.random() * (max - min + 1)) + min;
86+
if (onlyWholeNumbers) {
87+
randomValue = Math.floor(randomValue);
88+
}
89+
data.push(randomValue);
90+
}
91+
92+
return data;
93+
}
94+
8195
function getMockMetaData(height, width) {
8296
return {
8397
ImageWidth: width, // only necessary if values aren't multi-dimensional
@@ -103,6 +117,42 @@ function getMockMetaData(height, width) {
103117
};
104118
}
105119

120+
describe('writeTypedArrays', () => {
121+
const dataLength = 512 * 512 * 4;
122+
123+
const variousDataTypeExamples = [
124+
generateTestDataArray(0, 255, dataLength, true),
125+
new Uint8Array(generateTestDataArray(0, 255, dataLength, true)),
126+
new Uint16Array(generateTestDataArray(0, 65535, dataLength, true)),
127+
new Uint32Array(generateTestDataArray(0, 4294967295, dataLength, true)),
128+
new Float32Array(generateTestDataArray(-3.4e+38, 3.4e+38, dataLength, false)),
129+
new Float64Array(generateTestDataArray(Number.MIN_VALUE, Number.MAX_VALUE, dataLength, false)),
130+
];
131+
132+
const height = Math.sqrt(dataLength);
133+
const width = Math.sqrt(dataLength);
134+
135+
for (let s = 0; s < variousDataTypeExamples.length; ++s) {
136+
const originalValues = variousDataTypeExamples[s];
137+
const dataType = originalValues.constructor.name;
138+
139+
it(`should write ${dataType}`, async () => {
140+
const metadata = {
141+
height,
142+
width,
143+
};
144+
145+
const newGeoTiffAsBinaryData = await writeArrayBuffer(originalValues, metadata);
146+
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData);
147+
const image = await newGeoTiff.getImage();
148+
const newValues = await image.readRasters();
149+
const valueArray = toArrayRecursively(newValues)[0];
150+
const originalValueArray = Array.from(originalValues);
151+
expect(valueArray).to.be.deep.equal(originalValueArray);
152+
});
153+
}
154+
});
155+
106156
describe('GeoTIFF - external overviews', () => {
107157
it('Can load', async () => {
108158
const tiff = await fromUrls(
@@ -1112,7 +1162,10 @@ describe('writeTests', () => {
11121162
[0, 0, 0],
11131163
[255, 255, 255],
11141164
];
1115-
const originalValues = [originalRed, originalGreen, originalBlue];
1165+
const interleaved = originalRed.flatMap((row, rowIdx) => row.flatMap((value, colIdx) => [
1166+
value, originalGreen[rowIdx][colIdx], originalBlue[rowIdx][colIdx],
1167+
]));
1168+
const originalValues = new Uint8Array(interleaved);
11161169
const metadata = {
11171170
height: 3,
11181171
width: 3,

0 commit comments

Comments
 (0)