opentype.features.js
Version:
Library to list opentype fonts optional features.
612 lines (611 loc) • 21.7 kB
JavaScript
"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,
};