מערכים מוקלדים – נתונים בינאריים בדפדפן

Ilmari Heikkinen

מבוא

מערכי Typed נוספו לדפדפנים רק לאחרונה, בעקבות הצורך בדרך יעילה לטיפול בנתונים בינאריים ב-WebGL. מערך מוגדר הוא מקטע זיכרון עם תצוגה מוגדרת, בדומה לאופן שבו מערכים פועלים ב-C. מאחר ש-Typed Array מגובה בזיכרון גולמי, מנוע JavaScript יכול להעביר את הזיכרון ישירות לספריות מקוריות בלי צורך להמיר את הנתונים באופן ידני לייצוג מקורי. כתוצאה מכך, מערכי נתונים מוגדרים מניבים ביצועים טובים בהרבה ממערכי JavaScript להעברת נתונים ל-WebGL ולממשקי API אחרים שעוסקים בנתונים בינאריים.

תצוגות של מערכים מוקלדים פועלות כמו מערכים מסוג יחיד לפלח של ArrayBuffer. יש תצוגות לכל סוגי המספרים הרגילים, עם שמות שמתארים את עצמם, כמו Float32Array,‏ Float64Array,‏ Int32Array ו-Uint8Array. יש גם תצוגה מיוחדת שהחליפה את סוג מערך הפיקסלים ב-ImageData של Canvas: Uint8ClampedArray.

DataView הוא סוג התצוגה השני, והוא מיועד לטיפול בנתונים הטרוגניים. במקום API שדומה למערך, אובייקט DataView מספק API של קבלה/הגדרה לקריאה ולכתיבה של סוגי נתונים שרירותיים בהיסטים שרירותיים של בייטים. DataView מתאים במיוחד לקריאה ולכתיבה של כותרות של קבצים ונתונים אחרים מסוג struct.

עקרונות בסיסיים לשימוש במערכים מוקלדים

תצוגות של מערכים מוקלדים

כדי להשתמש במערכים מוגדרים, צריך ליצור ArrayBuffer ותצוגה שלו. הדרך הקלה ביותר היא ליצור תצוגה של מערך ממוין לפי סוג בגודל ובסוג הרצוי.

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

יש כמה סוגים שונים של תצוגות של מערכי טיפוסים. לכל השירותים יש את אותו ממשק API, כך שאחרי שמבינים איך משתמשים באחד מהם, אפשר להבין איך משתמשים בכל השאר. בדוגמה הבאה אצור אחת מכל תצוגה קיימת של מערך ממוין לפי טיפוס.

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

הפונקציה האחרונה קצת מיוחדת, היא מגבילה את ערכי הקלט בין 0 ל-255. האפשרות הזו שימושית במיוחד לאלגוריתמים של עיבוד תמונות ב-Canvas, כי עכשיו לא צריך לבצע ידנית קליפס (clamp) של החישובים של עיבוד התמונות כדי למנוע זליגה מטווח 8 הביט.

לדוגמה, כך מחילים גורם גמא על תמונה שמאוחסנת ב-Uint8Array. לא יפה במיוחד:

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

בעזרת Uint8ClampedArray אפשר לדלג על הלחיצה הידנית:

pixels[i] *= gamma;

הדרך השנייה ליצור תצוגות של מערכי נתונים מסוגים מוגדרים היא ליצור קודם ArrayBuffer ואז ליצור תצוגות שמצביעות עליו. ממשקי ה-API שמקבלים נתונים חיצוניים עובדים בדרך כלל עם ArrayBuffers, כך שזו הדרך לקבל תצוגה של מערך ממוין.

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

אפשר גם ליצור כמה תצוגות של אותו ArrayBuffer.

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

כדי להעתיק מערך מוגדר לסוג למערך מוגדר אחר, הדרך המהירה ביותר היא להשתמש בשיטה set של מערך מוגדר לסוג. לשימוש כמו memcpy, יוצרים Uint8Arrays למאגרים של התצוגות ומשתמשים ב-set כדי להעתיק את הנתונים.

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

כדי להשתמש ב-ArrayBuffers שמכילים נתונים עם סוגים הטרוגניים, הדרך הקלה ביותר היא להשתמש ב-DataView למאגר. נניח שיש לנו פורמט קובץ עם כותרת עם int ללא סימן באורך 8 ביט, ואחריה שני int באורך 16 ביט, ואחריה מערך של נתוני עומס שימושי של ערכים מסוג float באורך 32 ביט. אפשר לקרוא את זה חזרה באמצעות תצוגות של מערכי טיפוסים, אבל זה קצת קשה. באמצעות DataView אנחנו יכולים לקרוא את הכותרת ולהשתמש בתצוגת מערך ממוין של מערך ה-float.

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

בדוגמה שלמעלה, כל הערכים שקראנו הם big-endian. אם הערכים במאגר הם little-endian, אפשר להעביר את הפרמטר האופציונלי littleEndian ל-getter:

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

חשוב לזכור שתצוגות של מערכי טיפים תמיד מוצגות בסדר הבייטים המקורי. כך הם יהיו מהירים יותר. כדאי להשתמש ב-DataView כדי לקרוא ולכתוב נתונים שבהם endianness תהיה בעיה.

ל-DataView יש גם שיטות לכתיבה של ערכים למאגרים. השמות של ה-setters האלה נקבעים באותו אופן כמו השמות של ה-getters, "set" ואחריו סוג הנתונים.

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

דיון בנושא endianness

Endianness, או סדר הבייטים, הוא הסדר שבו מספרים עם כמה ביטים מאוחסנים בזיכרון המחשב. המונח big-endian מתאר ארכיטקטורת מעבד שמאחסנת את הבייט המשמעותי ביותר קודם. המונח little-endian מתאר ארכיטקטורת מעבד שמאחסנת את הבייט המשמעותי פחות קודם. בחירת סוג ה-endianness שמופעלת בארכיטקטורת CPU נתונה היא שרירותית לחלוטין. יש סיבות טובות לבחור בכל אחת מהאפשרויות. למעשה, אפשר להגדיר מעבדים מסוימים כך שיתמכו גם בנתונים מסוג big-endian וגם בנתונים מסוג little-endian.

למה צריך להתייחס ל-endianness? הסיבה לכך פשוטה. כשקוראים או כותבים נתונים מהדיסק או מהרשת, צריך לציין את endianness של הנתונים. כך אפשר להבטיח שהנתונים יפורשו בצורה נכונה, ללא קשר לסוג ה-endianness של המעבד שפועל איתם. בעולם שבו אנחנו מחוברים יותר ויותר לרשתות, חשוב מאוד לספק תמיכה הולמת בכל סוגי המכשירים, עם endian גדול או קטן, שעשויים להידרש לעבוד עם נתונים בינאריים שמגיעים משרתים או משותפים אחרים ברשת.

הממשק DataView תוכנן במיוחד לקריאה ולכתיבה של נתונים מקובצים ומהרשת, ולהפך. DataView פועל על נתונים עם סדר ביטים קצוב. צריך לציין את endianness, big או little, בכל גישה לכל ערך, כדי להבטיח שתקבלו תוצאות עקביות ותוצאות נכונות בקריאה או בכתיבת נתונים בינאריים, ללא קשר ל-endianness של המעבד שבו הדפדפן פועל.

בדרך כלל, כשהאפליקציה קוראת נתונים בינאריים משרת, צריך לסרוק אותם פעם אחת כדי להמיר אותם למבני הנתונים שבהם האפליקציה משתמשת באופן פנימי. בשלב הזה צריך להשתמש ב-DataView. לא מומלץ להשתמש בתצוגות של מערכי טיפים מרובת-בתים (Int16Array,‏ Uint16Array וכו') ישירות עם נתונים שאוחזרו באמצעות XMLHttpRequest,‏ FileReader או כל ממשק API אחר של קלט/פלט, כי בתצוגות של מערכי טיפים נעשה שימוש ב-endianness המקורי של המעבד. בהמשך נסביר על כך.

נבחן כמה דוגמאות פשוטות. פורמט הקובץ Windows BMP היה הפורמט הסטנדרטי לאחסון תמונות בימים הראשונים של Windows. במסמך המקושר למעלה מצוין בבירור שכל ערכי המספרים השלמים בקובץ מאוחסנים בפורמט little-endian. לפניכם קטע קוד שמנתח את תחילת הכותרת של קובץ ה-BMP באמצעות הספרייה DataStream.js שמצורפת למאמר הזה:

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

הנה דוגמה נוספת, הפעם מהדגמה של עיבוד תמונה בטווח דינמי גבוה בפרויקט הדוגמאות של WebGL. הדמו הזה מוריד נתונים גולמיים של נקודה צפה ב-little-endian שמייצגים טקסטורות של טווח דינמי גבוה, וצריך להעלות אותם ל-WebGL. זהו קטע הקוד שמפרש בצורה נכונה את ערכי הנקודה הצפה בכל הארכיטקטורות של המעבדים. נניח שהמשתנה arrayBuffer הוא ArrayBuffer שהורדתם מהשרת באמצעות 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);

הכלל הוא: כשמקבלים נתונים בינאריים משרת האינטרנט, מבצעים סריקה אחת שלהם באמצעות DataView. קוראים את הערכים המספריים הנפרדים ושומרים אותם במבנה נתונים אחר, אובייקט JavaScript (כמויות קטנות של נתונים מובְנים) או תצוגת מערך ממוין (כמויות גדולות של נתונים). כך תוכלו לוודא שהקוד יפעל כמו שצריך בכל סוגי המעבדים. אפשר גם להשתמש ב-DataView כדי לכתוב נתונים לקובץ או לרשת, וחשוב לציין את הארגומנט littleEndian באופן מתאים לשיטות השונות של set כדי ליצור את פורמט הקובץ שאתם יוצרים או משתמשים בו.

חשוב לזכור שלכל הנתונים שעוברים ברשת יש פורמט ו-endianness (לפחות לערכים של כמה בייטים). חשוב להגדיר בבירור ולתעד את הפורמט של כל הנתונים שהאפליקציה שולחת ברשת.

ממשקי API לדפדפנים שמשתמשים במערכים מוקלדים

אציג סקירה כללית קצרה של ממשקי ה-API השונים של הדפדפנים שמשתמשים כרגע במערכים מוגדרים. הרשימה הנוכחית כוללת את WebGL, ‏ Canvas, ‏ Web Audio API, ‏ XMLHttpRequests, ‏ WebSockets, ‏ Web Workers, ‏ Media Source API וממשקי File API. ברשימת ממשקי ה-API אפשר לראות ש-Typed Arrays מתאימים לעבודה עם מולטימדיה שמושפעת מאוד מהביצועים, וגם להעברת נתונים בצורה יעילה.

WebGL

השימוש הראשון ב-Typed Arrays היה ב-WebGL, שם הוא משמש להעברת נתוני מאגר ונתוני תמונה. כדי להגדיר את התוכן של אובייקט מאגר של WebGL, משתמשים בקריאה gl.bufferData()‎ עם מערך Typed.

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

מערכי Typed משמשים גם להעברת נתוני טקסטורה. הנה דוגמה בסיסית להעברת תוכן של טקסטורה באמצעות מערך מוגדר.

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

צריך גם מערכי Typed כדי לקרוא פיקסלים מההקשר של WebGL.

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

Canvas 2D

לאחרונה, אובייקט Canvas ImageData הותאם לעבודה עם מפרט Typed Arrays. עכשיו אפשר לקבל ייצוג של הפיקסלים ברכיב הקנבס באמצעות Typed Arrays. זה שימושי כי עכשיו אפשר גם ליצור ולערוך מערכי פיקסלים של קנבס בלי להתעסק עם רכיב הקנבס.

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

XMLHttpRequest2

נוספה תמיכה ב-Typed Array ל-XMLHttpRequest, ועכשיו אפשר לקבל תגובה מסוג Typed Array במקום לנתח מחרוזת JavaScript ל-Typed Array. זה שימושי מאוד להעברת נתונים שאוחזרו ישירות ל-API של מולטימדיה ולניתוח קבצים בינאריים שאוחזרו מהרשת.

כל מה שצריך לעשות הוא להגדיר את responseType של אובייקט XMLHttpRequest ל-'arraybuffer'.

xhr.responseType = 'arraybuffer';

חשוב לזכור שצריך לשים לב לבעיות של endianness כשמורידים נתונים מהרשת. אפשר לעיין בקטע על endianness למעלה.

ממשקי API של קבצים

ה-FileReader יכול לקרוא את תוכן הקובץ כ-ArrayBuffer. לאחר מכן תוכלו לצרף למאגר תצוגות של מערכי נתונים מסוגים שונים ותצוגות DataView כדי לבצע פעולות על התוכן שלו.

reader.readAsArrayBuffer(file);

חשוב גם לזכור את endianness. פרטים נוספים זמינים בקטע endianness.

אובייקטים שניתן להעביר

אובייקטים שניתן להעביר ב-postMessage מאפשרים להעביר נתונים בינאריים לחלונות אחרים ול-Web Workers מהר יותר בהרבה. כששולחים אובייקט ל-Worker כ-Transferable, אי אפשר לגשת לאובייקט בשרשור השליחה, ו-Worker המקבל מקבל את הבעלות על האובייקט. כך אפשר לבצע הטמעה עם אופטימיזציה גבוהה, שבה הנתונים שנשלחים לא מועתקים, אלא רק הבעלות על מערך ה-Typed מועברת לנמען.

כדי להשתמש באובייקטים שניתן להעביר עם Web Workers, צריך להשתמש בשיטה webkitPostMessage ב-worker. השיטה webkitPostMessage פועלת בדיוק כמו postMessage, אבל היא מקבלת שני ארגומנטים במקום רק אחד. הארגומנט השני שנוסף הוא מערך של אובייקטים שרוצים להעביר לעובד.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

כדי לקבל בחזרה את האובייקטים מה-worker, ה-worker יכול להעביר אותם בחזרה לשרשור הראשי באותו אופן.

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

אפס עותקים, יו!

Media Source API

לאחרונה נוספו לרכיבי המדיה גם כמה תכונות של מערכי Typed Array, בצורת Media Source API. אפשר להעביר ישירות מערך Typed שמכיל נתוני וידאו לאלמנט וידאו באמצעות webkitSourceAppend. כך רכיב הווידאו מוסיף את נתוני הסרטון אחרי הסרטון הקיים. האפשרות SourceAppend נהדרת לשימוש במודעות מעברון, בפלייליסטים, בסטרימינג ובשימושים אחרים שבהם רוצים להפעיל כמה סרטונים באמצעות רכיב וידאו אחד.

video.webkitSourceAppend(uint8Array);

WebSockets בינאריים

אפשר גם להשתמש במערכים מוגדרים עם WebSockets כדי להימנע מהצורך להפוך את כל הנתונים למחרוזת. נהדרת לכתיבת פרוטוקולים יעילים ולצמצום נפח התנועה ברשת.

socket.binaryType = 'arraybuffer';

איזה מזל! זהו הסיכום של בדיקת ה-API. עכשיו נעבור לספריות של צד שלישי לטיפול במערכים מוגדרים.

ספריות של צד שלישי

jDataView

jDataView מטמיע תוסף DataView לכל הדפדפנים. בעבר, DataView הייתה תכונה של WebKit בלבד, אבל עכשיו יש תמיכה בה ברוב הדפדפנים האחרים. צוות המפתחים של Mozilla עובד על תיקון שיאפשר להפעיל את DataView גם ב-Firefox.

איתמר ביטמן מצוות קשרי המפתחים של Chrome כתב דוגמה קטנה לקורא תגי MP3 ID3 שמשתמש ב-jDataView. דוגמה לשימוש מתוך הפוסט בבלוג:

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

עבודה עם מחרוזות במערכים מוגדרים היא קצת בעייתית כרגע, אבל יש את ספריית stringencoding שיכולה לעזור בעניין. ב-Stringencoding מוטמע המפרט המוצע לקידוד מחרוזות של מערך מוגדר, כך שזו גם דרך טובה לקבל מושג לגבי מה שעומד לקרות.

דוגמה בסיסית לשימוש ב-stringencoding:

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

BitView.js

כתבתי ספרייה קטנה לטיפול בביטים של מערכי Typed Arrays שנקראת BitView.js. כפי שרואים מהשם, ה-BitView פועל בדומה ל-DataView, אלא שהוא פועל עם ביטים. באמצעות BitView אפשר לקבל ולהגדיר את הערך של ביט במרווח ביט נתון ב-ArrayBuffer. ב-BitView יש גם שיטות לאחסון ולטעינה של מספרים שלמים באורך 6 ו-12 ביט בשינויי ביט שרירותיים.

מספרים שלמים של 12 ביט נוחים לעבודה עם קואורדינטות מסך, כי בדרך כלל במסכים יש פחות מ-4,096 פיקסלים בצד הארוך יותר. שימוש ב-ints של 12 ביט במקום ב-ints של 32 ביט מאפשר להפחית את הגודל ב-62%. דוגמה קיצונית יותר: עבדתי עם קובצי Shapefile שמשתמשים ב-floats של 64 ביט לצורך הקואורדינטות, אבל לא הייתי צריך את הדיוק הזה כי המודל היה אמור להופיע רק בגודל המסך. המעבר לקואורדינטות בסיס של 12 ביט עם דלתא של 6 ביט כדי לקודד שינויים מהקואורדינטה הקודמת צמצם את גודל הקובץ לעשירית. כאן אפשר לראות הדגמה של הנושא.

דוגמה לשימוש ב-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

אחד מהדברים המעניינים ביותר במערכים מסוגים הוא שהם מאפשרים לטפל בקלות רבה יותר בקבצים בינאריים ב-JavaScript. במקום לנתח מחרוזת תו אחרי תו ולהמיר את התווים למספרים בינאריים וכו' באופן ידני, עכשיו אפשר לקבל ArrayBuffer באמצעות XMLHttpRequest ולעבד אותו ישירות באמצעות DataView. כך קל, למשל, לטעון קובץ MP3 ולקרוא את תגי המטא-נתונים לשימוש בנגן האודיו. לחלופין, אפשר לטעון קובץ shapefile ולהפוך אותו למודל WebGL. אפשר גם לקרוא את תגי ה-EXIF מקובץ JPEG ולהציג אותם באפליקציית המצגת.

הבעיה ב-ArrayBuffer XHRs היא שקשה לקרוא מהמאגר נתונים שדומים למבנה. DataView מתאים לקריאת מספר מספרים בכל פעם באופן בטוח ל-endian. תצוגות של מערכי נתונים מותאמים מתאימות לקריאת מערכי מספרים ילידים ב-endian שמותאמים לגודל הרכיב. מה שחסר לנו הוא דרך לקרוא מערכי נתונים ומבנים של נתונים בצורה נוחה ובטוחה ל-endian. מזינים את DataStream.js.

DataStream.js היא ספרייה של מערכי Typed שמאפשרת לקרוא ולכתוב סקלר, מחרוזות, מערכי נתונים ומבנים של נתונים מ-ArrayBuffers באופן שדומה לקריאה וכתיבה של קובץ.

דוגמה לקריאה של מערך של מספרים מסוג float מ-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 שימושי במיוחד הוא בקריאת נתונים מורכבים יותר. נניח שיש לכם שיטה שקוראת סמנים של 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);
}

לחלופין, אפשר להשתמש בשיטה DataStream.readStruct כדי לקרוא מבנים של נתונים. השיטה readStruct מקבלת מערך של הגדרת struct שמכיל את סוגי חברי ה-struct. יש לו פונקציות קריאה חוזרת (callback) לטיפול בסוגי נתונים מורכבים, והוא מטפל גם במערכים של נתונים ובמבנים מורכבים בתצוגת עץ:

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

כפי שאפשר לראות, הגדרת המבנה היא מערך שטוח של צמדים [name, type]. כדי ליצור מבנים מורכבים בתצוגת עץ, צריך להשתמש במערך של הסוג. מגדירים מערכי נתונים באמצעות מערך בן שלושה רכיבים, כאשר הרכיב השני הוא סוג רכיב המערך והרכיב השלישי הוא אורך המערך (כמספר, כהפניה לשדה שנקרא בעבר או כפונקציית קריאה חוזרת). לא נעשה שימוש ברכיב הראשון של הגדרת המערך.

הערכים האפשריים של הסוג הם:

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.

כאן אפשר לראות דוגמה פעילה לקריאת מטא-נתונים של קובצי JPEG. הדגמה משתמשת ב-DataStream.js לקריאת המבנה ברמת התג של קובץ ה-JPEG (יחד עם ניתוח חלקי של EXIF), וב-jpg.js לפענוח ולהצגת התמונה בפורמט JPEG ב-JavaScript.

ההיסטוריה של מערכים מוקלדים

המערכים המודעים לטיפוסים הופיעו בשלב מוקדם של ההטמעה של WebGL, כשגילינו שהעברת מערכים של JavaScript למנהל הגרפיקה גורמת לבעיות בביצועים. במערכים של JavaScript, הקישור ל-WebGL היה צריך להקצות מערך מקומי ולמלא אותו על ידי סריקה של מערך ה-JavaScript והמרה של כל אובייקט JavaScript במערך לסוג המקומי הנדרש.

כדי לפתור את צוואר הבקבוק של המרת הנתונים, Vladimir Vukicevic מ-Mozilla כתב את CanvasFloatArray: מערך של ערכים מסוג float בסגנון C עם ממשק JavaScript. עכשיו אפשר לערוך את CanvasFloatArray ב-JavaScript ולהעביר אותו ישירות ל-WebGL בלי לבצע עבודה נוספת בקישור. בגרסאות מתקדמות יותר, השם של CanvasFloatArray שונה ל-WebGLFloatArray, ולאחר מכן השם שונה ל-Float32Array והוא חולק ל-ArrayBuffer תומך ולתצוגה מסוג Float32Array כדי לגשת למאגר. הוספנו גם סוגים לגדלים אחרים של מספרים שלמים ומספרים בנקודה צפה, ולוריאציות עם סימן או ללא סימן.

שיקולים לגבי עיצוב

כבר מההתחלה, העיצוב של מערכי Typed נקבע על סמך הצורך להעביר נתונים בינאריים לספריות מקוריות בצורה יעילה. לכן, תצוגות מערכי הנתונים המודעים לסוג פועלות על נתונים מותאמים בסדר ביטים מקורי של מעבד המארח. ההחלטות האלה מאפשרות ל-JavaScript להגיע לביצועים מקסימליים במהלך פעולות כמו שליחת נתוני קודקודים לכרטיס הגרפיקה.

DataView תוכנן במיוחד ל-I/O של קבצים ורשתות, שבהם לנתונים תמיד יש סדר ביטים קצוב, וייתכן שהם לא מותאמים לביצועים מקסימליים.

הפיצול בתכנון בין הרכבת נתונים בזיכרון (באמצעות תצוגות מערך מוגדרות-סוג) לבין קלט/פלט (באמצעות DataView) היה מכוון. מנועי JavaScript מודרניים מבצעים אופטימיזציה משמעותית של תצוגות מערכי הטיפוסים, ומשיגים ביצועים גבוהים בפעולות מספריות באמצעותן. רמות הביצועים הנוכחיות של תצוגות מערכי הטיפוסים הושגו בזכות ההחלטה הזו בתכנון.

קובצי עזר