astro-ghostcms/.pnpm-store/v3/files/f3/8f9261d1ba3ae08c2970381ce3b...

629 lines
19 KiB
Plaintext
Raw Normal View History

2024-02-14 14:10:47 +00:00
// 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 };