UNPKG

opentype.features.js

Version:

Library to list opentype fonts optional features.

612 lines (611 loc) 21.7 kB
"use strict"; // Parsing utility functions var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Parser = void 0; var check_1 = __importDefault(require("./check")); // Retrieve an unsigned byte from the DataView. function getByte(dataView, offset) { return dataView.getUint8(offset); } // Retrieve an unsigned 16-bit short from the DataView. // The value is stored in big endian. function getUShort(dataView, offset) { return dataView.getUint16(offset, false); } // Retrieve a signed 16-bit short from the DataView. // The value is stored in big endian. function getShort(dataView, offset) { return dataView.getInt16(offset, false); } // Retrieve an unsigned 32-bit long from the DataView. // The value is stored in big endian. function getULong(dataView, offset) { return dataView.getUint32(offset, false); } // Retrieve a 32-bit signed fixed-point number (16.16) from the DataView. // The value is stored in big endian. function getFixed(dataView, offset) { var decimal = dataView.getInt16(offset, false); var fraction = dataView.getUint16(offset + 2, false); return decimal + fraction / 65535; } // Retrieve a 4-character tag from the DataView. // Tags are used to identify tables. function getTag(dataView, offset) { var tag = ''; for (var i = offset; i < offset + 4; i += 1) { tag += String.fromCharCode(dataView.getInt8(i)); } return tag; } function getVLUInt(dataView, offset) { var val = 0; for (var i = 0; i < 5; i++) { var b = dataView.getUint8(offset + i); if (i == 0 && b === 0x80) return null; if (val & 0xFE000000) return null; val = val << 7 | b & 0x7f; if ((b & 0x80) == 0) return { val: val, length: i + 1 }; } return null; } // Retrieve an offset from the DataView. // Offsets are 1 to 4 bytes in length, depending on the offSize argument. function getOffset(dataView, offset, offSize) { var v = 0; for (var i = 0; i < offSize; i += 1) { v <<= 8; v += dataView.getUint8(offset + i); } return v; } // Retrieve a number of bytes from start offset to the end offset from the DataView. function getBytes(dataView, startOffset, endOffset) { var bytes = []; for (var i = startOffset; i < endOffset; i += 1) { bytes.push(dataView.getUint8(i)); } return bytes; } // Convert the list of bytes to a string. function bytesToString(bytes) { var s = ''; for (var i = 0; i < bytes.length; i += 1) { s += String.fromCharCode(bytes[i]); } return s; } var typeOffsets = { byte: 1, uShort: 2, short: 2, uLong: 4, fixed: 4, longDateTime: 8, tag: 4 }; // A stateful parser that changes the offset whenever a value is retrieved. // The data is a DataView. function Parser(data, offset) { this.data = data; this.offset = offset; this.relativeOffset = 0; } exports.Parser = Parser; Parser.prototype.parseByte = function () { var v = this.data.getUint8(this.offset + this.relativeOffset); this.relativeOffset += 1; return v; }; Parser.prototype.parseChar = function () { var v = this.data.getInt8(this.offset + this.relativeOffset); this.relativeOffset += 1; return v; }; Parser.prototype.parseCard8 = Parser.prototype.parseByte; Parser.prototype.parseUShort = function () { var v = this.data.getUint16(this.offset + this.relativeOffset); this.relativeOffset += 2; return v; }; Parser.prototype.parseCard16 = Parser.prototype.parseUShort; Parser.prototype.parseSID = Parser.prototype.parseUShort; Parser.prototype.parseOffset16 = Parser.prototype.parseUShort; Parser.prototype.parseShort = function () { var v = this.data.getInt16(this.offset + this.relativeOffset); this.relativeOffset += 2; return v; }; Parser.prototype.parseF2Dot14 = function () { var v = this.data.getInt16(this.offset + this.relativeOffset) / 16384; this.relativeOffset += 2; return v; }; Parser.prototype.parseULong = function () { var v = getULong(this.data, this.offset + this.relativeOffset); this.relativeOffset += 4; return v; }; Parser.prototype.parseOffset32 = Parser.prototype.parseULong; Parser.prototype.parseFixed = function () { var v = getFixed(this.data, this.offset + this.relativeOffset); this.relativeOffset += 4; return v; }; Parser.prototype.parseString = function (length) { var dataView = this.data; var offset = this.offset + this.relativeOffset; var string = ''; this.relativeOffset += length; for (var i = 0; i < length; i++) { string += String.fromCharCode(dataView.getUint8(offset + i)); } return string; }; Parser.prototype.parseTag = function () { return this.parseString(4); }; // LONGDATETIME is a 64-bit integer. // JavaScript and unix timestamps traditionally use 32 bits, so we // only take the last 32 bits. // + Since until 2038 those bits will be filled by zeros we can ignore them. Parser.prototype.parseLongDateTime = function () { var v = getULong(this.data, this.offset + this.relativeOffset + 4); // Subtract seconds between 01/01/1904 and 01/01/1970 // to convert Apple Mac timestamp to Standard Unix timestamp v -= 2082844800; this.relativeOffset += 8; return v; }; Parser.prototype.parseVersion = function (minorBase) { var major = getUShort(this.data, this.offset + this.relativeOffset); // How to interpret the minor version is very vague in the spec. 0x5000 is 5, 0x1000 is 1 // Default returns the correct number if minor = 0xN000 where N is 0-9 // Set minorBase to 1 for tables that use minor = N where N is 0-9 var minor = getUShort(this.data, this.offset + this.relativeOffset + 2); this.relativeOffset += 4; if (minorBase === undefined) minorBase = 0x1000; return major + minor / minorBase / 10; }; Parser.prototype.skip = function (type, amount) { if (amount === undefined) { amount = 1; } this.relativeOffset += typeOffsets[type] * amount; }; ///// Parsing lists and records /////////////////////////////// // Parse a list of 32 bit unsigned integers. Parser.prototype.parseULongList = function (count) { if (count === undefined) { count = this.parseULong(); } var offsets = new Array(count); var dataView = this.data; var offset = this.offset + this.relativeOffset; for (var i = 0; i < count; i++) { offsets[i] = dataView.getUint32(offset); offset += 4; } this.relativeOffset += count * 4; return offsets; }; // Parse a list of 16 bit unsigned integers. The length of the list can be read on the stream // or provided as an argument. Parser.prototype.parseOffset16List = Parser.prototype.parseUShortList = function (count) { if (count === undefined) { count = this.parseUShort(); } var offsets = new Array(count); var dataView = this.data; var offset = this.offset + this.relativeOffset; for (var i = 0; i < count; i++) { offsets[i] = dataView.getUint16(offset); offset += 2; } this.relativeOffset += count * 2; return offsets; }; // Parses a list of 16 bit signed integers. Parser.prototype.parseShortList = function (count) { var list = new Array(count); var dataView = this.data; var offset = this.offset + this.relativeOffset; for (var i = 0; i < count; i++) { list[i] = dataView.getInt16(offset); offset += 2; } this.relativeOffset += count * 2; return list; }; // Parses a list of bytes. Parser.prototype.parseByteList = function (count) { var list = new Array(count); var dataView = this.data; var offset = this.offset + this.relativeOffset; for (var i = 0; i < count; i++) { list[i] = dataView.getUint8(offset++); } this.relativeOffset += count; return list; }; /** * Parse a list of items. * Record count is optional, if omitted it is read from the stream. * itemCallback is one of the Parser methods. */ Parser.prototype.parseList = function (count, itemCallback) { if (!itemCallback) { itemCallback = count; count = this.parseUShort(); } var list = new Array(count); for (var i = 0; i < count; i++) { list[i] = itemCallback.call(this); } return list; }; Parser.prototype.parseList32 = function (count, itemCallback) { if (!itemCallback) { itemCallback = count; count = this.parseULong(); } var list = new Array(count); for (var i = 0; i < count; i++) { list[i] = itemCallback.call(this); } return list; }; /** * Parse a list of records. * Record count is optional, if omitted it is read from the stream. * Example of recordDescription: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort } */ Parser.prototype.parseRecordList = function (count, recordDescription) { // If the count argument is absent, read it in the stream. if (!recordDescription) { recordDescription = count; count = this.parseUShort(); } var records = new Array(count); var fields = Object.keys(recordDescription); for (var i = 0; i < count; i++) { var rec = {}; for (var j = 0; j < fields.length; j++) { var fieldName = fields[j]; var fieldType = recordDescription[fieldName]; rec[fieldName] = fieldType.call(this); } records[i] = rec; } return records; }; Parser.prototype.parseRecordList32 = function (count, recordDescription) { // If the count argument is absent, read it in the stream. if (!recordDescription) { recordDescription = count; count = this.parseULong(); } var records = new Array(count); var fields = Object.keys(recordDescription); for (var i = 0; i < count; i++) { var rec = {}; for (var j = 0; j < fields.length; j++) { var fieldName = fields[j]; var fieldType = recordDescription[fieldName]; rec[fieldName] = fieldType.call(this); } records[i] = rec; } return records; }; // Parse a data structure into an object // Example of description: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort } Parser.prototype.parseStruct = function (description) { if (typeof description === 'function') { return description.call(this); } else { var fields = Object.keys(description); var struct = {}; for (var j = 0; j < fields.length; j++) { var fieldName = fields[j]; var fieldType = description[fieldName]; struct[fieldName] = fieldType.call(this); } return struct; } }; /** * Parse a GPOS valueRecord * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record * valueFormat is optional, if omitted it is read from the stream. */ Parser.prototype.parseValueRecord = function (valueFormat) { if (valueFormat === undefined) { valueFormat = this.parseUShort(); } if (valueFormat === 0) { // valueFormat2 in kerning pairs is most often 0 // in this case return undefined instead of an empty object, to save space return; } var valueRecord = {}; if (valueFormat & 0x0001) { valueRecord.xPlacement = this.parseShort(); } if (valueFormat & 0x0002) { valueRecord.yPlacement = this.parseShort(); } if (valueFormat & 0x0004) { valueRecord.xAdvance = this.parseShort(); } if (valueFormat & 0x0008) { valueRecord.yAdvance = this.parseShort(); } // Device table (non-variable font) / VariationIndex table (variable font) not supported // https://docs.microsoft.com/fr-fr/typography/opentype/spec/chapter2#devVarIdxTbls if (valueFormat & 0x0010) { valueRecord.xPlaDevice = undefined; this.parseShort(); } if (valueFormat & 0x0020) { valueRecord.yPlaDevice = undefined; this.parseShort(); } if (valueFormat & 0x0040) { valueRecord.xAdvDevice = undefined; this.parseShort(); } if (valueFormat & 0x0080) { valueRecord.yAdvDevice = undefined; this.parseShort(); } return valueRecord; }; /** * Parse a list of GPOS valueRecords * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record * valueFormat and valueCount are read from the stream. */ Parser.prototype.parseValueRecordList = function () { var valueFormat = this.parseUShort(); var valueCount = this.parseUShort(); var values = new Array(valueCount); for (var i = 0; i < valueCount; i++) { values[i] = this.parseValueRecord(valueFormat); } return values; }; Parser.prototype.parsePointer = function (description) { var structOffset = this.parseOffset16(); if (structOffset > 0) { // NULL offset => return undefined return new Parser(this.data, this.offset + structOffset).parseStruct(description); } return undefined; }; Parser.prototype.parsePointer32 = function (description) { var structOffset = this.parseOffset32(); if (structOffset > 0) { // NULL offset => return undefined return new Parser(this.data, this.offset + structOffset).parseStruct(description); } return undefined; }; /** * Parse a list of offsets to lists of 16-bit integers, * or a list of offsets to lists of offsets to any kind of items. * If itemCallback is not provided, a list of list of UShort is assumed. * If provided, itemCallback is called on each item and must parse the item. * See examples in tables/gsub.js */ Parser.prototype.parseListOfLists = function (itemCallback) { var offsets = this.parseOffset16List(); var count = offsets.length; var relativeOffset = this.relativeOffset; var list = new Array(count); for (var i = 0; i < count; i++) { var start = offsets[i]; if (start === 0) { // NULL offset // Add i as owned property to list. Convenient with assert. list[i] = undefined; continue; } this.relativeOffset = start; if (itemCallback) { var subOffsets = this.parseOffset16List(); var subList = new Array(subOffsets.length); for (var j = 0; j < subOffsets.length; j++) { this.relativeOffset = start + subOffsets[j]; subList[j] = itemCallback.call(this); } list[i] = subList; } else { list[i] = this.parseUShortList(); } } this.relativeOffset = relativeOffset; return list; }; ///// Complex tables parsing ////////////////////////////////// // Parse a coverage table in a GSUB, GPOS or GDEF table. // https://www.microsoft.com/typography/OTSPEC/chapter2.htm // parser.offset must point to the start of the table containing the coverage. Parser.prototype.parseCoverage = function () { var startOffset = this.offset + this.relativeOffset; var format = this.parseUShort(); var count = this.parseUShort(); if (format === 1) { return { format: 1, glyphs: this.parseUShortList(count) }; } else if (format === 2) { var ranges = new Array(count); for (var i = 0; i < count; i++) { ranges[i] = { start: this.parseUShort(), end: this.parseUShort(), index: this.parseUShort() }; } return { format: 2, ranges: ranges }; } throw new Error('0x' + startOffset.toString(16) + ': Coverage format must be 1 or 2.'); }; // Parse a Class Definition Table in a GSUB, GPOS or GDEF table. // https://www.microsoft.com/typography/OTSPEC/chapter2.htm Parser.prototype.parseClassDef = function () { var startOffset = this.offset + this.relativeOffset; var format = this.parseUShort(); if (format === 1) { return { format: 1, startGlyph: this.parseUShort(), classes: this.parseUShortList() }; } else if (format === 2) { return { format: 2, ranges: this.parseRecordList({ start: Parser.uShort, end: Parser.uShort, classId: Parser.uShort }) }; } throw new Error('0x' + startOffset.toString(16) + ': ClassDef format must be 1 or 2.'); }; ///// Static methods /////////////////////////////////// // These convenience methods can be used as callbacks and should be called with "this" context set to a Parser instance. Parser.list = function (count, itemCallback) { return function () { return this.parseList(count, itemCallback); }; }; Parser.list32 = function (count, itemCallback) { return function () { return this.parseList32(count, itemCallback); }; }; Parser.recordList = function (count, recordDescription) { return function () { return this.parseRecordList(count, recordDescription); }; }; Parser.recordList32 = function (count, recordDescription) { return function () { return this.parseRecordList32(count, recordDescription); }; }; Parser.pointer = function (description) { return function () { return this.parsePointer(description); }; }; Parser.pointer32 = function (description) { return function () { return this.parsePointer32(description); }; }; Parser.tag = Parser.prototype.parseTag; Parser.byte = Parser.prototype.parseByte; Parser.uShort = Parser.offset16 = Parser.prototype.parseUShort; Parser.uShortList = Parser.prototype.parseUShortList; Parser.uLong = Parser.offset32 = Parser.prototype.parseULong; Parser.uLongList = Parser.prototype.parseULongList; Parser.struct = Parser.prototype.parseStruct; Parser.coverage = Parser.prototype.parseCoverage; Parser.classDef = Parser.prototype.parseClassDef; ///// Script, Feature, Lookup lists /////////////////////////////////////////////// // https://www.microsoft.com/typography/OTSPEC/chapter2.htm var langSysTable = { reserved: Parser.uShort, reqFeatureIndex: Parser.uShort, featureIndexes: Parser.uShortList }; Parser.prototype.parseScriptList = function () { return this.parsePointer(Parser.recordList({ tag: Parser.tag, script: Parser.pointer({ defaultLangSys: Parser.pointer(langSysTable), langSysRecords: Parser.recordList({ tag: Parser.tag, langSys: Parser.pointer(langSysTable) }) }) })) || []; }; Parser.prototype.parseFeatureList = function () { return this.parsePointer(Parser.recordList({ tag: Parser.tag, feature: Parser.pointer({ featureParams: Parser.pointer({ format: Parser.offset16, featUiLabelNameId: Parser.offset16, featUiTooltipTextNameId: Parser.offset16, sampleTextNameId: Parser.offset16, numNamedParameters: Parser.offset16, firstParamUiLabelNameId: Parser.offset16, charCount: Parser.offset16, }), lookupListIndexes: Parser.uShortList }) })) || []; }; Parser.prototype.parseLookupList = function (lookupTableParsers) { return this.parsePointer(Parser.list(Parser.pointer(function () { var lookupType = this.parseUShort(); check_1.default.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.'); var lookupFlag = this.parseUShort(); var useMarkFilteringSet = lookupFlag & 0x10; return { lookupType: lookupType, lookupFlag: lookupFlag, subtables: this.parseList(Parser.pointer(lookupTableParsers[lookupType])), markFilteringSet: useMarkFilteringSet ? this.parseUShort() : undefined }; }))) || []; }; Parser.prototype.parseFeatureVariationsList = function () { return this.parsePointer32(function () { var majorVersion = this.parseUShort(); var minorVersion = this.parseUShort(); check_1.default.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.'); var featureVariations = this.parseRecordList32({ conditionSetOffset: Parser.offset32, featureTableSubstitutionOffset: Parser.offset32 }); return featureVariations; }) || []; }; exports.default = { getByte: getByte, getCard8: getByte, getUShort: getUShort, getCard16: getUShort, getShort: getShort, getULong: getULong, getFixed: getFixed, getTag: getTag, getOffset: getOffset, getBytes: getBytes, getVLUInt: getVLUInt, bytesToString: bytesToString, Parser: Parser, };