UNPKG

opentype.features.js

Version:

Library to list opentype fonts optional features.

564 lines (563 loc) 28.4 kB
"use strict"; // (c) 2015 Frederik De Bleser // opentype.js may be freely distributed under the MIT license. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseBuffer = exports.load = exports.Font = void 0; /* global DataView, Uint8Array, XMLHttpRequest */ var pako_1 = require("pako"); var font_1 = __importDefault(require("./font")); exports.Font = font_1.default; var encoding_1 = require("./encoding"); var parse_1 = __importDefault(require("./parse")); var util_1 = require("./util"); var cmap_1 = __importDefault(require("./tables/cmap")); var gpos_1 = __importDefault(require("./tables/gpos")); var gsub_1 = __importDefault(require("./tables/gsub")); var head_1 = __importDefault(require("./tables/head")); var ltag_1 = __importDefault(require("./tables/ltag")); var name_1 = __importDefault(require("./tables/name")); /** * The opentype library. * @namespace opentype */ // File loaders ///////////////////////////////////////////////////////// /** * Loads a font from a file. The callback throws an error message as the first parameter if it fails * and the font as an ArrayBuffer in the second parameter if it succeeds. * @param {string} path - The path of the file * @param {Function} callback - The function to call when the font load completes */ function loadFromFile(path, callback) { return __awaiter(this, void 0, void 0, function () { var fs; return __generator(this, function (_a) { if ((0, util_1.isBrowser)()) return [2 /*return*/, null]; fs = eval('require("fs")'); if (!fs) return [2 /*return*/, null]; fs.readFile(path, function (err, buffer) { if (err) { return callback(err.message); } callback(null, (0, util_1.nodeBufferToArrayBuffer)(buffer)); }); return [2 /*return*/]; }); }); } /** * Loads a font from a URL. The callback throws an error message as the first parameter if it fails * and the font as an ArrayBuffer in the second parameter if it succeeds. * @param {string} url - The URL of the font file. * @param {Function} callback - The function to call when the font load completes */ function loadFromUrl(url, callback) { return __awaiter(this, void 0, void 0, function () { var request, xh2; return __generator(this, function (_a) { switch (_a.label) { case 0: request = null; if (!(0, util_1.isNode)()) return [3 /*break*/, 2]; return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require('xhr2')); })]; case 1: xh2 = _a.sent(); request = new xh2.XMLHttpRequest(); return [3 /*break*/, 3]; case 2: request = new XMLHttpRequest(); _a.label = 3; case 3: request.open('get', url, true); request.responseType = 'arraybuffer'; request.onload = function () { if (request.response) { return callback(null, request.response); } else { return callback('Font could not be loaded: ' + request.statusText); } }; request.onerror = function (e) { callback('Font could not be loaded ' + e); }; request.send(); return [2 /*return*/]; } }); }); } // Table Directory Entries ////////////////////////////////////////////// /** * Parses OpenType table entries. * @param {DataView} * @param {Number} * @return {Object[]} */ function parseOpenTypeTableEntries(data, numTables) { var tableEntries = []; var p = 12; for (var i = 0; i < numTables; i += 1) { var tag = parse_1.default.getTag(data, p); var checksum = parse_1.default.getULong(data, p + 4); var offset = parse_1.default.getULong(data, p + 8); var length_1 = parse_1.default.getULong(data, p + 12); tableEntries.push({ tag: tag, checksum: checksum, offset: offset, length: length_1, compression: false }); p += 16; } return tableEntries; } /** * Parses WOFF table entries. * @param {DataView} * @param {Number} * @return {Object[]} */ function parseWOFFTableEntries(data, numTables) { var tableEntries = []; var p = 44; // offset to the first table directory entry. for (var i = 0; i < numTables; i += 1) { var tag = parse_1.default.getTag(data, p); var offset = parse_1.default.getULong(data, p + 4); var compLength = parse_1.default.getULong(data, p + 8); var origLength = parse_1.default.getULong(data, p + 12); var compression = void 0; if (compLength < origLength) { compression = 'WOFF'; } else { compression = false; } tableEntries.push({ tag: tag, offset: offset, compression: compression, compressedLength: compLength, length: origLength }); p += 20; } return tableEntries; } // see https://www.w3.org/TR/WOFF2 function lookupTag(value) { if (value >= 63) throw 'Should be looking at tag field'; var lookupValues = [ 'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'OS/2', 'post', 'cvt ', 'fpgm', 'glyf', 'loca', 'prep', 'CFF ', 'VORG', 'EBDT', 'EBLC', 'gasp', 'hdmx', 'kern', 'LTSH', 'PCLT', 'VDMX', 'vhea', 'vmtx', 'BASE', 'GDEF', 'GPOS', 'GSUB', 'EBSC', 'JSTF', 'MATH', 'CBDT', 'CBLC', 'COLR', 'CPAL', 'SVG ', 'sbix', 'acnt', 'avar', 'bdat', 'bloc', 'bsln', 'cvar', 'fdsc', 'feat', 'fmtx', 'fvar', 'gvar', 'hsty', 'just', 'lcar', 'mort', 'morx', 'opbd', 'prop', 'trak', 'Zapf', 'Silf', 'Glat', 'Gloc', 'Feat', 'Sill' ]; return lookupValues[value]; } /** * Parses WOFF2 table entries. * @param {DataView} * @param {Number} */ function parseWOFF2TableEntries(data, numTables) { var tableEntries = []; // Offset to the start of table directory entries. var p = 48; var sumOfOrigLengths = 0; for (var i = 0; i < numTables; i += 1) { var flag = parse_1.default.getByte(data, p); // https://www.w3.org/TR/WOFF2/ // "If bits [0..5] of the flags byte have the value 63 (0x3f), // then following the flag byte is a 4-byte arbitrary tag value. // Otherwise, the tag field is omitted in the TableDirectoryEntry structure" var hasTagField = (flag & 0x3f) === 63; // For all tables in a font, except for 'glyf' and 'loca' tables, transformation version 0 indicates // the null transform where the original table data is passed directly to the Brotli // compressor for inclusion in the compressed data stream. For 'glyf' and 'loca' tables, transformation version 3 indicates the null transform var version = (flag & 0xc0); if (version < 0 || version > 3) throw new Error('Invalid version bits in flag'); p += 1; var tag = hasTagField ? parse_1.default.getTag(data, p) : lookupTag(flag & 0x3f); if (hasTagField) p += 4; var origLengthField = parse_1.default.getVLUInt(data, p); var origLength = origLengthField.val; p += origLengthField.length; var transformLength = 0; // The transformLength field is present in the table directory entry if, and only if, the table has been processed by a non-null transform prior // to Brotli compression. For tables that are not transformed, no transformLength field is present in the directory entry. var skipTransformLength = false; if (tag === 'glyf' || tag === 'loca') skipTransformLength = version === 3; else skipTransformLength = version === 0; if (!skipTransformLength) { var transformLengthField = parse_1.default.getVLUInt(data, p); transformLength = transformLengthField.val; p += transformLengthField.length; } // The 'loca' table doesn't appear to be included in the compressed data block. if (tag === 'loca') continue; // The sum of the origLength (for non-transformed tables) and transformLength (for transformed tables) fields in the table directory MUST equal // the size of the font data block after it has been decompressed using [Brotli] sumOfOrigLengths += (transformLength ? transformLength : origLength); //sumOfOrigLengths += (transformLength ? transformLength : origLength); tableEntries.push({ tag: tag, compressedLength: transformLength, length: origLength }); } // Add offset fields to the tableEntries. // Assuming CompressedFontData start directly after TableDirectory (don't support the optional CollectionDirectory table yet) var offset = 0; for (var _i = 0, tableEntries_1 = tableEntries; _i < tableEntries_1.length; _i++) { var table = tableEntries_1[_i]; table.offset = offset; offset += (table.compressedLength ? table.compressedLength : table.length); } return { tableEntries: tableEntries, tableDirectorySize: p - 48, totalLength: sumOfOrigLengths }; } /** * @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) { return __awaiter(this, void 0, void 0, function () { var inBuffer, outBuffer, view; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!(tableEntry.compression === 'WOFF')) return [3 /*break*/, 2]; inBuffer = new Uint8Array(data.buffer, tableEntry.offset, tableEntry.compressedLength); return [4 /*yield*/, (0, pako_1.inflate)(inBuffer)]; case 1: outBuffer = _a.sent(); if (outBuffer.byteLength !== tableEntry.length) { throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length'); } view = new DataView(outBuffer.buffer, 0); return [2 /*return*/, { data: view, offset: 0 }]; case 2: return [2 /*return*/, { data: data, offset: tableEntry.offset }]; } }); }); } function parseCommonWoffHeaders(data, signature) { var flavor = parse_1.default.getTag(data, 4); var outlinesFormat = null; if (flavor === String.fromCharCode(0, 1, 0, 0)) { outlinesFormat = 'truetype'; } else if (flavor === 'OTTO') { outlinesFormat = 'cff'; } else if (flavor === 'ttcf' && signature === 'wOF2') { throw new Error('Don\'t yet support the Collection directory'); // see https://www.w3.org/TR/WOFF2/#collection_dir_format } else { throw new Error("Unsupported OpenType flavor ".concat(flavor, " for ").concat(signature)); } var numTables = parse_1.default.getUShort(data, 12); return { outlinesFormat: outlinesFormat, numTables: numTables }; } // 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) { return __awaiter(this, void 0, void 0, function () { var indexToLocFormat, ltagTable, font, data, numTables, tableEntries, signature, wOFFcommonHeaders, wOFFcommonHeaders, totalCompressedSize, wOFF2TableEntries, inBuffer, outBuffer, decompress, e_1, newDataBuffer, gposTableEntry, gsubTableEntry, nameTableEntry, p, i, tableEntry, table, _a, nameTable, gposTable, gsubTable; return __generator(this, function (_b) { switch (_b.label) { case 0: opt = (opt === undefined || opt === null) ? {} : opt; font = new font_1.default({ empty: true }); data = new DataView(buffer, 0); tableEntries = []; signature = parse_1.default.getTag(data, 0); if (!(signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1')) return [3 /*break*/, 1]; font.outlinesFormat = 'truetype'; numTables = parse_1.default.getUShort(data, 4); tableEntries = parseOpenTypeTableEntries(data, numTables); return [3 /*break*/, 9]; case 1: if (!(signature === 'OTTO')) return [3 /*break*/, 2]; font.outlinesFormat = 'cff'; numTables = parse_1.default.getUShort(data, 4); tableEntries = parseOpenTypeTableEntries(data, numTables); return [3 /*break*/, 9]; case 2: if (!(signature === 'wOFF')) return [3 /*break*/, 3]; wOFFcommonHeaders = parseCommonWoffHeaders(data); font.outlinesFormat = wOFFcommonHeaders.outlinesFormat; numTables = wOFFcommonHeaders.numTables; tableEntries = parseWOFFTableEntries(data, numTables); return [3 /*break*/, 9]; case 3: if (!(signature === 'wOF2')) return [3 /*break*/, 8]; wOFFcommonHeaders = parseCommonWoffHeaders(data); font.outlinesFormat = wOFFcommonHeaders.outlinesFormat; numTables = wOFFcommonHeaders.numTables; totalCompressedSize = parse_1.default.getULong(data, 20); wOFF2TableEntries = parseWOFF2TableEntries(data, numTables); tableEntries = wOFF2TableEntries.tableEntries; // Recalculate numTables, as loca table, if present, isn't included in the table compressed data block. numTables = tableEntries.length; inBuffer = new Uint8Array(data.buffer, 48 + wOFF2TableEntries.tableDirectorySize, totalCompressedSize); outBuffer = null; _b.label = 4; case 4: _b.trys.push([4, 6, , 7]); return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require('brotli/decompress')); })]; case 5: decompress = (_b.sent()).default; outBuffer = decompress(inBuffer); return [3 /*break*/, 7]; case 6: e_1 = _b.sent(); console.log(e_1); throw e_1; case 7: // The sum of the origLength (for non-transformed tables) and transformLength (for transformed tables) fields in the table directory MUST equal the size of the font data block after it has been decompressed using [Brotli].If the size of the decompressed font data block doesn't match the sum of lengths defined in the table directory as described above, the WOFF2 file MUST NOT be loaded. if (outBuffer.byteLength !== wOFF2TableEntries.totalLength) { throw new Error('Decompressed length does not equal sum of table lengths'); } newDataBuffer = new Uint8Array(__spreadArray(__spreadArray(__spreadArray([], data.buffer.slice(0, 48 + wOFF2TableEntries.tableDirectorySize), true), outBuffer, true), data.buffer.slice(48 + wOFF2TableEntries.tableDirectorySize, totalCompressedSize), true)); data = new DataView(newDataBuffer.buffer, 0); return [3 /*break*/, 9]; case 8: throw new Error('Unsupported OpenType signature ' + signature); case 9: i = 0; _b.label = 10; case 10: if (!(i < numTables)) return [3 /*break*/, 27]; tableEntry = tableEntries[i]; table = void 0; _a = tableEntry.tag.toLowerCase(); switch (_a) { case 'cmap': return [3 /*break*/, 11]; case 'cvt ': return [3 /*break*/, 13]; case 'fpgm': return [3 /*break*/, 15]; case 'head': return [3 /*break*/, 17]; case 'ltag': return [3 /*break*/, 19]; case 'name': return [3 /*break*/, 21]; case 'prep': return [3 /*break*/, 22]; case 'gpos': return [3 /*break*/, 24]; case 'gsub': return [3 /*break*/, 25]; } return [3 /*break*/, 26]; case 11: return [4 /*yield*/, uncompressTable(data, tableEntry)]; case 12: table = _b.sent(); font.tables.cmap = cmap_1.default.parse(table.data, table.offset); font.encoding = new encoding_1.CmapEncoding(font.tables.cmap); return [3 /*break*/, 26]; case 13: return [4 /*yield*/, uncompressTable(data, tableEntry)]; case 14: table = _b.sent(); p = new parse_1.default.Parser(table.data, table.offset); font.tables.cvt = p.parseShortList(tableEntry.length / 2); return [3 /*break*/, 26]; case 15: return [4 /*yield*/, uncompressTable(data, tableEntry)]; case 16: table = _b.sent(); p = new parse_1.default.Parser(table.data, table.offset); font.tables.fpgm = p.parseByteList(tableEntry.length); return [3 /*break*/, 26]; case 17: return [4 /*yield*/, uncompressTable(data, tableEntry)]; case 18: table = _b.sent(); font.tables.head = head_1.default.parse(table.data, table.offset); font.unitsPerEm = font.tables.head.unitsPerEm; indexToLocFormat = font.tables.head.indexToLocFormat; return [3 /*break*/, 26]; case 19: return [4 /*yield*/, uncompressTable(data, tableEntry)]; case 20: table = _b.sent(); ltagTable = ltag_1.default.parse(table.data, table.offset); return [3 /*break*/, 26]; case 21: nameTableEntry = tableEntry; return [3 /*break*/, 26]; case 22: return [4 /*yield*/, uncompressTable(data, tableEntry)]; case 23: table = _b.sent(); p = new parse_1.default.Parser(table.data, table.offset); font.tables.prep = p.parseByteList(tableEntry.length); return [3 /*break*/, 26]; case 24: gposTableEntry = tableEntry; return [3 /*break*/, 26]; case 25: gsubTableEntry = tableEntry; return [3 /*break*/, 26]; case 26: i += 1; return [3 /*break*/, 10]; case 27: return [4 /*yield*/, uncompressTable(data, nameTableEntry)]; case 28: nameTable = _b.sent(); font.tables.name = name_1.default.parse(nameTable.data, nameTable.offset, ltagTable); font.names = font.tables.name; if (!gposTableEntry) return [3 /*break*/, 30]; return [4 /*yield*/, uncompressTable(data, gposTableEntry)]; case 29: gposTable = _b.sent(); font.tables.gpos = gpos_1.default.parse(gposTable.data, gposTable.offset); font.position.init(); _b.label = 30; case 30: if (!gsubTableEntry) return [3 /*break*/, 32]; return [4 /*yield*/, uncompressTable(data, gsubTableEntry)]; case 31: gsubTable = _b.sent(); font.tables.gsub = gsub_1.default.parse(gsubTable.data, gsubTable.offset); _b.label = 32; case 32: return [2 /*return*/, font]; } }); }); } exports.parseBuffer = parseBuffer; /** * Asynchronously load the font from a URL or a filesystem. When done, call the callback * with two arguments `(err, font)`. The `err` will be null on success, * the `font` is a Font object. * We use the node.js callback convention so that * opentype.js can integrate with frameworks like async.js. * @alias opentype.load * @param {string} url - The URL of the font to load. * @param {Function} callback - The callback. */ function load(url, callback, opt) { var _a; opt = (opt === undefined || opt === null) ? {} : opt; var isNode = typeof window === 'undefined' || ((_a = window === null || window === void 0 ? void 0 : window.process) === null || _a === void 0 ? void 0 : _a.type) === 'renderer'; var loadFn = isNode && !opt.isUrl ? loadFromFile : loadFromUrl; return new Promise(function (resolve, reject) { loadFn(url, function (err, arrayBuffer) { return __awaiter(this, void 0, void 0, function () { var font, e_2; return __generator(this, function (_a) { switch (_a.label) { case 0: if (err) { if (callback) { return [2 /*return*/, callback(err)]; } else { reject(err); } } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, parseBuffer(arrayBuffer, opt)]; case 2: font = _a.sent(); return [3 /*break*/, 4]; case 3: e_2 = _a.sent(); if (callback) { return [2 /*return*/, callback(e_2, null)]; } else { reject(e_2); } return [3 /*break*/, 4]; case 4: if (callback) { return [2 /*return*/, callback(null, font)]; } else { resolve(font); } return [2 /*return*/]; } }); }); }); }); } exports.load = load;