Skip to content

Instantly share code, notes, and snippets.

@smhanov
Last active March 30, 2025 18:19

Revisions

  1. smhanov revised this gist Aug 16, 2022. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions OpenType.ts
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,9 @@
    // To see this run, you first stub out these imports. Then put the file in a Uint8Array.
    // let slice = new Slice(array);
    // let font = new OTFFont(slice);
    // Then you can call methods like font.drawText(canvasContext, )
    //
    //
    import { ICanvasContext } from "./ICanvasContext"
    import { log as Log } from "./log"

  2. smhanov revised this gist Aug 16, 2022. 1 changed file with 2047 additions and 0 deletions.
    2,047 changes: 2,047 additions & 0 deletions OpenType.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2047 @@
    import { ICanvasContext } from "./ICanvasContext"
    import { log as Log } from "./log"

    const log = Log.create("OPENTYPE");

    function assert(condition: boolean, message = "Assertion failed.") {
    if (!condition) {
    console.log(new Error(message))
    throw new Error(message);
    }
    }

    // Represents a CFF instruction. They can have a opcode, plus
    // any number of floating point arguments. Some opcodes begin with 12. In this case
    // they are two bytes, with the high byte 0x0c.
    class OpCode {
    constructor(public code: number, public operands: number[]) { }
    toString() {
    let str: string;
    if (this.code > 255) {
    str = `code=12 ${this.code & 0xff}`
    } else {
    str = `${this.code}`
    }
    return str + " " + JSON.stringify(this.operands);
    }
    }

    // This slice lets us seek around a file and read various parts of it.
    class Slice {
    private pos = 0;
    constructor(private arr: Uint8Array, private start = 0, readonly length = arr.length - start) { }
    getUint(len: number) {
    let result = 0;
    while (len--) {
    result = result * 256 + this.arr[this.pos++ + this.start];
    }
    return result;
    }

    getInt16() {
    let result = this.getUint(2);
    if (result & 0x8000) {
    result -= (1 << 16);
    }
    return result;
    }

    getString(length: number, width: number = 1) {
    var result = "";
    for (var i = 0; i < length; i += width) {
    result += String.fromCharCode(this.getUint(width));
    }
    return result;
    }

    get2Dot14() {
    return this.getInt16() / (1 << 14);
    }

    seek(pos: number) {
    if (pos > this.length) {
    throw new Error("Seek to invalid location");
    }
    let ret = this.pos;
    this.pos = pos;
    return ret;
    }

    tell() {
    return this.pos;
    }

    slice(start: number, length = this.length - start) {
    return new Slice(this.arr, start + this.start, length);
    }

    indexSkip(start: number = this.tell()) {
    this.seek(start);
    const count = this.getUint(2);
    if (count > 0) {
    const offSize = this.getUint(1);
    this.seek(start + 3 + offSize * count);
    const elemOffset = this.getUint(offSize);
    this.seek(start + 3 + (count + 1) * offSize - 1 + elemOffset);
    }
    return start;
    }

    // CFF
    getIndexCount(start: number) {
    this.seek(start);
    let count = this.getUint(2);
    let offsetSize = this.getUint(1);
    if (offsetSize > 4) {
    throw new Error("Error: This is not an index; offsetSize=" + offsetSize);
    }
    return count;
    }

    /** Seek to the CFF index given at start, and then return a slice of that entry */
    indexSeek(start: number, n: number): Slice {
    this.seek(start);
    const count = this.getUint(2);
    if (n >= count) {
    throw new Error("Tried to read past end of index");
    }

    const offSize = this.getUint(1);
    this.seek(start + 3 + offSize * n);
    let elemOffset = this.getUint(offSize);
    //log("count=%s offSize=%s elemOffset=%s", count, offSize, elemOffset);
    let nextOffset = this.getUint(offSize);
    let offset = start + 3 + (count + 1) * offSize - 1 + elemOffset;
    return this.slice(offset, nextOffset - elemOffset);
    }

    eof() {
    return this.pos >= this.length;
    }

    getInstruction(type2 = false, stack: number[] = []): OpCode {
    let ret = new OpCode(0, stack);
    for (; ;) {
    if (this.eof()) {
    throw new Error("Unexpected end of charstring");
    }

    const b0 = this.getUint(1);
    //log("Read b0=%s", b0);

    if (b0 === 12) {
    ret.code = 0x0c00 | this.getUint(1);
    return ret;
    } else if (b0 < 28 || type2 && b0 >= 29 && b0 <= 31) {
    ret.code = b0;
    return ret;
    } else if (b0 === 30) {
    // real
    let str = "";
    outer:
    for (; ;) {
    let b = this.getUint(1);
    for (let i = 4; i >= 0; i -= 4) {
    let code = (b >> i) & 0x0f;
    switch (code) {
    case 0xa: str += "."; break;
    case 0xb: str += "E"; break;
    case 0xc: str += "E-"; break;
    case 0xe: str += "-"; break;
    case 0xf: break outer;
    case 0xe: /* reserved */ break;
    default:
    str += code.toString();
    }
    }
    }
    ret.operands.push(parseFloat(str));
    } else if (b0 === 29) {
    const b1 = this.getUint(1);
    const b2 = this.getUint(1);
    const b3 = this.getUint(1);
    const b4 = this.getUint(1);
    ret.operands.push((b1 << 24) | (b2 << 16) | (b3 << 8) | b4);
    } else if (b0 === 28) {
    const b1 = this.getUint(1);
    const b2 = this.getUint(1);
    ret.operands.push((b1 << 8) | b2);
    } else if (type2 && b0 === 255) {
    ret.operands.push(this.getUint(4) / 0x10000);
    } else if (b0 >= 251) {
    const b1 = this.getUint(1);
    ret.operands.push(-(b0 - 251) * 256 - b1 - 108);
    } else if (b0 >= 247) {
    const b1 = this.getUint(1);
    ret.operands.push((b0 - 247) * 256 + b1 + 108);
    } else {
    ret.operands.push(b0 - 139);
    }
    }
    }

    }

    // ------------------------------------------------------------------------------
    // This is a tokenizer, part of the byte parsing. I wanted to have something
    // where I can just write out the font structures and have code automatically
    // read them in. This is the tokenizer for the mini-language I created for this
    // purpose.
    //
    // The decode function takes the structure description, and a Slice, and
    // parses the data into a JSON structure.
    class Tokens {
    private tokens: string[] = []
    n = 0
    constructor(input: string) {
    const regex = /([:\[\]\(\)\-\.{}]|\d+)|\s+/;
    for (let item of input.split(regex)) {
    if (item && item.length) {
    this.tokens.push(item);
    }
    }
    }

    next() {
    return this.tokens[this.n++];
    }

    peek() {
    return this.tokens[this.n];
    }

    match(tok: string) {
    if (this.tokens[this.n] === tok) {
    this.n += 1;
    return true;
    }
    return false;
    }

    value(context: any) {
    let value = this.next();
    let num = parseFloat(value);
    if (isNaN(num)) {
    return context[value];
    }
    return num;
    }

    fail(oldPos: number) {
    this.n = oldPos;
    return false;
    }
    }

    function parseData(tokens: Tokens, input: Slice, obj: any, skipping: boolean) {
    /** data = '{' namedValue* '}' | value */
    if (tokens.match("{")) {
    obj = {};
    while (parseNamedValue(tokens, input, obj, skipping)) { }
    tokens.match("}");
    return obj;
    }
    return parseValue(tokens, input, obj, skipping);
    }

    function parseNamedValue(tokens: Tokens, input: Slice, obj: any, skipping: boolean) {
    // namedValue = 'push' expr namedValue* pop | name ':' value
    const at = tokens.n;
    if (tokens.match("push")) {
    const offset = parseExpr(tokens, obj);
    input = input.slice(offset);
    while (parseNamedValue(tokens, input, obj, skipping)) { }
    if (!tokens.match("pop")) {
    return false
    }
    return true;
    }

    let name = tokens.next();
    if (tokens.match(":")) {
    const value = parseValue(tokens, input, obj, skipping);
    if (name !== '_') {
    obj[name] = value;
    }
    return true;
    }
    return tokens.fail(at);
    }

    function parseValue(tokens: Tokens, input: Slice, obj: any, skipping: boolean) {
    const at = tokens.n;
    // value = ("[" expr "]")? type
    if (tokens.match("[")) {
    let count = parseExpr(tokens, obj);
    let tag = "";
    let ret: any = [];
    if (tokens.match('.')) {
    tag = tokens.next();
    ret = {};
    }
    if (!tokens.match("]")) {
    return tokens.fail(at);
    }
    const start = tokens.n;
    for (let i = 0; i < count; i++) {
    tokens.n = start;
    let result = parseData(tokens, input, obj, skipping);
    if (tag === "") {
    ret.push(result);
    } else {
    ret[result[tag]] = result;
    }
    }
    if (count === 0) { // skip over description
    parseData(tokens, input, obj, true);
    }
    return ret;
    }

    if (tokens.match("(")) {
    let num = parseExpr(tokens, obj);
    tokens.match(")");
    return num;
    } else if (tokens.match("uint")) {
    let num = tokens.value(obj) / 8;
    return skipping ? 0 : input.getUint(num);
    } else if (tokens.match("int")) {
    tokens.next(); // skip '16'
    return skipping ? 0 : input.getInt16();
    } else if (tokens.match("offset")) {
    return input.tell();
    } else if (tokens.match("date")) {
    const macTime = input.getUint(4) * 0x100000000 + input.getUint(4);
    const utcTime = macTime * 1000 + Date.UTC(1904, 1, 1);
    return new Date(utcTime);
    } else if (tokens.match("fixed")) {
    return skipping ? 0 : input.getUint(4) / (1 << 16);
    } else if (tokens.match("string")) {
    if (!tokens.match("(")) {
    return tokens.fail(at);
    }
    const length = parseExpr(tokens, obj);
    if (!tokens.match(")")) {
    return tokens.fail(at);
    }
    return skipping ? 0 : input.getString(length);
    }
    return tokens.fail(at);
    }

    function parseExpr(tokens: Tokens, obj: any) {
    // expr = num - num | num
    let value = tokens.value(obj);
    if (tokens.match("-")) {
    value -= tokens.value(obj);
    } else if (tokens.match(":")) { // item:bit#
    const bit = tokens.value(obj);
    value = (value >> bit) & 1;
    }
    return value;
    }

    // This is the magic function that takes a structure description and
    // parses it into JSON.
    function decode(input: Slice, spec: string): any {
    return parseData(new Tokens(spec), input, null, false);
    }

    const TTC_HEADER = `{
    ttcTag:string(4)
    majorVersion:uint16
    minorVersion:uint16
    numFonts:uint32
    offsetTable:[numFonts] uint32
    }`

    const OFFSET_TABLES = `{
    scalarType:uint32
    numTables:uint16
    searchRange:uint16
    entrySelector:uint16
    rangeShift:uint16
    tables:[numTables.tag] {
    tag:string(4)
    checksum:uint32
    offset:uint32
    length:uint32
    }
    }`

    const HEAD_TABLE = `{
    version:fixed
    fontRevision:fixed
    checksumAdjustment:uint32
    magicNumber:uint32
    flags:uint16
    unitsPerEm:uint16
    created:date
    modified:date
    xMin:int16
    yMmin:int16
    xMax:int16
    yMax:int16
    macStyle:uint16
    lowestRectPPEM:uint16
    fontDirectionHint:uint16
    indexToLocFormat:uint16
    glyphDataFormat:uint16
    bold:(macStyle:0)
    italic:(macStyle:1)
    }`

    const CMAP_TABLE = `{
    offset:offset
    version:uint16
    numberSubtables:uint16
    subTables:[numberSubtables] {
    platformID:uint16
    platformSpecificID:uint16
    offset:uint32
    }
    }`

    const NAME_TABLE = `{
    offset:offset
    format:uint16
    count:uint16
    stringOffset:uint16
    names:[count] {
    platformID:uint16
    platformSpecificID:uint16
    languageID:uint16
    nameID:uint16
    length:uint16
    offset:uint16
    }
    }`

    const HHEA_TABLE = `{
    version:fixed
    ascent:int16
    descent:int16
    lineGap:int16
    advanceWidthMax:uint16
    minLeftSideBearing:int16
    minRightSideBearing:int16
    xMaxExtent:int16
    caretSlopeRise:int16
    caretSlopeRun:uint16
    caretOffset:int16
    reserved:uint64
    metricDataFormat:int16
    numOfLongHorMetrics:uint16
    }`

    const OS2_TABLE = `{
    version:uint16
    xAvgCharWidth:int16
    usWeightClass:uint16
    usWidthClass:uint16
    }`

    const KERN_TABLE = `{
    version:uint16
    nTables:uint16
    tables:[nTables] {
    version:uint16
    length:uint16
    coverage:uint16
    offset:offset
    _:[length-6] uint8
    }
    }`

    const MAXP_TABLE = `{
    version:fixed
    numGlyphs:uint16
    }`

    const CFF_TABLE = `{
    offset:offset
    major:uint8
    minor:uint8
    }`

    // A CMAP is any class that maps a unicode codepoint to a glyph index.
    interface CMap {
    map(charCode: number): number; // or -1
    }

    interface Point {
    x: number;
    y: number;
    onCurve: boolean;
    }

    interface Glyph {
    contourEnds: number[];
    numberOfContours: number;
    points: Point[];
    xMin: number;
    xMax: number;
    yMin: number;
    yMax: number;
    }

    // In OpenType, a font file can contain many different fonts. Opening with the
    // Font Collection will let you access them.
    export class FontCollection {
    private fonts: OTFFont[] = []

    async add(url: string) {
    let response = await fetch(url);
    let buffer = await response.arrayBuffer();
    return this.openSync(new Uint8Array(buffer));
    }

    removeAll() {
    this.fonts.length = 0;
    }

    get(name: string): OTFFont | null {
    for (let font of this.fonts) {
    if (font.fullName === name || font.fontFamily === name) {
    return font;
    }
    }
    return null;
    }

    openSync(data: Uint8Array) {
    const f = new Slice(data);
    const magic = f.getUint(4);
    f.seek(0);
    let fonts: OTFFont[]
    if (magic === 0x74746366) { // ttcf
    // it's a font collection
    fonts = this.addOTCFile(f);
    } else { // it's a font file.
    fonts = [new OTFFont(f)];
    }

    outer:
    for (let font of fonts) {
    for (let have of this.fonts) {
    if (have.fullName === font.fullName) {
    log("Not adding %s; already have it.");
    continue outer;
    }
    }
    log("Opened font: %s weight=%s italic=%s", font.fullName, font.weight, font.italic);
    console.log(font);
    this.fonts.push(font);
    }

    return fonts;
    }

    private addOTCFile(f: Slice) {
    const header = decode(f, TTC_HEADER);
    log(JSON.stringify(header, null, 2));
    log("Collection contains %s fonts", header["numFonts"]);
    const ret = [];
    for (let offset of header["offsetTable"]) {
    ret.push(new OTFFont(f, offset))
    }
    return ret;
    }
    }

    export class OTFFont {
    private offsetTables: any;
    private tables: { [name: string]: any } = {}
    fontFamily = "";
    fontSubFamily = "";
    fullName = "";
    postscriptName = "";
    weight = 0;
    italic = false;
    bold = false;
    private cmaps: CMap[] = []
    private cff: CFFString | null = null;
    private kerners: KernAdjuster[] = [];
    constructor(private f: Slice, offset = 0) {
    f.seek(offset);
    this.offsetTables = decode(f, OFFSET_TABLES);
    log(JSON.stringify(this.offsetTables, null, 2));
    for (let name in this.offsetTables["tables"]) {
    log("Table: %s", name);
    }
    //console.log("offset", JSON.stringify(this.offsetTables, null, 2));
    this.decodeTable("head", HEAD_TABLE);
    this.decodeTable("name", NAME_TABLE);
    this.decodeTable("cmap", CMAP_TABLE);
    this.decodeTable("hhea", HHEA_TABLE);
    this.decodeTable("kern", KERN_TABLE);
    this.decodeTable("maxp", MAXP_TABLE);
    this.decodeTable("OS/2", OS2_TABLE);
    this.decodeTable("CFF ", CFF_TABLE);

    this.parseNameTable(f, this.tables["name"]);
    this.parseCmap(f, this.tables["cmap"]);
    this.parseGPOS(f);
    this.parseKern(f, this.tables["kern"]);
    this.italic = !!this.tables["head"]["italic"];
    this.bold = !!this.tables["head"]["bold"];
    this.weight = this.tables["OS/2"]["usWeightClass"];

    if ("CFF " in this.tables) {
    this.cff = new CFFString(f.slice(this.tables["CFF "]["offset"]));
    }

    }

    private decodeTable(name: string, spec: string) {
    if (name in this.offsetTables["tables"]) {
    log("Decoding %s", name);
    let offset = this.offsetTables["tables"][name]["offset"];
    this.f.seek(offset);
    this.tables[name] = decode(this.f, spec);
    log(`${name} table:`, JSON.stringify(this.tables[name], null, 2));
    }
    }

    private parseNameTable(f: Slice, nameTable: any) {
    for (let item of nameTable["names"]) {
    let name: string
    f.seek(nameTable["offset"] + nameTable["stringOffset"] + item["offset"]);

    if (item["platformID"] === 0 || item["platformID"] === 3) {
    name = f.getString(item["length"], 2); // UCS-2
    } else {
    name = f.getString(item["length"]); // ASCII
    }

    switch (item["nameID"]) {
    case 1: this.fontFamily = name; break;
    case 2: this.fontSubFamily = name; break;
    case 4: this.fullName = name; break;
    case 6: this.postscriptName = name; break;
    }
    }
    }

    private parseCmap(f: Slice, cmapTable: any) {
    for (let cmap of cmapTable["subTables"]) {
    let platformID = cmap["platformID"];
    let platformSpecificID = cmap["platformSpecificID"];
    let offset = cmapTable["offset"] + cmap["offset"];
    f.seek(offset);
    const format = f.getUint(2);
    const length = f.getUint(2);
    log(`CMAP platform ${platformID} ${platformSpecificID} format ${format} length ${length} at ${offset}`)
    switch (format) {
    case 0:
    this.cmaps.push(new CMap0(f.slice(offset, length)));
    break;
    case 4:
    this.cmaps.push(new CMap4(f.slice(offset, length)));
    break;
    }
    }
    }

    private parseGPOS(f: Slice) {
    let table = this.offsetTables["tables"]["GPOS"];
    if (table) {
    this.kerners.push(new GPOS(f.slice(table["offset"], table["length"])));
    }
    }

    private parseKern(f: Slice, kernTable: any) {
    if (!kernTable) return;
    for (let table of kernTable["tables"]) {
    if ((table["coverage"] >> 8) === 0) {
    log("Format 0 kern table detected");
    this.kerners.push(new Kern0(f.slice(table["offset"], table["length"])))
    }
    }
    }

    getGlyphCount() {
    return this.tables["maxp"]["numGlyphs"];
    }

    drawSingleGlyph(ctx: ICanvasContext, glyphIndex: number,
    x: number, y: number, size: number) {
    ctx.save();
    ctx.translate(x, y);
    this.transform(ctx, size);
    this.drawGlyph(ctx, glyphIndex, 0, 0);
    ctx.restore();
    }

    transform(ctx: ICanvasContext, size: number) {
    let scale = this.getScale(size);
    ctx.scale(scale, -scale);
    }

    private drawGlyph(ctx: ICanvasContext, index: number, x: number, y: number) {
    if (this.cff) {
    this.cff.drawGlyph(ctx, index, x, y);
    return;
    }
    var glyph = this.readGlyph(index);
    //log("Draw GLyph index %s", index);

    if (glyph === null) {
    return false;
    }

    var s = 0,
    p = 0,
    c = 0,
    contourStart = 0,
    prev;

    for (; p < glyph.points.length; p++) {
    var point = glyph.points[p];
    if (s === 0) {
    ctx.moveTo(point.x + x, point.y + y);
    s = 1;
    } else if (s === 1) {
    if (point.onCurve) {
    ctx.lineTo(point.x + x, point.y + y);
    } else {
    s = 2;
    }
    } else {
    prev = glyph.points[p - 1];
    if (point.onCurve) {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    point.x + x, point.y + y);
    s = 1;
    } else {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    (prev.x + point.x) / 2 + x,
    (prev.y + point.y) / 2 + y);
    }
    }

    if (p === glyph.contourEnds[c]) {
    if (s === 2) { // final point was off-curve. connect to start
    prev = point;
    point = glyph.points[contourStart];
    if (point.onCurve) {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    point.x + x, point.y + y);
    } else {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    (prev.x + point.x) / 2 + x,
    (prev.y + point.y) / 2 + y);
    }
    }
    contourStart = p + 1;
    c += 1;
    s = 0;
    }
    }

    return true;
    }

    private readGlyph(index: number): Glyph | null {
    var offset = this.getGlyphOffset(index);
    var file = this.f;
    let table = this.offsetTables["tables"]["glyf"];

    if (offset === 0 ||
    offset >= table["offset"] + table["length"]) {
    return null;
    }

    assert(offset >= table["offset"]);
    assert(offset < table["offset"] + table["length"]);

    file.seek(offset);

    var glyph = {
    contourEnds: [],
    numberOfContours: file.getInt16(),
    points: [],
    xMin: file.getInt16(),
    yMin: file.getInt16(),
    xMax: file.getInt16(),
    yMax: file.getInt16()
    };

    assert(glyph.numberOfContours >= -1);

    if (glyph.numberOfContours === -1) {
    this.readCompoundGlyph(file, glyph);
    } else {
    this.readSimpleGlyph(file, glyph);
    }

    return glyph;
    }


    private getGlyphOffset(index: number) {
    assert("loca" in this.offsetTables["tables"]);
    var table = this.offsetTables["tables"]["loca"];
    var file = this.f;
    var offset, old, next;

    if (this.tables["head"]["indexToLocFormat"] === 1) {
    old = file.seek(table["offset"] + index * 4);
    offset = file.getUint(4);
    next = file.getUint(4);
    } else {
    old = file.seek(table["offset"] + index * 2);
    offset = file.getUint(2) * 2;
    next = file.getUint(2) * 2;
    }

    file.seek(old);
    if (offset === next) {
    // indicates glyph has no outline( eg space)
    return 0;
    }

    //log("Offset for glyph index %s is %s", index, offset);

    return offset + this.offsetTables["tables"]["glyf"].offset;
    }

    private readCompoundGlyph(file: Slice, glyph: Glyph) {
    var ARG_1_AND_2_ARE_WORDS = 1,
    ARGS_ARE_XY_VALUES = 2,
    //ROUND_XY_TO_GRID = 4,
    WE_HAVE_A_SCALE = 8,
    // RESERVED = 16
    MORE_COMPONENTS = 32,
    WE_HAVE_AN_X_AND_Y_SCALE = 64,
    WE_HAVE_A_TWO_BY_TWO = 128,
    WE_HAVE_INSTRUCTIONS = 256;
    //USE_MY_METRICS = 512,
    //OVERLAP_COMPONENT = 1024;

    var flags = MORE_COMPONENTS;
    var component;
    glyph.contourEnds = [];
    glyph.points = [];
    while (flags & MORE_COMPONENTS) {
    var arg1, arg2;

    flags = file.getUint(2);
    component = {
    glyphIndex: file.getUint(2),
    matrix: {
    a: 1, b: 0, c: 0, d: 1, e: 0, f: 0
    },

    destPointIndex: 0,
    srcPointIndex: 0
    };

    if (flags & ARG_1_AND_2_ARE_WORDS) {
    arg1 = file.getInt16();
    arg2 = file.getInt16();
    } else {
    arg1 = file.getUint(1);
    arg2 = file.getUint(1);
    }

    if (flags & ARGS_ARE_XY_VALUES) {
    component.matrix.e = arg1;
    component.matrix.f = arg2;
    } else {
    component.destPointIndex = arg1;
    component.srcPointIndex = arg2;
    }

    if (flags & WE_HAVE_A_SCALE) {
    component.matrix.a = file.get2Dot14();
    component.matrix.d = component.matrix.a;
    } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
    component.matrix.a = file.get2Dot14();
    component.matrix.d = file.get2Dot14();
    } else if (flags & WE_HAVE_A_TWO_BY_TWO) {
    component.matrix.a = file.get2Dot14();
    component.matrix.b = file.get2Dot14();
    component.matrix.c = file.get2Dot14();
    component.matrix.d = file.get2Dot14();
    }

    //log("Read component glyph index %s", component.glyphIndex);
    //log("Transform: [%s %s %s %s %s %s]", component.matrix.a, component.matrix.b,
    // component.matrix.c, component.matrix.d, component.matrix.e, component.matrix.f);
    var old = file.tell();
    var simpleGlyph = this.readGlyph(component.glyphIndex);
    if (simpleGlyph) {
    var pointOffset = glyph.points.length;
    for (var i = 0; i < simpleGlyph.contourEnds.length; i++) {
    glyph.contourEnds.push(simpleGlyph.contourEnds[i] +
    pointOffset);
    }
    for (i = 0; i < simpleGlyph.points.length; i++) {
    var x = simpleGlyph.points[i].x;
    var y = simpleGlyph.points[i].y;
    x = component.matrix.a * x + component.matrix.b * y +
    component.matrix.e;
    y = component.matrix.c * x + component.matrix.d * y +
    component.matrix.f;
    glyph.points.push({
    x: x, y: y, onCurve:
    simpleGlyph.points[i].onCurve
    });
    }
    }

    file.seek(old);
    }

    glyph.numberOfContours = glyph.contourEnds.length;

    if (flags & WE_HAVE_INSTRUCTIONS) {
    file.seek(file.getUint(2) + file.tell());
    }
    }

    private readSimpleGlyph(file: Slice, glyph: Glyph) {

    var ON_CURVE = 1,
    X_IS_BYTE = 2,
    Y_IS_BYTE = 4,
    REPEAT = 8,
    X_DELTA = 16,
    Y_DELTA = 32;

    glyph.contourEnds = [];
    var points: Point[] = glyph.points = [];

    for (var i = 0; i < glyph.numberOfContours; i++) {
    glyph.contourEnds.push(file.getUint(2));
    }

    // skip over intructions
    file.seek(file.getUint(2) + file.tell());

    if (glyph.numberOfContours === 0) {
    return;
    }

    var numPoints = Math.max.apply(null, glyph.contourEnds) + 1;

    var flags: number[] = [];

    for (i = 0; i < numPoints; i++) {
    var flag = file.getUint(1);
    flags.push(flag);
    points.push({
    x: 0,
    y: 0,
    onCurve: (flag & ON_CURVE) > 0
    });

    if (flag & REPEAT) {
    var repeatCount = file.getUint(1);
    assert(repeatCount > 0);
    i += repeatCount;
    while (repeatCount--) {
    flags.push(flag);
    points.push({
    x: 0,
    y: 0,
    onCurve: (flag & ON_CURVE) > 0
    });
    }
    }
    }

    function readCoords(name: "x" | "y", byteFlag: number,
    deltaFlag: number) {
    var value = 0;

    for (var i = 0; i < numPoints; i++) {
    var flag = flags[i];
    if (flag & byteFlag) {
    if (flag & deltaFlag) {
    value += file.getUint(1);
    } else {
    value -= file.getUint(1);
    }
    } else if (~flag & deltaFlag) {
    value += file.getInt16();
    } else {
    // value is unchanged.
    }

    points[i][name] = value;
    }
    }

    readCoords("x", X_IS_BYTE, X_DELTA);
    readCoords("y", Y_IS_BYTE, Y_DELTA);
    }

    getScale(fontHeight: number) {
    return fontHeight / this.tables["head"]["unitsPerEm"];
    }

    getHorizontalMetrics(glyphIndex: number) {
    //console.log("Offset tables: ", this.offsetTables["tables"])
    assert("hmtx" in this.offsetTables["tables"]);
    var file = this.f;
    var offset = this.offsetTables["tables"]["hmtx"].offset;
    var old = file.seek(offset + 4);
    let advanceWidth, leftSideBearing;
    let numOfLongHorMetrics = this.tables["hhea"]["numOfLongHorMetrics"];
    if (glyphIndex < numOfLongHorMetrics) {
    offset += glyphIndex * 4;
    old = this.f.seek(offset);
    advanceWidth = file.getUint(2);
    leftSideBearing = file.getInt16();
    } else {
    // read the last entry of the hMetrics array
    old = file.seek(offset + (numOfLongHorMetrics - 1) * 4);
    advanceWidth = file.getUint(2);
    file.seek(offset + numOfLongHorMetrics * 4 +
    2 * (glyphIndex - numOfLongHorMetrics));
    leftSideBearing = file.getInt16();
    }

    file.seek(old);
    return {
    advanceWidth: advanceWidth,
    leftSideBearing: leftSideBearing
    };
    }

    getGlyphIndex(charCode: number) {
    var index = 0;
    // start at the last CMAP first, which is typically the full featured one that supports unicode.
    for (var i = this.cmaps.length - 1; i >= 0; i--) {
    var cmap = this.cmaps[i];
    index = cmap.map(charCode);
    if (index) {
    break;
    }
    }
    return index;
    }

    drawText(ctx: ICanvasContext, text: string, x: number, y: number,
    size: number) {
    ctx.save();
    ctx.translate(x, y);
    this.transform(ctx, size);
    let glyphs: KernAdjustment[] = [];
    for (var i = 0; i < text.length; i++) {
    glyphs.push(new KernAdjustment(this.getGlyphIndex(text.charCodeAt(i))));
    }

    for (let kerner of this.kerners) {
    kerner.kern(glyphs);
    }

    x = 0;
    y = 0;
    for (let i = 0; i < glyphs.length; i++) {
    let glyph = glyphs[i];
    var metrics = this.getHorizontalMetrics(glyph.glyph);
    log("Metrics for %s code %s index %s: %s %s", text.charAt(i),
    text.charCodeAt(i), glyph.glyph, metrics.advanceWidth, metrics.leftSideBearing);
    this.drawGlyph(ctx, glyph.glyph, x + glyph.placement.x, y + glyph.placement.y);
    x += metrics.advanceWidth + glyph.advance.x;
    y += glyph.advance.y;
    }
    ctx.restore();
    }
    }

    // Implements CMAP0 format, mapping unicode to a glyph index using an array.
    class CMap0 implements CMap {
    constructor(private f: Slice) { }

    map(charCode: number) {
    if (charCode >= 0 && charCode <= 255) {
    this.f.seek(charCode + 6);
    log("charCode %s maps to %s", charCode, this.f.getUint(1));
    this.f.seek(charCode + 6);
    return this.f.getUint(1);
    }
    return 0;
    }
    }

    // Implements CMAP4 format, mapping unicode to a glyph index using a list of
    // numerical ranges.
    class CMap4 implements CMap {
    private segCount: number;
    constructor(private f: Slice) {
    f.seek(6);
    this.segCount = f.getUint(2) / 2;
    }

    map(charCode: number) {
    log("Try to map charcode %s using %s ranges", charCode, this.segCount);
    let f = this.f, start = 0, result = 0;
    let i = bsearch(this.segCount - 1, (i) => {
    f.seek(14 + i * 2);
    let end = f.getUint(2);
    f.seek(14 + this.segCount * 2 + 2 + i * 2);
    start = f.getUint(2);
    if (charCode < start) {
    return -1;
    } else if (charCode > end) {
    return 1;
    }
    return 0;
    })

    if (i >= 0) {
    f.seek(14 + this.segCount * 3 * 2 + 2 + i * 2);
    const rangeOffset = f.getUint(2);
    f.seek(14 + this.segCount * 2 * 2 + 2 + i * 2);
    const delta = f.getInt16();
    if (rangeOffset === 0) {
    result = (delta + charCode) & 0xffff;
    } else {
    let ptr = 14 + this.segCount * 3 * 2 + 2 + i * 2;
    ptr += (charCode - start) * 2 + rangeOffset;
    f.seek(ptr);
    result = f.getUint(2);
    if (result !== 0) {
    result = (result + delta) & 0xffff;
    }
    }
    }
    return result;
    }
    }

    /** @param cmp returns target - i */
    function bsearch(count: number, cmp: (i: number) => number): number {
    let high = count, low = -1, probe: number;

    while (high - low > 1) {
    probe = (high + low) >> 1;

    const match = cmp(probe);

    if (match == 0) {
    return probe;
    } else if (match > 0) {
    low = probe;
    } else {
    high = probe;
    }
    }
    return -1;
    }

    /**
    CFF string is a collection of machine code instructions to render a font. They
    can call into subroutines marked "local" or "global". This reads the headers
    containing the description of the font, all of the instructions. It implements
    the drawGlyph function for CFF fonts.
    */
    class CFFString {
    dicts: number;
    strings: number; // string index
    subs: number;
    topDict: any;
    numGlyphs: number;
    fdSelect: FDSelect | null = null;
    fdDicts: any[] = [];

    constructor(private f: Slice) {
    const major = f.getUint(1);
    const minor = f.getUint(1);
    const hdrSize = f.getUint(1);
    log(`CFF Version ${major}.${minor}`);
    f.seek(hdrSize);
    f.indexSkip(); // name index
    this.dicts = f.indexSkip(); // font dicts
    this.strings = f.indexSkip(); // string index
    this.subs = f.tell();
    let topDict = this.topDict = this.readDict(f.indexSeek(this.dicts, 0));
    this.numGlyphs = f.getIndexCount(topDict["CharStrings"]);
    log(`CFF Font contains ${this.numGlyphs} glyphs`);
    if (this.topDict["FDSelect"]) {
    this.fdSelect = new FDSelect(f.slice(topDict["FDSelect"]));
    this.readFDArray(f, topDict);
    } else {
    this.fdDicts.push(topDict);
    }

    }

    readDict(f: Slice) {
    let dict = ReadCFFDict(f, {}, this);
    if (dict["Private"]) {
    let length = dict["Private"][0];
    let offset = dict["Private"][1];
    ReadCFFDict(this.f.slice(offset, length), dict, this);
    if (dict["Subrs"]) {
    dict["Subrs"] += offset;
    }
    }
    return dict;
    }

    getString(n: number) {
    if (n < STDSTRINGS.length) {
    return STDSTRINGS[n];
    }
    const slice = this.f.indexSeek(this.strings, n - STDSTRINGS.length);
    return slice.getString(slice.length);
    }

    readFDArray(f: Slice, topDict: any) {
    let fdindex = topDict["FDArray"];
    let count = f.getIndexCount(fdindex);
    log("There are %s items in FDArray", count);
    for (let i = 0; i < count; i++) {
    this.fdDicts.push(this.readDict(f.indexSeek(fdindex, i)));
    }
    }

    /** @param subNumber Is the unbiased number of the subroutine
    * @param fontIndex is -1 for a global sub, or the font glyphs FD index
    * for a local sub.
    */
    getSubr(subNumber: number, fontIndex: number = -1): Slice {
    let subIndex: number;
    let charStringType: number;
    if (fontIndex === -1) {
    subIndex = this.subs;
    charStringType = this.topDict["CharstringType"];
    } else {
    let dict = this.fdDicts[fontIndex];
    subIndex = dict["Subrs"];
    charStringType = dict["CharstringType"];
    }

    let count = this.f.getIndexCount(subIndex);
    subNumber += GetBias(charStringType, count);
    //log("Subindex for font %s has %s entries. Call sub# %s", fontIndex, count, subNumber);
    return this.f.indexSeek(subIndex, subNumber);
    }

    getFontNumber(glyph: number) {
    if (this.fdSelect) {
    return this.fdSelect.lookup(glyph);
    }
    return 0; // top dict in 0th entry of fdDicts
    }

    drawGlyph(ctx: ICanvasContext, glyphIndex: number, x: number, y: number) {
    ctx.translate(x, y);
    let index = this.topDict["CharStrings"];
    let fontNumber = this.getFontNumber(glyphIndex);
    DrawCFFGlyph(this.f.indexSeek(index, glyphIndex), ctx, fontNumber, this);
    ctx.translate(-x, -y);
    }
    }

    type OpType = number;
    const SID = 0;
    const BOOLEAN = 1;
    const NUMBER = 2;
    const ARRAY = 3;

    interface Instruction {
    type: OpType[];
    name: string;
    def: any;
    fn: (dict: any, args: number[]) => void;
    }

    const DICT_INSTRUCTIONS: { [code: number]: Instruction } = {};

    function assign(name: string, code: number, typeIn: OpType | OpType[], def?: any) {
    const array = typeIn instanceof Array;
    DICT_INSTRUCTIONS[code] = {
    name: name,
    def: def,
    type: (array ? typeIn : [typeIn]) as OpType[],
    fn: (dict: any, args: any[]) => {
    //console.log(`Assign ${name}=${JSON.stringify(args)}`);
    dict[name] = array || typeIn === ARRAY ? args : args[0];
    }
    }
    };

    assign("version", 0, SID);
    assign("Notice", 1, SID);
    assign("Copyright", 0xC | 0, SID);
    assign("FullName", 2, SID);
    assign("FamilyName", 3, SID);
    assign("Weight", 4, SID);
    assign("isFixedPitch", 0xc00 | 1, BOOLEAN, false);
    assign("ItalicAngle", 0xc00 | 2, NUMBER, 0);
    assign("UnderlinePosition", 0xc00 | 3, NUMBER, -100);
    assign("UnderlineThickness", 0xc00 | 4, NUMBER, 50);
    assign("PaintType", 0xc00 | 5, NUMBER, 0);
    assign("CharstringType", 0xc00 | 6, NUMBER, 2);
    assign("FontMatrix", 0xc00 | 7, ARRAY, [0.001, 0, 0, 0.001, 0, 0]);
    assign("UniqueID", 13, NUMBER);
    assign("FontBBox", 5, ARRAY, [0, 0, 0, 0]);
    assign("StrokeWidth", 0xc00 | 8, NUMBER, 0);
    assign("XUID", 14, ARRAY);
    assign("charset", 15, NUMBER, 0);
    assign("Encoding", 16, NUMBER, 0)
    assign("CharStrings", 17, NUMBER, 0);
    assign("Private", 18, [NUMBER, NUMBER]);
    assign("SyntheticBase", 0xc00 | 20, NUMBER);
    assign("PostSCript", 0xc00 | 21, SID);
    assign("BaseFontName", 0xc00 | 22, SID);
    assign("BaseFontBlend", 0xc00 | 23, SID);
    assign("ROS", 0xc00 | 30, [SID, SID]);
    assign("CIDFontVersion", 0xc00 | 31, NUMBER, 0);
    assign("CIDFontRevision", 0xc00 | 32, NUMBER, 0);
    assign("CIDFontType", 0xc00 | 33, NUMBER, 0);
    assign("CIDCount", 0xc00 | 34, NUMBER);
    assign("FDArray", 0xc00 | 36, NUMBER);
    assign("FDSelect", 0xc00 | 37, NUMBER);
    assign("FontName", 0xc00 | 38, SID);

    // private
    assign("BlueValues", 6, NUMBER);
    assign("OtherBlues", 7, NUMBER);
    assign("FamilyBlues", 8, NUMBER);
    assign("FamilyOtherBlues", 9, NUMBER);
    assign("BlueScale", 0xc09, NUMBER, 0.039625);
    assign("BlueShift", 0xc0a, 8);
    assign("BlueFuzz", 0xc01, 1);
    assign("StdHW", 10, NUMBER);
    assign("StdVW", 11, NUMBER);
    assign("StemSnapH", 0xc0c, NUMBER);
    assign("StemSnapV", 0xc0d, NUMBER);
    assign("ForceBold", 0xc0e, BOOLEAN, false);
    assign("LanguageGroup", 0xc11, NUMBER, 0);
    assign("ExpansionFactor", 0xc12, NUMBER, 0.6);
    assign("initialRandomSeed", 0xc13, NUMBER, 0);
    assign("Subrs", 19, NUMBER);
    assign("defaultWidthX", 20, NUMBER);
    assign("NominalWidthX", 21, NUMBER);

    interface StringGetter {
    getString(n: number): string;
    }

    function ReadCFFDict(f: Slice, dict: any, strings: StringGetter) {
    while (!f.eof()) {
    const op = f.getInstruction();
    if (op.code in DICT_INSTRUCTIONS) {
    let instr = DICT_INSTRUCTIONS[op.code];
    let args: any[] = [];
    outer:
    for (let i = 0; i < instr.type.length; i++) {
    let type = instr.type[i];
    if (i >= op.operands.length) {
    throw new Error("Not enough operands for instruction:" + op);
    }
    let arg = op.operands[i];
    switch (type) {
    case SID:
    args.push(strings.getString(arg));
    break;
    case BOOLEAN:
    args.push(!!arg);
    break;
    case ARRAY:
    args = op.operands;
    break outer;
    case NUMBER:
    args.push(arg);
    break;
    }
    }
    instr.fn(dict, args);
    } else {
    log("Uknown dict opcode: %s", op.code);
    }
    }
    for (let key in DICT_INSTRUCTIONS) {
    let instr = DICT_INSTRUCTIONS[key];
    if (instr.def && !(instr.name in dict)) {
    dict[instr.name] = instr.def;
    }
    }
    return dict;
    }

    interface SubGetter {
    getSubr(subIndex: number, fontNumber: number): Slice;
    }

    interface MachineState {
    first: boolean; // has first stack-clearing instruction been executed?

    width: number;
    numHints: number;
    stack: number[];
    x: number;
    y: number;
    started: boolean;
    startX: number;
    startY: number;
    }

    /*
    let DRAWCODES: { [key: number]: string } = {
    7: "vlineto",
    10: "callsubr",
    18: "hstemhm",
    19: "hintmask",
    21: "rmoveto",
    26: "vvcurveto",
    5: "rlineto",
    30: "vhcurveto",
    31: "hcurveto",
    14: "endchar",
    }
    */

    function DrawCFFGlyph(f: Slice, ctx: ICanvasContext, fontNumber: number, subs: SubGetter, s?: MachineState) {
    s = s || {
    first: true,
    width: 0,
    numHints: 0,
    stack: [],
    x: 0,
    y: 0,
    started: false,
    startX: 0,
    startY: 0,
    };

    while (!f.eof()) {
    let op = f.getInstruction(true, s.stack);
    //log("Exec opcode %s [%s] %s", op.code, DRAWCODES[op.code], JSON.stringify(s.stack));
    switch (op.code) {
    case 1: // hstem
    case 3: // vstem
    if (s.stack.length & 1) getWidth(s);
    break;
    case 4: // vmoveto
    if (s.stack.length > 1) getWidth(s);
    s.y += s.stack[0];
    moveto(s, ctx);
    break;
    case 5: // rlineto
    for (let i = 0; i + 1 < s.stack.length; i += 2) {
    s.x += s.stack[i];
    s.y += s.stack[i + 1];
    ctx.lineTo(s.x, s.y);
    }
    break;
    case 6: // hlineto
    for (let i = 0; i < s.stack.length; i += 2) {
    s.x += s.stack[i];
    ctx.lineTo(s.x, s.y);
    if (i + 1 < s.stack.length) {
    s.y += s.stack[i + 1];
    ctx.lineTo(s.x, s.y);
    }
    }
    break;
    case 7: // vlineto
    for (let i = 0; i < s.stack.length; i += 2) {
    s.y += s.stack[i];
    ctx.lineTo(s.x, s.y);
    if (i + 1 < s.stack.length) {
    s.x += s.stack[i + 1];
    ctx.lineTo(s.x, s.y);
    }
    }
    break;
    case 8: // rrcurveto
    rrcurve(s, ctx);
    break;
    case 10: // callsubr
    DrawCFFGlyph(subs.getSubr(s.stack.pop()!, fontNumber), ctx, fontNumber, subs, s);
    continue; // don't clear stack
    case 11: // return
    return;
    case 14: // endchar
    if (s.stack.length) getWidth(s);
    endpath(s, ctx);
    break;
    case 18: // hstemhm
    case 23: // vstemhm
    if (s.stack.length & 1) getWidth(s);
    s.numHints += s.stack.length >> 1;
    break;
    case 19: // hintmask
    case 20: // cntrmask
    if (s.stack.length & 1) getWidth(s);
    s.numHints += s.stack.length >> 1; // optional vstem values
    const numSkip = (s.numHints + 7) >> 3;
    for (let i = 0; i < numSkip; i++) {
    f.getUint(1);
    }
    break;
    case 21: // rmoveto
    if (s.stack.length & 1) getWidth(s);
    s.x += s.stack[0];
    s.y += s.stack[1];
    moveto(s, ctx);
    break;
    case 22: // hmoveto
    if (s.stack.length === 2) getWidth(s);
    s.x += s.stack[0];
    moveto(s, ctx);
    break;
    case 24: // rcurveline
    rrcurve(s, ctx);
    s.y += s.stack.pop()!;
    s.x += s.stack.pop()!;
    ctx.lineTo(s.x, s.y);
    break;
    case 25: { // rlinecurve
    for (var i = 0; i < s.stack.length - 6; i += 2) {
    s.x += s.stack[i];
    s.y += s.stack[i + 1];
    ctx.lineTo(s.x, s.y);
    }
    rrcurve(s, ctx, i);
    break;
    }
    case 26: { // vvcurveto
    let i = s.stack.length & 1;
    if (i) {
    s.x += s.stack[0];
    }
    for (; i < s.stack.length; i += 4) {
    const dxa = s.x;
    const dya = s.y += s.stack[i + 0];
    const dxb = s.x += s.stack[i + 1];
    const dyb = s.y += s.stack[i + 2];
    const dxc = s.x;
    const dyc = s.y += s.stack[i + 3];
    ctx.bezierCurveTo(dxa, dya, dxb, dyb, dxc, dyc);
    }
    break;
    }
    case 27: { // hhcurveto
    let i = s.stack.length & 1;
    if (i) {
    s.y += s.stack[0];
    }
    for (; i < s.stack.length; i += 4) {
    const dxa = s.x += s.stack[i + 0];
    const dya = s.y;
    const dxb = s.x += s.stack[i + 1];
    const dyb = s.y += s.stack[i + 2];
    const dxc = s.x += s.stack[i + 3];
    const dyc = s.y;
    ctx.bezierCurveTo(dxa, dya, dxb, dyb, dxc, dyc);
    }
    break;
    }
    case 29: // callgsubr
    DrawCFFGlyph(subs.getSubr(s.stack.pop()!, -1), ctx, fontNumber, subs, s);
    continue;
    case 30: // vhcurveto
    xxcurveto(s, false, ctx);
    break;
    case 31: // hvcurveto
    xxcurveto(s, true, ctx);
    break;
    case 0xc03: // and
    case 0xc04: // or
    case 0xc05: // not
    case 0xc09: // abs
    case 0xc0a: // add
    case 0xc0b: // sub
    case 0xc0c: // div
    case 0xc0e: // neg
    case 0xc0f: // eq
    case 0xc12: // drop
    case 0xc14: // put
    case 0xc15: // get
    case 0xc16: // ifelse
    case 0xc17: // random
    case 0xc18: // mul
    case 0xc1a: // sqrt
    case 0xc1b: // dup
    case 0xc1c: // exch
    case 0xc1d: // index
    case 0xc1e: // roll
    case 0xc22: // hflex
    case 0xc23: // flex
    case 0xc24: // hflex1
    case 0xc25: // flex1
    todo();
    break;
    }

    s.stack.length = 0;
    }
    }

    /**
    CFF font outlines don't end at their start. So before we move to another
    position, close off any existing paths.
    */
    function moveto(s: MachineState, ctx: ICanvasContext) {
    endpath(s, ctx);
    s.startX = s.x;
    s.startY = s.y;
    s.started = true;
    ctx.moveTo(s.x, s.y);
    }

    function endpath(s: MachineState, ctx: ICanvasContext) {
    if (s.started) {
    ctx.lineTo(s.startX, s.startY);
    }
    }

    function rrcurve(s: MachineState, ctx: ICanvasContext, i = 0) {
    for (; i + 5 < s.stack.length; i += 6) {
    const dxa = s.x += s.stack[i + 0];
    const dya = s.y += s.stack[i + 1];
    const dxb = s.x += s.stack[i + 2];
    const dyb = s.y += s.stack[i + 3];
    const dxc = s.x += s.stack[i + 4];
    const dyc = s.y += s.stack[i + 5];
    ctx.bezierCurveTo(dxa, dya, dxb, dyb, dxc, dyc);
    }
    }

    function xxcurveto(s: MachineState, h: boolean, ctx: ICanvasContext) {
    for (let i = 0; i + 1 < s.stack.length; i += 4) {
    const last = i == s.stack.length - 5;
    if (h) {
    const dxa = s.x += s.stack[i + 0];
    const dya = s.y;
    const dxb = s.x += s.stack[i + 1];
    const dyb = s.y += s.stack[i + 2];
    const dxc = last ? s.x += s.stack[i + 4] : s.x;
    const dyc = s.y += s.stack[i + 3];
    ctx.bezierCurveTo(dxa, dya, dxb, dyb, dxc, dyc);
    } else {
    const dxa = s.x;
    const dya = s.y += s.stack[i + 0];
    const dxb = s.x += s.stack[i + 1];
    const dyb = s.y += s.stack[i + 2];
    const dxc = s.x += s.stack[i + 3];
    const dyc = last ? s.y += s.stack[i + 4] : s.y;
    ctx.bezierCurveTo(dxa, dya, dxb, dyb, dxc, dyc);
    }
    h = !h;
    }
    }

    function todo() {
    throw new Error("Not implemented.")
    }
    function getWidth(s: MachineState) {
    if (!s.first) {
    log("Warning; already have width");
    s.stack.shift();
    return;
    //throw new Error("Error: Already got width arg.");
    }
    //log("Got width: " + s.stack[0]);
    s.width = s.stack.shift()!;
    s.first = false;
    }


    class FDSelect {
    /** @param f Already sliced binary reader */
    format: number;
    nRanges = 0;
    constructor(private f: Slice) {
    this.format = f.getUint(1);
    log("Reading FDSelect format %s", this.format);
    if (this.format !== 3 && this.format !== 0) {
    throw new Error(`Can't read format ${this.format} FDSelect`);
    }

    if (this.format === 3) {
    this.nRanges = f.getUint(2);
    }
    }

    lookup(target: number) {
    if (this.format === 0) {
    this.f.seek(1 + target);
    return this.f.getUint(1);
    }

    let fd = -1;
    bsearch(this.nRanges, (n) => {
    this.f.seek(3 + 3 * n);
    let first = this.f.getUint(2);
    fd = this.f.getUint(1);
    let next = this.f.getUint(2);
    if (target < first) {
    return -1;
    } else if (target >= next) {
    return 1;
    }
    return 0;
    });
    return fd;
    }
    }

    /**
    To save space, CFF font subroutine numbers are offset by a certain fixed number.
    This adds that number back in.
    */
    function GetBias(charStringType: number, count: number): number {
    if (charStringType === 0) {
    return 0;
    } else if (count < 1240) {
    return 107;
    } else if (count < 33900) {
    return 1131;
    }
    return 32768;
    }

    const STDSTRINGS = `.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 twodotenleader
    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`.
    split(/\s+/);

    const GPOS_TABLE = `{
    majorVersion:uint16
    minorVersion:uint16
    scriptListOffset:uint16
    featureListOffset:uint16
    lookupListOffset:uint16
    featureVariationsOffset:uint16
    push scriptListOffset
    scriptCount:uint16
    scriptRecords:[scriptCount] {
    tag:string(4)
    scriptOffset:uint16
    push scriptOffset
    defaultLangSys:uint16
    langSysCount:uint16
    langSysRecords:[langSysCount] {
    langSysTag:string(4)
    langSysOffset:uint16
    push langSysOffset
    lookupOrder:uint16
    requiredFeatureIndex:uint16
    featureIndexCount:uint16
    featureIndices:[featureIndexCount] uint16
    pop
    }
    pop
    }
    pop
    push lookupListOffset
    lookupCount:uint16
    lookups:[lookupCount] {
    offset:uint16
    push offset
    lookupType:uint16
    lookupFlag:uint16
    subTableCount:uint16
    subTables:[subTableCount] {
    offset:uint16
    push offset
    posFormat:uint16
    coverageOffset:uint16
    }
    pop
    }
    pop
    push featureListOffset
    featureCount:uint16
    featureRecords[featureCount] {
    featureTag:string(4)
    featureOffset:uint16
    push featureOffset
    featureParams:uint16
    lookupIndexCount:uint16
    lookupListIndicies:[lookupIndexCount] uint16
    pop
    }
    pop
    }`

    class KernAdjustment {
    constructor(public glyph: number) { }
    placement = { x: 0, y: 0 };
    advance = { x: 0, y: 0 };
    }

    interface KernAdjuster {
    kern(glyphs: KernAdjustment[]): void;
    }

    class GPOS implements KernAdjuster {
    header: any;
    adjusters: KernAdjuster[] = [];
    constructor(input: Slice) {
    this.header = decode(input, GPOS_TABLE);
    let offset = this.header["lookupListOffset"];
    for (let lookup of this.header["lookups"]) {
    let loffset = offset + lookup["offset"];
    let type = lookup["lookupType"];
    log("Lookup type %s has %s subtables", type, lookup["subTables"].length);
    for (let subtable of lookup["subTables"]) {
    let soffset = loffset + subtable["offset"];
    if (type === 2 && subtable["posFormat"] === 1) {
    this.adjusters.push(new LookupType2_1(input.slice(soffset)));
    } else if (type === 2 && subtable["posFormat"] === 2) {
    this.adjusters.push(new LookupType2_2(input.slice(soffset)));
    }
    }
    }
    }

    kern(glyphs: KernAdjustment[]) {
    for (let adjuster of this.adjusters) {
    adjuster.kern(glyphs);
    }
    }
    }

    class CoverageTable {
    format: number;
    count: number;
    headersize = 4;
    constructor(private f: Slice) {
    this.format = f.getUint(2);
    this.count = f.getUint(2);
    log("CoverageTable format %s count %s", this.format, this.count);
    }

    getCoverageIndex(glyph: number) {
    let index = -1;
    if (this.format === 1) {
    bsearch(this.count, (i: number) => {
    this.f.seek(this.headersize + i * 2);
    let j = this.f.getUint(2);
    if (glyph < j) {
    return -1;
    } else if (glyph > j) {
    return 1;
    }
    index = j;
    return 0;
    });
    } else {
    bsearch(this.count, (i: number) => {
    this.f.seek(this.headersize + i * 6);
    let start = this.f.getUint(2);
    this.f.getUint(2); // end
    let first = this.f.getUint(2);
    if (glyph < start) {
    return -1;
    } else if (glyph > start) {
    return 1;
    }
    index = first + glyph - start;
    return 0;
    })
    }
    return index;
    }
    }

    // Since this is so similar to the Coverage table formats
    // we can reuse the code.
    class ClassDefTable extends CoverageTable {
    startGlyph = 0
    constructor(f: Slice) {
    super(f)
    if (this.format === 1) {
    this.startGlyph = this.count;
    this.count = f.getUint(2);
    this.headersize += 2;
    }
    }

    getClass(glyph: number) {
    return Math.max(0, this.getCoverageIndex(glyph - this.startGlyph));
    }
    }

    function readValueRecord(f: Slice, kern: KernAdjustment, format: number) {
    if (format & 1) kern.placement.x = f.getInt16();
    if (format & 2) kern.placement.y = f.getInt16();
    if (format & 4) kern.advance.x = f.getInt16();
    if (format & 8) kern.advance.y = f.getInt16();
    if (format & 16) f.getInt16();
    if (format & 32) f.getInt16();
    if (format & 64) f.getInt16();
    if (format & 128) f.getInt16();
    }

    abstract class PairKerner {
    kern(glyphs: KernAdjustment[]) {
    for (let i = 0; i < glyphs.length - 1; i++) {
    this.lookup(glyphs[i], glyphs[i + 1]);
    }
    }
    abstract lookup(glyph1: KernAdjustment, glyph2: KernAdjustment): number
    }

    // Pair adjustment positioning pos format 1
    class LookupType2_1 extends PairKerner {
    coverage: CoverageTable
    valueFormat1: number;
    valueFormat2: number;
    count: number;
    recordLength: number;
    constructor(private f: Slice) {
    super()

    f.getUint(2); // format = 1
    let coverage = f.getUint(2);
    this.coverage = new CoverageTable(f.slice(coverage));
    this.valueFormat1 = f.getUint(2);
    this.valueFormat2 = f.getUint(2);
    this.count = f.getUint(2);
    this.recordLength = bitCount(this.valueFormat1) * 2 + bitCount(this.valueFormat2) * 2 + 2;
    }


    lookup(glyph1: KernAdjustment, glyph2: KernAdjustment) {
    // first, find glyph1 in coverage
    let glyph1Coverage = this.coverage.getCoverageIndex(glyph1.glyph);
    if (glyph1Coverage === -1) {
    return 0;
    }

    // now seek to its pairset table
    this.f.seek(10 + 2 * glyph1Coverage);
    this.f.seek(this.f.getUint(2));
    const at = this.f.tell();

    let count = this.f.getUint(2);
    let found = bsearch(count, (i) => {
    this.f.seek(at + i * this.recordLength);
    let secondGlyph = this.f.getUint(2);
    if (secondGlyph < glyph2.glyph) {
    return -1;
    } else if (secondGlyph > glyph2.glyph) {
    return 1;
    }
    readValueRecord(this.f, glyph1, this.valueFormat1);
    readValueRecord(this.f, glyph2, this.valueFormat2);
    return 0;
    });

    return found >= 0 ? 1 : 0;
    }
    }

    class LookupType2_2 extends PairKerner {
    coverage: CoverageTable
    classDef1: ClassDefTable
    classDef2: ClassDefTable
    class1Count: number
    class2Count: number
    valueFormat1: number
    valueFormat2: number
    recordSize: number;
    constructor(private f: Slice) {
    super();
    f.getUint(2); // format
    this.coverage = new CoverageTable(f.slice(f.getUint(2)));
    this.valueFormat1 = f.getUint(2);
    this.valueFormat2 = f.getUint(2);
    this.classDef1 = new ClassDefTable(f.slice(f.getUint(2)));
    this.classDef2 = new ClassDefTable(f.slice(f.getUint(2)));
    this.class1Count = f.getUint(2);
    this.class2Count = f.getUint(2);
    this.recordSize = bitCount(this.valueFormat1) * 2 + bitCount(this.valueFormat2) * 2;
    }

    lookup(glyph1: KernAdjustment, glyph2: KernAdjustment) {
    let glyph1Coverage = this.coverage.getCoverageIndex(glyph1.glyph);
    if (glyph1Coverage === -1) return 0;

    let glyph1Class = this.classDef1.getClass(glyph1.glyph);
    let glyph2Class = this.classDef2.getClass(glyph2.glyph);

    this.f.seek(16 + glyph1Class * this.recordSize * this.class2Count + glyph2Class * this.recordSize);
    readValueRecord(this.f, glyph1, this.valueFormat1);
    readValueRecord(this.f, glyph2, this.valueFormat2);
    return 1;
    }
    }

    class Kern0 extends PairKerner {
    constructor(
    private f: Slice,
    private count = f.getUint(2)
    ) {
    super();
    log("Kern table has %s entries", this.count);
    }

    lookup(glyph1: KernAdjustment, glyph2: KernAdjustment) {
    let key = (glyph1.glyph << 16) | glyph2.glyph;
    let ret = 0;
    bsearch(this.count, (i) => {
    this.f.seek(8 + 6 * i);
    let rec = this.f.getUint(4);
    if (key < rec) {
    return -1;
    } else if (key > rec) {
    return 1;
    }

    let shift = this.f.getInt16();
    glyph2.placement.x += shift;
    glyph2.advance.x += shift;
    ret = 1;
    return 0;
    })

    return ret;
    }

    }

    function bitCount(n: number) {
    n = n - ((n >> 1) & 0x55555555)
    n = (n & 0x33333333) + ((n >> 2) & 0x33333333)
    return ((n + (n >> 4) & 0xF0F0F0F) * 0x1010101) >> 24
    }
  3. smhanov revised this gist Aug 16, 2022. No changes.
  4. smhanov revised this gist Apr 19, 2020. No changes.
  5. smhanov revised this gist Apr 19, 2020. No changes.
  6. smhanov revised this gist Apr 19, 2020. 1 changed file with 5 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions TrueType.ts
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,8 @@
    // By Steve Hanov
    // steve.hanov@gmail.com
    // Released to the public domain on April 18, 2020


    //import { log } from "./log"

    // Usage:
  7. smhanov created this gist Apr 19, 2020.
    1,030 changes: 1,030 additions & 0 deletions TrueType.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1030 @@
    //import { log } from "./log"

    // Usage:
    /*
    declare let arrayBuffer:ArrayBuffer;
    let font = new TrueTypeFont(arrayBuffer);
    // Draw 15 pixel high "hello world" at x, y on canvas context ctx
    font.drawText(ctx, "hello world", x, y, 15);
    */

    // This is a stand-in for my own logging framework. Replace with yours.
    let log = {
    create(prefix: string) {
    return (...args:any[]) => {
    console.log(...args);
    }
    }
    };

    function assert(condition: boolean, message = "Assertion failed.") {
    if (!condition) {
    throw new Error(message);
    }
    }

    class BinaryReader {
    private pos = 0;
    getUint8: () => number;
    readonly length: number;

    constructor(dataIn: string | ArrayBuffer) {
    if (typeof dataIn === "string") {
    this.length = dataIn.length;
    this.getUint8 = () => {
    assert(this.pos < dataIn.length);
    return dataIn.charCodeAt(this.pos++) & 0xff;
    };
    } else {
    let data = new Uint8Array(dataIn);
    this.length = data.length;
    this.getUint8 = () => {
    assert(this.pos < data.length);
    return data[this.pos++];
    }
    }
    }

    log = log.create("BinaryReader");

    seek(pos: number) {
    assert(pos >= 0 && pos <= this.length);
    var oldPos = this.pos;
    this.pos = pos;
    return oldPos;
    }

    tell() {
    return this.pos;
    }

    getUint16() {
    return ((this.getUint8() << 8) | this.getUint8()) >>> 0;
    }

    getUint32() {
    return this.getInt32() >>> 0;
    }

    getInt16() {
    var result = this.getUint16();
    if (result & 0x8000) {
    result -= (1 << 16);
    }
    return result;
    }

    getInt32() {
    return ((this.getUint8() << 24) |
    (this.getUint8() << 16) |
    (this.getUint8() << 8) |
    (this.getUint8()));
    }

    getFword() {
    return this.getInt16();
    }

    getUFword() {
    return this.getUint16();
    }

    get2Dot14() {
    return this.getInt16() / (1 << 14);
    }

    getFixed() {
    return this.getInt32() / (1 << 16);
    }

    getString(length: number) {
    var result = "";
    for (var i = 0; i < length; i++) {
    result += String.fromCharCode(this.getUint8());
    }
    return result;
    }

    getUnicodeString(length: number) {
    var result = "";
    for (var i = 0; i < length; i += 2) {
    result += String.fromCharCode(this.getUint16());
    }
    return result;
    }

    getDate() {
    var macTime = this.getUint32() * 0x100000000 + this.getUint32();
    var utcTime = macTime * 1000 + Date.UTC(1904, 1, 1);
    return new Date(utcTime);
    }
    }

    interface CMap {
    readonly format: number;
    map(charCode: number): number;
    }

    /**
    Cmap format 0 is just a direct mapping from one byte to the glyph index.
    */
    class TrueTypeCmap0 implements CMap {
    format = 0;
    array: number[] = [];

    constructor(file: BinaryReader, length: number) {
    for (var i = 0; i < 256; i++) {
    var glyphIndex = file.getUint8();
    this.log(" Glyph[%s] = %s", i, glyphIndex);
    this.array.push(glyphIndex);
    }
    }

    log = log.create("CMAP0");

    map(charCode: number) {
    if (charCode >= 0 && charCode <= 255) {
    //this.log("charCode %s maps to %s", charCode, this.array[charCode]);
    return this.array[charCode];
    }
    return 0;
    }
    }

    interface Segment {
    idRangeOffset: number;
    startCode: number;
    endCode: number;
    idDelta: number;
    }

    /**
    Cmap format 4 is a list of segments which can possibly include gaps
    */
    class TrueTypeCmap4 implements CMap {
    format = 4;
    cache: { [key: number]: number } = {};
    segments: Segment[];

    constructor(private file: BinaryReader, length: number) {
    var i, segments: Segment[] = [];

    // 2x segcount
    var segCount = file.getUint16() / 2;

    // 2 * (2**floor(log2(segCount)))
    var searchRange = file.getUint16();
    // log2(searchRange)
    var entrySelector = file.getUint16();
    // (2*segCount) - searchRange
    var rangeShift = file.getUint16();
    // Ending character code for each segment, last is 0xffff
    for (i = 0; i < segCount; i++) {
    segments.push({
    idRangeOffset: 0,
    startCode: 0,
    endCode: file.getUint16(),
    idDelta: 0
    });
    }

    // reservePAd
    file.getUint16();

    // starting character code for each segment
    for (i = 0; i < segCount; i++) {
    segments[i].startCode = file.getUint16();
    }

    // Delta for all character codes in segment
    for (i = 0; i < segCount; i++) {
    segments[i].idDelta = file.getUint16();
    }

    // offset in bytes to glyph indexArray, or 0
    for (i = 0; i < segCount; i++) {
    var ro = file.getUint16();
    if (ro) {
    segments[i].idRangeOffset = file.tell() - 2 + ro;
    } else {
    segments[i].idRangeOffset = 0;
    }
    }

    /*
    for(i = 0; i < segCount; i++) {
    var seg = segments[i];
    this.log("segment[%s] = %s %s %s %s", i,
    seg.startCode, seg.endCode, seg.idDelta, seg.idRangeOffset);
    }
    */


    this.segments = segments;
    }

    log = log.create("CMAP4");

    map(charCode: number) {
    if (!(charCode in this.cache)) {
    for (var j = 0; j < this.segments.length; j++) {
    var segment = this.segments[j];
    if (segment.startCode <= charCode && segment.endCode >=
    charCode) {
    var index, glyphIndexAddress;
    if (segment.idRangeOffset) {
    glyphIndexAddress = segment.idRangeOffset + 2 *
    (charCode - segment.startCode);
    this.file.seek(glyphIndexAddress);
    index = this.file.getUint16();
    } else {
    index = (segment.idDelta + charCode) & 0xffff;
    }

    this.log("Charcode %s is between %s and %s; maps to %s (%s) roffset=%s",
    charCode, segment.startCode, segment.endCode,
    glyphIndexAddress, index, segment.idRangeOffset);

    this.cache[charCode] = index;
    break;
    }
    }

    if (j === this.segments.length) {
    this.cache[charCode] = 0;
    }
    }

    return this.cache[charCode];
    }
    };

    class Kern0Table {
    swap: boolean;
    offset: number;
    nPairs: number;
    map: { [key: number]: number } = {};
    oldIndex = -1;

    constructor(
    private file: BinaryReader,
    vertical: boolean,
    cross: boolean) {
    this.swap = vertical && !cross || !vertical && cross;
    this.file = file;
    this.offset = file.tell();
    this.nPairs = file.getUint16();
    file.getUint16(); // searchRange
    file.getUint16(); // entrySelector
    file.getUint16(); // rangeShift

    for (var i = 0; i < this.nPairs; i++) {
    var left = file.getUint16();
    var right = file.getUint16();
    var value = file.getFword();
    this.map[(left << 16) | right] = value;
    //this.log("Kern %s/%s->%s", left, right, value);
    }
    this.reset();
    }


    log = log.create("KERN0");

    reset() {
    this.oldIndex = -1;
    }

    get(glyphIndex: number) {
    var x = 0;
    if (this.oldIndex >= 0) {
    var ch = (this.oldIndex << 16) | glyphIndex;
    if (ch in this.map) {
    x = this.map[ch];
    }
    //this.log("Lookup kern pair %s/%s -> %s (%s)",
    // this.oldIndex, glyphIndex, x, ch);
    }
    this.oldIndex = glyphIndex;
    if (this.swap) {
    return {
    x: 0,
    y: x
    };
    } else {
    return {
    x: x,
    y: 0
    };
    }
    }
    };

    interface Table {
    checksum: number;
    offset: number;
    length: number;
    }

    interface Point {
    x: number;
    y: number;
    onCurve: boolean;
    }

    class GylphComponent {
    points: Point[] = [];
    }

    interface Glyph {
    contourEnds: number[];
    numberOfContours: number;
    points: Point[];
    xMin: number;
    xMax: number;
    yMin: number;
    yMax: number;
    }

    export class TrueTypeFont {
    private file: BinaryReader;
    private cmaps: CMap[] = [];
    private kern: Kern0Table[] = [];
    private tables: { [key: string]: Table };
    private length: number;
    private scalarType = 0;
    private searchRange = 0;
    private entrySelector = 0;
    private rangeShift = 0;
    private version = 0;
    private fontRevision = 0;
    private checksumAdjustment = 0;
    private magicNumber = 0;
    private flags = 0;
    private unitsPerEm = 0;
    private created = new Date();
    private modified = new Date();
    private xMin = 0;
    private yMin = 0;
    private xMax = 0;
    private yMax = 0;
    private macStyle = 0;
    private lowestRecPPEM = 0;
    private fontDirectionHint = 0;
    private indexToLocFormat = 0;
    private glyphDataFormat = 0;
    public fullName = "";
    public fontFamily = "";
    private fontSubFamily = "";
    public postscriptName = "";
    public ascent = 0;
    public descent = 0;
    public lineGap = 0;
    public advanceWidthMax = 0;
    public minLeftSideBearing = 0;
    public minRightSideBearing = 0;
    public xMaxExtent = 0;
    public caretSlopeRise = 0;
    public caretSlopeRun = 0;
    public caretOffset = 0;
    public metricDataFormat = 0;
    public numOfLongHorMetrics = 0;

    constructor(data: ArrayBuffer | string) {
    this.file = new BinaryReader(data);
    this.tables = this.readOffsetTables(this.file);
    this.readHeadTable(this.file);
    this.readNameTable(this.file);
    this.readCmapTable(this.file);
    this.readHheaTable(this.file);
    this.readKernTable(this.file);
    this.length = this.glyphCount();
    }

    log = log.create("TrueType");

    readOffsetTables(file: BinaryReader) {

    /**
    Mandatory tables:
    - cmap
    - glyf
    - head
    - hhead
    - hmtx
    - loca
    - maxp
    - name
    - post
    */
    var tables: { [key: string]: Table } = {};
    this.scalarType = file.getUint32();
    var numTables = file.getUint16();
    this.searchRange = file.getUint16();
    this.entrySelector = file.getUint16();
    this.rangeShift = file.getUint16();

    for (var i = 0; i < numTables; i++) {
    var tag = file.getString(4);
    tables[tag] = {
    checksum: file.getUint32(),
    offset: file.getUint32(),
    length: file.getUint32()
    };

    if (tag !== 'head') {
    this.log("Table %s has checksum 0x%s", tag,
    tables[tag].checksum.toString(16));
    //assert(this.calculateTableChecksum(file, tables[tag].offset,
    // tables[tag].length) === tables[tag].checksum);
    }
    }

    return tables;
    }

    calculateTableChecksum(file: BinaryReader, offset: number,
    length: number) {
    var old = file.seek(offset);
    var sum = 0;
    var nlongs = ((length + 3) / 4) >>> 0;
    this.log("nlongs=%s length=%s", nlongs, length);
    while (nlongs--) {
    sum = (sum + file.getUint32()) >>> 0;
    }

    file.seek(old);
    this.log("Checksum calculated is 0x%s", sum.toString(16));
    return sum;
    }

    readHeadTable(file: BinaryReader) {
    assert("head" in this.tables);
    file.seek(this.tables["head"].offset);

    this.version = file.getFixed();
    this.fontRevision = file.getFixed();
    this.checksumAdjustment = file.getUint32();
    this.magicNumber = file.getUint32();
    assert(this.magicNumber === 0x5f0f3cf5);
    this.flags = file.getUint16();
    this.unitsPerEm = file.getUint16();
    this.created = file.getDate();
    this.modified = file.getDate();
    this.xMin = file.getFword();
    this.yMin = file.getFword();
    this.xMax = file.getFword();
    this.yMax = file.getFword();
    this.macStyle = file.getUint16();
    this.lowestRecPPEM = file.getUint16();
    this.fontDirectionHint = file.getInt16();
    this.indexToLocFormat = file.getInt16();
    this.glyphDataFormat = file.getInt16();
    }

    readCmapTable(file: BinaryReader) {
    assert("cmap" in this.tables);
    var tableOffset = this.tables["cmap"].offset;
    file.seek(tableOffset);
    var version = file.getUint16(); // must be 0
    var numberSubtables = file.getUint16();

    // tables must be sorted by platform id and then platform specific
    // encoding.
    for (var i = 0; i < numberSubtables; i++) {
    // platforms are:
    // 0 - Unicode -- use specific id 6 for full coverage. 0/4 common.
    // 1 - MAcintosh (Discouraged)
    // 2 - reserved
    // 3 - Microsoft
    var platformID = file.getUint16();
    var platformSpecificID = file.getUint16();
    var offset = file.getUint32();
    this.log("CMap platformid=%s specificid=%s offset=%s", platformID,
    platformSpecificID, offset);
    if (platformID === 3 && (platformSpecificID <= 1)) {
    this.readCmap(file, tableOffset + offset);
    }
    }

    // use format 0 table preferably.
    //this.cmaps.sort(function(a, b) {
    // return a.format - b.format;
    //});
    }

    readCmap(file: BinaryReader, offset: number) {
    var oldPos = file.seek(offset);
    var format = file.getUint16();
    var length = file.getUint16();
    var language = file.getUint16();
    var cmap;

    this.log(" Cmap format %s length %s", format, length);
    if (format === 0) {
    cmap = new TrueTypeCmap0(file, length);
    } else if (format === 4) {
    cmap = new TrueTypeCmap4(file, length);
    }

    if (cmap) {
    this.cmaps.push(cmap);
    }

    file.seek(oldPos);
    }

    readKernTable(file: BinaryReader) {
    if (!("kern" in this.tables)) {
    return;
    }
    var tableOffset = this.tables["kern"].offset;
    file.seek(tableOffset);
    var version = file.getUint16(); // version 0
    var nTables = file.getUint16();

    this.log("Kern Table version: %s", version);
    this.log("Kern nTables: %s", nTables);
    for (var i = 0; i < nTables; i++) {
    version = file.getUint16(); // subtable version
    var length = file.getUint16();
    var coverage = file.getUint16();
    var format = coverage >> 8;
    var cross = coverage & 4;
    var vertical = (coverage & 0x1) === 0;
    this.log("Kerning subtable version %s format %s length %s coverage: %s",
    version, format, length, coverage);
    var kern = null;
    if (format === 0) {
    kern = new Kern0Table(file, vertical, cross != 0);
    } else {
    this.log("Unknown format -- skip");
    file.seek(file.tell() + length);
    }
    if (kern) {
    this.kern.push(kern);
    }
    }
    }

    readNameTable(file: BinaryReader) {
    assert("name" in this.tables);
    var tableOffset = this.tables["name"].offset;
    file.seek(tableOffset);
    var format = file.getUint16(); // must be 0
    var count = file.getUint16();
    var stringOffset = file.getUint16();
    for (var i = 0; i < count; i++) {
    var platformID = file.getUint16();
    var platformSpecificID = file.getUint16();
    var languageID = file.getUint16();
    var nameID = file.getUint16();
    var length = file.getUint16();
    var offset = file.getUint16();
    var old = file.seek(tableOffset + stringOffset + offset);
    var name;
    if (platformID === 0 || platformID === 3) {
    name = file.getUnicodeString(length);
    } else {
    name = file.getString(length);
    }
    this.log("Name %s/%s id %s language %s: %s",
    platformID, platformSpecificID, nameID, languageID, name);
    file.seek(old);

    switch (nameID) {
    case 1:
    this.fontFamily = name;
    break;
    case 2:
    this.fontSubFamily = name;
    break;
    case 4:
    this.fullName = name;
    break;
    case 6:
    this.postscriptName = name;
    break;
    }
    }
    }

    readHheaTable(file: BinaryReader) {
    assert("hhea" in this.tables);
    var tableOffset = this.tables["hhea"].offset;
    file.seek(tableOffset);
    var version = file.getFixed(); // 0x00010000
    this.ascent = file.getFword();
    this.descent = file.getFword();
    this.lineGap = file.getFword();
    this.advanceWidthMax = file.getUFword();
    this.minLeftSideBearing = file.getFword();
    this.minRightSideBearing = file.getFword();
    this.xMaxExtent = file.getFword();
    this.caretSlopeRise = file.getInt16();
    this.caretSlopeRun = file.getInt16();
    this.caretOffset = file.getFword();
    file.getInt16(); // reserved
    file.getInt16(); // reserved
    file.getInt16(); // reserved
    file.getInt16(); // reserved
    this.metricDataFormat = file.getInt16();
    this.numOfLongHorMetrics = file.getUint16();
    }

    getHorizontalMetrics(glyphIndex: number) {
    assert("hmtx" in this.tables);
    var file = this.file;
    var old = file.seek(this.tables["hmtx"].offset + 4);
    var offset = this.tables["hmtx"].offset;
    let advanceWidth, leftSideBearing;
    if (glyphIndex < this.numOfLongHorMetrics) {
    offset += glyphIndex * 4;
    old = this.file.seek(offset);
    advanceWidth = file.getUint16();
    leftSideBearing = file.getInt16();
    } else {
    // read the last entry of the hMetrics array
    old = file.seek(offset + (this.numOfLongHorMetrics - 1) * 4);
    advanceWidth = file.getUint16();
    file.seek(offset + this.numOfLongHorMetrics * 4 +
    2 * (glyphIndex - this.numOfLongHorMetrics));
    leftSideBearing = file.getFword();
    }

    this.file.seek(old);
    return {
    advanceWidth: advanceWidth,
    leftSideBearing: leftSideBearing
    };
    }

    glyphCount() {
    assert("maxp" in this.tables);
    var old = this.file.seek(this.tables["maxp"].offset + 4);
    var count = this.file.getUint16();
    this.file.seek(old);
    return count;
    }

    getGlyphOffset(index: number) {
    assert("loca" in this.tables);
    var table = this.tables["loca"];
    var file = this.file;
    var offset, old, next;

    if (this.indexToLocFormat === 1) {
    old = file.seek(table.offset + index * 4);
    offset = file.getUint32();
    next = file.getUint32();
    } else {
    old = file.seek(table.offset + index * 2);
    offset = file.getUint16() * 2;
    next = file.getUint16() * 2;
    }

    file.seek(old);
    if (offset === next) {
    // indicates glyph has no outline( eg space)
    return 0;
    }

    //this.log("Offset for glyph index %s is %s", index, offset);

    return offset + this.tables["glyf"].offset;
    }

    readGlyph(index: number): Glyph | null {
    var offset = this.getGlyphOffset(index);
    var file = this.file;

    if (offset === 0 ||
    offset >= this.tables["glyf"].offset + this.tables["glyf"].length) {
    return null;
    }

    assert(offset >= this.tables["glyf"].offset);
    assert(offset < this.tables["glyf"].offset + this.tables["glyf"].length);

    file.seek(offset);

    var glyph = {
    contourEnds: [],
    numberOfContours: file.getInt16(),
    points: [],
    xMin: file.getFword(),
    yMin: file.getFword(),
    xMax: file.getFword(),
    yMax: file.getFword()
    };

    assert(glyph.numberOfContours >= -1);

    if (glyph.numberOfContours === -1) {
    this.readCompoundGlyph(file, glyph);
    } else {
    this.readSimpleGlyph(file, glyph);
    }

    return glyph;
    }

    readSimpleGlyph(file: BinaryReader, glyph: Glyph) {

    var ON_CURVE = 1,
    X_IS_BYTE = 2,
    Y_IS_BYTE = 4,
    REPEAT = 8,
    X_DELTA = 16,
    Y_DELTA = 32;

    glyph.contourEnds = [];
    var points: Point[] = glyph.points = [];

    for (var i = 0; i < glyph.numberOfContours; i++) {
    glyph.contourEnds.push(file.getUint16());
    }

    // skip over intructions
    file.seek(file.getUint16() + file.tell());

    if (glyph.numberOfContours === 0) {
    return;
    }

    var numPoints = Math.max.apply(null, glyph.contourEnds) + 1;

    var flags: number[] = [];

    for (i = 0; i < numPoints; i++) {
    var flag = file.getUint8();
    flags.push(flag);
    points.push({
    x: 0,
    y: 0,
    onCurve: (flag & ON_CURVE) > 0
    });

    if (flag & REPEAT) {
    var repeatCount = file.getUint8();
    assert(repeatCount > 0);
    i += repeatCount;
    while (repeatCount--) {
    flags.push(flag);
    points.push({
    x: 0,
    y: 0,
    onCurve: (flag & ON_CURVE) > 0
    });
    }
    }
    }

    function readCoords(name: "x" | "y", byteFlag: number,
    deltaFlag: number, min: number, max: number) {
    var value = 0;

    for (var i = 0; i < numPoints; i++) {
    var flag = flags[i];
    if (flag & byteFlag) {
    if (flag & deltaFlag) {
    value += file.getUint8();
    } else {
    value -= file.getUint8();
    }
    } else if (~flag & deltaFlag) {
    value += file.getInt16();
    } else {
    // value is unchanged.
    }

    points[i][name] = value;
    }
    }

    readCoords("x", X_IS_BYTE, X_DELTA, glyph.xMin, glyph.xMax);
    readCoords("y", Y_IS_BYTE, Y_DELTA, glyph.yMin, glyph.yMax);
    }

    readCompoundGlyph(file: BinaryReader, glyph: Glyph) {
    var ARG_1_AND_2_ARE_WORDS = 1,
    ARGS_ARE_XY_VALUES = 2,
    ROUND_XY_TO_GRID = 4,
    WE_HAVE_A_SCALE = 8,
    // RESERVED = 16
    MORE_COMPONENTS = 32,
    WE_HAVE_AN_X_AND_Y_SCALE = 64,
    WE_HAVE_A_TWO_BY_TWO = 128,
    WE_HAVE_INSTRUCTIONS = 256,
    USE_MY_METRICS = 512,
    OVERLAP_COMPONENT = 1024;

    var flags = MORE_COMPONENTS;
    var component;
    glyph.contourEnds = [];
    glyph.points = [];
    while (flags & MORE_COMPONENTS) {
    var arg1, arg2;

    flags = file.getUint16();
    component = {
    glyphIndex: file.getUint16(),
    matrix: {
    a: 1, b: 0, c: 0, d: 1, e: 0, f: 0
    },

    destPointIndex: 0,
    srcPointIndex: 0
    };

    if (flags & ARG_1_AND_2_ARE_WORDS) {
    arg1 = file.getInt16();
    arg2 = file.getInt16();
    } else {
    arg1 = file.getUint8();
    arg2 = file.getUint8();
    }

    if (flags & ARGS_ARE_XY_VALUES) {
    component.matrix.e = arg1;
    component.matrix.f = arg2;
    } else {
    component.destPointIndex = arg1;
    component.srcPointIndex = arg2;
    }

    if (flags & WE_HAVE_A_SCALE) {
    component.matrix.a = file.get2Dot14();
    component.matrix.d = component.matrix.a;
    } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
    component.matrix.a = file.get2Dot14();
    component.matrix.d = file.get2Dot14();
    } else if (flags & WE_HAVE_A_TWO_BY_TWO) {
    component.matrix.a = file.get2Dot14();
    component.matrix.b = file.get2Dot14();
    component.matrix.c = file.get2Dot14();
    component.matrix.d = file.get2Dot14();
    }

    this.log("Read component glyph index %s", component.glyphIndex);
    this.log("Transform: [%s %s %s %s %s %s]", component.matrix.a, component.matrix.b,
    component.matrix.c, component.matrix.d, component.matrix.e, component.matrix.f);
    var old = file.tell();
    var simpleGlyph = this.readGlyph(component.glyphIndex);
    if (simpleGlyph) {
    var pointOffset = glyph.points.length;
    for (var i = 0; i < simpleGlyph.contourEnds.length; i++) {
    glyph.contourEnds.push(simpleGlyph.contourEnds[i] +
    pointOffset);
    }
    for (i = 0; i < simpleGlyph.points.length; i++) {
    var x = simpleGlyph.points[i].x;
    var y = simpleGlyph.points[i].y;
    x = component.matrix.a * x + component.matrix.b * y +
    component.matrix.e;
    y = component.matrix.c * x + component.matrix.d * y +
    component.matrix.f;
    glyph.points.push({
    x: x, y: y, onCurve:
    simpleGlyph.points[i].onCurve
    });
    }
    }

    file.seek(old);
    }

    glyph.numberOfContours = glyph.contourEnds.length;

    if (flags & WE_HAVE_INSTRUCTIONS) {
    file.seek(file.getUint16() + file.tell());
    }

    }

    drawGlyph(ctx: CanvasRenderingContext2D, index: number, x: number, y: number) {

    var glyph = this.readGlyph(index);
    //this.log("Draw GLyph index %s", index);

    if (glyph === null) {
    return false;
    }

    var s = 0,
    p = 0,
    c = 0,
    contourStart = 0,
    prev;

    for (; p < glyph.points.length; p++) {
    var point = glyph.points[p];
    if (s === 0) {
    ctx.moveTo(point.x + x, point.y + y);
    s = 1;
    } else if (s === 1) {
    if (point.onCurve) {
    ctx.lineTo(point.x + x, point.y + y);
    } else {
    s = 2;
    }
    } else {
    prev = glyph.points[p - 1];
    if (point.onCurve) {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    point.x + x, point.y + y);
    s = 1;
    } else {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    (prev.x + point.x) / 2 + x,
    (prev.y + point.y) / 2 + y);
    }
    }

    if (p === glyph.contourEnds[c]) {
    if (s === 2) { // final point was off-curve. connect to start
    prev = point;
    point = glyph.points[contourStart];
    if (point.onCurve) {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    point.x + x, point.y + y);
    } else {
    ctx.quadraticCurveTo(prev.x + x, prev.y + y,
    (prev.x + point.x) / 2 + x,
    (prev.y + point.y) / 2 + y);
    }
    }
    contourStart = p + 1;
    c += 1;
    s = 0;
    }
    }

    return true;
    }

    transform(ctx: CanvasRenderingContext2D, size: number) {
    ctx.scale(size / this.unitsPerEm, -size / this.unitsPerEm);
    }

    drawText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number,
    size: number) {
    ctx.save();
    ctx.translate(x, y);
    this.transform(ctx, size);
    x = 0;
    y = 0;
    this.resetKern();
    for (var i = 0; i < text.length; i++) {
    var index = this.mapCode(text.charCodeAt(i));
    var metrics = this.getHorizontalMetrics(index);
    var kern = this.nextKern(index);
    this.log("Metrics for %s code %s index %s: %s %s kern: %s,%s", text.charAt(i),
    text.charCodeAt(i), index, metrics.advanceWidth, metrics.leftSideBearing,
    kern.x, kern.y);
    this.drawGlyph(ctx, index, x + kern.x,//- metrics.leftSideBearing,
    y + kern.y);

    x += metrics.advanceWidth;
    }
    ctx.restore();
    }

    drawSingleGlyph(ctx: CanvasRenderingContext2D, glyphIndex: number,
    x: number, y: number, size: number) {
    ctx.save();
    ctx.translate(x, y);
    this.transform(ctx, size);
    this.drawGlyph(ctx, glyphIndex, 0, 0);
    ctx.restore();
    }

    mapCode(charCode: number) {
    var index = 0;
    for (var i = 0; i < this.cmaps.length; i++) {
    var cmap = this.cmaps[i];
    index = cmap.map(charCode);
    if (index) {
    break;
    }
    }
    return index;
    }

    resetKern() {
    for (var i = 0; i < this.kern.length; i++) {
    this.kern[i].reset();
    }
    }

    nextKern(glyphIndex: number) {
    var pt, x = 0, y = 0;
    for (var i = 0; i < this.kern.length; i++) {
    pt = this.kern[i].get(glyphIndex);
    x += pt.x;
    y += pt.y;
    }
    return { x: x, y: y };
    }
    }