Tableaux typés - Données binaires dans le navigateur

Ilmari Heikkinen

Introduction

Les tableaux typés sont un ajout relativement récent aux navigateurs, né de la nécessité d'avoir un moyen efficace de gérer les données binaires dans WebGL. Un tableau typé est un bloc de mémoire avec une vue typée, comme le font les tableaux en C. Étant donné qu'un tableau typé est basé sur une mémoire brute, le moteur JavaScript peut transmettre la mémoire directement aux bibliothèques natives sans avoir à convertir minutieusement les données en représentation native. Par conséquent, les tableaux typés sont beaucoup plus performants que les tableaux JavaScript pour transmettre des données à WebGL et à d'autres API qui traitent des données binaires.

Les vues de tableaux typés se comportent comme des tableaux à type unique pour un segment d'un ArrayBuffer. Il existe des vues pour tous les types numériques habituels, avec des noms descriptifs tels que Float32Array, Float64Array, Int32Array et Uint8Array. Une vue spéciale a également remplacé le type de tableau de pixels dans ImageData de Canvas : Uint8ClampedArray.

DataView est le deuxième type de vue. Il est conçu pour gérer des données hétérogènes. Au lieu d'avoir une API de type tableau, l'objet DataView vous fournit une API get/set pour lire et écrire des types de données arbitraires à des décalages d'octets arbitraires. DataView est idéal pour lire et écrire des en-têtes de fichiers et d'autres données de type struct.

Principes de base de l'utilisation des tableaux typés

Vues de tableaux typés

Pour utiliser des tableaux typés, vous devez créer un ArrayBuffer et une vue. Le moyen le plus simple est de créer une vue de tableau typé de la taille et du type souhaités.

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

Il existe plusieurs types de vues d'array typées. Elles partagent toutes la même API. Par conséquent, une fois que vous savez utiliser l'une d'entre elles, vous savez à peu près toutes les utiliser. Dans l'exemple suivant, je vais créer une vue de tableau typé pour chacune des vues existantes.

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

Le dernier est un peu spécial. Il limite les valeurs d'entrée entre 0 et 255. Cela est particulièrement utile pour les algorithmes de traitement d'images Canvas, car vous n'avez plus besoin de limiter manuellement les calculs de traitement de l'image pour éviter de dépasser la plage de 8 bits.

Par exemple, voici comment appliquer un facteur gamma à une image stockée dans un Uint8Array. Ce n'est pas très joli:

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

Avec Uint8ClampedArray, vous pouvez ignorer le forçage manuel :

pixels[i] *= gamma;

L'autre façon de créer des vues de tableau typés consiste à créer d'abord un objet ArrayBuffer, puis à créer des vues qui pointent vers celui-ci. Les API qui récupèrent des données externes traitent généralement des ArrayBuffers. C'est donc ainsi que vous obtenez une vue d'un tableau typé.

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

Vous pouvez également avoir plusieurs vues du même ArrayBuffer.

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

Pour copier un tableau typé dans un autre tableau typé, le moyen le plus rapide est d'utiliser la méthode de définition du tableau typé. Pour une utilisation semblable à memcpy, créez des Uint8Arrays dans les tampons des vues et utilisez set pour copier les données.

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

Pour utiliser des ArrayBuffers contenant des données de types hétérogènes, le moyen le plus simple est d'utiliser un DataView pour le tampon. Supposons que nous ayons un format de fichier comportant un en-tête avec un int non signé de 8 bits suivi de deux entiers de 16 bits, suivis d'un tableau de charge utile de floats 32 bits. Il est possible de lire cela avec des vues de tableaux typés, mais c'est un peu pénible. Avec un DataView, nous pouvons lire l'en-tête et utiliser une vue de tableau typée pour le tableau de nombres à virgule flottante.

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

Dans l'exemple ci-dessus, toutes les valeurs que je lis sont en ordre de grandeur. Si les valeurs du tampon sont en ordre octets de basse endianité, vous pouvez transmettre le paramètre littleEndian facultatif au getter :

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

Notez que les vues de tableau typées sont toujours dans l'ordre des octets natif. Cela permet de les rendre plus rapides. Vous devez utiliser une DataView pour lire et écrire des données où l'endianité pose problème.

DataView propose également des méthodes permettant d&#39;écrire des valeurs dans des tampons. Ces sétteurs sont nommés de la même manière que les getters, "set" suivi du type de données.

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

Discussion sur l'endianité

L'endianness, ou ordre des octets, est l'ordre dans lequel les nombres multi-octets sont stockés dans la mémoire de l'ordinateur. Le terme big-endian décrit une architecture de processeur qui stocke d'abord l'octet le plus significatif, et little-endian l'octet le moins significatif. L'endianité utilisée dans une architecture de processeur donnée est complètement arbitraire. Il existe de bonnes raisons de choisir l'une ou l'autre. En fait, certains processeurs peuvent être configurés pour prendre en charge à la fois les données big-endian et little-endian.

Pourquoi devez-vous vous soucier de l'endianité ? La raison est simple. Lors de la lecture ou de l'écriture de données à partir du disque ou du réseau, le caractère final des données doit être spécifié. Cela garantit que les données sont interprétées correctement, quel que soit l'endianness du processeur qui les utilise. Dans un monde de plus en plus connecté, il est essentiel de prendre en charge correctement tous les types d'appareils, qu'ils soient big-end ou small-endian, qui pourraient avoir besoin de travailler avec des données binaires provenant de serveurs ou d'autres pairs sur le réseau.

L'interface DataView est spécialement conçue pour lire et écrire des données à partir et vers des fichiers et le réseau. DataView utilise les données avec une endicité spécifiée. Le caractère final, petit ou grand, doit être spécifié à chaque accès de chaque valeur, ce qui vous permet d'obtenir des résultats cohérents et corrects lors de la lecture ou de l'écriture de données binaires, quelle que soit la finesse du processeur sur lequel le navigateur s'exécute.

En règle générale, lorsque votre application lit des données binaires à partir d'un serveur, vous devez les analyser une fois afin de les convertir en structures de données utilisées en interne par votre application. DataView doit être utilisé au cours de cette phase. Il n'est pas recommandé d'utiliser les vues d'array typé multi-octets (Int16Array, Uint16Array, etc.) directement avec des données extraites via XMLHttpRequest, FileReader ou toute autre API d'entrée/sortie, car les vues d'array typé utilisent l'endianité native du processeur. Nous reviendrons sur ce point.

Examinons quelques exemples simples. Le format de fichier Windows BMP était le format standard de stockage des images au début de Windows. La documentation associée au lien ci-dessus indique clairement que toutes les valeurs entières du fichier sont stockées au format little-endian. Voici un extrait de code qui analyse le début de l'en-tête BMP à l'aide de la bibliothèque DataStream.js qui accompagne cet article :

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();
  // ...
}

Voici un autre exemple, tiré de la démonstration de rendu HDR du projet d'exemples WebGL. Cette démonstration télécharge des données à virgule flottante brutes en little-endian représentant des textures à plage dynamique élevée, et doit les importer dans WebGL. Voici l'extrait de code qui interprète correctement les valeurs à virgule flottante sur toutes les architectures de processeur. Supposons que la variable "arrayBuffer" soit un objet ArrayBuffer qui vient d'être téléchargé à partir du serveur via XMLHttpRequest:

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

La règle d'or est la suivante : lorsque vous recevez des données binaires du serveur Web, effectuez une seule passe avec un DataView. Lisez les valeurs numériques individuelles et stockez-les dans une autre structure de données, un objet JavaScript (pour de petites quantités de données structurées) ou une vue de tableau typée (pour de grands blocs de données). Vous vous assurez ainsi que votre code fonctionne correctement sur tous les types de processeurs. Utilisez également DataView pour écrire des données dans un fichier ou sur le réseau, et veillez à spécifier correctement l'argument littleEndian aux différentes méthodes set afin de générer le format de fichier que vous créez ou utilisez.

N'oubliez pas que toutes les données qui transitent par le réseau ont implicitement un format et une finition (du moins, pour les valeurs à plusieurs octets). Assurez-vous de définir et de documenter clairement le format de toutes les données que votre application envoie sur le réseau.

API du navigateur qui utilisent des tableaux saisis

Je vais vous donner un bref aperçu des différentes API de navigateur qui utilisent actuellement des tableaux typés. La sélection actuelle comprend WebGL, Canvas, l'API Web Audio, les requêtes XHR, les WebSockets, les Web Workers, l'API Media Source et les API File. D'après la liste des API, vous pouvez constater que les tableaux typés conviennent bien aux tâches multimédias sensibles aux performances, ainsi qu'à la transmission efficace de données.

WebGL

Les premiers tableaux typés ont été utilisés dans WebGL, où ils servent à transmettre des données de tampon et des données d'image. Pour définir le contenu d'un objet de tampon WebGL, utilisez l'appel gl.bufferData() avec un tableau typé.

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

Les tableaux typés sont également utilisés pour transmettre des données de texture. Voici un exemple de base de transmission du contenu de la texture à l'aide d'un tableau typé.

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

Vous avez également besoin de tableaux typés pour lire les pixels à partir du contexte WebGL.

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

Canevas 2D

Récemment, l'objet Canvas ImageData a été conçu pour fonctionner avec la spécification des tableaux typés. Vous pouvez désormais obtenir une représentation des pixels d'un élément de canevas sous forme de tableaux typés. Cela est utile, car vous pouvez désormais également créer et modifier des tableaux de pixels de canevas sans avoir à manipuler l'élément de canevas.

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

XMLHttpRequest2

XMLHttpRequest a bénéficié d'un coup de pouce de la part des tableaux typés. Vous pouvez désormais recevoir une réponse de tableau typé au lieu d'avoir à analyser une chaîne JavaScript dans un tableau typé. Cela est très pratique pour transmettre directement les données extraites aux API multimédias et pour analyser les fichiers binaires extraits du réseau.

Il vous suffit de définir le responseType de l'objet XMLHttpRequest sur "arraybuffer".

xhr.responseType = 'arraybuffer';

N'oubliez pas que vous devez tenir compte des problèmes d'endianité lorsque vous téléchargez des données à partir du réseau. Consultez la section sur l'ordre des octets ci-dessus.

API File

FileReader peut lire le contenu du fichier sous forme de ArrayBuffer. Vous pouvez ensuite associer des vues d'array typées et des DataViews au tampon pour manipuler son contenu.

reader.readAsArrayBuffer(file);

Ici aussi, gardez à l'esprit le caractère final. Pour en savoir plus, consultez la section sur l'ordre des octets.

Objets transférables

Les objets transférables dans postMessage permettent de transmettre des données binaires à d'autres fenêtres et Web Workers beaucoup plus rapidement. Lorsque vous envoyez un objet à un worker en tant qu'objet transférable, il devient inaccessible dans le thread d'envoi et le worker destinataire en devient le propriétaire. Cela permet une implémentation hautement optimisée où les données envoyées ne sont pas copiées, mais que la propriété de la matrice typée est transférée au destinataire.

Pour utiliser des objets transférables avec des Web Workers, vous devez utiliser la méthode webkitPostMessage sur le worker. La méthode webkitPostMessage fonctionne de la même manière que postMessage, mais elle prend deux arguments au lieu d'un seul. Le deuxième argument ajouté est un tableau d'objets que vous souhaitez transférer au nœud de calcul.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Pour récupérer les objets du nœud de calcul, celui-ci peut les renvoyer au thread principal de la même manière.

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

Aucune copie, waouh !

API Media Source

Récemment, les éléments multimédias ont également bénéficié de certains avantages liés aux tableaux typés sous la forme de l'API Media Source. Vous pouvez transmettre directement un tableau Typed contenant des données vidéo à un élément vidéo à l'aide de webkitSourceAppend. L'élément vidéo ajoute alors les données vidéo après la vidéo existante. SourceAppend est idéal pour les interstitiels, les playlists, le streaming et d'autres utilisations où vous souhaitez lire plusieurs vidéos à l'aide d'un seul élément vidéo.

video.webkitSourceAppend(uint8Array);

WebSockets binaires

Vous pouvez également utiliser des tableaux typés avec WebSockets pour éviter de concaténer toutes vos données. Idéal pour écrire des protocoles efficaces et réduire le trafic réseau.

socket.binaryType = 'arraybuffer';

Ouf ! Voilà qui conclut l'examen de l'API. Passons maintenant aux bibliothèques tierces pour la gestion des tableaux typés.

Bibliothèques tierces

jDataView

jDataView implémente un shim DataView pour tous les navigateurs. DataView n'était auparavant disponible que dans WebKit, mais il est désormais compatible avec la plupart des autres navigateurs. L'équipe de développeurs Mozilla est en train de publier un correctif pour activer DataView dans Firefox.

Eric Bidelman, de l'équipe Chrome Developer Relations, a écrit un petit exemple de lecteur de tags ID3 MP3 qui utilise jDataView. Voici un exemple d'utilisation tiré de l'article de blog :

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

Travailler avec des chaînes dans des tableaux typés est un peu pénible pour le moment, mais la bibliothèque stringencoding peut vous aider. Stringencoding implémente la spécification d'encodage de chaîne de tableau typé proposée. Il s'agit donc également d'un bon moyen de se faire une idée de ce qui vous attend.

Voici un exemple d'utilisation de base du codage de chaînes:

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

BitView.js

J'ai écrit une petite bibliothèque de manipulation de bits pour les tableaux typés appelée BitView.js. Comme son nom l'indique, il fonctionne comme la vue de données, mais avec des bits. Avec BitView, vous pouvez obtenir et définir la valeur d'un bit à un décalage de bits donné dans un ArrayBuffer. BitView fournit également des méthodes pour stocker et charger des entiers de 6 et 12 bits à des décalages de bits arbitraires.

Les entiers 12 bits sont utiles pour travailler avec les coordonnées d'écran, car les écrans ont tendance à comporter moins de 4 096 pixels sur la dimension la plus longue. En utilisant des entiers 12 bits au lieu d'entiers 32 bits, vous obtenez une réduction de 62 % de la taille. Pour un exemple plus extrême, je travaillais avec des fichiers de forme qui utilisent des nombres à virgule flottante 64 bits pour les coordonnées, mais je n'avais pas besoin de cette précision, car le modèle ne devait être affiché qu'à la taille de l'écran. Le passage à des coordonnées de base 12 bits avec des deltas 6 bits pour encoder les modifications par rapport à la coordonnée précédente a réduit la taille du fichier d'un dixième. Pour voir une démonstration, cliquez ici.

Voici un exemple d'utilisation de 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

L'un des aspects les plus intéressants des tableaux typés est qu'ils facilitent la gestion des fichiers binaires en JavaScript. Au lieu d'analyser une chaîne caractère par caractère et de convertir manuellement les caractères en nombres binaires, vous pouvez désormais obtenir un ArrayBuffer avec XMLHttpRequest et le traiter directement à l'aide d'un DataView. Vous pouvez ainsi facilement charger un fichier MP3 et lire les balises de métadonnées à utiliser dans votre lecteur audio, par exemple. Vous pouvez également charger un fichier de forme et le convertir en modèle WebGL. Vous pouvez également lire les tags EXIF à partir d'un fichier JPEG et les afficher dans votre application de diaporama.

Le problème avec les XHR ArrayBuffer est que la lecture de données ressemblant à des structures à partir du tampon est un peu pénible. DataView est utile pour lire quelques nombres à la fois de manière endian-safe. Les vues de tableaux typés sont utiles pour lire des tableaux de nombres endian natifs alignés sur la taille des éléments. Nous avons ressenti le besoin d'un moyen de lire des tableaux et des structures de données de manière pratique et compatible avec l'endianité. Saisissez DataStream.js.

DataStream.js est une bibliothèque d'arrays typés qui lit et écrit des scalaires, des chaînes, des tableaux et des structures de données à partir d'ArrayBuffers de manière semblable à un fichier.

Exemple de lecture dans un tableau de nombres à virgule flottante à partir d'un 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 est particulièrement utile pour lire des données plus complexes. Supposons que vous ayez une méthode qui lit des repères JPEG :

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

Vous pouvez également utiliser la méthode DataStream.readStruct pour lire des structures de données. La méthode readStruct reçoit un tableau de définition de structure contenant les types des membres de la structure. Il dispose de fonctions de rappel pour gérer les types complexes, et gère également les tableaux de données et les structures imbriquées :

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

Comme vous pouvez le constater, la définition de la structure est un tableau plat de paires [nom, type]. Les structures imbriquées sont créées à l'aide d'un tableau pour le type. Les tableaux sont définis à l'aide d'un tableau à trois éléments, où le deuxième élément est le type d'élément de tableau et le troisième élément est la longueur du tableau (soit sous forme de nombre, comme référence au champ lu précédemment, soit en tant que fonction de rappel). Le premier élément de la définition du tableau n'est pas utilisé.

Les valeurs possibles pour le type sont les suivantes :

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.

Pour voir un exemple concret de lecture des métadonnées JPEG, cliquez ici. La démonstration utilise DataStream.js pour lire la structure au niveau des tags du fichier JPEG (et une analyse EXIF) et jpg.js pour le décodage et l'affichage de l'image JPEG en JavaScript.

Historique des tableaux typés

Les tableaux typés ont vu le jour au début de l'implémentation de WebGL, lorsque nous avons constaté que le transfert de tableaux JavaScript au pilote graphique entraînait des problèmes de performances. Avec les tableaux JavaScript, la liaison WebGL devait attribuer un tableau natif et le remplir en parcourant le tableau JavaScript, puis caster chaque objet JavaScript du tableau dans le type natif requis.

Pour résoudre le goulot d'étranglement de la conversion de données, Vladimir Vukicevic de Mozilla a écrit CanvasFloatArray : un tableau de nombres à virgule flottante de style C avec une interface JavaScript. Vous pouvez maintenant modifier le CanvasFloatArray en JavaScript et le transmettre directement à WebGL sans avoir à effectuer de travail supplémentaire dans la liaison. Dans les itérations suivantes, CanvasFloatArray a été renommé WebGLFloatArray, puis Float32Array et divisé en ArrayBuffer de base et en vue Float32Array typée pour accéder au tampon. Des types ont également été ajoutés pour d'autres tailles d'entiers et de nombres à virgule flottante, ainsi que pour les variantes signées/non signées.

Considérations de conception

Dès le départ, la conception des tableaux typés a été motivée par la nécessité de transmettre efficacement des données binaires aux bibliothèques natives. Pour cette raison, les vues de tableau typées opèrent sur des données alignées dans l'endianité native du processeur hôte. Ces décisions permettent à JavaScript d'atteindre des performances maximales lors d'opérations telles que l'envoi de données de sommet à la carte graphique.

DataView est spécialement conçu pour les E/S de fichiers et de réseau, où les données ont toujours un endianness spécifié et ne sont pas nécessairement alignées pour des performances maximales.

La répartition de la conception entre l'assemblage de données en mémoire (à l'aide des vues d'array typées) et les E/S (à l'aide de DataView) a été consciente. Les moteurs JavaScript modernes optimisent fortement les vues d'un tableau typé et obtiennent de hautes performances pour les opérations numériques avec celui-ci. Cette décision de conception a permis d'atteindre les niveaux de performances actuels des vues de tableaux typés.

Références