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$$1(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$$1(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) { 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 = 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); 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. cmd = { type: 'C', x: cmd.x, y: cmd.y, x1: _13 * x + _23 * cmd.x1, y1: _13 * y + _23 * cmd.y1, x2: _13 * cmd.x + _23 * cmd.x1, y2: _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. // https://www.microsoft.com/typography/OTSPEC/head.htm // 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. // https://www.microsoft.com/typography/OTSPEC/hhea.htm // 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. // https://www.microsoft.com/typography/OTSPEC/hmtx.htm // 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(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 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 // languages for which TrueType does not assign a numeric code. // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ltag.html // http://www.w3.org/International/articles/language-tags/ // http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry 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. // We need it just to get the number of glyphs in the font. // https://www.microsoft.com/typography/OTSPEC/maxp.htm // 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. // https://www.microsoft.com/typography/OTSPEC/name.htm // 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. // https://www.microsoft.com/typography/OTSPEC/os2.htm 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. // https://www.microsoft.com/typography/OTSPEC/post.htm // 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. // https://www.microsoft.com/typography/OTSPEC/gsub.htm 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))); } check.fail('Lookup type 1 substFormat must be 1 or 2.'); }; 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)) ); })); }))); }; 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. // https://www.microsoft.com/typography/OTSPEC/gpos.htm // 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 `sfnt` wrapper provides organization for the tables in the font. // It is the top-level data structure in a font. // https://www.microsoft.com/typography/OTSPEC/otff.htm // Recommendations for creating OpenType Fonts: // http://www.microsoft.com/typography/otspec140/recom.htm 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