Explorar o código

TTFLoader: Upgrade opentype.js. (#23845)

Michael Herzog %!s(int64=3) %!d(string=hai) anos
pai
achega
accbd283f4

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 19
examples/js/libs/opentype.min.js


+ 14568 - 0
examples/jsm/libs/opentype.module.js

@@ -0,0 +1,14568 @@
+/**
+ * https://opentype.js.org v1.3.4 | (c) Frederik De Bleser and other contributors | MIT License | Uses tiny-inflate by Devon Govett and string.prototype.codepointat polyfill by Mathias Bynens
+ */
+
+/*! https://mths.be/codepointat v0.2.0 by @mathias */
+if (!String.prototype.codePointAt) {
+	(function() {
+		var defineProperty = (function() {
+			// IE 8 only supports `Object.defineProperty` on DOM elements
+			try {
+				var object = {};
+				var $defineProperty = Object.defineProperty;
+				var result = $defineProperty(object, object, object) && $defineProperty;
+			} catch(error) {}
+			return result;
+		}());
+		var codePointAt = function(position) {
+			if (this == null) {
+				throw TypeError();
+			}
+			var string = String(this);
+			var size = string.length;
+			// `ToInteger`
+			var index = position ? Number(position) : 0;
+			if (index != index) { // better `isNaN`
+				index = 0;
+			}
+			// Account for out-of-bounds indices:
+			if (index < 0 || index >= size) {
+				return undefined;
+			}
+			// Get the first code unit
+			var first = string.charCodeAt(index);
+			var second;
+			if ( // check if it’s the start of a surrogate pair
+				first >= 0xD800 && first <= 0xDBFF && // high surrogate
+				size > index + 1 // there is a next code unit
+			) {
+				second = string.charCodeAt(index + 1);
+				if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
+					// https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
+					return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
+				}
+			}
+			return first;
+		};
+		if (defineProperty) {
+			defineProperty(String.prototype, 'codePointAt', {
+				'value': codePointAt,
+				'configurable': true,
+				'writable': true
+			});
+		} else {
+			String.prototype.codePointAt = codePointAt;
+		}
+	}());
+}
+
+var TINF_OK = 0;
+var TINF_DATA_ERROR = -3;
+
+function Tree() {
+  this.table = new Uint16Array(16);   /* table of code length counts */
+  this.trans = new Uint16Array(288);  /* code -> symbol translation table */
+}
+
+function Data(source, dest) {
+  this.source = source;
+  this.sourceIndex = 0;
+  this.tag = 0;
+  this.bitcount = 0;
+  
+  this.dest = dest;
+  this.destLen = 0;
+  
+  this.ltree = new Tree();  /* dynamic length/symbol tree */
+  this.dtree = new Tree();  /* dynamic distance tree */
+}
+
+/* --------------------------------------------------- *
+ * -- uninitialized global data (static structures) -- *
+ * --------------------------------------------------- */
+
+var sltree = new Tree();
+var sdtree = new Tree();
+
+/* extra bits and base tables for length codes */
+var length_bits = new Uint8Array(30);
+var length_base = new Uint16Array(30);
+
+/* extra bits and base tables for distance codes */
+var dist_bits = new Uint8Array(30);
+var dist_base = new Uint16Array(30);
+
+/* special ordering of code length codes */
+var clcidx = new Uint8Array([
+  16, 17, 18, 0, 8, 7, 9, 6,
+  10, 5, 11, 4, 12, 3, 13, 2,
+  14, 1, 15
+]);
+
+/* used by tinf_decode_trees, avoids allocations every call */
+var code_tree = new Tree();
+var lengths = new Uint8Array(288 + 32);
+
+/* ----------------------- *
+ * -- utility functions -- *
+ * ----------------------- */
+
+/* build extra bits and base tables */
+function tinf_build_bits_base(bits, base, delta, first) {
+  var i, sum;
+
+  /* build bits table */
+  for (i = 0; i < delta; ++i) { bits[i] = 0; }
+  for (i = 0; i < 30 - delta; ++i) { bits[i + delta] = i / delta | 0; }
+
+  /* build base table */
+  for (sum = first, i = 0; i < 30; ++i) {
+    base[i] = sum;
+    sum += 1 << bits[i];
+  }
+}
+
+/* build the fixed huffman trees */
+function tinf_build_fixed_trees(lt, dt) {
+  var i;
+
+  /* build fixed length tree */
+  for (i = 0; i < 7; ++i) { lt.table[i] = 0; }
+
+  lt.table[7] = 24;
+  lt.table[8] = 152;
+  lt.table[9] = 112;
+
+  for (i = 0; i < 24; ++i) { lt.trans[i] = 256 + i; }
+  for (i = 0; i < 144; ++i) { lt.trans[24 + i] = i; }
+  for (i = 0; i < 8; ++i) { lt.trans[24 + 144 + i] = 280 + i; }
+  for (i = 0; i < 112; ++i) { lt.trans[24 + 144 + 8 + i] = 144 + i; }
+
+  /* build fixed distance tree */
+  for (i = 0; i < 5; ++i) { dt.table[i] = 0; }
+
+  dt.table[5] = 32;
+
+  for (i = 0; i < 32; ++i) { dt.trans[i] = i; }
+}
+
+/* given an array of code lengths, build a tree */
+var offs = new Uint16Array(16);
+
+function tinf_build_tree(t, lengths, off, num) {
+  var i, sum;
+
+  /* clear code length count table */
+  for (i = 0; i < 16; ++i) { t.table[i] = 0; }
+
+  /* scan symbol lengths, and sum code length counts */
+  for (i = 0; i < num; ++i) { t.table[lengths[off + i]]++; }
+
+  t.table[0] = 0;
+
+  /* compute offset table for distribution sort */
+  for (sum = 0, i = 0; i < 16; ++i) {
+    offs[i] = sum;
+    sum += t.table[i];
+  }
+
+  /* create code->symbol translation table (symbols sorted by code) */
+  for (i = 0; i < num; ++i) {
+    if (lengths[off + i]) { t.trans[offs[lengths[off + i]]++] = i; }
+  }
+}
+
+/* ---------------------- *
+ * -- decode functions -- *
+ * ---------------------- */
+
+/* get one bit from source stream */
+function tinf_getbit(d) {
+  /* check if tag is empty */
+  if (!d.bitcount--) {
+    /* load next tag */
+    d.tag = d.source[d.sourceIndex++];
+    d.bitcount = 7;
+  }
+
+  /* shift bit out of tag */
+  var bit = d.tag & 1;
+  d.tag >>>= 1;
+
+  return bit;
+}
+
+/* read a num bit value from a stream and add base */
+function tinf_read_bits(d, num, base) {
+  if (!num)
+    { return base; }
+
+  while (d.bitcount < 24) {
+    d.tag |= d.source[d.sourceIndex++] << d.bitcount;
+    d.bitcount += 8;
+  }
+
+  var val = d.tag & (0xffff >>> (16 - num));
+  d.tag >>>= num;
+  d.bitcount -= num;
+  return val + base;
+}
+
+/* given a data stream and a tree, decode a symbol */
+function tinf_decode_symbol(d, t) {
+  while (d.bitcount < 24) {
+    d.tag |= d.source[d.sourceIndex++] << d.bitcount;
+    d.bitcount += 8;
+  }
+  
+  var sum = 0, cur = 0, len = 0;
+  var tag = d.tag;
+
+  /* get more bits while code value is above sum */
+  do {
+    cur = 2 * cur + (tag & 1);
+    tag >>>= 1;
+    ++len;
+
+    sum += t.table[len];
+    cur -= t.table[len];
+  } while (cur >= 0);
+  
+  d.tag = tag;
+  d.bitcount -= len;
+
+  return t.trans[sum + cur];
+}
+
+/* given a data stream, decode dynamic trees from it */
+function tinf_decode_trees(d, lt, dt) {
+  var hlit, hdist, hclen;
+  var i, num, length;
+
+  /* get 5 bits HLIT (257-286) */
+  hlit = tinf_read_bits(d, 5, 257);
+
+  /* get 5 bits HDIST (1-32) */
+  hdist = tinf_read_bits(d, 5, 1);
+
+  /* get 4 bits HCLEN (4-19) */
+  hclen = tinf_read_bits(d, 4, 4);
+
+  for (i = 0; i < 19; ++i) { lengths[i] = 0; }
+
+  /* read code lengths for code length alphabet */
+  for (i = 0; i < hclen; ++i) {
+    /* get 3 bits code length (0-7) */
+    var clen = tinf_read_bits(d, 3, 0);
+    lengths[clcidx[i]] = clen;
+  }
+
+  /* build code length tree */
+  tinf_build_tree(code_tree, lengths, 0, 19);
+
+  /* decode code lengths for the dynamic trees */
+  for (num = 0; num < hlit + hdist;) {
+    var sym = tinf_decode_symbol(d, code_tree);
+
+    switch (sym) {
+      case 16:
+        /* copy previous code length 3-6 times (read 2 bits) */
+        var prev = lengths[num - 1];
+        for (length = tinf_read_bits(d, 2, 3); length; --length) {
+          lengths[num++] = prev;
+        }
+        break;
+      case 17:
+        /* repeat code length 0 for 3-10 times (read 3 bits) */
+        for (length = tinf_read_bits(d, 3, 3); length; --length) {
+          lengths[num++] = 0;
+        }
+        break;
+      case 18:
+        /* repeat code length 0 for 11-138 times (read 7 bits) */
+        for (length = tinf_read_bits(d, 7, 11); length; --length) {
+          lengths[num++] = 0;
+        }
+        break;
+      default:
+        /* values 0-15 represent the actual code lengths */
+        lengths[num++] = sym;
+        break;
+    }
+  }
+
+  /* build dynamic trees */
+  tinf_build_tree(lt, lengths, 0, hlit);
+  tinf_build_tree(dt, lengths, hlit, hdist);
+}
+
+/* ----------------------------- *
+ * -- block inflate functions -- *
+ * ----------------------------- */
+
+/* given a stream and two trees, inflate a block of data */
+function tinf_inflate_block_data(d, lt, dt) {
+  while (1) {
+    var sym = tinf_decode_symbol(d, lt);
+
+    /* check for end of block */
+    if (sym === 256) {
+      return TINF_OK;
+    }
+
+    if (sym < 256) {
+      d.dest[d.destLen++] = sym;
+    } else {
+      var length, dist, offs;
+      var i;
+
+      sym -= 257;
+
+      /* possibly get more bits from length code */
+      length = tinf_read_bits(d, length_bits[sym], length_base[sym]);
+
+      dist = tinf_decode_symbol(d, dt);
+
+      /* possibly get more bits from distance code */
+      offs = d.destLen - tinf_read_bits(d, dist_bits[dist], dist_base[dist]);
+
+      /* copy match */
+      for (i = offs; i < offs + length; ++i) {
+        d.dest[d.destLen++] = d.dest[i];
+      }
+    }
+  }
+}
+
+/* inflate an uncompressed block of data */
+function tinf_inflate_uncompressed_block(d) {
+  var length, invlength;
+  var i;
+  
+  /* unread from bitbuffer */
+  while (d.bitcount > 8) {
+    d.sourceIndex--;
+    d.bitcount -= 8;
+  }
+
+  /* get length */
+  length = d.source[d.sourceIndex + 1];
+  length = 256 * length + d.source[d.sourceIndex];
+
+  /* get one's complement of length */
+  invlength = d.source[d.sourceIndex + 3];
+  invlength = 256 * invlength + d.source[d.sourceIndex + 2];
+
+  /* check length */
+  if (length !== (~invlength & 0x0000ffff))
+    { return TINF_DATA_ERROR; }
+
+  d.sourceIndex += 4;
+
+  /* copy block */
+  for (i = length; i; --i)
+    { d.dest[d.destLen++] = d.source[d.sourceIndex++]; }
+
+  /* make sure we start next block on a byte boundary */
+  d.bitcount = 0;
+
+  return TINF_OK;
+}
+
+/* inflate stream from source to dest */
+function tinf_uncompress(source, dest) {
+  var d = new Data(source, dest);
+  var bfinal, btype, res;
+
+  do {
+    /* read final block flag */
+    bfinal = tinf_getbit(d);
+
+    /* read block type (2 bits) */
+    btype = tinf_read_bits(d, 2, 0);
+
+    /* decompress block */
+    switch (btype) {
+      case 0:
+        /* decompress uncompressed block */
+        res = tinf_inflate_uncompressed_block(d);
+        break;
+      case 1:
+        /* decompress block with fixed huffman trees */
+        res = tinf_inflate_block_data(d, sltree, sdtree);
+        break;
+      case 2:
+        /* decompress block with dynamic huffman trees */
+        tinf_decode_trees(d, d.ltree, d.dtree);
+        res = tinf_inflate_block_data(d, d.ltree, d.dtree);
+        break;
+      default:
+        res = TINF_DATA_ERROR;
+    }
+
+    if (res !== TINF_OK)
+      { throw new Error('Data error'); }
+
+  } while (!bfinal);
+
+  if (d.destLen < d.dest.length) {
+    if (typeof d.dest.slice === 'function')
+      { return d.dest.slice(0, d.destLen); }
+    else
+      { return d.dest.subarray(0, d.destLen); }
+  }
+  
+  return d.dest;
+}
+
+/* -------------------- *
+ * -- initialization -- *
+ * -------------------- */
+
+/* build fixed huffman trees */
+tinf_build_fixed_trees(sltree, sdtree);
+
+/* build extra bits and base tables */
+tinf_build_bits_base(length_bits, length_base, 4, 3);
+tinf_build_bits_base(dist_bits, dist_base, 2, 1);
+
+/* fix a special case */
+length_bits[28] = 0;
+length_base[28] = 258;
+
+var tinyInflate = tinf_uncompress;
+
+// The Bounding Box object
+
+function derive(v0, v1, v2, v3, t) {
+    return Math.pow(1 - t, 3) * v0 +
+        3 * Math.pow(1 - t, 2) * t * v1 +
+        3 * (1 - t) * Math.pow(t, 2) * v2 +
+        Math.pow(t, 3) * v3;
+}
+/**
+ * A bounding box is an enclosing box that describes the smallest measure within which all the points lie.
+ * It is used to calculate the bounding box of a glyph or text path.
+ *
+ * On initialization, x1/y1/x2/y2 will be NaN. Check if the bounding box is empty using `isEmpty()`.
+ *
+ * @exports opentype.BoundingBox
+ * @class
+ * @constructor
+ */
+function BoundingBox() {
+    this.x1 = Number.NaN;
+    this.y1 = Number.NaN;
+    this.x2 = Number.NaN;
+    this.y2 = Number.NaN;
+}
+
+/**
+ * Returns true if the bounding box is empty, that is, no points have been added to the box yet.
+ */
+BoundingBox.prototype.isEmpty = function() {
+    return isNaN(this.x1) || isNaN(this.y1) || isNaN(this.x2) || isNaN(this.y2);
+};
+
+/**
+ * Add the point to the bounding box.
+ * The x1/y1/x2/y2 coordinates of the bounding box will now encompass the given point.
+ * @param {number} x - The X coordinate of the point.
+ * @param {number} y - The Y coordinate of the point.
+ */
+BoundingBox.prototype.addPoint = function(x, y) {
+    if (typeof x === 'number') {
+        if (isNaN(this.x1) || isNaN(this.x2)) {
+            this.x1 = x;
+            this.x2 = x;
+        }
+        if (x < this.x1) {
+            this.x1 = x;
+        }
+        if (x > this.x2) {
+            this.x2 = x;
+        }
+    }
+    if (typeof y === 'number') {
+        if (isNaN(this.y1) || isNaN(this.y2)) {
+            this.y1 = y;
+            this.y2 = y;
+        }
+        if (y < this.y1) {
+            this.y1 = y;
+        }
+        if (y > this.y2) {
+            this.y2 = y;
+        }
+    }
+};
+
+/**
+ * Add a X coordinate to the bounding box.
+ * This extends the bounding box to include the X coordinate.
+ * This function is used internally inside of addBezier.
+ * @param {number} x - The X coordinate of the point.
+ */
+BoundingBox.prototype.addX = function(x) {
+    this.addPoint(x, null);
+};
+
+/**
+ * Add a Y coordinate to the bounding box.
+ * This extends the bounding box to include the Y coordinate.
+ * This function is used internally inside of addBezier.
+ * @param {number} y - The Y coordinate of the point.
+ */
+BoundingBox.prototype.addY = function(y) {
+    this.addPoint(null, y);
+};
+
+/**
+ * Add a Bézier curve to the bounding box.
+ * This extends the bounding box to include the entire Bézier.
+ * @param {number} x0 - The starting X coordinate.
+ * @param {number} y0 - The starting Y coordinate.
+ * @param {number} x1 - The X coordinate of the first control point.
+ * @param {number} y1 - The Y coordinate of the first control point.
+ * @param {number} x2 - The X coordinate of the second control point.
+ * @param {number} y2 - The Y coordinate of the second control point.
+ * @param {number} x - The ending X coordinate.
+ * @param {number} y - The ending Y coordinate.
+ */
+BoundingBox.prototype.addBezier = function(x0, y0, x1, y1, x2, y2, x, y) {
+    // This code is based on http://nishiohirokazu.blogspot.com/2009/06/how-to-calculate-bezier-curves-bounding.html
+    // and https://github.com/icons8/svg-path-bounding-box
+
+    var p0 = [x0, y0];
+    var p1 = [x1, y1];
+    var p2 = [x2, y2];
+    var p3 = [x, y];
+
+    this.addPoint(x0, y0);
+    this.addPoint(x, y);
+
+    for (var i = 0; i <= 1; i++) {
+        var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];
+        var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];
+        var c = 3 * p1[i] - 3 * p0[i];
+
+        if (a === 0) {
+            if (b === 0) { continue; }
+            var t = -c / b;
+            if (0 < t && t < 1) {
+                if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t)); }
+                if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t)); }
+            }
+            continue;
+        }
+
+        var b2ac = Math.pow(b, 2) - 4 * c * a;
+        if (b2ac < 0) { continue; }
+        var t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
+        if (0 < t1 && t1 < 1) {
+            if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t1)); }
+            if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t1)); }
+        }
+        var t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
+        if (0 < t2 && t2 < 1) {
+            if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t2)); }
+            if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t2)); }
+        }
+    }
+};
+
+/**
+ * Add a quadratic curve to the bounding box.
+ * This extends the bounding box to include the entire quadratic curve.
+ * @param {number} x0 - The starting X coordinate.
+ * @param {number} y0 - The starting Y coordinate.
+ * @param {number} x1 - The X coordinate of the control point.
+ * @param {number} y1 - The Y coordinate of the control point.
+ * @param {number} x - The ending X coordinate.
+ * @param {number} y - The ending Y coordinate.
+ */
+BoundingBox.prototype.addQuad = function(x0, y0, x1, y1, x, y) {
+    var cp1x = x0 + 2 / 3 * (x1 - x0);
+    var cp1y = y0 + 2 / 3 * (y1 - y0);
+    var cp2x = cp1x + 1 / 3 * (x - x0);
+    var cp2y = cp1y + 1 / 3 * (y - y0);
+    this.addBezier(x0, y0, cp1x, cp1y, cp2x, cp2y, x, y);
+};
+
+// Geometric objects
+
+/**
+ * A bézier path containing a set of path commands similar to a SVG path.
+ * Paths can be drawn on a context using `draw`.
+ * @exports opentype.Path
+ * @class
+ * @constructor
+ */
+function Path() {
+    this.commands = [];
+    this.fill = 'black';
+    this.stroke = null;
+    this.strokeWidth = 1;
+}
+
+/**
+ * @param  {number} x
+ * @param  {number} y
+ */
+Path.prototype.moveTo = function(x, y) {
+    this.commands.push({
+        type: 'M',
+        x: x,
+        y: y
+    });
+};
+
+/**
+ * @param  {number} x
+ * @param  {number} y
+ */
+Path.prototype.lineTo = function(x, y) {
+    this.commands.push({
+        type: 'L',
+        x: x,
+        y: y
+    });
+};
+
+/**
+ * Draws cubic curve
+ * @function
+ * curveTo
+ * @memberof opentype.Path.prototype
+ * @param  {number} x1 - x of control 1
+ * @param  {number} y1 - y of control 1
+ * @param  {number} x2 - x of control 2
+ * @param  {number} y2 - y of control 2
+ * @param  {number} x - x of path point
+ * @param  {number} y - y of path point
+ */
+
+/**
+ * Draws cubic curve
+ * @function
+ * bezierCurveTo
+ * @memberof opentype.Path.prototype
+ * @param  {number} x1 - x of control 1
+ * @param  {number} y1 - y of control 1
+ * @param  {number} x2 - x of control 2
+ * @param  {number} y2 - y of control 2
+ * @param  {number} x - x of path point
+ * @param  {number} y - y of path point
+ * @see curveTo
+ */
+Path.prototype.curveTo = Path.prototype.bezierCurveTo = function(x1, y1, x2, y2, x, y) {
+    this.commands.push({
+        type: 'C',
+        x1: x1,
+        y1: y1,
+        x2: x2,
+        y2: y2,
+        x: x,
+        y: y
+    });
+};
+
+/**
+ * Draws quadratic curve
+ * @function
+ * quadraticCurveTo
+ * @memberof opentype.Path.prototype
+ * @param  {number} x1 - x of control
+ * @param  {number} y1 - y of control
+ * @param  {number} x - x of path point
+ * @param  {number} y - y of path point
+ */
+
+/**
+ * Draws quadratic curve
+ * @function
+ * quadTo
+ * @memberof opentype.Path.prototype
+ * @param  {number} x1 - x of control
+ * @param  {number} y1 - y of control
+ * @param  {number} x - x of path point
+ * @param  {number} y - y of path point
+ */
+Path.prototype.quadTo = Path.prototype.quadraticCurveTo = function(x1, y1, x, y) {
+    this.commands.push({
+        type: 'Q',
+        x1: x1,
+        y1: y1,
+        x: x,
+        y: y
+    });
+};
+
+/**
+ * Closes the path
+ * @function closePath
+ * @memberof opentype.Path.prototype
+ */
+
+/**
+ * Close the path
+ * @function close
+ * @memberof opentype.Path.prototype
+ */
+Path.prototype.close = Path.prototype.closePath = function() {
+    this.commands.push({
+        type: 'Z'
+    });
+};
+
+/**
+ * Add the given path or list of commands to the commands of this path.
+ * @param  {Array} pathOrCommands - another opentype.Path, an opentype.BoundingBox, or an array of commands.
+ */
+Path.prototype.extend = function(pathOrCommands) {
+    if (pathOrCommands.commands) {
+        pathOrCommands = pathOrCommands.commands;
+    } else if (pathOrCommands instanceof BoundingBox) {
+        var box = pathOrCommands;
+        this.moveTo(box.x1, box.y1);
+        this.lineTo(box.x2, box.y1);
+        this.lineTo(box.x2, box.y2);
+        this.lineTo(box.x1, box.y2);
+        this.close();
+        return;
+    }
+
+    Array.prototype.push.apply(this.commands, pathOrCommands);
+};
+
+/**
+ * Calculate the bounding box of the path.
+ * @returns {opentype.BoundingBox}
+ */
+Path.prototype.getBoundingBox = function() {
+    var box = new BoundingBox();
+
+    var startX = 0;
+    var startY = 0;
+    var prevX = 0;
+    var prevY = 0;
+    for (var i = 0; i < this.commands.length; i++) {
+        var cmd = this.commands[i];
+        switch (cmd.type) {
+            case 'M':
+                box.addPoint(cmd.x, cmd.y);
+                startX = prevX = cmd.x;
+                startY = prevY = cmd.y;
+                break;
+            case 'L':
+                box.addPoint(cmd.x, cmd.y);
+                prevX = cmd.x;
+                prevY = cmd.y;
+                break;
+            case 'Q':
+                box.addQuad(prevX, prevY, cmd.x1, cmd.y1, cmd.x, cmd.y);
+                prevX = cmd.x;
+                prevY = cmd.y;
+                break;
+            case 'C':
+                box.addBezier(prevX, prevY, cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
+                prevX = cmd.x;
+                prevY = cmd.y;
+                break;
+            case 'Z':
+                prevX = startX;
+                prevY = startY;
+                break;
+            default:
+                throw new Error('Unexpected path command ' + cmd.type);
+        }
+    }
+    if (box.isEmpty()) {
+        box.addPoint(0, 0);
+    }
+    return box;
+};
+
+/**
+ * Draw the path to a 2D context.
+ * @param {CanvasRenderingContext2D} ctx - A 2D drawing context.
+ */
+Path.prototype.draw = function(ctx) {
+    ctx.beginPath();
+    for (var i = 0; i < this.commands.length; i += 1) {
+        var cmd = this.commands[i];
+        if (cmd.type === 'M') {
+            ctx.moveTo(cmd.x, cmd.y);
+        } else if (cmd.type === 'L') {
+            ctx.lineTo(cmd.x, cmd.y);
+        } else if (cmd.type === 'C') {
+            ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
+        } else if (cmd.type === 'Q') {
+            ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y);
+        } else if (cmd.type === 'Z') {
+            ctx.closePath();
+        }
+    }
+
+    if (this.fill) {
+        ctx.fillStyle = this.fill;
+        ctx.fill();
+    }
+
+    if (this.stroke) {
+        ctx.strokeStyle = this.stroke;
+        ctx.lineWidth = this.strokeWidth;
+        ctx.stroke();
+    }
+};
+
+/**
+ * Convert the Path to a string of path data instructions
+ * See http://www.w3.org/TR/SVG/paths.html#PathData
+ * @param  {number} [decimalPlaces=2] - The amount of decimal places for floating-point values
+ * @return {string}
+ */
+Path.prototype.toPathData = function(decimalPlaces) {
+    decimalPlaces = decimalPlaces !== undefined ? decimalPlaces : 2;
+
+    function floatToString(v) {
+        if (Math.round(v) === v) {
+            return '' + Math.round(v);
+        } else {
+            return v.toFixed(decimalPlaces);
+        }
+    }
+
+    function packValues() {
+        var arguments$1 = arguments;
+
+        var s = '';
+        for (var i = 0; i < arguments.length; i += 1) {
+            var v = arguments$1[i];
+            if (v >= 0 && i > 0) {
+                s += ' ';
+            }
+
+            s += floatToString(v);
+        }
+
+        return s;
+    }
+
+    var d = '';
+    for (var i = 0; i < this.commands.length; i += 1) {
+        var cmd = this.commands[i];
+        if (cmd.type === 'M') {
+            d += 'M' + packValues(cmd.x, cmd.y);
+        } else if (cmd.type === 'L') {
+            d += 'L' + packValues(cmd.x, cmd.y);
+        } else if (cmd.type === 'C') {
+            d += 'C' + packValues(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
+        } else if (cmd.type === 'Q') {
+            d += 'Q' + packValues(cmd.x1, cmd.y1, cmd.x, cmd.y);
+        } else if (cmd.type === 'Z') {
+            d += 'Z';
+        }
+    }
+
+    return d;
+};
+
+/**
+ * Convert the path to an SVG <path> element, as a string.
+ * @param  {number} [decimalPlaces=2] - The amount of decimal places for floating-point values
+ * @return {string}
+ */
+Path.prototype.toSVG = function(decimalPlaces) {
+    var svg = '<path d="';
+    svg += this.toPathData(decimalPlaces);
+    svg += '"';
+    if (this.fill && this.fill !== 'black') {
+        if (this.fill === null) {
+            svg += ' fill="none"';
+        } else {
+            svg += ' fill="' + this.fill + '"';
+        }
+    }
+
+    if (this.stroke) {
+        svg += ' stroke="' + this.stroke + '" stroke-width="' + this.strokeWidth + '"';
+    }
+
+    svg += '/>';
+    return svg;
+};
+
+/**
+ * Convert the path to a DOM element.
+ * @param  {number} [decimalPlaces=2] - The amount of decimal places for floating-point values
+ * @return {SVGPathElement}
+ */
+Path.prototype.toDOMElement = function(decimalPlaces) {
+    var temporaryPath = this.toPathData(decimalPlaces);
+    var newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+
+    newPath.setAttribute('d', temporaryPath);
+
+    return newPath;
+};
+
+// Run-time checking of preconditions.
+
+function fail(message) {
+    throw new Error(message);
+}
+
+// Precondition function that checks if the given predicate is true.
+// If not, it will throw an error.
+function argument(predicate, message) {
+    if (!predicate) {
+        fail(message);
+    }
+}
+var check = { fail: fail, argument: argument, assert: argument };
+
+// Data types used in the OpenType font file.
+
+var LIMIT16 = 32768; // The limit at which a 16-bit number switches signs == 2^15
+var LIMIT32 = 2147483648; // The limit at which a 32-bit number switches signs == 2 ^ 31
+
+/**
+ * @exports opentype.decode
+ * @class
+ */
+var decode = {};
+/**
+ * @exports opentype.encode
+ * @class
+ */
+var encode = {};
+/**
+ * @exports opentype.sizeOf
+ * @class
+ */
+var sizeOf = {};
+
+// Return a function that always returns the same value.
+function constant(v) {
+    return function() {
+        return v;
+    };
+}
+
+// OpenType data types //////////////////////////////////////////////////////
+
+/**
+ * Convert an 8-bit unsigned integer to a list of 1 byte.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.BYTE = function(v) {
+    check.argument(v >= 0 && v <= 255, 'Byte value should be between 0 and 255.');
+    return [v];
+};
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.BYTE = constant(1);
+
+/**
+ * Convert a 8-bit signed integer to a list of 1 byte.
+ * @param {string}
+ * @returns {Array}
+ */
+encode.CHAR = function(v) {
+    return [v.charCodeAt(0)];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.CHAR = constant(1);
+
+/**
+ * Convert an ASCII string to a list of bytes.
+ * @param {string}
+ * @returns {Array}
+ */
+encode.CHARARRAY = function(v) {
+    if (typeof v === 'undefined') {
+        v = '';
+        console.warn('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.');
+    }
+    var b = [];
+    for (var i = 0; i < v.length; i += 1) {
+        b[i] = v.charCodeAt(i);
+    }
+
+    return b;
+};
+
+/**
+ * @param {Array}
+ * @returns {number}
+ */
+sizeOf.CHARARRAY = function(v) {
+    if (typeof v === 'undefined') {
+        return 0;
+    }
+    return v.length;
+};
+
+/**
+ * Convert a 16-bit unsigned integer to a list of 2 bytes.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.USHORT = function(v) {
+    return [(v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.USHORT = constant(2);
+
+/**
+ * Convert a 16-bit signed integer to a list of 2 bytes.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.SHORT = function(v) {
+    // Two's complement
+    if (v >= LIMIT16) {
+        v = -(2 * LIMIT16 - v);
+    }
+
+    return [(v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.SHORT = constant(2);
+
+/**
+ * Convert a 24-bit unsigned integer to a list of 3 bytes.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.UINT24 = function(v) {
+    return [(v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.UINT24 = constant(3);
+
+/**
+ * Convert a 32-bit unsigned integer to a list of 4 bytes.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.ULONG = function(v) {
+    return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.ULONG = constant(4);
+
+/**
+ * Convert a 32-bit unsigned integer to a list of 4 bytes.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.LONG = function(v) {
+    // Two's complement
+    if (v >= LIMIT32) {
+        v = -(2 * LIMIT32 - v);
+    }
+
+    return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.LONG = constant(4);
+
+encode.FIXED = encode.ULONG;
+sizeOf.FIXED = sizeOf.ULONG;
+
+encode.FWORD = encode.SHORT;
+sizeOf.FWORD = sizeOf.SHORT;
+
+encode.UFWORD = encode.USHORT;
+sizeOf.UFWORD = sizeOf.USHORT;
+
+/**
+ * Convert a 32-bit Apple Mac timestamp integer to a list of 8 bytes, 64-bit timestamp.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.LONGDATETIME = function(v) {
+    return [0, 0, 0, 0, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.LONGDATETIME = constant(8);
+
+/**
+ * Convert a 4-char tag to a list of 4 bytes.
+ * @param {string}
+ * @returns {Array}
+ */
+encode.TAG = function(v) {
+    check.argument(v.length === 4, 'Tag should be exactly 4 ASCII characters.');
+    return [v.charCodeAt(0),
+            v.charCodeAt(1),
+            v.charCodeAt(2),
+            v.charCodeAt(3)];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.TAG = constant(4);
+
+// CFF data types ///////////////////////////////////////////////////////////
+
+encode.Card8 = encode.BYTE;
+sizeOf.Card8 = sizeOf.BYTE;
+
+encode.Card16 = encode.USHORT;
+sizeOf.Card16 = sizeOf.USHORT;
+
+encode.OffSize = encode.BYTE;
+sizeOf.OffSize = sizeOf.BYTE;
+
+encode.SID = encode.USHORT;
+sizeOf.SID = sizeOf.USHORT;
+
+// Convert a numeric operand or charstring number to a variable-size list of bytes.
+/**
+ * Convert a numeric operand or charstring number to a variable-size list of bytes.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.NUMBER = function(v) {
+    if (v >= -107 && v <= 107) {
+        return [v + 139];
+    } else if (v >= 108 && v <= 1131) {
+        v = v - 108;
+        return [(v >> 8) + 247, v & 0xFF];
+    } else if (v >= -1131 && v <= -108) {
+        v = -v - 108;
+        return [(v >> 8) + 251, v & 0xFF];
+    } else if (v >= -32768 && v <= 32767) {
+        return encode.NUMBER16(v);
+    } else {
+        return encode.NUMBER32(v);
+    }
+};
+
+/**
+ * @param {number}
+ * @returns {number}
+ */
+sizeOf.NUMBER = function(v) {
+    return encode.NUMBER(v).length;
+};
+
+/**
+ * Convert a signed number between -32768 and +32767 to a three-byte value.
+ * This ensures we always use three bytes, but is not the most compact format.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.NUMBER16 = function(v) {
+    return [28, (v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.NUMBER16 = constant(3);
+
+/**
+ * Convert a signed number between -(2^31) and +(2^31-1) to a five-byte value.
+ * This is useful if you want to be sure you always use four bytes,
+ * at the expense of wasting a few bytes for smaller numbers.
+ * @param {number}
+ * @returns {Array}
+ */
+encode.NUMBER32 = function(v) {
+    return [29, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
+};
+
+/**
+ * @constant
+ * @type {number}
+ */
+sizeOf.NUMBER32 = constant(5);
+
+/**
+ * @param {number}
+ * @returns {Array}
+ */
+encode.REAL = function(v) {
+    var value = v.toString();
+
+    // Some numbers use an epsilon to encode the value. (e.g. JavaScript will store 0.0000001 as 1e-7)
+    // This code converts it back to a number without the epsilon.
+    var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value);
+    if (m) {
+        var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length));
+        value = (Math.round(v * epsilon) / epsilon).toString();
+    }
+
+    var nibbles = '';
+    for (var i = 0, ii = value.length; i < ii; i += 1) {
+        var c = value[i];
+        if (c === 'e') {
+            nibbles += value[++i] === '-' ? 'c' : 'b';
+        } else if (c === '.') {
+            nibbles += 'a';
+        } else if (c === '-') {
+            nibbles += 'e';
+        } else {
+            nibbles += c;
+        }
+    }
+
+    nibbles += (nibbles.length & 1) ? 'f' : 'ff';
+    var out = [30];
+    for (var i$1 = 0, ii$1 = nibbles.length; i$1 < ii$1; i$1 += 2) {
+        out.push(parseInt(nibbles.substr(i$1, 2), 16));
+    }
+
+    return out;
+};
+
+/**
+ * @param {number}
+ * @returns {number}
+ */
+sizeOf.REAL = function(v) {
+    return encode.REAL(v).length;
+};
+
+encode.NAME = encode.CHARARRAY;
+sizeOf.NAME = sizeOf.CHARARRAY;
+
+encode.STRING = encode.CHARARRAY;
+sizeOf.STRING = sizeOf.CHARARRAY;
+
+/**
+ * @param {DataView} data
+ * @param {number} offset
+ * @param {number} numBytes
+ * @returns {string}
+ */
+decode.UTF8 = function(data, offset, numBytes) {
+    var codePoints = [];
+    var numChars = numBytes;
+    for (var j = 0; j < numChars; j++, offset += 1) {
+        codePoints[j] = data.getUint8(offset);
+    }
+
+    return String.fromCharCode.apply(null, codePoints);
+};
+
+/**
+ * @param {DataView} data
+ * @param {number} offset
+ * @param {number} numBytes
+ * @returns {string}
+ */
+decode.UTF16 = function(data, offset, numBytes) {
+    var codePoints = [];
+    var numChars = numBytes / 2;
+    for (var j = 0; j < numChars; j++, offset += 2) {
+        codePoints[j] = data.getUint16(offset);
+    }
+
+    return String.fromCharCode.apply(null, codePoints);
+};
+
+/**
+ * Convert a JavaScript string to UTF16-BE.
+ * @param {string}
+ * @returns {Array}
+ */
+encode.UTF16 = function(v) {
+    var b = [];
+    for (var i = 0; i < v.length; i += 1) {
+        var codepoint = v.charCodeAt(i);
+        b[b.length] = (codepoint >> 8) & 0xFF;
+        b[b.length] = codepoint & 0xFF;
+    }
+
+    return b;
+};
+
+/**
+ * @param {string}
+ * @returns {number}
+ */
+sizeOf.UTF16 = function(v) {
+    return v.length * 2;
+};
+
+// Data for converting old eight-bit Macintosh encodings to Unicode.
+// This representation is optimized for decoding; encoding is slower
+// and needs more memory. The assumption is that all opentype.js users
+// want to open fonts, but saving a font will be comparatively rare
+// so it can be more expensive. Keyed by IANA character set name.
+//
+// Python script for generating these strings:
+//
+//     s = u''.join([chr(c).decode('mac_greek') for c in range(128, 256)])
+//     print(s.encode('utf-8'))
+/**
+ * @private
+ */
+var eightBitMacEncodings = {
+    'x-mac-croatian':  // Python: 'mac_croatian'
+    'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®Š™´¨≠ŽØ∞±≤≥∆µ∂∑∏š∫ªºΩžø' +
+    '¿¡¬√ƒ≈ƫȅ ÀÃÕŒœĐ—“”‘’÷◊©⁄€‹›Æ»–·‚„‰ÂćÁčÈÍÎÏÌÓÔđÒÚÛÙıˆ˜¯πË˚¸Êæˇ',
+    'x-mac-cyrillic':  // Python: 'mac_cyrillic'
+    'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°Ґ£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµґЈЄєЇїЉљЊњ' +
+    'јЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю',
+    'x-mac-gaelic': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/GAELIC.TXT
+    'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØḂ±≤≥ḃĊċḊḋḞḟĠġṀæø' +
+    'ṁṖṗɼƒſṠ«»… ÀÃÕŒœ–—“”‘’ṡẛÿŸṪ€‹›Ŷŷṫ·Ỳỳ⁊ÂÊÁËÈÍÎÏÌÓÔ♣ÒÚÛÙıÝýŴŵẄẅẀẁẂẃ',
+    'x-mac-greek':  // Python: 'mac_greek'
+    'Ĺ²É³ÖÜ΅àâä΄¨çéèê룙î‰ôö¦€ùûü†ΓΔΘΛΞΠß®©ΣΪ§≠°·Α±≤≥¥ΒΕΖΗΙΚΜΦΫΨΩ' +
+    'άΝ¬ΟΡ≈Τ«»… ΥΧΆΈœ–―“”‘’÷ΉΊΌΎέήίόΏύαβψδεφγηιξκλμνοπώρστθωςχυζϊϋΐΰ\u00AD',
+    'x-mac-icelandic':  // Python: 'mac_iceland'
+    'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûüݰ¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' +
+    '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€ÐðÞþý·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ',
+    'x-mac-inuit': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/INUIT.TXT
+    'ᐃᐄᐅᐆᐊᐋᐱᐲᐳᐴᐸᐹᑉᑎᑏᑐᑑᑕᑖᑦᑭᑮᑯᑰᑲᑳᒃᒋᒌᒍᒎᒐᒑ°ᒡᒥᒦ•¶ᒧ®©™ᒨᒪᒫᒻᓂᓃᓄᓅᓇᓈᓐᓯᓰᓱᓲᓴᓵᔅᓕᓖᓗ' +
+    'ᓘᓚᓛᓪᔨᔩᔪᔫᔭ… ᔮᔾᕕᕖᕗ–—“”‘’ᕘᕙᕚᕝᕆᕇᕈᕉᕋᕌᕐᕿᖀᖁᖂᖃᖄᖅᖏᖐᖑᖒᖓᖔᖕᙱᙲᙳᙴᙵᙶᖖᖠᖡᖢᖣᖤᖥᖦᕼŁł',
+    'x-mac-ce':  // Python: 'mac_latin2'
+    'ÄĀāÉĄÖÜáąČäčĆć鏟ĎíďĒēĖóėôöõúĚěü†°Ę£§•¶ß®©™ę¨≠ģĮįĪ≤≥īĶ∂∑łĻļĽľĹĺŅ' +
+    'ņѬ√ńŇ∆«»… ňŐÕőŌ–—“”‘’÷◊ōŔŕŘ‹›řŖŗŠ‚„šŚśÁŤťÍŽžŪÓÔūŮÚůŰűŲųÝýķŻŁżĢˇ',
+    macintosh:  // Python: 'mac_roman'
+    'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' +
+    '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ',
+    'x-mac-romanian':  // Python: 'mac_romanian'
+    'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ĂȘ∞±≤≥¥µ∂∑∏π∫ªºΩăș' +
+    '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›Țț‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ',
+    'x-mac-turkish':  // Python: 'mac_turkish'
+    'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' +
+    '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸĞğİıŞş‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙˆ˜¯˘˙˚¸˝˛ˇ'
+};
+
+/**
+ * Decodes an old-style Macintosh string. Returns either a Unicode JavaScript
+ * string, or 'undefined' if the encoding is unsupported. For example, we do
+ * not support Chinese, Japanese or Korean because these would need large
+ * mapping tables.
+ * @param {DataView} dataView
+ * @param {number} offset
+ * @param {number} dataLength
+ * @param {string} encoding
+ * @returns {string}
+ */
+decode.MACSTRING = function(dataView, offset, dataLength, encoding) {
+    var table = eightBitMacEncodings[encoding];
+    if (table === undefined) {
+        return undefined;
+    }
+
+    var result = '';
+    for (var i = 0; i < dataLength; i++) {
+        var c = dataView.getUint8(offset + i);
+        // In all eight-bit Mac encodings, the characters 0x00..0x7F are
+        // mapped to U+0000..U+007F; we only need to look up the others.
+        if (c <= 0x7F) {
+            result += String.fromCharCode(c);
+        } else {
+            result += table[c & 0x7F];
+        }
+    }
+
+    return result;
+};
+
+// Helper function for encode.MACSTRING. Returns a dictionary for mapping
+// Unicode character codes to their 8-bit MacOS equivalent. This table
+// is not exactly a super cheap data structure, but we do not care because
+// encoding Macintosh strings is only rarely needed in typical applications.
+var macEncodingTableCache = typeof WeakMap === 'function' && new WeakMap();
+var macEncodingCacheKeys;
+var getMacEncodingTable = function (encoding) {
+    // Since we use encoding as a cache key for WeakMap, it has to be
+    // a String object and not a literal. And at least on NodeJS 2.10.1,
+    // WeakMap requires that the same String instance is passed for cache hits.
+    if (!macEncodingCacheKeys) {
+        macEncodingCacheKeys = {};
+        for (var e in eightBitMacEncodings) {
+            /*jshint -W053 */  // Suppress "Do not use String as a constructor."
+            macEncodingCacheKeys[e] = new String(e);
+        }
+    }
+
+    var cacheKey = macEncodingCacheKeys[encoding];
+    if (cacheKey === undefined) {
+        return undefined;
+    }
+
+    // We can't do "if (cache.has(key)) {return cache.get(key)}" here:
+    // since garbage collection may run at any time, it could also kick in
+    // between the calls to cache.has() and cache.get(). In that case,
+    // we would return 'undefined' even though we do support the encoding.
+    if (macEncodingTableCache) {
+        var cachedTable = macEncodingTableCache.get(cacheKey);
+        if (cachedTable !== undefined) {
+            return cachedTable;
+        }
+    }
+
+    var decodingTable = eightBitMacEncodings[encoding];
+    if (decodingTable === undefined) {
+        return undefined;
+    }
+
+    var encodingTable = {};
+    for (var i = 0; i < decodingTable.length; i++) {
+        encodingTable[decodingTable.charCodeAt(i)] = i + 0x80;
+    }
+
+    if (macEncodingTableCache) {
+        macEncodingTableCache.set(cacheKey, encodingTable);
+    }
+
+    return encodingTable;
+};
+
+/**
+ * Encodes an old-style Macintosh string. Returns a byte array upon success.
+ * If the requested encoding is unsupported, or if the input string contains
+ * a character that cannot be expressed in the encoding, the function returns
+ * 'undefined'.
+ * @param {string} str
+ * @param {string} encoding
+ * @returns {Array}
+ */
+encode.MACSTRING = function(str, encoding) {
+    var table = getMacEncodingTable(encoding);
+    if (table === undefined) {
+        return undefined;
+    }
+
+    var result = [];
+    for (var i = 0; i < str.length; i++) {
+        var c = str.charCodeAt(i);
+
+        // In all eight-bit Mac encodings, the characters 0x00..0x7F are
+        // mapped to U+0000..U+007F; we only need to look up the others.
+        if (c >= 0x80) {
+            c = table[c];
+            if (c === undefined) {
+                // str contains a Unicode character that cannot be encoded
+                // in the requested encoding.
+                return undefined;
+            }
+        }
+        result[i] = c;
+        // result.push(c);
+    }
+
+    return result;
+};
+
+/**
+ * @param {string} str
+ * @param {string} encoding
+ * @returns {number}
+ */
+sizeOf.MACSTRING = function(str, encoding) {
+    var b = encode.MACSTRING(str, encoding);
+    if (b !== undefined) {
+        return b.length;
+    } else {
+        return 0;
+    }
+};
+
+// Helper for encode.VARDELTAS
+function isByteEncodable(value) {
+    return value >= -128 && value <= 127;
+}
+
+// Helper for encode.VARDELTAS
+function encodeVarDeltaRunAsZeroes(deltas, pos, result) {
+    var runLength = 0;
+    var numDeltas = deltas.length;
+    while (pos < numDeltas && runLength < 64 && deltas[pos] === 0) {
+        ++pos;
+        ++runLength;
+    }
+    result.push(0x80 | (runLength - 1));
+    return pos;
+}
+
+// Helper for encode.VARDELTAS
+function encodeVarDeltaRunAsBytes(deltas, offset, result) {
+    var runLength = 0;
+    var numDeltas = deltas.length;
+    var pos = offset;
+    while (pos < numDeltas && runLength < 64) {
+        var value = deltas[pos];
+        if (!isByteEncodable(value)) {
+            break;
+        }
+
+        // Within a byte-encoded run of deltas, a single zero is best
+        // stored literally as 0x00 value. However, if we have two or
+        // more zeroes in a sequence, it is better to start a new run.
+        // Fore example, the sequence of deltas [15, 15, 0, 15, 15]
+        // becomes 6 bytes (04 0F 0F 00 0F 0F) when storing the zero
+        // within the current run, but 7 bytes (01 0F 0F 80 01 0F 0F)
+        // when starting a new run.
+        if (value === 0 && pos + 1 < numDeltas && deltas[pos + 1] === 0) {
+            break;
+        }
+
+        ++pos;
+        ++runLength;
+    }
+    result.push(runLength - 1);
+    for (var i = offset; i < pos; ++i) {
+        result.push((deltas[i] + 256) & 0xff);
+    }
+    return pos;
+}
+
+// Helper for encode.VARDELTAS
+function encodeVarDeltaRunAsWords(deltas, offset, result) {
+    var runLength = 0;
+    var numDeltas = deltas.length;
+    var pos = offset;
+    while (pos < numDeltas && runLength < 64) {
+        var value = deltas[pos];
+
+        // Within a word-encoded run of deltas, it is easiest to start
+        // a new run (with a different encoding) whenever we encounter
+        // a zero value. For example, the sequence [0x6666, 0, 0x7777]
+        // needs 7 bytes when storing the zero inside the current run
+        // (42 66 66 00 00 77 77), and equally 7 bytes when starting a
+        // new run (40 66 66 80 40 77 77).
+        if (value === 0) {
+            break;
+        }
+
+        // Within a word-encoded run of deltas, a single value in the
+        // range (-128..127) should be encoded within the current run
+        // because it is more compact. For example, the sequence
+        // [0x6666, 2, 0x7777] becomes 7 bytes when storing the value
+        // literally (42 66 66 00 02 77 77), but 8 bytes when starting
+        // a new run (40 66 66 00 02 40 77 77).
+        if (isByteEncodable(value) && pos + 1 < numDeltas && isByteEncodable(deltas[pos + 1])) {
+            break;
+        }
+
+        ++pos;
+        ++runLength;
+    }
+    result.push(0x40 | (runLength - 1));
+    for (var i = offset; i < pos; ++i) {
+        var val = deltas[i];
+        result.push(((val + 0x10000) >> 8) & 0xff, (val + 0x100) & 0xff);
+    }
+    return pos;
+}
+
+/**
+ * Encode a list of variation adjustment deltas.
+ *
+ * Variation adjustment deltas are used in ‘gvar’ and ‘cvar’ tables.
+ * They indicate how points (in ‘gvar’) or values (in ‘cvar’) get adjusted
+ * when generating instances of variation fonts.
+ *
+ * @see https://www.microsoft.com/typography/otspec/gvar.htm
+ * @see https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
+ * @param {Array}
+ * @return {Array}
+ */
+encode.VARDELTAS = function(deltas) {
+    var pos = 0;
+    var result = [];
+    while (pos < deltas.length) {
+        var value = deltas[pos];
+        if (value === 0) {
+            pos = encodeVarDeltaRunAsZeroes(deltas, pos, result);
+        } else if (value >= -128 && value <= 127) {
+            pos = encodeVarDeltaRunAsBytes(deltas, pos, result);
+        } else {
+            pos = encodeVarDeltaRunAsWords(deltas, pos, result);
+        }
+    }
+    return result;
+};
+
+// Convert a list of values to a CFF INDEX structure.
+// The values should be objects containing name / type / value.
+/**
+ * @param {Array} l
+ * @returns {Array}
+ */
+encode.INDEX = function(l) {
+    //var offset, offsets, offsetEncoder, encodedOffsets, encodedOffset, data,
+    //    i, v;
+    // Because we have to know which data type to use to encode the offsets,
+    // we have to go through the values twice: once to encode the data and
+    // calculate the offsets, then again to encode the offsets using the fitting data type.
+    var offset = 1; // First offset is always 1.
+    var offsets = [offset];
+    var data = [];
+    for (var i = 0; i < l.length; i += 1) {
+        var v = encode.OBJECT(l[i]);
+        Array.prototype.push.apply(data, v);
+        offset += v.length;
+        offsets.push(offset);
+    }
+
+    if (data.length === 0) {
+        return [0, 0];
+    }
+
+    var encodedOffsets = [];
+    var offSize = (1 + Math.floor(Math.log(offset) / Math.log(2)) / 8) | 0;
+    var offsetEncoder = [undefined, encode.BYTE, encode.USHORT, encode.UINT24, encode.ULONG][offSize];
+    for (var i$1 = 0; i$1 < offsets.length; i$1 += 1) {
+        var encodedOffset = offsetEncoder(offsets[i$1]);
+        Array.prototype.push.apply(encodedOffsets, encodedOffset);
+    }
+
+    return Array.prototype.concat(encode.Card16(l.length),
+                           encode.OffSize(offSize),
+                           encodedOffsets,
+                           data);
+};
+
+/**
+ * @param {Array}
+ * @returns {number}
+ */
+sizeOf.INDEX = function(v) {
+    return encode.INDEX(v).length;
+};
+
+/**
+ * Convert an object to a CFF DICT structure.
+ * The keys should be numeric.
+ * The values should be objects containing name / type / value.
+ * @param {Object} m
+ * @returns {Array}
+ */
+encode.DICT = function(m) {
+    var d = [];
+    var keys = Object.keys(m);
+    var length = keys.length;
+
+    for (var i = 0; i < length; i += 1) {
+        // Object.keys() return string keys, but our keys are always numeric.
+        var k = parseInt(keys[i], 0);
+        var v = m[k];
+        // Value comes before the key.
+        d = d.concat(encode.OPERAND(v.value, v.type));
+        d = d.concat(encode.OPERATOR(k));
+    }
+
+    return d;
+};
+
+/**
+ * @param {Object}
+ * @returns {number}
+ */
+sizeOf.DICT = function(m) {
+    return encode.DICT(m).length;
+};
+
+/**
+ * @param {number}
+ * @returns {Array}
+ */
+encode.OPERATOR = function(v) {
+    if (v < 1200) {
+        return [v];
+    } else {
+        return [12, v - 1200];
+    }
+};
+
+/**
+ * @param {Array} v
+ * @param {string}
+ * @returns {Array}
+ */
+encode.OPERAND = function(v, type) {
+    var d = [];
+    if (Array.isArray(type)) {
+        for (var i = 0; i < type.length; i += 1) {
+            check.argument(v.length === type.length, 'Not enough arguments given for type' + type);
+            d = d.concat(encode.OPERAND(v[i], type[i]));
+        }
+    } else {
+        if (type === 'SID') {
+            d = d.concat(encode.NUMBER(v));
+        } else if (type === 'offset') {
+            // We make it easy for ourselves and always encode offsets as
+            // 4 bytes. This makes offset calculation for the top dict easier.
+            d = d.concat(encode.NUMBER32(v));
+        } else if (type === 'number') {
+            d = d.concat(encode.NUMBER(v));
+        } else if (type === 'real') {
+            d = d.concat(encode.REAL(v));
+        } else {
+            throw new Error('Unknown operand type ' + type);
+            // FIXME Add support for booleans
+        }
+    }
+
+    return d;
+};
+
+encode.OP = encode.BYTE;
+sizeOf.OP = sizeOf.BYTE;
+
+// memoize charstring encoding using WeakMap if available
+var wmm = typeof WeakMap === 'function' && new WeakMap();
+
+/**
+ * Convert a list of CharString operations to bytes.
+ * @param {Array}
+ * @returns {Array}
+ */
+encode.CHARSTRING = function(ops) {
+    // See encode.MACSTRING for why we don't do "if (wmm && wmm.has(ops))".
+    if (wmm) {
+        var cachedValue = wmm.get(ops);
+        if (cachedValue !== undefined) {
+            return cachedValue;
+        }
+    }
+
+    var d = [];
+    var length = ops.length;
+
+    for (var i = 0; i < length; i += 1) {
+        var op = ops[i];
+        d = d.concat(encode[op.type](op.value));
+    }
+
+    if (wmm) {
+        wmm.set(ops, d);
+    }
+
+    return d;
+};
+
+/**
+ * @param {Array}
+ * @returns {number}
+ */
+sizeOf.CHARSTRING = function(ops) {
+    return encode.CHARSTRING(ops).length;
+};
+
+// Utility functions ////////////////////////////////////////////////////////
+
+/**
+ * Convert an object containing name / type / value to bytes.
+ * @param {Object}
+ * @returns {Array}
+ */
+encode.OBJECT = function(v) {
+    var encodingFunction = encode[v.type];
+    check.argument(encodingFunction !== undefined, 'No encoding function for type ' + v.type);
+    return encodingFunction(v.value);
+};
+
+/**
+ * @param {Object}
+ * @returns {number}
+ */
+sizeOf.OBJECT = function(v) {
+    var sizeOfFunction = sizeOf[v.type];
+    check.argument(sizeOfFunction !== undefined, 'No sizeOf function for type ' + v.type);
+    return sizeOfFunction(v.value);
+};
+
+/**
+ * Convert a table object to bytes.
+ * A table contains a list of fields containing the metadata (name, type and default value).
+ * The table itself has the field values set as attributes.
+ * @param {opentype.Table}
+ * @returns {Array}
+ */
+encode.TABLE = function(table) {
+    var d = [];
+    var length = table.fields.length;
+    var subtables = [];
+    var subtableOffsets = [];
+
+    for (var i = 0; i < length; i += 1) {
+        var field = table.fields[i];
+        var encodingFunction = encode[field.type];
+        check.argument(encodingFunction !== undefined, 'No encoding function for field type ' + field.type + ' (' + field.name + ')');
+        var value = table[field.name];
+        if (value === undefined) {
+            value = field.value;
+        }
+
+        var bytes = encodingFunction(value);
+
+        if (field.type === 'TABLE') {
+            subtableOffsets.push(d.length);
+            d = d.concat([0, 0]);
+            subtables.push(bytes);
+        } else {
+            d = d.concat(bytes);
+        }
+    }
+
+    for (var i$1 = 0; i$1 < subtables.length; i$1 += 1) {
+        var o = subtableOffsets[i$1];
+        var offset = d.length;
+        check.argument(offset < 65536, 'Table ' + table.tableName + ' too big.');
+        d[o] = offset >> 8;
+        d[o + 1] = offset & 0xff;
+        d = d.concat(subtables[i$1]);
+    }
+
+    return d;
+};
+
+/**
+ * @param {opentype.Table}
+ * @returns {number}
+ */
+sizeOf.TABLE = function(table) {
+    var numBytes = 0;
+    var length = table.fields.length;
+
+    for (var i = 0; i < length; i += 1) {
+        var field = table.fields[i];
+        var sizeOfFunction = sizeOf[field.type];
+        check.argument(sizeOfFunction !== undefined, 'No sizeOf function for field type ' + field.type + ' (' + field.name + ')');
+        var value = table[field.name];
+        if (value === undefined) {
+            value = field.value;
+        }
+
+        numBytes += sizeOfFunction(value);
+
+        // Subtables take 2 more bytes for offsets.
+        if (field.type === 'TABLE') {
+            numBytes += 2;
+        }
+    }
+
+    return numBytes;
+};
+
+encode.RECORD = encode.TABLE;
+sizeOf.RECORD = sizeOf.TABLE;
+
+// Merge in a list of bytes.
+encode.LITERAL = function(v) {
+    return v;
+};
+
+sizeOf.LITERAL = function(v) {
+    return v.length;
+};
+
+// Table metadata
+
+/**
+ * @exports opentype.Table
+ * @class
+ * @param {string} tableName
+ * @param {Array} fields
+ * @param {Object} options
+ * @constructor
+ */
+function Table(tableName, fields, options) {
+    // For coverage tables with coverage format 2, we do not want to add the coverage data directly to the table object,
+    // as this will result in wrong encoding order of the coverage data on serialization to bytes.
+    // The fallback of using the field values directly when not present on the table is handled in types.encode.TABLE() already.
+    if (fields.length && (fields[0].name !== 'coverageFormat' || fields[0].value === 1)) {
+        for (var i = 0; i < fields.length; i += 1) {
+            var field = fields[i];
+            this[field.name] = field.value;
+        }
+    }
+
+    this.tableName = tableName;
+    this.fields = fields;
+    if (options) {
+        var optionKeys = Object.keys(options);
+        for (var i$1 = 0; i$1 < optionKeys.length; i$1 += 1) {
+            var k = optionKeys[i$1];
+            var v = options[k];
+            if (this[k] !== undefined) {
+                this[k] = v;
+            }
+        }
+    }
+}
+
+/**
+ * Encodes the table and returns an array of bytes
+ * @return {Array}
+ */
+Table.prototype.encode = function() {
+    return encode.TABLE(this);
+};
+
+/**
+ * Get the size of the table.
+ * @return {number}
+ */
+Table.prototype.sizeOf = function() {
+    return sizeOf.TABLE(this);
+};
+
+/**
+ * @private
+ */
+function ushortList(itemName, list, count) {
+    if (count === undefined) {
+        count = list.length;
+    }
+    var fields = new Array(list.length + 1);
+    fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count};
+    for (var i = 0; i < list.length; i++) {
+        fields[i + 1] = {name: itemName + i, type: 'USHORT', value: list[i]};
+    }
+    return fields;
+}
+
+/**
+ * @private
+ */
+function tableList(itemName, records, itemCallback) {
+    var count = records.length;
+    var fields = new Array(count + 1);
+    fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count};
+    for (var i = 0; i < count; i++) {
+        fields[i + 1] = {name: itemName + i, type: 'TABLE', value: itemCallback(records[i], i)};
+    }
+    return fields;
+}
+
+/**
+ * @private
+ */
+function recordList(itemName, records, itemCallback) {
+    var count = records.length;
+    var fields = [];
+    fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count};
+    for (var i = 0; i < count; i++) {
+        fields = fields.concat(itemCallback(records[i], i));
+    }
+    return fields;
+}
+
+// Common Layout Tables
+
+/**
+ * @exports opentype.Coverage
+ * @class
+ * @param {opentype.Table}
+ * @constructor
+ * @extends opentype.Table
+ */
+function Coverage(coverageTable) {
+    if (coverageTable.format === 1) {
+        Table.call(this, 'coverageTable',
+            [{name: 'coverageFormat', type: 'USHORT', value: 1}]
+            .concat(ushortList('glyph', coverageTable.glyphs))
+        );
+    } else if (coverageTable.format === 2) {
+        Table.call(this, 'coverageTable',
+            [{name: 'coverageFormat', type: 'USHORT', value: 2}]
+            .concat(recordList('rangeRecord', coverageTable.ranges, function(RangeRecord) {
+                return [
+                    {name: 'startGlyphID', type: 'USHORT', value: RangeRecord.start},
+                    {name: 'endGlyphID', type: 'USHORT', value: RangeRecord.end},
+                    {name: 'startCoverageIndex', type: 'USHORT', value: RangeRecord.index} ];
+            }))
+        );
+    } else {
+        check.assert(false, 'Coverage format must be 1 or 2.');
+    }
+}
+Coverage.prototype = Object.create(Table.prototype);
+Coverage.prototype.constructor = Coverage;
+
+function ScriptList(scriptListTable) {
+    Table.call(this, 'scriptListTable',
+        recordList('scriptRecord', scriptListTable, function(scriptRecord, i) {
+            var script = scriptRecord.script;
+            var defaultLangSys = script.defaultLangSys;
+            check.assert(!!defaultLangSys, 'Unable to write GSUB: script ' + scriptRecord.tag + ' has no default language system.');
+            return [
+                {name: 'scriptTag' + i, type: 'TAG', value: scriptRecord.tag},
+                {name: 'script' + i, type: 'TABLE', value: new Table('scriptTable', [
+                    {name: 'defaultLangSys', type: 'TABLE', value: new Table('defaultLangSys', [
+                        {name: 'lookupOrder', type: 'USHORT', value: 0},
+                        {name: 'reqFeatureIndex', type: 'USHORT', value: defaultLangSys.reqFeatureIndex}]
+                        .concat(ushortList('featureIndex', defaultLangSys.featureIndexes)))}
+                    ].concat(recordList('langSys', script.langSysRecords, function(langSysRecord, i) {
+                        var langSys = langSysRecord.langSys;
+                        return [
+                            {name: 'langSysTag' + i, type: 'TAG', value: langSysRecord.tag},
+                            {name: 'langSys' + i, type: 'TABLE', value: new Table('langSys', [
+                                {name: 'lookupOrder', type: 'USHORT', value: 0},
+                                {name: 'reqFeatureIndex', type: 'USHORT', value: langSys.reqFeatureIndex}
+                                ].concat(ushortList('featureIndex', langSys.featureIndexes)))}
+                        ];
+                    })))}
+            ];
+        })
+    );
+}
+ScriptList.prototype = Object.create(Table.prototype);
+ScriptList.prototype.constructor = ScriptList;
+
+/**
+ * @exports opentype.FeatureList
+ * @class
+ * @param {opentype.Table}
+ * @constructor
+ * @extends opentype.Table
+ */
+function FeatureList(featureListTable) {
+    Table.call(this, 'featureListTable',
+        recordList('featureRecord', featureListTable, function(featureRecord, i) {
+            var feature = featureRecord.feature;
+            return [
+                {name: 'featureTag' + i, type: 'TAG', value: featureRecord.tag},
+                {name: 'feature' + i, type: 'TABLE', value: new Table('featureTable', [
+                    {name: 'featureParams', type: 'USHORT', value: feature.featureParams} ].concat(ushortList('lookupListIndex', feature.lookupListIndexes)))}
+            ];
+        })
+    );
+}
+FeatureList.prototype = Object.create(Table.prototype);
+FeatureList.prototype.constructor = FeatureList;
+
+/**
+ * @exports opentype.LookupList
+ * @class
+ * @param {opentype.Table}
+ * @param {Object}
+ * @constructor
+ * @extends opentype.Table
+ */
+function LookupList(lookupListTable, subtableMakers) {
+    Table.call(this, 'lookupListTable', tableList('lookup', lookupListTable, function(lookupTable) {
+        var subtableCallback = subtableMakers[lookupTable.lookupType];
+        check.assert(!!subtableCallback, 'Unable to write GSUB lookup type ' + lookupTable.lookupType + ' tables.');
+        return new Table('lookupTable', [
+            {name: 'lookupType', type: 'USHORT', value: lookupTable.lookupType},
+            {name: 'lookupFlag', type: 'USHORT', value: lookupTable.lookupFlag}
+        ].concat(tableList('subtable', lookupTable.subtables, subtableCallback)));
+    }));
+}
+LookupList.prototype = Object.create(Table.prototype);
+LookupList.prototype.constructor = LookupList;
+
+// Record = same as Table, but inlined (a Table has an offset and its data is further in the stream)
+// Don't use offsets inside Records (probable bug), only in Tables.
+var table = {
+    Table: Table,
+    Record: Table,
+    Coverage: Coverage,
+    ScriptList: ScriptList,
+    FeatureList: FeatureList,
+    LookupList: LookupList,
+    ushortList: ushortList,
+    tableList: tableList,
+    recordList: recordList,
+};
+
+// Parsing utility functions
+
+// Retrieve an unsigned byte from the DataView.
+function getByte(dataView, offset) {
+    return dataView.getUint8(offset);
+}
+
+// Retrieve an unsigned 16-bit short from the DataView.
+// The value is stored in big endian.
+function getUShort(dataView, offset) {
+    return dataView.getUint16(offset, false);
+}
+
+// Retrieve a signed 16-bit short from the DataView.
+// The value is stored in big endian.
+function getShort(dataView, offset) {
+    return dataView.getInt16(offset, false);
+}
+
+// Retrieve an unsigned 32-bit long from the DataView.
+// The value is stored in big endian.
+function getULong(dataView, offset) {
+    return dataView.getUint32(offset, false);
+}
+
+// Retrieve a 32-bit signed fixed-point number (16.16) from the DataView.
+// The value is stored in big endian.
+function getFixed(dataView, offset) {
+    var decimal = dataView.getInt16(offset, false);
+    var fraction = dataView.getUint16(offset + 2, false);
+    return decimal + fraction / 65535;
+}
+
+// Retrieve a 4-character tag from the DataView.
+// Tags are used to identify tables.
+function getTag(dataView, offset) {
+    var tag = '';
+    for (var i = offset; i < offset + 4; i += 1) {
+        tag += String.fromCharCode(dataView.getInt8(i));
+    }
+
+    return tag;
+}
+
+// Retrieve an offset from the DataView.
+// Offsets are 1 to 4 bytes in length, depending on the offSize argument.
+function getOffset(dataView, offset, offSize) {
+    var v = 0;
+    for (var i = 0; i < offSize; i += 1) {
+        v <<= 8;
+        v += dataView.getUint8(offset + i);
+    }
+
+    return v;
+}
+
+// Retrieve a number of bytes from start offset to the end offset from the DataView.
+function getBytes(dataView, startOffset, endOffset) {
+    var bytes = [];
+    for (var i = startOffset; i < endOffset; i += 1) {
+        bytes.push(dataView.getUint8(i));
+    }
+
+    return bytes;
+}
+
+// Convert the list of bytes to a string.
+function bytesToString(bytes) {
+    var s = '';
+    for (var i = 0; i < bytes.length; i += 1) {
+        s += String.fromCharCode(bytes[i]);
+    }
+
+    return s;
+}
+
+var typeOffsets = {
+    byte: 1,
+    uShort: 2,
+    short: 2,
+    uLong: 4,
+    fixed: 4,
+    longDateTime: 8,
+    tag: 4
+};
+
+// A stateful parser that changes the offset whenever a value is retrieved.
+// The data is a DataView.
+function Parser(data, offset) {
+    this.data = data;
+    this.offset = offset;
+    this.relativeOffset = 0;
+}
+
+Parser.prototype.parseByte = function() {
+    var v = this.data.getUint8(this.offset + this.relativeOffset);
+    this.relativeOffset += 1;
+    return v;
+};
+
+Parser.prototype.parseChar = function() {
+    var v = this.data.getInt8(this.offset + this.relativeOffset);
+    this.relativeOffset += 1;
+    return v;
+};
+
+Parser.prototype.parseCard8 = Parser.prototype.parseByte;
+
+Parser.prototype.parseUShort = function() {
+    var v = this.data.getUint16(this.offset + this.relativeOffset);
+    this.relativeOffset += 2;
+    return v;
+};
+
+Parser.prototype.parseCard16 = Parser.prototype.parseUShort;
+Parser.prototype.parseSID = Parser.prototype.parseUShort;
+Parser.prototype.parseOffset16 = Parser.prototype.parseUShort;
+
+Parser.prototype.parseShort = function() {
+    var v = this.data.getInt16(this.offset + this.relativeOffset);
+    this.relativeOffset += 2;
+    return v;
+};
+
+Parser.prototype.parseF2Dot14 = function() {
+    var v = this.data.getInt16(this.offset + this.relativeOffset) / 16384;
+    this.relativeOffset += 2;
+    return v;
+};
+
+Parser.prototype.parseULong = function() {
+    var v = getULong(this.data, this.offset + this.relativeOffset);
+    this.relativeOffset += 4;
+    return v;
+};
+
+Parser.prototype.parseOffset32 = Parser.prototype.parseULong;
+
+Parser.prototype.parseFixed = function() {
+    var v = getFixed(this.data, this.offset + this.relativeOffset);
+    this.relativeOffset += 4;
+    return v;
+};
+
+Parser.prototype.parseString = function(length) {
+    var dataView = this.data;
+    var offset = this.offset + this.relativeOffset;
+    var string = '';
+    this.relativeOffset += length;
+    for (var i = 0; i < length; i++) {
+        string += String.fromCharCode(dataView.getUint8(offset + i));
+    }
+
+    return string;
+};
+
+Parser.prototype.parseTag = function() {
+    return this.parseString(4);
+};
+
+// LONGDATETIME is a 64-bit integer.
+// JavaScript and unix timestamps traditionally use 32 bits, so we
+// only take the last 32 bits.
+// + Since until 2038 those bits will be filled by zeros we can ignore them.
+Parser.prototype.parseLongDateTime = function() {
+    var v = getULong(this.data, this.offset + this.relativeOffset + 4);
+    // Subtract seconds between 01/01/1904 and 01/01/1970
+    // to convert Apple Mac timestamp to Standard Unix timestamp
+    v -= 2082844800;
+    this.relativeOffset += 8;
+    return v;
+};
+
+Parser.prototype.parseVersion = function(minorBase) {
+    var major = getUShort(this.data, this.offset + this.relativeOffset);
+
+    // How to interpret the minor version is very vague in the spec. 0x5000 is 5, 0x1000 is 1
+    // Default returns the correct number if minor = 0xN000 where N is 0-9
+    // Set minorBase to 1 for tables that use minor = N where N is 0-9
+    var minor = getUShort(this.data, this.offset + this.relativeOffset + 2);
+    this.relativeOffset += 4;
+    if (minorBase === undefined) { minorBase = 0x1000; }
+    return major + minor / minorBase / 10;
+};
+
+Parser.prototype.skip = function(type, amount) {
+    if (amount === undefined) {
+        amount = 1;
+    }
+
+    this.relativeOffset += typeOffsets[type] * amount;
+};
+
+///// Parsing lists and records ///////////////////////////////
+
+// Parse a list of 32 bit unsigned integers.
+Parser.prototype.parseULongList = function(count) {
+    if (count === undefined) { count = this.parseULong(); }
+    var offsets = new Array(count);
+    var dataView = this.data;
+    var offset = this.offset + this.relativeOffset;
+    for (var i = 0; i < count; i++) {
+        offsets[i] = dataView.getUint32(offset);
+        offset += 4;
+    }
+
+    this.relativeOffset += count * 4;
+    return offsets;
+};
+
+// Parse a list of 16 bit unsigned integers. The length of the list can be read on the stream
+// or provided as an argument.
+Parser.prototype.parseOffset16List =
+Parser.prototype.parseUShortList = function(count) {
+    if (count === undefined) { count = this.parseUShort(); }
+    var offsets = new Array(count);
+    var dataView = this.data;
+    var offset = this.offset + this.relativeOffset;
+    for (var i = 0; i < count; i++) {
+        offsets[i] = dataView.getUint16(offset);
+        offset += 2;
+    }
+
+    this.relativeOffset += count * 2;
+    return offsets;
+};
+
+// Parses a list of 16 bit signed integers.
+Parser.prototype.parseShortList = function(count) {
+    var list = new Array(count);
+    var dataView = this.data;
+    var offset = this.offset + this.relativeOffset;
+    for (var i = 0; i < count; i++) {
+        list[i] = dataView.getInt16(offset);
+        offset += 2;
+    }
+
+    this.relativeOffset += count * 2;
+    return list;
+};
+
+// Parses a list of bytes.
+Parser.prototype.parseByteList = function(count) {
+    var list = new Array(count);
+    var dataView = this.data;
+    var offset = this.offset + this.relativeOffset;
+    for (var i = 0; i < count; i++) {
+        list[i] = dataView.getUint8(offset++);
+    }
+
+    this.relativeOffset += count;
+    return list;
+};
+
+/**
+ * Parse a list of items.
+ * Record count is optional, if omitted it is read from the stream.
+ * itemCallback is one of the Parser methods.
+ */
+Parser.prototype.parseList = function(count, itemCallback) {
+    if (!itemCallback) {
+        itemCallback = count;
+        count = this.parseUShort();
+    }
+    var list = new Array(count);
+    for (var i = 0; i < count; i++) {
+        list[i] = itemCallback.call(this);
+    }
+    return list;
+};
+
+Parser.prototype.parseList32 = function(count, itemCallback) {
+    if (!itemCallback) {
+        itemCallback = count;
+        count = this.parseULong();
+    }
+    var list = new Array(count);
+    for (var i = 0; i < count; i++) {
+        list[i] = itemCallback.call(this);
+    }
+    return list;
+};
+
+/**
+ * Parse a list of records.
+ * Record count is optional, if omitted it is read from the stream.
+ * Example of recordDescription: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort }
+ */
+Parser.prototype.parseRecordList = function(count, recordDescription) {
+    // If the count argument is absent, read it in the stream.
+    if (!recordDescription) {
+        recordDescription = count;
+        count = this.parseUShort();
+    }
+    var records = new Array(count);
+    var fields = Object.keys(recordDescription);
+    for (var i = 0; i < count; i++) {
+        var rec = {};
+        for (var j = 0; j < fields.length; j++) {
+            var fieldName = fields[j];
+            var fieldType = recordDescription[fieldName];
+            rec[fieldName] = fieldType.call(this);
+        }
+        records[i] = rec;
+    }
+    return records;
+};
+
+Parser.prototype.parseRecordList32 = function(count, recordDescription) {
+    // If the count argument is absent, read it in the stream.
+    if (!recordDescription) {
+        recordDescription = count;
+        count = this.parseULong();
+    }
+    var records = new Array(count);
+    var fields = Object.keys(recordDescription);
+    for (var i = 0; i < count; i++) {
+        var rec = {};
+        for (var j = 0; j < fields.length; j++) {
+            var fieldName = fields[j];
+            var fieldType = recordDescription[fieldName];
+            rec[fieldName] = fieldType.call(this);
+        }
+        records[i] = rec;
+    }
+    return records;
+};
+
+// Parse a data structure into an object
+// Example of description: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort }
+Parser.prototype.parseStruct = function(description) {
+    if (typeof description === 'function') {
+        return description.call(this);
+    } else {
+        var fields = Object.keys(description);
+        var struct = {};
+        for (var j = 0; j < fields.length; j++) {
+            var fieldName = fields[j];
+            var fieldType = description[fieldName];
+            struct[fieldName] = fieldType.call(this);
+        }
+        return struct;
+    }
+};
+
+/**
+ * Parse a GPOS valueRecord
+ * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
+ * valueFormat is optional, if omitted it is read from the stream.
+ */
+Parser.prototype.parseValueRecord = function(valueFormat) {
+    if (valueFormat === undefined) {
+        valueFormat = this.parseUShort();
+    }
+    if (valueFormat === 0) {
+        // valueFormat2 in kerning pairs is most often 0
+        // in this case return undefined instead of an empty object, to save space
+        return;
+    }
+    var valueRecord = {};
+
+    if (valueFormat & 0x0001) { valueRecord.xPlacement = this.parseShort(); }
+    if (valueFormat & 0x0002) { valueRecord.yPlacement = this.parseShort(); }
+    if (valueFormat & 0x0004) { valueRecord.xAdvance = this.parseShort(); }
+    if (valueFormat & 0x0008) { valueRecord.yAdvance = this.parseShort(); }
+
+    // Device table (non-variable font) / VariationIndex table (variable font) not supported
+    // https://docs.microsoft.com/fr-fr/typography/opentype/spec/chapter2#devVarIdxTbls
+    if (valueFormat & 0x0010) { valueRecord.xPlaDevice = undefined; this.parseShort(); }
+    if (valueFormat & 0x0020) { valueRecord.yPlaDevice = undefined; this.parseShort(); }
+    if (valueFormat & 0x0040) { valueRecord.xAdvDevice = undefined; this.parseShort(); }
+    if (valueFormat & 0x0080) { valueRecord.yAdvDevice = undefined; this.parseShort(); }
+
+    return valueRecord;
+};
+
+/**
+ * Parse a list of GPOS valueRecords
+ * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
+ * valueFormat and valueCount are read from the stream.
+ */
+Parser.prototype.parseValueRecordList = function() {
+    var valueFormat = this.parseUShort();
+    var valueCount = this.parseUShort();
+    var values = new Array(valueCount);
+    for (var i = 0; i < valueCount; i++) {
+        values[i] = this.parseValueRecord(valueFormat);
+    }
+    return values;
+};
+
+Parser.prototype.parsePointer = function(description) {
+    var structOffset = this.parseOffset16();
+    if (structOffset > 0) {
+        // NULL offset => return undefined
+        return new Parser(this.data, this.offset + structOffset).parseStruct(description);
+    }
+    return undefined;
+};
+
+Parser.prototype.parsePointer32 = function(description) {
+    var structOffset = this.parseOffset32();
+    if (structOffset > 0) {
+        // NULL offset => return undefined
+        return new Parser(this.data, this.offset + structOffset).parseStruct(description);
+    }
+    return undefined;
+};
+
+/**
+ * Parse a list of offsets to lists of 16-bit integers,
+ * or a list of offsets to lists of offsets to any kind of items.
+ * If itemCallback is not provided, a list of list of UShort is assumed.
+ * If provided, itemCallback is called on each item and must parse the item.
+ * See examples in tables/gsub.js
+ */
+Parser.prototype.parseListOfLists = function(itemCallback) {
+    var offsets = this.parseOffset16List();
+    var count = offsets.length;
+    var relativeOffset = this.relativeOffset;
+    var list = new Array(count);
+    for (var i = 0; i < count; i++) {
+        var start = offsets[i];
+        if (start === 0) {
+            // NULL offset
+            // Add i as owned property to list. Convenient with assert.
+            list[i] = undefined;
+            continue;
+        }
+        this.relativeOffset = start;
+        if (itemCallback) {
+            var subOffsets = this.parseOffset16List();
+            var subList = new Array(subOffsets.length);
+            for (var j = 0; j < subOffsets.length; j++) {
+                this.relativeOffset = start + subOffsets[j];
+                subList[j] = itemCallback.call(this);
+            }
+            list[i] = subList;
+        } else {
+            list[i] = this.parseUShortList();
+        }
+    }
+    this.relativeOffset = relativeOffset;
+    return list;
+};
+
+///// Complex tables parsing //////////////////////////////////
+
+// Parse a coverage table in a GSUB, GPOS or GDEF table.
+// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
+// parser.offset must point to the start of the table containing the coverage.
+Parser.prototype.parseCoverage = function() {
+    var startOffset = this.offset + this.relativeOffset;
+    var format = this.parseUShort();
+    var count = this.parseUShort();
+    if (format === 1) {
+        return {
+            format: 1,
+            glyphs: this.parseUShortList(count)
+        };
+    } else if (format === 2) {
+        var ranges = new Array(count);
+        for (var i = 0; i < count; i++) {
+            ranges[i] = {
+                start: this.parseUShort(),
+                end: this.parseUShort(),
+                index: this.parseUShort()
+            };
+        }
+        return {
+            format: 2,
+            ranges: ranges
+        };
+    }
+    throw new Error('0x' + startOffset.toString(16) + ': Coverage format must be 1 or 2.');
+};
+
+// Parse a Class Definition Table in a GSUB, GPOS or GDEF table.
+// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
+Parser.prototype.parseClassDef = function() {
+    var startOffset = this.offset + this.relativeOffset;
+    var format = this.parseUShort();
+    if (format === 1) {
+        return {
+            format: 1,
+            startGlyph: this.parseUShort(),
+            classes: this.parseUShortList()
+        };
+    } else if (format === 2) {
+        return {
+            format: 2,
+            ranges: this.parseRecordList({
+                start: Parser.uShort,
+                end: Parser.uShort,
+                classId: Parser.uShort
+            })
+        };
+    }
+    throw new Error('0x' + startOffset.toString(16) + ': ClassDef format must be 1 or 2.');
+};
+
+///// Static methods ///////////////////////////////////
+// These convenience methods can be used as callbacks and should be called with "this" context set to a Parser instance.
+
+Parser.list = function(count, itemCallback) {
+    return function() {
+        return this.parseList(count, itemCallback);
+    };
+};
+
+Parser.list32 = function(count, itemCallback) {
+    return function() {
+        return this.parseList32(count, itemCallback);
+    };
+};
+
+Parser.recordList = function(count, recordDescription) {
+    return function() {
+        return this.parseRecordList(count, recordDescription);
+    };
+};
+
+Parser.recordList32 = function(count, recordDescription) {
+    return function() {
+        return this.parseRecordList32(count, recordDescription);
+    };
+};
+
+Parser.pointer = function(description) {
+    return function() {
+        return this.parsePointer(description);
+    };
+};
+
+Parser.pointer32 = function(description) {
+    return function() {
+        return this.parsePointer32(description);
+    };
+};
+
+Parser.tag = Parser.prototype.parseTag;
+Parser.byte = Parser.prototype.parseByte;
+Parser.uShort = Parser.offset16 = Parser.prototype.parseUShort;
+Parser.uShortList = Parser.prototype.parseUShortList;
+Parser.uLong = Parser.offset32 = Parser.prototype.parseULong;
+Parser.uLongList = Parser.prototype.parseULongList;
+Parser.struct = Parser.prototype.parseStruct;
+Parser.coverage = Parser.prototype.parseCoverage;
+Parser.classDef = Parser.prototype.parseClassDef;
+
+///// Script, Feature, Lookup lists ///////////////////////////////////////////////
+// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
+
+var langSysTable = {
+    reserved: Parser.uShort,
+    reqFeatureIndex: Parser.uShort,
+    featureIndexes: Parser.uShortList
+};
+
+Parser.prototype.parseScriptList = function() {
+    return this.parsePointer(Parser.recordList({
+        tag: Parser.tag,
+        script: Parser.pointer({
+            defaultLangSys: Parser.pointer(langSysTable),
+            langSysRecords: Parser.recordList({
+                tag: Parser.tag,
+                langSys: Parser.pointer(langSysTable)
+            })
+        })
+    })) || [];
+};
+
+Parser.prototype.parseFeatureList = function() {
+    return this.parsePointer(Parser.recordList({
+        tag: Parser.tag,
+        feature: Parser.pointer({
+            featureParams: Parser.offset16,
+            lookupListIndexes: Parser.uShortList
+        })
+    })) || [];
+};
+
+Parser.prototype.parseLookupList = function(lookupTableParsers) {
+    return this.parsePointer(Parser.list(Parser.pointer(function() {
+        var lookupType = this.parseUShort();
+        check.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.');
+        var lookupFlag = this.parseUShort();
+        var useMarkFilteringSet = lookupFlag & 0x10;
+        return {
+            lookupType: lookupType,
+            lookupFlag: lookupFlag,
+            subtables: this.parseList(Parser.pointer(lookupTableParsers[lookupType])),
+            markFilteringSet: useMarkFilteringSet ? this.parseUShort() : undefined
+        };
+    }))) || [];
+};
+
+Parser.prototype.parseFeatureVariationsList = function() {
+    return this.parsePointer32(function() {
+        var majorVersion = this.parseUShort();
+        var minorVersion = this.parseUShort();
+        check.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.');
+        var featureVariations = this.parseRecordList32({
+            conditionSetOffset: Parser.offset32,
+            featureTableSubstitutionOffset: Parser.offset32
+        });
+        return featureVariations;
+    }) || [];
+};
+
+var parse = {
+    getByte: getByte,
+    getCard8: getByte,
+    getUShort: getUShort,
+    getCard16: getUShort,
+    getShort: getShort,
+    getULong: getULong,
+    getFixed: getFixed,
+    getTag: getTag,
+    getOffset: getOffset,
+    getBytes: getBytes,
+    bytesToString: bytesToString,
+    Parser: Parser,
+};
+
+// The `cmap` table stores the mappings from characters to glyphs.
+
+function parseCmapTableFormat12(cmap, p) {
+    //Skip reserved.
+    p.parseUShort();
+
+    // Length in bytes of the sub-tables.
+    cmap.length = p.parseULong();
+    cmap.language = p.parseULong();
+
+    var groupCount;
+    cmap.groupCount = groupCount = p.parseULong();
+    cmap.glyphIndexMap = {};
+
+    for (var i = 0; i < groupCount; i += 1) {
+        var startCharCode = p.parseULong();
+        var endCharCode = p.parseULong();
+        var startGlyphId = p.parseULong();
+
+        for (var c = startCharCode; c <= endCharCode; c += 1) {
+            cmap.glyphIndexMap[c] = startGlyphId;
+            startGlyphId++;
+        }
+    }
+}
+
+function parseCmapTableFormat4(cmap, p, data, start, offset) {
+    // Length in bytes of the sub-tables.
+    cmap.length = p.parseUShort();
+    cmap.language = p.parseUShort();
+
+    // segCount is stored x 2.
+    var segCount;
+    cmap.segCount = segCount = p.parseUShort() >> 1;
+
+    // Skip searchRange, entrySelector, rangeShift.
+    p.skip('uShort', 3);
+
+    // The "unrolled" mapping from character codes to glyph indices.
+    cmap.glyphIndexMap = {};
+    var endCountParser = new parse.Parser(data, start + offset + 14);
+    var startCountParser = new parse.Parser(data, start + offset + 16 + segCount * 2);
+    var idDeltaParser = new parse.Parser(data, start + offset + 16 + segCount * 4);
+    var idRangeOffsetParser = new parse.Parser(data, start + offset + 16 + segCount * 6);
+    var glyphIndexOffset = start + offset + 16 + segCount * 8;
+    for (var i = 0; i < segCount - 1; i += 1) {
+        var glyphIndex = (void 0);
+        var endCount = endCountParser.parseUShort();
+        var startCount = startCountParser.parseUShort();
+        var idDelta = idDeltaParser.parseShort();
+        var idRangeOffset = idRangeOffsetParser.parseUShort();
+        for (var c = startCount; c <= endCount; c += 1) {
+            if (idRangeOffset !== 0) {
+                // The idRangeOffset is relative to the current position in the idRangeOffset array.
+                // Take the current offset in the idRangeOffset array.
+                glyphIndexOffset = (idRangeOffsetParser.offset + idRangeOffsetParser.relativeOffset - 2);
+
+                // Add the value of the idRangeOffset, which will move us into the glyphIndex array.
+                glyphIndexOffset += idRangeOffset;
+
+                // Then add the character index of the current segment, multiplied by 2 for USHORTs.
+                glyphIndexOffset += (c - startCount) * 2;
+                glyphIndex = parse.getUShort(data, glyphIndexOffset);
+                if (glyphIndex !== 0) {
+                    glyphIndex = (glyphIndex + idDelta) & 0xFFFF;
+                }
+            } else {
+                glyphIndex = (c + idDelta) & 0xFFFF;
+            }
+
+            cmap.glyphIndexMap[c] = glyphIndex;
+        }
+    }
+}
+
+// Parse the `cmap` table. This table stores the mappings from characters to glyphs.
+// There are many available formats, but we only support the Windows format 4 and 12.
+// This function returns a `CmapEncoding` object or null if no supported format could be found.
+function parseCmapTable(data, start) {
+    var cmap = {};
+    cmap.version = parse.getUShort(data, start);
+    check.argument(cmap.version === 0, 'cmap table version should be 0.');
+
+    // The cmap table can contain many sub-tables, each with their own format.
+    // We're only interested in a "platform 0" (Unicode format) and "platform 3" (Windows format) table.
+    cmap.numTables = parse.getUShort(data, start + 2);
+    var offset = -1;
+    for (var i = cmap.numTables - 1; i >= 0; i -= 1) {
+        var platformId = parse.getUShort(data, start + 4 + (i * 8));
+        var encodingId = parse.getUShort(data, start + 4 + (i * 8) + 2);
+        if ((platformId === 3 && (encodingId === 0 || encodingId === 1 || encodingId === 10)) ||
+            (platformId === 0 && (encodingId === 0 || encodingId === 1 || encodingId === 2 || encodingId === 3 || encodingId === 4))) {
+            offset = parse.getULong(data, start + 4 + (i * 8) + 4);
+            break;
+        }
+    }
+
+    if (offset === -1) {
+        // There is no cmap table in the font that we support.
+        throw new Error('No valid cmap sub-tables found.');
+    }
+
+    var p = new parse.Parser(data, start + offset);
+    cmap.format = p.parseUShort();
+
+    if (cmap.format === 12) {
+        parseCmapTableFormat12(cmap, p);
+    } else if (cmap.format === 4) {
+        parseCmapTableFormat4(cmap, p, data, start, offset);
+    } else {
+        throw new Error('Only format 4 and 12 cmap tables are supported (found format ' + cmap.format + ').');
+    }
+
+    return cmap;
+}
+
+function addSegment(t, code, glyphIndex) {
+    t.segments.push({
+        end: code,
+        start: code,
+        delta: -(code - glyphIndex),
+        offset: 0,
+        glyphIndex: glyphIndex
+    });
+}
+
+function addTerminatorSegment(t) {
+    t.segments.push({
+        end: 0xFFFF,
+        start: 0xFFFF,
+        delta: 1,
+        offset: 0
+    });
+}
+
+// Make cmap table, format 4 by default, 12 if needed only
+function makeCmapTable(glyphs) {
+    // Plan 0 is the base Unicode Plan but emojis, for example are on another plan, and needs cmap 12 format (with 32bit)
+    var isPlan0Only = true;
+    var i;
+
+    // Check if we need to add cmap format 12 or if format 4 only is fine
+    for (i = glyphs.length - 1; i > 0; i -= 1) {
+        var g = glyphs.get(i);
+        if (g.unicode > 65535) {
+            console.log('Adding CMAP format 12 (needed!)');
+            isPlan0Only = false;
+            break;
+        }
+    }
+
+    var cmapTable = [
+        {name: 'version', type: 'USHORT', value: 0},
+        {name: 'numTables', type: 'USHORT', value: isPlan0Only ? 1 : 2},
+
+        // CMAP 4 header
+        {name: 'platformID', type: 'USHORT', value: 3},
+        {name: 'encodingID', type: 'USHORT', value: 1},
+        {name: 'offset', type: 'ULONG', value: isPlan0Only ? 12 : (12 + 8)}
+    ];
+
+    if (!isPlan0Only)
+        { cmapTable = cmapTable.concat([
+            // CMAP 12 header
+            {name: 'cmap12PlatformID', type: 'USHORT', value: 3}, // We encode only for PlatformID = 3 (Windows) because it is supported everywhere
+            {name: 'cmap12EncodingID', type: 'USHORT', value: 10},
+            {name: 'cmap12Offset', type: 'ULONG', value: 0}
+        ]); }
+
+    cmapTable = cmapTable.concat([
+        // CMAP 4 Subtable
+        {name: 'format', type: 'USHORT', value: 4},
+        {name: 'cmap4Length', type: 'USHORT', value: 0},
+        {name: 'language', type: 'USHORT', value: 0},
+        {name: 'segCountX2', type: 'USHORT', value: 0},
+        {name: 'searchRange', type: 'USHORT', value: 0},
+        {name: 'entrySelector', type: 'USHORT', value: 0},
+        {name: 'rangeShift', type: 'USHORT', value: 0}
+    ]);
+
+    var t = new table.Table('cmap', cmapTable);
+
+    t.segments = [];
+    for (i = 0; i < glyphs.length; i += 1) {
+        var glyph = glyphs.get(i);
+        for (var j = 0; j < glyph.unicodes.length; j += 1) {
+            addSegment(t, glyph.unicodes[j], i);
+        }
+
+        t.segments = t.segments.sort(function (a, b) {
+            return a.start - b.start;
+        });
+    }
+
+    addTerminatorSegment(t);
+
+    var segCount = t.segments.length;
+    var segCountToRemove = 0;
+
+    // CMAP 4
+    // Set up parallel segment arrays.
+    var endCounts = [];
+    var startCounts = [];
+    var idDeltas = [];
+    var idRangeOffsets = [];
+    var glyphIds = [];
+
+    // CMAP 12
+    var cmap12Groups = [];
+
+    // Reminder this loop is not following the specification at 100%
+    // The specification -> find suites of characters and make a group
+    // Here we're doing one group for each letter
+    // Doing as the spec can save 8 times (or more) space
+    for (i = 0; i < segCount; i += 1) {
+        var segment = t.segments[i];
+
+        // CMAP 4
+        if (segment.end <= 65535 && segment.start <= 65535) {
+            endCounts = endCounts.concat({name: 'end_' + i, type: 'USHORT', value: segment.end});
+            startCounts = startCounts.concat({name: 'start_' + i, type: 'USHORT', value: segment.start});
+            idDeltas = idDeltas.concat({name: 'idDelta_' + i, type: 'SHORT', value: segment.delta});
+            idRangeOffsets = idRangeOffsets.concat({name: 'idRangeOffset_' + i, type: 'USHORT', value: segment.offset});
+            if (segment.glyphId !== undefined) {
+                glyphIds = glyphIds.concat({name: 'glyph_' + i, type: 'USHORT', value: segment.glyphId});
+            }
+        } else {
+            // Skip Unicode > 65535 (16bit unsigned max) for CMAP 4, will be added in CMAP 12
+            segCountToRemove += 1;
+        }
+
+        // CMAP 12
+        // Skip Terminator Segment
+        if (!isPlan0Only && segment.glyphIndex !== undefined) {
+            cmap12Groups = cmap12Groups.concat({name: 'cmap12Start_' + i, type: 'ULONG', value: segment.start});
+            cmap12Groups = cmap12Groups.concat({name: 'cmap12End_' + i, type: 'ULONG', value: segment.end});
+            cmap12Groups = cmap12Groups.concat({name: 'cmap12Glyph_' + i, type: 'ULONG', value: segment.glyphIndex});
+        }
+    }
+
+    // CMAP 4 Subtable
+    t.segCountX2 = (segCount - segCountToRemove) * 2;
+    t.searchRange = Math.pow(2, Math.floor(Math.log((segCount - segCountToRemove)) / Math.log(2))) * 2;
+    t.entrySelector = Math.log(t.searchRange / 2) / Math.log(2);
+    t.rangeShift = t.segCountX2 - t.searchRange;
+
+    t.fields = t.fields.concat(endCounts);
+    t.fields.push({name: 'reservedPad', type: 'USHORT', value: 0});
+    t.fields = t.fields.concat(startCounts);
+    t.fields = t.fields.concat(idDeltas);
+    t.fields = t.fields.concat(idRangeOffsets);
+    t.fields = t.fields.concat(glyphIds);
+
+    t.cmap4Length = 14 + // Subtable header
+        endCounts.length * 2 +
+        2 + // reservedPad
+        startCounts.length * 2 +
+        idDeltas.length * 2 +
+        idRangeOffsets.length * 2 +
+        glyphIds.length * 2;
+
+    if (!isPlan0Only) {
+        // CMAP 12 Subtable
+        var cmap12Length = 16 + // Subtable header
+            cmap12Groups.length * 4;
+
+        t.cmap12Offset = 12 + (2 * 2) + 4 + t.cmap4Length;
+        t.fields = t.fields.concat([
+            {name: 'cmap12Format', type: 'USHORT', value: 12},
+            {name: 'cmap12Reserved', type: 'USHORT', value: 0},
+            {name: 'cmap12Length', type: 'ULONG', value: cmap12Length},
+            {name: 'cmap12Language', type: 'ULONG', value: 0},
+            {name: 'cmap12nGroups', type: 'ULONG', value: cmap12Groups.length / 3}
+        ]);
+
+        t.fields = t.fields.concat(cmap12Groups);
+    }
+
+    return t;
+}
+
+var cmap = { parse: parseCmapTable, make: makeCmapTable };
+
+// Glyph encoding
+
+var cffStandardStrings = [
+    '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright',
+    'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two',
+    'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater',
+    'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+    'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore',
+    'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+    'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', 'sterling',
+    'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft',
+    'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', 'periodcentered', 'paragraph',
+    'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand',
+    'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', 'ring',
+    'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE',
+    'ordmasculine', 'ae', 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu',
+    'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn',
+    'threequarters', 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright',
+    'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex',
+    'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex',
+    'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute',
+    'Ydieresis', 'Zcaron', 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', 'eacute',
+    'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute',
+    'ocircumflex', 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave',
+    'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior',
+    'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', '266 ff', 'onedotenleader',
+    'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle',
+    'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior',
+    'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior',
+    'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl',
+    'parenleftinferior', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall',
+    'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall',
+    'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall',
+    'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall',
+    'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall',
+    'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall',
+    'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds',
+    'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior',
+    'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior',
+    'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior',
+    'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall',
+    'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall',
+    'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall',
+    'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall',
+    'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000',
+    '001.001', '001.002', '001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold'];
+
+var cffStandardEncoding = [
+    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
+    '', '', '', '', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright',
+    'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two',
+    'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater',
+    'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+    'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore',
+    'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+    'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', '', '', '', '', '', '', '', '',
+    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
+    'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle',
+    'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', '', 'endash', 'dagger',
+    'daggerdbl', 'periodcentered', '', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright',
+    'guillemotright', 'ellipsis', 'perthousand', '', 'questiondown', '', 'grave', 'acute', 'circumflex', 'tilde',
+    'macron', 'breve', 'dotaccent', 'dieresis', '', 'ring', 'cedilla', '', 'hungarumlaut', 'ogonek', 'caron',
+    'emdash', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'AE', '', 'ordfeminine', '', '', '',
+    '', 'Lslash', 'Oslash', 'OE', 'ordmasculine', '', '', '', '', '', 'ae', '', '', '', 'dotlessi', '', '',
+    'lslash', 'oslash', 'oe', 'germandbls'];
+
+var cffExpertEncoding = [
+    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
+    '', '', '', '', 'space', 'exclamsmall', 'Hungarumlautsmall', '', 'dollaroldstyle', 'dollarsuperior',
+    'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader',
+    'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle',
+    'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon',
+    'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'questionsmall', '', 'asuperior',
+    'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', '', '', 'isuperior', '', '', 'lsuperior', 'msuperior',
+    'nsuperior', 'osuperior', '', '', 'rsuperior', 'ssuperior', 'tsuperior', '', 'ff', 'fi', 'fl', 'ffi', 'ffl',
+    'parenleftinferior', '', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall',
+    'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall',
+    'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall',
+    'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', '', '', '', '', '', '', '',
+    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
+    'exclamdownsmall', 'centoldstyle', 'Lslashsmall', '', '', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall',
+    'Brevesmall', 'Caronsmall', '', 'Dotaccentsmall', '', '', 'Macronsmall', '', '', 'figuredash', 'hypheninferior',
+    '', '', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', '', '', '', 'onequarter', 'onehalf', 'threequarters',
+    'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', '',
+    '', 'zerosuperior', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior',
+    'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior',
+    'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior',
+    'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall',
+    'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall',
+    'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall',
+    'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall',
+    'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall',
+    'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall'];
+
+var standardNames = [
+    '.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent',
+    'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash',
+    'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less',
+    'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright',
+    'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde',
+    'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave',
+    'acircumflex', 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis',
+    'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis',
+    'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section',
+    'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', 'dieresis', 'notequal',
+    'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation',
+    'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', 'questiondown',
+    'exclamdown', 'logicalnot', 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', 'guillemotright',
+    'ellipsis', 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft',
+    'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction',
+    'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase',
+    'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute',
+    'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex',
+    'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut',
+    'ogonek', 'caron', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar', 'Eth', 'eth',
+    'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply', 'onesuperior', 'twosuperior', 'threesuperior',
+    'onehalf', 'onequarter', 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla', 'scedilla',
+    'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat'];
+
+/**
+ * This is the encoding used for fonts created from scratch.
+ * It loops through all glyphs and finds the appropriate unicode value.
+ * Since it's linear time, other encodings will be faster.
+ * @exports opentype.DefaultEncoding
+ * @class
+ * @constructor
+ * @param {opentype.Font}
+ */
+function DefaultEncoding(font) {
+    this.font = font;
+}
+
+DefaultEncoding.prototype.charToGlyphIndex = function(c) {
+    var code = c.codePointAt(0);
+    var glyphs = this.font.glyphs;
+    if (glyphs) {
+        for (var i = 0; i < glyphs.length; i += 1) {
+            var glyph = glyphs.get(i);
+            for (var j = 0; j < glyph.unicodes.length; j += 1) {
+                if (glyph.unicodes[j] === code) {
+                    return i;
+                }
+            }
+        }
+    }
+    return null;
+};
+
+/**
+ * @exports opentype.CmapEncoding
+ * @class
+ * @constructor
+ * @param {Object} cmap - a object with the cmap encoded data
+ */
+function CmapEncoding(cmap) {
+    this.cmap = cmap;
+}
+
+/**
+ * @param  {string} c - the character
+ * @return {number} The glyph index.
+ */
+CmapEncoding.prototype.charToGlyphIndex = function(c) {
+    return this.cmap.glyphIndexMap[c.codePointAt(0)] || 0;
+};
+
+/**
+ * @exports opentype.CffEncoding
+ * @class
+ * @constructor
+ * @param {string} encoding - The encoding
+ * @param {Array} charset - The character set.
+ */
+function CffEncoding(encoding, charset) {
+    this.encoding = encoding;
+    this.charset = charset;
+}
+
+/**
+ * @param  {string} s - The character
+ * @return {number} The index.
+ */
+CffEncoding.prototype.charToGlyphIndex = function(s) {
+    var code = s.codePointAt(0);
+    var charName = this.encoding[code];
+    return this.charset.indexOf(charName);
+};
+
+/**
+ * @exports opentype.GlyphNames
+ * @class
+ * @constructor
+ * @param {Object} post
+ */
+function GlyphNames(post) {
+    switch (post.version) {
+        case 1:
+            this.names = standardNames.slice();
+            break;
+        case 2:
+            this.names = new Array(post.numberOfGlyphs);
+            for (var i = 0; i < post.numberOfGlyphs; i++) {
+                if (post.glyphNameIndex[i] < standardNames.length) {
+                    this.names[i] = standardNames[post.glyphNameIndex[i]];
+                } else {
+                    this.names[i] = post.names[post.glyphNameIndex[i] - standardNames.length];
+                }
+            }
+
+            break;
+        case 2.5:
+            this.names = new Array(post.numberOfGlyphs);
+            for (var i$1 = 0; i$1 < post.numberOfGlyphs; i$1++) {
+                this.names[i$1] = standardNames[i$1 + post.glyphNameIndex[i$1]];
+            }
+
+            break;
+        case 3:
+            this.names = [];
+            break;
+        default:
+            this.names = [];
+            break;
+    }
+}
+
+/**
+ * Gets the index of a glyph by name.
+ * @param  {string} name - The glyph name
+ * @return {number} The index
+ */
+GlyphNames.prototype.nameToGlyphIndex = function(name) {
+    return this.names.indexOf(name);
+};
+
+/**
+ * @param  {number} gid
+ * @return {string}
+ */
+GlyphNames.prototype.glyphIndexToName = function(gid) {
+    return this.names[gid];
+};
+
+function addGlyphNamesAll(font) {
+    var glyph;
+    var glyphIndexMap = font.tables.cmap.glyphIndexMap;
+    var charCodes = Object.keys(glyphIndexMap);
+
+    for (var i = 0; i < charCodes.length; i += 1) {
+        var c = charCodes[i];
+        var glyphIndex = glyphIndexMap[c];
+        glyph = font.glyphs.get(glyphIndex);
+        glyph.addUnicode(parseInt(c));
+    }
+
+    for (var i$1 = 0; i$1 < font.glyphs.length; i$1 += 1) {
+        glyph = font.glyphs.get(i$1);
+        if (font.cffEncoding) {
+            if (font.isCIDFont) {
+                glyph.name = 'gid' + i$1;
+            } else {
+                glyph.name = font.cffEncoding.charset[i$1];
+            }
+        } else if (font.glyphNames.names) {
+            glyph.name = font.glyphNames.glyphIndexToName(i$1);
+        }
+    }
+}
+
+function addGlyphNamesToUnicodeMap(font) {
+    font._IndexToUnicodeMap = {};
+
+    var glyphIndexMap = font.tables.cmap.glyphIndexMap;
+    var charCodes = Object.keys(glyphIndexMap);
+
+    for (var i = 0; i < charCodes.length; i += 1) {
+        var c = charCodes[i];
+        var glyphIndex = glyphIndexMap[c];
+        if (font._IndexToUnicodeMap[glyphIndex] === undefined) {
+            font._IndexToUnicodeMap[glyphIndex] = {
+                unicodes: [parseInt(c)]
+            };
+        } else {
+            font._IndexToUnicodeMap[glyphIndex].unicodes.push(parseInt(c));
+        }
+    }
+}
+
+/**
+ * @alias opentype.addGlyphNames
+ * @param {opentype.Font}
+ * @param {Object}
+ */
+function addGlyphNames(font, opt) {
+    if (opt.lowMemory) {
+        addGlyphNamesToUnicodeMap(font);
+    } else {
+        addGlyphNamesAll(font);
+    }
+}
+
+// Drawing utility functions.
+
+// Draw a line on the given context from point `x1,y1` to point `x2,y2`.
+function line(ctx, x1, y1, x2, y2) {
+    ctx.beginPath();
+    ctx.moveTo(x1, y1);
+    ctx.lineTo(x2, y2);
+    ctx.stroke();
+}
+
+var draw = { line: line };
+
+// The Glyph object
+// import glyf from './tables/glyf' Can't be imported here, because it's a circular dependency
+
+function getPathDefinition(glyph, path) {
+    var _path = path || new Path();
+    return {
+        configurable: true,
+
+        get: function() {
+            if (typeof _path === 'function') {
+                _path = _path();
+            }
+
+            return _path;
+        },
+
+        set: function(p) {
+            _path = p;
+        }
+    };
+}
+/**
+ * @typedef GlyphOptions
+ * @type Object
+ * @property {string} [name] - The glyph name
+ * @property {number} [unicode]
+ * @property {Array} [unicodes]
+ * @property {number} [xMin]
+ * @property {number} [yMin]
+ * @property {number} [xMax]
+ * @property {number} [yMax]
+ * @property {number} [advanceWidth]
+ */
+
+// A Glyph is an individual mark that often corresponds to a character.
+// Some glyphs, such as ligatures, are a combination of many characters.
+// Glyphs are the basic building blocks of a font.
+//
+// The `Glyph` class contains utility methods for drawing the path and its points.
+/**
+ * @exports opentype.Glyph
+ * @class
+ * @param {GlyphOptions}
+ * @constructor
+ */
+function Glyph(options) {
+    // By putting all the code on a prototype function (which is only declared once)
+    // we reduce the memory requirements for larger fonts by some 2%
+    this.bindConstructorValues(options);
+}
+
+/**
+ * @param  {GlyphOptions}
+ */
+Glyph.prototype.bindConstructorValues = function(options) {
+    this.index = options.index || 0;
+
+    // These three values cannot be deferred for memory optimization:
+    this.name = options.name || null;
+    this.unicode = options.unicode || undefined;
+    this.unicodes = options.unicodes || options.unicode !== undefined ? [options.unicode] : [];
+
+    // But by binding these values only when necessary, we reduce can
+    // the memory requirements by almost 3% for larger fonts.
+    if ('xMin' in options) {
+        this.xMin = options.xMin;
+    }
+
+    if ('yMin' in options) {
+        this.yMin = options.yMin;
+    }
+
+    if ('xMax' in options) {
+        this.xMax = options.xMax;
+    }
+
+    if ('yMax' in options) {
+        this.yMax = options.yMax;
+    }
+
+    if ('advanceWidth' in options) {
+        this.advanceWidth = options.advanceWidth;
+    }
+
+    // The path for a glyph is the most memory intensive, and is bound as a value
+    // with a getter/setter to ensure we actually do path parsing only once the
+    // path is actually needed by anything.
+    Object.defineProperty(this, 'path', getPathDefinition(this, options.path));
+};
+
+/**
+ * @param {number}
+ */
+Glyph.prototype.addUnicode = function(unicode) {
+    if (this.unicodes.length === 0) {
+        this.unicode = unicode;
+    }
+
+    this.unicodes.push(unicode);
+};
+
+/**
+ * Calculate the minimum bounding box for this glyph.
+ * @return {opentype.BoundingBox}
+ */
+Glyph.prototype.getBoundingBox = function() {
+    return this.path.getBoundingBox();
+};
+
+/**
+ * Convert the glyph to a Path we can draw on a drawing context.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {Object=} options - xScale, yScale to stretch the glyph.
+ * @param  {opentype.Font} if hinting is to be used, the font
+ * @return {opentype.Path}
+ */
+Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
+    x = x !== undefined ? x : 0;
+    y = y !== undefined ? y : 0;
+    fontSize = fontSize !== undefined ? fontSize : 72;
+    var commands;
+    var hPoints;
+    if (!options) { options = { }; }
+    var xScale = options.xScale;
+    var yScale = options.yScale;
+
+    if (options.hinting && font && font.hinting) {
+        // in case of hinting, the hinting engine takes care
+        // of scaling the points (not the path) before hinting.
+        hPoints = this.path && font.hinting.exec(this, fontSize);
+        // in case the hinting engine failed hPoints is undefined
+        // and thus reverts to plain rending
+    }
+
+    if (hPoints) {
+        // Call font.hinting.getCommands instead of `glyf.getPath(hPoints).commands` to avoid a circular dependency
+        commands = font.hinting.getCommands(hPoints);
+        x = Math.round(x);
+        y = Math.round(y);
+        // TODO in case of hinting xyScaling is not yet supported
+        xScale = yScale = 1;
+    } else {
+        commands = this.path.commands;
+        var scale = 1 / (this.path.unitsPerEm || 1000) * fontSize;
+        if (xScale === undefined) { xScale = scale; }
+        if (yScale === undefined) { yScale = scale; }
+    }
+
+    var p = new Path();
+    for (var i = 0; i < commands.length; i += 1) {
+        var cmd = commands[i];
+        if (cmd.type === 'M') {
+            p.moveTo(x + (cmd.x * xScale), y + (-cmd.y * yScale));
+        } else if (cmd.type === 'L') {
+            p.lineTo(x + (cmd.x * xScale), y + (-cmd.y * yScale));
+        } else if (cmd.type === 'Q') {
+            p.quadraticCurveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale),
+                               x + (cmd.x * xScale), y + (-cmd.y * yScale));
+        } else if (cmd.type === 'C') {
+            p.curveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale),
+                      x + (cmd.x2 * xScale), y + (-cmd.y2 * yScale),
+                      x + (cmd.x * xScale), y + (-cmd.y * yScale));
+        } else if (cmd.type === 'Z') {
+            p.closePath();
+        }
+    }
+
+    return p;
+};
+
+/**
+ * Split the glyph into contours.
+ * This function is here for backwards compatibility, and to
+ * provide raw access to the TrueType glyph outlines.
+ * @return {Array}
+ */
+Glyph.prototype.getContours = function() {
+    if (this.points === undefined) {
+        return [];
+    }
+
+    var contours = [];
+    var currentContour = [];
+    for (var i = 0; i < this.points.length; i += 1) {
+        var pt = this.points[i];
+        currentContour.push(pt);
+        if (pt.lastPointOfContour) {
+            contours.push(currentContour);
+            currentContour = [];
+        }
+    }
+
+    check.argument(currentContour.length === 0, 'There are still points left in the current contour.');
+    return contours;
+};
+
+/**
+ * Calculate the xMin/yMin/xMax/yMax/lsb/rsb for a Glyph.
+ * @return {Object}
+ */
+Glyph.prototype.getMetrics = function() {
+    var commands = this.path.commands;
+    var xCoords = [];
+    var yCoords = [];
+    for (var i = 0; i < commands.length; i += 1) {
+        var cmd = commands[i];
+        if (cmd.type !== 'Z') {
+            xCoords.push(cmd.x);
+            yCoords.push(cmd.y);
+        }
+
+        if (cmd.type === 'Q' || cmd.type === 'C') {
+            xCoords.push(cmd.x1);
+            yCoords.push(cmd.y1);
+        }
+
+        if (cmd.type === 'C') {
+            xCoords.push(cmd.x2);
+            yCoords.push(cmd.y2);
+        }
+    }
+
+    var metrics = {
+        xMin: Math.min.apply(null, xCoords),
+        yMin: Math.min.apply(null, yCoords),
+        xMax: Math.max.apply(null, xCoords),
+        yMax: Math.max.apply(null, yCoords),
+        leftSideBearing: this.leftSideBearing
+    };
+
+    if (!isFinite(metrics.xMin)) {
+        metrics.xMin = 0;
+    }
+
+    if (!isFinite(metrics.xMax)) {
+        metrics.xMax = this.advanceWidth;
+    }
+
+    if (!isFinite(metrics.yMin)) {
+        metrics.yMin = 0;
+    }
+
+    if (!isFinite(metrics.yMax)) {
+        metrics.yMax = 0;
+    }
+
+    metrics.rightSideBearing = this.advanceWidth - metrics.leftSideBearing - (metrics.xMax - metrics.xMin);
+    return metrics;
+};
+
+/**
+ * Draw the glyph on the given context.
+ * @param  {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {Object=} options - xScale, yScale to stretch the glyph.
+ */
+Glyph.prototype.draw = function(ctx, x, y, fontSize, options) {
+    this.getPath(x, y, fontSize, options).draw(ctx);
+};
+
+/**
+ * Draw the points of the glyph.
+ * On-curve points will be drawn in blue, off-curve points will be drawn in red.
+ * @param  {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ */
+Glyph.prototype.drawPoints = function(ctx, x, y, fontSize) {
+    function drawCircles(l, x, y, scale) {
+        ctx.beginPath();
+        for (var j = 0; j < l.length; j += 1) {
+            ctx.moveTo(x + (l[j].x * scale), y + (l[j].y * scale));
+            ctx.arc(x + (l[j].x * scale), y + (l[j].y * scale), 2, 0, Math.PI * 2, false);
+        }
+
+        ctx.closePath();
+        ctx.fill();
+    }
+
+    x = x !== undefined ? x : 0;
+    y = y !== undefined ? y : 0;
+    fontSize = fontSize !== undefined ? fontSize : 24;
+    var scale = 1 / this.path.unitsPerEm * fontSize;
+
+    var blueCircles = [];
+    var redCircles = [];
+    var path = this.path;
+    for (var i = 0; i < path.commands.length; i += 1) {
+        var cmd = path.commands[i];
+        if (cmd.x !== undefined) {
+            blueCircles.push({x: cmd.x, y: -cmd.y});
+        }
+
+        if (cmd.x1 !== undefined) {
+            redCircles.push({x: cmd.x1, y: -cmd.y1});
+        }
+
+        if (cmd.x2 !== undefined) {
+            redCircles.push({x: cmd.x2, y: -cmd.y2});
+        }
+    }
+
+    ctx.fillStyle = 'blue';
+    drawCircles(blueCircles, x, y, scale);
+    ctx.fillStyle = 'red';
+    drawCircles(redCircles, x, y, scale);
+};
+
+/**
+ * Draw lines indicating important font measurements.
+ * Black lines indicate the origin of the coordinate system (point 0,0).
+ * Blue lines indicate the glyph bounding box.
+ * Green line indicates the advance width of the glyph.
+ * @param  {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ */
+Glyph.prototype.drawMetrics = function(ctx, x, y, fontSize) {
+    var scale;
+    x = x !== undefined ? x : 0;
+    y = y !== undefined ? y : 0;
+    fontSize = fontSize !== undefined ? fontSize : 24;
+    scale = 1 / this.path.unitsPerEm * fontSize;
+    ctx.lineWidth = 1;
+
+    // Draw the origin
+    ctx.strokeStyle = 'black';
+    draw.line(ctx, x, -10000, x, 10000);
+    draw.line(ctx, -10000, y, 10000, y);
+
+    // This code is here due to memory optimization: by not using
+    // defaults in the constructor, we save a notable amount of memory.
+    var xMin = this.xMin || 0;
+    var yMin = this.yMin || 0;
+    var xMax = this.xMax || 0;
+    var yMax = this.yMax || 0;
+    var advanceWidth = this.advanceWidth || 0;
+
+    // Draw the glyph box
+    ctx.strokeStyle = 'blue';
+    draw.line(ctx, x + (xMin * scale), -10000, x + (xMin * scale), 10000);
+    draw.line(ctx, x + (xMax * scale), -10000, x + (xMax * scale), 10000);
+    draw.line(ctx, -10000, y + (-yMin * scale), 10000, y + (-yMin * scale));
+    draw.line(ctx, -10000, y + (-yMax * scale), 10000, y + (-yMax * scale));
+
+    // Draw the advance width
+    ctx.strokeStyle = 'green';
+    draw.line(ctx, x + (advanceWidth * scale), -10000, x + (advanceWidth * scale), 10000);
+};
+
+// The GlyphSet object
+
+// Define a property on the glyph that depends on the path being loaded.
+function defineDependentProperty(glyph, externalName, internalName) {
+    Object.defineProperty(glyph, externalName, {
+        get: function() {
+            // Request the path property to make sure the path is loaded.
+            glyph.path; // jshint ignore:line
+            return glyph[internalName];
+        },
+        set: function(newValue) {
+            glyph[internalName] = newValue;
+        },
+        enumerable: true,
+        configurable: true
+    });
+}
+
+/**
+ * A GlyphSet represents all glyphs available in the font, but modelled using
+ * a deferred glyph loader, for retrieving glyphs only once they are absolutely
+ * necessary, to keep the memory footprint down.
+ * @exports opentype.GlyphSet
+ * @class
+ * @param {opentype.Font}
+ * @param {Array}
+ */
+function GlyphSet(font, glyphs) {
+    this.font = font;
+    this.glyphs = {};
+    if (Array.isArray(glyphs)) {
+        for (var i = 0; i < glyphs.length; i++) {
+            var glyph = glyphs[i];
+            glyph.path.unitsPerEm = font.unitsPerEm;
+            this.glyphs[i] = glyph;
+        }
+    }
+
+    this.length = (glyphs && glyphs.length) || 0;
+}
+
+/**
+ * @param  {number} index
+ * @return {opentype.Glyph}
+ */
+GlyphSet.prototype.get = function(index) {
+    // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only.
+    if (this.glyphs[index] === undefined) {
+        this.font._push(index);
+        if (typeof this.glyphs[index] === 'function') {
+            this.glyphs[index] = this.glyphs[index]();
+        }
+
+        var glyph = this.glyphs[index];
+        var unicodeObj = this.font._IndexToUnicodeMap[index];
+
+        if (unicodeObj) {
+            for (var j = 0; j < unicodeObj.unicodes.length; j++)
+                { glyph.addUnicode(unicodeObj.unicodes[j]); }
+        }
+
+        if (this.font.cffEncoding) {
+            if (this.font.isCIDFont) {
+                glyph.name = 'gid' + index;
+            } else {
+                glyph.name = this.font.cffEncoding.charset[index];
+            }
+        } else if (this.font.glyphNames.names) {
+            glyph.name = this.font.glyphNames.glyphIndexToName(index);
+        }
+
+        this.glyphs[index].advanceWidth = this.font._hmtxTableData[index].advanceWidth;
+        this.glyphs[index].leftSideBearing = this.font._hmtxTableData[index].leftSideBearing;
+    } else {
+        if (typeof this.glyphs[index] === 'function') {
+            this.glyphs[index] = this.glyphs[index]();
+        }
+    }
+
+    return this.glyphs[index];
+};
+
+/**
+ * @param  {number} index
+ * @param  {Object}
+ */
+GlyphSet.prototype.push = function(index, loader) {
+    this.glyphs[index] = loader;
+    this.length++;
+};
+
+/**
+ * @alias opentype.glyphLoader
+ * @param  {opentype.Font} font
+ * @param  {number} index
+ * @return {opentype.Glyph}
+ */
+function glyphLoader(font, index) {
+    return new Glyph({index: index, font: font});
+}
+
+/**
+ * Generate a stub glyph that can be filled with all metadata *except*
+ * the "points" and "path" properties, which must be loaded only once
+ * the glyph's path is actually requested for text shaping.
+ * @alias opentype.ttfGlyphLoader
+ * @param  {opentype.Font} font
+ * @param  {number} index
+ * @param  {Function} parseGlyph
+ * @param  {Object} data
+ * @param  {number} position
+ * @param  {Function} buildPath
+ * @return {opentype.Glyph}
+ */
+function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) {
+    return function() {
+        var glyph = new Glyph({index: index, font: font});
+
+        glyph.path = function() {
+            parseGlyph(glyph, data, position);
+            var path = buildPath(font.glyphs, glyph);
+            path.unitsPerEm = font.unitsPerEm;
+            return path;
+        };
+
+        defineDependentProperty(glyph, 'xMin', '_xMin');
+        defineDependentProperty(glyph, 'xMax', '_xMax');
+        defineDependentProperty(glyph, 'yMin', '_yMin');
+        defineDependentProperty(glyph, 'yMax', '_yMax');
+
+        return glyph;
+    };
+}
+/**
+ * @alias opentype.cffGlyphLoader
+ * @param  {opentype.Font} font
+ * @param  {number} index
+ * @param  {Function} parseCFFCharstring
+ * @param  {string} charstring
+ * @return {opentype.Glyph}
+ */
+function cffGlyphLoader(font, index, parseCFFCharstring, charstring) {
+    return function() {
+        var glyph = new Glyph({index: index, font: font});
+
+        glyph.path = function() {
+            var path = parseCFFCharstring(font, glyph, charstring);
+            path.unitsPerEm = font.unitsPerEm;
+            return path;
+        };
+
+        return glyph;
+    };
+}
+
+var glyphset = { GlyphSet: GlyphSet, glyphLoader: glyphLoader, ttfGlyphLoader: ttfGlyphLoader, cffGlyphLoader: cffGlyphLoader };
+
+// The `CFF` table contains the glyph outlines in PostScript format.
+
+// Custom equals function that can also check lists.
+function equals(a, b) {
+    if (a === b) {
+        return true;
+    } else if (Array.isArray(a) && Array.isArray(b)) {
+        if (a.length !== b.length) {
+            return false;
+        }
+
+        for (var i = 0; i < a.length; i += 1) {
+            if (!equals(a[i], b[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    } else {
+        return false;
+    }
+}
+
+// Subroutines are encoded using the negative half of the number space.
+// See type 2 chapter 4.7 "Subroutine operators".
+function calcCFFSubroutineBias(subrs) {
+    var bias;
+    if (subrs.length < 1240) {
+        bias = 107;
+    } else if (subrs.length < 33900) {
+        bias = 1131;
+    } else {
+        bias = 32768;
+    }
+
+    return bias;
+}
+
+// Parse a `CFF` INDEX array.
+// An index array consists of a list of offsets, then a list of objects at those offsets.
+function parseCFFIndex(data, start, conversionFn) {
+    var offsets = [];
+    var objects = [];
+    var count = parse.getCard16(data, start);
+    var objectOffset;
+    var endOffset;
+    if (count !== 0) {
+        var offsetSize = parse.getByte(data, start + 2);
+        objectOffset = start + ((count + 1) * offsetSize) + 2;
+        var pos = start + 3;
+        for (var i = 0; i < count + 1; i += 1) {
+            offsets.push(parse.getOffset(data, pos, offsetSize));
+            pos += offsetSize;
+        }
+
+        // The total size of the index array is 4 header bytes + the value of the last offset.
+        endOffset = objectOffset + offsets[count];
+    } else {
+        endOffset = start + 2;
+    }
+
+    for (var i$1 = 0; i$1 < offsets.length - 1; i$1 += 1) {
+        var value = parse.getBytes(data, objectOffset + offsets[i$1], objectOffset + offsets[i$1 + 1]);
+        if (conversionFn) {
+            value = conversionFn(value);
+        }
+
+        objects.push(value);
+    }
+
+    return {objects: objects, startOffset: start, endOffset: endOffset};
+}
+
+function parseCFFIndexLowMemory(data, start) {
+    var offsets = [];
+    var count = parse.getCard16(data, start);
+    var objectOffset;
+    var endOffset;
+    if (count !== 0) {
+        var offsetSize = parse.getByte(data, start + 2);
+        objectOffset = start + ((count + 1) * offsetSize) + 2;
+        var pos = start + 3;
+        for (var i = 0; i < count + 1; i += 1) {
+            offsets.push(parse.getOffset(data, pos, offsetSize));
+            pos += offsetSize;
+        }
+
+        // The total size of the index array is 4 header bytes + the value of the last offset.
+        endOffset = objectOffset + offsets[count];
+    } else {
+        endOffset = start + 2;
+    }
+
+    return {offsets: offsets, startOffset: start, endOffset: endOffset};
+}
+function getCffIndexObject(i, offsets, data, start, conversionFn) {
+    var count = parse.getCard16(data, start);
+    var objectOffset = 0;
+    if (count !== 0) {
+        var offsetSize = parse.getByte(data, start + 2);
+        objectOffset = start + ((count + 1) * offsetSize) + 2;
+    }
+
+    var value = parse.getBytes(data, objectOffset + offsets[i], objectOffset + offsets[i + 1]);
+    if (conversionFn) {
+        value = conversionFn(value);
+    }
+    return value;
+}
+
+// Parse a `CFF` DICT real value.
+function parseFloatOperand(parser) {
+    var s = '';
+    var eof = 15;
+    var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', null, '-'];
+    while (true) {
+        var b = parser.parseByte();
+        var n1 = b >> 4;
+        var n2 = b & 15;
+
+        if (n1 === eof) {
+            break;
+        }
+
+        s += lookup[n1];
+
+        if (n2 === eof) {
+            break;
+        }
+
+        s += lookup[n2];
+    }
+
+    return parseFloat(s);
+}
+
+// Parse a `CFF` DICT operand.
+function parseOperand(parser, b0) {
+    var b1;
+    var b2;
+    var b3;
+    var b4;
+    if (b0 === 28) {
+        b1 = parser.parseByte();
+        b2 = parser.parseByte();
+        return b1 << 8 | b2;
+    }
+
+    if (b0 === 29) {
+        b1 = parser.parseByte();
+        b2 = parser.parseByte();
+        b3 = parser.parseByte();
+        b4 = parser.parseByte();
+        return b1 << 24 | b2 << 16 | b3 << 8 | b4;
+    }
+
+    if (b0 === 30) {
+        return parseFloatOperand(parser);
+    }
+
+    if (b0 >= 32 && b0 <= 246) {
+        return b0 - 139;
+    }
+
+    if (b0 >= 247 && b0 <= 250) {
+        b1 = parser.parseByte();
+        return (b0 - 247) * 256 + b1 + 108;
+    }
+
+    if (b0 >= 251 && b0 <= 254) {
+        b1 = parser.parseByte();
+        return -(b0 - 251) * 256 - b1 - 108;
+    }
+
+    throw new Error('Invalid b0 ' + b0);
+}
+
+// Convert the entries returned by `parseDict` to a proper dictionary.
+// If a value is a list of one, it is unpacked.
+function entriesToObject(entries) {
+    var o = {};
+    for (var i = 0; i < entries.length; i += 1) {
+        var key = entries[i][0];
+        var values = entries[i][1];
+        var value = (void 0);
+        if (values.length === 1) {
+            value = values[0];
+        } else {
+            value = values;
+        }
+
+        if (o.hasOwnProperty(key) && !isNaN(o[key])) {
+            throw new Error('Object ' + o + ' already has key ' + key);
+        }
+
+        o[key] = value;
+    }
+
+    return o;
+}
+
+// Parse a `CFF` DICT object.
+// A dictionary contains key-value pairs in a compact tokenized format.
+function parseCFFDict(data, start, size) {
+    start = start !== undefined ? start : 0;
+    var parser = new parse.Parser(data, start);
+    var entries = [];
+    var operands = [];
+    size = size !== undefined ? size : data.length;
+
+    while (parser.relativeOffset < size) {
+        var op = parser.parseByte();
+
+        // The first byte for each dict item distinguishes between operator (key) and operand (value).
+        // Values <= 21 are operators.
+        if (op <= 21) {
+            // Two-byte operators have an initial escape byte of 12.
+            if (op === 12) {
+                op = 1200 + parser.parseByte();
+            }
+
+            entries.push([op, operands]);
+            operands = [];
+        } else {
+            // Since the operands (values) come before the operators (keys), we store all operands in a list
+            // until we encounter an operator.
+            operands.push(parseOperand(parser, op));
+        }
+    }
+
+    return entriesToObject(entries);
+}
+
+// Given a String Index (SID), return the value of the string.
+// Strings below index 392 are standard CFF strings and are not encoded in the font.
+function getCFFString(strings, index) {
+    if (index <= 390) {
+        index = cffStandardStrings[index];
+    } else {
+        index = strings[index - 391];
+    }
+
+    return index;
+}
+
+// Interpret a dictionary and return a new dictionary with readable keys and values for missing entries.
+// This function takes `meta` which is a list of objects containing `operand`, `name` and `default`.
+function interpretDict(dict, meta, strings) {
+    var newDict = {};
+    var value;
+
+    // Because we also want to include missing values, we start out from the meta list
+    // and lookup values in the dict.
+    for (var i = 0; i < meta.length; i += 1) {
+        var m = meta[i];
+
+        if (Array.isArray(m.type)) {
+            var values = [];
+            values.length = m.type.length;
+            for (var j = 0; j < m.type.length; j++) {
+                value = dict[m.op] !== undefined ? dict[m.op][j] : undefined;
+                if (value === undefined) {
+                    value = m.value !== undefined && m.value[j] !== undefined ? m.value[j] : null;
+                }
+                if (m.type[j] === 'SID') {
+                    value = getCFFString(strings, value);
+                }
+                values[j] = value;
+            }
+            newDict[m.name] = values;
+        } else {
+            value = dict[m.op];
+            if (value === undefined) {
+                value = m.value !== undefined ? m.value : null;
+            }
+
+            if (m.type === 'SID') {
+                value = getCFFString(strings, value);
+            }
+            newDict[m.name] = value;
+        }
+    }
+
+    return newDict;
+}
+
+// Parse the CFF header.
+function parseCFFHeader(data, start) {
+    var header = {};
+    header.formatMajor = parse.getCard8(data, start);
+    header.formatMinor = parse.getCard8(data, start + 1);
+    header.size = parse.getCard8(data, start + 2);
+    header.offsetSize = parse.getCard8(data, start + 3);
+    header.startOffset = start;
+    header.endOffset = start + 4;
+    return header;
+}
+
+var TOP_DICT_META = [
+    {name: 'version', op: 0, type: 'SID'},
+    {name: 'notice', op: 1, type: 'SID'},
+    {name: 'copyright', op: 1200, type: 'SID'},
+    {name: 'fullName', op: 2, type: 'SID'},
+    {name: 'familyName', op: 3, type: 'SID'},
+    {name: 'weight', op: 4, type: 'SID'},
+    {name: 'isFixedPitch', op: 1201, type: 'number', value: 0},
+    {name: 'italicAngle', op: 1202, type: 'number', value: 0},
+    {name: 'underlinePosition', op: 1203, type: 'number', value: -100},
+    {name: 'underlineThickness', op: 1204, type: 'number', value: 50},
+    {name: 'paintType', op: 1205, type: 'number', value: 0},
+    {name: 'charstringType', op: 1206, type: 'number', value: 2},
+    {
+        name: 'fontMatrix',
+        op: 1207,
+        type: ['real', 'real', 'real', 'real', 'real', 'real'],
+        value: [0.001, 0, 0, 0.001, 0, 0]
+    },
+    {name: 'uniqueId', op: 13, type: 'number'},
+    {name: 'fontBBox', op: 5, type: ['number', 'number', 'number', 'number'], value: [0, 0, 0, 0]},
+    {name: 'strokeWidth', op: 1208, type: 'number', value: 0},
+    {name: 'xuid', op: 14, type: [], value: null},
+    {name: 'charset', op: 15, type: 'offset', value: 0},
+    {name: 'encoding', op: 16, type: 'offset', value: 0},
+    {name: 'charStrings', op: 17, type: 'offset', value: 0},
+    {name: 'private', op: 18, type: ['number', 'offset'], value: [0, 0]},
+    {name: 'ros', op: 1230, type: ['SID', 'SID', 'number']},
+    {name: 'cidFontVersion', op: 1231, type: 'number', value: 0},
+    {name: 'cidFontRevision', op: 1232, type: 'number', value: 0},
+    {name: 'cidFontType', op: 1233, type: 'number', value: 0},
+    {name: 'cidCount', op: 1234, type: 'number', value: 8720},
+    {name: 'uidBase', op: 1235, type: 'number'},
+    {name: 'fdArray', op: 1236, type: 'offset'},
+    {name: 'fdSelect', op: 1237, type: 'offset'},
+    {name: 'fontName', op: 1238, type: 'SID'}
+];
+
+var PRIVATE_DICT_META = [
+    {name: 'subrs', op: 19, type: 'offset', value: 0},
+    {name: 'defaultWidthX', op: 20, type: 'number', value: 0},
+    {name: 'nominalWidthX', op: 21, type: 'number', value: 0}
+];
+
+// Parse the CFF top dictionary. A CFF table can contain multiple fonts, each with their own top dictionary.
+// The top dictionary contains the essential metadata for the font, together with the private dictionary.
+function parseCFFTopDict(data, strings) {
+    var dict = parseCFFDict(data, 0, data.byteLength);
+    return interpretDict(dict, TOP_DICT_META, strings);
+}
+
+// Parse the CFF private dictionary. We don't fully parse out all the values, only the ones we need.
+function parseCFFPrivateDict(data, start, size, strings) {
+    var dict = parseCFFDict(data, start, size);
+    return interpretDict(dict, PRIVATE_DICT_META, strings);
+}
+
+// Returns a list of "Top DICT"s found using an INDEX list.
+// Used to read both the usual high-level Top DICTs and also the FDArray
+// discovered inside CID-keyed fonts.  When a Top DICT has a reference to
+// a Private DICT that is read and saved into the Top DICT.
+//
+// In addition to the expected/optional values as outlined in TOP_DICT_META
+// the following values might be saved into the Top DICT.
+//
+//    _subrs []        array of local CFF subroutines from Private DICT
+//    _subrsBias       bias value computed from number of subroutines
+//                      (see calcCFFSubroutineBias() and parseCFFCharstring())
+//    _defaultWidthX   default widths for CFF characters
+//    _nominalWidthX   bias added to width embedded within glyph description
+//
+//    _privateDict     saved copy of parsed Private DICT from Top DICT
+function gatherCFFTopDicts(data, start, cffIndex, strings) {
+    var topDictArray = [];
+    for (var iTopDict = 0; iTopDict < cffIndex.length; iTopDict += 1) {
+        var topDictData = new DataView(new Uint8Array(cffIndex[iTopDict]).buffer);
+        var topDict = parseCFFTopDict(topDictData, strings);
+        topDict._subrs = [];
+        topDict._subrsBias = 0;
+        topDict._defaultWidthX = 0;
+        topDict._nominalWidthX = 0;
+        var privateSize = topDict.private[0];
+        var privateOffset = topDict.private[1];
+        if (privateSize !== 0 && privateOffset !== 0) {
+            var privateDict = parseCFFPrivateDict(data, privateOffset + start, privateSize, strings);
+            topDict._defaultWidthX = privateDict.defaultWidthX;
+            topDict._nominalWidthX = privateDict.nominalWidthX;
+            if (privateDict.subrs !== 0) {
+                var subrOffset = privateOffset + privateDict.subrs;
+                var subrIndex = parseCFFIndex(data, subrOffset + start);
+                topDict._subrs = subrIndex.objects;
+                topDict._subrsBias = calcCFFSubroutineBias(topDict._subrs);
+            }
+            topDict._privateDict = privateDict;
+        }
+        topDictArray.push(topDict);
+    }
+    return topDictArray;
+}
+
+// Parse the CFF charset table, which contains internal names for all the glyphs.
+// This function will return a list of glyph names.
+// See Adobe TN #5176 chapter 13, "Charsets".
+function parseCFFCharset(data, start, nGlyphs, strings) {
+    var sid;
+    var count;
+    var parser = new parse.Parser(data, start);
+
+    // The .notdef glyph is not included, so subtract 1.
+    nGlyphs -= 1;
+    var charset = ['.notdef'];
+
+    var format = parser.parseCard8();
+    if (format === 0) {
+        for (var i = 0; i < nGlyphs; i += 1) {
+            sid = parser.parseSID();
+            charset.push(getCFFString(strings, sid));
+        }
+    } else if (format === 1) {
+        while (charset.length <= nGlyphs) {
+            sid = parser.parseSID();
+            count = parser.parseCard8();
+            for (var i$1 = 0; i$1 <= count; i$1 += 1) {
+                charset.push(getCFFString(strings, sid));
+                sid += 1;
+            }
+        }
+    } else if (format === 2) {
+        while (charset.length <= nGlyphs) {
+            sid = parser.parseSID();
+            count = parser.parseCard16();
+            for (var i$2 = 0; i$2 <= count; i$2 += 1) {
+                charset.push(getCFFString(strings, sid));
+                sid += 1;
+            }
+        }
+    } else {
+        throw new Error('Unknown charset format ' + format);
+    }
+
+    return charset;
+}
+
+// Parse the CFF encoding data. Only one encoding can be specified per font.
+// See Adobe TN #5176 chapter 12, "Encodings".
+function parseCFFEncoding(data, start, charset) {
+    var code;
+    var enc = {};
+    var parser = new parse.Parser(data, start);
+    var format = parser.parseCard8();
+    if (format === 0) {
+        var nCodes = parser.parseCard8();
+        for (var i = 0; i < nCodes; i += 1) {
+            code = parser.parseCard8();
+            enc[code] = i;
+        }
+    } else if (format === 1) {
+        var nRanges = parser.parseCard8();
+        code = 1;
+        for (var i$1 = 0; i$1 < nRanges; i$1 += 1) {
+            var first = parser.parseCard8();
+            var nLeft = parser.parseCard8();
+            for (var j = first; j <= first + nLeft; j += 1) {
+                enc[j] = code;
+                code += 1;
+            }
+        }
+    } else {
+        throw new Error('Unknown encoding format ' + format);
+    }
+
+    return new CffEncoding(enc, charset);
+}
+
+// Take in charstring code and return a Glyph object.
+// The encoding is described in the Type 2 Charstring Format
+// https://www.microsoft.com/typography/OTSPEC/charstr2.htm
+function parseCFFCharstring(font, glyph, code) {
+    var c1x;
+    var c1y;
+    var c2x;
+    var c2y;
+    var p = new Path();
+    var stack = [];
+    var nStems = 0;
+    var haveWidth = false;
+    var open = false;
+    var x = 0;
+    var y = 0;
+    var subrs;
+    var subrsBias;
+    var defaultWidthX;
+    var nominalWidthX;
+    if (font.isCIDFont) {
+        var fdIndex = font.tables.cff.topDict._fdSelect[glyph.index];
+        var fdDict = font.tables.cff.topDict._fdArray[fdIndex];
+        subrs = fdDict._subrs;
+        subrsBias = fdDict._subrsBias;
+        defaultWidthX = fdDict._defaultWidthX;
+        nominalWidthX = fdDict._nominalWidthX;
+    } else {
+        subrs = font.tables.cff.topDict._subrs;
+        subrsBias = font.tables.cff.topDict._subrsBias;
+        defaultWidthX = font.tables.cff.topDict._defaultWidthX;
+        nominalWidthX = font.tables.cff.topDict._nominalWidthX;
+    }
+    var width = defaultWidthX;
+
+    function newContour(x, y) {
+        if (open) {
+            p.closePath();
+        }
+
+        p.moveTo(x, y);
+        open = true;
+    }
+
+    function parseStems() {
+        var hasWidthArg;
+
+        // The number of stem operators on the stack is always even.
+        // If the value is uneven, that means a width is specified.
+        hasWidthArg = stack.length % 2 !== 0;
+        if (hasWidthArg && !haveWidth) {
+            width = stack.shift() + nominalWidthX;
+        }
+
+        nStems += stack.length >> 1;
+        stack.length = 0;
+        haveWidth = true;
+    }
+
+    function parse(code) {
+        var b1;
+        var b2;
+        var b3;
+        var b4;
+        var codeIndex;
+        var subrCode;
+        var jpx;
+        var jpy;
+        var c3x;
+        var c3y;
+        var c4x;
+        var c4y;
+
+        var i = 0;
+        while (i < code.length) {
+            var v = code[i];
+            i += 1;
+            switch (v) {
+                case 1: // hstem
+                    parseStems();
+                    break;
+                case 3: // vstem
+                    parseStems();
+                    break;
+                case 4: // vmoveto
+                    if (stack.length > 1 && !haveWidth) {
+                        width = stack.shift() + nominalWidthX;
+                        haveWidth = true;
+                    }
+
+                    y += stack.pop();
+                    newContour(x, y);
+                    break;
+                case 5: // rlineto
+                    while (stack.length > 0) {
+                        x += stack.shift();
+                        y += stack.shift();
+                        p.lineTo(x, y);
+                    }
+
+                    break;
+                case 6: // hlineto
+                    while (stack.length > 0) {
+                        x += stack.shift();
+                        p.lineTo(x, y);
+                        if (stack.length === 0) {
+                            break;
+                        }
+
+                        y += stack.shift();
+                        p.lineTo(x, y);
+                    }
+
+                    break;
+                case 7: // vlineto
+                    while (stack.length > 0) {
+                        y += stack.shift();
+                        p.lineTo(x, y);
+                        if (stack.length === 0) {
+                            break;
+                        }
+
+                        x += stack.shift();
+                        p.lineTo(x, y);
+                    }
+
+                    break;
+                case 8: // rrcurveto
+                    while (stack.length > 0) {
+                        c1x = x + stack.shift();
+                        c1y = y + stack.shift();
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        x = c2x + stack.shift();
+                        y = c2y + stack.shift();
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    }
+
+                    break;
+                case 10: // callsubr
+                    codeIndex = stack.pop() + subrsBias;
+                    subrCode = subrs[codeIndex];
+                    if (subrCode) {
+                        parse(subrCode);
+                    }
+
+                    break;
+                case 11: // return
+                    return;
+                case 12: // flex operators
+                    v = code[i];
+                    i += 1;
+                    switch (v) {
+                        case 35: // flex
+                            // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex (12 35) |-
+                            c1x = x   + stack.shift();    // dx1
+                            c1y = y   + stack.shift();    // dy1
+                            c2x = c1x + stack.shift();    // dx2
+                            c2y = c1y + stack.shift();    // dy2
+                            jpx = c2x + stack.shift();    // dx3
+                            jpy = c2y + stack.shift();    // dy3
+                            c3x = jpx + stack.shift();    // dx4
+                            c3y = jpy + stack.shift();    // dy4
+                            c4x = c3x + stack.shift();    // dx5
+                            c4y = c3y + stack.shift();    // dy5
+                            x = c4x   + stack.shift();    // dx6
+                            y = c4y   + stack.shift();    // dy6
+                            stack.shift();                // flex depth
+                            p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
+                            p.curveTo(c3x, c3y, c4x, c4y, x, y);
+                            break;
+                        case 34: // hflex
+                            // |- dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex (12 34) |-
+                            c1x = x   + stack.shift();    // dx1
+                            c1y = y;                      // dy1
+                            c2x = c1x + stack.shift();    // dx2
+                            c2y = c1y + stack.shift();    // dy2
+                            jpx = c2x + stack.shift();    // dx3
+                            jpy = c2y;                    // dy3
+                            c3x = jpx + stack.shift();    // dx4
+                            c3y = c2y;                    // dy4
+                            c4x = c3x + stack.shift();    // dx5
+                            c4y = y;                      // dy5
+                            x = c4x + stack.shift();      // dx6
+                            p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
+                            p.curveTo(c3x, c3y, c4x, c4y, x, y);
+                            break;
+                        case 36: // hflex1
+                            // |- dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1 (12 36) |-
+                            c1x = x   + stack.shift();    // dx1
+                            c1y = y   + stack.shift();    // dy1
+                            c2x = c1x + stack.shift();    // dx2
+                            c2y = c1y + stack.shift();    // dy2
+                            jpx = c2x + stack.shift();    // dx3
+                            jpy = c2y;                    // dy3
+                            c3x = jpx + stack.shift();    // dx4
+                            c3y = c2y;                    // dy4
+                            c4x = c3x + stack.shift();    // dx5
+                            c4y = c3y + stack.shift();    // dy5
+                            x = c4x + stack.shift();      // dx6
+                            p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
+                            p.curveTo(c3x, c3y, c4x, c4y, x, y);
+                            break;
+                        case 37: // flex1
+                            // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1 (12 37) |-
+                            c1x = x   + stack.shift();    // dx1
+                            c1y = y   + stack.shift();    // dy1
+                            c2x = c1x + stack.shift();    // dx2
+                            c2y = c1y + stack.shift();    // dy2
+                            jpx = c2x + stack.shift();    // dx3
+                            jpy = c2y + stack.shift();    // dy3
+                            c3x = jpx + stack.shift();    // dx4
+                            c3y = jpy + stack.shift();    // dy4
+                            c4x = c3x + stack.shift();    // dx5
+                            c4y = c3y + stack.shift();    // dy5
+                            if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
+                                x = c4x + stack.shift();
+                            } else {
+                                y = c4y + stack.shift();
+                            }
+
+                            p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
+                            p.curveTo(c3x, c3y, c4x, c4y, x, y);
+                            break;
+                        default:
+                            console.log('Glyph ' + glyph.index + ': unknown operator ' + 1200 + v);
+                            stack.length = 0;
+                    }
+                    break;
+                case 14: // endchar
+                    if (stack.length > 0 && !haveWidth) {
+                        width = stack.shift() + nominalWidthX;
+                        haveWidth = true;
+                    }
+
+                    if (open) {
+                        p.closePath();
+                        open = false;
+                    }
+
+                    break;
+                case 18: // hstemhm
+                    parseStems();
+                    break;
+                case 19: // hintmask
+                case 20: // cntrmask
+                    parseStems();
+                    i += (nStems + 7) >> 3;
+                    break;
+                case 21: // rmoveto
+                    if (stack.length > 2 && !haveWidth) {
+                        width = stack.shift() + nominalWidthX;
+                        haveWidth = true;
+                    }
+
+                    y += stack.pop();
+                    x += stack.pop();
+                    newContour(x, y);
+                    break;
+                case 22: // hmoveto
+                    if (stack.length > 1 && !haveWidth) {
+                        width = stack.shift() + nominalWidthX;
+                        haveWidth = true;
+                    }
+
+                    x += stack.pop();
+                    newContour(x, y);
+                    break;
+                case 23: // vstemhm
+                    parseStems();
+                    break;
+                case 24: // rcurveline
+                    while (stack.length > 2) {
+                        c1x = x + stack.shift();
+                        c1y = y + stack.shift();
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        x = c2x + stack.shift();
+                        y = c2y + stack.shift();
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    }
+
+                    x += stack.shift();
+                    y += stack.shift();
+                    p.lineTo(x, y);
+                    break;
+                case 25: // rlinecurve
+                    while (stack.length > 6) {
+                        x += stack.shift();
+                        y += stack.shift();
+                        p.lineTo(x, y);
+                    }
+
+                    c1x = x + stack.shift();
+                    c1y = y + stack.shift();
+                    c2x = c1x + stack.shift();
+                    c2y = c1y + stack.shift();
+                    x = c2x + stack.shift();
+                    y = c2y + stack.shift();
+                    p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    break;
+                case 26: // vvcurveto
+                    if (stack.length % 2) {
+                        x += stack.shift();
+                    }
+
+                    while (stack.length > 0) {
+                        c1x = x;
+                        c1y = y + stack.shift();
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        x = c2x;
+                        y = c2y + stack.shift();
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    }
+
+                    break;
+                case 27: // hhcurveto
+                    if (stack.length % 2) {
+                        y += stack.shift();
+                    }
+
+                    while (stack.length > 0) {
+                        c1x = x + stack.shift();
+                        c1y = y;
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        x = c2x + stack.shift();
+                        y = c2y;
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    }
+
+                    break;
+                case 28: // shortint
+                    b1 = code[i];
+                    b2 = code[i + 1];
+                    stack.push(((b1 << 24) | (b2 << 16)) >> 16);
+                    i += 2;
+                    break;
+                case 29: // callgsubr
+                    codeIndex = stack.pop() + font.gsubrsBias;
+                    subrCode = font.gsubrs[codeIndex];
+                    if (subrCode) {
+                        parse(subrCode);
+                    }
+
+                    break;
+                case 30: // vhcurveto
+                    while (stack.length > 0) {
+                        c1x = x;
+                        c1y = y + stack.shift();
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        x = c2x + stack.shift();
+                        y = c2y + (stack.length === 1 ? stack.shift() : 0);
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                        if (stack.length === 0) {
+                            break;
+                        }
+
+                        c1x = x + stack.shift();
+                        c1y = y;
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        y = c2y + stack.shift();
+                        x = c2x + (stack.length === 1 ? stack.shift() : 0);
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    }
+
+                    break;
+                case 31: // hvcurveto
+                    while (stack.length > 0) {
+                        c1x = x + stack.shift();
+                        c1y = y;
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        y = c2y + stack.shift();
+                        x = c2x + (stack.length === 1 ? stack.shift() : 0);
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                        if (stack.length === 0) {
+                            break;
+                        }
+
+                        c1x = x;
+                        c1y = y + stack.shift();
+                        c2x = c1x + stack.shift();
+                        c2y = c1y + stack.shift();
+                        x = c2x + stack.shift();
+                        y = c2y + (stack.length === 1 ? stack.shift() : 0);
+                        p.curveTo(c1x, c1y, c2x, c2y, x, y);
+                    }
+
+                    break;
+                default:
+                    if (v < 32) {
+                        console.log('Glyph ' + glyph.index + ': unknown operator ' + v);
+                    } else if (v < 247) {
+                        stack.push(v - 139);
+                    } else if (v < 251) {
+                        b1 = code[i];
+                        i += 1;
+                        stack.push((v - 247) * 256 + b1 + 108);
+                    } else if (v < 255) {
+                        b1 = code[i];
+                        i += 1;
+                        stack.push(-(v - 251) * 256 - b1 - 108);
+                    } else {
+                        b1 = code[i];
+                        b2 = code[i + 1];
+                        b3 = code[i + 2];
+                        b4 = code[i + 3];
+                        i += 4;
+                        stack.push(((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65536);
+                    }
+            }
+        }
+    }
+
+    parse(code);
+
+    glyph.advanceWidth = width;
+    return p;
+}
+
+function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount) {
+    var fdSelect = [];
+    var fdIndex;
+    var parser = new parse.Parser(data, start);
+    var format = parser.parseCard8();
+    if (format === 0) {
+        // Simple list of nGlyphs elements
+        for (var iGid = 0; iGid < nGlyphs; iGid++) {
+            fdIndex = parser.parseCard8();
+            if (fdIndex >= fdArrayCount) {
+                throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')');
+            }
+            fdSelect.push(fdIndex);
+        }
+    } else if (format === 3) {
+        // Ranges
+        var nRanges = parser.parseCard16();
+        var first = parser.parseCard16();
+        if (first !== 0) {
+            throw new Error('CFF Table CID Font FDSelect format 3 range has bad initial GID ' + first);
+        }
+        var next;
+        for (var iRange = 0; iRange < nRanges; iRange++) {
+            fdIndex = parser.parseCard8();
+            next = parser.parseCard16();
+            if (fdIndex >= fdArrayCount) {
+                throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')');
+            }
+            if (next > nGlyphs) {
+                throw new Error('CFF Table CID Font FDSelect format 3 range has bad GID ' + next);
+            }
+            for (; first < next; first++) {
+                fdSelect.push(fdIndex);
+            }
+            first = next;
+        }
+        if (next !== nGlyphs) {
+            throw new Error('CFF Table CID Font FDSelect format 3 range has bad final GID ' + next);
+        }
+    } else {
+        throw new Error('CFF Table CID Font FDSelect table has unsupported format ' + format);
+    }
+    return fdSelect;
+}
+
+// Parse the `CFF` table, which contains the glyph outlines in PostScript format.
+function parseCFFTable(data, start, font, opt) {
+    font.tables.cff = {};
+    var header = parseCFFHeader(data, start);
+    var nameIndex = parseCFFIndex(data, header.endOffset, parse.bytesToString);
+    var topDictIndex = parseCFFIndex(data, nameIndex.endOffset);
+    var stringIndex = parseCFFIndex(data, topDictIndex.endOffset, parse.bytesToString);
+    var globalSubrIndex = parseCFFIndex(data, stringIndex.endOffset);
+    font.gsubrs = globalSubrIndex.objects;
+    font.gsubrsBias = calcCFFSubroutineBias(font.gsubrs);
+
+    var topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects);
+    if (topDictArray.length !== 1) {
+        throw new Error('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length);
+    }
+
+    var topDict = topDictArray[0];
+    font.tables.cff.topDict = topDict;
+
+    if (topDict._privateDict) {
+        font.defaultWidthX = topDict._privateDict.defaultWidthX;
+        font.nominalWidthX = topDict._privateDict.nominalWidthX;
+    }
+
+    if (topDict.ros[0] !== undefined && topDict.ros[1] !== undefined) {
+        font.isCIDFont = true;
+    }
+
+    if (font.isCIDFont) {
+        var fdArrayOffset = topDict.fdArray;
+        var fdSelectOffset = topDict.fdSelect;
+        if (fdArrayOffset === 0 || fdSelectOffset === 0) {
+            throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing');
+        }
+        fdArrayOffset += start;
+        var fdArrayIndex = parseCFFIndex(data, fdArrayOffset);
+        var fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects);
+        topDict._fdArray = fdArray;
+        fdSelectOffset += start;
+        topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length);
+    }
+
+    var privateDictOffset = start + topDict.private[1];
+    var privateDict = parseCFFPrivateDict(data, privateDictOffset, topDict.private[0], stringIndex.objects);
+    font.defaultWidthX = privateDict.defaultWidthX;
+    font.nominalWidthX = privateDict.nominalWidthX;
+
+    if (privateDict.subrs !== 0) {
+        var subrOffset = privateDictOffset + privateDict.subrs;
+        var subrIndex = parseCFFIndex(data, subrOffset);
+        font.subrs = subrIndex.objects;
+        font.subrsBias = calcCFFSubroutineBias(font.subrs);
+    } else {
+        font.subrs = [];
+        font.subrsBias = 0;
+    }
+
+    // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset.
+    var charStringsIndex;
+    if (opt.lowMemory) {
+        charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings);
+        font.nGlyphs = charStringsIndex.offsets.length;
+    } else {
+        charStringsIndex = parseCFFIndex(data, start + topDict.charStrings);
+        font.nGlyphs = charStringsIndex.objects.length;
+    }
+
+    var charset = parseCFFCharset(data, start + topDict.charset, font.nGlyphs, stringIndex.objects);
+    if (topDict.encoding === 0) {
+        // Standard encoding
+        font.cffEncoding = new CffEncoding(cffStandardEncoding, charset);
+    } else if (topDict.encoding === 1) {
+        // Expert encoding
+        font.cffEncoding = new CffEncoding(cffExpertEncoding, charset);
+    } else {
+        font.cffEncoding = parseCFFEncoding(data, start + topDict.encoding, charset);
+    }
+
+    // Prefer the CMAP encoding to the CFF encoding.
+    font.encoding = font.encoding || font.cffEncoding;
+
+    font.glyphs = new glyphset.GlyphSet(font);
+    if (opt.lowMemory) {
+        font._push = function(i) {
+            var charString = getCffIndexObject(i, charStringsIndex.offsets, data, start + topDict.charStrings);
+            font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString));
+        };
+    } else {
+        for (var i = 0; i < font.nGlyphs; i += 1) {
+            var charString = charStringsIndex.objects[i];
+            font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString));
+        }
+    }
+}
+
+// Convert a string to a String ID (SID).
+// The list of strings is modified in place.
+function encodeString(s, strings) {
+    var sid;
+
+    // Is the string in the CFF standard strings?
+    var i = cffStandardStrings.indexOf(s);
+    if (i >= 0) {
+        sid = i;
+    }
+
+    // Is the string already in the string index?
+    i = strings.indexOf(s);
+    if (i >= 0) {
+        sid = i + cffStandardStrings.length;
+    } else {
+        sid = cffStandardStrings.length + strings.length;
+        strings.push(s);
+    }
+
+    return sid;
+}
+
+function makeHeader() {
+    return new table.Record('Header', [
+        {name: 'major', type: 'Card8', value: 1},
+        {name: 'minor', type: 'Card8', value: 0},
+        {name: 'hdrSize', type: 'Card8', value: 4},
+        {name: 'major', type: 'Card8', value: 1}
+    ]);
+}
+
+function makeNameIndex(fontNames) {
+    var t = new table.Record('Name INDEX', [
+        {name: 'names', type: 'INDEX', value: []}
+    ]);
+    t.names = [];
+    for (var i = 0; i < fontNames.length; i += 1) {
+        t.names.push({name: 'name_' + i, type: 'NAME', value: fontNames[i]});
+    }
+
+    return t;
+}
+
+// Given a dictionary's metadata, create a DICT structure.
+function makeDict(meta, attrs, strings) {
+    var m = {};
+    for (var i = 0; i < meta.length; i += 1) {
+        var entry = meta[i];
+        var value = attrs[entry.name];
+        if (value !== undefined && !equals(value, entry.value)) {
+            if (entry.type === 'SID') {
+                value = encodeString(value, strings);
+            }
+
+            m[entry.op] = {name: entry.name, type: entry.type, value: value};
+        }
+    }
+
+    return m;
+}
+
+// The Top DICT houses the global font attributes.
+function makeTopDict(attrs, strings) {
+    var t = new table.Record('Top DICT', [
+        {name: 'dict', type: 'DICT', value: {}}
+    ]);
+    t.dict = makeDict(TOP_DICT_META, attrs, strings);
+    return t;
+}
+
+function makeTopDictIndex(topDict) {
+    var t = new table.Record('Top DICT INDEX', [
+        {name: 'topDicts', type: 'INDEX', value: []}
+    ]);
+    t.topDicts = [{name: 'topDict_0', type: 'TABLE', value: topDict}];
+    return t;
+}
+
+function makeStringIndex(strings) {
+    var t = new table.Record('String INDEX', [
+        {name: 'strings', type: 'INDEX', value: []}
+    ]);
+    t.strings = [];
+    for (var i = 0; i < strings.length; i += 1) {
+        t.strings.push({name: 'string_' + i, type: 'STRING', value: strings[i]});
+    }
+
+    return t;
+}
+
+function makeGlobalSubrIndex() {
+    // Currently we don't use subroutines.
+    return new table.Record('Global Subr INDEX', [
+        {name: 'subrs', type: 'INDEX', value: []}
+    ]);
+}
+
+function makeCharsets(glyphNames, strings) {
+    var t = new table.Record('Charsets', [
+        {name: 'format', type: 'Card8', value: 0}
+    ]);
+    for (var i = 0; i < glyphNames.length; i += 1) {
+        var glyphName = glyphNames[i];
+        var glyphSID = encodeString(glyphName, strings);
+        t.fields.push({name: 'glyph_' + i, type: 'SID', value: glyphSID});
+    }
+
+    return t;
+}
+
+function glyphToOps(glyph) {
+    var ops = [];
+    var path = glyph.path;
+    ops.push({name: 'width', type: 'NUMBER', value: glyph.advanceWidth});
+    var x = 0;
+    var y = 0;
+    for (var i = 0; i < path.commands.length; i += 1) {
+        var dx = (void 0);
+        var dy = (void 0);
+        var cmd = path.commands[i];
+        if (cmd.type === 'Q') {
+            // CFF only supports bézier curves, so convert the quad to a bézier.
+            var _13 = 1 / 3;
+            var _23 = 2 / 3;
+
+            // We're going to create a new command so we don't change the original path.
+            // Since all coordinates are relative, we round() them ASAP to avoid propagating errors.
+            cmd = {
+                type: 'C',
+                x: cmd.x,
+                y: cmd.y,
+                x1: Math.round(_13 * x + _23 * cmd.x1),
+                y1: Math.round(_13 * y + _23 * cmd.y1),
+                x2: Math.round(_13 * cmd.x + _23 * cmd.x1),
+                y2: Math.round(_13 * cmd.y + _23 * cmd.y1)
+            };
+        }
+
+        if (cmd.type === 'M') {
+            dx = Math.round(cmd.x - x);
+            dy = Math.round(cmd.y - y);
+            ops.push({name: 'dx', type: 'NUMBER', value: dx});
+            ops.push({name: 'dy', type: 'NUMBER', value: dy});
+            ops.push({name: 'rmoveto', type: 'OP', value: 21});
+            x = Math.round(cmd.x);
+            y = Math.round(cmd.y);
+        } else if (cmd.type === 'L') {
+            dx = Math.round(cmd.x - x);
+            dy = Math.round(cmd.y - y);
+            ops.push({name: 'dx', type: 'NUMBER', value: dx});
+            ops.push({name: 'dy', type: 'NUMBER', value: dy});
+            ops.push({name: 'rlineto', type: 'OP', value: 5});
+            x = Math.round(cmd.x);
+            y = Math.round(cmd.y);
+        } else if (cmd.type === 'C') {
+            var dx1 = Math.round(cmd.x1 - x);
+            var dy1 = Math.round(cmd.y1 - y);
+            var dx2 = Math.round(cmd.x2 - cmd.x1);
+            var dy2 = Math.round(cmd.y2 - cmd.y1);
+            dx = Math.round(cmd.x - cmd.x2);
+            dy = Math.round(cmd.y - cmd.y2);
+            ops.push({name: 'dx1', type: 'NUMBER', value: dx1});
+            ops.push({name: 'dy1', type: 'NUMBER', value: dy1});
+            ops.push({name: 'dx2', type: 'NUMBER', value: dx2});
+            ops.push({name: 'dy2', type: 'NUMBER', value: dy2});
+            ops.push({name: 'dx', type: 'NUMBER', value: dx});
+            ops.push({name: 'dy', type: 'NUMBER', value: dy});
+            ops.push({name: 'rrcurveto', type: 'OP', value: 8});
+            x = Math.round(cmd.x);
+            y = Math.round(cmd.y);
+        }
+
+        // Contours are closed automatically.
+    }
+
+    ops.push({name: 'endchar', type: 'OP', value: 14});
+    return ops;
+}
+
+function makeCharStringsIndex(glyphs) {
+    var t = new table.Record('CharStrings INDEX', [
+        {name: 'charStrings', type: 'INDEX', value: []}
+    ]);
+
+    for (var i = 0; i < glyphs.length; i += 1) {
+        var glyph = glyphs.get(i);
+        var ops = glyphToOps(glyph);
+        t.charStrings.push({name: glyph.name, type: 'CHARSTRING', value: ops});
+    }
+
+    return t;
+}
+
+function makePrivateDict(attrs, strings) {
+    var t = new table.Record('Private DICT', [
+        {name: 'dict', type: 'DICT', value: {}}
+    ]);
+    t.dict = makeDict(PRIVATE_DICT_META, attrs, strings);
+    return t;
+}
+
+function makeCFFTable(glyphs, options) {
+    var t = new table.Table('CFF ', [
+        {name: 'header', type: 'RECORD'},
+        {name: 'nameIndex', type: 'RECORD'},
+        {name: 'topDictIndex', type: 'RECORD'},
+        {name: 'stringIndex', type: 'RECORD'},
+        {name: 'globalSubrIndex', type: 'RECORD'},
+        {name: 'charsets', type: 'RECORD'},
+        {name: 'charStringsIndex', type: 'RECORD'},
+        {name: 'privateDict', type: 'RECORD'}
+    ]);
+
+    var fontScale = 1 / options.unitsPerEm;
+    // We use non-zero values for the offsets so that the DICT encodes them.
+    // This is important because the size of the Top DICT plays a role in offset calculation,
+    // and the size shouldn't change after we've written correct offsets.
+    var attrs = {
+        version: options.version,
+        fullName: options.fullName,
+        familyName: options.familyName,
+        weight: options.weightName,
+        fontBBox: options.fontBBox || [0, 0, 0, 0],
+        fontMatrix: [fontScale, 0, 0, fontScale, 0, 0],
+        charset: 999,
+        encoding: 0,
+        charStrings: 999,
+        private: [0, 999]
+    };
+
+    var privateAttrs = {};
+
+    var glyphNames = [];
+    var glyph;
+
+    // Skip first glyph (.notdef)
+    for (var i = 1; i < glyphs.length; i += 1) {
+        glyph = glyphs.get(i);
+        glyphNames.push(glyph.name);
+    }
+
+    var strings = [];
+
+    t.header = makeHeader();
+    t.nameIndex = makeNameIndex([options.postScriptName]);
+    var topDict = makeTopDict(attrs, strings);
+    t.topDictIndex = makeTopDictIndex(topDict);
+    t.globalSubrIndex = makeGlobalSubrIndex();
+    t.charsets = makeCharsets(glyphNames, strings);
+    t.charStringsIndex = makeCharStringsIndex(glyphs);
+    t.privateDict = makePrivateDict(privateAttrs, strings);
+
+    // Needs to come at the end, to encode all custom strings used in the font.
+    t.stringIndex = makeStringIndex(strings);
+
+    var startOffset = t.header.sizeOf() +
+        t.nameIndex.sizeOf() +
+        t.topDictIndex.sizeOf() +
+        t.stringIndex.sizeOf() +
+        t.globalSubrIndex.sizeOf();
+    attrs.charset = startOffset;
+
+    // We use the CFF standard encoding; proper encoding will be handled in cmap.
+    attrs.encoding = 0;
+    attrs.charStrings = attrs.charset + t.charsets.sizeOf();
+    attrs.private[1] = attrs.charStrings + t.charStringsIndex.sizeOf();
+
+    // Recreate the Top DICT INDEX with the correct offsets.
+    topDict = makeTopDict(attrs, strings);
+    t.topDictIndex = makeTopDictIndex(topDict);
+
+    return t;
+}
+
+var cff = { parse: parseCFFTable, make: makeCFFTable };
+
+// The `head` table contains global information about the font.
+
+// Parse the header `head` table
+function parseHeadTable(data, start) {
+    var head = {};
+    var p = new parse.Parser(data, start);
+    head.version = p.parseVersion();
+    head.fontRevision = Math.round(p.parseFixed() * 1000) / 1000;
+    head.checkSumAdjustment = p.parseULong();
+    head.magicNumber = p.parseULong();
+    check.argument(head.magicNumber === 0x5F0F3CF5, 'Font header has wrong magic number.');
+    head.flags = p.parseUShort();
+    head.unitsPerEm = p.parseUShort();
+    head.created = p.parseLongDateTime();
+    head.modified = p.parseLongDateTime();
+    head.xMin = p.parseShort();
+    head.yMin = p.parseShort();
+    head.xMax = p.parseShort();
+    head.yMax = p.parseShort();
+    head.macStyle = p.parseUShort();
+    head.lowestRecPPEM = p.parseUShort();
+    head.fontDirectionHint = p.parseShort();
+    head.indexToLocFormat = p.parseShort();
+    head.glyphDataFormat = p.parseShort();
+    return head;
+}
+
+function makeHeadTable(options) {
+    // Apple Mac timestamp epoch is 01/01/1904 not 01/01/1970
+    var timestamp = Math.round(new Date().getTime() / 1000) + 2082844800;
+    var createdTimestamp = timestamp;
+
+    if (options.createdTimestamp) {
+        createdTimestamp = options.createdTimestamp + 2082844800;
+    }
+
+    return new table.Table('head', [
+        {name: 'version', type: 'FIXED', value: 0x00010000},
+        {name: 'fontRevision', type: 'FIXED', value: 0x00010000},
+        {name: 'checkSumAdjustment', type: 'ULONG', value: 0},
+        {name: 'magicNumber', type: 'ULONG', value: 0x5F0F3CF5},
+        {name: 'flags', type: 'USHORT', value: 0},
+        {name: 'unitsPerEm', type: 'USHORT', value: 1000},
+        {name: 'created', type: 'LONGDATETIME', value: createdTimestamp},
+        {name: 'modified', type: 'LONGDATETIME', value: timestamp},
+        {name: 'xMin', type: 'SHORT', value: 0},
+        {name: 'yMin', type: 'SHORT', value: 0},
+        {name: 'xMax', type: 'SHORT', value: 0},
+        {name: 'yMax', type: 'SHORT', value: 0},
+        {name: 'macStyle', type: 'USHORT', value: 0},
+        {name: 'lowestRecPPEM', type: 'USHORT', value: 0},
+        {name: 'fontDirectionHint', type: 'SHORT', value: 2},
+        {name: 'indexToLocFormat', type: 'SHORT', value: 0},
+        {name: 'glyphDataFormat', type: 'SHORT', value: 0}
+    ], options);
+}
+
+var head = { parse: parseHeadTable, make: makeHeadTable };
+
+// The `hhea` table contains information for horizontal layout.
+
+// Parse the horizontal header `hhea` table
+function parseHheaTable(data, start) {
+    var hhea = {};
+    var p = new parse.Parser(data, start);
+    hhea.version = p.parseVersion();
+    hhea.ascender = p.parseShort();
+    hhea.descender = p.parseShort();
+    hhea.lineGap = p.parseShort();
+    hhea.advanceWidthMax = p.parseUShort();
+    hhea.minLeftSideBearing = p.parseShort();
+    hhea.minRightSideBearing = p.parseShort();
+    hhea.xMaxExtent = p.parseShort();
+    hhea.caretSlopeRise = p.parseShort();
+    hhea.caretSlopeRun = p.parseShort();
+    hhea.caretOffset = p.parseShort();
+    p.relativeOffset += 8;
+    hhea.metricDataFormat = p.parseShort();
+    hhea.numberOfHMetrics = p.parseUShort();
+    return hhea;
+}
+
+function makeHheaTable(options) {
+    return new table.Table('hhea', [
+        {name: 'version', type: 'FIXED', value: 0x00010000},
+        {name: 'ascender', type: 'FWORD', value: 0},
+        {name: 'descender', type: 'FWORD', value: 0},
+        {name: 'lineGap', type: 'FWORD', value: 0},
+        {name: 'advanceWidthMax', type: 'UFWORD', value: 0},
+        {name: 'minLeftSideBearing', type: 'FWORD', value: 0},
+        {name: 'minRightSideBearing', type: 'FWORD', value: 0},
+        {name: 'xMaxExtent', type: 'FWORD', value: 0},
+        {name: 'caretSlopeRise', type: 'SHORT', value: 1},
+        {name: 'caretSlopeRun', type: 'SHORT', value: 0},
+        {name: 'caretOffset', type: 'SHORT', value: 0},
+        {name: 'reserved1', type: 'SHORT', value: 0},
+        {name: 'reserved2', type: 'SHORT', value: 0},
+        {name: 'reserved3', type: 'SHORT', value: 0},
+        {name: 'reserved4', type: 'SHORT', value: 0},
+        {name: 'metricDataFormat', type: 'SHORT', value: 0},
+        {name: 'numberOfHMetrics', type: 'USHORT', value: 0}
+    ], options);
+}
+
+var hhea = { parse: parseHheaTable, make: makeHheaTable };
+
+// The `hmtx` table contains the horizontal metrics for all glyphs.
+
+function parseHmtxTableAll(data, start, numMetrics, numGlyphs, glyphs) {
+    var advanceWidth;
+    var leftSideBearing;
+    var p = new parse.Parser(data, start);
+    for (var i = 0; i < numGlyphs; i += 1) {
+        // If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs.
+        if (i < numMetrics) {
+            advanceWidth = p.parseUShort();
+            leftSideBearing = p.parseShort();
+        }
+
+        var glyph = glyphs.get(i);
+        glyph.advanceWidth = advanceWidth;
+        glyph.leftSideBearing = leftSideBearing;
+    }
+}
+
+function parseHmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs) {
+    font._hmtxTableData = {};
+
+    var advanceWidth;
+    var leftSideBearing;
+    var p = new parse.Parser(data, start);
+    for (var i = 0; i < numGlyphs; i += 1) {
+        // If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs.
+        if (i < numMetrics) {
+            advanceWidth = p.parseUShort();
+            leftSideBearing = p.parseShort();
+        }
+
+        font._hmtxTableData[i] = {
+            advanceWidth: advanceWidth,
+            leftSideBearing: leftSideBearing
+        };
+    }
+}
+
+// Parse the `hmtx` table, which contains the horizontal metrics for all glyphs.
+// This function augments the glyph array, adding the advanceWidth and leftSideBearing to each glyph.
+function parseHmtxTable(font, data, start, numMetrics, numGlyphs, glyphs, opt) {
+    if (opt.lowMemory)
+        { parseHmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs); }
+    else
+        { parseHmtxTableAll(data, start, numMetrics, numGlyphs, glyphs); }
+}
+
+function makeHmtxTable(glyphs) {
+    var t = new table.Table('hmtx', []);
+    for (var i = 0; i < glyphs.length; i += 1) {
+        var glyph = glyphs.get(i);
+        var advanceWidth = glyph.advanceWidth || 0;
+        var leftSideBearing = glyph.leftSideBearing || 0;
+        t.fields.push({name: 'advanceWidth_' + i, type: 'USHORT', value: advanceWidth});
+        t.fields.push({name: 'leftSideBearing_' + i, type: 'SHORT', value: leftSideBearing});
+    }
+
+    return t;
+}
+
+var hmtx = { parse: parseHmtxTable, make: makeHmtxTable };
+
+// The `ltag` table stores IETF BCP-47 language tags. It allows supporting
+
+function makeLtagTable(tags) {
+    var result = new table.Table('ltag', [
+        {name: 'version', type: 'ULONG', value: 1},
+        {name: 'flags', type: 'ULONG', value: 0},
+        {name: 'numTags', type: 'ULONG', value: tags.length}
+    ]);
+
+    var stringPool = '';
+    var stringPoolOffset = 12 + tags.length * 4;
+    for (var i = 0; i < tags.length; ++i) {
+        var pos = stringPool.indexOf(tags[i]);
+        if (pos < 0) {
+            pos = stringPool.length;
+            stringPool += tags[i];
+        }
+
+        result.fields.push({name: 'offset ' + i, type: 'USHORT', value: stringPoolOffset + pos});
+        result.fields.push({name: 'length ' + i, type: 'USHORT', value: tags[i].length});
+    }
+
+    result.fields.push({name: 'stringPool', type: 'CHARARRAY', value: stringPool});
+    return result;
+}
+
+function parseLtagTable(data, start) {
+    var p = new parse.Parser(data, start);
+    var tableVersion = p.parseULong();
+    check.argument(tableVersion === 1, 'Unsupported ltag table version.');
+    // The 'ltag' specification does not define any flags; skip the field.
+    p.skip('uLong', 1);
+    var numTags = p.parseULong();
+
+    var tags = [];
+    for (var i = 0; i < numTags; i++) {
+        var tag = '';
+        var offset = start + p.parseUShort();
+        var length = p.parseUShort();
+        for (var j = offset; j < offset + length; ++j) {
+            tag += String.fromCharCode(data.getInt8(j));
+        }
+
+        tags.push(tag);
+    }
+
+    return tags;
+}
+
+var ltag = { make: makeLtagTable, parse: parseLtagTable };
+
+// The `maxp` table establishes the memory requirements for the font.
+
+// Parse the maximum profile `maxp` table.
+function parseMaxpTable(data, start) {
+    var maxp = {};
+    var p = new parse.Parser(data, start);
+    maxp.version = p.parseVersion();
+    maxp.numGlyphs = p.parseUShort();
+    if (maxp.version === 1.0) {
+        maxp.maxPoints = p.parseUShort();
+        maxp.maxContours = p.parseUShort();
+        maxp.maxCompositePoints = p.parseUShort();
+        maxp.maxCompositeContours = p.parseUShort();
+        maxp.maxZones = p.parseUShort();
+        maxp.maxTwilightPoints = p.parseUShort();
+        maxp.maxStorage = p.parseUShort();
+        maxp.maxFunctionDefs = p.parseUShort();
+        maxp.maxInstructionDefs = p.parseUShort();
+        maxp.maxStackElements = p.parseUShort();
+        maxp.maxSizeOfInstructions = p.parseUShort();
+        maxp.maxComponentElements = p.parseUShort();
+        maxp.maxComponentDepth = p.parseUShort();
+    }
+
+    return maxp;
+}
+
+function makeMaxpTable(numGlyphs) {
+    return new table.Table('maxp', [
+        {name: 'version', type: 'FIXED', value: 0x00005000},
+        {name: 'numGlyphs', type: 'USHORT', value: numGlyphs}
+    ]);
+}
+
+var maxp = { parse: parseMaxpTable, make: makeMaxpTable };
+
+// The `name` naming table.
+
+// NameIDs for the name table.
+var nameTableNames = [
+    'copyright',              // 0
+    'fontFamily',             // 1
+    'fontSubfamily',          // 2
+    'uniqueID',               // 3
+    'fullName',               // 4
+    'version',                // 5
+    'postScriptName',         // 6
+    'trademark',              // 7
+    'manufacturer',           // 8
+    'designer',               // 9
+    'description',            // 10
+    'manufacturerURL',        // 11
+    'designerURL',            // 12
+    'license',                // 13
+    'licenseURL',             // 14
+    'reserved',               // 15
+    'preferredFamily',        // 16
+    'preferredSubfamily',     // 17
+    'compatibleFullName',     // 18
+    'sampleText',             // 19
+    'postScriptFindFontName', // 20
+    'wwsFamily',              // 21
+    'wwsSubfamily'            // 22
+];
+
+var macLanguages = {
+    0: 'en',
+    1: 'fr',
+    2: 'de',
+    3: 'it',
+    4: 'nl',
+    5: 'sv',
+    6: 'es',
+    7: 'da',
+    8: 'pt',
+    9: 'no',
+    10: 'he',
+    11: 'ja',
+    12: 'ar',
+    13: 'fi',
+    14: 'el',
+    15: 'is',
+    16: 'mt',
+    17: 'tr',
+    18: 'hr',
+    19: 'zh-Hant',
+    20: 'ur',
+    21: 'hi',
+    22: 'th',
+    23: 'ko',
+    24: 'lt',
+    25: 'pl',
+    26: 'hu',
+    27: 'es',
+    28: 'lv',
+    29: 'se',
+    30: 'fo',
+    31: 'fa',
+    32: 'ru',
+    33: 'zh',
+    34: 'nl-BE',
+    35: 'ga',
+    36: 'sq',
+    37: 'ro',
+    38: 'cz',
+    39: 'sk',
+    40: 'si',
+    41: 'yi',
+    42: 'sr',
+    43: 'mk',
+    44: 'bg',
+    45: 'uk',
+    46: 'be',
+    47: 'uz',
+    48: 'kk',
+    49: 'az-Cyrl',
+    50: 'az-Arab',
+    51: 'hy',
+    52: 'ka',
+    53: 'mo',
+    54: 'ky',
+    55: 'tg',
+    56: 'tk',
+    57: 'mn-CN',
+    58: 'mn',
+    59: 'ps',
+    60: 'ks',
+    61: 'ku',
+    62: 'sd',
+    63: 'bo',
+    64: 'ne',
+    65: 'sa',
+    66: 'mr',
+    67: 'bn',
+    68: 'as',
+    69: 'gu',
+    70: 'pa',
+    71: 'or',
+    72: 'ml',
+    73: 'kn',
+    74: 'ta',
+    75: 'te',
+    76: 'si',
+    77: 'my',
+    78: 'km',
+    79: 'lo',
+    80: 'vi',
+    81: 'id',
+    82: 'tl',
+    83: 'ms',
+    84: 'ms-Arab',
+    85: 'am',
+    86: 'ti',
+    87: 'om',
+    88: 'so',
+    89: 'sw',
+    90: 'rw',
+    91: 'rn',
+    92: 'ny',
+    93: 'mg',
+    94: 'eo',
+    128: 'cy',
+    129: 'eu',
+    130: 'ca',
+    131: 'la',
+    132: 'qu',
+    133: 'gn',
+    134: 'ay',
+    135: 'tt',
+    136: 'ug',
+    137: 'dz',
+    138: 'jv',
+    139: 'su',
+    140: 'gl',
+    141: 'af',
+    142: 'br',
+    143: 'iu',
+    144: 'gd',
+    145: 'gv',
+    146: 'ga',
+    147: 'to',
+    148: 'el-polyton',
+    149: 'kl',
+    150: 'az',
+    151: 'nn'
+};
+
+// MacOS language ID → MacOS script ID
+//
+// Note that the script ID is not sufficient to determine what encoding
+// to use in TrueType files. For some languages, MacOS used a modification
+// of a mainstream script. For example, an Icelandic name would be stored
+// with smRoman in the TrueType naming table, but the actual encoding
+// is a special Icelandic version of the normal Macintosh Roman encoding.
+// As another example, Inuktitut uses an 8-bit encoding for Canadian Aboriginal
+// Syllables but MacOS had run out of available script codes, so this was
+// done as a (pretty radical) "modification" of Ethiopic.
+//
+// http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt
+var macLanguageToScript = {
+    0: 0,  // langEnglish → smRoman
+    1: 0,  // langFrench → smRoman
+    2: 0,  // langGerman → smRoman
+    3: 0,  // langItalian → smRoman
+    4: 0,  // langDutch → smRoman
+    5: 0,  // langSwedish → smRoman
+    6: 0,  // langSpanish → smRoman
+    7: 0,  // langDanish → smRoman
+    8: 0,  // langPortuguese → smRoman
+    9: 0,  // langNorwegian → smRoman
+    10: 5,  // langHebrew → smHebrew
+    11: 1,  // langJapanese → smJapanese
+    12: 4,  // langArabic → smArabic
+    13: 0,  // langFinnish → smRoman
+    14: 6,  // langGreek → smGreek
+    15: 0,  // langIcelandic → smRoman (modified)
+    16: 0,  // langMaltese → smRoman
+    17: 0,  // langTurkish → smRoman (modified)
+    18: 0,  // langCroatian → smRoman (modified)
+    19: 2,  // langTradChinese → smTradChinese
+    20: 4,  // langUrdu → smArabic
+    21: 9,  // langHindi → smDevanagari
+    22: 21,  // langThai → smThai
+    23: 3,  // langKorean → smKorean
+    24: 29,  // langLithuanian → smCentralEuroRoman
+    25: 29,  // langPolish → smCentralEuroRoman
+    26: 29,  // langHungarian → smCentralEuroRoman
+    27: 29,  // langEstonian → smCentralEuroRoman
+    28: 29,  // langLatvian → smCentralEuroRoman
+    29: 0,  // langSami → smRoman
+    30: 0,  // langFaroese → smRoman (modified)
+    31: 4,  // langFarsi → smArabic (modified)
+    32: 7,  // langRussian → smCyrillic
+    33: 25,  // langSimpChinese → smSimpChinese
+    34: 0,  // langFlemish → smRoman
+    35: 0,  // langIrishGaelic → smRoman (modified)
+    36: 0,  // langAlbanian → smRoman
+    37: 0,  // langRomanian → smRoman (modified)
+    38: 29,  // langCzech → smCentralEuroRoman
+    39: 29,  // langSlovak → smCentralEuroRoman
+    40: 0,  // langSlovenian → smRoman (modified)
+    41: 5,  // langYiddish → smHebrew
+    42: 7,  // langSerbian → smCyrillic
+    43: 7,  // langMacedonian → smCyrillic
+    44: 7,  // langBulgarian → smCyrillic
+    45: 7,  // langUkrainian → smCyrillic (modified)
+    46: 7,  // langByelorussian → smCyrillic
+    47: 7,  // langUzbek → smCyrillic
+    48: 7,  // langKazakh → smCyrillic
+    49: 7,  // langAzerbaijani → smCyrillic
+    50: 4,  // langAzerbaijanAr → smArabic
+    51: 24,  // langArmenian → smArmenian
+    52: 23,  // langGeorgian → smGeorgian
+    53: 7,  // langMoldavian → smCyrillic
+    54: 7,  // langKirghiz → smCyrillic
+    55: 7,  // langTajiki → smCyrillic
+    56: 7,  // langTurkmen → smCyrillic
+    57: 27,  // langMongolian → smMongolian
+    58: 7,  // langMongolianCyr → smCyrillic
+    59: 4,  // langPashto → smArabic
+    60: 4,  // langKurdish → smArabic
+    61: 4,  // langKashmiri → smArabic
+    62: 4,  // langSindhi → smArabic
+    63: 26,  // langTibetan → smTibetan
+    64: 9,  // langNepali → smDevanagari
+    65: 9,  // langSanskrit → smDevanagari
+    66: 9,  // langMarathi → smDevanagari
+    67: 13,  // langBengali → smBengali
+    68: 13,  // langAssamese → smBengali
+    69: 11,  // langGujarati → smGujarati
+    70: 10,  // langPunjabi → smGurmukhi
+    71: 12,  // langOriya → smOriya
+    72: 17,  // langMalayalam → smMalayalam
+    73: 16,  // langKannada → smKannada
+    74: 14,  // langTamil → smTamil
+    75: 15,  // langTelugu → smTelugu
+    76: 18,  // langSinhalese → smSinhalese
+    77: 19,  // langBurmese → smBurmese
+    78: 20,  // langKhmer → smKhmer
+    79: 22,  // langLao → smLao
+    80: 30,  // langVietnamese → smVietnamese
+    81: 0,  // langIndonesian → smRoman
+    82: 0,  // langTagalog → smRoman
+    83: 0,  // langMalayRoman → smRoman
+    84: 4,  // langMalayArabic → smArabic
+    85: 28,  // langAmharic → smEthiopic
+    86: 28,  // langTigrinya → smEthiopic
+    87: 28,  // langOromo → smEthiopic
+    88: 0,  // langSomali → smRoman
+    89: 0,  // langSwahili → smRoman
+    90: 0,  // langKinyarwanda → smRoman
+    91: 0,  // langRundi → smRoman
+    92: 0,  // langNyanja → smRoman
+    93: 0,  // langMalagasy → smRoman
+    94: 0,  // langEsperanto → smRoman
+    128: 0,  // langWelsh → smRoman (modified)
+    129: 0,  // langBasque → smRoman
+    130: 0,  // langCatalan → smRoman
+    131: 0,  // langLatin → smRoman
+    132: 0,  // langQuechua → smRoman
+    133: 0,  // langGuarani → smRoman
+    134: 0,  // langAymara → smRoman
+    135: 7,  // langTatar → smCyrillic
+    136: 4,  // langUighur → smArabic
+    137: 26,  // langDzongkha → smTibetan
+    138: 0,  // langJavaneseRom → smRoman
+    139: 0,  // langSundaneseRom → smRoman
+    140: 0,  // langGalician → smRoman
+    141: 0,  // langAfrikaans → smRoman
+    142: 0,  // langBreton → smRoman (modified)
+    143: 28,  // langInuktitut → smEthiopic (modified)
+    144: 0,  // langScottishGaelic → smRoman (modified)
+    145: 0,  // langManxGaelic → smRoman (modified)
+    146: 0,  // langIrishGaelicScript → smRoman (modified)
+    147: 0,  // langTongan → smRoman
+    148: 6,  // langGreekAncient → smRoman
+    149: 0,  // langGreenlandic → smRoman
+    150: 0,  // langAzerbaijanRoman → smRoman
+    151: 0   // langNynorsk → smRoman
+};
+
+// While Microsoft indicates a region/country for all its language
+// IDs, we omit the region code if it's equal to the "most likely
+// region subtag" according to Unicode CLDR. For scripts, we omit
+// the subtag if it is equal to the Suppress-Script entry in the
+// IANA language subtag registry for IETF BCP 47.
+//
+// For example, Microsoft states that its language code 0x041A is
+// Croatian in Croatia. We transform this to the BCP 47 language code 'hr'
+// and not 'hr-HR' because Croatia is the default country for Croatian,
+// according to Unicode CLDR. As another example, Microsoft states
+// that 0x101A is Croatian (Latin) in Bosnia-Herzegovina. We transform
+// this to 'hr-BA' and not 'hr-Latn-BA' because Latin is the default script
+// for the Croatian language, according to IANA.
+//
+// http://www.unicode.org/cldr/charts/latest/supplemental/likely_subtags.html
+// http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
+var windowsLanguages = {
+    0x0436: 'af',
+    0x041C: 'sq',
+    0x0484: 'gsw',
+    0x045E: 'am',
+    0x1401: 'ar-DZ',
+    0x3C01: 'ar-BH',
+    0x0C01: 'ar',
+    0x0801: 'ar-IQ',
+    0x2C01: 'ar-JO',
+    0x3401: 'ar-KW',
+    0x3001: 'ar-LB',
+    0x1001: 'ar-LY',
+    0x1801: 'ary',
+    0x2001: 'ar-OM',
+    0x4001: 'ar-QA',
+    0x0401: 'ar-SA',
+    0x2801: 'ar-SY',
+    0x1C01: 'aeb',
+    0x3801: 'ar-AE',
+    0x2401: 'ar-YE',
+    0x042B: 'hy',
+    0x044D: 'as',
+    0x082C: 'az-Cyrl',
+    0x042C: 'az',
+    0x046D: 'ba',
+    0x042D: 'eu',
+    0x0423: 'be',
+    0x0845: 'bn',
+    0x0445: 'bn-IN',
+    0x201A: 'bs-Cyrl',
+    0x141A: 'bs',
+    0x047E: 'br',
+    0x0402: 'bg',
+    0x0403: 'ca',
+    0x0C04: 'zh-HK',
+    0x1404: 'zh-MO',
+    0x0804: 'zh',
+    0x1004: 'zh-SG',
+    0x0404: 'zh-TW',
+    0x0483: 'co',
+    0x041A: 'hr',
+    0x101A: 'hr-BA',
+    0x0405: 'cs',
+    0x0406: 'da',
+    0x048C: 'prs',
+    0x0465: 'dv',
+    0x0813: 'nl-BE',
+    0x0413: 'nl',
+    0x0C09: 'en-AU',
+    0x2809: 'en-BZ',
+    0x1009: 'en-CA',
+    0x2409: 'en-029',
+    0x4009: 'en-IN',
+    0x1809: 'en-IE',
+    0x2009: 'en-JM',
+    0x4409: 'en-MY',
+    0x1409: 'en-NZ',
+    0x3409: 'en-PH',
+    0x4809: 'en-SG',
+    0x1C09: 'en-ZA',
+    0x2C09: 'en-TT',
+    0x0809: 'en-GB',
+    0x0409: 'en',
+    0x3009: 'en-ZW',
+    0x0425: 'et',
+    0x0438: 'fo',
+    0x0464: 'fil',
+    0x040B: 'fi',
+    0x080C: 'fr-BE',
+    0x0C0C: 'fr-CA',
+    0x040C: 'fr',
+    0x140C: 'fr-LU',
+    0x180C: 'fr-MC',
+    0x100C: 'fr-CH',
+    0x0462: 'fy',
+    0x0456: 'gl',
+    0x0437: 'ka',
+    0x0C07: 'de-AT',
+    0x0407: 'de',
+    0x1407: 'de-LI',
+    0x1007: 'de-LU',
+    0x0807: 'de-CH',
+    0x0408: 'el',
+    0x046F: 'kl',
+    0x0447: 'gu',
+    0x0468: 'ha',
+    0x040D: 'he',
+    0x0439: 'hi',
+    0x040E: 'hu',
+    0x040F: 'is',
+    0x0470: 'ig',
+    0x0421: 'id',
+    0x045D: 'iu',
+    0x085D: 'iu-Latn',
+    0x083C: 'ga',
+    0x0434: 'xh',
+    0x0435: 'zu',
+    0x0410: 'it',
+    0x0810: 'it-CH',
+    0x0411: 'ja',
+    0x044B: 'kn',
+    0x043F: 'kk',
+    0x0453: 'km',
+    0x0486: 'quc',
+    0x0487: 'rw',
+    0x0441: 'sw',
+    0x0457: 'kok',
+    0x0412: 'ko',
+    0x0440: 'ky',
+    0x0454: 'lo',
+    0x0426: 'lv',
+    0x0427: 'lt',
+    0x082E: 'dsb',
+    0x046E: 'lb',
+    0x042F: 'mk',
+    0x083E: 'ms-BN',
+    0x043E: 'ms',
+    0x044C: 'ml',
+    0x043A: 'mt',
+    0x0481: 'mi',
+    0x047A: 'arn',
+    0x044E: 'mr',
+    0x047C: 'moh',
+    0x0450: 'mn',
+    0x0850: 'mn-CN',
+    0x0461: 'ne',
+    0x0414: 'nb',
+    0x0814: 'nn',
+    0x0482: 'oc',
+    0x0448: 'or',
+    0x0463: 'ps',
+    0x0415: 'pl',
+    0x0416: 'pt',
+    0x0816: 'pt-PT',
+    0x0446: 'pa',
+    0x046B: 'qu-BO',
+    0x086B: 'qu-EC',
+    0x0C6B: 'qu',
+    0x0418: 'ro',
+    0x0417: 'rm',
+    0x0419: 'ru',
+    0x243B: 'smn',
+    0x103B: 'smj-NO',
+    0x143B: 'smj',
+    0x0C3B: 'se-FI',
+    0x043B: 'se',
+    0x083B: 'se-SE',
+    0x203B: 'sms',
+    0x183B: 'sma-NO',
+    0x1C3B: 'sms',
+    0x044F: 'sa',
+    0x1C1A: 'sr-Cyrl-BA',
+    0x0C1A: 'sr',
+    0x181A: 'sr-Latn-BA',
+    0x081A: 'sr-Latn',
+    0x046C: 'nso',
+    0x0432: 'tn',
+    0x045B: 'si',
+    0x041B: 'sk',
+    0x0424: 'sl',
+    0x2C0A: 'es-AR',
+    0x400A: 'es-BO',
+    0x340A: 'es-CL',
+    0x240A: 'es-CO',
+    0x140A: 'es-CR',
+    0x1C0A: 'es-DO',
+    0x300A: 'es-EC',
+    0x440A: 'es-SV',
+    0x100A: 'es-GT',
+    0x480A: 'es-HN',
+    0x080A: 'es-MX',
+    0x4C0A: 'es-NI',
+    0x180A: 'es-PA',
+    0x3C0A: 'es-PY',
+    0x280A: 'es-PE',
+    0x500A: 'es-PR',
+
+    // Microsoft has defined two different language codes for
+    // “Spanish with modern sorting” and “Spanish with traditional
+    // sorting”. This makes sense for collation APIs, and it would be
+    // possible to express this in BCP 47 language tags via Unicode
+    // extensions (eg., es-u-co-trad is Spanish with traditional
+    // sorting). However, for storing names in fonts, the distinction
+    // does not make sense, so we give “es” in both cases.
+    0x0C0A: 'es',
+    0x040A: 'es',
+
+    0x540A: 'es-US',
+    0x380A: 'es-UY',
+    0x200A: 'es-VE',
+    0x081D: 'sv-FI',
+    0x041D: 'sv',
+    0x045A: 'syr',
+    0x0428: 'tg',
+    0x085F: 'tzm',
+    0x0449: 'ta',
+    0x0444: 'tt',
+    0x044A: 'te',
+    0x041E: 'th',
+    0x0451: 'bo',
+    0x041F: 'tr',
+    0x0442: 'tk',
+    0x0480: 'ug',
+    0x0422: 'uk',
+    0x042E: 'hsb',
+    0x0420: 'ur',
+    0x0843: 'uz-Cyrl',
+    0x0443: 'uz',
+    0x042A: 'vi',
+    0x0452: 'cy',
+    0x0488: 'wo',
+    0x0485: 'sah',
+    0x0478: 'ii',
+    0x046A: 'yo'
+};
+
+// Returns a IETF BCP 47 language code, for example 'zh-Hant'
+// for 'Chinese in the traditional script'.
+function getLanguageCode(platformID, languageID, ltag) {
+    switch (platformID) {
+        case 0:  // Unicode
+            if (languageID === 0xFFFF) {
+                return 'und';
+            } else if (ltag) {
+                return ltag[languageID];
+            }
+
+            break;
+
+        case 1:  // Macintosh
+            return macLanguages[languageID];
+
+        case 3:  // Windows
+            return windowsLanguages[languageID];
+    }
+
+    return undefined;
+}
+
+var utf16 = 'utf-16';
+
+// MacOS script ID → encoding. This table stores the default case,
+// which can be overridden by macLanguageEncodings.
+var macScriptEncodings = {
+    0: 'macintosh',           // smRoman
+    1: 'x-mac-japanese',      // smJapanese
+    2: 'x-mac-chinesetrad',   // smTradChinese
+    3: 'x-mac-korean',        // smKorean
+    6: 'x-mac-greek',         // smGreek
+    7: 'x-mac-cyrillic',      // smCyrillic
+    9: 'x-mac-devanagai',     // smDevanagari
+    10: 'x-mac-gurmukhi',     // smGurmukhi
+    11: 'x-mac-gujarati',     // smGujarati
+    12: 'x-mac-oriya',        // smOriya
+    13: 'x-mac-bengali',      // smBengali
+    14: 'x-mac-tamil',        // smTamil
+    15: 'x-mac-telugu',       // smTelugu
+    16: 'x-mac-kannada',      // smKannada
+    17: 'x-mac-malayalam',    // smMalayalam
+    18: 'x-mac-sinhalese',    // smSinhalese
+    19: 'x-mac-burmese',      // smBurmese
+    20: 'x-mac-khmer',        // smKhmer
+    21: 'x-mac-thai',         // smThai
+    22: 'x-mac-lao',          // smLao
+    23: 'x-mac-georgian',     // smGeorgian
+    24: 'x-mac-armenian',     // smArmenian
+    25: 'x-mac-chinesesimp',  // smSimpChinese
+    26: 'x-mac-tibetan',      // smTibetan
+    27: 'x-mac-mongolian',    // smMongolian
+    28: 'x-mac-ethiopic',     // smEthiopic
+    29: 'x-mac-ce',           // smCentralEuroRoman
+    30: 'x-mac-vietnamese',   // smVietnamese
+    31: 'x-mac-extarabic'     // smExtArabic
+};
+
+// MacOS language ID → encoding. This table stores the exceptional
+// cases, which override macScriptEncodings. For writing MacOS naming
+// tables, we need to emit a MacOS script ID. Therefore, we cannot
+// merge macScriptEncodings into macLanguageEncodings.
+//
+// http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt
+var macLanguageEncodings = {
+    15: 'x-mac-icelandic',    // langIcelandic
+    17: 'x-mac-turkish',      // langTurkish
+    18: 'x-mac-croatian',     // langCroatian
+    24: 'x-mac-ce',           // langLithuanian
+    25: 'x-mac-ce',           // langPolish
+    26: 'x-mac-ce',           // langHungarian
+    27: 'x-mac-ce',           // langEstonian
+    28: 'x-mac-ce',           // langLatvian
+    30: 'x-mac-icelandic',    // langFaroese
+    37: 'x-mac-romanian',     // langRomanian
+    38: 'x-mac-ce',           // langCzech
+    39: 'x-mac-ce',           // langSlovak
+    40: 'x-mac-ce',           // langSlovenian
+    143: 'x-mac-inuit',       // langInuktitut
+    146: 'x-mac-gaelic'       // langIrishGaelicScript
+};
+
+function getEncoding(platformID, encodingID, languageID) {
+    switch (platformID) {
+        case 0:  // Unicode
+            return utf16;
+
+        case 1:  // Apple Macintosh
+            return macLanguageEncodings[languageID] || macScriptEncodings[encodingID];
+
+        case 3:  // Microsoft Windows
+            if (encodingID === 1 || encodingID === 10) {
+                return utf16;
+            }
+
+            break;
+    }
+
+    return undefined;
+}
+
+// Parse the naming `name` table.
+// FIXME: Format 1 additional fields are not supported yet.
+// ltag is the content of the `ltag' table, such as ['en', 'zh-Hans', 'de-CH-1904'].
+function parseNameTable(data, start, ltag) {
+    var name = {};
+    var p = new parse.Parser(data, start);
+    var format = p.parseUShort();
+    var count = p.parseUShort();
+    var stringOffset = p.offset + p.parseUShort();
+    for (var i = 0; i < count; i++) {
+        var platformID = p.parseUShort();
+        var encodingID = p.parseUShort();
+        var languageID = p.parseUShort();
+        var nameID = p.parseUShort();
+        var property = nameTableNames[nameID] || nameID;
+        var byteLength = p.parseUShort();
+        var offset = p.parseUShort();
+        var language = getLanguageCode(platformID, languageID, ltag);
+        var encoding = getEncoding(platformID, encodingID, languageID);
+        if (encoding !== undefined && language !== undefined) {
+            var text = (void 0);
+            if (encoding === utf16) {
+                text = decode.UTF16(data, stringOffset + offset, byteLength);
+            } else {
+                text = decode.MACSTRING(data, stringOffset + offset, byteLength, encoding);
+            }
+
+            if (text) {
+                var translations = name[property];
+                if (translations === undefined) {
+                    translations = name[property] = {};
+                }
+
+                translations[language] = text;
+            }
+        }
+    }
+
+    var langTagCount = 0;
+    if (format === 1) {
+        // FIXME: Also handle Microsoft's 'name' table 1.
+        langTagCount = p.parseUShort();
+    }
+
+    return name;
+}
+
+// {23: 'foo'} → {'foo': 23}
+// ['bar', 'baz'] → {'bar': 0, 'baz': 1}
+function reverseDict(dict) {
+    var result = {};
+    for (var key in dict) {
+        result[dict[key]] = parseInt(key);
+    }
+
+    return result;
+}
+
+function makeNameRecord(platformID, encodingID, languageID, nameID, length, offset) {
+    return new table.Record('NameRecord', [
+        {name: 'platformID', type: 'USHORT', value: platformID},
+        {name: 'encodingID', type: 'USHORT', value: encodingID},
+        {name: 'languageID', type: 'USHORT', value: languageID},
+        {name: 'nameID', type: 'USHORT', value: nameID},
+        {name: 'length', type: 'USHORT', value: length},
+        {name: 'offset', type: 'USHORT', value: offset}
+    ]);
+}
+
+// Finds the position of needle in haystack, or -1 if not there.
+// Like String.indexOf(), but for arrays.
+function findSubArray(needle, haystack) {
+    var needleLength = needle.length;
+    var limit = haystack.length - needleLength + 1;
+
+    loop:
+    for (var pos = 0; pos < limit; pos++) {
+        for (; pos < limit; pos++) {
+            for (var k = 0; k < needleLength; k++) {
+                if (haystack[pos + k] !== needle[k]) {
+                    continue loop;
+                }
+            }
+
+            return pos;
+        }
+    }
+
+    return -1;
+}
+
+function addStringToPool(s, pool) {
+    var offset = findSubArray(s, pool);
+    if (offset < 0) {
+        offset = pool.length;
+        var i = 0;
+        var len = s.length;
+        for (; i < len; ++i) {
+            pool.push(s[i]);
+        }
+
+    }
+
+    return offset;
+}
+
+function makeNameTable(names, ltag) {
+    var nameID;
+    var nameIDs = [];
+
+    var namesWithNumericKeys = {};
+    var nameTableIds = reverseDict(nameTableNames);
+    for (var key in names) {
+        var id = nameTableIds[key];
+        if (id === undefined) {
+            id = key;
+        }
+
+        nameID = parseInt(id);
+
+        if (isNaN(nameID)) {
+            throw new Error('Name table entry "' + key + '" does not exist, see nameTableNames for complete list.');
+        }
+
+        namesWithNumericKeys[nameID] = names[key];
+        nameIDs.push(nameID);
+    }
+
+    var macLanguageIds = reverseDict(macLanguages);
+    var windowsLanguageIds = reverseDict(windowsLanguages);
+
+    var nameRecords = [];
+    var stringPool = [];
+
+    for (var i = 0; i < nameIDs.length; i++) {
+        nameID = nameIDs[i];
+        var translations = namesWithNumericKeys[nameID];
+        for (var lang in translations) {
+            var text = translations[lang];
+
+            // For MacOS, we try to emit the name in the form that was introduced
+            // in the initial version of the TrueType spec (in the late 1980s).
+            // However, this can fail for various reasons: the requested BCP 47
+            // language code might not have an old-style Mac equivalent;
+            // we might not have a codec for the needed character encoding;
+            // or the name might contain characters that cannot be expressed
+            // in the old-style Macintosh encoding. In case of failure, we emit
+            // the name in a more modern fashion (Unicode encoding with BCP 47
+            // language tags) that is recognized by MacOS 10.5, released in 2009.
+            // If fonts were only read by operating systems, we could simply
+            // emit all names in the modern form; this would be much easier.
+            // However, there are many applications and libraries that read
+            // 'name' tables directly, and these will usually only recognize
+            // the ancient form (silently skipping the unrecognized names).
+            var macPlatform = 1;  // Macintosh
+            var macLanguage = macLanguageIds[lang];
+            var macScript = macLanguageToScript[macLanguage];
+            var macEncoding = getEncoding(macPlatform, macScript, macLanguage);
+            var macName = encode.MACSTRING(text, macEncoding);
+            if (macName === undefined) {
+                macPlatform = 0;  // Unicode
+                macLanguage = ltag.indexOf(lang);
+                if (macLanguage < 0) {
+                    macLanguage = ltag.length;
+                    ltag.push(lang);
+                }
+
+                macScript = 4;  // Unicode 2.0 and later
+                macName = encode.UTF16(text);
+            }
+
+            var macNameOffset = addStringToPool(macName, stringPool);
+            nameRecords.push(makeNameRecord(macPlatform, macScript, macLanguage,
+                                            nameID, macName.length, macNameOffset));
+
+            var winLanguage = windowsLanguageIds[lang];
+            if (winLanguage !== undefined) {
+                var winName = encode.UTF16(text);
+                var winNameOffset = addStringToPool(winName, stringPool);
+                nameRecords.push(makeNameRecord(3, 1, winLanguage,
+                                                nameID, winName.length, winNameOffset));
+            }
+        }
+    }
+
+    nameRecords.sort(function(a, b) {
+        return ((a.platformID - b.platformID) ||
+                (a.encodingID - b.encodingID) ||
+                (a.languageID - b.languageID) ||
+                (a.nameID - b.nameID));
+    });
+
+    var t = new table.Table('name', [
+        {name: 'format', type: 'USHORT', value: 0},
+        {name: 'count', type: 'USHORT', value: nameRecords.length},
+        {name: 'stringOffset', type: 'USHORT', value: 6 + nameRecords.length * 12}
+    ]);
+
+    for (var r = 0; r < nameRecords.length; r++) {
+        t.fields.push({name: 'record_' + r, type: 'RECORD', value: nameRecords[r]});
+    }
+
+    t.fields.push({name: 'strings', type: 'LITERAL', value: stringPool});
+    return t;
+}
+
+var _name = { parse: parseNameTable, make: makeNameTable };
+
+// The `OS/2` table contains metrics required in OpenType fonts.
+
+var unicodeRanges = [
+    {begin: 0x0000, end: 0x007F}, // Basic Latin
+    {begin: 0x0080, end: 0x00FF}, // Latin-1 Supplement
+    {begin: 0x0100, end: 0x017F}, // Latin Extended-A
+    {begin: 0x0180, end: 0x024F}, // Latin Extended-B
+    {begin: 0x0250, end: 0x02AF}, // IPA Extensions
+    {begin: 0x02B0, end: 0x02FF}, // Spacing Modifier Letters
+    {begin: 0x0300, end: 0x036F}, // Combining Diacritical Marks
+    {begin: 0x0370, end: 0x03FF}, // Greek and Coptic
+    {begin: 0x2C80, end: 0x2CFF}, // Coptic
+    {begin: 0x0400, end: 0x04FF}, // Cyrillic
+    {begin: 0x0530, end: 0x058F}, // Armenian
+    {begin: 0x0590, end: 0x05FF}, // Hebrew
+    {begin: 0xA500, end: 0xA63F}, // Vai
+    {begin: 0x0600, end: 0x06FF}, // Arabic
+    {begin: 0x07C0, end: 0x07FF}, // NKo
+    {begin: 0x0900, end: 0x097F}, // Devanagari
+    {begin: 0x0980, end: 0x09FF}, // Bengali
+    {begin: 0x0A00, end: 0x0A7F}, // Gurmukhi
+    {begin: 0x0A80, end: 0x0AFF}, // Gujarati
+    {begin: 0x0B00, end: 0x0B7F}, // Oriya
+    {begin: 0x0B80, end: 0x0BFF}, // Tamil
+    {begin: 0x0C00, end: 0x0C7F}, // Telugu
+    {begin: 0x0C80, end: 0x0CFF}, // Kannada
+    {begin: 0x0D00, end: 0x0D7F}, // Malayalam
+    {begin: 0x0E00, end: 0x0E7F}, // Thai
+    {begin: 0x0E80, end: 0x0EFF}, // Lao
+    {begin: 0x10A0, end: 0x10FF}, // Georgian
+    {begin: 0x1B00, end: 0x1B7F}, // Balinese
+    {begin: 0x1100, end: 0x11FF}, // Hangul Jamo
+    {begin: 0x1E00, end: 0x1EFF}, // Latin Extended Additional
+    {begin: 0x1F00, end: 0x1FFF}, // Greek Extended
+    {begin: 0x2000, end: 0x206F}, // General Punctuation
+    {begin: 0x2070, end: 0x209F}, // Superscripts And Subscripts
+    {begin: 0x20A0, end: 0x20CF}, // Currency Symbol
+    {begin: 0x20D0, end: 0x20FF}, // Combining Diacritical Marks For Symbols
+    {begin: 0x2100, end: 0x214F}, // Letterlike Symbols
+    {begin: 0x2150, end: 0x218F}, // Number Forms
+    {begin: 0x2190, end: 0x21FF}, // Arrows
+    {begin: 0x2200, end: 0x22FF}, // Mathematical Operators
+    {begin: 0x2300, end: 0x23FF}, // Miscellaneous Technical
+    {begin: 0x2400, end: 0x243F}, // Control Pictures
+    {begin: 0x2440, end: 0x245F}, // Optical Character Recognition
+    {begin: 0x2460, end: 0x24FF}, // Enclosed Alphanumerics
+    {begin: 0x2500, end: 0x257F}, // Box Drawing
+    {begin: 0x2580, end: 0x259F}, // Block Elements
+    {begin: 0x25A0, end: 0x25FF}, // Geometric Shapes
+    {begin: 0x2600, end: 0x26FF}, // Miscellaneous Symbols
+    {begin: 0x2700, end: 0x27BF}, // Dingbats
+    {begin: 0x3000, end: 0x303F}, // CJK Symbols And Punctuation
+    {begin: 0x3040, end: 0x309F}, // Hiragana
+    {begin: 0x30A0, end: 0x30FF}, // Katakana
+    {begin: 0x3100, end: 0x312F}, // Bopomofo
+    {begin: 0x3130, end: 0x318F}, // Hangul Compatibility Jamo
+    {begin: 0xA840, end: 0xA87F}, // Phags-pa
+    {begin: 0x3200, end: 0x32FF}, // Enclosed CJK Letters And Months
+    {begin: 0x3300, end: 0x33FF}, // CJK Compatibility
+    {begin: 0xAC00, end: 0xD7AF}, // Hangul Syllables
+    {begin: 0xD800, end: 0xDFFF}, // Non-Plane 0 *
+    {begin: 0x10900, end: 0x1091F}, // Phoenicia
+    {begin: 0x4E00, end: 0x9FFF}, // CJK Unified Ideographs
+    {begin: 0xE000, end: 0xF8FF}, // Private Use Area (plane 0)
+    {begin: 0x31C0, end: 0x31EF}, // CJK Strokes
+    {begin: 0xFB00, end: 0xFB4F}, // Alphabetic Presentation Forms
+    {begin: 0xFB50, end: 0xFDFF}, // Arabic Presentation Forms-A
+    {begin: 0xFE20, end: 0xFE2F}, // Combining Half Marks
+    {begin: 0xFE10, end: 0xFE1F}, // Vertical Forms
+    {begin: 0xFE50, end: 0xFE6F}, // Small Form Variants
+    {begin: 0xFE70, end: 0xFEFF}, // Arabic Presentation Forms-B
+    {begin: 0xFF00, end: 0xFFEF}, // Halfwidth And Fullwidth Forms
+    {begin: 0xFFF0, end: 0xFFFF}, // Specials
+    {begin: 0x0F00, end: 0x0FFF}, // Tibetan
+    {begin: 0x0700, end: 0x074F}, // Syriac
+    {begin: 0x0780, end: 0x07BF}, // Thaana
+    {begin: 0x0D80, end: 0x0DFF}, // Sinhala
+    {begin: 0x1000, end: 0x109F}, // Myanmar
+    {begin: 0x1200, end: 0x137F}, // Ethiopic
+    {begin: 0x13A0, end: 0x13FF}, // Cherokee
+    {begin: 0x1400, end: 0x167F}, // Unified Canadian Aboriginal Syllabics
+    {begin: 0x1680, end: 0x169F}, // Ogham
+    {begin: 0x16A0, end: 0x16FF}, // Runic
+    {begin: 0x1780, end: 0x17FF}, // Khmer
+    {begin: 0x1800, end: 0x18AF}, // Mongolian
+    {begin: 0x2800, end: 0x28FF}, // Braille Patterns
+    {begin: 0xA000, end: 0xA48F}, // Yi Syllables
+    {begin: 0x1700, end: 0x171F}, // Tagalog
+    {begin: 0x10300, end: 0x1032F}, // Old Italic
+    {begin: 0x10330, end: 0x1034F}, // Gothic
+    {begin: 0x10400, end: 0x1044F}, // Deseret
+    {begin: 0x1D000, end: 0x1D0FF}, // Byzantine Musical Symbols
+    {begin: 0x1D400, end: 0x1D7FF}, // Mathematical Alphanumeric Symbols
+    {begin: 0xFF000, end: 0xFFFFD}, // Private Use (plane 15)
+    {begin: 0xFE00, end: 0xFE0F}, // Variation Selectors
+    {begin: 0xE0000, end: 0xE007F}, // Tags
+    {begin: 0x1900, end: 0x194F}, // Limbu
+    {begin: 0x1950, end: 0x197F}, // Tai Le
+    {begin: 0x1980, end: 0x19DF}, // New Tai Lue
+    {begin: 0x1A00, end: 0x1A1F}, // Buginese
+    {begin: 0x2C00, end: 0x2C5F}, // Glagolitic
+    {begin: 0x2D30, end: 0x2D7F}, // Tifinagh
+    {begin: 0x4DC0, end: 0x4DFF}, // Yijing Hexagram Symbols
+    {begin: 0xA800, end: 0xA82F}, // Syloti Nagri
+    {begin: 0x10000, end: 0x1007F}, // Linear B Syllabary
+    {begin: 0x10140, end: 0x1018F}, // Ancient Greek Numbers
+    {begin: 0x10380, end: 0x1039F}, // Ugaritic
+    {begin: 0x103A0, end: 0x103DF}, // Old Persian
+    {begin: 0x10450, end: 0x1047F}, // Shavian
+    {begin: 0x10480, end: 0x104AF}, // Osmanya
+    {begin: 0x10800, end: 0x1083F}, // Cypriot Syllabary
+    {begin: 0x10A00, end: 0x10A5F}, // Kharoshthi
+    {begin: 0x1D300, end: 0x1D35F}, // Tai Xuan Jing Symbols
+    {begin: 0x12000, end: 0x123FF}, // Cuneiform
+    {begin: 0x1D360, end: 0x1D37F}, // Counting Rod Numerals
+    {begin: 0x1B80, end: 0x1BBF}, // Sundanese
+    {begin: 0x1C00, end: 0x1C4F}, // Lepcha
+    {begin: 0x1C50, end: 0x1C7F}, // Ol Chiki
+    {begin: 0xA880, end: 0xA8DF}, // Saurashtra
+    {begin: 0xA900, end: 0xA92F}, // Kayah Li
+    {begin: 0xA930, end: 0xA95F}, // Rejang
+    {begin: 0xAA00, end: 0xAA5F}, // Cham
+    {begin: 0x10190, end: 0x101CF}, // Ancient Symbols
+    {begin: 0x101D0, end: 0x101FF}, // Phaistos Disc
+    {begin: 0x102A0, end: 0x102DF}, // Carian
+    {begin: 0x1F030, end: 0x1F09F}  // Domino Tiles
+];
+
+function getUnicodeRange(unicode) {
+    for (var i = 0; i < unicodeRanges.length; i += 1) {
+        var range = unicodeRanges[i];
+        if (unicode >= range.begin && unicode < range.end) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+// Parse the OS/2 and Windows metrics `OS/2` table
+function parseOS2Table(data, start) {
+    var os2 = {};
+    var p = new parse.Parser(data, start);
+    os2.version = p.parseUShort();
+    os2.xAvgCharWidth = p.parseShort();
+    os2.usWeightClass = p.parseUShort();
+    os2.usWidthClass = p.parseUShort();
+    os2.fsType = p.parseUShort();
+    os2.ySubscriptXSize = p.parseShort();
+    os2.ySubscriptYSize = p.parseShort();
+    os2.ySubscriptXOffset = p.parseShort();
+    os2.ySubscriptYOffset = p.parseShort();
+    os2.ySuperscriptXSize = p.parseShort();
+    os2.ySuperscriptYSize = p.parseShort();
+    os2.ySuperscriptXOffset = p.parseShort();
+    os2.ySuperscriptYOffset = p.parseShort();
+    os2.yStrikeoutSize = p.parseShort();
+    os2.yStrikeoutPosition = p.parseShort();
+    os2.sFamilyClass = p.parseShort();
+    os2.panose = [];
+    for (var i = 0; i < 10; i++) {
+        os2.panose[i] = p.parseByte();
+    }
+
+    os2.ulUnicodeRange1 = p.parseULong();
+    os2.ulUnicodeRange2 = p.parseULong();
+    os2.ulUnicodeRange3 = p.parseULong();
+    os2.ulUnicodeRange4 = p.parseULong();
+    os2.achVendID = String.fromCharCode(p.parseByte(), p.parseByte(), p.parseByte(), p.parseByte());
+    os2.fsSelection = p.parseUShort();
+    os2.usFirstCharIndex = p.parseUShort();
+    os2.usLastCharIndex = p.parseUShort();
+    os2.sTypoAscender = p.parseShort();
+    os2.sTypoDescender = p.parseShort();
+    os2.sTypoLineGap = p.parseShort();
+    os2.usWinAscent = p.parseUShort();
+    os2.usWinDescent = p.parseUShort();
+    if (os2.version >= 1) {
+        os2.ulCodePageRange1 = p.parseULong();
+        os2.ulCodePageRange2 = p.parseULong();
+    }
+
+    if (os2.version >= 2) {
+        os2.sxHeight = p.parseShort();
+        os2.sCapHeight = p.parseShort();
+        os2.usDefaultChar = p.parseUShort();
+        os2.usBreakChar = p.parseUShort();
+        os2.usMaxContent = p.parseUShort();
+    }
+
+    return os2;
+}
+
+function makeOS2Table(options) {
+    return new table.Table('OS/2', [
+        {name: 'version', type: 'USHORT', value: 0x0003},
+        {name: 'xAvgCharWidth', type: 'SHORT', value: 0},
+        {name: 'usWeightClass', type: 'USHORT', value: 0},
+        {name: 'usWidthClass', type: 'USHORT', value: 0},
+        {name: 'fsType', type: 'USHORT', value: 0},
+        {name: 'ySubscriptXSize', type: 'SHORT', value: 650},
+        {name: 'ySubscriptYSize', type: 'SHORT', value: 699},
+        {name: 'ySubscriptXOffset', type: 'SHORT', value: 0},
+        {name: 'ySubscriptYOffset', type: 'SHORT', value: 140},
+        {name: 'ySuperscriptXSize', type: 'SHORT', value: 650},
+        {name: 'ySuperscriptYSize', type: 'SHORT', value: 699},
+        {name: 'ySuperscriptXOffset', type: 'SHORT', value: 0},
+        {name: 'ySuperscriptYOffset', type: 'SHORT', value: 479},
+        {name: 'yStrikeoutSize', type: 'SHORT', value: 49},
+        {name: 'yStrikeoutPosition', type: 'SHORT', value: 258},
+        {name: 'sFamilyClass', type: 'SHORT', value: 0},
+        {name: 'bFamilyType', type: 'BYTE', value: 0},
+        {name: 'bSerifStyle', type: 'BYTE', value: 0},
+        {name: 'bWeight', type: 'BYTE', value: 0},
+        {name: 'bProportion', type: 'BYTE', value: 0},
+        {name: 'bContrast', type: 'BYTE', value: 0},
+        {name: 'bStrokeVariation', type: 'BYTE', value: 0},
+        {name: 'bArmStyle', type: 'BYTE', value: 0},
+        {name: 'bLetterform', type: 'BYTE', value: 0},
+        {name: 'bMidline', type: 'BYTE', value: 0},
+        {name: 'bXHeight', type: 'BYTE', value: 0},
+        {name: 'ulUnicodeRange1', type: 'ULONG', value: 0},
+        {name: 'ulUnicodeRange2', type: 'ULONG', value: 0},
+        {name: 'ulUnicodeRange3', type: 'ULONG', value: 0},
+        {name: 'ulUnicodeRange4', type: 'ULONG', value: 0},
+        {name: 'achVendID', type: 'CHARARRAY', value: 'XXXX'},
+        {name: 'fsSelection', type: 'USHORT', value: 0},
+        {name: 'usFirstCharIndex', type: 'USHORT', value: 0},
+        {name: 'usLastCharIndex', type: 'USHORT', value: 0},
+        {name: 'sTypoAscender', type: 'SHORT', value: 0},
+        {name: 'sTypoDescender', type: 'SHORT', value: 0},
+        {name: 'sTypoLineGap', type: 'SHORT', value: 0},
+        {name: 'usWinAscent', type: 'USHORT', value: 0},
+        {name: 'usWinDescent', type: 'USHORT', value: 0},
+        {name: 'ulCodePageRange1', type: 'ULONG', value: 0},
+        {name: 'ulCodePageRange2', type: 'ULONG', value: 0},
+        {name: 'sxHeight', type: 'SHORT', value: 0},
+        {name: 'sCapHeight', type: 'SHORT', value: 0},
+        {name: 'usDefaultChar', type: 'USHORT', value: 0},
+        {name: 'usBreakChar', type: 'USHORT', value: 0},
+        {name: 'usMaxContext', type: 'USHORT', value: 0}
+    ], options);
+}
+
+var os2 = { parse: parseOS2Table, make: makeOS2Table, unicodeRanges: unicodeRanges, getUnicodeRange: getUnicodeRange };
+
+// The `post` table stores additional PostScript information, such as glyph names.
+
+// Parse the PostScript `post` table
+function parsePostTable(data, start) {
+    var post = {};
+    var p = new parse.Parser(data, start);
+    post.version = p.parseVersion();
+    post.italicAngle = p.parseFixed();
+    post.underlinePosition = p.parseShort();
+    post.underlineThickness = p.parseShort();
+    post.isFixedPitch = p.parseULong();
+    post.minMemType42 = p.parseULong();
+    post.maxMemType42 = p.parseULong();
+    post.minMemType1 = p.parseULong();
+    post.maxMemType1 = p.parseULong();
+    switch (post.version) {
+        case 1:
+            post.names = standardNames.slice();
+            break;
+        case 2:
+            post.numberOfGlyphs = p.parseUShort();
+            post.glyphNameIndex = new Array(post.numberOfGlyphs);
+            for (var i = 0; i < post.numberOfGlyphs; i++) {
+                post.glyphNameIndex[i] = p.parseUShort();
+            }
+
+            post.names = [];
+            for (var i$1 = 0; i$1 < post.numberOfGlyphs; i$1++) {
+                if (post.glyphNameIndex[i$1] >= standardNames.length) {
+                    var nameLength = p.parseChar();
+                    post.names.push(p.parseString(nameLength));
+                }
+            }
+
+            break;
+        case 2.5:
+            post.numberOfGlyphs = p.parseUShort();
+            post.offset = new Array(post.numberOfGlyphs);
+            for (var i$2 = 0; i$2 < post.numberOfGlyphs; i$2++) {
+                post.offset[i$2] = p.parseChar();
+            }
+
+            break;
+    }
+    return post;
+}
+
+function makePostTable() {
+    return new table.Table('post', [
+        {name: 'version', type: 'FIXED', value: 0x00030000},
+        {name: 'italicAngle', type: 'FIXED', value: 0},
+        {name: 'underlinePosition', type: 'FWORD', value: 0},
+        {name: 'underlineThickness', type: 'FWORD', value: 0},
+        {name: 'isFixedPitch', type: 'ULONG', value: 0},
+        {name: 'minMemType42', type: 'ULONG', value: 0},
+        {name: 'maxMemType42', type: 'ULONG', value: 0},
+        {name: 'minMemType1', type: 'ULONG', value: 0},
+        {name: 'maxMemType1', type: 'ULONG', value: 0}
+    ]);
+}
+
+var post = { parse: parsePostTable, make: makePostTable };
+
+// The `GSUB` table contains ligatures, among other things.
+
+var subtableParsers = new Array(9);         // subtableParsers[0] is unused
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#SS
+subtableParsers[1] = function parseLookup1() {
+    var start = this.offset + this.relativeOffset;
+    var substFormat = this.parseUShort();
+    if (substFormat === 1) {
+        return {
+            substFormat: 1,
+            coverage: this.parsePointer(Parser.coverage),
+            deltaGlyphId: this.parseUShort()
+        };
+    } else if (substFormat === 2) {
+        return {
+            substFormat: 2,
+            coverage: this.parsePointer(Parser.coverage),
+            substitute: this.parseOffset16List()
+        };
+    }
+    check.assert(false, '0x' + start.toString(16) + ': lookup type 1 format must be 1 or 2.');
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#MS
+subtableParsers[2] = function parseLookup2() {
+    var substFormat = this.parseUShort();
+    check.argument(substFormat === 1, 'GSUB Multiple Substitution Subtable identifier-format must be 1');
+    return {
+        substFormat: substFormat,
+        coverage: this.parsePointer(Parser.coverage),
+        sequences: this.parseListOfLists()
+    };
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#AS
+subtableParsers[3] = function parseLookup3() {
+    var substFormat = this.parseUShort();
+    check.argument(substFormat === 1, 'GSUB Alternate Substitution Subtable identifier-format must be 1');
+    return {
+        substFormat: substFormat,
+        coverage: this.parsePointer(Parser.coverage),
+        alternateSets: this.parseListOfLists()
+    };
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#LS
+subtableParsers[4] = function parseLookup4() {
+    var substFormat = this.parseUShort();
+    check.argument(substFormat === 1, 'GSUB ligature table identifier-format must be 1');
+    return {
+        substFormat: substFormat,
+        coverage: this.parsePointer(Parser.coverage),
+        ligatureSets: this.parseListOfLists(function() {
+            return {
+                ligGlyph: this.parseUShort(),
+                components: this.parseUShortList(this.parseUShort() - 1)
+            };
+        })
+    };
+};
+
+var lookupRecordDesc = {
+    sequenceIndex: Parser.uShort,
+    lookupListIndex: Parser.uShort
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CSF
+subtableParsers[5] = function parseLookup5() {
+    var start = this.offset + this.relativeOffset;
+    var substFormat = this.parseUShort();
+
+    if (substFormat === 1) {
+        return {
+            substFormat: substFormat,
+            coverage: this.parsePointer(Parser.coverage),
+            ruleSets: this.parseListOfLists(function() {
+                var glyphCount = this.parseUShort();
+                var substCount = this.parseUShort();
+                return {
+                    input: this.parseUShortList(glyphCount - 1),
+                    lookupRecords: this.parseRecordList(substCount, lookupRecordDesc)
+                };
+            })
+        };
+    } else if (substFormat === 2) {
+        return {
+            substFormat: substFormat,
+            coverage: this.parsePointer(Parser.coverage),
+            classDef: this.parsePointer(Parser.classDef),
+            classSets: this.parseListOfLists(function() {
+                var glyphCount = this.parseUShort();
+                var substCount = this.parseUShort();
+                return {
+                    classes: this.parseUShortList(glyphCount - 1),
+                    lookupRecords: this.parseRecordList(substCount, lookupRecordDesc)
+                };
+            })
+        };
+    } else if (substFormat === 3) {
+        var glyphCount = this.parseUShort();
+        var substCount = this.parseUShort();
+        return {
+            substFormat: substFormat,
+            coverages: this.parseList(glyphCount, Parser.pointer(Parser.coverage)),
+            lookupRecords: this.parseRecordList(substCount, lookupRecordDesc)
+        };
+    }
+    check.assert(false, '0x' + start.toString(16) + ': lookup type 5 format must be 1, 2 or 3.');
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CC
+subtableParsers[6] = function parseLookup6() {
+    var start = this.offset + this.relativeOffset;
+    var substFormat = this.parseUShort();
+    if (substFormat === 1) {
+        return {
+            substFormat: 1,
+            coverage: this.parsePointer(Parser.coverage),
+            chainRuleSets: this.parseListOfLists(function() {
+                return {
+                    backtrack: this.parseUShortList(),
+                    input: this.parseUShortList(this.parseShort() - 1),
+                    lookahead: this.parseUShortList(),
+                    lookupRecords: this.parseRecordList(lookupRecordDesc)
+                };
+            })
+        };
+    } else if (substFormat === 2) {
+        return {
+            substFormat: 2,
+            coverage: this.parsePointer(Parser.coverage),
+            backtrackClassDef: this.parsePointer(Parser.classDef),
+            inputClassDef: this.parsePointer(Parser.classDef),
+            lookaheadClassDef: this.parsePointer(Parser.classDef),
+            chainClassSet: this.parseListOfLists(function() {
+                return {
+                    backtrack: this.parseUShortList(),
+                    input: this.parseUShortList(this.parseShort() - 1),
+                    lookahead: this.parseUShortList(),
+                    lookupRecords: this.parseRecordList(lookupRecordDesc)
+                };
+            })
+        };
+    } else if (substFormat === 3) {
+        return {
+            substFormat: 3,
+            backtrackCoverage: this.parseList(Parser.pointer(Parser.coverage)),
+            inputCoverage: this.parseList(Parser.pointer(Parser.coverage)),
+            lookaheadCoverage: this.parseList(Parser.pointer(Parser.coverage)),
+            lookupRecords: this.parseRecordList(lookupRecordDesc)
+        };
+    }
+    check.assert(false, '0x' + start.toString(16) + ': lookup type 6 format must be 1, 2 or 3.');
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#ES
+subtableParsers[7] = function parseLookup7() {
+    // Extension Substitution subtable
+    var substFormat = this.parseUShort();
+    check.argument(substFormat === 1, 'GSUB Extension Substitution subtable identifier-format must be 1');
+    var extensionLookupType = this.parseUShort();
+    var extensionParser = new Parser(this.data, this.offset + this.parseULong());
+    return {
+        substFormat: 1,
+        lookupType: extensionLookupType,
+        extension: subtableParsers[extensionLookupType].call(extensionParser)
+    };
+};
+
+// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#RCCS
+subtableParsers[8] = function parseLookup8() {
+    var substFormat = this.parseUShort();
+    check.argument(substFormat === 1, 'GSUB Reverse Chaining Contextual Single Substitution Subtable identifier-format must be 1');
+    return {
+        substFormat: substFormat,
+        coverage: this.parsePointer(Parser.coverage),
+        backtrackCoverage: this.parseList(Parser.pointer(Parser.coverage)),
+        lookaheadCoverage: this.parseList(Parser.pointer(Parser.coverage)),
+        substitutes: this.parseUShortList()
+    };
+};
+
+// https://www.microsoft.com/typography/OTSPEC/gsub.htm
+function parseGsubTable(data, start) {
+    start = start || 0;
+    var p = new Parser(data, start);
+    var tableVersion = p.parseVersion(1);
+    check.argument(tableVersion === 1 || tableVersion === 1.1, 'Unsupported GSUB table version.');
+    if (tableVersion === 1) {
+        return {
+            version: tableVersion,
+            scripts: p.parseScriptList(),
+            features: p.parseFeatureList(),
+            lookups: p.parseLookupList(subtableParsers)
+        };
+    } else {
+        return {
+            version: tableVersion,
+            scripts: p.parseScriptList(),
+            features: p.parseFeatureList(),
+            lookups: p.parseLookupList(subtableParsers),
+            variations: p.parseFeatureVariationsList()
+        };
+    }
+
+}
+
+// GSUB Writing //////////////////////////////////////////////
+var subtableMakers = new Array(9);
+
+subtableMakers[1] = function makeLookup1(subtable) {
+    if (subtable.substFormat === 1) {
+        return new table.Table('substitutionTable', [
+            {name: 'substFormat', type: 'USHORT', value: 1},
+            {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)},
+            {name: 'deltaGlyphID', type: 'USHORT', value: subtable.deltaGlyphId}
+        ]);
+    } else {
+        return new table.Table('substitutionTable', [
+            {name: 'substFormat', type: 'USHORT', value: 2},
+            {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
+        ].concat(table.ushortList('substitute', subtable.substitute)));
+    }
+};
+
+subtableMakers[2] = function makeLookup2(subtable) {
+    check.assert(subtable.substFormat === 1, 'Lookup type 2 substFormat must be 1.');
+    return new table.Table('substitutionTable', [
+        {name: 'substFormat', type: 'USHORT', value: 1},
+        {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
+    ].concat(table.tableList('seqSet', subtable.sequences, function(sequenceSet) {
+        return new table.Table('sequenceSetTable', table.ushortList('sequence', sequenceSet));
+    })));
+};
+
+subtableMakers[3] = function makeLookup3(subtable) {
+    check.assert(subtable.substFormat === 1, 'Lookup type 3 substFormat must be 1.');
+    return new table.Table('substitutionTable', [
+        {name: 'substFormat', type: 'USHORT', value: 1},
+        {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
+    ].concat(table.tableList('altSet', subtable.alternateSets, function(alternateSet) {
+        return new table.Table('alternateSetTable', table.ushortList('alternate', alternateSet));
+    })));
+};
+
+subtableMakers[4] = function makeLookup4(subtable) {
+    check.assert(subtable.substFormat === 1, 'Lookup type 4 substFormat must be 1.');
+    return new table.Table('substitutionTable', [
+        {name: 'substFormat', type: 'USHORT', value: 1},
+        {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
+    ].concat(table.tableList('ligSet', subtable.ligatureSets, function(ligatureSet) {
+        return new table.Table('ligatureSetTable', table.tableList('ligature', ligatureSet, function(ligature) {
+            return new table.Table('ligatureTable',
+                [{name: 'ligGlyph', type: 'USHORT', value: ligature.ligGlyph}]
+                .concat(table.ushortList('component', ligature.components, ligature.components.length + 1))
+            );
+        }));
+    })));
+};
+
+subtableMakers[6] = function makeLookup6(subtable) {
+    if (subtable.substFormat === 1) {
+        var returnTable = new table.Table('chainContextTable', [
+            {name: 'substFormat', type: 'USHORT', value: subtable.substFormat},
+            {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
+        ].concat(table.tableList('chainRuleSet', subtable.chainRuleSets, function(chainRuleSet) {
+            return new table.Table('chainRuleSetTable', table.tableList('chainRule', chainRuleSet, function(chainRule) {
+                var tableData = table.ushortList('backtrackGlyph', chainRule.backtrack, chainRule.backtrack.length)
+                    .concat(table.ushortList('inputGlyph', chainRule.input, chainRule.input.length + 1))
+                    .concat(table.ushortList('lookaheadGlyph', chainRule.lookahead, chainRule.lookahead.length))
+                    .concat(table.ushortList('substitution', [], chainRule.lookupRecords.length));
+
+                chainRule.lookupRecords.forEach(function (record, i) {
+                    tableData = tableData
+                        .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex})
+                        .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex});
+                });
+                return new table.Table('chainRuleTable', tableData);
+            }));
+        })));
+        return returnTable;
+    } else if (subtable.substFormat === 2) {
+        check.assert(false, 'lookup type 6 format 2 is not yet supported.');
+    } else if (subtable.substFormat === 3) {
+        var tableData = [
+            {name: 'substFormat', type: 'USHORT', value: subtable.substFormat} ];
+
+        tableData.push({name: 'backtrackGlyphCount', type: 'USHORT', value: subtable.backtrackCoverage.length});
+        subtable.backtrackCoverage.forEach(function (coverage, i) {
+            tableData.push({name: 'backtrackCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)});
+        });
+        tableData.push({name: 'inputGlyphCount', type: 'USHORT', value: subtable.inputCoverage.length});
+        subtable.inputCoverage.forEach(function (coverage, i) {
+            tableData.push({name: 'inputCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)});
+        });
+        tableData.push({name: 'lookaheadGlyphCount', type: 'USHORT', value: subtable.lookaheadCoverage.length});
+        subtable.lookaheadCoverage.forEach(function (coverage, i) {
+            tableData.push({name: 'lookaheadCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)});
+        });
+
+        tableData.push({name: 'substitutionCount', type: 'USHORT', value: subtable.lookupRecords.length});
+        subtable.lookupRecords.forEach(function (record, i) {
+            tableData = tableData
+                .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex})
+                .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex});
+        });
+
+        var returnTable$1 = new table.Table('chainContextTable', tableData);
+
+        return returnTable$1;
+    }
+
+    check.assert(false, 'lookup type 6 format must be 1, 2 or 3.');
+};
+
+function makeGsubTable(gsub) {
+    return new table.Table('GSUB', [
+        {name: 'version', type: 'ULONG', value: 0x10000},
+        {name: 'scripts', type: 'TABLE', value: new table.ScriptList(gsub.scripts)},
+        {name: 'features', type: 'TABLE', value: new table.FeatureList(gsub.features)},
+        {name: 'lookups', type: 'TABLE', value: new table.LookupList(gsub.lookups, subtableMakers)}
+    ]);
+}
+
+var gsub = { parse: parseGsubTable, make: makeGsubTable };
+
+// The `GPOS` table contains kerning pairs, among other things.
+
+// Parse the metadata `meta` table.
+// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6meta.html
+function parseMetaTable(data, start) {
+    var p = new parse.Parser(data, start);
+    var tableVersion = p.parseULong();
+    check.argument(tableVersion === 1, 'Unsupported META table version.');
+    p.parseULong(); // flags - currently unused and set to 0
+    p.parseULong(); // tableOffset
+    var numDataMaps = p.parseULong();
+
+    var tags = {};
+    for (var i = 0; i < numDataMaps; i++) {
+        var tag = p.parseTag();
+        var dataOffset = p.parseULong();
+        var dataLength = p.parseULong();
+        var text = decode.UTF8(data, start + dataOffset, dataLength);
+
+        tags[tag] = text;
+    }
+    return tags;
+}
+
+function makeMetaTable(tags) {
+    var numTags = Object.keys(tags).length;
+    var stringPool = '';
+    var stringPoolOffset = 16 + numTags * 12;
+
+    var result = new table.Table('meta', [
+        {name: 'version', type: 'ULONG', value: 1},
+        {name: 'flags', type: 'ULONG', value: 0},
+        {name: 'offset', type: 'ULONG', value: stringPoolOffset},
+        {name: 'numTags', type: 'ULONG', value: numTags}
+    ]);
+
+    for (var tag in tags) {
+        var pos = stringPool.length;
+        stringPool += tags[tag];
+
+        result.fields.push({name: 'tag ' + tag, type: 'TAG', value: tag});
+        result.fields.push({name: 'offset ' + tag, type: 'ULONG', value: stringPoolOffset + pos});
+        result.fields.push({name: 'length ' + tag, type: 'ULONG', value: tags[tag].length});
+    }
+
+    result.fields.push({name: 'stringPool', type: 'CHARARRAY', value: stringPool});
+
+    return result;
+}
+
+var meta = { parse: parseMetaTable, make: makeMetaTable };
+
+// The `COLR` table adds support for multi-colored glyphs
+
+function parseColrTable(data, start) {
+    var p = new Parser(data, start);
+    var version = p.parseUShort();
+    check.argument(version === 0x0000, 'Only COLRv0 supported.');
+    var numBaseGlyphRecords = p.parseUShort();
+    var baseGlyphRecordsOffset = p.parseOffset32();
+    var layerRecordsOffset = p.parseOffset32();
+    var numLayerRecords = p.parseUShort();
+    p.relativeOffset = baseGlyphRecordsOffset;
+    var baseGlyphRecords = p.parseRecordList(numBaseGlyphRecords, {
+        glyphID: Parser.uShort,
+        firstLayerIndex: Parser.uShort,
+        numLayers: Parser.uShort,
+    });
+    p.relativeOffset = layerRecordsOffset;
+    var layerRecords = p.parseRecordList(numLayerRecords, {
+        glyphID: Parser.uShort,
+        paletteIndex: Parser.uShort
+    });
+
+    return {
+        version: version,
+        baseGlyphRecords: baseGlyphRecords,
+        layerRecords: layerRecords,
+    };
+}
+
+function makeColrTable(ref) {
+    var version = ref.version; if ( version === void 0 ) version = 0x0000;
+    var baseGlyphRecords = ref.baseGlyphRecords; if ( baseGlyphRecords === void 0 ) baseGlyphRecords = [];
+    var layerRecords = ref.layerRecords; if ( layerRecords === void 0 ) layerRecords = [];
+
+    check.argument(version === 0x0000, 'Only COLRv0 supported.');
+    var baseGlyphRecordsOffset = 14;
+    var layerRecordsOffset = baseGlyphRecordsOffset + (baseGlyphRecords.length * 6);
+    return new table.Table('COLR', [
+        { name: 'version', type: 'USHORT', value: version },
+        { name: 'numBaseGlyphRecords', type: 'USHORT', value: baseGlyphRecords.length },
+        { name: 'baseGlyphRecordsOffset', type: 'ULONG', value: baseGlyphRecordsOffset },
+        { name: 'layerRecordsOffset', type: 'ULONG', value: layerRecordsOffset },
+        { name: 'numLayerRecords', type: 'USHORT', value: layerRecords.length } ].concat( baseGlyphRecords.map(function (glyph, i) { return [
+            { name: 'glyphID_' + i, type: 'USHORT', value: glyph.glyphID },
+            { name: 'firstLayerIndex_' + i, type: 'USHORT', value: glyph.firstLayerIndex },
+            { name: 'numLayers_' + i, type: 'USHORT', value: glyph.numLayers } ]; }).flat(),
+        layerRecords.map(function (layer, i) { return [
+            { name: 'LayerGlyphID_' + i, type: 'USHORT', value: layer.glyphID },
+            { name: 'paletteIndex_' + i, type: 'USHORT', value: layer.paletteIndex } ]; }).flat() ));
+}
+
+var colr = { parse: parseColrTable, make: makeColrTable };
+
+// The `CPAL` define a contiguous list of colors (colorRecords)
+
+// Parse the header `head` table
+function parseCpalTable(data, start) {
+  var p = new Parser(data, start);
+  var version = p.parseShort();
+  var numPaletteEntries = p.parseShort();
+  var numPalettes = p.parseShort();
+  var numColorRecords = p.parseShort();
+  var colorRecordsArrayOffset = p.parseOffset32();
+  var colorRecordIndices = p.parseUShortList(numPalettes);
+  p.relativeOffset = colorRecordsArrayOffset;
+  var colorRecords = p.parseULongList(numColorRecords);
+  return {
+    version: version,
+    numPaletteEntries: numPaletteEntries,
+    colorRecords: colorRecords,
+    colorRecordIndices: colorRecordIndices,
+  };
+}
+
+function makeCpalTable(ref) {
+  var version = ref.version; if ( version === void 0 ) version = 0;
+  var numPaletteEntries = ref.numPaletteEntries; if ( numPaletteEntries === void 0 ) numPaletteEntries = 0;
+  var colorRecords = ref.colorRecords; if ( colorRecords === void 0 ) colorRecords = [];
+  var colorRecordIndices = ref.colorRecordIndices; if ( colorRecordIndices === void 0 ) colorRecordIndices = [0];
+
+  check.argument(version === 0, 'Only CPALv0 are supported.');
+  check.argument(colorRecords.length, 'No colorRecords given.');
+  check.argument(colorRecordIndices.length, 'No colorRecordIndices given.');
+  check.argument(!numPaletteEntries && colorRecordIndices.length == 1, 'Can\'t infer numPaletteEntries on multiple colorRecordIndices');
+  return new table.Table('CPAL', [
+    { name: 'version', type: 'USHORT', value: version },
+    { name: 'numPaletteEntries', type: 'USHORT', value: numPaletteEntries || colorRecords.length },
+    { name: 'numPalettes', type: 'USHORT', value: colorRecordIndices.length },
+    { name: 'numColorRecords', type: 'USHORT', value: colorRecords.length },
+    { name: 'colorRecordsArrayOffset', type: 'ULONG', value: 12 + 2 * colorRecordIndices.length } ].concat( colorRecordIndices.map(function (palette, i) { return ({ name: 'colorRecordIndices_' + i, type: 'USHORT', value: palette }); }),
+    colorRecords.map(function (color, i) { return ({ name: 'colorRecords_' + i, type: 'ULONG', value: color }); }) ));
+}
+
+var cpal = { parse: parseCpalTable, make: makeCpalTable };
+
+// The `sfnt` wrapper provides organization for the tables in the font.
+
+function log2(v) {
+    return Math.log(v) / Math.log(2) | 0;
+}
+
+function computeCheckSum(bytes) {
+    while (bytes.length % 4 !== 0) {
+        bytes.push(0);
+    }
+
+    var sum = 0;
+    for (var i = 0; i < bytes.length; i += 4) {
+        sum += (bytes[i] << 24) +
+            (bytes[i + 1] << 16) +
+            (bytes[i + 2] << 8) +
+            (bytes[i + 3]);
+    }
+
+    sum %= Math.pow(2, 32);
+    return sum;
+}
+
+function makeTableRecord(tag, checkSum, offset, length) {
+    return new table.Record('Table Record', [
+        {name: 'tag', type: 'TAG', value: tag !== undefined ? tag : ''},
+        {name: 'checkSum', type: 'ULONG', value: checkSum !== undefined ? checkSum : 0},
+        {name: 'offset', type: 'ULONG', value: offset !== undefined ? offset : 0},
+        {name: 'length', type: 'ULONG', value: length !== undefined ? length : 0}
+    ]);
+}
+
+function makeSfntTable(tables) {
+    var sfnt = new table.Table('sfnt', [
+        {name: 'version', type: 'TAG', value: 'OTTO'},
+        {name: 'numTables', type: 'USHORT', value: 0},
+        {name: 'searchRange', type: 'USHORT', value: 0},
+        {name: 'entrySelector', type: 'USHORT', value: 0},
+        {name: 'rangeShift', type: 'USHORT', value: 0}
+    ]);
+    sfnt.tables = tables;
+    sfnt.numTables = tables.length;
+    var highestPowerOf2 = Math.pow(2, log2(sfnt.numTables));
+    sfnt.searchRange = 16 * highestPowerOf2;
+    sfnt.entrySelector = log2(highestPowerOf2);
+    sfnt.rangeShift = sfnt.numTables * 16 - sfnt.searchRange;
+
+    var recordFields = [];
+    var tableFields = [];
+
+    var offset = sfnt.sizeOf() + (makeTableRecord().sizeOf() * sfnt.numTables);
+    while (offset % 4 !== 0) {
+        offset += 1;
+        tableFields.push({name: 'padding', type: 'BYTE', value: 0});
+    }
+
+    for (var i = 0; i < tables.length; i += 1) {
+        var t = tables[i];
+        check.argument(t.tableName.length === 4, 'Table name' + t.tableName + ' is invalid.');
+        var tableLength = t.sizeOf();
+        var tableRecord = makeTableRecord(t.tableName, computeCheckSum(t.encode()), offset, tableLength);
+        recordFields.push({name: tableRecord.tag + ' Table Record', type: 'RECORD', value: tableRecord});
+        tableFields.push({name: t.tableName + ' table', type: 'RECORD', value: t});
+        offset += tableLength;
+        check.argument(!isNaN(offset), 'Something went wrong calculating the offset.');
+        while (offset % 4 !== 0) {
+            offset += 1;
+            tableFields.push({name: 'padding', type: 'BYTE', value: 0});
+        }
+    }
+
+    // Table records need to be sorted alphabetically.
+    recordFields.sort(function(r1, r2) {
+        if (r1.value.tag > r2.value.tag) {
+            return 1;
+        } else {
+            return -1;
+        }
+    });
+
+    sfnt.fields = sfnt.fields.concat(recordFields);
+    sfnt.fields = sfnt.fields.concat(tableFields);
+    return sfnt;
+}
+
+// Get the metrics for a character. If the string has more than one character
+// this function returns metrics for the first available character.
+// You can provide optional fallback metrics if no characters are available.
+function metricsForChar(font, chars, notFoundMetrics) {
+    for (var i = 0; i < chars.length; i += 1) {
+        var glyphIndex = font.charToGlyphIndex(chars[i]);
+        if (glyphIndex > 0) {
+            var glyph = font.glyphs.get(glyphIndex);
+            return glyph.getMetrics();
+        }
+    }
+
+    return notFoundMetrics;
+}
+
+function average(vs) {
+    var sum = 0;
+    for (var i = 0; i < vs.length; i += 1) {
+        sum += vs[i];
+    }
+
+    return sum / vs.length;
+}
+
+// Convert the font object to a SFNT data structure.
+// This structure contains all the necessary tables and metadata to create a binary OTF file.
+function fontToSfntTable(font) {
+    var xMins = [];
+    var yMins = [];
+    var xMaxs = [];
+    var yMaxs = [];
+    var advanceWidths = [];
+    var leftSideBearings = [];
+    var rightSideBearings = [];
+    var firstCharIndex;
+    var lastCharIndex = 0;
+    var ulUnicodeRange1 = 0;
+    var ulUnicodeRange2 = 0;
+    var ulUnicodeRange3 = 0;
+    var ulUnicodeRange4 = 0;
+
+    for (var i = 0; i < font.glyphs.length; i += 1) {
+        var glyph = font.glyphs.get(i);
+        var unicode = glyph.unicode | 0;
+
+        if (isNaN(glyph.advanceWidth)) {
+            throw new Error('Glyph ' + glyph.name + ' (' + i + '): advanceWidth is not a number.');
+        }
+
+        if (firstCharIndex > unicode || firstCharIndex === undefined) {
+            // ignore .notdef char
+            if (unicode > 0) {
+                firstCharIndex = unicode;
+            }
+        }
+
+        if (lastCharIndex < unicode) {
+            lastCharIndex = unicode;
+        }
+
+        var position = os2.getUnicodeRange(unicode);
+        if (position < 32) {
+            ulUnicodeRange1 |= 1 << position;
+        } else if (position < 64) {
+            ulUnicodeRange2 |= 1 << position - 32;
+        } else if (position < 96) {
+            ulUnicodeRange3 |= 1 << position - 64;
+        } else if (position < 123) {
+            ulUnicodeRange4 |= 1 << position - 96;
+        } else {
+            throw new Error('Unicode ranges bits > 123 are reserved for internal usage');
+        }
+        // Skip non-important characters.
+        if (glyph.name === '.notdef') { continue; }
+        var metrics = glyph.getMetrics();
+        xMins.push(metrics.xMin);
+        yMins.push(metrics.yMin);
+        xMaxs.push(metrics.xMax);
+        yMaxs.push(metrics.yMax);
+        leftSideBearings.push(metrics.leftSideBearing);
+        rightSideBearings.push(metrics.rightSideBearing);
+        advanceWidths.push(glyph.advanceWidth);
+    }
+
+    var globals = {
+        xMin: Math.min.apply(null, xMins),
+        yMin: Math.min.apply(null, yMins),
+        xMax: Math.max.apply(null, xMaxs),
+        yMax: Math.max.apply(null, yMaxs),
+        advanceWidthMax: Math.max.apply(null, advanceWidths),
+        advanceWidthAvg: average(advanceWidths),
+        minLeftSideBearing: Math.min.apply(null, leftSideBearings),
+        maxLeftSideBearing: Math.max.apply(null, leftSideBearings),
+        minRightSideBearing: Math.min.apply(null, rightSideBearings)
+    };
+    globals.ascender = font.ascender;
+    globals.descender = font.descender;
+
+    var headTable = head.make({
+        flags: 3, // 00000011 (baseline for font at y=0; left sidebearing point at x=0)
+        unitsPerEm: font.unitsPerEm,
+        xMin: globals.xMin,
+        yMin: globals.yMin,
+        xMax: globals.xMax,
+        yMax: globals.yMax,
+        lowestRecPPEM: 3,
+        createdTimestamp: font.createdTimestamp
+    });
+
+    var hheaTable = hhea.make({
+        ascender: globals.ascender,
+        descender: globals.descender,
+        advanceWidthMax: globals.advanceWidthMax,
+        minLeftSideBearing: globals.minLeftSideBearing,
+        minRightSideBearing: globals.minRightSideBearing,
+        xMaxExtent: globals.maxLeftSideBearing + (globals.xMax - globals.xMin),
+        numberOfHMetrics: font.glyphs.length
+    });
+
+    var maxpTable = maxp.make(font.glyphs.length);
+
+    var os2Table = os2.make(Object.assign({
+        xAvgCharWidth: Math.round(globals.advanceWidthAvg),
+        usFirstCharIndex: firstCharIndex,
+        usLastCharIndex: lastCharIndex,
+        ulUnicodeRange1: ulUnicodeRange1,
+        ulUnicodeRange2: ulUnicodeRange2,
+        ulUnicodeRange3: ulUnicodeRange3,
+        ulUnicodeRange4: ulUnicodeRange4,
+        // See http://typophile.com/node/13081 for more info on vertical metrics.
+        // We get metrics for typical characters (such as "x" for xHeight).
+        // We provide some fallback characters if characters are unavailable: their
+        // ordering was chosen experimentally.
+        sTypoAscender: globals.ascender,
+        sTypoDescender: globals.descender,
+        sTypoLineGap: 0,
+        usWinAscent: globals.yMax,
+        usWinDescent: Math.abs(globals.yMin),
+        ulCodePageRange1: 1, // FIXME: hard-code Latin 1 support for now
+        sxHeight: metricsForChar(font, 'xyvw', {yMax: Math.round(globals.ascender / 2)}).yMax,
+        sCapHeight: metricsForChar(font, 'HIKLEFJMNTZBDPRAGOQSUVWXY', globals).yMax,
+        usDefaultChar: font.hasChar(' ') ? 32 : 0, // Use space as the default character, if available.
+        usBreakChar: font.hasChar(' ') ? 32 : 0, // Use space as the break character, if available.
+    }, font.tables.os2));
+
+    var hmtxTable = hmtx.make(font.glyphs);
+    var cmapTable = cmap.make(font.glyphs);
+
+    var englishFamilyName = font.getEnglishName('fontFamily');
+    var englishStyleName = font.getEnglishName('fontSubfamily');
+    var englishFullName = englishFamilyName + ' ' + englishStyleName;
+    var postScriptName = font.getEnglishName('postScriptName');
+    if (!postScriptName) {
+        postScriptName = englishFamilyName.replace(/\s/g, '') + '-' + englishStyleName;
+    }
+
+    var names = {};
+    for (var n in font.names) {
+        names[n] = font.names[n];
+    }
+
+    if (!names.uniqueID) {
+        names.uniqueID = {en: font.getEnglishName('manufacturer') + ':' + englishFullName};
+    }
+
+    if (!names.postScriptName) {
+        names.postScriptName = {en: postScriptName};
+    }
+
+    if (!names.preferredFamily) {
+        names.preferredFamily = font.names.fontFamily;
+    }
+
+    if (!names.preferredSubfamily) {
+        names.preferredSubfamily = font.names.fontSubfamily;
+    }
+
+    var languageTags = [];
+    var nameTable = _name.make(names, languageTags);
+    var ltagTable = (languageTags.length > 0 ? ltag.make(languageTags) : undefined);
+
+    var postTable = post.make();
+    var cffTable = cff.make(font.glyphs, {
+        version: font.getEnglishName('version'),
+        fullName: englishFullName,
+        familyName: englishFamilyName,
+        weightName: englishStyleName,
+        postScriptName: postScriptName,
+        unitsPerEm: font.unitsPerEm,
+        fontBBox: [0, globals.yMin, globals.ascender, globals.advanceWidthMax]
+    });
+
+    var metaTable = (font.metas && Object.keys(font.metas).length > 0) ? meta.make(font.metas) : undefined;
+
+    // The order does not matter because makeSfntTable() will sort them.
+    var tables = [headTable, hheaTable, maxpTable, os2Table, nameTable, cmapTable, postTable, cffTable, hmtxTable];
+    if (ltagTable) {
+        tables.push(ltagTable);
+    }
+    // Optional tables
+    if (font.tables.gsub) {
+        tables.push(gsub.make(font.tables.gsub));
+    }
+    if (font.tables.cpal) {
+        tables.push(cpal.make(font.tables.cpal));
+    }
+    if (font.tables.colr) {
+        tables.push(colr.make(font.tables.colr));
+    }
+    if (metaTable) {
+        tables.push(metaTable);
+    }
+
+    var sfntTable = makeSfntTable(tables);
+
+    // Compute the font's checkSum and store it in head.checkSumAdjustment.
+    var bytes = sfntTable.encode();
+    var checkSum = computeCheckSum(bytes);
+    var tableFields = sfntTable.fields;
+    var checkSumAdjusted = false;
+    for (var i$1 = 0; i$1 < tableFields.length; i$1 += 1) {
+        if (tableFields[i$1].name === 'head table') {
+            tableFields[i$1].value.checkSumAdjustment = 0xB1B0AFBA - checkSum;
+            checkSumAdjusted = true;
+            break;
+        }
+    }
+
+    if (!checkSumAdjusted) {
+        throw new Error('Could not find head table with checkSum to adjust.');
+    }
+
+    return sfntTable;
+}
+
+var sfnt = { make: makeSfntTable, fontToTable: fontToSfntTable, computeCheckSum: computeCheckSum };
+
+// The Layout object is the prototype of Substitution objects, and provides
+
+function searchTag(arr, tag) {
+    /* jshint bitwise: false */
+    var imin = 0;
+    var imax = arr.length - 1;
+    while (imin <= imax) {
+        var imid = (imin + imax) >>> 1;
+        var val = arr[imid].tag;
+        if (val === tag) {
+            return imid;
+        } else if (val < tag) {
+            imin = imid + 1;
+        } else { imax = imid - 1; }
+    }
+    // Not found: return -1-insertion point
+    return -imin - 1;
+}
+
+function binSearch(arr, value) {
+    /* jshint bitwise: false */
+    var imin = 0;
+    var imax = arr.length - 1;
+    while (imin <= imax) {
+        var imid = (imin + imax) >>> 1;
+        var val = arr[imid];
+        if (val === value) {
+            return imid;
+        } else if (val < value) {
+            imin = imid + 1;
+        } else { imax = imid - 1; }
+    }
+    // Not found: return -1-insertion point
+    return -imin - 1;
+}
+
+// binary search in a list of ranges (coverage, class definition)
+function searchRange(ranges, value) {
+    // jshint bitwise: false
+    var range;
+    var imin = 0;
+    var imax = ranges.length - 1;
+    while (imin <= imax) {
+        var imid = (imin + imax) >>> 1;
+        range = ranges[imid];
+        var start = range.start;
+        if (start === value) {
+            return range;
+        } else if (start < value) {
+            imin = imid + 1;
+        } else { imax = imid - 1; }
+    }
+    if (imin > 0) {
+        range = ranges[imin - 1];
+        if (value > range.end) { return 0; }
+        return range;
+    }
+}
+
+/**
+ * @exports opentype.Layout
+ * @class
+ */
+function Layout(font, tableName) {
+    this.font = font;
+    this.tableName = tableName;
+}
+
+Layout.prototype = {
+
+    /**
+     * Binary search an object by "tag" property
+     * @instance
+     * @function searchTag
+     * @memberof opentype.Layout
+     * @param  {Array} arr
+     * @param  {string} tag
+     * @return {number}
+     */
+    searchTag: searchTag,
+
+    /**
+     * Binary search in a list of numbers
+     * @instance
+     * @function binSearch
+     * @memberof opentype.Layout
+     * @param  {Array} arr
+     * @param  {number} value
+     * @return {number}
+     */
+    binSearch: binSearch,
+
+    /**
+     * Get or create the Layout table (GSUB, GPOS etc).
+     * @param  {boolean} create - Whether to create a new one.
+     * @return {Object} The GSUB or GPOS table.
+     */
+    getTable: function(create) {
+        var layout = this.font.tables[this.tableName];
+        if (!layout && create) {
+            layout = this.font.tables[this.tableName] = this.createDefaultTable();
+        }
+        return layout;
+    },
+
+    /**
+     * Returns all scripts in the substitution table.
+     * @instance
+     * @return {Array}
+     */
+    getScriptNames: function() {
+        var layout = this.getTable();
+        if (!layout) { return []; }
+        return layout.scripts.map(function(script) {
+            return script.tag;
+        });
+    },
+
+    /**
+     * Returns the best bet for a script name.
+     * Returns 'DFLT' if it exists.
+     * If not, returns 'latn' if it exists.
+     * If neither exist, returns undefined.
+     */
+    getDefaultScriptName: function() {
+        var layout = this.getTable();
+        if (!layout) { return; }
+        var hasLatn = false;
+        for (var i = 0; i < layout.scripts.length; i++) {
+            var name = layout.scripts[i].tag;
+            if (name === 'DFLT') { return name; }
+            if (name === 'latn') { hasLatn = true; }
+        }
+        if (hasLatn) { return 'latn'; }
+    },
+
+    /**
+     * Returns all LangSysRecords in the given script.
+     * @instance
+     * @param {string} [script='DFLT']
+     * @param {boolean} create - forces the creation of this script table if it doesn't exist.
+     * @return {Object} An object with tag and script properties.
+     */
+    getScriptTable: function(script, create) {
+        var layout = this.getTable(create);
+        if (layout) {
+            script = script || 'DFLT';
+            var scripts = layout.scripts;
+            var pos = searchTag(layout.scripts, script);
+            if (pos >= 0) {
+                return scripts[pos].script;
+            } else if (create) {
+                var scr = {
+                    tag: script,
+                    script: {
+                        defaultLangSys: {reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: []},
+                        langSysRecords: []
+                    }
+                };
+                scripts.splice(-1 - pos, 0, scr);
+                return scr.script;
+            }
+        }
+    },
+
+    /**
+     * Returns a language system table
+     * @instance
+     * @param {string} [script='DFLT']
+     * @param {string} [language='dlft']
+     * @param {boolean} create - forces the creation of this langSysTable if it doesn't exist.
+     * @return {Object}
+     */
+    getLangSysTable: function(script, language, create) {
+        var scriptTable = this.getScriptTable(script, create);
+        if (scriptTable) {
+            if (!language || language === 'dflt' || language === 'DFLT') {
+                return scriptTable.defaultLangSys;
+            }
+            var pos = searchTag(scriptTable.langSysRecords, language);
+            if (pos >= 0) {
+                return scriptTable.langSysRecords[pos].langSys;
+            } else if (create) {
+                var langSysRecord = {
+                    tag: language,
+                    langSys: {reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: []}
+                };
+                scriptTable.langSysRecords.splice(-1 - pos, 0, langSysRecord);
+                return langSysRecord.langSys;
+            }
+        }
+    },
+
+    /**
+     * Get a specific feature table.
+     * @instance
+     * @param {string} [script='DFLT']
+     * @param {string} [language='dlft']
+     * @param {string} feature - One of the codes listed at https://www.microsoft.com/typography/OTSPEC/featurelist.htm
+     * @param {boolean} create - forces the creation of the feature table if it doesn't exist.
+     * @return {Object}
+     */
+    getFeatureTable: function(script, language, feature, create) {
+        var langSysTable = this.getLangSysTable(script, language, create);
+        if (langSysTable) {
+            var featureRecord;
+            var featIndexes = langSysTable.featureIndexes;
+            var allFeatures = this.font.tables[this.tableName].features;
+            // The FeatureIndex array of indices is in arbitrary order,
+            // even if allFeatures is sorted alphabetically by feature tag.
+            for (var i = 0; i < featIndexes.length; i++) {
+                featureRecord = allFeatures[featIndexes[i]];
+                if (featureRecord.tag === feature) {
+                    return featureRecord.feature;
+                }
+            }
+            if (create) {
+                var index = allFeatures.length;
+                // Automatic ordering of features would require to shift feature indexes in the script list.
+                check.assert(index === 0 || feature >= allFeatures[index - 1].tag, 'Features must be added in alphabetical order.');
+                featureRecord = {
+                    tag: feature,
+                    feature: { params: 0, lookupListIndexes: [] }
+                };
+                allFeatures.push(featureRecord);
+                featIndexes.push(index);
+                return featureRecord.feature;
+            }
+        }
+    },
+
+    /**
+     * Get the lookup tables of a given type for a script/language/feature.
+     * @instance
+     * @param {string} [script='DFLT']
+     * @param {string} [language='dlft']
+     * @param {string} feature - 4-letter feature code
+     * @param {number} lookupType - 1 to 9
+     * @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables.
+     * @return {Object[]}
+     */
+    getLookupTables: function(script, language, feature, lookupType, create) {
+        var featureTable = this.getFeatureTable(script, language, feature, create);
+        var tables = [];
+        if (featureTable) {
+            var lookupTable;
+            var lookupListIndexes = featureTable.lookupListIndexes;
+            var allLookups = this.font.tables[this.tableName].lookups;
+            // lookupListIndexes are in no particular order, so use naive search.
+            for (var i = 0; i < lookupListIndexes.length; i++) {
+                lookupTable = allLookups[lookupListIndexes[i]];
+                if (lookupTable.lookupType === lookupType) {
+                    tables.push(lookupTable);
+                }
+            }
+            if (tables.length === 0 && create) {
+                lookupTable = {
+                    lookupType: lookupType,
+                    lookupFlag: 0,
+                    subtables: [],
+                    markFilteringSet: undefined
+                };
+                var index = allLookups.length;
+                allLookups.push(lookupTable);
+                lookupListIndexes.push(index);
+                return [lookupTable];
+            }
+        }
+        return tables;
+    },
+
+    /**
+     * Find a glyph in a class definition table
+     * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table
+     * @param {object} classDefTable - an OpenType Layout class definition table
+     * @param {number} glyphIndex - the index of the glyph to find
+     * @returns {number} -1 if not found
+     */
+    getGlyphClass: function(classDefTable, glyphIndex) {
+        switch (classDefTable.format) {
+            case 1:
+                if (classDefTable.startGlyph <= glyphIndex && glyphIndex < classDefTable.startGlyph + classDefTable.classes.length) {
+                    return classDefTable.classes[glyphIndex - classDefTable.startGlyph];
+                }
+                return 0;
+            case 2:
+                var range = searchRange(classDefTable.ranges, glyphIndex);
+                return range ? range.classId : 0;
+        }
+    },
+
+    /**
+     * Find a glyph in a coverage table
+     * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-table
+     * @param {object} coverageTable - an OpenType Layout coverage table
+     * @param {number} glyphIndex - the index of the glyph to find
+     * @returns {number} -1 if not found
+     */
+    getCoverageIndex: function(coverageTable, glyphIndex) {
+        switch (coverageTable.format) {
+            case 1:
+                var index = binSearch(coverageTable.glyphs, glyphIndex);
+                return index >= 0 ? index : -1;
+            case 2:
+                var range = searchRange(coverageTable.ranges, glyphIndex);
+                return range ? range.index + glyphIndex - range.start : -1;
+        }
+    },
+
+    /**
+     * Returns the list of glyph indexes of a coverage table.
+     * Format 1: the list is stored raw
+     * Format 2: compact list as range records.
+     * @instance
+     * @param  {Object} coverageTable
+     * @return {Array}
+     */
+    expandCoverage: function(coverageTable) {
+        if (coverageTable.format === 1) {
+            return coverageTable.glyphs;
+        } else {
+            var glyphs = [];
+            var ranges = coverageTable.ranges;
+            for (var i = 0; i < ranges.length; i++) {
+                var range = ranges[i];
+                var start = range.start;
+                var end = range.end;
+                for (var j = start; j <= end; j++) {
+                    glyphs.push(j);
+                }
+            }
+            return glyphs;
+        }
+    }
+
+};
+
+// The Position object provides utility methods to manipulate
+
+/**
+ * @exports opentype.Position
+ * @class
+ * @extends opentype.Layout
+ * @param {opentype.Font}
+ * @constructor
+ */
+function Position(font) {
+    Layout.call(this, font, 'gpos');
+}
+
+Position.prototype = Layout.prototype;
+
+/**
+ * Init some data for faster and easier access later.
+ */
+Position.prototype.init = function() {
+    var script = this.getDefaultScriptName();
+    this.defaultKerningTables = this.getKerningTables(script);
+};
+
+/**
+ * Find a glyph pair in a list of lookup tables of type 2 and retrieve the xAdvance kerning value.
+ *
+ * @param {integer} leftIndex - left glyph index
+ * @param {integer} rightIndex - right glyph index
+ * @returns {integer}
+ */
+Position.prototype.getKerningValue = function(kerningLookups, leftIndex, rightIndex) {
+    for (var i = 0; i < kerningLookups.length; i++) {
+        var subtables = kerningLookups[i].subtables;
+        for (var j = 0; j < subtables.length; j++) {
+            var subtable = subtables[j];
+            var covIndex = this.getCoverageIndex(subtable.coverage, leftIndex);
+            if (covIndex < 0) { continue; }
+            switch (subtable.posFormat) {
+                case 1:
+                    // Search Pair Adjustment Positioning Format 1
+                    var pairSet = subtable.pairSets[covIndex];
+                    for (var k = 0; k < pairSet.length; k++) {
+                        var pair = pairSet[k];
+                        if (pair.secondGlyph === rightIndex) {
+                            return pair.value1 && pair.value1.xAdvance || 0;
+                        }
+                    }
+                    break;      // left glyph found, not right glyph - try next subtable
+                case 2:
+                    // Search Pair Adjustment Positioning Format 2
+                    var class1 = this.getGlyphClass(subtable.classDef1, leftIndex);
+                    var class2 = this.getGlyphClass(subtable.classDef2, rightIndex);
+                    var pair$1 = subtable.classRecords[class1][class2];
+                    return pair$1.value1 && pair$1.value1.xAdvance || 0;
+            }
+        }
+    }
+    return 0;
+};
+
+/**
+ * List all kerning lookup tables.
+ *
+ * @param {string} [script='DFLT'] - use font.position.getDefaultScriptName() for a better default value
+ * @param {string} [language='dflt']
+ * @return {object[]} The list of kerning lookup tables (may be empty), or undefined if there is no GPOS table (and we should use the kern table)
+ */
+Position.prototype.getKerningTables = function(script, language) {
+    if (this.font.tables.gpos) {
+        return this.getLookupTables(script, language, 'kern', 2);
+    }
+};
+
+// The Substitution object provides utility methods to manipulate
+
+/**
+ * @exports opentype.Substitution
+ * @class
+ * @extends opentype.Layout
+ * @param {opentype.Font}
+ * @constructor
+ */
+function Substitution(font) {
+    Layout.call(this, font, 'gsub');
+}
+
+// Check if 2 arrays of primitives are equal.
+function arraysEqual(ar1, ar2) {
+    var n = ar1.length;
+    if (n !== ar2.length) { return false; }
+    for (var i = 0; i < n; i++) {
+        if (ar1[i] !== ar2[i]) { return false; }
+    }
+    return true;
+}
+
+// Find the first subtable of a lookup table in a particular format.
+function getSubstFormat(lookupTable, format, defaultSubtable) {
+    var subtables = lookupTable.subtables;
+    for (var i = 0; i < subtables.length; i++) {
+        var subtable = subtables[i];
+        if (subtable.substFormat === format) {
+            return subtable;
+        }
+    }
+    if (defaultSubtable) {
+        subtables.push(defaultSubtable);
+        return defaultSubtable;
+    }
+    return undefined;
+}
+
+Substitution.prototype = Layout.prototype;
+
+/**
+ * Create a default GSUB table.
+ * @return {Object} gsub - The GSUB table.
+ */
+Substitution.prototype.createDefaultTable = function() {
+    // Generate a default empty GSUB table with just a DFLT script and dflt lang sys.
+    return {
+        version: 1,
+        scripts: [{
+            tag: 'DFLT',
+            script: {
+                defaultLangSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [] },
+                langSysRecords: []
+            }
+        }],
+        features: [],
+        lookups: []
+    };
+};
+
+/**
+ * List all single substitutions (lookup type 1) for a given script, language, and feature.
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ * @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...)
+ * @return {Array} substitutions - The list of substitutions.
+ */
+Substitution.prototype.getSingle = function(feature, script, language) {
+    var substitutions = [];
+    var lookupTables = this.getLookupTables(script, language, feature, 1);
+    for (var idx = 0; idx < lookupTables.length; idx++) {
+        var subtables = lookupTables[idx].subtables;
+        for (var i = 0; i < subtables.length; i++) {
+            var subtable = subtables[i];
+            var glyphs = this.expandCoverage(subtable.coverage);
+            var j = (void 0);
+            if (subtable.substFormat === 1) {
+                var delta = subtable.deltaGlyphId;
+                for (j = 0; j < glyphs.length; j++) {
+                    var glyph = glyphs[j];
+                    substitutions.push({ sub: glyph, by: glyph + delta });
+                }
+            } else {
+                var substitute = subtable.substitute;
+                for (j = 0; j < glyphs.length; j++) {
+                    substitutions.push({ sub: glyphs[j], by: substitute[j] });
+                }
+            }
+        }
+    }
+    return substitutions;
+};
+
+/**
+ * List all multiple substitutions (lookup type 2) for a given script, language, and feature.
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ * @param {string} feature - 4-character feature name ('ccmp', 'stch')
+ * @return {Array} substitutions - The list of substitutions.
+ */
+Substitution.prototype.getMultiple = function(feature, script, language) {
+    var substitutions = [];
+    var lookupTables = this.getLookupTables(script, language, feature, 2);
+    for (var idx = 0; idx < lookupTables.length; idx++) {
+        var subtables = lookupTables[idx].subtables;
+        for (var i = 0; i < subtables.length; i++) {
+            var subtable = subtables[i];
+            var glyphs = this.expandCoverage(subtable.coverage);
+            var j = (void 0);
+
+            for (j = 0; j < glyphs.length; j++) {
+                var glyph = glyphs[j];
+                var replacements = subtable.sequences[j];
+                substitutions.push({ sub: glyph, by: replacements });
+            }
+        }
+    }
+    return substitutions;
+};
+
+/**
+ * List all alternates (lookup type 3) for a given script, language, and feature.
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ * @param {string} feature - 4-character feature name ('aalt', 'salt'...)
+ * @return {Array} alternates - The list of alternates
+ */
+Substitution.prototype.getAlternates = function(feature, script, language) {
+    var alternates = [];
+    var lookupTables = this.getLookupTables(script, language, feature, 3);
+    for (var idx = 0; idx < lookupTables.length; idx++) {
+        var subtables = lookupTables[idx].subtables;
+        for (var i = 0; i < subtables.length; i++) {
+            var subtable = subtables[i];
+            var glyphs = this.expandCoverage(subtable.coverage);
+            var alternateSets = subtable.alternateSets;
+            for (var j = 0; j < glyphs.length; j++) {
+                alternates.push({ sub: glyphs[j], by: alternateSets[j] });
+            }
+        }
+    }
+    return alternates;
+};
+
+/**
+ * List all ligatures (lookup type 4) for a given script, language, and feature.
+ * The result is an array of ligature objects like { sub: [ids], by: id }
+ * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ * @return {Array} ligatures - The list of ligatures.
+ */
+Substitution.prototype.getLigatures = function(feature, script, language) {
+    var ligatures = [];
+    var lookupTables = this.getLookupTables(script, language, feature, 4);
+    for (var idx = 0; idx < lookupTables.length; idx++) {
+        var subtables = lookupTables[idx].subtables;
+        for (var i = 0; i < subtables.length; i++) {
+            var subtable = subtables[i];
+            var glyphs = this.expandCoverage(subtable.coverage);
+            var ligatureSets = subtable.ligatureSets;
+            for (var j = 0; j < glyphs.length; j++) {
+                var startGlyph = glyphs[j];
+                var ligSet = ligatureSets[j];
+                for (var k = 0; k < ligSet.length; k++) {
+                    var lig = ligSet[k];
+                    ligatures.push({
+                        sub: [startGlyph].concat(lig.components),
+                        by: lig.ligGlyph
+                    });
+                }
+            }
+        }
+    }
+    return ligatures;
+};
+
+/**
+ * Add or modify a single substitution (lookup type 1)
+ * Format 2, more flexible, is always used.
+ * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
+ * @param {Object} substitution - { sub: id, by: id } (format 1 is not supported)
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ */
+Substitution.prototype.addSingle = function(feature, substitution, script, language) {
+    var lookupTable = this.getLookupTables(script, language, feature, 1, true)[0];
+    var subtable = getSubstFormat(lookupTable, 2, {                // lookup type 1 subtable, format 2, coverage format 1
+        substFormat: 2,
+        coverage: {format: 1, glyphs: []},
+        substitute: []
+    });
+    check.assert(subtable.coverage.format === 1, 'Single: unable to modify coverage table format ' + subtable.coverage.format);
+    var coverageGlyph = substitution.sub;
+    var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
+    if (pos < 0) {
+        pos = -1 - pos;
+        subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
+        subtable.substitute.splice(pos, 0, 0);
+    }
+    subtable.substitute[pos] = substitution.by;
+};
+
+/**
+ * Add or modify a multiple substitution (lookup type 2)
+ * @param {string} feature - 4-letter feature name ('ccmp', 'stch')
+ * @param {Object} substitution - { sub: id, by: [id] } for format 2.
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ */
+Substitution.prototype.addMultiple = function(feature, substitution, script, language) {
+    check.assert(substitution.by instanceof Array && substitution.by.length > 1, 'Multiple: "by" must be an array of two or more ids');
+    var lookupTable = this.getLookupTables(script, language, feature, 2, true)[0];
+    var subtable = getSubstFormat(lookupTable, 1, {                // lookup type 2 subtable, format 1, coverage format 1
+        substFormat: 1,
+        coverage: {format: 1, glyphs: []},
+        sequences: []
+    });
+    check.assert(subtable.coverage.format === 1, 'Multiple: unable to modify coverage table format ' + subtable.coverage.format);
+    var coverageGlyph = substitution.sub;
+    var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
+    if (pos < 0) {
+        pos = -1 - pos;
+        subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
+        subtable.sequences.splice(pos, 0, 0);
+    }
+    subtable.sequences[pos] = substitution.by;
+};
+
+/**
+ * Add or modify an alternate substitution (lookup type 3)
+ * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
+ * @param {Object} substitution - { sub: id, by: [ids] }
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ */
+Substitution.prototype.addAlternate = function(feature, substitution, script, language) {
+    var lookupTable = this.getLookupTables(script, language, feature, 3, true)[0];
+    var subtable = getSubstFormat(lookupTable, 1, {                // lookup type 3 subtable, format 1, coverage format 1
+        substFormat: 1,
+        coverage: {format: 1, glyphs: []},
+        alternateSets: []
+    });
+    check.assert(subtable.coverage.format === 1, 'Alternate: unable to modify coverage table format ' + subtable.coverage.format);
+    var coverageGlyph = substitution.sub;
+    var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
+    if (pos < 0) {
+        pos = -1 - pos;
+        subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
+        subtable.alternateSets.splice(pos, 0, 0);
+    }
+    subtable.alternateSets[pos] = substitution.by;
+};
+
+/**
+ * Add a ligature (lookup type 4)
+ * Ligatures with more components must be stored ahead of those with fewer components in order to be found
+ * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
+ * @param {Object} ligature - { sub: [ids], by: id }
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ */
+Substitution.prototype.addLigature = function(feature, ligature, script, language) {
+    var lookupTable = this.getLookupTables(script, language, feature, 4, true)[0];
+    var subtable = lookupTable.subtables[0];
+    if (!subtable) {
+        subtable = {                // lookup type 4 subtable, format 1, coverage format 1
+            substFormat: 1,
+            coverage: { format: 1, glyphs: [] },
+            ligatureSets: []
+        };
+        lookupTable.subtables[0] = subtable;
+    }
+    check.assert(subtable.coverage.format === 1, 'Ligature: unable to modify coverage table format ' + subtable.coverage.format);
+    var coverageGlyph = ligature.sub[0];
+    var ligComponents = ligature.sub.slice(1);
+    var ligatureTable = {
+        ligGlyph: ligature.by,
+        components: ligComponents
+    };
+    var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
+    if (pos >= 0) {
+        // ligatureSet already exists
+        var ligatureSet = subtable.ligatureSets[pos];
+        for (var i = 0; i < ligatureSet.length; i++) {
+            // If ligature already exists, return.
+            if (arraysEqual(ligatureSet[i].components, ligComponents)) {
+                return;
+            }
+        }
+        // ligature does not exist: add it.
+        ligatureSet.push(ligatureTable);
+    } else {
+        // Create a new ligatureSet and add coverage for the first glyph.
+        pos = -1 - pos;
+        subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
+        subtable.ligatureSets.splice(pos, 0, [ligatureTable]);
+    }
+};
+
+/**
+ * List all feature data for a given script and language.
+ * @param {string} feature - 4-letter feature name
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ * @return {Array} substitutions - The list of substitutions.
+ */
+Substitution.prototype.getFeature = function(feature, script, language) {
+    if (/ss\d\d/.test(feature)) {
+        // ss01 - ss20
+        return this.getSingle(feature, script, language);
+    }
+    switch (feature) {
+        case 'aalt':
+        case 'salt':
+            return this.getSingle(feature, script, language)
+                    .concat(this.getAlternates(feature, script, language));
+        case 'dlig':
+        case 'liga':
+        case 'rlig':
+            return this.getLigatures(feature, script, language);
+        case 'ccmp':
+            return this.getMultiple(feature, script, language)
+                .concat(this.getLigatures(feature, script, language));
+        case 'stch':
+            return this.getMultiple(feature, script, language);
+    }
+    return undefined;
+};
+
+/**
+ * Add a substitution to a feature for a given script and language.
+ * @param {string} feature - 4-letter feature name
+ * @param {Object} sub - the substitution to add (an object like { sub: id or [ids], by: id or [ids] })
+ * @param {string} [script='DFLT']
+ * @param {string} [language='dflt']
+ */
+Substitution.prototype.add = function(feature, sub, script, language) {
+    if (/ss\d\d/.test(feature)) {
+        // ss01 - ss20
+        return this.addSingle(feature, sub, script, language);
+    }
+    switch (feature) {
+        case 'aalt':
+        case 'salt':
+            if (typeof sub.by === 'number') {
+                return this.addSingle(feature, sub, script, language);
+            }
+            return this.addAlternate(feature, sub, script, language);
+        case 'dlig':
+        case 'liga':
+        case 'rlig':
+            return this.addLigature(feature, sub, script, language);
+        case 'ccmp':
+            if (sub.by instanceof Array) {
+                return this.addMultiple(feature, sub, script, language);
+            }
+            return this.addLigature(feature, sub, script, language);
+    }
+    return undefined;
+};
+
+function isBrowser() {
+    return typeof window !== 'undefined';
+}
+
+function nodeBufferToArrayBuffer(buffer) {
+    var ab = new ArrayBuffer(buffer.length);
+    var view = new Uint8Array(ab);
+    for (var i = 0; i < buffer.length; ++i) {
+        view[i] = buffer[i];
+    }
+
+    return ab;
+}
+
+function arrayBufferToNodeBuffer(ab) {
+    var buffer = new Buffer(ab.byteLength);
+    var view = new Uint8Array(ab);
+    for (var i = 0; i < buffer.length; ++i) {
+        buffer[i] = view[i];
+    }
+
+    return buffer;
+}
+
+function checkArgument(expression, message) {
+    if (!expression) {
+        throw message;
+    }
+}
+
+// The `glyf` table describes the glyphs in TrueType outline format.
+
+// Parse the coordinate data for a glyph.
+function parseGlyphCoordinate(p, flag, previousValue, shortVectorBitMask, sameBitMask) {
+    var v;
+    if ((flag & shortVectorBitMask) > 0) {
+        // The coordinate is 1 byte long.
+        v = p.parseByte();
+        // The `same` bit is re-used for short values to signify the sign of the value.
+        if ((flag & sameBitMask) === 0) {
+            v = -v;
+        }
+
+        v = previousValue + v;
+    } else {
+        //  The coordinate is 2 bytes long.
+        // If the `same` bit is set, the coordinate is the same as the previous coordinate.
+        if ((flag & sameBitMask) > 0) {
+            v = previousValue;
+        } else {
+            // Parse the coordinate as a signed 16-bit delta value.
+            v = previousValue + p.parseShort();
+        }
+    }
+
+    return v;
+}
+
+// Parse a TrueType glyph.
+function parseGlyph(glyph, data, start) {
+    var p = new parse.Parser(data, start);
+    glyph.numberOfContours = p.parseShort();
+    glyph._xMin = p.parseShort();
+    glyph._yMin = p.parseShort();
+    glyph._xMax = p.parseShort();
+    glyph._yMax = p.parseShort();
+    var flags;
+    var flag;
+
+    if (glyph.numberOfContours > 0) {
+        // This glyph is not a composite.
+        var endPointIndices = glyph.endPointIndices = [];
+        for (var i = 0; i < glyph.numberOfContours; i += 1) {
+            endPointIndices.push(p.parseUShort());
+        }
+
+        glyph.instructionLength = p.parseUShort();
+        glyph.instructions = [];
+        for (var i$1 = 0; i$1 < glyph.instructionLength; i$1 += 1) {
+            glyph.instructions.push(p.parseByte());
+        }
+
+        var numberOfCoordinates = endPointIndices[endPointIndices.length - 1] + 1;
+        flags = [];
+        for (var i$2 = 0; i$2 < numberOfCoordinates; i$2 += 1) {
+            flag = p.parseByte();
+            flags.push(flag);
+            // If bit 3 is set, we repeat this flag n times, where n is the next byte.
+            if ((flag & 8) > 0) {
+                var repeatCount = p.parseByte();
+                for (var j = 0; j < repeatCount; j += 1) {
+                    flags.push(flag);
+                    i$2 += 1;
+                }
+            }
+        }
+
+        check.argument(flags.length === numberOfCoordinates, 'Bad flags.');
+
+        if (endPointIndices.length > 0) {
+            var points = [];
+            var point;
+            // X/Y coordinates are relative to the previous point, except for the first point which is relative to 0,0.
+            if (numberOfCoordinates > 0) {
+                for (var i$3 = 0; i$3 < numberOfCoordinates; i$3 += 1) {
+                    flag = flags[i$3];
+                    point = {};
+                    point.onCurve = !!(flag & 1);
+                    point.lastPointOfContour = endPointIndices.indexOf(i$3) >= 0;
+                    points.push(point);
+                }
+
+                var px = 0;
+                for (var i$4 = 0; i$4 < numberOfCoordinates; i$4 += 1) {
+                    flag = flags[i$4];
+                    point = points[i$4];
+                    point.x = parseGlyphCoordinate(p, flag, px, 2, 16);
+                    px = point.x;
+                }
+
+                var py = 0;
+                for (var i$5 = 0; i$5 < numberOfCoordinates; i$5 += 1) {
+                    flag = flags[i$5];
+                    point = points[i$5];
+                    point.y = parseGlyphCoordinate(p, flag, py, 4, 32);
+                    py = point.y;
+                }
+            }
+
+            glyph.points = points;
+        } else {
+            glyph.points = [];
+        }
+    } else if (glyph.numberOfContours === 0) {
+        glyph.points = [];
+    } else {
+        glyph.isComposite = true;
+        glyph.points = [];
+        glyph.components = [];
+        var moreComponents = true;
+        while (moreComponents) {
+            flags = p.parseUShort();
+            var component = {
+                glyphIndex: p.parseUShort(),
+                xScale: 1,
+                scale01: 0,
+                scale10: 0,
+                yScale: 1,
+                dx: 0,
+                dy: 0
+            };
+            if ((flags & 1) > 0) {
+                // The arguments are words
+                if ((flags & 2) > 0) {
+                    // values are offset
+                    component.dx = p.parseShort();
+                    component.dy = p.parseShort();
+                } else {
+                    // values are matched points
+                    component.matchedPoints = [p.parseUShort(), p.parseUShort()];
+                }
+
+            } else {
+                // The arguments are bytes
+                if ((flags & 2) > 0) {
+                    // values are offset
+                    component.dx = p.parseChar();
+                    component.dy = p.parseChar();
+                } else {
+                    // values are matched points
+                    component.matchedPoints = [p.parseByte(), p.parseByte()];
+                }
+            }
+
+            if ((flags & 8) > 0) {
+                // We have a scale
+                component.xScale = component.yScale = p.parseF2Dot14();
+            } else if ((flags & 64) > 0) {
+                // We have an X / Y scale
+                component.xScale = p.parseF2Dot14();
+                component.yScale = p.parseF2Dot14();
+            } else if ((flags & 128) > 0) {
+                // We have a 2x2 transformation
+                component.xScale = p.parseF2Dot14();
+                component.scale01 = p.parseF2Dot14();
+                component.scale10 = p.parseF2Dot14();
+                component.yScale = p.parseF2Dot14();
+            }
+
+            glyph.components.push(component);
+            moreComponents = !!(flags & 32);
+        }
+        if (flags & 0x100) {
+            // We have instructions
+            glyph.instructionLength = p.parseUShort();
+            glyph.instructions = [];
+            for (var i$6 = 0; i$6 < glyph.instructionLength; i$6 += 1) {
+                glyph.instructions.push(p.parseByte());
+            }
+        }
+    }
+}
+
+// Transform an array of points and return a new array.
+function transformPoints(points, transform) {
+    var newPoints = [];
+    for (var i = 0; i < points.length; i += 1) {
+        var pt = points[i];
+        var newPt = {
+            x: transform.xScale * pt.x + transform.scale01 * pt.y + transform.dx,
+            y: transform.scale10 * pt.x + transform.yScale * pt.y + transform.dy,
+            onCurve: pt.onCurve,
+            lastPointOfContour: pt.lastPointOfContour
+        };
+        newPoints.push(newPt);
+    }
+
+    return newPoints;
+}
+
+function getContours(points) {
+    var contours = [];
+    var currentContour = [];
+    for (var i = 0; i < points.length; i += 1) {
+        var pt = points[i];
+        currentContour.push(pt);
+        if (pt.lastPointOfContour) {
+            contours.push(currentContour);
+            currentContour = [];
+        }
+    }
+
+    check.argument(currentContour.length === 0, 'There are still points left in the current contour.');
+    return contours;
+}
+
+// Convert the TrueType glyph outline to a Path.
+function getPath(points) {
+    var p = new Path();
+    if (!points) {
+        return p;
+    }
+
+    var contours = getContours(points);
+
+    for (var contourIndex = 0; contourIndex < contours.length; ++contourIndex) {
+        var contour = contours[contourIndex];
+
+        var prev = null;
+        var curr = contour[contour.length - 1];
+        var next = contour[0];
+
+        if (curr.onCurve) {
+            p.moveTo(curr.x, curr.y);
+        } else {
+            if (next.onCurve) {
+                p.moveTo(next.x, next.y);
+            } else {
+                // If both first and last points are off-curve, start at their middle.
+                var start = {x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5};
+                p.moveTo(start.x, start.y);
+            }
+        }
+
+        for (var i = 0; i < contour.length; ++i) {
+            prev = curr;
+            curr = next;
+            next = contour[(i + 1) % contour.length];
+
+            if (curr.onCurve) {
+                // This is a straight line.
+                p.lineTo(curr.x, curr.y);
+            } else {
+                var prev2 = prev;
+                var next2 = next;
+
+                if (!prev.onCurve) {
+                    prev2 = { x: (curr.x + prev.x) * 0.5, y: (curr.y + prev.y) * 0.5 };
+                }
+
+                if (!next.onCurve) {
+                    next2 = { x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5 };
+                }
+
+                p.quadraticCurveTo(curr.x, curr.y, next2.x, next2.y);
+            }
+        }
+
+        p.closePath();
+    }
+    return p;
+}
+
+function buildPath(glyphs, glyph) {
+    if (glyph.isComposite) {
+        for (var j = 0; j < glyph.components.length; j += 1) {
+            var component = glyph.components[j];
+            var componentGlyph = glyphs.get(component.glyphIndex);
+            // Force the ttfGlyphLoader to parse the glyph.
+            componentGlyph.getPath();
+            if (componentGlyph.points) {
+                var transformedPoints = (void 0);
+                if (component.matchedPoints === undefined) {
+                    // component positioned by offset
+                    transformedPoints = transformPoints(componentGlyph.points, component);
+                } else {
+                    // component positioned by matched points
+                    if ((component.matchedPoints[0] > glyph.points.length - 1) ||
+                        (component.matchedPoints[1] > componentGlyph.points.length - 1)) {
+                        throw Error('Matched points out of range in ' + glyph.name);
+                    }
+                    var firstPt = glyph.points[component.matchedPoints[0]];
+                    var secondPt = componentGlyph.points[component.matchedPoints[1]];
+                    var transform = {
+                        xScale: component.xScale, scale01: component.scale01,
+                        scale10: component.scale10, yScale: component.yScale,
+                        dx: 0, dy: 0
+                    };
+                    secondPt = transformPoints([secondPt], transform)[0];
+                    transform.dx = firstPt.x - secondPt.x;
+                    transform.dy = firstPt.y - secondPt.y;
+                    transformedPoints = transformPoints(componentGlyph.points, transform);
+                }
+                glyph.points = glyph.points.concat(transformedPoints);
+            }
+        }
+    }
+
+    return getPath(glyph.points);
+}
+
+function parseGlyfTableAll(data, start, loca, font) {
+    var glyphs = new glyphset.GlyphSet(font);
+
+    // The last element of the loca table is invalid.
+    for (var i = 0; i < loca.length - 1; i += 1) {
+        var offset = loca[i];
+        var nextOffset = loca[i + 1];
+        if (offset !== nextOffset) {
+            glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath));
+        } else {
+            glyphs.push(i, glyphset.glyphLoader(font, i));
+        }
+    }
+
+    return glyphs;
+}
+
+function parseGlyfTableOnLowMemory(data, start, loca, font) {
+    var glyphs = new glyphset.GlyphSet(font);
+
+    font._push = function(i) {
+        var offset = loca[i];
+        var nextOffset = loca[i + 1];
+        if (offset !== nextOffset) {
+            glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath));
+        } else {
+            glyphs.push(i, glyphset.glyphLoader(font, i));
+        }
+    };
+
+    return glyphs;
+}
+
+// Parse all the glyphs according to the offsets from the `loca` table.
+function parseGlyfTable(data, start, loca, font, opt) {
+    if (opt.lowMemory)
+        { return parseGlyfTableOnLowMemory(data, start, loca, font); }
+    else
+        { return parseGlyfTableAll(data, start, loca, font); }
+}
+
+var glyf = { getPath: getPath, parse: parseGlyfTable};
+
+/* A TrueType font hinting interpreter.
+*
+* (c) 2017 Axel Kittenberger
+*
+* This interpreter has been implemented according to this documentation:
+* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM05/Chap5.html
+*
+* According to the documentation F24DOT6 values are used for pixels.
+* That means calculation is 1/64 pixel accurate and uses integer operations.
+* However, Javascript has floating point operations by default and only
+* those are available. One could make a case to simulate the 1/64 accuracy
+* exactly by truncating after every division operation
+* (for example with << 0) to get pixel exactly results as other TrueType
+* implementations. It may make sense since some fonts are pixel optimized
+* by hand using DELTAP instructions. The current implementation doesn't
+* and rather uses full floating point precision.
+*
+* xScale, yScale and rotation is currently ignored.
+*
+* A few non-trivial instructions are missing as I didn't encounter yet
+* a font that used them to test a possible implementation.
+*
+* Some fonts seem to use undocumented features regarding the twilight zone.
+* Only some of them are implemented as they were encountered.
+*
+* The exports.DEBUG statements are removed on the minified distribution file.
+*/
+
+var instructionTable;
+var exec;
+var execGlyph;
+var execComponent;
+
+/*
+* Creates a hinting object.
+*
+* There ought to be exactly one
+* for each truetype font that is used for hinting.
+*/
+function Hinting(font) {
+    // the font this hinting object is for
+    this.font = font;
+
+    this.getCommands = function (hPoints) {
+        return glyf.getPath(hPoints).commands;
+    };
+
+    // cached states
+    this._fpgmState  =
+    this._prepState  =
+        undefined;
+
+    // errorState
+    // 0 ... all okay
+    // 1 ... had an error in a glyf,
+    //       continue working but stop spamming
+    //       the console
+    // 2 ... error at prep, stop hinting at this ppem
+    // 3 ... error at fpeg, stop hinting for this font at all
+    this._errorState = 0;
+}
+
+/*
+* Not rounding.
+*/
+function roundOff(v) {
+    return v;
+}
+
+/*
+* Rounding to grid.
+*/
+function roundToGrid(v) {
+    //Rounding in TT is supposed to "symmetrical around zero"
+    return Math.sign(v) * Math.round(Math.abs(v));
+}
+
+/*
+* Rounding to double grid.
+*/
+function roundToDoubleGrid(v) {
+    return Math.sign(v) * Math.round(Math.abs(v * 2)) / 2;
+}
+
+/*
+* Rounding to half grid.
+*/
+function roundToHalfGrid(v) {
+    return Math.sign(v) * (Math.round(Math.abs(v) + 0.5) - 0.5);
+}
+
+/*
+* Rounding to up to grid.
+*/
+function roundUpToGrid(v) {
+    return Math.sign(v) * Math.ceil(Math.abs(v));
+}
+
+/*
+* Rounding to down to grid.
+*/
+function roundDownToGrid(v) {
+    return Math.sign(v) * Math.floor(Math.abs(v));
+}
+
+/*
+* Super rounding.
+*/
+var roundSuper = function (v) {
+    var period = this.srPeriod;
+    var phase = this.srPhase;
+    var threshold = this.srThreshold;
+    var sign = 1;
+
+    if (v < 0) {
+        v = -v;
+        sign = -1;
+    }
+
+    v += threshold - phase;
+
+    v = Math.trunc(v / period) * period;
+
+    v += phase;
+
+    // according to http://xgridfit.sourceforge.net/round.html
+    if (v < 0) { return phase * sign; }
+
+    return v * sign;
+};
+
+/*
+* Unit vector of x-axis.
+*/
+var xUnitVector = {
+    x: 1,
+
+    y: 0,
+
+    axis: 'x',
+
+    // Gets the projected distance between two points.
+    // o1/o2 ... if true, respective original position is used.
+    distance: function (p1, p2, o1, o2) {
+        return (o1 ? p1.xo : p1.x) - (o2 ? p2.xo : p2.x);
+    },
+
+    // Moves point p so the moved position has the same relative
+    // position to the moved positions of rp1 and rp2 than the
+    // original positions had.
+    //
+    // See APPENDIX on INTERPOLATE at the bottom of this file.
+    interpolate: function (p, rp1, rp2, pv) {
+        var do1;
+        var do2;
+        var doa1;
+        var doa2;
+        var dm1;
+        var dm2;
+        var dt;
+
+        if (!pv || pv === this) {
+            do1 = p.xo - rp1.xo;
+            do2 = p.xo - rp2.xo;
+            dm1 = rp1.x - rp1.xo;
+            dm2 = rp2.x - rp2.xo;
+            doa1 = Math.abs(do1);
+            doa2 = Math.abs(do2);
+            dt = doa1 + doa2;
+
+            if (dt === 0) {
+                p.x = p.xo + (dm1 + dm2) / 2;
+                return;
+            }
+
+            p.x = p.xo + (dm1 * doa2 + dm2 * doa1) / dt;
+            return;
+        }
+
+        do1 = pv.distance(p, rp1, true, true);
+        do2 = pv.distance(p, rp2, true, true);
+        dm1 = pv.distance(rp1, rp1, false, true);
+        dm2 = pv.distance(rp2, rp2, false, true);
+        doa1 = Math.abs(do1);
+        doa2 = Math.abs(do2);
+        dt = doa1 + doa2;
+
+        if (dt === 0) {
+            xUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
+            return;
+        }
+
+        xUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
+    },
+
+    // Slope of line normal to this
+    normalSlope: Number.NEGATIVE_INFINITY,
+
+    // Sets the point 'p' relative to point 'rp'
+    // by the distance 'd'.
+    //
+    // See APPENDIX on SETRELATIVE at the bottom of this file.
+    //
+    // p   ... point to set
+    // rp  ... reference point
+    // d   ... distance on projection vector
+    // pv  ... projection vector (undefined = this)
+    // org ... if true, uses the original position of rp as reference.
+    setRelative: function (p, rp, d, pv, org) {
+        if (!pv || pv === this) {
+            p.x = (org ? rp.xo : rp.x) + d;
+            return;
+        }
+
+        var rpx = org ? rp.xo : rp.x;
+        var rpy = org ? rp.yo : rp.y;
+        var rpdx = rpx + d * pv.x;
+        var rpdy = rpy + d * pv.y;
+
+        p.x = rpdx + (p.y - rpdy) / pv.normalSlope;
+    },
+
+    // Slope of vector line.
+    slope: 0,
+
+    // Touches the point p.
+    touch: function (p) {
+        p.xTouched = true;
+    },
+
+    // Tests if a point p is touched.
+    touched: function (p) {
+        return p.xTouched;
+    },
+
+    // Untouches the point p.
+    untouch: function (p) {
+        p.xTouched = false;
+    }
+};
+
+/*
+* Unit vector of y-axis.
+*/
+var yUnitVector = {
+    x: 0,
+
+    y: 1,
+
+    axis: 'y',
+
+    // Gets the projected distance between two points.
+    // o1/o2 ... if true, respective original position is used.
+    distance: function (p1, p2, o1, o2) {
+        return (o1 ? p1.yo : p1.y) - (o2 ? p2.yo : p2.y);
+    },
+
+    // Moves point p so the moved position has the same relative
+    // position to the moved positions of rp1 and rp2 than the
+    // original positions had.
+    //
+    // See APPENDIX on INTERPOLATE at the bottom of this file.
+    interpolate: function (p, rp1, rp2, pv) {
+        var do1;
+        var do2;
+        var doa1;
+        var doa2;
+        var dm1;
+        var dm2;
+        var dt;
+
+        if (!pv || pv === this) {
+            do1 = p.yo - rp1.yo;
+            do2 = p.yo - rp2.yo;
+            dm1 = rp1.y - rp1.yo;
+            dm2 = rp2.y - rp2.yo;
+            doa1 = Math.abs(do1);
+            doa2 = Math.abs(do2);
+            dt = doa1 + doa2;
+
+            if (dt === 0) {
+                p.y = p.yo + (dm1 + dm2) / 2;
+                return;
+            }
+
+            p.y = p.yo + (dm1 * doa2 + dm2 * doa1) / dt;
+            return;
+        }
+
+        do1 = pv.distance(p, rp1, true, true);
+        do2 = pv.distance(p, rp2, true, true);
+        dm1 = pv.distance(rp1, rp1, false, true);
+        dm2 = pv.distance(rp2, rp2, false, true);
+        doa1 = Math.abs(do1);
+        doa2 = Math.abs(do2);
+        dt = doa1 + doa2;
+
+        if (dt === 0) {
+            yUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
+            return;
+        }
+
+        yUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
+    },
+
+    // Slope of line normal to this.
+    normalSlope: 0,
+
+    // Sets the point 'p' relative to point 'rp'
+    // by the distance 'd'
+    //
+    // See APPENDIX on SETRELATIVE at the bottom of this file.
+    //
+    // p   ... point to set
+    // rp  ... reference point
+    // d   ... distance on projection vector
+    // pv  ... projection vector (undefined = this)
+    // org ... if true, uses the original position of rp as reference.
+    setRelative: function (p, rp, d, pv, org) {
+        if (!pv || pv === this) {
+            p.y = (org ? rp.yo : rp.y) + d;
+            return;
+        }
+
+        var rpx = org ? rp.xo : rp.x;
+        var rpy = org ? rp.yo : rp.y;
+        var rpdx = rpx + d * pv.x;
+        var rpdy = rpy + d * pv.y;
+
+        p.y = rpdy + pv.normalSlope * (p.x - rpdx);
+    },
+
+    // Slope of vector line.
+    slope: Number.POSITIVE_INFINITY,
+
+    // Touches the point p.
+    touch: function (p) {
+        p.yTouched = true;
+    },
+
+    // Tests if a point p is touched.
+    touched: function (p) {
+        return p.yTouched;
+    },
+
+    // Untouches the point p.
+    untouch: function (p) {
+        p.yTouched = false;
+    }
+};
+
+Object.freeze(xUnitVector);
+Object.freeze(yUnitVector);
+
+/*
+* Creates a unit vector that is not x- or y-axis.
+*/
+function UnitVector(x, y) {
+    this.x = x;
+    this.y = y;
+    this.axis = undefined;
+    this.slope = y / x;
+    this.normalSlope = -x / y;
+    Object.freeze(this);
+}
+
+/*
+* Gets the projected distance between two points.
+* o1/o2 ... if true, respective original position is used.
+*/
+UnitVector.prototype.distance = function(p1, p2, o1, o2) {
+    return (
+        this.x * xUnitVector.distance(p1, p2, o1, o2) +
+        this.y * yUnitVector.distance(p1, p2, o1, o2)
+    );
+};
+
+/*
+* Moves point p so the moved position has the same relative
+* position to the moved positions of rp1 and rp2 than the
+* original positions had.
+*
+* See APPENDIX on INTERPOLATE at the bottom of this file.
+*/
+UnitVector.prototype.interpolate = function(p, rp1, rp2, pv) {
+    var dm1;
+    var dm2;
+    var do1;
+    var do2;
+    var doa1;
+    var doa2;
+    var dt;
+
+    do1 = pv.distance(p, rp1, true, true);
+    do2 = pv.distance(p, rp2, true, true);
+    dm1 = pv.distance(rp1, rp1, false, true);
+    dm2 = pv.distance(rp2, rp2, false, true);
+    doa1 = Math.abs(do1);
+    doa2 = Math.abs(do2);
+    dt = doa1 + doa2;
+
+    if (dt === 0) {
+        this.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
+        return;
+    }
+
+    this.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
+};
+
+/*
+* Sets the point 'p' relative to point 'rp'
+* by the distance 'd'
+*
+* See APPENDIX on SETRELATIVE at the bottom of this file.
+*
+* p   ...  point to set
+* rp  ... reference point
+* d   ... distance on projection vector
+* pv  ... projection vector (undefined = this)
+* org ... if true, uses the original position of rp as reference.
+*/
+UnitVector.prototype.setRelative = function(p, rp, d, pv, org) {
+    pv = pv || this;
+
+    var rpx = org ? rp.xo : rp.x;
+    var rpy = org ? rp.yo : rp.y;
+    var rpdx = rpx + d * pv.x;
+    var rpdy = rpy + d * pv.y;
+
+    var pvns = pv.normalSlope;
+    var fvs = this.slope;
+
+    var px = p.x;
+    var py = p.y;
+
+    p.x = (fvs * px - pvns * rpdx + rpdy - py) / (fvs - pvns);
+    p.y = fvs * (p.x - px) + py;
+};
+
+/*
+* Touches the point p.
+*/
+UnitVector.prototype.touch = function(p) {
+    p.xTouched = true;
+    p.yTouched = true;
+};
+
+/*
+* Returns a unit vector with x/y coordinates.
+*/
+function getUnitVector(x, y) {
+    var d = Math.sqrt(x * x + y * y);
+
+    x /= d;
+    y /= d;
+
+    if (x === 1 && y === 0) { return xUnitVector; }
+    else if (x === 0 && y === 1) { return yUnitVector; }
+    else { return new UnitVector(x, y); }
+}
+
+/*
+* Creates a point in the hinting engine.
+*/
+function HPoint(
+    x,
+    y,
+    lastPointOfContour,
+    onCurve
+) {
+    this.x = this.xo = Math.round(x * 64) / 64; // hinted x value and original x-value
+    this.y = this.yo = Math.round(y * 64) / 64; // hinted y value and original y-value
+
+    this.lastPointOfContour = lastPointOfContour;
+    this.onCurve = onCurve;
+    this.prevPointOnContour = undefined;
+    this.nextPointOnContour = undefined;
+    this.xTouched = false;
+    this.yTouched = false;
+
+    Object.preventExtensions(this);
+}
+
+/*
+* Returns the next touched point on the contour.
+*
+* v  ... unit vector to test touch axis.
+*/
+HPoint.prototype.nextTouched = function(v) {
+    var p = this.nextPointOnContour;
+
+    while (!v.touched(p) && p !== this) { p = p.nextPointOnContour; }
+
+    return p;
+};
+
+/*
+* Returns the previous touched point on the contour
+*
+* v  ... unit vector to test touch axis.
+*/
+HPoint.prototype.prevTouched = function(v) {
+    var p = this.prevPointOnContour;
+
+    while (!v.touched(p) && p !== this) { p = p.prevPointOnContour; }
+
+    return p;
+};
+
+/*
+* The zero point.
+*/
+var HPZero = Object.freeze(new HPoint(0, 0));
+
+/*
+* The default state of the interpreter.
+*
+* Note: Freezing the defaultState and then deriving from it
+* makes the V8 Javascript engine going awkward,
+* so this is avoided, albeit the defaultState shouldn't
+* ever change.
+*/
+var defaultState = {
+    cvCutIn: 17 / 16,    // control value cut in
+    deltaBase: 9,
+    deltaShift: 0.125,
+    loop: 1,             // loops some instructions
+    minDis: 1,           // minimum distance
+    autoFlip: true
+};
+
+/*
+* The current state of the interpreter.
+*
+* env  ... 'fpgm' or 'prep' or 'glyf'
+* prog ... the program
+*/
+function State(env, prog) {
+    this.env = env;
+    this.stack = [];
+    this.prog = prog;
+
+    switch (env) {
+        case 'glyf' :
+            this.zp0 = this.zp1 = this.zp2 = 1;
+            this.rp0 = this.rp1 = this.rp2 = 0;
+            /* fall through */
+        case 'prep' :
+            this.fv = this.pv = this.dpv = xUnitVector;
+            this.round = roundToGrid;
+    }
+}
+
+/*
+* Executes a glyph program.
+*
+* This does the hinting for each glyph.
+*
+* Returns an array of moved points.
+*
+* glyph: the glyph to hint
+* ppem: the size the glyph is rendered for
+*/
+Hinting.prototype.exec = function(glyph, ppem) {
+    if (typeof ppem !== 'number') {
+        throw new Error('Point size is not a number!');
+    }
+
+    // Received a fatal error, don't do any hinting anymore.
+    if (this._errorState > 2) { return; }
+
+    var font = this.font;
+    var prepState = this._prepState;
+
+    if (!prepState || prepState.ppem !== ppem) {
+        var fpgmState = this._fpgmState;
+
+        if (!fpgmState) {
+            // Executes the fpgm state.
+            // This is used by fonts to define functions.
+            State.prototype = defaultState;
+
+            fpgmState =
+            this._fpgmState =
+                new State('fpgm', font.tables.fpgm);
+
+            fpgmState.funcs = [ ];
+            fpgmState.font = font;
+
+            if (exports.DEBUG) {
+                console.log('---EXEC FPGM---');
+                fpgmState.step = -1;
+            }
+
+            try {
+                exec(fpgmState);
+            } catch (e) {
+                console.log('Hinting error in FPGM:' + e);
+                this._errorState = 3;
+                return;
+            }
+        }
+
+        // Executes the prep program for this ppem setting.
+        // This is used by fonts to set cvt values
+        // depending on to be rendered font size.
+
+        State.prototype = fpgmState;
+        prepState =
+        this._prepState =
+            new State('prep', font.tables.prep);
+
+        prepState.ppem = ppem;
+
+        // Creates a copy of the cvt table
+        // and scales it to the current ppem setting.
+        var oCvt = font.tables.cvt;
+        if (oCvt) {
+            var cvt = prepState.cvt = new Array(oCvt.length);
+            var scale = ppem / font.unitsPerEm;
+            for (var c = 0; c < oCvt.length; c++) {
+                cvt[c] = oCvt[c] * scale;
+            }
+        } else {
+            prepState.cvt = [];
+        }
+
+        if (exports.DEBUG) {
+            console.log('---EXEC PREP---');
+            prepState.step = -1;
+        }
+
+        try {
+            exec(prepState);
+        } catch (e) {
+            if (this._errorState < 2) {
+                console.log('Hinting error in PREP:' + e);
+            }
+            this._errorState = 2;
+        }
+    }
+
+    if (this._errorState > 1) { return; }
+
+    try {
+        return execGlyph(glyph, prepState);
+    } catch (e) {
+        if (this._errorState < 1) {
+            console.log('Hinting error:' + e);
+            console.log('Note: further hinting errors are silenced');
+        }
+        this._errorState = 1;
+        return undefined;
+    }
+};
+
+/*
+* Executes the hinting program for a glyph.
+*/
+execGlyph = function(glyph, prepState) {
+    // original point positions
+    var xScale = prepState.ppem / prepState.font.unitsPerEm;
+    var yScale = xScale;
+    var components = glyph.components;
+    var contours;
+    var gZone;
+    var state;
+
+    State.prototype = prepState;
+    if (!components) {
+        state = new State('glyf', glyph.instructions);
+        if (exports.DEBUG) {
+            console.log('---EXEC GLYPH---');
+            state.step = -1;
+        }
+        execComponent(glyph, state, xScale, yScale);
+        gZone = state.gZone;
+    } else {
+        var font = prepState.font;
+        gZone = [];
+        contours = [];
+        for (var i = 0; i < components.length; i++) {
+            var c = components[i];
+            var cg = font.glyphs.get(c.glyphIndex);
+
+            state = new State('glyf', cg.instructions);
+
+            if (exports.DEBUG) {
+                console.log('---EXEC COMP ' + i + '---');
+                state.step = -1;
+            }
+
+            execComponent(cg, state, xScale, yScale);
+            // appends the computed points to the result array
+            // post processes the component points
+            var dx = Math.round(c.dx * xScale);
+            var dy = Math.round(c.dy * yScale);
+            var gz = state.gZone;
+            var cc = state.contours;
+            for (var pi = 0; pi < gz.length; pi++) {
+                var p = gz[pi];
+                p.xTouched = p.yTouched = false;
+                p.xo = p.x = p.x + dx;
+                p.yo = p.y = p.y + dy;
+            }
+
+            var gLen = gZone.length;
+            gZone.push.apply(gZone, gz);
+            for (var j = 0; j < cc.length; j++) {
+                contours.push(cc[j] + gLen);
+            }
+        }
+
+        if (glyph.instructions && !state.inhibitGridFit) {
+            // the composite has instructions on its own
+            state = new State('glyf', glyph.instructions);
+
+            state.gZone = state.z0 = state.z1 = state.z2 = gZone;
+
+            state.contours = contours;
+
+            // note: HPZero cannot be used here, since
+            //       the point might be modified
+            gZone.push(
+                new HPoint(0, 0),
+                new HPoint(Math.round(glyph.advanceWidth * xScale), 0)
+            );
+
+            if (exports.DEBUG) {
+                console.log('---EXEC COMPOSITE---');
+                state.step = -1;
+            }
+
+            exec(state);
+
+            gZone.length -= 2;
+        }
+    }
+
+    return gZone;
+};
+
+/*
+* Executes the hinting program for a component of a multi-component glyph
+* or of the glyph itself for a non-component glyph.
+*/
+execComponent = function(glyph, state, xScale, yScale)
+{
+    var points = glyph.points || [];
+    var pLen = points.length;
+    var gZone = state.gZone = state.z0 = state.z1 = state.z2 = [];
+    var contours = state.contours = [];
+
+    // Scales the original points and
+    // makes copies for the hinted points.
+    var cp; // current point
+    for (var i = 0; i < pLen; i++) {
+        cp = points[i];
+
+        gZone[i] = new HPoint(
+            cp.x * xScale,
+            cp.y * yScale,
+            cp.lastPointOfContour,
+            cp.onCurve
+        );
+    }
+
+    // Chain links the contours.
+    var sp; // start point
+    var np; // next point
+
+    for (var i$1 = 0; i$1 < pLen; i$1++) {
+        cp = gZone[i$1];
+
+        if (!sp) {
+            sp = cp;
+            contours.push(i$1);
+        }
+
+        if (cp.lastPointOfContour) {
+            cp.nextPointOnContour = sp;
+            sp.prevPointOnContour = cp;
+            sp = undefined;
+        } else {
+            np = gZone[i$1 + 1];
+            cp.nextPointOnContour = np;
+            np.prevPointOnContour = cp;
+        }
+    }
+
+    if (state.inhibitGridFit) { return; }
+
+    if (exports.DEBUG) {
+        console.log('PROCESSING GLYPH', state.stack);
+        for (var i$2 = 0; i$2 < pLen; i$2++) {
+            console.log(i$2, gZone[i$2].x, gZone[i$2].y);
+        }
+    }
+
+    gZone.push(
+        new HPoint(0, 0),
+        new HPoint(Math.round(glyph.advanceWidth * xScale), 0)
+    );
+
+    exec(state);
+
+    // Removes the extra points.
+    gZone.length -= 2;
+
+    if (exports.DEBUG) {
+        console.log('FINISHED GLYPH', state.stack);
+        for (var i$3 = 0; i$3 < pLen; i$3++) {
+            console.log(i$3, gZone[i$3].x, gZone[i$3].y);
+        }
+    }
+};
+
+/*
+* Executes the program loaded in state.
+*/
+exec = function(state) {
+    var prog = state.prog;
+
+    if (!prog) { return; }
+
+    var pLen = prog.length;
+    var ins;
+
+    for (state.ip = 0; state.ip < pLen; state.ip++) {
+        if (exports.DEBUG) { state.step++; }
+        ins = instructionTable[prog[state.ip]];
+
+        if (!ins) {
+            throw new Error(
+                'unknown instruction: 0x' +
+                Number(prog[state.ip]).toString(16)
+            );
+        }
+
+        ins(state);
+
+        // very extensive debugging for each step
+        /*
+        if (exports.DEBUG) {
+            var da;
+            if (state.gZone) {
+                da = [];
+                for (let i = 0; i < state.gZone.length; i++)
+                {
+                    da.push(i + ' ' +
+                        state.gZone[i].x * 64 + ' ' +
+                        state.gZone[i].y * 64 + ' ' +
+                        (state.gZone[i].xTouched ? 'x' : '') +
+                        (state.gZone[i].yTouched ? 'y' : '')
+                    );
+                }
+                console.log('GZ', da);
+            }
+
+            if (state.tZone) {
+                da = [];
+                for (let i = 0; i < state.tZone.length; i++) {
+                    da.push(i + ' ' +
+                        state.tZone[i].x * 64 + ' ' +
+                        state.tZone[i].y * 64 + ' ' +
+                        (state.tZone[i].xTouched ? 'x' : '') +
+                        (state.tZone[i].yTouched ? 'y' : '')
+                    );
+                }
+                console.log('TZ', da);
+            }
+
+            if (state.stack.length > 10) {
+                console.log(
+                    state.stack.length,
+                    '...', state.stack.slice(state.stack.length - 10)
+                );
+            } else {
+                console.log(state.stack.length, state.stack);
+            }
+        }
+        */
+    }
+};
+
+/*
+* Initializes the twilight zone.
+*
+* This is only done if a SZPx instruction
+* refers to the twilight zone.
+*/
+function initTZone(state)
+{
+    var tZone = state.tZone = new Array(state.gZone.length);
+
+    // no idea if this is actually correct...
+    for (var i = 0; i < tZone.length; i++)
+    {
+        tZone[i] = new HPoint(0, 0);
+    }
+}
+
+/*
+* Skips the instruction pointer ahead over an IF/ELSE block.
+* handleElse .. if true breaks on matching ELSE
+*/
+function skip(state, handleElse)
+{
+    var prog = state.prog;
+    var ip = state.ip;
+    var nesting = 1;
+    var ins;
+
+    do {
+        ins = prog[++ip];
+        if (ins === 0x58) // IF
+            { nesting++; }
+        else if (ins === 0x59) // EIF
+            { nesting--; }
+        else if (ins === 0x40) // NPUSHB
+            { ip += prog[ip + 1] + 1; }
+        else if (ins === 0x41) // NPUSHW
+            { ip += 2 * prog[ip + 1] + 1; }
+        else if (ins >= 0xB0 && ins <= 0xB7) // PUSHB
+            { ip += ins - 0xB0 + 1; }
+        else if (ins >= 0xB8 && ins <= 0xBF) // PUSHW
+            { ip += (ins - 0xB8 + 1) * 2; }
+        else if (handleElse && nesting === 1 && ins === 0x1B) // ELSE
+            { break; }
+    } while (nesting > 0);
+
+    state.ip = ip;
+}
+
+/*----------------------------------------------------------*
+*          And then a lot of instructions...                *
+*----------------------------------------------------------*/
+
+// SVTCA[a] Set freedom and projection Vectors To Coordinate Axis
+// 0x00-0x01
+function SVTCA(v, state) {
+    if (exports.DEBUG) { console.log(state.step, 'SVTCA[' + v.axis + ']'); }
+
+    state.fv = state.pv = state.dpv = v;
+}
+
+// SPVTCA[a] Set Projection Vector to Coordinate Axis
+// 0x02-0x03
+function SPVTCA(v, state) {
+    if (exports.DEBUG) { console.log(state.step, 'SPVTCA[' + v.axis + ']'); }
+
+    state.pv = state.dpv = v;
+}
+
+// SFVTCA[a] Set Freedom Vector to Coordinate Axis
+// 0x04-0x05
+function SFVTCA(v, state) {
+    if (exports.DEBUG) { console.log(state.step, 'SFVTCA[' + v.axis + ']'); }
+
+    state.fv = v;
+}
+
+// SPVTL[a] Set Projection Vector To Line
+// 0x06-0x07
+function SPVTL(a, state) {
+    var stack = state.stack;
+    var p2i = stack.pop();
+    var p1i = stack.pop();
+    var p2 = state.z2[p2i];
+    var p1 = state.z1[p1i];
+
+    if (exports.DEBUG) { console.log('SPVTL[' + a + ']', p2i, p1i); }
+
+    var dx;
+    var dy;
+
+    if (!a) {
+        dx = p1.x - p2.x;
+        dy = p1.y - p2.y;
+    } else {
+        dx = p2.y - p1.y;
+        dy = p1.x - p2.x;
+    }
+
+    state.pv = state.dpv = getUnitVector(dx, dy);
+}
+
+// SFVTL[a] Set Freedom Vector To Line
+// 0x08-0x09
+function SFVTL(a, state) {
+    var stack = state.stack;
+    var p2i = stack.pop();
+    var p1i = stack.pop();
+    var p2 = state.z2[p2i];
+    var p1 = state.z1[p1i];
+
+    if (exports.DEBUG) { console.log('SFVTL[' + a + ']', p2i, p1i); }
+
+    var dx;
+    var dy;
+
+    if (!a) {
+        dx = p1.x - p2.x;
+        dy = p1.y - p2.y;
+    } else {
+        dx = p2.y - p1.y;
+        dy = p1.x - p2.x;
+    }
+
+    state.fv = getUnitVector(dx, dy);
+}
+
+// SPVFS[] Set Projection Vector From Stack
+// 0x0A
+function SPVFS(state) {
+    var stack = state.stack;
+    var y = stack.pop();
+    var x = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SPVFS[]', y, x); }
+
+    state.pv = state.dpv = getUnitVector(x, y);
+}
+
+// SFVFS[] Set Freedom Vector From Stack
+// 0x0B
+function SFVFS(state) {
+    var stack = state.stack;
+    var y = stack.pop();
+    var x = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SPVFS[]', y, x); }
+
+    state.fv = getUnitVector(x, y);
+}
+
+// GPV[] Get Projection Vector
+// 0x0C
+function GPV(state) {
+    var stack = state.stack;
+    var pv = state.pv;
+
+    if (exports.DEBUG) { console.log(state.step, 'GPV[]'); }
+
+    stack.push(pv.x * 0x4000);
+    stack.push(pv.y * 0x4000);
+}
+
+// GFV[] Get Freedom Vector
+// 0x0C
+function GFV(state) {
+    var stack = state.stack;
+    var fv = state.fv;
+
+    if (exports.DEBUG) { console.log(state.step, 'GFV[]'); }
+
+    stack.push(fv.x * 0x4000);
+    stack.push(fv.y * 0x4000);
+}
+
+// SFVTPV[] Set Freedom Vector To Projection Vector
+// 0x0E
+function SFVTPV(state) {
+    state.fv = state.pv;
+
+    if (exports.DEBUG) { console.log(state.step, 'SFVTPV[]'); }
+}
+
+// ISECT[] moves point p to the InterSECTion of two lines
+// 0x0F
+function ISECT(state)
+{
+    var stack = state.stack;
+    var pa0i = stack.pop();
+    var pa1i = stack.pop();
+    var pb0i = stack.pop();
+    var pb1i = stack.pop();
+    var pi = stack.pop();
+    var z0 = state.z0;
+    var z1 = state.z1;
+    var pa0 = z0[pa0i];
+    var pa1 = z0[pa1i];
+    var pb0 = z1[pb0i];
+    var pb1 = z1[pb1i];
+    var p = state.z2[pi];
+
+    if (exports.DEBUG) { console.log('ISECT[], ', pa0i, pa1i, pb0i, pb1i, pi); }
+
+    // math from
+    // en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
+
+    var x1 = pa0.x;
+    var y1 = pa0.y;
+    var x2 = pa1.x;
+    var y2 = pa1.y;
+    var x3 = pb0.x;
+    var y3 = pb0.y;
+    var x4 = pb1.x;
+    var y4 = pb1.y;
+
+    var div = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
+    var f1 = x1 * y2 - y1 * x2;
+    var f2 = x3 * y4 - y3 * x4;
+
+    p.x = (f1 * (x3 - x4) - f2 * (x1 - x2)) / div;
+    p.y = (f1 * (y3 - y4) - f2 * (y1 - y2)) / div;
+}
+
+// SRP0[] Set Reference Point 0
+// 0x10
+function SRP0(state) {
+    state.rp0 = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SRP0[]', state.rp0); }
+}
+
+// SRP1[] Set Reference Point 1
+// 0x11
+function SRP1(state) {
+    state.rp1 = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SRP1[]', state.rp1); }
+}
+
+// SRP1[] Set Reference Point 2
+// 0x12
+function SRP2(state) {
+    state.rp2 = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SRP2[]', state.rp2); }
+}
+
+// SZP0[] Set Zone Pointer 0
+// 0x13
+function SZP0(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SZP0[]', n); }
+
+    state.zp0 = n;
+
+    switch (n) {
+        case 0:
+            if (!state.tZone) { initTZone(state); }
+            state.z0 = state.tZone;
+            break;
+        case 1 :
+            state.z0 = state.gZone;
+            break;
+        default :
+            throw new Error('Invalid zone pointer');
+    }
+}
+
+// SZP1[] Set Zone Pointer 1
+// 0x14
+function SZP1(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SZP1[]', n); }
+
+    state.zp1 = n;
+
+    switch (n) {
+        case 0:
+            if (!state.tZone) { initTZone(state); }
+            state.z1 = state.tZone;
+            break;
+        case 1 :
+            state.z1 = state.gZone;
+            break;
+        default :
+            throw new Error('Invalid zone pointer');
+    }
+}
+
+// SZP2[] Set Zone Pointer 2
+// 0x15
+function SZP2(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SZP2[]', n); }
+
+    state.zp2 = n;
+
+    switch (n) {
+        case 0:
+            if (!state.tZone) { initTZone(state); }
+            state.z2 = state.tZone;
+            break;
+        case 1 :
+            state.z2 = state.gZone;
+            break;
+        default :
+            throw new Error('Invalid zone pointer');
+    }
+}
+
+// SZPS[] Set Zone PointerS
+// 0x16
+function SZPS(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SZPS[]', n); }
+
+    state.zp0 = state.zp1 = state.zp2 = n;
+
+    switch (n) {
+        case 0:
+            if (!state.tZone) { initTZone(state); }
+            state.z0 = state.z1 = state.z2 = state.tZone;
+            break;
+        case 1 :
+            state.z0 = state.z1 = state.z2 = state.gZone;
+            break;
+        default :
+            throw new Error('Invalid zone pointer');
+    }
+}
+
+// SLOOP[] Set LOOP variable
+// 0x17
+function SLOOP(state) {
+    state.loop = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SLOOP[]', state.loop); }
+}
+
+// RTG[] Round To Grid
+// 0x18
+function RTG(state) {
+    if (exports.DEBUG) { console.log(state.step, 'RTG[]'); }
+
+    state.round = roundToGrid;
+}
+
+// RTHG[] Round To Half Grid
+// 0x19
+function RTHG(state) {
+    if (exports.DEBUG) { console.log(state.step, 'RTHG[]'); }
+
+    state.round = roundToHalfGrid;
+}
+
+// SMD[] Set Minimum Distance
+// 0x1A
+function SMD(state) {
+    var d = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SMD[]', d); }
+
+    state.minDis = d / 0x40;
+}
+
+// ELSE[] ELSE clause
+// 0x1B
+function ELSE(state) {
+    // This instruction has been reached by executing a then branch
+    // so it just skips ahead until matching EIF.
+    //
+    // In case the IF was negative the IF[] instruction already
+    // skipped forward over the ELSE[]
+
+    if (exports.DEBUG) { console.log(state.step, 'ELSE[]'); }
+
+    skip(state, false);
+}
+
+// JMPR[] JuMP Relative
+// 0x1C
+function JMPR(state) {
+    var o = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'JMPR[]', o); }
+
+    // A jump by 1 would do nothing.
+    state.ip += o - 1;
+}
+
+// SCVTCI[] Set Control Value Table Cut-In
+// 0x1D
+function SCVTCI(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SCVTCI[]', n); }
+
+    state.cvCutIn = n / 0x40;
+}
+
+// DUP[] DUPlicate top stack element
+// 0x20
+function DUP(state) {
+    var stack = state.stack;
+
+    if (exports.DEBUG) { console.log(state.step, 'DUP[]'); }
+
+    stack.push(stack[stack.length - 1]);
+}
+
+// POP[] POP top stack element
+// 0x21
+function POP(state) {
+    if (exports.DEBUG) { console.log(state.step, 'POP[]'); }
+
+    state.stack.pop();
+}
+
+// CLEAR[] CLEAR the stack
+// 0x22
+function CLEAR(state) {
+    if (exports.DEBUG) { console.log(state.step, 'CLEAR[]'); }
+
+    state.stack.length = 0;
+}
+
+// SWAP[] SWAP the top two elements on the stack
+// 0x23
+function SWAP(state) {
+    var stack = state.stack;
+
+    var a = stack.pop();
+    var b = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SWAP[]'); }
+
+    stack.push(a);
+    stack.push(b);
+}
+
+// DEPTH[] DEPTH of the stack
+// 0x24
+function DEPTH(state) {
+    var stack = state.stack;
+
+    if (exports.DEBUG) { console.log(state.step, 'DEPTH[]'); }
+
+    stack.push(stack.length);
+}
+
+// LOOPCALL[] LOOPCALL function
+// 0x2A
+function LOOPCALL(state) {
+    var stack = state.stack;
+    var fn = stack.pop();
+    var c = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'LOOPCALL[]', fn, c); }
+
+    // saves callers program
+    var cip = state.ip;
+    var cprog = state.prog;
+
+    state.prog = state.funcs[fn];
+
+    // executes the function
+    for (var i = 0; i < c; i++) {
+        exec(state);
+
+        if (exports.DEBUG) { console.log(
+            ++state.step,
+            i + 1 < c ? 'next loopcall' : 'done loopcall',
+            i
+        ); }
+    }
+
+    // restores the callers program
+    state.ip = cip;
+    state.prog = cprog;
+}
+
+// CALL[] CALL function
+// 0x2B
+function CALL(state) {
+    var fn = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'CALL[]', fn); }
+
+    // saves callers program
+    var cip = state.ip;
+    var cprog = state.prog;
+
+    state.prog = state.funcs[fn];
+
+    // executes the function
+    exec(state);
+
+    // restores the callers program
+    state.ip = cip;
+    state.prog = cprog;
+
+    if (exports.DEBUG) { console.log(++state.step, 'returning from', fn); }
+}
+
+// CINDEX[] Copy the INDEXed element to the top of the stack
+// 0x25
+function CINDEX(state) {
+    var stack = state.stack;
+    var k = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'CINDEX[]', k); }
+
+    // In case of k == 1, it copies the last element after popping
+    // thus stack.length - k.
+    stack.push(stack[stack.length - k]);
+}
+
+// MINDEX[] Move the INDEXed element to the top of the stack
+// 0x26
+function MINDEX(state) {
+    var stack = state.stack;
+    var k = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'MINDEX[]', k); }
+
+    stack.push(stack.splice(stack.length - k, 1)[0]);
+}
+
+// FDEF[] Function DEFinition
+// 0x2C
+function FDEF(state) {
+    if (state.env !== 'fpgm') { throw new Error('FDEF not allowed here'); }
+    var stack = state.stack;
+    var prog = state.prog;
+    var ip = state.ip;
+
+    var fn = stack.pop();
+    var ipBegin = ip;
+
+    if (exports.DEBUG) { console.log(state.step, 'FDEF[]', fn); }
+
+    while (prog[++ip] !== 0x2D){ }
+
+    state.ip = ip;
+    state.funcs[fn] = prog.slice(ipBegin + 1, ip);
+}
+
+// MDAP[a] Move Direct Absolute Point
+// 0x2E-0x2F
+function MDAP(round, state) {
+    var pi = state.stack.pop();
+    var p = state.z0[pi];
+    var fv = state.fv;
+    var pv = state.pv;
+
+    if (exports.DEBUG) { console.log(state.step, 'MDAP[' + round + ']', pi); }
+
+    var d = pv.distance(p, HPZero);
+
+    if (round) { d = state.round(d); }
+
+    fv.setRelative(p, HPZero, d, pv);
+    fv.touch(p);
+
+    state.rp0 = state.rp1 = pi;
+}
+
+// IUP[a] Interpolate Untouched Points through the outline
+// 0x30
+function IUP(v, state) {
+    var z2 = state.z2;
+    var pLen = z2.length - 2;
+    var cp;
+    var pp;
+    var np;
+
+    if (exports.DEBUG) { console.log(state.step, 'IUP[' + v.axis + ']'); }
+
+    for (var i = 0; i < pLen; i++) {
+        cp = z2[i]; // current point
+
+        // if this point has been touched go on
+        if (v.touched(cp)) { continue; }
+
+        pp = cp.prevTouched(v);
+
+        // no point on the contour has been touched?
+        if (pp === cp) { continue; }
+
+        np = cp.nextTouched(v);
+
+        if (pp === np) {
+            // only one point on the contour has been touched
+            // so simply moves the point like that
+
+            v.setRelative(cp, cp, v.distance(pp, pp, false, true), v, true);
+        }
+
+        v.interpolate(cp, pp, np, v);
+    }
+}
+
+// SHP[] SHift Point using reference point
+// 0x32-0x33
+function SHP(a, state) {
+    var stack = state.stack;
+    var rpi = a ? state.rp1 : state.rp2;
+    var rp = (a ? state.z0 : state.z1)[rpi];
+    var fv = state.fv;
+    var pv = state.pv;
+    var loop = state.loop;
+    var z2 = state.z2;
+
+    while (loop--)
+    {
+        var pi = stack.pop();
+        var p = z2[pi];
+
+        var d = pv.distance(rp, rp, false, true);
+        fv.setRelative(p, p, d, pv);
+        fv.touch(p);
+
+        if (exports.DEBUG) {
+            console.log(
+                state.step,
+                (state.loop > 1 ?
+                   'loop ' + (state.loop - loop) + ': ' :
+                   ''
+                ) +
+                'SHP[' + (a ? 'rp1' : 'rp2') + ']', pi
+            );
+        }
+    }
+
+    state.loop = 1;
+}
+
+// SHC[] SHift Contour using reference point
+// 0x36-0x37
+function SHC(a, state) {
+    var stack = state.stack;
+    var rpi = a ? state.rp1 : state.rp2;
+    var rp = (a ? state.z0 : state.z1)[rpi];
+    var fv = state.fv;
+    var pv = state.pv;
+    var ci = stack.pop();
+    var sp = state.z2[state.contours[ci]];
+    var p = sp;
+
+    if (exports.DEBUG) { console.log(state.step, 'SHC[' + a + ']', ci); }
+
+    var d = pv.distance(rp, rp, false, true);
+
+    do {
+        if (p !== rp) { fv.setRelative(p, p, d, pv); }
+        p = p.nextPointOnContour;
+    } while (p !== sp);
+}
+
+// SHZ[] SHift Zone using reference point
+// 0x36-0x37
+function SHZ(a, state) {
+    var stack = state.stack;
+    var rpi = a ? state.rp1 : state.rp2;
+    var rp = (a ? state.z0 : state.z1)[rpi];
+    var fv = state.fv;
+    var pv = state.pv;
+
+    var e = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SHZ[' + a + ']', e); }
+
+    var z;
+    switch (e) {
+        case 0 : z = state.tZone; break;
+        case 1 : z = state.gZone; break;
+        default : throw new Error('Invalid zone');
+    }
+
+    var p;
+    var d = pv.distance(rp, rp, false, true);
+    var pLen = z.length - 2;
+    for (var i = 0; i < pLen; i++)
+    {
+        p = z[i];
+        fv.setRelative(p, p, d, pv);
+        //if (p !== rp) fv.setRelative(p, p, d, pv);
+    }
+}
+
+// SHPIX[] SHift point by a PIXel amount
+// 0x38
+function SHPIX(state) {
+    var stack = state.stack;
+    var loop = state.loop;
+    var fv = state.fv;
+    var d = stack.pop() / 0x40;
+    var z2 = state.z2;
+
+    while (loop--) {
+        var pi = stack.pop();
+        var p = z2[pi];
+
+        if (exports.DEBUG) {
+            console.log(
+                state.step,
+                (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
+                'SHPIX[]', pi, d
+            );
+        }
+
+        fv.setRelative(p, p, d);
+        fv.touch(p);
+    }
+
+    state.loop = 1;
+}
+
+// IP[] Interpolate Point
+// 0x39
+function IP(state) {
+    var stack = state.stack;
+    var rp1i = state.rp1;
+    var rp2i = state.rp2;
+    var loop = state.loop;
+    var rp1 = state.z0[rp1i];
+    var rp2 = state.z1[rp2i];
+    var fv = state.fv;
+    var pv = state.dpv;
+    var z2 = state.z2;
+
+    while (loop--) {
+        var pi = stack.pop();
+        var p = z2[pi];
+
+        if (exports.DEBUG) {
+            console.log(
+                state.step,
+                (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
+                'IP[]', pi, rp1i, '<->', rp2i
+            );
+        }
+
+        fv.interpolate(p, rp1, rp2, pv);
+
+        fv.touch(p);
+    }
+
+    state.loop = 1;
+}
+
+// MSIRP[a] Move Stack Indirect Relative Point
+// 0x3A-0x3B
+function MSIRP(a, state) {
+    var stack = state.stack;
+    var d = stack.pop() / 64;
+    var pi = stack.pop();
+    var p = state.z1[pi];
+    var rp0 = state.z0[state.rp0];
+    var fv = state.fv;
+    var pv = state.pv;
+
+    fv.setRelative(p, rp0, d, pv);
+    fv.touch(p);
+
+    if (exports.DEBUG) { console.log(state.step, 'MSIRP[' + a + ']', d, pi); }
+
+    state.rp1 = state.rp0;
+    state.rp2 = pi;
+    if (a) { state.rp0 = pi; }
+}
+
+// ALIGNRP[] Align to reference point.
+// 0x3C
+function ALIGNRP(state) {
+    var stack = state.stack;
+    var rp0i = state.rp0;
+    var rp0 = state.z0[rp0i];
+    var loop = state.loop;
+    var fv = state.fv;
+    var pv = state.pv;
+    var z1 = state.z1;
+
+    while (loop--) {
+        var pi = stack.pop();
+        var p = z1[pi];
+
+        if (exports.DEBUG) {
+            console.log(
+                state.step,
+                (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
+                'ALIGNRP[]', pi
+            );
+        }
+
+        fv.setRelative(p, rp0, 0, pv);
+        fv.touch(p);
+    }
+
+    state.loop = 1;
+}
+
+// RTG[] Round To Double Grid
+// 0x3D
+function RTDG(state) {
+    if (exports.DEBUG) { console.log(state.step, 'RTDG[]'); }
+
+    state.round = roundToDoubleGrid;
+}
+
+// MIAP[a] Move Indirect Absolute Point
+// 0x3E-0x3F
+function MIAP(round, state) {
+    var stack = state.stack;
+    var n = stack.pop();
+    var pi = stack.pop();
+    var p = state.z0[pi];
+    var fv = state.fv;
+    var pv = state.pv;
+    var cv = state.cvt[n];
+
+    if (exports.DEBUG) {
+        console.log(
+            state.step,
+            'MIAP[' + round + ']',
+            n, '(', cv, ')', pi
+        );
+    }
+
+    var d = pv.distance(p, HPZero);
+
+    if (round) {
+        if (Math.abs(d - cv) < state.cvCutIn) { d = cv; }
+
+        d = state.round(d);
+    }
+
+    fv.setRelative(p, HPZero, d, pv);
+
+    if (state.zp0 === 0) {
+        p.xo = p.x;
+        p.yo = p.y;
+    }
+
+    fv.touch(p);
+
+    state.rp0 = state.rp1 = pi;
+}
+
+// NPUSB[] PUSH N Bytes
+// 0x40
+function NPUSHB(state) {
+    var prog = state.prog;
+    var ip = state.ip;
+    var stack = state.stack;
+
+    var n = prog[++ip];
+
+    if (exports.DEBUG) { console.log(state.step, 'NPUSHB[]', n); }
+
+    for (var i = 0; i < n; i++) { stack.push(prog[++ip]); }
+
+    state.ip = ip;
+}
+
+// NPUSHW[] PUSH N Words
+// 0x41
+function NPUSHW(state) {
+    var ip = state.ip;
+    var prog = state.prog;
+    var stack = state.stack;
+    var n = prog[++ip];
+
+    if (exports.DEBUG) { console.log(state.step, 'NPUSHW[]', n); }
+
+    for (var i = 0; i < n; i++) {
+        var w = (prog[++ip] << 8) | prog[++ip];
+        if (w & 0x8000) { w = -((w ^ 0xffff) + 1); }
+        stack.push(w);
+    }
+
+    state.ip = ip;
+}
+
+// WS[] Write Store
+// 0x42
+function WS(state) {
+    var stack = state.stack;
+    var store = state.store;
+
+    if (!store) { store = state.store = []; }
+
+    var v = stack.pop();
+    var l = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'WS', v, l); }
+
+    store[l] = v;
+}
+
+// RS[] Read Store
+// 0x43
+function RS(state) {
+    var stack = state.stack;
+    var store = state.store;
+
+    var l = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'RS', l); }
+
+    var v = (store && store[l]) || 0;
+
+    stack.push(v);
+}
+
+// WCVTP[] Write Control Value Table in Pixel units
+// 0x44
+function WCVTP(state) {
+    var stack = state.stack;
+
+    var v = stack.pop();
+    var l = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'WCVTP', v, l); }
+
+    state.cvt[l] = v / 0x40;
+}
+
+// RCVT[] Read Control Value Table entry
+// 0x45
+function RCVT(state) {
+    var stack = state.stack;
+    var cvte = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'RCVT', cvte); }
+
+    stack.push(state.cvt[cvte] * 0x40);
+}
+
+// GC[] Get Coordinate projected onto the projection vector
+// 0x46-0x47
+function GC(a, state) {
+    var stack = state.stack;
+    var pi = stack.pop();
+    var p = state.z2[pi];
+
+    if (exports.DEBUG) { console.log(state.step, 'GC[' + a + ']', pi); }
+
+    stack.push(state.dpv.distance(p, HPZero, a, false) * 0x40);
+}
+
+// MD[a] Measure Distance
+// 0x49-0x4A
+function MD(a, state) {
+    var stack = state.stack;
+    var pi2 = stack.pop();
+    var pi1 = stack.pop();
+    var p2 = state.z1[pi2];
+    var p1 = state.z0[pi1];
+    var d = state.dpv.distance(p1, p2, a, a);
+
+    if (exports.DEBUG) { console.log(state.step, 'MD[' + a + ']', pi2, pi1, '->', d); }
+
+    state.stack.push(Math.round(d * 64));
+}
+
+// MPPEM[] Measure Pixels Per EM
+// 0x4B
+function MPPEM(state) {
+    if (exports.DEBUG) { console.log(state.step, 'MPPEM[]'); }
+    state.stack.push(state.ppem);
+}
+
+// FLIPON[] set the auto FLIP Boolean to ON
+// 0x4D
+function FLIPON(state) {
+    if (exports.DEBUG) { console.log(state.step, 'FLIPON[]'); }
+    state.autoFlip = true;
+}
+
+// LT[] Less Than
+// 0x50
+function LT(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'LT[]', e2, e1); }
+
+    stack.push(e1 < e2 ? 1 : 0);
+}
+
+// LTEQ[] Less Than or EQual
+// 0x53
+function LTEQ(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'LTEQ[]', e2, e1); }
+
+    stack.push(e1 <= e2 ? 1 : 0);
+}
+
+// GTEQ[] Greater Than
+// 0x52
+function GT(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'GT[]', e2, e1); }
+
+    stack.push(e1 > e2 ? 1 : 0);
+}
+
+// GTEQ[] Greater Than or EQual
+// 0x53
+function GTEQ(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'GTEQ[]', e2, e1); }
+
+    stack.push(e1 >= e2 ? 1 : 0);
+}
+
+// EQ[] EQual
+// 0x54
+function EQ(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'EQ[]', e2, e1); }
+
+    stack.push(e2 === e1 ? 1 : 0);
+}
+
+// NEQ[] Not EQual
+// 0x55
+function NEQ(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'NEQ[]', e2, e1); }
+
+    stack.push(e2 !== e1 ? 1 : 0);
+}
+
+// ODD[] ODD
+// 0x56
+function ODD(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'ODD[]', n); }
+
+    stack.push(Math.trunc(n) % 2 ? 1 : 0);
+}
+
+// EVEN[] EVEN
+// 0x57
+function EVEN(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'EVEN[]', n); }
+
+    stack.push(Math.trunc(n) % 2 ? 0 : 1);
+}
+
+// IF[] IF test
+// 0x58
+function IF(state) {
+    var test = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'IF[]', test); }
+
+    // if test is true it just continues
+    // if not the ip is skipped until matching ELSE or EIF
+    if (!test) {
+        skip(state, true);
+
+        if (exports.DEBUG) { console.log(state.step,  'EIF[]'); }
+    }
+}
+
+// EIF[] End IF
+// 0x59
+function EIF(state) {
+    // this can be reached normally when
+    // executing an else branch.
+    // -> just ignore it
+
+    if (exports.DEBUG) { console.log(state.step, 'EIF[]'); }
+}
+
+// AND[] logical AND
+// 0x5A
+function AND(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'AND[]', e2, e1); }
+
+    stack.push(e2 && e1 ? 1 : 0);
+}
+
+// OR[] logical OR
+// 0x5B
+function OR(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'OR[]', e2, e1); }
+
+    stack.push(e2 || e1 ? 1 : 0);
+}
+
+// NOT[] logical NOT
+// 0x5C
+function NOT(state) {
+    var stack = state.stack;
+    var e = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'NOT[]', e); }
+
+    stack.push(e ? 0 : 1);
+}
+
+// DELTAP1[] DELTA exception P1
+// DELTAP2[] DELTA exception P2
+// DELTAP3[] DELTA exception P3
+// 0x5D, 0x71, 0x72
+function DELTAP123(b, state) {
+    var stack = state.stack;
+    var n = stack.pop();
+    var fv = state.fv;
+    var pv = state.pv;
+    var ppem = state.ppem;
+    var base = state.deltaBase + (b - 1) * 16;
+    var ds = state.deltaShift;
+    var z0 = state.z0;
+
+    if (exports.DEBUG) { console.log(state.step, 'DELTAP[' + b + ']', n, stack); }
+
+    for (var i = 0; i < n; i++) {
+        var pi = stack.pop();
+        var arg = stack.pop();
+        var appem = base + ((arg & 0xF0) >> 4);
+        if (appem !== ppem) { continue; }
+
+        var mag = (arg & 0x0F) - 8;
+        if (mag >= 0) { mag++; }
+        if (exports.DEBUG) { console.log(state.step, 'DELTAPFIX', pi, 'by', mag * ds); }
+
+        var p = z0[pi];
+        fv.setRelative(p, p, mag * ds, pv);
+    }
+}
+
+// SDB[] Set Delta Base in the graphics state
+// 0x5E
+function SDB(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SDB[]', n); }
+
+    state.deltaBase = n;
+}
+
+// SDS[] Set Delta Shift in the graphics state
+// 0x5F
+function SDS(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SDS[]', n); }
+
+    state.deltaShift = Math.pow(0.5, n);
+}
+
+// ADD[] ADD
+// 0x60
+function ADD(state) {
+    var stack = state.stack;
+    var n2 = stack.pop();
+    var n1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'ADD[]', n2, n1); }
+
+    stack.push(n1 + n2);
+}
+
+// SUB[] SUB
+// 0x61
+function SUB(state) {
+    var stack = state.stack;
+    var n2 = stack.pop();
+    var n1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SUB[]', n2, n1); }
+
+    stack.push(n1 - n2);
+}
+
+// DIV[] DIV
+// 0x62
+function DIV(state) {
+    var stack = state.stack;
+    var n2 = stack.pop();
+    var n1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'DIV[]', n2, n1); }
+
+    stack.push(n1 * 64 / n2);
+}
+
+// MUL[] MUL
+// 0x63
+function MUL(state) {
+    var stack = state.stack;
+    var n2 = stack.pop();
+    var n1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'MUL[]', n2, n1); }
+
+    stack.push(n1 * n2 / 64);
+}
+
+// ABS[] ABSolute value
+// 0x64
+function ABS(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'ABS[]', n); }
+
+    stack.push(Math.abs(n));
+}
+
+// NEG[] NEGate
+// 0x65
+function NEG(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'NEG[]', n); }
+
+    stack.push(-n);
+}
+
+// FLOOR[] FLOOR
+// 0x66
+function FLOOR(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'FLOOR[]', n); }
+
+    stack.push(Math.floor(n / 0x40) * 0x40);
+}
+
+// CEILING[] CEILING
+// 0x67
+function CEILING(state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'CEILING[]', n); }
+
+    stack.push(Math.ceil(n / 0x40) * 0x40);
+}
+
+// ROUND[ab] ROUND value
+// 0x68-0x6B
+function ROUND(dt, state) {
+    var stack = state.stack;
+    var n = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'ROUND[]'); }
+
+    stack.push(state.round(n / 0x40) * 0x40);
+}
+
+// WCVTF[] Write Control Value Table in Funits
+// 0x70
+function WCVTF(state) {
+    var stack = state.stack;
+    var v = stack.pop();
+    var l = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'WCVTF[]', v, l); }
+
+    state.cvt[l] = v * state.ppem / state.font.unitsPerEm;
+}
+
+// DELTAC1[] DELTA exception C1
+// DELTAC2[] DELTA exception C2
+// DELTAC3[] DELTA exception C3
+// 0x73, 0x74, 0x75
+function DELTAC123(b, state) {
+    var stack = state.stack;
+    var n = stack.pop();
+    var ppem = state.ppem;
+    var base = state.deltaBase + (b - 1) * 16;
+    var ds = state.deltaShift;
+
+    if (exports.DEBUG) { console.log(state.step, 'DELTAC[' + b + ']', n, stack); }
+
+    for (var i = 0; i < n; i++) {
+        var c = stack.pop();
+        var arg = stack.pop();
+        var appem = base + ((arg & 0xF0) >> 4);
+        if (appem !== ppem) { continue; }
+
+        var mag = (arg & 0x0F) - 8;
+        if (mag >= 0) { mag++; }
+
+        var delta = mag * ds;
+
+        if (exports.DEBUG) { console.log(state.step, 'DELTACFIX', c, 'by', delta); }
+
+        state.cvt[c] += delta;
+    }
+}
+
+// SROUND[] Super ROUND
+// 0x76
+function SROUND(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'SROUND[]', n); }
+
+    state.round = roundSuper;
+
+    var period;
+
+    switch (n & 0xC0) {
+        case 0x00:
+            period = 0.5;
+            break;
+        case 0x40:
+            period = 1;
+            break;
+        case 0x80:
+            period = 2;
+            break;
+        default:
+            throw new Error('invalid SROUND value');
+    }
+
+    state.srPeriod = period;
+
+    switch (n & 0x30) {
+        case 0x00:
+            state.srPhase = 0;
+            break;
+        case 0x10:
+            state.srPhase = 0.25 * period;
+            break;
+        case 0x20:
+            state.srPhase = 0.5  * period;
+            break;
+        case 0x30:
+            state.srPhase = 0.75 * period;
+            break;
+        default: throw new Error('invalid SROUND value');
+    }
+
+    n &= 0x0F;
+
+    if (n === 0) { state.srThreshold = 0; }
+    else { state.srThreshold = (n / 8 - 0.5) * period; }
+}
+
+// S45ROUND[] Super ROUND 45 degrees
+// 0x77
+function S45ROUND(state) {
+    var n = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'S45ROUND[]', n); }
+
+    state.round = roundSuper;
+
+    var period;
+
+    switch (n & 0xC0) {
+        case 0x00:
+            period = Math.sqrt(2) / 2;
+            break;
+        case 0x40:
+            period = Math.sqrt(2);
+            break;
+        case 0x80:
+            period = 2 * Math.sqrt(2);
+            break;
+        default:
+            throw new Error('invalid S45ROUND value');
+    }
+
+    state.srPeriod = period;
+
+    switch (n & 0x30) {
+        case 0x00:
+            state.srPhase = 0;
+            break;
+        case 0x10:
+            state.srPhase = 0.25 * period;
+            break;
+        case 0x20:
+            state.srPhase = 0.5  * period;
+            break;
+        case 0x30:
+            state.srPhase = 0.75 * period;
+            break;
+        default:
+            throw new Error('invalid S45ROUND value');
+    }
+
+    n &= 0x0F;
+
+    if (n === 0) { state.srThreshold = 0; }
+    else { state.srThreshold = (n / 8 - 0.5) * period; }
+}
+
+// ROFF[] Round Off
+// 0x7A
+function ROFF(state) {
+    if (exports.DEBUG) { console.log(state.step, 'ROFF[]'); }
+
+    state.round = roundOff;
+}
+
+// RUTG[] Round Up To Grid
+// 0x7C
+function RUTG(state) {
+    if (exports.DEBUG) { console.log(state.step, 'RUTG[]'); }
+
+    state.round = roundUpToGrid;
+}
+
+// RDTG[] Round Down To Grid
+// 0x7D
+function RDTG(state) {
+    if (exports.DEBUG) { console.log(state.step, 'RDTG[]'); }
+
+    state.round = roundDownToGrid;
+}
+
+// SCANCTRL[] SCAN conversion ConTRoL
+// 0x85
+function SCANCTRL(state) {
+    var n = state.stack.pop();
+
+    // ignored by opentype.js
+
+    if (exports.DEBUG) { console.log(state.step, 'SCANCTRL[]', n); }
+}
+
+// SDPVTL[a] Set Dual Projection Vector To Line
+// 0x86-0x87
+function SDPVTL(a, state) {
+    var stack = state.stack;
+    var p2i = stack.pop();
+    var p1i = stack.pop();
+    var p2 = state.z2[p2i];
+    var p1 = state.z1[p1i];
+
+    if (exports.DEBUG) { console.log(state.step, 'SDPVTL[' + a + ']', p2i, p1i); }
+
+    var dx;
+    var dy;
+
+    if (!a) {
+        dx = p1.x - p2.x;
+        dy = p1.y - p2.y;
+    } else {
+        dx = p2.y - p1.y;
+        dy = p1.x - p2.x;
+    }
+
+    state.dpv = getUnitVector(dx, dy);
+}
+
+// GETINFO[] GET INFOrmation
+// 0x88
+function GETINFO(state) {
+    var stack = state.stack;
+    var sel = stack.pop();
+    var r = 0;
+
+    if (exports.DEBUG) { console.log(state.step, 'GETINFO[]', sel); }
+
+    // v35 as in no subpixel hinting
+    if (sel & 0x01) { r = 35; }
+
+    // TODO rotation and stretch currently not supported
+    // and thus those GETINFO are always 0.
+
+    // opentype.js is always gray scaling
+    if (sel & 0x20) { r |= 0x1000; }
+
+    stack.push(r);
+}
+
+// ROLL[] ROLL the top three stack elements
+// 0x8A
+function ROLL(state) {
+    var stack = state.stack;
+    var a = stack.pop();
+    var b = stack.pop();
+    var c = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'ROLL[]'); }
+
+    stack.push(b);
+    stack.push(a);
+    stack.push(c);
+}
+
+// MAX[] MAXimum of top two stack elements
+// 0x8B
+function MAX(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'MAX[]', e2, e1); }
+
+    stack.push(Math.max(e1, e2));
+}
+
+// MIN[] MINimum of top two stack elements
+// 0x8C
+function MIN(state) {
+    var stack = state.stack;
+    var e2 = stack.pop();
+    var e1 = stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'MIN[]', e2, e1); }
+
+    stack.push(Math.min(e1, e2));
+}
+
+// SCANTYPE[] SCANTYPE
+// 0x8D
+function SCANTYPE(state) {
+    var n = state.stack.pop();
+    // ignored by opentype.js
+    if (exports.DEBUG) { console.log(state.step, 'SCANTYPE[]', n); }
+}
+
+// INSTCTRL[] INSTCTRL
+// 0x8D
+function INSTCTRL(state) {
+    var s = state.stack.pop();
+    var v = state.stack.pop();
+
+    if (exports.DEBUG) { console.log(state.step, 'INSTCTRL[]', s, v); }
+
+    switch (s) {
+        case 1 : state.inhibitGridFit = !!v; return;
+        case 2 : state.ignoreCvt = !!v; return;
+        default: throw new Error('invalid INSTCTRL[] selector');
+    }
+}
+
+// PUSHB[abc] PUSH Bytes
+// 0xB0-0xB7
+function PUSHB(n, state) {
+    var stack = state.stack;
+    var prog = state.prog;
+    var ip = state.ip;
+
+    if (exports.DEBUG) { console.log(state.step, 'PUSHB[' + n + ']'); }
+
+    for (var i = 0; i < n; i++) { stack.push(prog[++ip]); }
+
+    state.ip = ip;
+}
+
+// PUSHW[abc] PUSH Words
+// 0xB8-0xBF
+function PUSHW(n, state) {
+    var ip = state.ip;
+    var prog = state.prog;
+    var stack = state.stack;
+
+    if (exports.DEBUG) { console.log(state.ip, 'PUSHW[' + n + ']'); }
+
+    for (var i = 0; i < n; i++) {
+        var w = (prog[++ip] << 8) | prog[++ip];
+        if (w & 0x8000) { w = -((w ^ 0xffff) + 1); }
+        stack.push(w);
+    }
+
+    state.ip = ip;
+}
+
+// MDRP[abcde] Move Direct Relative Point
+// 0xD0-0xEF
+// (if indirect is 0)
+//
+// and
+//
+// MIRP[abcde] Move Indirect Relative Point
+// 0xE0-0xFF
+// (if indirect is 1)
+
+function MDRP_MIRP(indirect, setRp0, keepD, ro, dt, state) {
+    var stack = state.stack;
+    var cvte = indirect && stack.pop();
+    var pi = stack.pop();
+    var rp0i = state.rp0;
+    var rp = state.z0[rp0i];
+    var p = state.z1[pi];
+
+    var md = state.minDis;
+    var fv = state.fv;
+    var pv = state.dpv;
+    var od; // original distance
+    var d; // moving distance
+    var sign; // sign of distance
+    var cv;
+
+    d = od = pv.distance(p, rp, true, true);
+    sign = d >= 0 ? 1 : -1; // Math.sign would be 0 in case of 0
+
+    // TODO consider autoFlip
+    d = Math.abs(d);
+
+    if (indirect) {
+        cv = state.cvt[cvte];
+
+        if (ro && Math.abs(d - cv) < state.cvCutIn) { d = cv; }
+    }
+
+    if (keepD && d < md) { d = md; }
+
+    if (ro) { d = state.round(d); }
+
+    fv.setRelative(p, rp, sign * d, pv);
+    fv.touch(p);
+
+    if (exports.DEBUG) {
+        console.log(
+            state.step,
+            (indirect ? 'MIRP[' : 'MDRP[') +
+            (setRp0 ? 'M' : 'm') +
+            (keepD ? '>' : '_') +
+            (ro ? 'R' : '_') +
+            (dt === 0 ? 'Gr' : (dt === 1 ? 'Bl' : (dt === 2 ? 'Wh' : ''))) +
+            ']',
+            indirect ?
+                cvte + '(' + state.cvt[cvte] + ',' +  cv + ')' :
+                '',
+            pi,
+            '(d =', od, '->', sign * d, ')'
+        );
+    }
+
+    state.rp1 = state.rp0;
+    state.rp2 = pi;
+    if (setRp0) { state.rp0 = pi; }
+}
+
+/*
+* The instruction table.
+*/
+instructionTable = [
+    /* 0x00 */ SVTCA.bind(undefined, yUnitVector),
+    /* 0x01 */ SVTCA.bind(undefined, xUnitVector),
+    /* 0x02 */ SPVTCA.bind(undefined, yUnitVector),
+    /* 0x03 */ SPVTCA.bind(undefined, xUnitVector),
+    /* 0x04 */ SFVTCA.bind(undefined, yUnitVector),
+    /* 0x05 */ SFVTCA.bind(undefined, xUnitVector),
+    /* 0x06 */ SPVTL.bind(undefined, 0),
+    /* 0x07 */ SPVTL.bind(undefined, 1),
+    /* 0x08 */ SFVTL.bind(undefined, 0),
+    /* 0x09 */ SFVTL.bind(undefined, 1),
+    /* 0x0A */ SPVFS,
+    /* 0x0B */ SFVFS,
+    /* 0x0C */ GPV,
+    /* 0x0D */ GFV,
+    /* 0x0E */ SFVTPV,
+    /* 0x0F */ ISECT,
+    /* 0x10 */ SRP0,
+    /* 0x11 */ SRP1,
+    /* 0x12 */ SRP2,
+    /* 0x13 */ SZP0,
+    /* 0x14 */ SZP1,
+    /* 0x15 */ SZP2,
+    /* 0x16 */ SZPS,
+    /* 0x17 */ SLOOP,
+    /* 0x18 */ RTG,
+    /* 0x19 */ RTHG,
+    /* 0x1A */ SMD,
+    /* 0x1B */ ELSE,
+    /* 0x1C */ JMPR,
+    /* 0x1D */ SCVTCI,
+    /* 0x1E */ undefined,   // TODO SSWCI
+    /* 0x1F */ undefined,   // TODO SSW
+    /* 0x20 */ DUP,
+    /* 0x21 */ POP,
+    /* 0x22 */ CLEAR,
+    /* 0x23 */ SWAP,
+    /* 0x24 */ DEPTH,
+    /* 0x25 */ CINDEX,
+    /* 0x26 */ MINDEX,
+    /* 0x27 */ undefined,   // TODO ALIGNPTS
+    /* 0x28 */ undefined,
+    /* 0x29 */ undefined,   // TODO UTP
+    /* 0x2A */ LOOPCALL,
+    /* 0x2B */ CALL,
+    /* 0x2C */ FDEF,
+    /* 0x2D */ undefined,   // ENDF (eaten by FDEF)
+    /* 0x2E */ MDAP.bind(undefined, 0),
+    /* 0x2F */ MDAP.bind(undefined, 1),
+    /* 0x30 */ IUP.bind(undefined, yUnitVector),
+    /* 0x31 */ IUP.bind(undefined, xUnitVector),
+    /* 0x32 */ SHP.bind(undefined, 0),
+    /* 0x33 */ SHP.bind(undefined, 1),
+    /* 0x34 */ SHC.bind(undefined, 0),
+    /* 0x35 */ SHC.bind(undefined, 1),
+    /* 0x36 */ SHZ.bind(undefined, 0),
+    /* 0x37 */ SHZ.bind(undefined, 1),
+    /* 0x38 */ SHPIX,
+    /* 0x39 */ IP,
+    /* 0x3A */ MSIRP.bind(undefined, 0),
+    /* 0x3B */ MSIRP.bind(undefined, 1),
+    /* 0x3C */ ALIGNRP,
+    /* 0x3D */ RTDG,
+    /* 0x3E */ MIAP.bind(undefined, 0),
+    /* 0x3F */ MIAP.bind(undefined, 1),
+    /* 0x40 */ NPUSHB,
+    /* 0x41 */ NPUSHW,
+    /* 0x42 */ WS,
+    /* 0x43 */ RS,
+    /* 0x44 */ WCVTP,
+    /* 0x45 */ RCVT,
+    /* 0x46 */ GC.bind(undefined, 0),
+    /* 0x47 */ GC.bind(undefined, 1),
+    /* 0x48 */ undefined,   // TODO SCFS
+    /* 0x49 */ MD.bind(undefined, 0),
+    /* 0x4A */ MD.bind(undefined, 1),
+    /* 0x4B */ MPPEM,
+    /* 0x4C */ undefined,   // TODO MPS
+    /* 0x4D */ FLIPON,
+    /* 0x4E */ undefined,   // TODO FLIPOFF
+    /* 0x4F */ undefined,   // TODO DEBUG
+    /* 0x50 */ LT,
+    /* 0x51 */ LTEQ,
+    /* 0x52 */ GT,
+    /* 0x53 */ GTEQ,
+    /* 0x54 */ EQ,
+    /* 0x55 */ NEQ,
+    /* 0x56 */ ODD,
+    /* 0x57 */ EVEN,
+    /* 0x58 */ IF,
+    /* 0x59 */ EIF,
+    /* 0x5A */ AND,
+    /* 0x5B */ OR,
+    /* 0x5C */ NOT,
+    /* 0x5D */ DELTAP123.bind(undefined, 1),
+    /* 0x5E */ SDB,
+    /* 0x5F */ SDS,
+    /* 0x60 */ ADD,
+    /* 0x61 */ SUB,
+    /* 0x62 */ DIV,
+    /* 0x63 */ MUL,
+    /* 0x64 */ ABS,
+    /* 0x65 */ NEG,
+    /* 0x66 */ FLOOR,
+    /* 0x67 */ CEILING,
+    /* 0x68 */ ROUND.bind(undefined, 0),
+    /* 0x69 */ ROUND.bind(undefined, 1),
+    /* 0x6A */ ROUND.bind(undefined, 2),
+    /* 0x6B */ ROUND.bind(undefined, 3),
+    /* 0x6C */ undefined,   // TODO NROUND[ab]
+    /* 0x6D */ undefined,   // TODO NROUND[ab]
+    /* 0x6E */ undefined,   // TODO NROUND[ab]
+    /* 0x6F */ undefined,   // TODO NROUND[ab]
+    /* 0x70 */ WCVTF,
+    /* 0x71 */ DELTAP123.bind(undefined, 2),
+    /* 0x72 */ DELTAP123.bind(undefined, 3),
+    /* 0x73 */ DELTAC123.bind(undefined, 1),
+    /* 0x74 */ DELTAC123.bind(undefined, 2),
+    /* 0x75 */ DELTAC123.bind(undefined, 3),
+    /* 0x76 */ SROUND,
+    /* 0x77 */ S45ROUND,
+    /* 0x78 */ undefined,   // TODO JROT[]
+    /* 0x79 */ undefined,   // TODO JROF[]
+    /* 0x7A */ ROFF,
+    /* 0x7B */ undefined,
+    /* 0x7C */ RUTG,
+    /* 0x7D */ RDTG,
+    /* 0x7E */ POP, // actually SANGW, supposed to do only a pop though
+    /* 0x7F */ POP, // actually AA, supposed to do only a pop though
+    /* 0x80 */ undefined,   // TODO FLIPPT
+    /* 0x81 */ undefined,   // TODO FLIPRGON
+    /* 0x82 */ undefined,   // TODO FLIPRGOFF
+    /* 0x83 */ undefined,
+    /* 0x84 */ undefined,
+    /* 0x85 */ SCANCTRL,
+    /* 0x86 */ SDPVTL.bind(undefined, 0),
+    /* 0x87 */ SDPVTL.bind(undefined, 1),
+    /* 0x88 */ GETINFO,
+    /* 0x89 */ undefined,   // TODO IDEF
+    /* 0x8A */ ROLL,
+    /* 0x8B */ MAX,
+    /* 0x8C */ MIN,
+    /* 0x8D */ SCANTYPE,
+    /* 0x8E */ INSTCTRL,
+    /* 0x8F */ undefined,
+    /* 0x90 */ undefined,
+    /* 0x91 */ undefined,
+    /* 0x92 */ undefined,
+    /* 0x93 */ undefined,
+    /* 0x94 */ undefined,
+    /* 0x95 */ undefined,
+    /* 0x96 */ undefined,
+    /* 0x97 */ undefined,
+    /* 0x98 */ undefined,
+    /* 0x99 */ undefined,
+    /* 0x9A */ undefined,
+    /* 0x9B */ undefined,
+    /* 0x9C */ undefined,
+    /* 0x9D */ undefined,
+    /* 0x9E */ undefined,
+    /* 0x9F */ undefined,
+    /* 0xA0 */ undefined,
+    /* 0xA1 */ undefined,
+    /* 0xA2 */ undefined,
+    /* 0xA3 */ undefined,
+    /* 0xA4 */ undefined,
+    /* 0xA5 */ undefined,
+    /* 0xA6 */ undefined,
+    /* 0xA7 */ undefined,
+    /* 0xA8 */ undefined,
+    /* 0xA9 */ undefined,
+    /* 0xAA */ undefined,
+    /* 0xAB */ undefined,
+    /* 0xAC */ undefined,
+    /* 0xAD */ undefined,
+    /* 0xAE */ undefined,
+    /* 0xAF */ undefined,
+    /* 0xB0 */ PUSHB.bind(undefined, 1),
+    /* 0xB1 */ PUSHB.bind(undefined, 2),
+    /* 0xB2 */ PUSHB.bind(undefined, 3),
+    /* 0xB3 */ PUSHB.bind(undefined, 4),
+    /* 0xB4 */ PUSHB.bind(undefined, 5),
+    /* 0xB5 */ PUSHB.bind(undefined, 6),
+    /* 0xB6 */ PUSHB.bind(undefined, 7),
+    /* 0xB7 */ PUSHB.bind(undefined, 8),
+    /* 0xB8 */ PUSHW.bind(undefined, 1),
+    /* 0xB9 */ PUSHW.bind(undefined, 2),
+    /* 0xBA */ PUSHW.bind(undefined, 3),
+    /* 0xBB */ PUSHW.bind(undefined, 4),
+    /* 0xBC */ PUSHW.bind(undefined, 5),
+    /* 0xBD */ PUSHW.bind(undefined, 6),
+    /* 0xBE */ PUSHW.bind(undefined, 7),
+    /* 0xBF */ PUSHW.bind(undefined, 8),
+    /* 0xC0 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 0),
+    /* 0xC1 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 1),
+    /* 0xC2 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 2),
+    /* 0xC3 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 3),
+    /* 0xC4 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 0),
+    /* 0xC5 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 1),
+    /* 0xC6 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 2),
+    /* 0xC7 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 3),
+    /* 0xC8 */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 0),
+    /* 0xC9 */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 1),
+    /* 0xCA */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 2),
+    /* 0xCB */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 3),
+    /* 0xCC */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 0),
+    /* 0xCD */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 1),
+    /* 0xCE */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 2),
+    /* 0xCF */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 3),
+    /* 0xD0 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 0),
+    /* 0xD1 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 1),
+    /* 0xD2 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 2),
+    /* 0xD3 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 3),
+    /* 0xD4 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 0),
+    /* 0xD5 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 1),
+    /* 0xD6 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 2),
+    /* 0xD7 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 3),
+    /* 0xD8 */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 0),
+    /* 0xD9 */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 1),
+    /* 0xDA */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 2),
+    /* 0xDB */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 3),
+    /* 0xDC */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 0),
+    /* 0xDD */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 1),
+    /* 0xDE */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 2),
+    /* 0xDF */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 3),
+    /* 0xE0 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 0),
+    /* 0xE1 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 1),
+    /* 0xE2 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 2),
+    /* 0xE3 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 3),
+    /* 0xE4 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 0),
+    /* 0xE5 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 1),
+    /* 0xE6 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 2),
+    /* 0xE7 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 3),
+    /* 0xE8 */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 0),
+    /* 0xE9 */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 1),
+    /* 0xEA */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 2),
+    /* 0xEB */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 3),
+    /* 0xEC */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 0),
+    /* 0xED */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 1),
+    /* 0xEE */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 2),
+    /* 0xEF */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 3),
+    /* 0xF0 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 0),
+    /* 0xF1 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 1),
+    /* 0xF2 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 2),
+    /* 0xF3 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 3),
+    /* 0xF4 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 0),
+    /* 0xF5 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 1),
+    /* 0xF6 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 2),
+    /* 0xF7 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 3),
+    /* 0xF8 */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 0),
+    /* 0xF9 */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 1),
+    /* 0xFA */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 2),
+    /* 0xFB */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 3),
+    /* 0xFC */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 0),
+    /* 0xFD */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 1),
+    /* 0xFE */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 2),
+    /* 0xFF */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 3)
+];
+
+/*****************************
+  Mathematical Considerations
+******************************
+
+fv ... refers to freedom vector
+pv ... refers to projection vector
+rp ... refers to reference point
+p  ... refers to to point being operated on
+d  ... refers to distance
+
+SETRELATIVE:
+============
+
+case freedom vector == x-axis:
+------------------------------
+
+                        (pv)
+                     .-'
+              rpd .-'
+               .-*
+          d .-'90°'
+         .-'       '
+      .-'           '
+   *-'               ' b
+  rp                  '
+                       '
+                        '
+            p *----------*-------------- (fv)
+                          pm
+
+  rpdx = rpx + d * pv.x
+  rpdy = rpy + d * pv.y
+
+  equation of line b
+
+   y - rpdy = pvns * (x- rpdx)
+
+   y = p.y
+
+   x = rpdx + ( p.y - rpdy ) / pvns
+
+
+case freedom vector == y-axis:
+------------------------------
+
+    * pm
+    |\
+    | \
+    |  \
+    |   \
+    |    \
+    |     \
+    |      \
+    |       \
+    |        \
+    |         \ b
+    |          \
+    |           \
+    |            \    .-' (pv)
+    |         90° \.-'
+    |           .-'* rpd
+    |        .-'
+    *     *-'  d
+    p     rp
+
+  rpdx = rpx + d * pv.x
+  rpdy = rpy + d * pv.y
+
+  equation of line b:
+           pvns ... normal slope to pv
+
+   y - rpdy = pvns * (x - rpdx)
+
+   x = p.x
+
+   y = rpdy +  pvns * (p.x - rpdx)
+
+
+
+generic case:
+-------------
+
+
+                              .'(fv)
+                            .'
+                          .* pm
+                        .' !
+                      .'    .
+                    .'      !
+                  .'         . b
+                .'           !
+               *              .
+              p               !
+                         90°   .    ... (pv)
+                           ...-*-'''
+                  ...---'''    rpd
+         ...---'''   d
+   *--'''
+  rp
+
+    rpdx = rpx + d * pv.x
+    rpdy = rpy + d * pv.y
+
+ equation of line b:
+    pvns... normal slope to pv
+
+    y - rpdy = pvns * (x - rpdx)
+
+ equation of freedom vector line:
+    fvs ... slope of freedom vector (=fy/fx)
+
+    y - py = fvs * (x - px)
+
+
+  on pm both equations are true for same x/y
+
+    y - rpdy = pvns * (x - rpdx)
+
+    y - py = fvs * (x - px)
+
+  form to y and set equal:
+
+    pvns * (x - rpdx) + rpdy = fvs * (x - px) + py
+
+  expand:
+
+    pvns * x - pvns * rpdx + rpdy = fvs * x - fvs * px + py
+
+  switch:
+
+    fvs * x - fvs * px + py = pvns * x - pvns * rpdx + rpdy
+
+  solve for x:
+
+    fvs * x - pvns * x = fvs * px - pvns * rpdx - py + rpdy
+
+
+
+          fvs * px - pvns * rpdx + rpdy - py
+    x =  -----------------------------------
+                 fvs - pvns
+
+  and:
+
+    y = fvs * (x - px) + py
+
+
+
+INTERPOLATE:
+============
+
+Examples of point interpolation.
+
+The weight of the movement of the reference point gets bigger
+the further the other reference point is away, thus the safest
+option (that is avoiding 0/0 divisions) is to weight the
+original distance of the other point by the sum of both distances.
+
+If the sum of both distances is 0, then move the point by the
+arithmetic average of the movement of both reference points.
+
+
+
+
+           (+6)
+    rp1o *---->*rp1
+         .     .                          (+12)
+         .     .                  rp2o *---------->* rp2
+         .     .                       .           .
+         .     .                       .           .
+         .    10          20           .           .
+         |.........|...................|           .
+               .   .                               .
+               .   . (+8)                          .
+                po *------>*p                      .
+               .           .                       .
+               .    12     .          24           .
+               |...........|.......................|
+                                  36
+
+
+-------
+
+
+
+           (+10)
+    rp1o *-------->*rp1
+         .         .                      (-10)
+         .         .              rp2 *<---------* rpo2
+         .         .                   .         .
+         .         .                   .         .
+         .    10   .          30       .         .
+         |.........|.............................|
+                   .                   .
+                   . (+5)              .
+                po *--->* p            .
+                   .    .              .
+                   .    .   20         .
+                   |....|..............|
+                     5        15
+
+
+-------
+
+
+           (+10)
+    rp1o *-------->*rp1
+         .         .
+         .         .
+    rp2o *-------->*rp2
+
+
+                               (+10)
+                          po *-------->* p
+
+-------
+
+
+           (+10)
+    rp1o *-------->*rp1
+         .         .
+         .         .(+30)
+    rp2o *---------------------------->*rp2
+
+
+                                        (+25)
+                          po *----------------------->* p
+
+
+
+vim: set ts=4 sw=4 expandtab:
+*****/
+
+/**
+ * Converts a string into a list of tokens.
+ */
+
+/**
+ * Create a new token
+ * @param {string} char a single char
+ */
+function Token(char) {
+    this.char = char;
+    this.state = {};
+    this.activeState = null;
+}
+
+/**
+ * Create a new context range
+ * @param {number} startIndex range start index
+ * @param {number} endOffset range end index offset
+ * @param {string} contextName owner context name
+ */
+function ContextRange(startIndex, endOffset, contextName) {
+    this.contextName = contextName;
+    this.startIndex = startIndex;
+    this.endOffset = endOffset;
+}
+
+/**
+ * Check context start and end
+ * @param {string} contextName a unique context name
+ * @param {function} checkStart a predicate function the indicates a context's start
+ * @param {function} checkEnd a predicate function the indicates a context's end
+ */
+function ContextChecker(contextName, checkStart, checkEnd) {
+    this.contextName = contextName;
+    this.openRange = null;
+    this.ranges = [];
+    this.checkStart = checkStart;
+    this.checkEnd = checkEnd;
+}
+
+/**
+ * @typedef ContextParams
+ * @type Object
+ * @property {array} context context items
+ * @property {number} currentIndex current item index
+ */
+
+/**
+ * Create a context params
+ * @param {array} context a list of items
+ * @param {number} currentIndex current item index
+ */
+function ContextParams(context, currentIndex) {
+    this.context = context;
+    this.index = currentIndex;
+    this.length = context.length;
+    this.current = context[currentIndex];
+    this.backtrack = context.slice(0, currentIndex);
+    this.lookahead = context.slice(currentIndex + 1);
+}
+
+/**
+ * Create an event instance
+ * @param {string} eventId event unique id
+ */
+function Event(eventId) {
+    this.eventId = eventId;
+    this.subscribers = [];
+}
+
+/**
+ * Initialize a core events and auto subscribe required event handlers
+ * @param {any} events an object that enlists core events handlers
+ */
+function initializeCoreEvents(events) {
+    var this$1 = this;
+
+    var coreEvents = [
+        'start', 'end', 'next', 'newToken', 'contextStart',
+        'contextEnd', 'insertToken', 'removeToken', 'removeRange',
+        'replaceToken', 'replaceRange', 'composeRUD', 'updateContextsRanges'
+    ];
+
+    coreEvents.forEach(function (eventId) {
+        Object.defineProperty(this$1.events, eventId, {
+            value: new Event(eventId)
+        });
+    });
+
+    if (!!events) {
+        coreEvents.forEach(function (eventId) {
+            var event = events[eventId];
+            if (typeof event === 'function') {
+                this$1.events[eventId].subscribe(event);
+            }
+        });
+    }
+    var requiresContextUpdate = [
+        'insertToken', 'removeToken', 'removeRange',
+        'replaceToken', 'replaceRange', 'composeRUD'
+    ];
+    requiresContextUpdate.forEach(function (eventId) {
+        this$1.events[eventId].subscribe(
+            this$1.updateContextsRanges
+        );
+    });
+}
+
+/**
+ * Converts a string into a list of tokens
+ * @param {any} events tokenizer core events
+ */
+function Tokenizer(events) {
+    this.tokens = [];
+    this.registeredContexts = {};
+    this.contextCheckers = [];
+    this.events = {};
+    this.registeredModifiers = [];
+
+    initializeCoreEvents.call(this, events);
+}
+
+/**
+ * Sets the state of a token, usually called by a state modifier.
+ * @param {string} key state item key
+ * @param {any} value state item value
+ */
+Token.prototype.setState = function(key, value) {
+    this.state[key] = value;
+    this.activeState = { key: key, value: this.state[key] };
+    return this.activeState;
+};
+
+Token.prototype.getState = function (stateId) {
+    return this.state[stateId] || null;
+};
+
+/**
+ * Checks if an index exists in the tokens list.
+ * @param {number} index token index
+ */
+Tokenizer.prototype.inboundIndex = function(index) {
+    return index >= 0 && index < this.tokens.length;
+};
+
+/**
+ * Compose and apply a list of operations (replace, update, delete)
+ * @param {array} RUDs replace, update and delete operations
+ * TODO: Perf. Optimization (lengthBefore === lengthAfter ? dispatch once)
+ */
+Tokenizer.prototype.composeRUD = function (RUDs) {
+    var this$1 = this;
+
+    var silent = true;
+    var state = RUDs.map(function (RUD) { return (
+        this$1[RUD[0]].apply(this$1, RUD.slice(1).concat(silent))
+    ); });
+    var hasFAILObject = function (obj) { return (
+        typeof obj === 'object' &&
+        obj.hasOwnProperty('FAIL')
+    ); };
+    if (state.every(hasFAILObject)) {
+        return {
+            FAIL: "composeRUD: one or more operations hasn't completed successfully",
+            report: state.filter(hasFAILObject)
+        };
+    }
+    this.dispatch('composeRUD', [state.filter(function (op) { return !hasFAILObject(op); })]);
+};
+
+/**
+ * Replace a range of tokens with a list of tokens
+ * @param {number} startIndex range start index
+ * @param {number} offset range offset
+ * @param {token} tokens a list of tokens to replace
+ * @param {boolean} silent dispatch events and update context ranges
+ */
+Tokenizer.prototype.replaceRange = function (startIndex, offset, tokens, silent) {
+    offset = offset !== null ? offset : this.tokens.length;
+    var isTokenType = tokens.every(function (token) { return token instanceof Token; });
+    if (!isNaN(startIndex) && this.inboundIndex(startIndex) && isTokenType) {
+        var replaced = this.tokens.splice.apply(
+            this.tokens, [startIndex, offset].concat(tokens)
+        );
+        if (!silent) { this.dispatch('replaceToken', [startIndex, offset, tokens]); }
+        return [replaced, tokens];
+    } else {
+        return { FAIL: 'replaceRange: invalid tokens or startIndex.' };
+    }
+};
+
+/**
+ * Replace a token with another token
+ * @param {number} index token index
+ * @param {token} token a token to replace
+ * @param {boolean} silent dispatch events and update context ranges
+ */
+Tokenizer.prototype.replaceToken = function (index, token, silent) {
+    if (!isNaN(index) && this.inboundIndex(index) && token instanceof Token) {
+        var replaced = this.tokens.splice(index, 1, token);
+        if (!silent) { this.dispatch('replaceToken', [index, token]); }
+        return [replaced[0], token];
+    } else {
+        return { FAIL: 'replaceToken: invalid token or index.' };
+    }
+};
+
+/**
+ * Removes a range of tokens
+ * @param {number} startIndex range start index
+ * @param {number} offset range offset
+ * @param {boolean} silent dispatch events and update context ranges
+ */
+Tokenizer.prototype.removeRange = function(startIndex, offset, silent) {
+    offset = !isNaN(offset) ? offset : this.tokens.length;
+    var tokens = this.tokens.splice(startIndex, offset);
+    if (!silent) { this.dispatch('removeRange', [tokens, startIndex, offset]); }
+    return tokens;
+};
+
+/**
+ * Remove a token at a certain index
+ * @param {number} index token index
+ * @param {boolean} silent dispatch events and update context ranges
+ */
+Tokenizer.prototype.removeToken = function(index, silent) {
+    if (!isNaN(index) && this.inboundIndex(index)) {
+        var token = this.tokens.splice(index, 1);
+        if (!silent) { this.dispatch('removeToken', [token, index]); }
+        return token;
+    } else {
+        return { FAIL: 'removeToken: invalid token index.' };
+    }
+};
+
+/**
+ * Insert a list of tokens at a certain index
+ * @param {array} tokens a list of tokens to insert
+ * @param {number} index insert the list of tokens at index
+ * @param {boolean} silent dispatch events and update context ranges
+ */
+Tokenizer.prototype.insertToken = function (tokens, index, silent) {
+    var tokenType = tokens.every(
+        function (token) { return token instanceof Token; }
+    );
+    if (tokenType) {
+        this.tokens.splice.apply(
+            this.tokens, [index, 0].concat(tokens)
+        );
+        if (!silent) { this.dispatch('insertToken', [tokens, index]); }
+        return tokens;
+    } else {
+        return { FAIL: 'insertToken: invalid token(s).' };
+    }
+};
+
+/**
+ * A state modifier that is called on 'newToken' event
+ * @param {string} modifierId state modifier id
+ * @param {function} condition a predicate function that returns true or false
+ * @param {function} modifier a function to update token state
+ */
+Tokenizer.prototype.registerModifier = function(modifierId, condition, modifier) {
+    this.events.newToken.subscribe(function(token, contextParams) {
+        var conditionParams = [token, contextParams];
+        var canApplyModifier = (
+            condition === null ||
+            condition.apply(this, conditionParams) === true
+        );
+        var modifierParams = [token, contextParams];
+        if (canApplyModifier) {
+            var newStateValue = modifier.apply(this, modifierParams);
+            token.setState(modifierId, newStateValue);
+        }
+    });
+    this.registeredModifiers.push(modifierId);
+};
+
+/**
+ * Subscribe a handler to an event
+ * @param {function} eventHandler an event handler function
+ */
+Event.prototype.subscribe = function (eventHandler) {
+    if (typeof eventHandler === 'function') {
+        return ((this.subscribers.push(eventHandler)) - 1);
+    } else {
+        return { FAIL: ("invalid '" + (this.eventId) + "' event handler")};
+    }
+};
+
+/**
+ * Unsubscribe an event handler
+ * @param {string} subsId subscription id
+ */
+Event.prototype.unsubscribe = function (subsId) {
+    this.subscribers.splice(subsId, 1);
+};
+
+/**
+ * Sets context params current value index
+ * @param {number} index context params current value index
+ */
+ContextParams.prototype.setCurrentIndex = function(index) {
+    this.index = index;
+    this.current = this.context[index];
+    this.backtrack = this.context.slice(0, index);
+    this.lookahead = this.context.slice(index + 1);
+};
+
+/**
+ * Get an item at an offset from the current value
+ * example (current value is 3):
+ *  1    2   [3]   4    5   |   items values
+ * -2   -1    0    1    2   |   offset values
+ * @param {number} offset an offset from current value index
+ */
+ContextParams.prototype.get = function (offset) {
+    switch (true) {
+        case (offset === 0):
+            return this.current;
+        case (offset < 0 && Math.abs(offset) <= this.backtrack.length):
+            return this.backtrack.slice(offset)[0];
+        case (offset > 0 && offset <= this.lookahead.length):
+            return this.lookahead[offset - 1];
+        default:
+            return null;
+    }
+};
+
+/**
+ * Converts a context range into a string value
+ * @param {contextRange} range a context range
+ */
+Tokenizer.prototype.rangeToText = function (range) {
+    if (range instanceof ContextRange) {
+        return (
+            this.getRangeTokens(range)
+                .map(function (token) { return token.char; }).join('')
+        );
+    }
+};
+
+/**
+ * Converts all tokens into a string
+ */
+Tokenizer.prototype.getText = function () {
+    return this.tokens.map(function (token) { return token.char; }).join('');
+};
+
+/**
+ * Get a context by name
+ * @param {string} contextName context name to get
+ */
+Tokenizer.prototype.getContext = function (contextName) {
+    var context = this.registeredContexts[contextName];
+    return !!context ? context : null;
+};
+
+/**
+ * Subscribes a new event handler to an event
+ * @param {string} eventName event name to subscribe to
+ * @param {function} eventHandler a function to be invoked on event
+ */
+Tokenizer.prototype.on = function(eventName, eventHandler) {
+    var event = this.events[eventName];
+    if (!!event) {
+        return event.subscribe(eventHandler);
+    } else {
+        return null;
+    }
+};
+
+/**
+ * Dispatches an event
+ * @param {string} eventName event name
+ * @param {any} args event handler arguments
+ */
+Tokenizer.prototype.dispatch = function(eventName, args) {
+    var this$1 = this;
+
+    var event = this.events[eventName];
+    if (event instanceof Event) {
+        event.subscribers.forEach(function (subscriber) {
+            subscriber.apply(this$1, args || []);
+        });
+    }
+};
+
+/**
+ * Register a new context checker
+ * @param {string} contextName a unique context name
+ * @param {function} contextStartCheck a predicate function that returns true on context start
+ * @param {function} contextEndCheck  a predicate function that returns true on context end
+ * TODO: call tokenize on registration to update context ranges with the new context.
+ */
+Tokenizer.prototype.registerContextChecker = function(contextName, contextStartCheck, contextEndCheck) {
+    if (!!this.getContext(contextName)) { return {
+        FAIL:
+        ("context name '" + contextName + "' is already registered.")
+    }; }
+    if (typeof contextStartCheck !== 'function') { return {
+        FAIL:
+        "missing context start check."
+    }; }
+    if (typeof contextEndCheck !== 'function') { return {
+        FAIL:
+        "missing context end check."
+    }; }
+    var contextCheckers = new ContextChecker(
+        contextName, contextStartCheck, contextEndCheck
+    );
+    this.registeredContexts[contextName] = contextCheckers;
+    this.contextCheckers.push(contextCheckers);
+    return contextCheckers;
+};
+
+/**
+ * Gets a context range tokens
+ * @param {contextRange} range a context range
+ */
+Tokenizer.prototype.getRangeTokens = function(range) {
+    var endIndex = range.startIndex + range.endOffset;
+    return [].concat(
+        this.tokens
+            .slice(range.startIndex, endIndex)
+    );
+};
+
+/**
+ * Gets the ranges of a context
+ * @param {string} contextName context name
+ */
+Tokenizer.prototype.getContextRanges = function(contextName) {
+    var context = this.getContext(contextName);
+    if (!!context) {
+        return context.ranges;
+    } else {
+        return { FAIL: ("context checker '" + contextName + "' is not registered.") };
+    }
+};
+
+/**
+ * Resets context ranges to run context update
+ */
+Tokenizer.prototype.resetContextsRanges = function () {
+    var registeredContexts = this.registeredContexts;
+    for (var contextName in registeredContexts) {
+        if (registeredContexts.hasOwnProperty(contextName)) {
+            var context = registeredContexts[contextName];
+            context.ranges = [];
+        }
+    }
+};
+
+/**
+ * Updates context ranges
+ */
+Tokenizer.prototype.updateContextsRanges = function () {
+    this.resetContextsRanges();
+    var chars = this.tokens.map(function (token) { return token.char; });
+    for (var i = 0; i < chars.length; i++) {
+        var contextParams = new ContextParams(chars, i);
+        this.runContextCheck(contextParams);
+    }
+    this.dispatch('updateContextsRanges', [this.registeredContexts]);
+};
+
+/**
+ * Sets the end offset of an open range
+ * @param {number} offset range end offset
+ * @param {string} contextName context name
+ */
+Tokenizer.prototype.setEndOffset = function (offset, contextName) {
+    var startIndex = this.getContext(contextName).openRange.startIndex;
+    var range = new ContextRange(startIndex, offset, contextName);
+    var ranges = this.getContext(contextName).ranges;
+    range.rangeId = contextName + "." + (ranges.length);
+    ranges.push(range);
+    this.getContext(contextName).openRange = null;
+    return range;
+};
+
+/**
+ * Runs a context check on the current context
+ * @param {contextParams} contextParams current context params
+ */
+Tokenizer.prototype.runContextCheck = function(contextParams) {
+    var this$1 = this;
+
+    var index = contextParams.index;
+    this.contextCheckers.forEach(function (contextChecker) {
+        var contextName = contextChecker.contextName;
+        var openRange = this$1.getContext(contextName).openRange;
+        if (!openRange && contextChecker.checkStart(contextParams)) {
+            openRange = new ContextRange(index, null, contextName);
+            this$1.getContext(contextName).openRange = openRange;
+            this$1.dispatch('contextStart', [contextName, index]);
+        }
+        if (!!openRange && contextChecker.checkEnd(contextParams)) {
+            var offset = (index - openRange.startIndex) + 1;
+            var range = this$1.setEndOffset(offset, contextName);
+            this$1.dispatch('contextEnd', [contextName, range]);
+        }
+    });
+};
+
+/**
+ * Converts a text into a list of tokens
+ * @param {string} text a text to tokenize
+ */
+Tokenizer.prototype.tokenize = function (text) {
+    this.tokens = [];
+    this.resetContextsRanges();
+    var chars = Array.from(text);
+    this.dispatch('start');
+    for (var i = 0; i < chars.length; i++) {
+        var char = chars[i];
+        var contextParams = new ContextParams(chars, i);
+        this.dispatch('next', [contextParams]);
+        this.runContextCheck(contextParams);
+        var token = new Token(char);
+        this.tokens.push(token);
+        this.dispatch('newToken', [token, contextParams]);
+    }
+    this.dispatch('end', [this.tokens]);
+    return this.tokens;
+};
+
+// ╭─┄┄┄────────────────────────┄─────────────────────────────────────────────╮
+// ┊ Character Class Assertions ┊ Checks if a char belongs to a certain class ┊
+// ╰─╾──────────────────────────┄─────────────────────────────────────────────╯
+// jscs:disable maximumLineLength
+/**
+ * Check if a char is Arabic
+ * @param {string} c a single char
+ */
+function isArabicChar(c) {
+    return /[\u0600-\u065F\u066A-\u06D2\u06FA-\u06FF]/.test(c);
+}
+
+/**
+ * Check if a char is an isolated arabic char
+ * @param {string} c a single char
+ */
+function isIsolatedArabicChar(char) {
+    return /[\u0630\u0690\u0621\u0631\u0661\u0671\u0622\u0632\u0672\u0692\u06C2\u0623\u0673\u0693\u06C3\u0624\u0694\u06C4\u0625\u0675\u0695\u06C5\u06E5\u0676\u0696\u06C6\u0627\u0677\u0697\u06C7\u0648\u0688\u0698\u06C8\u0689\u0699\u06C9\u068A\u06CA\u066B\u068B\u06CB\u068C\u068D\u06CD\u06FD\u068E\u06EE\u06FE\u062F\u068F\u06CF\u06EF]/.test(char);
+}
+
+/**
+ * Check if a char is an Arabic Tashkeel char
+ * @param {string} c a single char
+ */
+function isTashkeelArabicChar(char) {
+    return /[\u0600-\u0605\u060C-\u060E\u0610-\u061B\u061E\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED]/.test(char);
+}
+
+/**
+ * Check if a char is Latin
+ * @param {string} c a single char
+ */
+function isLatinChar(c) {
+    return /[A-z]/.test(c);
+}
+
+/**
+ * Check if a char is whitespace char
+ * @param {string} c a single char
+ */
+function isWhiteSpace(c) {
+    return /\s/.test(c);
+}
+
+/**
+ * Query a feature by some of it's properties to lookup a glyph substitution.
+ */
+
+/**
+ * Create feature query instance
+ * @param {Font} font opentype font instance
+ */
+function FeatureQuery(font) {
+    this.font = font;
+    this.features = {};
+}
+
+/**
+ * @typedef SubstitutionAction
+ * @type Object
+ * @property {number} id substitution type
+ * @property {string} tag feature tag
+ * @property {any} substitution substitution value(s)
+ */
+
+/**
+ * Create a substitution action instance
+ * @param {SubstitutionAction} action
+ */
+function SubstitutionAction(action) {
+    this.id = action.id;
+    this.tag = action.tag;
+    this.substitution = action.substitution;
+}
+
+/**
+ * Lookup a coverage table
+ * @param {number} glyphIndex glyph index
+ * @param {CoverageTable} coverage coverage table
+ */
+function lookupCoverage(glyphIndex, coverage) {
+    if (!glyphIndex) { return -1; }
+    switch (coverage.format) {
+        case 1:
+            return coverage.glyphs.indexOf(glyphIndex);
+
+        case 2:
+            var ranges = coverage.ranges;
+            for (var i = 0; i < ranges.length; i++) {
+                var range = ranges[i];
+                if (glyphIndex >= range.start && glyphIndex <= range.end) {
+                    var offset = glyphIndex - range.start;
+                    return range.index + offset;
+                }
+            }
+            break;
+        default:
+            return -1; // not found
+    }
+    return -1;
+}
+
+/**
+ * Handle a single substitution - format 1
+ * @param {ContextParams} contextParams context params to lookup
+ */
+function singleSubstitutionFormat1(glyphIndex, subtable) {
+    var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
+    if (substituteIndex === -1) { return null; }
+    return glyphIndex + subtable.deltaGlyphId;
+}
+
+/**
+ * Handle a single substitution - format 2
+ * @param {ContextParams} contextParams context params to lookup
+ */
+function singleSubstitutionFormat2(glyphIndex, subtable) {
+    var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
+    if (substituteIndex === -1) { return null; }
+    return subtable.substitute[substituteIndex];
+}
+
+/**
+ * Lookup a list of coverage tables
+ * @param {any} coverageList a list of coverage tables
+ * @param {ContextParams} contextParams context params to lookup
+ */
+function lookupCoverageList(coverageList, contextParams) {
+    var lookupList = [];
+    for (var i = 0; i < coverageList.length; i++) {
+        var coverage = coverageList[i];
+        var glyphIndex = contextParams.current;
+        glyphIndex = Array.isArray(glyphIndex) ? glyphIndex[0] : glyphIndex;
+        var lookupIndex = lookupCoverage(glyphIndex, coverage);
+        if (lookupIndex !== -1) {
+            lookupList.push(lookupIndex);
+        }
+    }
+    if (lookupList.length !== coverageList.length) { return -1; }
+    return lookupList;
+}
+
+/**
+ * Handle chaining context substitution - format 3
+ * @param {ContextParams} contextParams context params to lookup
+ */
+function chainingSubstitutionFormat3(contextParams, subtable) {
+    var lookupsCount = (
+        subtable.inputCoverage.length +
+        subtable.lookaheadCoverage.length +
+        subtable.backtrackCoverage.length
+    );
+    if (contextParams.context.length < lookupsCount) { return []; }
+    // INPUT LOOKUP //
+    var inputLookups = lookupCoverageList(
+        subtable.inputCoverage, contextParams
+    );
+    if (inputLookups === -1) { return []; }
+    // LOOKAHEAD LOOKUP //
+    var lookaheadOffset = subtable.inputCoverage.length - 1;
+    if (contextParams.lookahead.length < subtable.lookaheadCoverage.length) { return []; }
+    var lookaheadContext = contextParams.lookahead.slice(lookaheadOffset);
+    while (lookaheadContext.length && isTashkeelArabicChar(lookaheadContext[0].char)) {
+        lookaheadContext.shift();
+    }
+    var lookaheadParams = new ContextParams(lookaheadContext, 0);
+    var lookaheadLookups = lookupCoverageList(
+        subtable.lookaheadCoverage, lookaheadParams
+    );
+    // BACKTRACK LOOKUP //
+    var backtrackContext = [].concat(contextParams.backtrack);
+    backtrackContext.reverse();
+    while (backtrackContext.length && isTashkeelArabicChar(backtrackContext[0].char)) {
+        backtrackContext.shift();
+    }
+    if (backtrackContext.length < subtable.backtrackCoverage.length) { return []; }
+    var backtrackParams = new ContextParams(backtrackContext, 0);
+    var backtrackLookups = lookupCoverageList(
+        subtable.backtrackCoverage, backtrackParams
+    );
+    var contextRulesMatch = (
+        inputLookups.length === subtable.inputCoverage.length &&
+        lookaheadLookups.length === subtable.lookaheadCoverage.length &&
+        backtrackLookups.length === subtable.backtrackCoverage.length
+    );
+    var substitutions = [];
+    if (contextRulesMatch) {
+        for (var i = 0; i < subtable.lookupRecords.length; i++) {
+            var lookupRecord = subtable.lookupRecords[i];
+            var lookupListIndex = lookupRecord.lookupListIndex;
+            var lookupTable = this.getLookupByIndex(lookupListIndex);
+            for (var s = 0; s < lookupTable.subtables.length; s++) {
+                var subtable$1 = lookupTable.subtables[s];
+                var lookup = this.getLookupMethod(lookupTable, subtable$1);
+                var substitutionType = this.getSubstitutionType(lookupTable, subtable$1);
+                if (substitutionType === '12') {
+                    for (var n = 0; n < inputLookups.length; n++) {
+                        var glyphIndex = contextParams.get(n);
+                        var substitution = lookup(glyphIndex);
+                        if (substitution) { substitutions.push(substitution); }
+                    }
+                }
+            }
+        }
+    }
+    return substitutions;
+}
+
+/**
+ * Handle ligature substitution - format 1
+ * @param {ContextParams} contextParams context params to lookup
+ */
+function ligatureSubstitutionFormat1(contextParams, subtable) {
+    // COVERAGE LOOKUP //
+    var glyphIndex = contextParams.current;
+    var ligSetIndex = lookupCoverage(glyphIndex, subtable.coverage);
+    if (ligSetIndex === -1) { return null; }
+    // COMPONENTS LOOKUP
+    // (!) note, components are ordered in the written direction.
+    var ligature;
+    var ligatureSet = subtable.ligatureSets[ligSetIndex];
+    for (var s = 0; s < ligatureSet.length; s++) {
+        ligature = ligatureSet[s];
+        for (var l = 0; l < ligature.components.length; l++) {
+            var lookaheadItem = contextParams.lookahead[l];
+            var component = ligature.components[l];
+            if (lookaheadItem !== component) { break; }
+            if (l === ligature.components.length - 1) { return ligature; }
+        }
+    }
+    return null;
+}
+
+/**
+ * Handle decomposition substitution - format 1
+ * @param {number} glyphIndex glyph index
+ * @param {any} subtable subtable
+ */
+function decompositionSubstitutionFormat1(glyphIndex, subtable) {
+    var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
+    if (substituteIndex === -1) { return null; }
+    return subtable.sequences[substituteIndex];
+}
+
+/**
+ * Get default script features indexes
+ */
+FeatureQuery.prototype.getDefaultScriptFeaturesIndexes = function () {
+    var scripts = this.font.tables.gsub.scripts;
+    for (var s = 0; s < scripts.length; s++) {
+        var script = scripts[s];
+        if (script.tag === 'DFLT') { return (
+            script.script.defaultLangSys.featureIndexes
+        ); }
+    }
+    return [];
+};
+
+/**
+ * Get feature indexes of a specific script
+ * @param {string} scriptTag script tag
+ */
+FeatureQuery.prototype.getScriptFeaturesIndexes = function(scriptTag) {
+    var tables = this.font.tables;
+    if (!tables.gsub) { return []; }
+    if (!scriptTag) { return this.getDefaultScriptFeaturesIndexes(); }
+    var scripts = this.font.tables.gsub.scripts;
+    for (var i = 0; i < scripts.length; i++) {
+        var script = scripts[i];
+        if (script.tag === scriptTag && script.script.defaultLangSys) {
+            return script.script.defaultLangSys.featureIndexes;
+        } else {
+            var langSysRecords = script.langSysRecords;
+            if (!!langSysRecords) {
+                for (var j = 0; j < langSysRecords.length; j++) {
+                    var langSysRecord = langSysRecords[j];
+                    if (langSysRecord.tag === scriptTag) {
+                        var langSys = langSysRecord.langSys;
+                        return langSys.featureIndexes;
+                    }
+                }
+            }
+        }
+    }
+    return this.getDefaultScriptFeaturesIndexes();
+};
+
+/**
+ * Map a feature tag to a gsub feature
+ * @param {any} features gsub features
+ * @param {string} scriptTag script tag
+ */
+FeatureQuery.prototype.mapTagsToFeatures = function (features, scriptTag) {
+    var tags = {};
+    for (var i = 0; i < features.length; i++) {
+        var tag = features[i].tag;
+        var feature = features[i].feature;
+        tags[tag] = feature;
+    }
+    this.features[scriptTag].tags = tags;
+};
+
+/**
+ * Get features of a specific script
+ * @param {string} scriptTag script tag
+ */
+FeatureQuery.prototype.getScriptFeatures = function (scriptTag) {
+    var features = this.features[scriptTag];
+    if (this.features.hasOwnProperty(scriptTag)) { return features; }
+    var featuresIndexes = this.getScriptFeaturesIndexes(scriptTag);
+    if (!featuresIndexes) { return null; }
+    var gsub = this.font.tables.gsub;
+    features = featuresIndexes.map(function (index) { return gsub.features[index]; });
+    this.features[scriptTag] = features;
+    this.mapTagsToFeatures(features, scriptTag);
+    return features;
+};
+
+/**
+ * Get substitution type
+ * @param {any} lookupTable lookup table
+ * @param {any} subtable subtable
+ */
+FeatureQuery.prototype.getSubstitutionType = function(lookupTable, subtable) {
+    var lookupType = lookupTable.lookupType.toString();
+    var substFormat = subtable.substFormat.toString();
+    return lookupType + substFormat;
+};
+
+/**
+ * Get lookup method
+ * @param {any} lookupTable lookup table
+ * @param {any} subtable subtable
+ */
+FeatureQuery.prototype.getLookupMethod = function(lookupTable, subtable) {
+    var this$1 = this;
+
+    var substitutionType = this.getSubstitutionType(lookupTable, subtable);
+    switch (substitutionType) {
+        case '11':
+            return function (glyphIndex) { return singleSubstitutionFormat1.apply(
+                this$1, [glyphIndex, subtable]
+            ); };
+        case '12':
+            return function (glyphIndex) { return singleSubstitutionFormat2.apply(
+                this$1, [glyphIndex, subtable]
+            ); };
+        case '63':
+            return function (contextParams) { return chainingSubstitutionFormat3.apply(
+                this$1, [contextParams, subtable]
+            ); };
+        case '41':
+            return function (contextParams) { return ligatureSubstitutionFormat1.apply(
+                this$1, [contextParams, subtable]
+            ); };
+        case '21':
+            return function (glyphIndex) { return decompositionSubstitutionFormat1.apply(
+                this$1, [glyphIndex, subtable]
+            ); };
+        default:
+            throw new Error(
+                "lookupType: " + (lookupTable.lookupType) + " - " +
+                "substFormat: " + (subtable.substFormat) + " " +
+                "is not yet supported"
+            );
+    }
+};
+
+/**
+ * [ LOOKUP TYPES ]
+ * -------------------------------
+ * Single                        1;
+ * Multiple                      2;
+ * Alternate                     3;
+ * Ligature                      4;
+ * Context                       5;
+ * ChainingContext               6;
+ * ExtensionSubstitution         7;
+ * ReverseChainingContext        8;
+ * -------------------------------
+ *
+ */
+
+/**
+ * @typedef FQuery
+ * @type Object
+ * @param {string} tag feature tag
+ * @param {string} script feature script
+ * @param {ContextParams} contextParams context params
+ */
+
+/**
+ * Lookup a feature using a query parameters
+ * @param {FQuery} query feature query
+ */
+FeatureQuery.prototype.lookupFeature = function (query) {
+    var contextParams = query.contextParams;
+    var currentIndex = contextParams.index;
+    var feature = this.getFeature({
+        tag: query.tag, script: query.script
+    });
+    if (!feature) { return new Error(
+        "font '" + (this.font.names.fullName.en) + "' " +
+        "doesn't support feature '" + (query.tag) + "' " +
+        "for script '" + (query.script) + "'."
+    ); }
+    var lookups = this.getFeatureLookups(feature);
+    var substitutions = [].concat(contextParams.context);
+    for (var l = 0; l < lookups.length; l++) {
+        var lookupTable = lookups[l];
+        var subtables = this.getLookupSubtables(lookupTable);
+        for (var s = 0; s < subtables.length; s++) {
+            var subtable = subtables[s];
+            var substType = this.getSubstitutionType(lookupTable, subtable);
+            var lookup = this.getLookupMethod(lookupTable, subtable);
+            var substitution = (void 0);
+            switch (substType) {
+                case '11':
+                    substitution = lookup(contextParams.current);
+                    if (substitution) {
+                        substitutions.splice(currentIndex, 1, new SubstitutionAction({
+                            id: 11, tag: query.tag, substitution: substitution
+                        }));
+                    }
+                    break;
+                case '12':
+                    substitution = lookup(contextParams.current);
+                    if (substitution) {
+                        substitutions.splice(currentIndex, 1, new SubstitutionAction({
+                            id: 12, tag: query.tag, substitution: substitution
+                        }));
+                    }
+                    break;
+                case '63':
+                    substitution = lookup(contextParams);
+                    if (Array.isArray(substitution) && substitution.length) {
+                        substitutions.splice(currentIndex, 1, new SubstitutionAction({
+                            id: 63, tag: query.tag, substitution: substitution
+                        }));
+                    }
+                    break;
+                case '41':
+                    substitution = lookup(contextParams);
+                    if (substitution) {
+                        substitutions.splice(currentIndex, 1, new SubstitutionAction({
+                            id: 41, tag: query.tag, substitution: substitution
+                        }));
+                    }
+                    break;
+                case '21':
+                    substitution = lookup(contextParams.current);
+                    if (substitution) {
+                        substitutions.splice(currentIndex, 1, new SubstitutionAction({
+                            id: 21, tag: query.tag, substitution: substitution
+                        }));
+                    }
+                    break;
+            }
+            contextParams = new ContextParams(substitutions, currentIndex);
+            if (Array.isArray(substitution) && !substitution.length) { continue; }
+            substitution = null;
+        }
+    }
+    return substitutions.length ? substitutions : null;
+};
+
+/**
+ * Checks if a font supports a specific features
+ * @param {FQuery} query feature query object
+ */
+FeatureQuery.prototype.supports = function (query) {
+    if (!query.script) { return false; }
+    this.getScriptFeatures(query.script);
+    var supportedScript = this.features.hasOwnProperty(query.script);
+    if (!query.tag) { return supportedScript; }
+    var supportedFeature = (
+        this.features[query.script].some(function (feature) { return feature.tag === query.tag; })
+    );
+    return supportedScript && supportedFeature;
+};
+
+/**
+ * Get lookup table subtables
+ * @param {any} lookupTable lookup table
+ */
+FeatureQuery.prototype.getLookupSubtables = function (lookupTable) {
+    return lookupTable.subtables || null;
+};
+
+/**
+ * Get lookup table by index
+ * @param {number} index lookup table index
+ */
+FeatureQuery.prototype.getLookupByIndex = function (index) {
+    var lookups = this.font.tables.gsub.lookups;
+    return lookups[index] || null;
+};
+
+/**
+ * Get lookup tables for a feature
+ * @param {string} feature
+ */
+FeatureQuery.prototype.getFeatureLookups = function (feature) {
+    // TODO: memoize
+    return feature.lookupListIndexes.map(this.getLookupByIndex.bind(this));
+};
+
+/**
+ * Query a feature by it's properties
+ * @param {any} query an object that describes the properties of a query
+ */
+FeatureQuery.prototype.getFeature = function getFeature(query) {
+    if (!this.font) { return { FAIL: "No font was found"}; }
+    if (!this.features.hasOwnProperty(query.script)) {
+        this.getScriptFeatures(query.script);
+    }
+    var scriptFeatures = this.features[query.script];
+    if (!scriptFeatures) { return (
+        { FAIL: ("No feature for script " + (query.script))}
+    ); }
+    if (!scriptFeatures.tags[query.tag]) { return null; }
+    return this.features[query.script].tags[query.tag];
+};
+
+/**
+ * Arabic word context checkers
+ */
+
+function arabicWordStartCheck(contextParams) {
+    var char = contextParams.current;
+    var prevChar = contextParams.get(-1);
+    return (
+        // ? arabic first char
+        (prevChar === null && isArabicChar(char)) ||
+        // ? arabic char preceded with a non arabic char
+        (!isArabicChar(prevChar) && isArabicChar(char))
+    );
+}
+
+function arabicWordEndCheck(contextParams) {
+    var nextChar = contextParams.get(1);
+    return (
+        // ? last arabic char
+        (nextChar === null) ||
+        // ? next char is not arabic
+        (!isArabicChar(nextChar))
+    );
+}
+
+var arabicWordCheck = {
+    startCheck: arabicWordStartCheck,
+    endCheck: arabicWordEndCheck
+};
+
+/**
+ * Arabic sentence context checkers
+ */
+
+function arabicSentenceStartCheck(contextParams) {
+    var char = contextParams.current;
+    var prevChar = contextParams.get(-1);
+    return (
+        // ? an arabic char preceded with a non arabic char
+        (isArabicChar(char) || isTashkeelArabicChar(char)) &&
+        !isArabicChar(prevChar)
+    );
+}
+
+function arabicSentenceEndCheck(contextParams) {
+    var nextChar = contextParams.get(1);
+    switch (true) {
+        case nextChar === null:
+            return true;
+        case (!isArabicChar(nextChar) && !isTashkeelArabicChar(nextChar)):
+            var nextIsWhitespace = isWhiteSpace(nextChar);
+            if (!nextIsWhitespace) { return true; }
+            if (nextIsWhitespace) {
+                var arabicCharAhead = false;
+                arabicCharAhead = (
+                    contextParams.lookahead.some(
+                        function (c) { return isArabicChar(c) || isTashkeelArabicChar(c); }
+                    )
+                );
+                if (!arabicCharAhead) { return true; }
+            }
+            break;
+        default:
+            return false;
+    }
+}
+
+var arabicSentenceCheck = {
+    startCheck: arabicSentenceStartCheck,
+    endCheck: arabicSentenceEndCheck
+};
+
+/**
+ * Apply single substitution format 1
+ * @param {Array} substitutions substitutions
+ * @param {any} tokens a list of tokens
+ * @param {number} index token index
+ */
+function singleSubstitutionFormat1$1(action, tokens, index) {
+    tokens[index].setState(action.tag, action.substitution);
+}
+
+/**
+ * Apply single substitution format 2
+ * @param {Array} substitutions substitutions
+ * @param {any} tokens a list of tokens
+ * @param {number} index token index
+ */
+function singleSubstitutionFormat2$1(action, tokens, index) {
+    tokens[index].setState(action.tag, action.substitution);
+}
+
+/**
+ * Apply chaining context substitution format 3
+ * @param {Array} substitutions substitutions
+ * @param {any} tokens a list of tokens
+ * @param {number} index token index
+ */
+function chainingSubstitutionFormat3$1(action, tokens, index) {
+    action.substitution.forEach(function (subst, offset) {
+        var token = tokens[index + offset];
+        token.setState(action.tag, subst);
+    });
+}
+
+/**
+ * Apply ligature substitution format 1
+ * @param {Array} substitutions substitutions
+ * @param {any} tokens a list of tokens
+ * @param {number} index token index
+ */
+function ligatureSubstitutionFormat1$1(action, tokens, index) {
+    var token = tokens[index];
+    token.setState(action.tag, action.substitution.ligGlyph);
+    var compsCount = action.substitution.components.length;
+    for (var i = 0; i < compsCount; i++) {
+        token = tokens[index + i + 1];
+        token.setState('deleted', true);
+    }
+}
+
+/**
+ * Supported substitutions
+ */
+var SUBSTITUTIONS = {
+    11: singleSubstitutionFormat1$1,
+    12: singleSubstitutionFormat2$1,
+    63: chainingSubstitutionFormat3$1,
+    41: ligatureSubstitutionFormat1$1
+};
+
+/**
+ * Apply substitutions to a list of tokens
+ * @param {Array} substitutions substitutions
+ * @param {any} tokens a list of tokens
+ * @param {number} index token index
+ */
+function applySubstitution(action, tokens, index) {
+    if (action instanceof SubstitutionAction && SUBSTITUTIONS[action.id]) {
+        SUBSTITUTIONS[action.id](action, tokens, index);
+    }
+}
+
+/**
+ * Apply Arabic presentation forms to a range of tokens
+ */
+
+/**
+ * Check if a char can be connected to it's preceding char
+ * @param {ContextParams} charContextParams context params of a char
+ */
+function willConnectPrev(charContextParams) {
+    var backtrack = [].concat(charContextParams.backtrack);
+    for (var i = backtrack.length - 1; i >= 0; i--) {
+        var prevChar = backtrack[i];
+        var isolated = isIsolatedArabicChar(prevChar);
+        var tashkeel = isTashkeelArabicChar(prevChar);
+        if (!isolated && !tashkeel) { return true; }
+        if (isolated) { return false; }
+    }
+    return false;
+}
+
+/**
+ * Check if a char can be connected to it's proceeding char
+ * @param {ContextParams} charContextParams context params of a char
+ */
+function willConnectNext(charContextParams) {
+    if (isIsolatedArabicChar(charContextParams.current)) { return false; }
+    for (var i = 0; i < charContextParams.lookahead.length; i++) {
+        var nextChar = charContextParams.lookahead[i];
+        var tashkeel = isTashkeelArabicChar(nextChar);
+        if (!tashkeel) { return true; }
+    }
+    return false;
+}
+
+/**
+ * Apply arabic presentation forms to a list of tokens
+ * @param {ContextRange} range a range of tokens
+ */
+function arabicPresentationForms(range) {
+    var this$1 = this;
+
+    var script = 'arab';
+    var tags = this.featuresTags[script];
+    var tokens = this.tokenizer.getRangeTokens(range);
+    if (tokens.length === 1) { return; }
+    var contextParams = new ContextParams(
+        tokens.map(function (token) { return token.getState('glyphIndex'); }
+    ), 0);
+    var charContextParams = new ContextParams(
+        tokens.map(function (token) { return token.char; }
+    ), 0);
+    tokens.forEach(function (token, index) {
+        if (isTashkeelArabicChar(token.char)) { return; }
+        contextParams.setCurrentIndex(index);
+        charContextParams.setCurrentIndex(index);
+        var CONNECT = 0; // 2 bits 00 (10: can connect next) (01: can connect prev)
+        if (willConnectPrev(charContextParams)) { CONNECT |= 1; }
+        if (willConnectNext(charContextParams)) { CONNECT |= 2; }
+        var tag;
+        switch (CONNECT) {
+            case 1: (tag = 'fina'); break;
+            case 2: (tag = 'init'); break;
+            case 3: (tag = 'medi'); break;
+        }
+        if (tags.indexOf(tag) === -1) { return; }
+        var substitutions = this$1.query.lookupFeature({
+            tag: tag, script: script, contextParams: contextParams
+        });
+        if (substitutions instanceof Error) { return console.info(substitutions.message); }
+        substitutions.forEach(function (action, index) {
+            if (action instanceof SubstitutionAction) {
+                applySubstitution(action, tokens, index);
+                contextParams.context[index] = action.substitution;
+            }
+        });
+    });
+}
+
+/**
+ * Apply Arabic required ligatures feature to a range of tokens
+ */
+
+/**
+ * Update context params
+ * @param {any} tokens a list of tokens
+ * @param {number} index current item index
+ */
+function getContextParams(tokens, index) {
+    var context = tokens.map(function (token) { return token.activeState.value; });
+    return new ContextParams(context, index || 0);
+}
+
+/**
+ * Apply Arabic required ligatures to a context range
+ * @param {ContextRange} range a range of tokens
+ */
+function arabicRequiredLigatures(range) {
+    var this$1 = this;
+
+    var script = 'arab';
+    var tokens = this.tokenizer.getRangeTokens(range);
+    var contextParams = getContextParams(tokens);
+    contextParams.context.forEach(function (glyphIndex, index) {
+        contextParams.setCurrentIndex(index);
+        var substitutions = this$1.query.lookupFeature({
+            tag: 'rlig', script: script, contextParams: contextParams
+        });
+        if (substitutions.length) {
+            substitutions.forEach(
+                function (action) { return applySubstitution(action, tokens, index); }
+            );
+            contextParams = getContextParams(tokens);
+        }
+    });
+}
+
+/**
+ * Latin word context checkers
+ */
+
+function latinWordStartCheck(contextParams) {
+    var char = contextParams.current;
+    var prevChar = contextParams.get(-1);
+    return (
+        // ? latin first char
+        (prevChar === null && isLatinChar(char)) ||
+        // ? latin char preceded with a non latin char
+        (!isLatinChar(prevChar) && isLatinChar(char))
+    );
+}
+
+function latinWordEndCheck(contextParams) {
+    var nextChar = contextParams.get(1);
+    return (
+        // ? last latin char
+        (nextChar === null) ||
+        // ? next char is not latin
+        (!isLatinChar(nextChar))
+    );
+}
+
+var latinWordCheck = {
+    startCheck: latinWordStartCheck,
+    endCheck: latinWordEndCheck
+};
+
+/**
+ * Apply Latin ligature feature to a range of tokens
+ */
+
+/**
+ * Update context params
+ * @param {any} tokens a list of tokens
+ * @param {number} index current item index
+ */
+function getContextParams$1(tokens, index) {
+    var context = tokens.map(function (token) { return token.activeState.value; });
+    return new ContextParams(context, index || 0);
+}
+
+/**
+ * Apply Arabic required ligatures to a context range
+ * @param {ContextRange} range a range of tokens
+ */
+function latinLigature(range) {
+    var this$1 = this;
+
+    var script = 'latn';
+    var tokens = this.tokenizer.getRangeTokens(range);
+    var contextParams = getContextParams$1(tokens);
+    contextParams.context.forEach(function (glyphIndex, index) {
+        contextParams.setCurrentIndex(index);
+        var substitutions = this$1.query.lookupFeature({
+            tag: 'liga', script: script, contextParams: contextParams
+        });
+        if (substitutions.length) {
+            substitutions.forEach(
+                function (action) { return applySubstitution(action, tokens, index); }
+            );
+            contextParams = getContextParams$1(tokens);
+        }
+    });
+}
+
+/**
+ * Infer bidirectional properties for a given text and apply
+ * the corresponding layout rules.
+ */
+
+/**
+ * Create Bidi. features
+ * @param {string} baseDir text base direction. value either 'ltr' or 'rtl'
+ */
+function Bidi(baseDir) {
+    this.baseDir = baseDir || 'ltr';
+    this.tokenizer = new Tokenizer();
+    this.featuresTags = {};
+}
+
+/**
+ * Sets Bidi text
+ * @param {string} text a text input
+ */
+Bidi.prototype.setText = function (text) {
+    this.text = text;
+};
+
+/**
+ * Store essential context checks:
+ * arabic word check for applying gsub features
+ * arabic sentence check for adjusting arabic layout
+ */
+Bidi.prototype.contextChecks = ({
+    latinWordCheck: latinWordCheck,
+    arabicWordCheck: arabicWordCheck,
+    arabicSentenceCheck: arabicSentenceCheck
+});
+
+/**
+ * Register arabic word check
+ */
+function registerContextChecker(checkId) {
+    var check = this.contextChecks[(checkId + "Check")];
+    return this.tokenizer.registerContextChecker(
+        checkId, check.startCheck, check.endCheck
+    );
+}
+
+/**
+ * Perform pre tokenization procedure then
+ * tokenize text input
+ */
+function tokenizeText() {
+    registerContextChecker.call(this, 'latinWord');
+    registerContextChecker.call(this, 'arabicWord');
+    registerContextChecker.call(this, 'arabicSentence');
+    return this.tokenizer.tokenize(this.text);
+}
+
+/**
+ * Reverse arabic sentence layout
+ * TODO: check base dir before applying adjustments - priority low
+ */
+function reverseArabicSentences() {
+    var this$1 = this;
+
+    var ranges = this.tokenizer.getContextRanges('arabicSentence');
+    ranges.forEach(function (range) {
+        var rangeTokens = this$1.tokenizer.getRangeTokens(range);
+        this$1.tokenizer.replaceRange(
+            range.startIndex,
+            range.endOffset,
+            rangeTokens.reverse()
+        );
+    });
+}
+
+/**
+ * Register supported features tags
+ * @param {script} script script tag
+ * @param {Array} tags features tags list
+ */
+Bidi.prototype.registerFeatures = function (script, tags) {
+    var this$1 = this;
+
+    var supportedTags = tags.filter(
+        function (tag) { return this$1.query.supports({script: script, tag: tag}); }
+    );
+    if (!this.featuresTags.hasOwnProperty(script)) {
+        this.featuresTags[script] = supportedTags;
+    } else {
+        this.featuresTags[script] =
+        this.featuresTags[script].concat(supportedTags);
+    }
+};
+
+/**
+ * Apply GSUB features
+ * @param {Array} tagsList a list of features tags
+ * @param {string} script a script tag
+ * @param {Font} font opentype font instance
+ */
+Bidi.prototype.applyFeatures = function (font, features) {
+    if (!font) { throw new Error(
+        'No valid font was provided to apply features'
+    ); }
+    if (!this.query) { this.query = new FeatureQuery(font); }
+    for (var f = 0; f < features.length; f++) {
+        var feature = features[f];
+        if (!this.query.supports({script: feature.script})) { continue; }
+        this.registerFeatures(feature.script, feature.tags);
+    }
+};
+
+/**
+ * Register a state modifier
+ * @param {string} modifierId state modifier id
+ * @param {function} condition a predicate function that returns true or false
+ * @param {function} modifier a modifier function to set token state
+ */
+Bidi.prototype.registerModifier = function (modifierId, condition, modifier) {
+    this.tokenizer.registerModifier(modifierId, condition, modifier);
+};
+
+/**
+ * Check if 'glyphIndex' is registered
+ */
+function checkGlyphIndexStatus() {
+    if (this.tokenizer.registeredModifiers.indexOf('glyphIndex') === -1) {
+        throw new Error(
+            'glyphIndex modifier is required to apply ' +
+            'arabic presentation features.'
+        );
+    }
+}
+
+/**
+ * Apply arabic presentation forms features
+ */
+function applyArabicPresentationForms() {
+    var this$1 = this;
+
+    var script = 'arab';
+    if (!this.featuresTags.hasOwnProperty(script)) { return; }
+    checkGlyphIndexStatus.call(this);
+    var ranges = this.tokenizer.getContextRanges('arabicWord');
+    ranges.forEach(function (range) {
+        arabicPresentationForms.call(this$1, range);
+    });
+}
+
+/**
+ * Apply required arabic ligatures
+ */
+function applyArabicRequireLigatures() {
+    var this$1 = this;
+
+    var script = 'arab';
+    if (!this.featuresTags.hasOwnProperty(script)) { return; }
+    var tags = this.featuresTags[script];
+    if (tags.indexOf('rlig') === -1) { return; }
+    checkGlyphIndexStatus.call(this);
+    var ranges = this.tokenizer.getContextRanges('arabicWord');
+    ranges.forEach(function (range) {
+        arabicRequiredLigatures.call(this$1, range);
+    });
+}
+
+/**
+ * Apply required arabic ligatures
+ */
+function applyLatinLigatures() {
+    var this$1 = this;
+
+    var script = 'latn';
+    if (!this.featuresTags.hasOwnProperty(script)) { return; }
+    var tags = this.featuresTags[script];
+    if (tags.indexOf('liga') === -1) { return; }
+    checkGlyphIndexStatus.call(this);
+    var ranges = this.tokenizer.getContextRanges('latinWord');
+    ranges.forEach(function (range) {
+        latinLigature.call(this$1, range);
+    });
+}
+
+/**
+ * Check if a context is registered
+ * @param {string} contextId context id
+ */
+Bidi.prototype.checkContextReady = function (contextId) {
+    return !!this.tokenizer.getContext(contextId);
+};
+
+/**
+ * Apply features to registered contexts
+ */
+Bidi.prototype.applyFeaturesToContexts = function () {
+    if (this.checkContextReady('arabicWord')) {
+        applyArabicPresentationForms.call(this);
+        applyArabicRequireLigatures.call(this);
+    }
+    if (this.checkContextReady('latinWord')) {
+        applyLatinLigatures.call(this);
+    }
+    if (this.checkContextReady('arabicSentence')) {
+        reverseArabicSentences.call(this);
+    }
+};
+
+/**
+ * process text input
+ * @param {string} text an input text
+ */
+Bidi.prototype.processText = function(text) {
+    if (!this.text || this.text !== text) {
+        this.setText(text);
+        tokenizeText.call(this);
+        this.applyFeaturesToContexts();
+    }
+};
+
+/**
+ * Process a string of text to identify and adjust
+ * bidirectional text entities.
+ * @param {string} text input text
+ */
+Bidi.prototype.getBidiText = function (text) {
+    this.processText(text);
+    return this.tokenizer.getText();
+};
+
+/**
+ * Get the current state index of each token
+ * @param {text} text an input text
+ */
+Bidi.prototype.getTextGlyphs = function (text) {
+    this.processText(text);
+    var indexes = [];
+    for (var i = 0; i < this.tokenizer.tokens.length; i++) {
+        var token = this.tokenizer.tokens[i];
+        if (token.state.deleted) { continue; }
+        var index = token.activeState.value;
+        indexes.push(Array.isArray(index) ? index[0] : index);
+    }
+    return indexes;
+};
+
+// The Font object
+
+/**
+ * @typedef FontOptions
+ * @type Object
+ * @property {Boolean} empty - whether to create a new empty font
+ * @property {string} familyName
+ * @property {string} styleName
+ * @property {string=} fullName
+ * @property {string=} postScriptName
+ * @property {string=} designer
+ * @property {string=} designerURL
+ * @property {string=} manufacturer
+ * @property {string=} manufacturerURL
+ * @property {string=} license
+ * @property {string=} licenseURL
+ * @property {string=} version
+ * @property {string=} description
+ * @property {string=} copyright
+ * @property {string=} trademark
+ * @property {Number} unitsPerEm
+ * @property {Number} ascender
+ * @property {Number} descender
+ * @property {Number} createdTimestamp
+ * @property {string=} weightClass
+ * @property {string=} widthClass
+ * @property {string=} fsSelection
+ */
+
+/**
+ * A Font represents a loaded OpenType font file.
+ * It contains a set of glyphs and methods to draw text on a drawing context,
+ * or to get a path representing the text.
+ * @exports opentype.Font
+ * @class
+ * @param {FontOptions}
+ * @constructor
+ */
+function Font(options) {
+    options = options || {};
+    options.tables = options.tables || {};
+
+    if (!options.empty) {
+        // Check that we've provided the minimum set of names.
+        checkArgument(options.familyName, 'When creating a new Font object, familyName is required.');
+        checkArgument(options.styleName, 'When creating a new Font object, styleName is required.');
+        checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.');
+        checkArgument(options.ascender, 'When creating a new Font object, ascender is required.');
+        checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.');
+
+        // OS X will complain if the names are empty, so we put a single space everywhere by default.
+        this.names = {
+            fontFamily: {en: options.familyName || ' '},
+            fontSubfamily: {en: options.styleName || ' '},
+            fullName: {en: options.fullName || options.familyName + ' ' + options.styleName},
+            // postScriptName may not contain any whitespace
+            postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')},
+            designer: {en: options.designer || ' '},
+            designerURL: {en: options.designerURL || ' '},
+            manufacturer: {en: options.manufacturer || ' '},
+            manufacturerURL: {en: options.manufacturerURL || ' '},
+            license: {en: options.license || ' '},
+            licenseURL: {en: options.licenseURL || ' '},
+            version: {en: options.version || 'Version 0.1'},
+            description: {en: options.description || ' '},
+            copyright: {en: options.copyright || ' '},
+            trademark: {en: options.trademark || ' '}
+        };
+        this.unitsPerEm = options.unitsPerEm || 1000;
+        this.ascender = options.ascender;
+        this.descender = options.descender;
+        this.createdTimestamp = options.createdTimestamp;
+        this.tables = Object.assign(options.tables, {
+            os2: Object.assign({
+                usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM,
+                usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM,
+                fsSelection: options.fsSelection || this.fsSelectionValues.REGULAR,
+            }, options.tables.os2)
+        });
+    }
+
+    this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported.
+    this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []);
+    this.encoding = new DefaultEncoding(this);
+    this.position = new Position(this);
+    this.substitution = new Substitution(this);
+    this.tables = this.tables || {};
+
+    // needed for low memory mode only.
+    this._push = null;
+    this._hmtxTableData = {};
+
+    Object.defineProperty(this, 'hinting', {
+        get: function() {
+            if (this._hinting) { return this._hinting; }
+            if (this.outlinesFormat === 'truetype') {
+                return (this._hinting = new Hinting(this));
+            }
+        }
+    });
+}
+
+/**
+ * Check if the font has a glyph for the given character.
+ * @param  {string}
+ * @return {Boolean}
+ */
+Font.prototype.hasChar = function(c) {
+    return this.encoding.charToGlyphIndex(c) !== null;
+};
+
+/**
+ * Convert the given character to a single glyph index.
+ * Note that this function assumes that there is a one-to-one mapping between
+ * the given character and a glyph; for complex scripts this might not be the case.
+ * @param  {string}
+ * @return {Number}
+ */
+Font.prototype.charToGlyphIndex = function(s) {
+    return this.encoding.charToGlyphIndex(s);
+};
+
+/**
+ * Convert the given character to a single Glyph object.
+ * Note that this function assumes that there is a one-to-one mapping between
+ * the given character and a glyph; for complex scripts this might not be the case.
+ * @param  {string}
+ * @return {opentype.Glyph}
+ */
+Font.prototype.charToGlyph = function(c) {
+    var glyphIndex = this.charToGlyphIndex(c);
+    var glyph = this.glyphs.get(glyphIndex);
+    if (!glyph) {
+        // .notdef
+        glyph = this.glyphs.get(0);
+    }
+
+    return glyph;
+};
+
+/**
+ * Update features
+ * @param {any} options features options
+ */
+Font.prototype.updateFeatures = function (options) {
+    // TODO: update all features options not only 'latn'.
+    return this.defaultRenderOptions.features.map(function (feature) {
+        if (feature.script === 'latn') {
+            return {
+                script: 'latn',
+                tags: feature.tags.filter(function (tag) { return options[tag]; })
+            };
+        } else {
+            return feature;
+        }
+    });
+};
+
+/**
+ * Convert the given text to a list of Glyph objects.
+ * Note that there is no strict one-to-one mapping between characters and
+ * glyphs, so the list of returned glyphs can be larger or smaller than the
+ * length of the given string.
+ * @param  {string}
+ * @param  {GlyphRenderOptions} [options]
+ * @return {opentype.Glyph[]}
+ */
+Font.prototype.stringToGlyphs = function(s, options) {
+    var this$1 = this;
+
+
+    var bidi = new Bidi();
+
+    // Create and register 'glyphIndex' state modifier
+    var charToGlyphIndexMod = function (token) { return this$1.charToGlyphIndex(token.char); };
+    bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod);
+
+    // roll-back to default features
+    var features = options ?
+    this.updateFeatures(options.features) :
+    this.defaultRenderOptions.features;
+
+    bidi.applyFeatures(this, features);
+
+    var indexes = bidi.getTextGlyphs(s);
+
+    var length = indexes.length;
+
+    // convert glyph indexes to glyph objects
+    var glyphs = new Array(length);
+    var notdef = this.glyphs.get(0);
+    for (var i = 0; i < length; i += 1) {
+        glyphs[i] = this.glyphs.get(indexes[i]) || notdef;
+    }
+    return glyphs;
+};
+
+/**
+ * @param  {string}
+ * @return {Number}
+ */
+Font.prototype.nameToGlyphIndex = function(name) {
+    return this.glyphNames.nameToGlyphIndex(name);
+};
+
+/**
+ * @param  {string}
+ * @return {opentype.Glyph}
+ */
+Font.prototype.nameToGlyph = function(name) {
+    var glyphIndex = this.nameToGlyphIndex(name);
+    var glyph = this.glyphs.get(glyphIndex);
+    if (!glyph) {
+        // .notdef
+        glyph = this.glyphs.get(0);
+    }
+
+    return glyph;
+};
+
+/**
+ * @param  {Number}
+ * @return {String}
+ */
+Font.prototype.glyphIndexToName = function(gid) {
+    if (!this.glyphNames.glyphIndexToName) {
+        return '';
+    }
+
+    return this.glyphNames.glyphIndexToName(gid);
+};
+
+/**
+ * Retrieve the value of the kerning pair between the left glyph (or its index)
+ * and the right glyph (or its index). If no kerning pair is found, return 0.
+ * The kerning value gets added to the advance width when calculating the spacing
+ * between glyphs.
+ * For GPOS kerning, this method uses the default script and language, which covers
+ * most use cases. To have greater control, use font.position.getKerningValue .
+ * @param  {opentype.Glyph} leftGlyph
+ * @param  {opentype.Glyph} rightGlyph
+ * @return {Number}
+ */
+Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) {
+    leftGlyph = leftGlyph.index || leftGlyph;
+    rightGlyph = rightGlyph.index || rightGlyph;
+    var gposKerning = this.position.defaultKerningTables;
+    if (gposKerning) {
+        return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph);
+    }
+    // "kern" table
+    return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0;
+};
+
+/**
+ * @typedef GlyphRenderOptions
+ * @type Object
+ * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used.
+ *                               See https://www.microsoft.com/typography/otspec/scripttags.htm
+ * @property {string} [language='dflt'] - language system used to determine which features to apply.
+ *                                        See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx
+ * @property {boolean} [kerning=true] - whether to include kerning values
+ * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system.
+ *                                 See https://www.microsoft.com/typography/otspec/featuretags.htm
+ */
+Font.prototype.defaultRenderOptions = {
+    kerning: true,
+    features: [
+        /**
+         * these 4 features are required to render Arabic text properly
+         * and shouldn't be turned off when rendering arabic text.
+         */
+        { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
+        { script: 'latn', tags: ['liga', 'rlig'] }
+    ]
+};
+
+/**
+ * Helper function that invokes the given callback for each glyph in the given text.
+ * The callback gets `(glyph, x, y, fontSize, options)`.* @param  {string} text
+ * @param {string} text - The text to apply.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {GlyphRenderOptions=} options
+ * @param  {Function} callback
+ */
+Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) {
+    x = x !== undefined ? x : 0;
+    y = y !== undefined ? y : 0;
+    fontSize = fontSize !== undefined ? fontSize : 72;
+    options = Object.assign({}, this.defaultRenderOptions, options);
+    var fontScale = 1 / this.unitsPerEm * fontSize;
+    var glyphs = this.stringToGlyphs(text, options);
+    var kerningLookups;
+    if (options.kerning) {
+        var script = options.script || this.position.getDefaultScriptName();
+        kerningLookups = this.position.getKerningTables(script, options.language);
+    }
+    for (var i = 0; i < glyphs.length; i += 1) {
+        var glyph = glyphs[i];
+        callback.call(this, glyph, x, y, fontSize, options);
+        if (glyph.advanceWidth) {
+            x += glyph.advanceWidth * fontScale;
+        }
+
+        if (options.kerning && i < glyphs.length - 1) {
+            // We should apply position adjustment lookups in a more generic way.
+            // Here we only use the xAdvance value.
+            var kerningValue = kerningLookups ?
+                  this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) :
+                  this.getKerningValue(glyph, glyphs[i + 1]);
+            x += kerningValue * fontScale;
+        }
+
+        if (options.letterSpacing) {
+            x += options.letterSpacing * fontSize;
+        } else if (options.tracking) {
+            x += (options.tracking / 1000) * fontSize;
+        }
+    }
+    return x;
+};
+
+/**
+ * Create a Path object that represents the given text.
+ * @param  {string} text - The text to create.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {GlyphRenderOptions=} options
+ * @return {opentype.Path}
+ */
+Font.prototype.getPath = function(text, x, y, fontSize, options) {
+    var fullPath = new Path();
+    this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
+        var glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
+        fullPath.extend(glyphPath);
+    });
+    return fullPath;
+};
+
+/**
+ * Create an array of Path objects that represent the glyphs of a given text.
+ * @param  {string} text - The text to create.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {GlyphRenderOptions=} options
+ * @return {opentype.Path[]}
+ */
+Font.prototype.getPaths = function(text, x, y, fontSize, options) {
+    var glyphPaths = [];
+    this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
+        var glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
+        glyphPaths.push(glyphPath);
+    });
+
+    return glyphPaths;
+};
+
+/**
+ * Returns the advance width of a text.
+ *
+ * This is something different than Path.getBoundingBox() as for example a
+ * suffixed whitespace increases the advanceWidth but not the bounding box
+ * or an overhanging letter like a calligraphic 'f' might have a quite larger
+ * bounding box than its advance width.
+ *
+ * This corresponds to canvas2dContext.measureText(text).width
+ *
+ * @param  {string} text - The text to create.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {GlyphRenderOptions=} options
+ * @return advance width
+ */
+Font.prototype.getAdvanceWidth = function(text, fontSize, options) {
+    return this.forEachGlyph(text, 0, 0, fontSize, options, function() {});
+};
+
+/**
+ * Draw the text on the given drawing context.
+ * @param  {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
+ * @param  {string} text - The text to create.
+ * @param  {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param  {GlyphRenderOptions=} options
+ */
+Font.prototype.draw = function(ctx, text, x, y, fontSize, options) {
+    this.getPath(text, x, y, fontSize, options).draw(ctx);
+};
+
+/**
+ * Draw the points of all glyphs in the text.
+ * On-curve points will be drawn in blue, off-curve points will be drawn in red.
+ * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
+ * @param {string} text - The text to create.
+ * @param {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param {GlyphRenderOptions=} options
+ */
+Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) {
+    this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
+        glyph.drawPoints(ctx, gX, gY, gFontSize);
+    });
+};
+
+/**
+ * Draw lines indicating important font measurements for all glyphs in the text.
+ * Black lines indicate the origin of the coordinate system (point 0,0).
+ * Blue lines indicate the glyph bounding box.
+ * Green line indicates the advance width of the glyph.
+ * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
+ * @param {string} text - The text to create.
+ * @param {number} [x=0] - Horizontal position of the beginning of the text.
+ * @param {number} [y=0] - Vertical position of the *baseline* of the text.
+ * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
+ * @param {GlyphRenderOptions=} options
+ */
+Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) {
+    this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
+        glyph.drawMetrics(ctx, gX, gY, gFontSize);
+    });
+};
+
+/**
+ * @param  {string}
+ * @return {string}
+ */
+Font.prototype.getEnglishName = function(name) {
+    var translations = this.names[name];
+    if (translations) {
+        return translations.en;
+    }
+};
+
+/**
+ * Validate
+ */
+Font.prototype.validate = function() {
+    var _this = this;
+
+    function assert(predicate, message) {
+    }
+
+    function assertNamePresent(name) {
+        var englishName = _this.getEnglishName(name);
+        assert(englishName && englishName.trim().length > 0);
+    }
+
+    // Identification information
+    assertNamePresent('fontFamily');
+    assertNamePresent('weightName');
+    assertNamePresent('manufacturer');
+    assertNamePresent('copyright');
+    assertNamePresent('version');
+
+    // Dimension information
+    assert(this.unitsPerEm > 0);
+};
+
+/**
+ * Convert the font object to a SFNT data structure.
+ * This structure contains all the necessary tables and metadata to create a binary OTF file.
+ * @return {opentype.Table}
+ */
+Font.prototype.toTables = function() {
+    return sfnt.fontToTable(this);
+};
+/**
+ * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.
+ */
+Font.prototype.toBuffer = function() {
+    console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.');
+    return this.toArrayBuffer();
+};
+/**
+ * Converts a `opentype.Font` into an `ArrayBuffer`
+ * @return {ArrayBuffer}
+ */
+Font.prototype.toArrayBuffer = function() {
+    var sfntTable = this.toTables();
+    var bytes = sfntTable.encode();
+    var buffer = new ArrayBuffer(bytes.length);
+    var intArray = new Uint8Array(buffer);
+    for (var i = 0; i < bytes.length; i++) {
+        intArray[i] = bytes[i];
+    }
+
+    return buffer;
+};
+
+/**
+ * Initiate a download of the OpenType font.
+ */
+Font.prototype.download = function(fileName) {
+    var familyName = this.getEnglishName('fontFamily');
+    var styleName = this.getEnglishName('fontSubfamily');
+    fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf';
+    var arrayBuffer = this.toArrayBuffer();
+
+    if (isBrowser()) {
+        window.URL = window.URL || window.webkitURL;
+
+        if (window.URL) {
+            var dataView = new DataView(arrayBuffer);
+            var blob = new Blob([dataView], {type: 'font/opentype'});
+
+            var link = document.createElement('a');
+            link.href = window.URL.createObjectURL(blob);
+            link.download = fileName;
+
+            var event = document.createEvent('MouseEvents');
+            event.initEvent('click', true, false);
+            link.dispatchEvent(event);
+        } else {
+            console.warn('Font file could not be downloaded. Try using a different browser.');
+        }
+    } else {
+        var fs = require('fs');
+        var buffer = arrayBufferToNodeBuffer(arrayBuffer);
+        fs.writeFileSync(fileName, buffer);
+    }
+};
+/**
+ * @private
+ */
+Font.prototype.fsSelectionValues = {
+    ITALIC:              0x001, //1
+    UNDERSCORE:          0x002, //2
+    NEGATIVE:            0x004, //4
+    OUTLINED:            0x008, //8
+    STRIKEOUT:           0x010, //16
+    BOLD:                0x020, //32
+    REGULAR:             0x040, //64
+    USER_TYPO_METRICS:   0x080, //128
+    WWS:                 0x100, //256
+    OBLIQUE:             0x200  //512
+};
+
+/**
+ * @private
+ */
+Font.prototype.usWidthClasses = {
+    ULTRA_CONDENSED: 1,
+    EXTRA_CONDENSED: 2,
+    CONDENSED: 3,
+    SEMI_CONDENSED: 4,
+    MEDIUM: 5,
+    SEMI_EXPANDED: 6,
+    EXPANDED: 7,
+    EXTRA_EXPANDED: 8,
+    ULTRA_EXPANDED: 9
+};
+
+/**
+ * @private
+ */
+Font.prototype.usWeightClasses = {
+    THIN: 100,
+    EXTRA_LIGHT: 200,
+    LIGHT: 300,
+    NORMAL: 400,
+    MEDIUM: 500,
+    SEMI_BOLD: 600,
+    BOLD: 700,
+    EXTRA_BOLD: 800,
+    BLACK:    900
+};
+
+// The `fvar` table stores font variation axes and instances.
+
+function addName(name, names) {
+    var nameString = JSON.stringify(name);
+    var nameID = 256;
+    for (var nameKey in names) {
+        var n = parseInt(nameKey);
+        if (!n || n < 256) {
+            continue;
+        }
+
+        if (JSON.stringify(names[nameKey]) === nameString) {
+            return n;
+        }
+
+        if (nameID <= n) {
+            nameID = n + 1;
+        }
+    }
+
+    names[nameID] = name;
+    return nameID;
+}
+
+function makeFvarAxis(n, axis, names) {
+    var nameID = addName(axis.name, names);
+    return [
+        {name: 'tag_' + n, type: 'TAG', value: axis.tag},
+        {name: 'minValue_' + n, type: 'FIXED', value: axis.minValue << 16},
+        {name: 'defaultValue_' + n, type: 'FIXED', value: axis.defaultValue << 16},
+        {name: 'maxValue_' + n, type: 'FIXED', value: axis.maxValue << 16},
+        {name: 'flags_' + n, type: 'USHORT', value: 0},
+        {name: 'nameID_' + n, type: 'USHORT', value: nameID}
+    ];
+}
+
+function parseFvarAxis(data, start, names) {
+    var axis = {};
+    var p = new parse.Parser(data, start);
+    axis.tag = p.parseTag();
+    axis.minValue = p.parseFixed();
+    axis.defaultValue = p.parseFixed();
+    axis.maxValue = p.parseFixed();
+    p.skip('uShort', 1);  // reserved for flags; no values defined
+    axis.name = names[p.parseUShort()] || {};
+    return axis;
+}
+
+function makeFvarInstance(n, inst, axes, names) {
+    var nameID = addName(inst.name, names);
+    var fields = [
+        {name: 'nameID_' + n, type: 'USHORT', value: nameID},
+        {name: 'flags_' + n, type: 'USHORT', value: 0}
+    ];
+
+    for (var i = 0; i < axes.length; ++i) {
+        var axisTag = axes[i].tag;
+        fields.push({
+            name: 'axis_' + n + ' ' + axisTag,
+            type: 'FIXED',
+            value: inst.coordinates[axisTag] << 16
+        });
+    }
+
+    return fields;
+}
+
+function parseFvarInstance(data, start, axes, names) {
+    var inst = {};
+    var p = new parse.Parser(data, start);
+    inst.name = names[p.parseUShort()] || {};
+    p.skip('uShort', 1);  // reserved for flags; no values defined
+
+    inst.coordinates = {};
+    for (var i = 0; i < axes.length; ++i) {
+        inst.coordinates[axes[i].tag] = p.parseFixed();
+    }
+
+    return inst;
+}
+
+function makeFvarTable(fvar, names) {
+    var result = new table.Table('fvar', [
+        {name: 'version', type: 'ULONG', value: 0x10000},
+        {name: 'offsetToData', type: 'USHORT', value: 0},
+        {name: 'countSizePairs', type: 'USHORT', value: 2},
+        {name: 'axisCount', type: 'USHORT', value: fvar.axes.length},
+        {name: 'axisSize', type: 'USHORT', value: 20},
+        {name: 'instanceCount', type: 'USHORT', value: fvar.instances.length},
+        {name: 'instanceSize', type: 'USHORT', value: 4 + fvar.axes.length * 4}
+    ]);
+    result.offsetToData = result.sizeOf();
+
+    for (var i = 0; i < fvar.axes.length; i++) {
+        result.fields = result.fields.concat(makeFvarAxis(i, fvar.axes[i], names));
+    }
+
+    for (var j = 0; j < fvar.instances.length; j++) {
+        result.fields = result.fields.concat(makeFvarInstance(j, fvar.instances[j], fvar.axes, names));
+    }
+
+    return result;
+}
+
+function parseFvarTable(data, start, names) {
+    var p = new parse.Parser(data, start);
+    var tableVersion = p.parseULong();
+    check.argument(tableVersion === 0x00010000, 'Unsupported fvar table version.');
+    var offsetToData = p.parseOffset16();
+    // Skip countSizePairs.
+    p.skip('uShort', 1);
+    var axisCount = p.parseUShort();
+    var axisSize = p.parseUShort();
+    var instanceCount = p.parseUShort();
+    var instanceSize = p.parseUShort();
+
+    var axes = [];
+    for (var i = 0; i < axisCount; i++) {
+        axes.push(parseFvarAxis(data, start + offsetToData + i * axisSize, names));
+    }
+
+    var instances = [];
+    var instanceStart = start + offsetToData + axisCount * axisSize;
+    for (var j = 0; j < instanceCount; j++) {
+        instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names));
+    }
+
+    return {axes: axes, instances: instances};
+}
+
+var fvar = { make: makeFvarTable, parse: parseFvarTable };
+
+// The `GDEF` table contains various glyph properties
+
+var attachList = function() {
+    return {
+        coverage: this.parsePointer(Parser.coverage),
+        attachPoints: this.parseList(Parser.pointer(Parser.uShortList))
+    };
+};
+
+var caretValue = function() {
+    var format = this.parseUShort();
+    check.argument(format === 1 || format === 2 || format === 3,
+        'Unsupported CaretValue table version.');
+    if (format === 1) {
+        return { coordinate: this.parseShort() };
+    } else if (format === 2) {
+        return { pointindex: this.parseShort() };
+    } else if (format === 3) {
+        // Device / Variation Index tables unsupported
+        return { coordinate: this.parseShort() };
+    }
+};
+
+var ligGlyph = function() {
+    return this.parseList(Parser.pointer(caretValue));
+};
+
+var ligCaretList = function() {
+    return {
+        coverage: this.parsePointer(Parser.coverage),
+        ligGlyphs: this.parseList(Parser.pointer(ligGlyph))
+    };
+};
+
+var markGlyphSets = function() {
+    this.parseUShort(); // Version
+    return this.parseList(Parser.pointer(Parser.coverage));
+};
+
+function parseGDEFTable(data, start) {
+    start = start || 0;
+    var p = new Parser(data, start);
+    var tableVersion = p.parseVersion(1);
+    check.argument(tableVersion === 1 || tableVersion === 1.2 || tableVersion === 1.3,
+        'Unsupported GDEF table version.');
+    var gdef = {
+        version: tableVersion,
+        classDef: p.parsePointer(Parser.classDef),
+        attachList: p.parsePointer(attachList),
+        ligCaretList: p.parsePointer(ligCaretList),
+        markAttachClassDef: p.parsePointer(Parser.classDef)
+    };
+    if (tableVersion >= 1.2) {
+        gdef.markGlyphSets = p.parsePointer(markGlyphSets);
+    }
+    return gdef;
+}
+var gdef = { parse: parseGDEFTable };
+
+// The `GPOS` table contains kerning pairs, among other things.
+
+var subtableParsers$1 = new Array(10);         // subtableParsers[0] is unused
+
+// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-1-single-adjustment-positioning-subtable
+// this = Parser instance
+subtableParsers$1[1] = function parseLookup1() {
+    var start = this.offset + this.relativeOffset;
+    var posformat = this.parseUShort();
+    if (posformat === 1) {
+        return {
+            posFormat: 1,
+            coverage: this.parsePointer(Parser.coverage),
+            value: this.parseValueRecord()
+        };
+    } else if (posformat === 2) {
+        return {
+            posFormat: 2,
+            coverage: this.parsePointer(Parser.coverage),
+            values: this.parseValueRecordList()
+        };
+    }
+    check.assert(false, '0x' + start.toString(16) + ': GPOS lookup type 1 format must be 1 or 2.');
+};
+
+// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable
+subtableParsers$1[2] = function parseLookup2() {
+    var start = this.offset + this.relativeOffset;
+    var posFormat = this.parseUShort();
+    check.assert(posFormat === 1 || posFormat === 2, '0x' + start.toString(16) + ': GPOS lookup type 2 format must be 1 or 2.');
+    var coverage = this.parsePointer(Parser.coverage);
+    var valueFormat1 = this.parseUShort();
+    var valueFormat2 = this.parseUShort();
+    if (posFormat === 1) {
+        // Adjustments for Glyph Pairs
+        return {
+            posFormat: posFormat,
+            coverage: coverage,
+            valueFormat1: valueFormat1,
+            valueFormat2: valueFormat2,
+            pairSets: this.parseList(Parser.pointer(Parser.list(function() {
+                return {        // pairValueRecord
+                    secondGlyph: this.parseUShort(),
+                    value1: this.parseValueRecord(valueFormat1),
+                    value2: this.parseValueRecord(valueFormat2)
+                };
+            })))
+        };
+    } else if (posFormat === 2) {
+        var classDef1 = this.parsePointer(Parser.classDef);
+        var classDef2 = this.parsePointer(Parser.classDef);
+        var class1Count = this.parseUShort();
+        var class2Count = this.parseUShort();
+        return {
+            // Class Pair Adjustment
+            posFormat: posFormat,
+            coverage: coverage,
+            valueFormat1: valueFormat1,
+            valueFormat2: valueFormat2,
+            classDef1: classDef1,
+            classDef2: classDef2,
+            class1Count: class1Count,
+            class2Count: class2Count,
+            classRecords: this.parseList(class1Count, Parser.list(class2Count, function() {
+                return {
+                    value1: this.parseValueRecord(valueFormat1),
+                    value2: this.parseValueRecord(valueFormat2)
+                };
+            }))
+        };
+    }
+};
+
+subtableParsers$1[3] = function parseLookup3() { return { error: 'GPOS Lookup 3 not supported' }; };
+subtableParsers$1[4] = function parseLookup4() { return { error: 'GPOS Lookup 4 not supported' }; };
+subtableParsers$1[5] = function parseLookup5() { return { error: 'GPOS Lookup 5 not supported' }; };
+subtableParsers$1[6] = function parseLookup6() { return { error: 'GPOS Lookup 6 not supported' }; };
+subtableParsers$1[7] = function parseLookup7() { return { error: 'GPOS Lookup 7 not supported' }; };
+subtableParsers$1[8] = function parseLookup8() { return { error: 'GPOS Lookup 8 not supported' }; };
+subtableParsers$1[9] = function parseLookup9() { return { error: 'GPOS Lookup 9 not supported' }; };
+
+// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
+function parseGposTable(data, start) {
+    start = start || 0;
+    var p = new Parser(data, start);
+    var tableVersion = p.parseVersion(1);
+    check.argument(tableVersion === 1 || tableVersion === 1.1, 'Unsupported GPOS table version ' + tableVersion);
+
+    if (tableVersion === 1) {
+        return {
+            version: tableVersion,
+            scripts: p.parseScriptList(),
+            features: p.parseFeatureList(),
+            lookups: p.parseLookupList(subtableParsers$1)
+        };
+    } else {
+        return {
+            version: tableVersion,
+            scripts: p.parseScriptList(),
+            features: p.parseFeatureList(),
+            lookups: p.parseLookupList(subtableParsers$1),
+            variations: p.parseFeatureVariationsList()
+        };
+    }
+
+}
+
+// GPOS Writing //////////////////////////////////////////////
+// NOT SUPPORTED
+var subtableMakers$1 = new Array(10);
+
+function makeGposTable(gpos) {
+    return new table.Table('GPOS', [
+        {name: 'version', type: 'ULONG', value: 0x10000},
+        {name: 'scripts', type: 'TABLE', value: new table.ScriptList(gpos.scripts)},
+        {name: 'features', type: 'TABLE', value: new table.FeatureList(gpos.features)},
+        {name: 'lookups', type: 'TABLE', value: new table.LookupList(gpos.lookups, subtableMakers$1)}
+    ]);
+}
+
+var gpos = { parse: parseGposTable, make: makeGposTable };
+
+// The `kern` table contains kerning pairs.
+
+function parseWindowsKernTable(p) {
+    var pairs = {};
+    // Skip nTables.
+    p.skip('uShort');
+    var subtableVersion = p.parseUShort();
+    check.argument(subtableVersion === 0, 'Unsupported kern sub-table version.');
+    // Skip subtableLength, subtableCoverage
+    p.skip('uShort', 2);
+    var nPairs = p.parseUShort();
+    // Skip searchRange, entrySelector, rangeShift.
+    p.skip('uShort', 3);
+    for (var i = 0; i < nPairs; i += 1) {
+        var leftIndex = p.parseUShort();
+        var rightIndex = p.parseUShort();
+        var value = p.parseShort();
+        pairs[leftIndex + ',' + rightIndex] = value;
+    }
+    return pairs;
+}
+
+function parseMacKernTable(p) {
+    var pairs = {};
+    // The Mac kern table stores the version as a fixed (32 bits) but we only loaded the first 16 bits.
+    // Skip the rest.
+    p.skip('uShort');
+    var nTables = p.parseULong();
+    //check.argument(nTables === 1, 'Only 1 subtable is supported (got ' + nTables + ').');
+    if (nTables > 1) {
+        console.warn('Only the first kern subtable is supported.');
+    }
+    p.skip('uLong');
+    var coverage = p.parseUShort();
+    var subtableVersion = coverage & 0xFF;
+    p.skip('uShort');
+    if (subtableVersion === 0) {
+        var nPairs = p.parseUShort();
+        // Skip searchRange, entrySelector, rangeShift.
+        p.skip('uShort', 3);
+        for (var i = 0; i < nPairs; i += 1) {
+            var leftIndex = p.parseUShort();
+            var rightIndex = p.parseUShort();
+            var value = p.parseShort();
+            pairs[leftIndex + ',' + rightIndex] = value;
+        }
+    }
+    return pairs;
+}
+
+// Parse the `kern` table which contains kerning pairs.
+function parseKernTable(data, start) {
+    var p = new parse.Parser(data, start);
+    var tableVersion = p.parseUShort();
+    if (tableVersion === 0) {
+        return parseWindowsKernTable(p);
+    } else if (tableVersion === 1) {
+        return parseMacKernTable(p);
+    } else {
+        throw new Error('Unsupported kern table version (' + tableVersion + ').');
+    }
+}
+
+var kern = { parse: parseKernTable };
+
+// The `loca` table stores the offsets to the locations of the glyphs in the font.
+
+// Parse the `loca` table. This table stores the offsets to the locations of the glyphs in the font,
+// relative to the beginning of the glyphData table.
+// The number of glyphs stored in the `loca` table is specified in the `maxp` table (under numGlyphs)
+// The loca table has two versions: a short version where offsets are stored as uShorts, and a long
+// version where offsets are stored as uLongs. The `head` table specifies which version to use
+// (under indexToLocFormat).
+function parseLocaTable(data, start, numGlyphs, shortVersion) {
+    var p = new parse.Parser(data, start);
+    var parseFn = shortVersion ? p.parseUShort : p.parseULong;
+    // There is an extra entry after the last index element to compute the length of the last glyph.
+    // That's why we use numGlyphs + 1.
+    var glyphOffsets = [];
+    for (var i = 0; i < numGlyphs + 1; i += 1) {
+        var glyphOffset = parseFn.call(p);
+        if (shortVersion) {
+            // The short table version stores the actual offset divided by 2.
+            glyphOffset *= 2;
+        }
+
+        glyphOffsets.push(glyphOffset);
+    }
+
+    return glyphOffsets;
+}
+
+var loca = { parse: parseLocaTable };
+
+// opentype.js
+
+/**
+ * The opentype library.
+ * @namespace opentype
+ */
+
+// File loaders /////////////////////////////////////////////////////////
+/**
+ * Loads a font from a file. The callback throws an error message as the first parameter if it fails
+ * and the font as an ArrayBuffer in the second parameter if it succeeds.
+ * @param  {string} path - The path of the file
+ * @param  {Function} callback - The function to call when the font load completes
+ */
+function loadFromFile(path, callback) {
+    var fs = require('fs');
+    fs.readFile(path, function(err, buffer) {
+        if (err) {
+            return callback(err.message);
+        }
+
+        callback(null, nodeBufferToArrayBuffer(buffer));
+    });
+}
+/**
+ * Loads a font from a URL. The callback throws an error message as the first parameter if it fails
+ * and the font as an ArrayBuffer in the second parameter if it succeeds.
+ * @param  {string} url - The URL of the font file.
+ * @param  {Function} callback - The function to call when the font load completes
+ */
+function loadFromUrl(url, callback) {
+    var request = new XMLHttpRequest();
+    request.open('get', url, true);
+    request.responseType = 'arraybuffer';
+    request.onload = function() {
+        if (request.response) {
+            return callback(null, request.response);
+        } else {
+            return callback('Font could not be loaded: ' + request.statusText);
+        }
+    };
+
+    request.onerror = function () {
+        callback('Font could not be loaded');
+    };
+
+    request.send();
+}
+
+// Table Directory Entries //////////////////////////////////////////////
+/**
+ * Parses OpenType table entries.
+ * @param  {DataView}
+ * @param  {Number}
+ * @return {Object[]}
+ */
+function parseOpenTypeTableEntries(data, numTables) {
+    var tableEntries = [];
+    var p = 12;
+    for (var i = 0; i < numTables; i += 1) {
+        var tag = parse.getTag(data, p);
+        var checksum = parse.getULong(data, p + 4);
+        var offset = parse.getULong(data, p + 8);
+        var length = parse.getULong(data, p + 12);
+        tableEntries.push({tag: tag, checksum: checksum, offset: offset, length: length, compression: false});
+        p += 16;
+    }
+
+    return tableEntries;
+}
+
+/**
+ * Parses WOFF table entries.
+ * @param  {DataView}
+ * @param  {Number}
+ * @return {Object[]}
+ */
+function parseWOFFTableEntries(data, numTables) {
+    var tableEntries = [];
+    var p = 44; // offset to the first table directory entry.
+    for (var i = 0; i < numTables; i += 1) {
+        var tag = parse.getTag(data, p);
+        var offset = parse.getULong(data, p + 4);
+        var compLength = parse.getULong(data, p + 8);
+        var origLength = parse.getULong(data, p + 12);
+        var compression = (void 0);
+        if (compLength < origLength) {
+            compression = 'WOFF';
+        } else {
+            compression = false;
+        }
+
+        tableEntries.push({tag: tag, offset: offset, compression: compression,
+            compressedLength: compLength, length: origLength});
+        p += 20;
+    }
+
+    return tableEntries;
+}
+
+/**
+ * @typedef TableData
+ * @type Object
+ * @property {DataView} data - The DataView
+ * @property {number} offset - The data offset.
+ */
+
+/**
+ * @param  {DataView}
+ * @param  {Object}
+ * @return {TableData}
+ */
+function uncompressTable(data, tableEntry) {
+    if (tableEntry.compression === 'WOFF') {
+        var inBuffer = new Uint8Array(data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2);
+        var outBuffer = new Uint8Array(tableEntry.length);
+        tinyInflate(inBuffer, outBuffer);
+        if (outBuffer.byteLength !== tableEntry.length) {
+            throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length');
+        }
+
+        var view = new DataView(outBuffer.buffer, 0);
+        return {data: view, offset: 0};
+    } else {
+        return {data: data, offset: tableEntry.offset};
+    }
+}
+
+// Public API ///////////////////////////////////////////////////////////
+
+/**
+ * Parse the OpenType file data (as an ArrayBuffer) and return a Font object.
+ * Throws an error if the font could not be parsed.
+ * @param  {ArrayBuffer}
+ * @param  {Object} opt - options for parsing
+ * @return {opentype.Font}
+ */
+function parseBuffer(buffer, opt) {
+    opt = (opt === undefined || opt === null) ?  {} : opt;
+
+    var indexToLocFormat;
+    var ltagTable;
+
+    // Since the constructor can also be called to create new fonts from scratch, we indicate this
+    // should be an empty font that we'll fill with our own data.
+    var font = new Font({empty: true});
+
+    // OpenType fonts use big endian byte ordering.
+    // We can't rely on typed array view types, because they operate with the endianness of the host computer.
+    // Instead we use DataViews where we can specify endianness.
+    var data = new DataView(buffer, 0);
+    var numTables;
+    var tableEntries = [];
+    var signature = parse.getTag(data, 0);
+    if (signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1') {
+        font.outlinesFormat = 'truetype';
+        numTables = parse.getUShort(data, 4);
+        tableEntries = parseOpenTypeTableEntries(data, numTables);
+    } else if (signature === 'OTTO') {
+        font.outlinesFormat = 'cff';
+        numTables = parse.getUShort(data, 4);
+        tableEntries = parseOpenTypeTableEntries(data, numTables);
+    } else if (signature === 'wOFF') {
+        var flavor = parse.getTag(data, 4);
+        if (flavor === String.fromCharCode(0, 1, 0, 0)) {
+            font.outlinesFormat = 'truetype';
+        } else if (flavor === 'OTTO') {
+            font.outlinesFormat = 'cff';
+        } else {
+            throw new Error('Unsupported OpenType flavor ' + signature);
+        }
+
+        numTables = parse.getUShort(data, 12);
+        tableEntries = parseWOFFTableEntries(data, numTables);
+    } else {
+        throw new Error('Unsupported OpenType signature ' + signature);
+    }
+
+    var cffTableEntry;
+    var fvarTableEntry;
+    var glyfTableEntry;
+    var gdefTableEntry;
+    var gposTableEntry;
+    var gsubTableEntry;
+    var hmtxTableEntry;
+    var kernTableEntry;
+    var locaTableEntry;
+    var nameTableEntry;
+    var metaTableEntry;
+    var p;
+
+    for (var i = 0; i < numTables; i += 1) {
+        var tableEntry = tableEntries[i];
+        var table = (void 0);
+        switch (tableEntry.tag) {
+            case 'cmap':
+                table = uncompressTable(data, tableEntry);
+                font.tables.cmap = cmap.parse(table.data, table.offset);
+                font.encoding = new CmapEncoding(font.tables.cmap);
+                break;
+            case 'cvt ' :
+                table = uncompressTable(data, tableEntry);
+                p = new parse.Parser(table.data, table.offset);
+                font.tables.cvt = p.parseShortList(tableEntry.length / 2);
+                break;
+            case 'fvar':
+                fvarTableEntry = tableEntry;
+                break;
+            case 'fpgm' :
+                table = uncompressTable(data, tableEntry);
+                p = new parse.Parser(table.data, table.offset);
+                font.tables.fpgm = p.parseByteList(tableEntry.length);
+                break;
+            case 'head':
+                table = uncompressTable(data, tableEntry);
+                font.tables.head = head.parse(table.data, table.offset);
+                font.unitsPerEm = font.tables.head.unitsPerEm;
+                indexToLocFormat = font.tables.head.indexToLocFormat;
+                break;
+            case 'hhea':
+                table = uncompressTable(data, tableEntry);
+                font.tables.hhea = hhea.parse(table.data, table.offset);
+                font.ascender = font.tables.hhea.ascender;
+                font.descender = font.tables.hhea.descender;
+                font.numberOfHMetrics = font.tables.hhea.numberOfHMetrics;
+                break;
+            case 'hmtx':
+                hmtxTableEntry = tableEntry;
+                break;
+            case 'ltag':
+                table = uncompressTable(data, tableEntry);
+                ltagTable = ltag.parse(table.data, table.offset);
+                break;
+            case 'COLR':
+                table = uncompressTable(data, tableEntry);
+                font.tables.colr = colr.parse(table.data, table.offset);
+                break;
+            case 'CPAL':
+                table = uncompressTable(data, tableEntry);
+                font.tables.cpal = cpal.parse(table.data, table.offset);
+                break;
+            case 'maxp':
+                table = uncompressTable(data, tableEntry);
+                font.tables.maxp = maxp.parse(table.data, table.offset);
+                font.numGlyphs = font.tables.maxp.numGlyphs;
+                break;
+            case 'name':
+                nameTableEntry = tableEntry;
+                break;
+            case 'OS/2':
+                table = uncompressTable(data, tableEntry);
+                font.tables.os2 = os2.parse(table.data, table.offset);
+                break;
+            case 'post':
+                table = uncompressTable(data, tableEntry);
+                font.tables.post = post.parse(table.data, table.offset);
+                font.glyphNames = new GlyphNames(font.tables.post);
+                break;
+            case 'prep' :
+                table = uncompressTable(data, tableEntry);
+                p = new parse.Parser(table.data, table.offset);
+                font.tables.prep = p.parseByteList(tableEntry.length);
+                break;
+            case 'glyf':
+                glyfTableEntry = tableEntry;
+                break;
+            case 'loca':
+                locaTableEntry = tableEntry;
+                break;
+            case 'CFF ':
+                cffTableEntry = tableEntry;
+                break;
+            case 'kern':
+                kernTableEntry = tableEntry;
+                break;
+            case 'GDEF':
+                gdefTableEntry = tableEntry;
+                break;
+            case 'GPOS':
+                gposTableEntry = tableEntry;
+                break;
+            case 'GSUB':
+                gsubTableEntry = tableEntry;
+                break;
+            case 'meta':
+                metaTableEntry = tableEntry;
+                break;
+        }
+    }
+
+    var nameTable = uncompressTable(data, nameTableEntry);
+    font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
+    font.names = font.tables.name;
+
+    if (glyfTableEntry && locaTableEntry) {
+        var shortVersion = indexToLocFormat === 0;
+        var locaTable = uncompressTable(data, locaTableEntry);
+        var locaOffsets = loca.parse(locaTable.data, locaTable.offset, font.numGlyphs, shortVersion);
+        var glyfTable = uncompressTable(data, glyfTableEntry);
+        font.glyphs = glyf.parse(glyfTable.data, glyfTable.offset, locaOffsets, font, opt);
+    } else if (cffTableEntry) {
+        var cffTable = uncompressTable(data, cffTableEntry);
+        cff.parse(cffTable.data, cffTable.offset, font, opt);
+    } else {
+        throw new Error('Font doesn\'t contain TrueType or CFF outlines.');
+    }
+
+    var hmtxTable = uncompressTable(data, hmtxTableEntry);
+    hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
+    addGlyphNames(font, opt);
+
+    if (kernTableEntry) {
+        var kernTable = uncompressTable(data, kernTableEntry);
+        font.kerningPairs = kern.parse(kernTable.data, kernTable.offset);
+    } else {
+        font.kerningPairs = {};
+    }
+
+    if (gdefTableEntry) {
+        var gdefTable = uncompressTable(data, gdefTableEntry);
+        font.tables.gdef = gdef.parse(gdefTable.data, gdefTable.offset);
+    }
+
+    if (gposTableEntry) {
+        var gposTable = uncompressTable(data, gposTableEntry);
+        font.tables.gpos = gpos.parse(gposTable.data, gposTable.offset);
+        font.position.init();
+    }
+
+    if (gsubTableEntry) {
+        var gsubTable = uncompressTable(data, gsubTableEntry);
+        font.tables.gsub = gsub.parse(gsubTable.data, gsubTable.offset);
+    }
+
+    if (fvarTableEntry) {
+        var fvarTable = uncompressTable(data, fvarTableEntry);
+        font.tables.fvar = fvar.parse(fvarTable.data, fvarTable.offset, font.names);
+    }
+
+    if (metaTableEntry) {
+        var metaTable = uncompressTable(data, metaTableEntry);
+        font.tables.meta = meta.parse(metaTable.data, metaTable.offset);
+        font.metas = font.tables.meta;
+    }
+
+    return font;
+}
+
+/**
+ * Asynchronously load the font from a URL or a filesystem. When done, call the callback
+ * with two arguments `(err, font)`. The `err` will be null on success,
+ * the `font` is a Font object.
+ * We use the node.js callback convention so that
+ * opentype.js can integrate with frameworks like async.js.
+ * @alias opentype.load
+ * @param  {string} url - The URL of the font to load.
+ * @param  {Function} callback - The callback.
+ */
+function load(url, callback, opt) {
+    opt = (opt === undefined || opt === null) ?  {} : opt;
+    var isNode = typeof window === 'undefined';
+    var loadFn = isNode && !opt.isUrl ? loadFromFile : loadFromUrl;
+
+    return new Promise(function (resolve, reject) {
+        loadFn(url, function(err, arrayBuffer) {
+            if (err) {
+                if (callback) {
+                    return callback(err);
+                } else {
+                    reject(err);
+                }
+            }
+            var font;
+            try {
+                font = parseBuffer(arrayBuffer, opt);
+            } catch (e) {
+                if (callback) {
+                    return callback(e, null);
+                } else {
+                    reject(e);
+                }
+            }
+            if (callback) {
+                return callback(null, font);
+            } else {
+                resolve(font);
+            }
+        });
+    });
+}
+
+/**
+ * Synchronously load the font from a URL or file.
+ * When done, returns the font object or throws an error.
+ * @alias opentype.loadSync
+ * @param  {string} url - The URL of the font to load.
+ * @param  {Object} opt - opt.lowMemory
+ * @return {opentype.Font}
+ */
+function loadSync(url, opt) {
+    var fs = require('fs');
+    var buffer = fs.readFileSync(url);
+    return parseBuffer(nodeBufferToArrayBuffer(buffer), opt);
+}
+
+var opentype = /*#__PURE__*/Object.freeze({
+	__proto__: null,
+	Font: Font,
+	Glyph: Glyph,
+	Path: Path,
+	BoundingBox: BoundingBox,
+	_parse: parse,
+	parse: parseBuffer,
+	load: load,
+	loadSync: loadSync
+});
+
+export default opentype;
+export { BoundingBox, Font, Glyph, Path, parse as _parse, load, loadSync, parseBuffer as parse };

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 19
examples/jsm/libs/opentype.module.min.js


+ 1 - 1
examples/jsm/loaders/TTFLoader.js

@@ -2,7 +2,7 @@ import {
 	FileLoader,
 	Loader
 } from 'three';
-import { opentype } from '../libs/opentype.module.min.js';
+import opentype from '../libs/opentype.module.js';
 
 /**
  * Requires opentype.js to be included in the project.

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio