// Parsing utility functions import check from './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) { const decimal = dataView.getInt16(offset, false); const 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) { let tag = ''; for (let i = offset; i < offset + 4; i += 1) { tag += String.fromCharCode(dataView.getInt8(i)); } return tag; } // Retrieve an offset from the DataView. // Offsets are 1 to 4 bytes in length, depending on the offSize argument. function getOffset(dataView, offset, offSize) { let v = 0; for (let 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) { const bytes = []; for (let i = startOffset; i < endOffset; i += 1) { bytes.push(dataView.getUint8(i)); } return bytes; } // Convert the list of bytes to a string. function bytesToString(bytes) { let s = ''; for (let i = 0; i < bytes.length; i += 1) { s += String.fromCharCode(bytes[i]); } return s; } const 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; } Parser.prototype.parseByte = function() { const v = this.data.getUint8(this.offset + this.relativeOffset); this.relativeOffset += 1; return v; }; Parser.prototype.parseChar = function() { const v = this.data.getInt8(this.offset + this.relativeOffset); this.relativeOffset += 1; return v; }; Parser.prototype.parseCard8 = Parser.prototype.parseByte; Parser.prototype.parseUShort = function() { const 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() { const v = this.data.getInt16(this.offset + this.relativeOffset); this.relativeOffset += 2; return v; }; Parser.prototype.parseF2Dot14 = function() { const v = this.data.getInt16(this.offset + this.relativeOffset) / 16384; this.relativeOffset += 2; return v; }; Parser.prototype.parseULong = function() { const v = getULong(this.data, this.offset + this.relativeOffset); this.relativeOffset += 4; return v; }; Parser.prototype.parseOffset32 = Parser.prototype.parseULong; Parser.prototype.parseFixed = function() { const v = getFixed(this.data, this.offset + this.relativeOffset); this.relativeOffset += 4; return v; }; Parser.prototype.parseString = function(length) { const dataView = this.data; const offset = this.offset + this.relativeOffset; let string = ''; this.relativeOffset += length; for (let 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() { let 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) { const 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 const 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(); } const offsets = new Array(count); const dataView = this.data; let offset = this.offset + this.relativeOffset; for (let 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(); } const offsets = new Array(count); const dataView = this.data; let offset = this.offset + this.relativeOffset; for (let 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) { const list = new Array(count); const dataView = this.data; let offset = this.offset + this.relativeOffset; for (let 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) { const list = new Array(count); const dataView = this.data; let offset = this.offset + this.relativeOffset; for (let 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(); } const list = new Array(count); for (let 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(); } const list = new Array(count); for (let 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(); } const records = new Array(count); const fields = Object.keys(recordDescription); for (let i = 0; i < count; i++) { const rec = {}; for (let j = 0; j < fields.length; j++) { const fieldName = fields[j]; const 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(); } const records = new Array(count); const fields = Object.keys(recordDescription); for (let i = 0; i < count; i++) { const rec = {}; for (let j = 0; j < fields.length; j++) { const fieldName = fields[j]; const 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 { const fields = Object.keys(description); const struct = {}; for (let j = 0; j < fields.length; j++) { const fieldName = fields[j]; const 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; } const 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() { const valueFormat = this.parseUShort(); const valueCount = this.parseUShort(); const values = new Array(valueCount); for (let i = 0; i < valueCount; i++) { values[i] = this.parseValueRecord(valueFormat); } return values; }; Parser.prototype.parsePointer = function(description) { const 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) { const 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) { const offsets = this.parseOffset16List(); const count = offsets.length; const relativeOffset = this.relativeOffset; const list = new Array(count); for (let i = 0; i < count; i++) { const 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) { const subOffsets = this.parseOffset16List(); const subList = new Array(subOffsets.length); for (let 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() { const startOffset = this.offset + this.relativeOffset; const format = this.parseUShort(); const count = this.parseUShort(); if (format === 1) { return { format: 1, glyphs: this.parseUShortList(count) }; } else if (format === 2) { const ranges = new Array(count); for (let 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() { const startOffset = this.offset + this.relativeOffset; const 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 const 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.offset16, lookupListIndexes: Parser.uShortList }) })) || []; }; Parser.prototype.parseLookupList = function(lookupTableParsers) { return this.parsePointer(Parser.list(Parser.pointer(function() { const lookupType = this.parseUShort(); check.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.'); const lookupFlag = this.parseUShort(); const 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() { const majorVersion = this.parseUShort(); const minorVersion = this.parseUShort(); check.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.'); const featureVariations = this.parseRecordList32({ conditionSetOffset: Parser.offset32, featureTableSubstitutionOffset: Parser.offset32 }); return featureVariations; }) || []; }; export default { getByte, getCard8: getByte, getUShort, getCard16: getUShort, getShort, getULong, getFixed, getTag, getOffset, getBytes, bytesToString, Parser, }; export { Parser };