Eingegebene Arrays – Binärdaten im Browser

Ilmari Heikkinen

Einleitung

Typisierte Arrays sind eine relativ neue Browserversion, die aus der Notwendigkeit einer effizienten Verarbeitung von Binärdaten in WebGL resultiert. Ein Typed Array ist eine Speicherkarte mit einer typisierten Ansicht, ähnlich wie Arrays in C funktionieren. Da ein typisiertes Array von Roharbeitsspeicher unterstützt wird, kann die JavaScript-Engine den Speicher direkt an native Bibliotheken weiterleiten, ohne die Daten mühsam in eine native Darstellung konvertieren zu müssen. Daher sind typisierte Arrays bei der Weitergabe von Daten an WebGL und andere APIs, die mit Binärdaten arbeiten, viel besser als JavaScript-Arrays.

Eingegebene Arrayansichten verhalten sich wie Arrays mit einem Typ für ein Segment eines ArrayBuffers. Es gibt Ansichten für alle üblichen numerischen Typen mit selbstbeschreibenden Namen wie Float32Array, Float64Array, Int32Array und Uint8Array. Außerdem gibt es eine spezielle Ansicht, die den Pixel-Array-Typ in ImageData von Canvas ersetzt hat: Uint8ClampedArray.

DataView ist der zweite Ansichtstyp und dient der Verarbeitung heterogener Daten. Anstatt eine Array-ähnliche API zu haben, bietet Ihnen das DataView-Objekt eine Get/Set-API zum Lesen und Schreiben beliebiger Datentypen mit beliebigen Byte-Offsets. DataView eignet sich hervorragend zum Lesen und Schreiben von Datei-Headern und anderen strukturähnlichen Daten.

Grundlagen der Verwendung von typisierten Arrays

Eingegebene Array-Ansichten

Um Typed Arrays zu verwenden, müssen Sie einen ArrayBuffer und eine Ansicht dafür erstellen. Am einfachsten ist es, eine typisierte Arrayansicht der gewünschten Größe und des gewünschten Typs zu erstellen.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

Es gibt verschiedene Arten von typisierten Array-Ansichten. Da sie alle dieselbe API verwenden, wissen Sie bereits, wie sie funktioniert. Im nächsten Beispiel werde ich eine der derzeit vorhandenen typisierten Array-Ansichten erstellen.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

Die letzte Option ist etwas Besonderes. Sie grenzt Eingabewerte zwischen 0 und 255 ein. Dies ist besonders praktisch für die Canvas-Bildverarbeitungsalgorithmen, da Sie die Berechnungen für die Bildverarbeitung jetzt nicht mehr manuell anpassen müssen, um ein Überlaufen des 8-Bit-Bereichs zu vermeiden.

So wenden Sie beispielsweise einen Gammafaktor auf ein Bild an, das in einem Uint8Array gespeichert ist. Nicht sehr hübsch:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

Mit Uint8ClampedArray können Sie die manuelle Einschränkung überspringen:

pixels[i] *= gamma;

Eine andere Möglichkeit zum Erstellen typisierter Array-Ansichten besteht darin, zuerst einen ArrayBuffer zu erstellen und dann Ansichten zu erstellen, die darauf verweisen. Die APIs, die externe Daten abrufen, verarbeiten in der Regel ArrayBuffers. Auf diese Weise erhalten Sie eine typisierte Arrayansicht zu diesen.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

Sie können denselben ArrayBuffer auch über mehrere Ansichten erstellen.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

Ein typisiertes Array in ein anderes typisiertes Array lässt sich am schnellsten mit der typisierten Array-Set-Methode kopieren. Erstellen Sie für eine memcpy-ähnliche Verwendung Uint8Arrays in den Puffern der Ansichten und verwenden Sie „set“, um die Daten hinüber zu kopieren.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

Die einfachste Möglichkeit, ArrayBuffers zu verwenden, die Daten mit heterogenen Typen enthalten, ist die Verwendung einer DataView für den Zwischenspeicher. Nehmen wir an, wir haben ein Dateiformat mit einem Header mit einer vorzeichenlosen 8-Bit-Ganzzahl gefolgt von zwei 16-Bit-Ints, gefolgt von einem Payload-Array aus 32-Bit-Gleitkommazahlen. Es ist machbar, dies mit typisierten Array-Ansichten zu lesen, aber etwas mühsam. Mit einer DataView können wir den Header lesen und eine typisierte Array-Ansicht für das Float-Array verwenden.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

Im obigen Beispiel sind alle von mir gelesenen Werte Big-Endian. Wenn die Werte im Puffer Lilit-Endian sind, können Sie den optionalen Parameter "smallEndian" an den Getter übergeben:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

Beachten Sie, dass typisierte Array-Ansichten immer in der nativen Bytereihenfolge vorliegen. Das macht sie schnell. Sie sollten eine DataView verwenden, um Daten zu lesen und zu schreiben, bei denen Endianness ein Problem sein wird.

Die DataView verfügt auch über Methoden zum Schreiben von Werten in Puffer. Diese Setter werden in der gleichen Weise benannt wie die Getter, nämlich "set", gefolgt vom Datentyp.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

Eine Diskussion über Endianness

Die Endianness oder Bytereihenfolge ist die Reihenfolge, in der Multibyte-Zahlen im Arbeitsspeicher des Computers gespeichert werden. Der Begriff Big-Endian beschreibt eine CPU-Architektur, bei der das wichtigste Byte zuerst gespeichert wird, Little-Endian, also das niedrigstwertige Byte zuerst. Welche Endianness in einer bestimmten CPU-Architektur verwendet wird, ist völlig frei wählbar. Es gibt gute Gründe, sich für eine von beiden zu entscheiden. Tatsächlich können einige CPUs so konfiguriert werden, dass sie sowohl Big-Endian- als auch Little-Endian-Daten unterstützen.

Warum musst du dir Sorgen um Endianness machen? Der Grund dafür ist einfach. Beim Lesen oder Schreiben von Daten vom Laufwerk oder aus dem Netzwerk muss die Endianness der Daten angegeben werden. Dadurch wird sichergestellt, dass die Daten korrekt interpretiert werden, unabhängig von der Endianness der CPU, die mit ihnen arbeitet. In unserer zunehmend vernetzten Welt ist es wichtig, alle Arten von Geräten angemessen zu unterstützen, egal ob Big- oder Little-Endian, die mit Binärdaten arbeiten müssen, die von Servern oder anderen Peers im Netzwerk stammen.

Die DataView-Schnittstelle wurde speziell für das Lesen und Schreiben von Daten in und aus Dateien und dem Netzwerk entwickelt. DataView verarbeitet Daten mit einer angegebenen Endianness. Die Endianness, ob groß oder klein, muss bei jedem Zugriff auf jeden Wert festgelegt werden. Dadurch wird sichergestellt, dass Sie beim Lesen oder Schreiben von Binärdaten einheitliche und korrekte Ergebnisse erhalten, unabhängig davon, auf welcher Endianness der CPU, auf der der Browser ausgeführt wird, läuft.

Wenn Ihre Anwendung Binärdaten von einem Server liest, müssen Sie sie in der Regel einmal durchsuchen, um sie in die Datenstrukturen umzuwandeln, die Ihre Anwendung intern verwendet. In dieser Phase sollte DataView verwendet werden. Es ist keine gute Idee, die typisierten Multibyte-Array-Ansichten (Int16Array, Uint16Array usw.) direkt mit Daten zu verwenden, die über XMLHttpRequest, FileReader oder eine andere Input/Output-API abgerufen werden, da die typisierten Array-Ansichten die native Endianness der CPU verwenden. Mehr dazu später.

Sehen wir uns ein paar einfache Beispiele an. Das Dateiformat Windows BMP war in den frühen Tagen von Windows das Standardformat für die Speicherung von Bildern. Aus der oben verlinkten Dokumentation geht eindeutig hervor, dass alle Ganzzahlwerte in der Datei im Little-Endian-Format gespeichert sind. Hier ist ein Code-Snippet, das den Anfang des BMP-Headers mithilfe der DataStream.js-Bibliothek parst, die zu diesem Artikel gehört:

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

Hier ein weiteres Beispiel aus der High Dynamic Range-Rendering-Demo im WebGL-Beispielprojekt. In dieser Demo werden rohe Little-Endian-Gleitkommadaten heruntergeladen, die High Dynamic Range-Texturen darstellen, und müssen anschließend in WebGL hochgeladen werden. Hier ist das Code-Snippet, das die Gleitkommawerte auf allen CPU-Architekturen korrekt interpretiert. Angenommen, die Variable "arrayBuffer" ist ein ArrayBuffer, der gerade über XMLHttpRequest vom Server heruntergeladen wurde:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

Als Faustregel gilt: Nach dem Empfang von Binärdaten vom Webserver wird ein Durchlauf mit einer DataView durchgeführt. Lesen Sie die einzelnen numerischen Werte und speichern Sie sie in einer anderen Datenstruktur, entweder in einem JavaScript-Objekt (für kleine Mengen strukturierter Daten) oder in einer typisierten Array-Ansicht (für große Datenblöcke). Dadurch wird sichergestellt, dass Ihr Code auf allen Arten von CPUs ordnungsgemäß funktioniert. Verwenden Sie DataView auch, um Daten in eine Datei oder das Netzwerk zu schreiben, und achten Sie darauf, das littleEndian-Argument für die verschiedenen set-Methoden entsprechend anzugeben, damit das von Ihnen erstellte oder verwendete Dateiformat erzeugt wird.

Denken Sie daran, dass alle Daten, die über das Netzwerk gehen, implizit ein Format und eine Endianness haben (zumindest für alle Multibyte-Werte). Achten Sie darauf, das Format aller Daten, die Ihre Anwendung über das Netzwerk sendet, klar zu definieren und zu dokumentieren.

Browser-APIs, die Typed Arrays verwenden

Ich gebe Ihnen einen kurzen Überblick über die verschiedenen Browser-APIs, die derzeit Typed Arrays verwenden. Zu den aktuellen Bildausschnitten gehören WebGL, Canvas, Web Audio API, XMLHttpRequests, WebSockets, Web Worker, Media Source API und File APIs. Aus der Liste der APIs geht hervor, dass Typed Arrays sich gut für leistungskritische Multimedia-Arbeiten sowie zur effizienten Weitergabe von Daten eignen.

WebGL

Typisierte Arrays wurden erstmals in WebGL eingesetzt, um Pufferdaten und Bilddaten zu übergeben. Um den Inhalt eines WebGL-Pufferobjekts festzulegen, verwenden Sie den Aufruf gl.bufferData() mit einem Typed Array.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

Typisierte Arrays werden auch zur Weitergabe von Texturdaten verwendet. Hier ist ein einfaches Beispiel für das Übergeben von Texturinhalten mithilfe eines Typed Arrays.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

Sie benötigen auch Typed Arrays, um Pixel aus dem WebGL-Kontext zu lesen.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

Canvas 2D

Seit Kurzem ist das Canvas ImageData-Objekt mit der Spezifikation „Typed Arrays“ kompatibel. Jetzt können Sie eine Typed Arrays-Darstellung der Pixel eines Canvas-Elements abrufen. Dies ist hilfreich, da Sie jetzt auch Canvas-Pixel-Arrays erstellen und bearbeiten können, ohne sich mit dem Canvas-Element herumschlagen zu müssen.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest hat einen Typed Array-Boost erhalten und Sie können jetzt eine Typed Array-Antwort erhalten, anstatt einen JavaScript-String in ein Typed Array parsen zu müssen. Dies ist wirklich praktisch, um abgerufene Daten direkt an Multimedia-APIs zu übergeben und um aus dem Netzwerk abgerufene Binärdateien zu parsen.

Dazu müssen Sie lediglich den "responseType" des XMLHttpRequest-Objekts auf "arraybuffer" setzen.

xhr.responseType = 'arraybuffer';

Denken Sie daran, dass Sie sich der Endianness-Probleme bewusst sein müssen, wenn Sie Daten aus dem Netzwerk herunterladen! Siehe Abschnitt über Endianness oben.

Datei-APIs

Der FileReader kann Dateiinhalte als ArrayBuffer lesen. Anschließend können Sie typisierte Arrayansichten und DataViews an den Zwischenspeicher anhängen, um dessen Inhalt zu bearbeiten.

reader.readAsArrayBuffer(file);

Du solltest auch hier die Endianness im Hinterkopf behalten. Weitere Informationen findest du im Abschnitt „Endianness“.

Übertragbare Objekte

Übertragbare Objekte in postMessage machen die Weitergabe von Binärdaten an andere Fenster und Web Worker erheblich schneller. Wenn Sie ein Objekt als übertragbares Objekt an einen Worker senden, kann im sendenden Thread nicht mehr auf das Objekt zugegriffen werden und der empfangende Worker erhält die Inhaberschaft des Objekts. Dies ermöglicht eine hochgradig optimierte Implementierung, bei der die gesendeten Daten nicht kopiert werden, sondern nur die Eigentumsrechte des Typed Array an den Empfänger übertragen werden.

Um übertragbare Objekte mit Web Workern zu verwenden, müssen Sie die Methode webkitPostMessage für den Worker verwenden. Die Methode webkitPostMessage funktioniert wie postMessage, erfordert jedoch zwei Argumente statt eines. Das hinzugefügte zweite Argument ist ein Array von Objekten, die Sie an den Worker übertragen möchten.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Um die Objekte vom Worker zurückzuerhalten, kann der Worker sie auf dieselbe Weise an den Hauptthread zurückgeben.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

Null Kopien, juhu!

Medienquellen-API

Seit Kurzem gibt es für die Medienelemente auch einige Extras für das Typed Array (Typed Array) in Form der Media Source API. Sie können ein Typed Array mit Videodaten mit „webkitSourceAppend“ direkt an ein Videoelement übergeben. Dadurch hängt das Videoelement die Videodaten an das vorhandene Video an. SourceAppend eignet sich hervorragend für Interstitials, Playlists, Streaming und andere Anwendungen, bei denen Sie mehrere Videos mit einem einzigen Videoelement abspielen möchten.

video.webkitSourceAppend(uint8Array);

Binäre WebSockets

Sie können Typed Arrays auch mit WebSockets verwenden, um zu vermeiden, dass Sie alle Daten aneinanderreihen müssen. Hervorragend geeignet, um effiziente Protokolle zu schreiben und den Netzwerkverkehr zu minimieren.

socket.binaryType = 'arraybuffer';

Puh! Damit sind wir am Ende der API-Überprüfung angelangt. Sehen wir uns nun Drittanbieter-Bibliotheken für die Verarbeitung von Typisierten Arrays an.

Bibliotheken von Drittanbietern

jDataView

jDataView implementiert einen DataView-Shim für alle Browser. DataView war früher eine reine WebKit-Funktion, wird jetzt aber von den meisten anderen Browsern unterstützt. Das Mozilla-Entwicklerteam ist gerade dabei, einen Patch zu entwickeln, um DataView auch für Firefox aktivieren zu können.

Eric Bidelman vom Chrome Developer Relations-Team hat ein kleines Beispiel für einen MP3-ID3-Tag-Reader mit jDataView geschrieben. Hier ist ein Anwendungsbeispiel aus dem Blogpost:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

Stringcodierung

Das Arbeiten mit Strings in Typed Arrays ist im Moment recht mühsam, aber die Stringencoding-Bibliothek hilft Ihnen dabei. Mit der Stringencodierung wird die vorgeschlagene Codierungsspezifikation für Typd Array-Strings implementiert. Sie bietet also auch eine gute Möglichkeit, sich einen Eindruck von den zukünftigen Ereignissen zu verschaffen.

Hier ein einfaches Anwendungsbeispiel für die Stringcodierung:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

Ich habe eine kleine Manipulationsbibliothek für Typed Arrays namens BitView.js geschrieben. Wie der Name schon sagt, funktioniert es ähnlich wie die DataView, außer dass es mit Bits arbeitet. Mit BitView können Sie den Wert eines Bits bei einem bestimmten Bit-Offset in einem ArrayBuffer abrufen und festlegen. BitView verfügt auch über Methoden zum Speichern und Laden von 6-Bit- und 12-Bit-Ints bei beliebigen Bit-Offsets.

12-Bit-Ints eignen sich gut für die Arbeit mit Bildschirmkoordinaten, da Bildschirme in der Regel weniger als 4096 Pixel entlang der längeren Seite haben. Wenn Sie 12-Bit-Ints anstelle von 32-Bit-Ints verwenden, erzielen Sie eine Größenreduzierung um 62 %. Ein extremeres Beispiel: Ich habe mit Shapefiles gearbeitet, die 64-Bit-Gleitkommazahlen für die Koordinaten verwendeten. Dafür benötigte ich keine Präzision, weil das Modell nur in der Bildschirmgröße dargestellt werden sollte. Durch den Wechsel zu 12-Bit-Basiskoordinaten mit 6-Bit-Deltas zur Codierung von Änderungen gegenüber der vorherigen Koordinate wurde die Dateigröße auf ein Zehntel reduziert. Eine Demo dazu können Sie sich hier ansehen.

Hier ein Beispiel für die Verwendung von BitView.js:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

Einer der interessantesten Aspekte typisierter Arrays ist die Erleichterung der Arbeit mit Binärdateien in JavaScript. Anstatt Zeichen für Zeichen einzeln zu parsen und Zeichen manuell in Binärzahlen umzuwandeln, können Sie jetzt einen ArrayBuffer mit XMLHttpRequest abrufen und ihn direkt mit einer DataView verarbeiten. So kannst du beispielsweise ganz einfach eine MP3-Datei laden und die Metadaten-Tags zur Verwendung in deinem Audioplayer lesen. Sie können auch eine Shapefile laden und in ein WebGL-Modell umwandeln. Oder lesen Sie die EXIF-Tags in einer JPEG-Datei und zeigen Sie sie in Ihrer Diaschau-App an.

Das Problem mit ArrayBuffer XHRs ist, dass das Lesen von strukturähnlichen Daten aus dem Puffer etwas mühsam ist. DataView eignet sich zum gleichzeitigen Lesen mehrerer Zahlen in Endian-Sicherheit. Typisierte Array-Ansichten eignen sich gut zum Lesen von Arrays, die auf Elementgröße ausgerichtet sind, native Endian-Zahlen. Was uns fehlte, ist eine Möglichkeit, Arrays und Datenstrukturen auf eine geeignete endian-sichere Weise einzulesen. Geben Sie DataStream.js ein.

DataStream.js ist eine Typed Arrays-Bibliothek, die Skalare, Strings, Arrays und Datenstrukturen aus ArrayBuffers dateiähnliche liest und schreibt.

Beispiel für das Lesen eines Arrays mit Gleitkommazahlen aus einem ArrayBuffer:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

Besonders nützlich ist DataStream.js beim Lesen komplexerer Daten. Angenommen, Sie haben eine Methode, die JPEG-Markierungen einliest:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

Oder verwenden Sie die Methode DataStream.readStruct, um Datenstrukturen einzulesen. Die readStruct-Methode übernimmt ein Struct-Definitions-Array, das die Typen der Struktur-Mitglieder enthält. Es verfügt über Callback-Funktionen für die Verarbeitung komplexer Typen und für Datenarrays und verschachtelte Strukturen:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

Wie Sie sehen, ist die Strukturdefinition ein flaches Array aus [Name, Typ]-Paaren. Verschachtelte Strukturen werden über ein Array für den Typ erstellt. Arrays werden mithilfe eines Arrays mit drei Elementen definiert, wobei das zweite Element der Arrayelementtyp und das dritte Element die Arraylänge ist (entweder als Zahl, als Verweis auf ein zuvor gelesenes Feld oder als Callback-Funktion). Das erste Element der Arraydefinition wird nicht verwendet.

Folgende Werte sind für den Typ möglich:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

Ein Live-Beispiel für das Lesen von JPEG-Metadaten finden Sie hier. In der Demo wird DataStream.js verwendet, um die Struktur der JPEG-Datei auf Tag-Ebene (zusammen mit einem EXIF-Parsing) zu lesen, und jpg.js, um das JPEG-Bild in JavaScript zu decodieren und anzuzeigen.

Verlauf der eingegebenen Arrays

Typisierte Arrays begannen in der frühen Implementierungsphase von WebGL, als wir festgestellt haben, dass die Weitergabe von JavaScript-Arrays an den Grafiktreiber zu Leistungsproblemen führt. Bei JavaScript-Arrays musste die WebGL-Bindung ein natives Array zuweisen und dieses füllen, indem sie das JavaScript-Array durchlaufen und jedes JavaScript-Objekt im Array in den erforderlichen nativen Typ umwandelt.

Um den Engpass bei der Datenkonvertierung zu beheben, schrieb Vladimir Vukicevic von Mozilla CanvasFloatArray: ein Float-Array im C-Stil mit einer JavaScript-Schnittstelle. Jetzt können Sie das CanvasFloatArray in JavaScript bearbeiten und direkt an WebGL übergeben, ohne zusätzliche Arbeit an der Bindung vornehmen zu müssen. In weiteren Iterationen wurde CanvasFloatArray in WebGLFloatArray umbenannt, das wiederum in Float32Array umbenannt und in einen Back-up-ArrayBuffer und die Float32Array-View aufgeteilt wurde, um auf den Puffer zuzugreifen. Es wurden auch Typen für andere Ganzzahl- und Gleitkommagrößen sowie signierte/unsignierte Varianten hinzugefügt.

Designaspekte

Von Anfang an war das Design von Typed Arrays darauf ausgerichtet, Binärdaten effizient an native Bibliotheken zu übergeben. Aus diesem Grund basieren die typisierten Array-Ansichten auf ausgerichteten Daten in der nativen Endianness der Host-CPU. Durch diese Entscheidungen kann JavaScript bei Vorgängen wie dem Senden von Vertexdaten an die Grafikkarte die maximale Leistung erreichen.

DataView wurde speziell für Datei- und Netzwerk-E/A entwickelt, bei denen die Daten immer eine angegebene Endianness haben und möglicherweise nicht auf maximale Leistung ausgerichtet sind.

Die Aufteilung zwischen In-Memory-Datenassemblierung (mithilfe der typisierten Array-Ansichten) und E/A (mit DataView) war bewusst. Moderne JavaScript-Engines optimieren die typisierten Array-Ansichten stark und erzielen damit eine hohe Leistung für numerische Operationen. Die aktuelle Leistungsstärke der typisierten Array-Ansichten wurde durch diese Designentscheidung ermöglicht.

Verweise