Eingegebene Arrays – Binärdaten im Browser

Ilmari Heikkinen

Typisierte Arrays sind eine relativ neue Funktion in Browsern, die aus der Notwendigkeit entstanden ist, eine effiziente Möglichkeit zum Umgang mit Binärdaten in WebGL zu haben. Ein typisiertes Array ist ein Speicherblock mit einer typisierten Ansicht, ähnlich wie Arrays in C funktionieren. Da ein typisiertes Array auf Rohspeicher basiert, kann der JavaScript-Engine den Speicher direkt an native Bibliotheken übergeben, ohne die Daten mühsam in eine native Darstellung umwandeln zu müssen. Daher eignen sich typisierte Arrays viel besser als JavaScript-Arrays, um Daten an WebGL und andere APIs zu übergeben, die mit Binärdaten arbeiten.

Typisierte Array-Ansichten verhalten sich wie Arrays vom Typ "Einzeln" für ein Segment eines ArrayBuffer. Es gibt Ansichten für alle üblichen numerischen Typen mit selbstbeschreibenden Namen wie Float32Array, Float64Array, Int32Array und Uint8Array. Es gibt auch eine spezielle Ansicht, die den Pixelarraytyp in der ImageData von Canvas ersetzt hat: Uint8ClampedArray.

DataView ist der zweite Ansichtstyp. Er ist für die Verarbeitung heterogener Daten vorgesehen. Anstelle eines Array-ähnlichen APIs bietet 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 Dateiheadern und anderen ähnlichen strukturähnlichen Daten.

Grundlagen der Verwendung von typisierten Arrays

Ansichten mit typisierten Arrays

Wenn Sie typisierte Arrays verwenden möchten, müssen Sie ein ArrayBuffer und eine Ansicht dazu 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. Sie alle verwenden dieselbe API. Wenn Sie also wissen, wie eine davon verwendet wird, wissen Sie sozusagen, wie sie alle verwendet werden. Im nächsten Beispiel erstelle ich jeweils eine der derzeit vorhandenen typisierten Array-Ansichten.

// 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);

Der letzte ist etwas Besonderes: Er begrenzt Eingabewerte auf 0 bis 255. Das ist besonders praktisch für Canvas-Bildverarbeitungsalgorithmen, da Sie die mathematischen Berechnungen für die Bildverarbeitung jetzt nicht mehr manuell begrenzen müssen, um einen Überlauf des 8‑Bit-Bereichs zu vermeiden.

So wenden Sie beispielsweise einen Gammafaktor auf ein Bild an, das in einem Uint8Array gespeichert ist: Nicht sehr schön:

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

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

pixels[i] *= gamma;

Die andere Möglichkeit zum Erstellen von typisierten Arrays besteht darin, zuerst ein ArrayBuffer zu erstellen und dann Ansichten zu erstellen, die darauf verweisen. Die APIs, über die Sie externe Daten abrufen, arbeiten in der Regel mit ArrayBuffers. So erhalten Sie eine typisierte Arrayansicht für diese.

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);

Es ist auch möglich, mehrere Ansichten für denselben ArrayBuffer zu haben.

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

Wenn Sie ein typisiertes Array in ein anderes typisiertes Array kopieren möchten, ist die schnellste Methode die Verwendung der Set-Methode für typisierte Arrays. Für eine memcpy-ähnliche Verwendung erstellen Sie Uint8Arrays für die Buffers der Ansichten und verwenden Sie set, um die Daten 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

Wenn Sie ArrayBuffers mit Daten mit heterogenen Typen verwenden möchten, ist es am einfachsten, eine DataView für den Buffer zu verwenden. Angenommen, wir haben ein Dateiformat mit einem Header mit einer 8-Bit-int-int, gefolgt von zwei 16-Bit-Ganzzahlen, gefolgt von einem Nutzlast-Array aus 32-Bit-Gleitkommazahlen. Das Lesen dieser Daten mit typisierten Array-Ansichten ist möglich, aber etwas mühsam. Mit einer DataView können wir die Kopfzeile lesen und eine typisierte Arrayansicht 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 gelesenen Werte Big-Endian. Wenn die Werte im Zwischenspeicher "Little-Endian" sind, können Sie den optionalen Parameter "lessEndian" 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 Arrayansichten immer in der nativen Bytereihenfolge vorliegen. Das soll sie beschleunigen. Sie sollten eine DataView verwenden, um Daten zu lesen und zu schreiben, bei denen das Endianness-Problem auftritt.

DataView verfügt auch über Methoden zum Schreiben von Werten in Puffer. Diese Setter werden genauso benannt wie die Getter: „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 das Endianness-Konzept

Die Endianness oder Byte-Reihenfolge ist die Reihenfolge, in der Mehrbyte-Zahlen im Arbeitsspeicher des Computers gespeichert werden. Der Begriff Big-Endian beschreibt eine CPU-Architektur, in der das höchstwertige Byte zuerst und Little-Endian das niedrigstwertige Byte zuerst gespeichert wird. Welche Endianness in einer bestimmten CPU-Architektur verwendet wird, ist völlig willkürlich. Es gibt gute Gründe, sich für eine der beiden zu entscheiden. Einige CPUs können sogar so konfiguriert werden, dass sie sowohl Big-Endian- als auch Little-Endian-Daten unterstützen.

Warum müssen Sie sich um das Endianness kümmern? Der Grund dafür ist einfach. Beim Lesen oder Schreiben von Daten von der Festplatte oder dem Netzwerk muss die Endianität der Daten angegeben werden. So werden die Daten unabhängig von der Endianness der CPU, mit der sie verarbeitet werden, richtig interpretiert. In unserer zunehmend vernetzten Welt ist es wichtig, alle Arten von Geräten, Big- oder Little-Endian, ordnungsgemäß zu unterstützen, die möglicherweise mit Binärdaten von Servern oder anderen Peers im Netzwerk arbeiten müssen.

Die DataView-Schnittstelle wurde speziell für das Lesen und Schreiben von Daten in Dateien und im Netzwerk entwickelt. DataView verarbeitet Daten mit einer angegebenen Endianness. Die Endianness (Big- oder Little-Endian) muss bei jedem Zugriff auf jeden Wert angegeben werden, damit beim Lesen oder Schreiben von Binärdaten konsistente und korrekte Ergebnisse erzielt werden, unabhängig von der Endianness der CPU, auf der der Browser ausgeführt wird.

Wenn Ihre Anwendung binäre Daten 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 nicht empfehlenswert, die mehrzeiligen typisierten Array-Ansichten (Int16Array, Uint16Array usw.) direkt mit Daten zu verwenden, die über XMLHttpRequest, FileReader oder eine andere Eingabe-/Ausgabe-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 zum Speichern von Bildern. In der oben verlinkten Dokumentation wird deutlich darauf hingewiesen, 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 Bibliothek DataStream.js aus diesem Artikel analysiert:

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 ist ein weiteres Beispiel aus der High Dynamic Range-Rendering-Demo im WebGL-Beispielprojekt. In dieser Demo werden rohe Little-Endian-Gleitkommadaten heruntergeladen, die Texturen mit hohem Dynamikbereich darstellen, und in WebGL hochgeladen. Hier ist das Code-Snippet, mit dem die Gleitkommawerte auf allen CPU-Architekturen korrekt interpretiert werden. Angenommen, die Variable „arrayBuffer“ ist ein ArrayBuffer, das 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: Wenn Sie binäre Daten vom Webserver empfangen, sollten Sie sie einmal mit einer DataView durchlaufen. Die einzelnen numerischen Werte lesen und in einer anderen Datenstruktur speichern, entweder in einem JavaScript-Objekt (für kleine Mengen strukturierter Daten) oder in einer typisierten Arrayansicht (für große Datenblöcke). Dadurch wird sichergestellt, dass der Code auf allen Arten von CPUs korrekt funktioniert. Verwenden Sie DataView auch, um Daten in eine Datei oder in das Netzwerk zu schreiben. Achten Sie darauf, das Argument littleEndian für die verschiedenen set-Methoden korrekt anzugeben, um das von Ihnen erstellte oder verwendete Dateiformat zu generieren.

Denken Sie daran, dass alle Daten, die über das Netzwerk geleitet werden, implizit ein Format und eine Endianness haben (zumindest bei Multibyte-Werten). Das Format aller Daten, die Ihre Anwendung über das Netzwerk sendet, muss klar definiert und dokumentiert sein.

Browser-APIs, die typisierte Arrays verwenden

Ich gebe Ihnen einen kurzen Überblick über die verschiedenen Browser-APIs, die derzeit typisierte Arrays verwenden. Dazu gehören WebGL, Canvas, Web Audio API, XMLHttpRequests, WebSockets, Web Workers, Media Source API und File APIs. Der Liste der APIs können Sie entnehmen, dass typisierte Arrays sich gut für leistungsorientierte Multimedia-Arbeiten sowie für die effiziente Weitergabe von Daten eignen.

WebGL

Typed Arrays wurden zum ersten Mal in WebGL verwendet, um Puffer- und Bilddaten zu übergeben. Um den Inhalt eines WebGL-Pufferobjekts festzulegen, verwenden Sie den Aufruf gl.bufferData() mit einem typisierten Array.

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

Typisierte Arrays werden auch zum Übergeben von Texturdaten verwendet. Hier ein einfaches Beispiel für die Weitergabe von Texturinhalten mit einem typisierten Array.

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

Das Canvas ImageData-Objekt wurde vor Kurzem so angepasst, dass es mit der Spezifikation für typisierte Arrays funktioniert. Sie können jetzt eine typisierte Arrays-Darstellung der Pixel in einem Canvas-Element abrufen. Dies ist hilfreich, da Sie jetzt auch Canvas-Pixel-Arrays erstellen und bearbeiten können, ohne mit dem Canvas-Element herumzuarbeiten.

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

XMLHttpRequest2

XMLHttpRequest wurde um Typed Array-Unterstützung erweitert. Sie können jetzt eine Typed Array-Antwort erhalten, anstatt einen JavaScript-String in ein Typed Array parsen zu müssen. Das ist sehr praktisch, um abgerufene Daten direkt an Multimedia-APIs weiterzuleiten und binäre Dateien zu parsen, die aus dem Netzwerk abgerufen wurden.

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

xhr.responseType = 'arraybuffer';

Denken Sie daran, dass Sie beim Herunterladen von Daten aus dem Netzwerk auf Endianness-Probleme achten müssen. Weitere Informationen finden Sie oben im Abschnitt zu Endianness.

Datei-APIs

Der FileReader kann Dateiinhalte als ArrayBuffer lesen. Sie können dann typisierte Arrayansichten und DataViews an den Puffer anhängen, um seinen Inhalt zu bearbeiten.

reader.readAsArrayBuffer(file);

Auch hier sollten Sie die Endianness berücksichtigen. Weitere Informationen finden Sie im Abschnitt zu Big- und Little-Endian-Formaten.

Übertragbare Objekte

Übertragbare Objekte in postMessage ermöglichen die wesentlich schnellere Übertragung von Binärdaten an andere Fenster und Webworker. Wenn Sie ein Objekt als übertragbar an einen Worker senden, ist es im Sendethread nicht mehr zugänglich und der Empfänger-Worker erhält die Eigentümerschaft des Objekts. Dies ermöglicht eine hoch optimierte Implementierung, bei der die gesendeten Daten nicht kopiert werden, sondern nur die Inhaberschaft des typisierten Arrays an den Empfänger übertragen wird.

Wenn Sie übertragbare Objekte mit Webworkern verwenden möchten, müssen Sie die Methode webkitPostMessage auf dem Worker verwenden. Die Methode webkitPostMessage funktioniert genauso wie postMessage, nimmt aber zwei statt nur ein Argument entgegen. Das hinzugefügte zweite Argument ist ein Array von Objekten, die an den Worker übertragen werden sollen.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Um die Objekte vom Worker zurückzubekommen, kann der Worker sie auf die gleiche Weise an den Haupt-Thread zurückgeben.

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

Keine Kopien, super!

Media Source API

Vor Kurzem wurden die Medienelemente um die Media Source API mit typisierten Arrays erweitert. Mit webkitSourceAppend können Sie ein typisiertes Array mit Videodaten direkt an ein Videoelement übergeben. Dadurch werden die Videodaten an das vorhandene Video angehängt. SourceAppend eignet sich hervorragend für Interstitials, Playlists, Streaming und andere Anwendungen, bei denen mehrere Videos mit einem einzigen Videoelement wiedergegeben werden sollen.

video.webkitSourceAppend(uint8Array);

Binäre WebSockets

Sie können auch typisierte Arrays mit WebSockets verwenden, um nicht alle Daten in eine String-Datei umwandeln zu müssen. Ideal für das Schreiben effizienter Protokolle und das Minimieren des Netzwerktraffics.

socket.binaryType = 'arraybuffer';

Puh! Damit ist die API-Überprüfung abgeschlossen. Sehen wir uns nun Drittanbieterbibliotheken zur Verarbeitung typisierter Arrays an.

Bibliotheken von Drittanbietern

jDataView

jDataView implementiert einen DataView-Shim für alle Browser. DataView war früher eine reine WebKit-Funktion, wird aber jetzt von den meisten anderen Browsern unterstützt. Das Mozilla-Entwicklerteam arbeitet derzeit an einem Patch, um DataView auch in Firefox zu ermöglichen.

Eric Bidelman vom Chrome Developer Relations-Team hat ein kleines Beispiel für einen MP3-ID3-Tag-Reader mit jDataView verfasst. 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.
}

stringencoding

Die Arbeit mit Strings in typisierten Arrays ist derzeit etwas mühsam, aber es gibt die StringEncoding-Bibliothek, die dabei hilft. Bei der Stringcodierung wird die vorgeschlagene Spezifikation für die Codierung von typisiertem Array-String implementiert. Sie bietet also eine gute Möglichkeit, einen Eindruck von zukünftigen Änderungen zu erhalten.

Hier ein einfaches Beispiel für die Verwendung der Stringcodierung:

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

BitView.js

Ich habe eine kleine Bitmanipulationsbibliothek für typisierte Arrays namens BitView.js geschrieben. Wie der Name schon sagt, funktioniert sie ähnlich wie die DataView, nur dass sie mit Bits arbeitet. Mit BitView können Sie den Wert eines Bits an einem bestimmten Bitoffset in einem ArrayBuffer abrufen und festlegen. BitView bietet auch Methoden zum Speichern und Laden von 6‑Bit- und 12‑Bit-Ints an beliebigen Bitoffsets.

12‑Bit-Ints eignen sich gut für die Arbeit mit Bildschirmkoordinaten, da Displays in der Regel weniger als 4.096 Pixel in der längeren Dimension haben. Wenn Sie 12‑Bit-Ints anstelle von 32‑Bit-Ints verwenden, können Sie die Größe um 62 % reduzieren. Ein extremeres Beispiel: Ich habe mit Shapefiles gearbeitet, die 64-Bit-Floats für die Koordinaten verwenden. Diese Genauigkeit war jedoch nicht erforderlich, da das Modell nur in Bildschirmgröße angezeigt werden sollte. Durch die Umstellung auf 12-Bit-Basiskoordinaten mit 6-Bit-Deltas zur Codierung von Änderungen gegenüber der vorherigen Koordinate konnte die Dateigröße auf ein Zehntel reduziert werden. Eine Demo dazu finden Sie hier.

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

Eines der interessantesten Aspekte an typisierten Arrays ist, dass sie die Arbeit mit Binärdateien in JavaScript erleichtern. Anstatt einen String Zeichen für Zeichen zu parsen und die Zeichen manuell in Binärzahlen umzuwandeln, können Sie jetzt einen ArrayBuffer mit XMLHttpRequest abrufen und direkt mit einer DataView verarbeiten. So können Sie beispielsweise ganz einfach eine MP3-Datei laden und die Metadaten-Tags für die Verwendung in Ihrem Audioplayer lesen. Sie können auch eine Shapefile laden und in ein WebGL-Modell umwandeln. Oder lesen Sie die EXIF-Tags aus einer JPEG-Datei ab und zeigen Sie sie in Ihrer Bildschirmpräsentations-App an.

Das Problem bei ArrayBuffer-XHRs ist, dass das Lesen strukturähnlicher Daten aus dem Buffer etwas mühsam ist. DataView eignet sich gut, um mehrere Zahlen gleichzeitig auf Endian-sichere Weise zu lesen. Typierte Array-Ansichten eignen sich gut zum Lesen von Arrays mit an die Elementgröße angepassten nativen Endian-Zahlen. Uns fehlte eine Möglichkeit, Arrays und Datenstrukturen auf bequeme und endian-sichere Weise einzulesen. Geben Sie „DataStream.js“ ein.

DataStream.js ist eine Bibliothek mit typisierten Arrays, die Skalare, Strings, Arrays und Datenstrukturen auf ArrayBuffer-ähnliche Weise aus ArrayBuffers liest und schreibt.

Beispiel für das Lesen eines Float-Arrays 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);

DataStream.js ist besonders nützlich, wenn komplexere Daten gelesen werden sollen. Angenommen, Sie haben eine Methode, mit der JPEG-Markierungen gelesen werden:

// 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 verwendet ein Strukturdefinitions-Array, das die Typen der Strukturmitglieder enthält. Es gibt Rückruffunktionen für die Verarbeitung komplexer Typen sowie 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 von [Name, Typ]-Paaren. Verschachtelte Strukturen werden mit einem Array für den Typ erstellt. Arrays werden mithilfe eines Arrays mit drei Elementen definiert, wobei das zweite Element der Array-Elementtyp und das dritte Element die Länge des Arrays ist (entweder als Zahl, als Verweis auf ein zuvor gelesenes Feld oder als Rückruffunktion). Das erste Element der Arraydefinition wird nicht verwendet.

Die möglichen Werte für den Typ sind:

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.

Hier finden Sie ein Live-Beispiel zum Lesen von JPEG-Metadaten. In der Demo wird DataStream.js zum Lesen der Struktur der JPEG-Datei auf Tagebene (zusammen mit einigen EXIF-Parsings) und jpg.js zum Decodieren und Darstellen des JPEG-Bildes in JavaScript verwendet.

Geschichte von typisierten Arrays

Typisierte Arrays wurden in der frühen Implementierungsphase von WebGL entwickelt, als wir feststellten, dass das Übergeben von JavaScript-Arrays an den Grafiktreiber zu Leistungsproblemen führte. Bei JavaScript-Arrays musste die WebGL-Bindung ein natives Array zuordnen und füllen, indem das JavaScript-Array durchlaufen und jedes JavaScript-Objekt im Array in den erforderlichen nativen Typ umgewandelt wurde.

Um das Nadelöhr bei der Datenkonvertierung zu beheben, schrieb Vladimir Vukicevic von Mozilla CanvasFloatArray: ein C-Float-Array mit einer JavaScript-Schnittstelle. Jetzt können Sie das CanvasFloatArray in JavaScript bearbeiten und direkt an WebGL übergeben, ohne zusätzliche Arbeit bei der Bindung leisten zu müssen. In weiteren Iterationen wurde CanvasFloatArray in WebGLFloatArray umbenannt, das in Float32Array umbenannt und in einen unterstützenden ArrayBuffer und die typisierte Float32Array-view aufgeteilt wurde, um auf den Zwischenspeicher zuzugreifen. Außerdem wurden Typen für andere Ganzzahl- und Gleitkommagrößen sowie für signierte/unsignierte Varianten hinzugefügt.

Designaspekte

Das Design von Typed Arrays war von Anfang an darauf ausgerichtet, Binärdaten effizient an native Bibliotheken zu übergeben. Aus diesem Grund arbeiten die typisierten Arrays mit ausgerichteten Daten im nativen Endianness-Format der Host-CPU. Durch diese Entscheidungen kann JavaScript bei Vorgängen wie dem Senden von Vertex-Daten an die Grafikkarte die maximale Leistung erreichen.

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

Die Aufteilung zwischen In-Memory-Datenzusammenführung (mithilfe der typisierten Array-Ansichten) und E/A (mithilfe von DataView) war bewusst. Moderne JavaScript-Engines optimieren die typisierten Array-Ansichten erheblich und erzielen damit eine hohe Leistung bei numerischen Vorgängen. Die aktuellen Leistungsniveaus der typisierten Array-Ansichten wurden durch diese Designentscheidung ermöglicht.

Verweise