629 lines
19 KiB
Plaintext
629 lines
19 KiB
Plaintext
// 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 };
|