Hi everyone, and welcome to another exciting edition of Boring JavaScript! Today, we tackle the ‘TypedArray’ construct in JavaScript. Although, we’re really not talking about ‘TypedArray’ – there is no such a thing. But there are a number of classes that are grouped under the term ‘TypedArray’, so we’ll cover a few of them and show you how the same concepts can be used for all of them.
Don’t like to read? Then watch our video.
You’re Just Not My Type
Basically, a TypedArray maps an array to an ArrayBuffer, which in turn is mapped to a series of bytes. Within this mapping, one could have values such as integers, floats, and strings associated with physical bytes within memory, instead of having JavaScript create them for you.
For example, suppose you had read an image – let’s say JPG – for later file manipulation. The data returned will be a series of bytes laid out in memory. You need to get access to those bytes and interpret them in a way that is meaning to you. That is where TypedArrays come in.
If the first eight bytes of the file represent four words (a ‘word’ being 2 bytes), each of them unsigned integers, then you can get access to this data thusly:
// 'jpgImage.read' below is a custom object. It really doesn't exist except in this example
const buffer = jpgImage.read('./myimage.jpg'); // returns an ArrayBuffer of the bytes in the JPG
const header = new Uint16Array(buffer, 0, 4); // points to the first eight bytes
console.log(header); // shows an array of four unsigned intergers, range 0 - 65535
In the example above, Line 3, we use the TypedArray called ‘Uint16Array’ to specify that the first eight bytes (starting at Location 0 and going for 4 elements) will be assigned to four unsigned integers and stored in an array of four integer.
What if the first eight bytes represent two floating point numbers? TypedArrays have that covered, too:
// 'jpgImage.read' below is a custom object. It really doesn't exist except in this example
const buffer = jpgImage.read('./myimage.jpg'); // returns an ArrayBuffer of the bytes in the JPG
const header = new Float32Array(buffer, 0, 2); // points to the first eight bytes
console.log(header); // shows an array of two floating point numbers
In fact, there are 11 different TypedArrays you can choose from. The best place to see them are in the Mozilla Development Documentation.
The key here to remember is that these TypedArrays are mapped to individual bytes within memory, always represented by an ArrayBuffer.
The Map’s The Thing
Let’s take this to the next logical step. Suppose you read in a binary file that had a sequence of data values within a ‘header’. They are a mixture of signed integers, unsigned integers, strings, and what not. They’re not all the same type. Can TypedArrays deal with that?
Of course! Let’s look at this header for an imaginary file:
header:
unsigned word Signature
unsigned byte Status
signed byte Weight
char(8) Title
float Value
Here, our binary file has a header of 16 bytes (1 word = 2 bytes, 1 byte = 1 byte, 1 char = 1 byte, 1 float = 4 bytes). We can use TypedArrays to map values to this header. It would look something like this:
const buffer = getBufferData(); // getting the data from somewhere
const header = {
signature: new Uint16Array(buffer, 0, 1)[0],
status: new Uint8Array(buffer, 2, 1)[0],
weight: new Int8Array(buffer, 3, 1)[0],
title: String.fromCharCode.apply(null, new Uint8Array(buffer, 4, 8)),
value: new Float32Array(buffer, 12, 1)[0]
};
console.log(header);
/* output (for Example)
header {
signature: 6773,
status: 4,
weight: -5,
title: 'ABCDEFGH',
value: 74.556
}
*/
Here, we’ve been able to mix and match our TypedArrays to read different parts of a series of bytes.
Hold Your Horses, Tex.
As great as the last example was, frankly, it fails in the real world.
Here is the actual code to map a ZIP file header (truncated for only the first compressed file in the ZIP file):
this.header = {
signature: new Uint32Array(buffer, start, 1),
version: new Uint16Array(buffer, start + 4, 1),
bitFlag: new Uint16Array(buffer, start + 6, 1),
compressionMethod: new Uint16Array(buffer, start + 8, 1),
lastModFileTime: new Uint16Array(buffer, start + 10, 1),
lastModFileDate: new Uint16Array(buffer, start + 12, 1),
crc32: new Uint32Array(buffer, start + 14, 1),
compressedSize: new Uint32Array(buffer, start + 18, 1),
uncompressedSize: new Uint32Array(buffer, start + 22, 1),
fileNameLength: new Uint16Array(buffer, start + 26, 1),
extraFieldLength: new Uint16Array(buffer, start + 28, 1)
}
Run the above code, and you will get a nasty little error like this:
crc32: new Uint32Array(buffer, start + 14, 1),
^
RangeError: start offset of Uint32Array should be a multiple of 4
What gives? When using TypedArrays, each one must begin on the a byte boundary that is specific to the size of the TypedArray. For the ‘8’ arrays, that is 1 byte. For the ’16’ arrays, that is 2 bytes. For ’32’ arrays, it’s 4 bytes, and for ’64’ arrays, it is 8 bytes.
And there in lies the problem with using TypedArrays. It works great as long as your data is all of one type, else you have to take great care in ensuring you read everything on the correct boundary. And real life doesn’t work like that.
What can you do? You can write helper functions to move the data to another buffer (as we have done in our video example), or use another JavaScript construct called ‘Dataview’ (which we’ll cover in a later blog and video).
The Video
Here is the video that helps explain it all.
Shameless Plug
Check us out everywhere!
Check out all our videos at: https://www.boringjavascript.com
Check out everything at: https://www.thevirtuoid.com
Facebook: https://www.facebook.com/TheVirtuoid
Twitter: https://twitter.com/TheVirtuoid
YouTube: https://www.youtube.com/channel/UCKZ7CV6fI7xlh7zIE9TWqgw
Categories: Boring JavaScript Javascript
thevirtuoid
Web Tinkerer. No, not like Tinkerbell.
Creator of the game Virtuoid. Boring JavaScript. Visit us at thevirtuoid.com
Leave a Reply