// opentype.js // https://github.com/opentypejs/opentype.js // (c) 2015 Frederik De Bleser // opentype.js may be freely distributed under the MIT license. /* global DataView, Uint8Array */ import { inflateSync } from 'fflate'; import Font from './font'; import Glyph from './glyph'; import { CmapEncoding, addGlyphNames } from './encoding'; import parse from './parse'; import Path from './path'; import cmap from './tables/cmap'; import cff from './tables/cff'; import fvar from './tables/fvar'; import glyf from './tables/glyf'; import gdef from './tables/gdef'; import gpos from './tables/gpos'; import gsub from './tables/gsub'; import head from './tables/head'; import hhea from './tables/hhea'; import hmtx from './tables/hmtx'; import kern from './tables/kern'; import ltag from './tables/ltag'; import loca from './tables/loca'; import maxp from './tables/maxp'; import os2 from './tables/os2'; import post from './tables/post'; import meta from './tables/meta'; /** * The opentype library. * @namespace opentype */ // Table Directory Entries ////////////////////////////////////////////// /** * Parses OpenType table entries. * @param {DataView} * @param {Number} * @return {Object[]} */ function parseOpenTypeTableEntries(data, numTables) { const tableEntries = []; let p = 12; for (let i = 0; i < numTables; i += 1) { const tag = parse.getTag(data, p); const checksum = parse.getULong(data, p + 4); const offset = parse.getULong(data, p + 8); const length = parse.getULong(data, p + 12); tableEntries.push({ tag: tag, checksum: checksum, offset: offset, length: length, compression: false, }); p += 16; } return tableEntries; } /** * Parses WOFF table entries. * @param {DataView} * @param {Number} * @return {Object[]} */ function parseWOFFTableEntries(data, numTables) { const tableEntries = []; let p = 44; // offset to the first table directory entry. for (let i = 0; i < numTables; i += 1) { const tag = parse.getTag(data, p); const offset = parse.getULong(data, p + 4); const compLength = parse.getULong(data, p + 8); const origLength = parse.getULong(data, p + 12); let compression; if (compLength < origLength) { compression = 'WOFF'; } else { compression = false; } tableEntries.push({ tag: tag, offset: offset, compression: compression, compressedLength: compLength, length: origLength, }); p += 20; } return tableEntries; } /** * @typedef TableData * @type Object * @property {DataView} data - The DataView * @property {number} offset - The data offset. */ /** * @param {DataView} * @param {Object} * @return {TableData} */ function uncompressTable(data, tableEntry) { if (tableEntry.compression === 'WOFF') { const inBuffer = new Uint8Array( data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2 ); const outBuffer = new Uint8Array(tableEntry.length); inflateSync(inBuffer, outBuffer); if (outBuffer.byteLength !== tableEntry.length) { throw new Error( 'Decompression error: ' + tableEntry.tag + ` decompressed length doesn't match recorded length` ); } const view = new DataView(outBuffer.buffer, 0); return { data: view, offset: 0 }; } else { return { data: data, offset: tableEntry.offset }; } } // Public API /////////////////////////////////////////////////////////// /** * Parse the OpenType file data (as an ArrayBuffer) and return a Font object. * Throws an error if the font could not be parsed. * @param {ArrayBuffer} * @param {Object} opt - options for parsing * @return {opentype.Font} */ function parseBuffer(buffer, opt) { opt = opt === undefined || opt === null ? {} : opt; let indexToLocFormat; // Since the constructor can also be called to create new fonts from scratch, we indicate this // should be an empty font that we'll fill with our own data. const font = new Font({ empty: true }); // OpenType fonts use big endian byte ordering. // We can't rely on typed array view types, because they operate with the endianness of the host computer. // Instead we use DataViews where we can specify endianness. const data = new DataView(buffer, 0); let numTables; let tableEntries = []; const signature = parse.getTag(data, 0); if ( signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1' ) { font.outlinesFormat = 'truetype'; numTables = parse.getUShort(data, 4); tableEntries = parseOpenTypeTableEntries(data, numTables); } else if (signature === 'OTTO') { font.outlinesFormat = 'cff'; numTables = parse.getUShort(data, 4); tableEntries = parseOpenTypeTableEntries(data, numTables); } else if (signature === 'wOFF') { const flavor = parse.getTag(data, 4); if (flavor === String.fromCharCode(0, 1, 0, 0)) { font.outlinesFormat = 'truetype'; } else if (flavor === 'OTTO') { font.outlinesFormat = 'cff'; } else { throw new Error('Unsupported OpenType flavor ' + signature); } numTables = parse.getUShort(data, 12); tableEntries = parseWOFFTableEntries(data, numTables); } else { throw new Error('Unsupported OpenType signature ' + signature); } let cffTableEntry; let fvarTableEntry; let glyfTableEntry; let gdefTableEntry; let gposTableEntry; let gsubTableEntry; let hmtxTableEntry; let kernTableEntry; let locaTableEntry; let metaTableEntry; let p; for (let i = 0; i < numTables; i += 1) { const tableEntry = tableEntries[i]; let table; switch (tableEntry.tag) { case 'cmap': table = uncompressTable(data, tableEntry); font.tables.cmap = cmap.parse(table.data, table.offset); font.encoding = new CmapEncoding(font.tables.cmap); break; case 'cvt ': table = uncompressTable(data, tableEntry); p = new parse.Parser(table.data, table.offset); font.tables.cvt = p.parseShortList(tableEntry.length / 2); break; case 'fvar': fvarTableEntry = tableEntry; break; case 'fpgm': table = uncompressTable(data, tableEntry); p = new parse.Parser(table.data, table.offset); font.tables.fpgm = p.parseByteList(tableEntry.length); break; case 'head': table = uncompressTable(data, tableEntry); font.tables.head = head.parse(table.data, table.offset); font.unitsPerEm = font.tables.head.unitsPerEm; indexToLocFormat = font.tables.head.indexToLocFormat; break; case 'hhea': table = uncompressTable(data, tableEntry); font.tables.hhea = hhea.parse(table.data, table.offset); font.ascender = font.tables.hhea.ascender; font.descender = font.tables.hhea.descender; font.numberOfHMetrics = font.tables.hhea.numberOfHMetrics; break; case 'hmtx': hmtxTableEntry = tableEntry; break; case 'ltag': table = uncompressTable(data, tableEntry); ltagTable = ltag.parse(table.data, table.offset); break; case 'maxp': table = uncompressTable(data, tableEntry); font.tables.maxp = maxp.parse(table.data, table.offset); font.numGlyphs = font.tables.maxp.numGlyphs; break; case 'OS/2': table = uncompressTable(data, tableEntry); font.tables.os2 = os2.parse(table.data, table.offset); break; case 'post': table = uncompressTable(data, tableEntry); font.tables.post = post.parse(table.data, table.offset); break; case 'prep': table = uncompressTable(data, tableEntry); p = new parse.Parser(table.data, table.offset); font.tables.prep = p.parseByteList(tableEntry.length); break; case 'glyf': glyfTableEntry = tableEntry; break; case 'loca': locaTableEntry = tableEntry; break; case 'CFF ': cffTableEntry = tableEntry; break; case 'kern': kernTableEntry = tableEntry; break; case 'GDEF': gdefTableEntry = tableEntry; break; case 'GPOS': gposTableEntry = tableEntry; break; case 'GSUB': gsubTableEntry = tableEntry; break; case 'meta': metaTableEntry = tableEntry; break; } } if (glyfTableEntry && locaTableEntry) { const shortVersion = indexToLocFormat === 0; const locaTable = uncompressTable(data, locaTableEntry); const locaOffsets = loca.parse( locaTable.data, locaTable.offset, font.numGlyphs, shortVersion ); const glyfTable = uncompressTable(data, glyfTableEntry); font.glyphs = glyf.parse( glyfTable.data, glyfTable.offset, locaOffsets, font, opt ); } else if (cffTableEntry) { const cffTable = uncompressTable(data, cffTableEntry); cff.parse(cffTable.data, cffTable.offset, font, opt); } else { throw new Error(`Font doesn't contain TrueType or CFF outlines.`); } const hmtxTable = uncompressTable(data, hmtxTableEntry); hmtx.parse( font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt ); addGlyphNames(font, opt); if (kernTableEntry) { const kernTable = uncompressTable(data, kernTableEntry); font.kerningPairs = kern.parse(kernTable.data, kernTable.offset); } else { font.kerningPairs = {}; } if (gdefTableEntry) { const gdefTable = uncompressTable(data, gdefTableEntry); font.tables.gdef = gdef.parse(gdefTable.data, gdefTable.offset); } if (gposTableEntry) { const gposTable = uncompressTable(data, gposTableEntry); font.tables.gpos = gpos.parse(gposTable.data, gposTable.offset); font.position.init(); } if (gsubTableEntry) { const gsubTable = uncompressTable(data, gsubTableEntry); font.tables.gsub = gsub.parse(gsubTable.data, gsubTable.offset); } if (fvarTableEntry) { const fvarTable = uncompressTable(data, fvarTableEntry); font.tables.fvar = fvar.parse( fvarTable.data, fvarTable.offset, font.names ); } if (metaTableEntry) { const metaTable = uncompressTable(data, metaTableEntry); font.tables.meta = meta.parse(metaTable.data, metaTable.offset); font.metas = font.tables.meta; } return font; } function load() {} function loadSync() {} export { Font, Glyph, Path, parse as _parse, parseBuffer as parse, load, loadSync, };