-
Notifications
You must be signed in to change notification settings - Fork 184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature write uint8 uint16 uint32 float32 typedarrays #366
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,8 @@ | |
https://github.com/photopea/UTIF.js/blob/master/LICENSE | ||
*/ | ||
import { fieldTagNames, fieldTagTypes, fieldTypeNames, geoKeyNames } from './globals.js'; | ||
import { assign, endsWith, forEach, invert, times } from './utils.js'; | ||
import { assign, endsWith, forEach, invert, times, typeMap, | ||
isTypedUintArray, isTypedIntArray, isTypedFloatArray } from './utils.js'; | ||
|
||
const tagName2Code = invert(fieldTagNames); | ||
const geoKeyName2Code = invert(geoKeyNames); | ||
|
@@ -251,17 +252,47 @@ const encodeImage = (values, width, height, metadata) => { | |
} | ||
|
||
const prfx = new Uint8Array(encodeIfds([ifd])); | ||
const samplesPerPixel = ifd[277]; | ||
|
||
const img = new Uint8Array(values); | ||
const dataType = values.constructor.name; | ||
const TypedArray = typeMap[dataType]; | ||
|
||
const samplesPerPixel = ifd[277]; | ||
let elementSize = 4; | ||
if (TypedArray) { | ||
elementSize = TypedArray.BYTES_PER_ELEMENT; | ||
} | ||
|
||
const data = new Uint8Array(numBytesInIfd + (values.length * elementSize * samplesPerPixel)); | ||
|
||
const data = new Uint8Array(numBytesInIfd + (width * height * samplesPerPixel)); | ||
times(prfx.length, (i) => { | ||
data[i] = prfx[i]; | ||
}); | ||
forEach(img, (value, i) => { | ||
data[numBytesInIfd + i] = value; | ||
|
||
forEach(values, (value, i) => { | ||
if (!TypedArray) { | ||
data[numBytesInIfd + i] = value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would only work if the Array of values is within the 0-255 range. Is there any way we could update this to support Float64 by default? Could we do Open to your thoughts! |
||
return; | ||
} | ||
|
||
const buffer = new ArrayBuffer(elementSize); | ||
const view = new DataView(buffer); | ||
|
||
if (dataType === 'Float32Array') { | ||
view.setFloat32(0, value, false); | ||
} else if (dataType === 'Uint32Array') { | ||
view.setUint32(0, value, false); | ||
} else if (dataType === 'Uint16Array') { | ||
view.setUint16(0, value, false); | ||
} else if (dataType === 'Uint8Array') { | ||
view.setUint8(0, value); | ||
} | ||
|
||
const typedArray = new Uint8Array(view.buffer); | ||
const idx = numBytesInIfd + (i * elementSize); | ||
|
||
for (let j = 0; j < elementSize; j++) { | ||
data[idx + j] = typedArray[j]; | ||
} | ||
}); | ||
|
||
return data.buffer; | ||
|
@@ -328,7 +359,11 @@ export function writeGeotiff(data, metadata) { | |
// consult https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml | ||
|
||
if (!metadata.BitsPerSample) { | ||
metadata.BitsPerSample = times(numBands, () => 8); | ||
let bitsPerSample = 8; | ||
if (ArrayBuffer.isView(flattenedValues)) { | ||
bitsPerSample = 8 * flattenedValues.BYTES_PER_ELEMENT; | ||
} | ||
metadata.BitsPerSample = times(numBands, () => bitsPerSample); | ||
} | ||
|
||
metadataDefaults.forEach((tag) => { | ||
|
@@ -352,7 +387,15 @@ export function writeGeotiff(data, metadata) { | |
|
||
if (!metadata.StripByteCounts) { | ||
// we are only writing one strip | ||
metadata.StripByteCounts = [numBands * height * width]; | ||
|
||
// default for Uint8 | ||
let elementSize = 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we want to change the default to 8 for Float64 instead of Uint8? Ideally, the writer will work for the greatest number of use cases by default. If someone wants Uint8, they could pass in a Uint8Array of values instead of an Array. |
||
|
||
if (ArrayBuffer.isView(flattenedValues)) { | ||
elementSize = flattenedValues.BYTES_PER_ELEMENT; | ||
} | ||
|
||
metadata.StripByteCounts = [numBands * elementSize * height * width]; | ||
} | ||
|
||
if (!metadata.ModelPixelScale) { | ||
|
@@ -361,7 +404,17 @@ export function writeGeotiff(data, metadata) { | |
} | ||
|
||
if (!metadata.SampleFormat) { | ||
metadata.SampleFormat = times(numBands, () => 1); | ||
let sampleFormat = 1; | ||
if (isTypedFloatArray(flattenedValues)) { | ||
sampleFormat = 3; | ||
} | ||
if (isTypedIntArray(flattenedValues)) { | ||
sampleFormat = 2; | ||
} | ||
if (isTypedUintArray(flattenedValues)) { | ||
sampleFormat = 1; | ||
} | ||
metadata.SampleFormat = times(numBands, () => sampleFormat); | ||
} | ||
|
||
// if didn't pass in projection information, assume the popular 4326 "geographic projection" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -156,3 +156,40 @@ export class CustomAggregateError extends Error { | |
} | ||
|
||
export const AggregateError = CustomAggregateError; | ||
|
||
export function isTypedFloatArray(input) { | ||
if (ArrayBuffer.isView(input)) { | ||
const ctr = input.constructor; | ||
if (ctr === Float32Array || ctr === Float64Array) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
export function isTypedIntArray(input) { | ||
if (ArrayBuffer.isView(input)) { | ||
const ctr = input.constructor; | ||
if (ctr === Int8Array || ctr === Int16Array || ctr === Int32Array) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
export function isTypedUintArray(input) { | ||
if (ArrayBuffer.isView(input)) { | ||
const ctr = input.constructor; | ||
if (ctr === Uint8Array || ctr === Uint16Array || ctr === Uint32Array || ctr === Uint8ClampedArray) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch also including the clamped version of Uint8! |
||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
export const typeMap = { | ||
Float32Array, | ||
Uint32Array, | ||
Uint16Array, | ||
Uint8Array, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,6 +78,20 @@ function normalize(input) { | |
return JSON.stringify(toArrayRecursively(input)); | ||
} | ||
|
||
function generateTestDataArray(min, max, length, onlyWholeNumbers) { | ||
const data = []; | ||
|
||
for (let i = 0; i < length; i++) { | ||
let randomValue = (Math.random() * (max - min + 1)) + min; | ||
if (onlyWholeNumbers) { | ||
randomValue = Math.floor(randomValue); | ||
} | ||
data.push(randomValue); | ||
} | ||
|
||
return data; | ||
} | ||
|
||
function getMockMetaData(height, width) { | ||
return { | ||
ImageWidth: width, // only necessary if values aren't multi-dimensional | ||
|
@@ -103,6 +117,41 @@ function getMockMetaData(height, width) { | |
}; | ||
} | ||
|
||
describe('writeTypedArrays', () => { | ||
const dataLength = 512 * 512 * 4; | ||
|
||
const variousDataTypeExamples = [ | ||
generateTestDataArray(0, 255, dataLength, true), | ||
new Uint8Array(generateTestDataArray(0, 255, dataLength, true)), | ||
new Uint16Array(generateTestDataArray(0, 65535, dataLength, true)), | ||
new Uint32Array(generateTestDataArray(0, 4294967295, dataLength, true)), | ||
new Float32Array(generateTestDataArray(-3.4e+38, 3.4e+38, dataLength, false)), | ||
]; | ||
|
||
const height = Math.sqrt(dataLength); | ||
const width = Math.sqrt(dataLength); | ||
|
||
for (let s = 0; s < variousDataTypeExamples.length; ++s) { | ||
const originalValues = variousDataTypeExamples[s]; | ||
const dataType = originalValues.constructor.name; | ||
|
||
it(`should write ${dataType}`, async () => { | ||
const metadata = { | ||
height, | ||
width, | ||
}; | ||
|
||
const newGeoTiffAsBinaryData = await writeArrayBuffer((originalValues), metadata); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you have an extra parens wrapping |
||
const newGeoTiff = await fromArrayBuffer(newGeoTiffAsBinaryData); | ||
const image = await newGeoTiff.getImage(); | ||
const newValues = await image.readRasters(); | ||
const valueArray = toArrayRecursively(newValues)[0]; | ||
const originalValueArray = Array.from(originalValues); | ||
expect(valueArray).to.be.deep.equal(originalValueArray); | ||
}); | ||
} | ||
}); | ||
|
||
describe('GeoTIFF - external overviews', () => { | ||
it('Can load', async () => { | ||
const tiff = await fromUrls( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add a comment describing why you set the default to 4?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we consider defaulting elementSize to 8, so we can support all floating point numbers by default?