samsa-core
Version:
Library for processing TrueType font files.
1,793 lines (1,590 loc) • 232 kB
JavaScript
"use strict";
// samsa-core.js
// Version 2.0 alpha
/*
Find this on GitHub https://github.com/lorp/samsa-core
Find this on NPM https://www.npmjs.com/package/samsa-core
To update the NPM version, increment the version property in /package.json (not /src/package.json), then run `npm publish` from the root directory
A note on variable naming. You may see a few of these:
const _runCount = buf.u16; // get the data from the buffer
const runCount = _runCount & 0x7FFF; // refine the data into a useable variable
The underscore prefix is intended to mean the initial version of the variable (_runCount) that needs to be refined into a useable variable (runCount). This way we can
accurately reflect fields described in the spec, derive some data from flags, then use them under similar name for the purpose decribed by the name.
2023-07-27: All occurrences of "??" nullish coalescing operator have been replaced (it’s not supported by something in the Figma plugin build process). The ?? lines remain as comments above their replacements.
2024-10-10: Allowed "??" and "??=" nullish operators.
*/
// expose these to the client
const SAMSAGLOBAL = {
version: "2.0.0",
browser: typeof window !== "undefined",
endianness: endianness(),
littleendian: endianness("LE"),
bigendian: endianness("BE"),
fingerprints: { WOFF2: 0x774f4632, TTF: 0x00010000, OTF: 0x4f54544f, true: 0x74727565 }, // 0x4f54544f/"OTTO" is for CFF fonts, 0x74727565/"true" is for Skia.ttf
stdGlyphNames: [".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"], // 258 standard glyph names
mvarLookup: {
"hasc": ["OS/2","sTypoAscender"],
"hdsc": ["OS/2","sTypoDescender"],
"hlgp": ["OS/2","sTypoLineGap"],
"hcla": ["OS/2","usWinAscent"],
"hcld": ["OS/2","usWinDescent"],
"xhgt": ["OS/2","sxHeight"],
"cpht": ["OS/2","sCapHeight"],
"sbxs": ["OS/2","ySubscriptXSize"],
"sbys": ["OS/2","ySubscriptYSize"],
"sbxo": ["OS/2","ySubscriptXOffset"],
"sbyo": ["OS/2","ySubscriptYOffset"],
"spxs": ["OS/2","ySuperscriptXSize"],
"spys": ["OS/2","ySuperscriptYSize"],
"spxo": ["OS/2","ySuperscriptXOffset"],
"spyo": ["OS/2","ySuperscriptYOffset"],
"strs": ["OS/2","yStrikeoutSize"],
"stro": ["OS/2","yStrikeoutPosition"],
"hcrs": ["hhea","caretSlopeRise"],
"hcrn": ["hhea","caretSlopeRun"],
"hcof": ["hhea","caretOffset"],
"unds": ["post","underlineThickness"],
"undo": ["post","underlinePosition"],
"vasc": ["vhea","ascent"],
"vdsc": ["vhea","descent"],
"vlgp": ["vhea","lineGap"],
"vcrs": ["vhea","caretSlopeRise"],
"vcrn": ["vhea","caretSlopeRun"],
"vcof": ["vhea","caretOffset"],
"gsp0": ["gasp","0"],
"gsp1": ["gasp","1"],
"gsp2": ["gasp","2"],
"gsp3": ["gasp","3"],
"gsp4": ["gasp","4"],
"gsp5": ["gasp","5"],
"gsp6": ["gasp","6"],
"gsp7": ["gasp","7"],
"gsp8": ["gasp","8"],
"gsp9": ["gasp","9"],
},
};
// format codes
const U4 = 0;
const U8 = 1;
const I8 = 2;
const U16 = 3;
const I16 = 4;
const U24 = 5;
const I24 = 6;
const U32 = 7;
const I32 = 8;
const U64 = 9;
const I64 = 10;
const F1616 = 11;
const F214 = 12;
const STR = 13;
const TAG = 14;
const CHAR = 15;
const FORMATS = {
head: {
version: [U16,2],
fontRevision: [U16,2],
checkSumAdjustment: U32,
magicNumber: U32,
flags: U16,
unitsPerEm: U16,
created: I64,
modified: I64,
xMin: I16,
yMin: I16,
xMax: I16,
yMax: I16,
macStyle: U16,
lowestRecPPEM: U16,
fontDirectionHint: I16,
indexToLocFormat: I16,
glyphDataFormat: I16,
},
hhea: {
version: [U16,2],
ascender: I16,
descender: I16,
lineGap: I16,
advanceWidthMax: U16,
minLeftSideBearing: I16,
minRightSideBearing: I16,
xMaxExtent: I16,
caretSlopeRise: I16,
caretSlopeRun: I16,
caretOffset: I16,
reserved: [I16,4],
metricDataFormat: I16,
numberOfHMetrics: U16,
},
vhea: {
version: [U32],
_IF_10: [["version", "==", 0x00010000], {
ascent: I16,
descent: I16,
lineGap: I16,
}],
_IF_11: [["version", "==", 0x00011000], {
vertTypoAscender: I16,
vertTypoDescender: I16,
vertTypoLineGap: I16,
}],
advanceHeightMax: I16,
minTop: I16,
minBottom: I16,
yMaxExtent: I16,
caretSlopeRise: I16,
caretSlopeRun: I16,
caretOffset: I16,
reserved: [I16,4],
metricDataFormat: I16,
numOfLongVerMetrics: U16,
},
maxp: {
version: F1616,
numGlyphs: U16,
_IF_: [["version", ">=", 1.0], {
maxPoints: U16,
maxContours: U16,
maxCompositePoints: U16,
maxCompositeContours: U16,
maxZones: U16,
maxTwilightPoints: U16,
maxStorage: U16,
maxFunctionDefs: U16,
maxInstructionDefs: U16,
maxStackElements: U16,
maxSizeOfInstructions: U16,
maxComponentElements: U16,
maxComponentDepth: U16,
}],
},
post: {
version: F1616,
italicAngle: F1616,
underlinePosition: I16,
underlineThickness: I16,
isFixedPitch: U32,
minMemType42: U32,
maxMemType42: U32,
minMemType1: U32,
maxMemType1: U32,
_IF_: [["version", "==", 2.0], {
numGlyphs: U16,
glyphNameIndex: [U16, "numGlyphs"],
}],
_IF_2: [["version", "==", 2.5], {
numGlyphs: U16,
offset: [I8, "numGlyphs"],
}],
},
name: {
version: U16,
count: U16,
storageOffset: U16,
},
cmap: {
version: U16,
numTables: U16,
},
"OS/2": {
version: U16,
xAvgCharWidth: U16,
usWeightClass: U16,
usWidthClass: U16,
fsType: U16,
ySubscriptXSize: U16,
ySubscriptYSize: U16,
ySubscriptXOffset: U16,
ySubscriptYOffset: U16,
ySuperscriptXSize: U16,
ySuperscriptYSize: U16,
ySuperscriptXOffset: U16,
ySuperscriptYOffset: U16,
yStrikeoutSize: U16,
yStrikeoutPosition: U16,
sFamilyClass: I16,
panose: [U8,10],
ulUnicodeRange1: U32,
ulUnicodeRange2: U32,
ulUnicodeRange3: U32,
ulUnicodeRange4: U32,
achVendID: TAG,
fsSelection: U16,
usFirstCharIndex: U16,
usLastCharIndex: U16,
sTypoAscender: I16,
sTypoDescender: I16,
sTypoLineGap: I16,
usWinAscent: U16,
usWinDescent: U16,
_IF_: [["version", ">=", 1], {
ulCodePageRange1: U32,
ulCodePageRange2: U32,
}],
_IF_2: [["version", ">=", 2], {
sxHeight: I16,
sCapHeight: I16,
usDefaultChar: U16,
usBreakChar: U16,
usMaxContext: U16,
}],
_IF_3: [["version", ">=", 5], {
usLowerOpticalPointSize: U16,
usUpperOpticalPointSize: U16,
}],
},
fvar: {
version: [U16,2],
axesArrayOffset: U16,
reserved: U16,
axisCount: U16,
axisSize: U16,
instanceCount: U16,
instanceSize: U16
},
gvar: {
version: [U16,2],
axisCount: U16,
sharedTupleCount: U16,
sharedTuplesOffset: U32,
glyphCount: U16,
flags: U16,
glyphVariationDataArrayOffset: U32,
},
avar: {
version: [U16,2],
reserved: U16,
axisCount: U16,
},
COLR: {
version: U16,
numBaseGlyphRecords: U16,
baseGlyphRecordsOffset: U32,
layerRecordsOffset: U32,
numLayerRecords: U16,
_IF_: [["version", "==", 1], {
baseGlyphListOffset: U32,
layerListOffset: U32,
clipListOffset: U32,
varIndexMapOffset: U32,
itemVariationStoreOffset: U32,
}],
},
CPAL: {
version: U16,
numPaletteEntries: U16,
numPalettes: U16,
numColorRecords: U16,
colorRecordsArrayOffset: U32,
colorRecordIndices: [U16,"numPalettes"],
_IF_: [["version", "==", 1], {
paletteTypesArrayOffset: U32,
paletteLabelsArrayOffset: U32,
paletteEntryLabelsArrayOffset: U32,
}],
// colorRecords[U32, "numColorRecords", "@colorRecordsArrayOffset"], // maybe this format
},
STAT: {
version: [U16,2],
designAxisSize: U16,
designAxisCount: U16,
designAxesOffset: U32,
axisValueCount: U16,
offsetToAxisValueOffsets: U32,
},
MVAR: {
version: [U16,2],
reserved: U16,
valueRecordSize: U16,
valueRecordCount: U16,
itemVariationStoreOffset: U16,
},
HVAR: {
version: [U16,2],
itemVariationStoreOffset: U32,
advanceWidthMappingOffset: U32,
lsbMappingOffset: U32,
rsbMappingOffset: U32,
},
VVAR: {
version: [U16,2],
itemVariationStoreOffset: U32,
advanceHeightMappingOffset: U32,
tsbMappingOffset: U32,
bsbMappingOffset: U32,
vOrgMappingOffset: U32,
},
TableDirectory: {
sfntVersion: U32,
numTables: U16,
searchRange: U16,
entrySelector: U16,
rangeShift: U16,
},
TableRecord: {
tag: TAG,
checkSum: U32,
offset: U32,
length: U32,
},
NameRecord: {
platformID: U16,
encodingID: U16,
languageID: U16,
nameID: U16,
length: U16,
stringOffset: U16,
},
EncodingRecord: {
platformID: U16,
encodingID: U16,
subtableOffset: U32,
},
CharacterEncoding: {
format: U16,
_IF_: [["format", "<=", 6], {
length: U16,
language: U16,
}],
_IF_1: [["format", "==", 8], {
reserved: U16,
length: U32,
language: U32,
}],
_IF_2: [["format", "==", 10], {
reserved: U16,
length: U32,
language: U32,
}],
_IF_3: [["format", "==", 12], {
reserved: U16,
length: U32,
language: U32,
}],
_IF_4: [["format", "==", 14], {
length: U32,
}],
},
GlyphHeader: {
numberOfContours: I16,
xMin: I16,
yMin: I16,
xMax: I16,
yMax: I16,
},
VariationAxisRecord: {
axisTag: TAG,
minValue: F1616,
defaultValue: F1616,
maxValue: F1616,
flags: U16,
axisNameID: U16,
},
InstanceRecord: {
subfamilyNameID: U16,
flags: U16,
coordinates: [F1616, "_ARG0_"],
_IF_: [["_ARG1_", "==", true], {
postScriptNameID: U16,
}],
},
AxisRecord: {
axisTag: TAG,
axisNameID: U16,
axisOrdering: U16,
},
AxisValueFormat: {
format: U16,
_IF_: [["format", "<", 4], {
axisIndex: U16,
}],
_IF_4: [["format", "==", 4], {
axisCount: U16,
}],
flags: U16,
valueNameID: U16,
_IF_1: [["format", "==", 1], {
value: F1616,
}],
_IF_2: [["format", "==", 2], {
value: F1616,
rangeMinValue: F1616,
rangeMaxValue: F1616,
}],
_IF_3: [["format", "==", 3], {
value: F1616,
linkedValue: F1616,
}],
},
ItemVariationStoreHeader: {
format: U16,
regionListOffset: U32,
itemVariationDataCount: U16,
itemVariationDataOffsets: [U32, "itemVariationDataCount"],
},
ItemVariationData: {
itemCount: U16,
wordDeltaCount: U16,
regionIndexCount: U16,
regionIndexes: [U16, "regionIndexCount"],
},
WOFF2_header: {
signature: TAG,
flavor: U32,
length: U32,
numTables: U16,
reserved: U16,
totalSfntSize: U32,
totalCompressedSize: U32,
majorVersion: U16,
minorVersion: U16,
metaOffset: U32,
metaLength: U32,
metaOrigLength: U32,
privOffset: U32,
privLength: U32,
},
WOFF2_Transformed_glyf: {
reserved: U16,
optionFlags: U16,
numGlyphs: U16,
indexFormat: U16,
nContourStreamSize: U32,
nPointsStreamSize: U32,
flagStreamSize: U32,
glyphStreamSize: U32,
compositeStreamSize: U32,
bboxStreamSize: U32,
instructionStreamSize: U32,
},
};
// gvar
const GVAR_SHARED_POINT_NUMBERS = 0x8000;
const GVAR_EMBEDDED_PEAK_TUPLE = 0x8000;
const GVAR_INTERMEDIATE_REGION = 0x4000;
const GVAR_PRIVATE_POINT_NUMBERS = 0x2000;
const GVAR_DELTAS_ARE_ZERO = 0x80;
const GVAR_DELTAS_ARE_WORDS = 0x40;
const GVAR_DELTA_RUN_COUNT_MASK = 0x3f;
const GVAR_POINTS_ARE_WORDS = 0x80;
const GVAR_POINT_RUN_COUNT_MASK = 0x7f;
// COLRv1 paint types (multiple formats have the same type)
const PAINT_LAYERS = 1;
const PAINT_SHAPE = 2;
const PAINT_TRANSFORM = 3;
const PAINT_COMPOSE = 4;
// there are 4 types of paint tables: PAINT_LAYERS, PAINT_TRANSFORM, PAINT_SHAPE, PAINT_COMPOSE
// all DAG leaves are PAINT_COMPOSE
const PAINT_TYPES = [
0,
PAINT_LAYERS,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_COMPOSE,
PAINT_SHAPE,
PAINT_LAYERS,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_TRANSFORM,
PAINT_COMPOSE,
];
const PAINT_VAR_OPERANDS = [0,0,1,1,6,6,6,6,4,4,0,0,6,6,2,2,2,2,4,4,1,1,3,3,1,1,3,3,2,2,4,4,0]; // the number of variable operands that each paint has, indexed by paint.format (prepended with a record for invalid paint.format 0) thus 33 items from 0 to 32
// constants for SVG conversion of gradient extend modes and PaintComposite modes
const SVG_GRADIENT_EXTEND_MODES = ["pad", "repeat", "reflect"];
const SVG_PAINTCOMPOSITE_MODES = [
["-", "clear"], // COMPOSITE_CLEAR
["-", "src"], // COMPOSITE_SRC
["-", "dest"], // COMPOSITE_DEST
["F", "over"], // COMPOSITE_SRC_OVER
["F", "over"], // COMPOSITE_DEST_OVER
["F", "in"], // COMPOSITE_SRC_IN
["F", "in"], // COMPOSITE_DEST_IN
["F", "out"], // COMPOSITE_SRC_OUT
["F", "out"], // COMPOSITE_DEST_OUT
["F", "atop"], // COMPOSITE_SRC_ATOP
["F", "atop"], // COMPOSITE_DEST_ATOP
["F", "xor"], // COMPOSITE_XOR
["F", "lighter"], // COMPOSITE_PLUS (sic)
["M", "screen"], // COMPOSITE_SCREEN
["M", "overlay"], // COMPOSITE_OVERLAY
["M", "darken"], // COMPOSITE_DARKEN
["M", "lighten"], // COMPOSITE_LIGHTEN
["M", "color-dodge"], // COMPOSITE_COLOR_DODGE
["M", "color-burn"], // COMPOSITE_COLOR_BURN
["M", "hard-light"], // COMPOSITE_HARD_LIGHT
["M", "soft-light"], // COMPOSITE_SOFT_LIGHT
["M", "difference"], // COMPOSITE_DIFFERENCE
["M", "exclusion"], // COMPOSITE_EXCLUSION
["M", "multiply"], // COMPOSITE_MULTIPLY
["M", "hue"], // COMPOSITE_HSL_HUE
["M", "saturation"], // COMPOSITE_HSL_SATURATION
["M", "color"], // COMPOSITE_HSL_COLOR
["M", "luminosity"], // COMPOSITE_HSL_LUMINOSITY
];
// table decoders are called *after* the first part has been decoded using the FORMATS[<tableTag>] definition
// - font is the SamsaFont object
// - buf is a SamsaBuffer object already set up to cover the table data only, initialized with the table's offset being p=0 and length = its length
// - return (none), but font.<tableTag> now contains more (possibly all) of the decoded table data
SAMSAGLOBAL.TABLE_DECODERS = {
"avar": (font, buf) => {
// avar1 and avar2
// https://learn.microsoft.com/en-us/typography/opentype/spec/avar
// https://github.com/harfbuzz/boring-expansion-spec/blob/main/avar2.md
font.avar.axisSegmentMaps = [];
console.assert(font.avar.axisCount === font.fvar.axisCount || font.avar.axisCount === 0, `fvar.axisCount (${font.fvar.axisCount}) and avar.axisCount (${font.avar.axisCount}) must match, or else avar.axisCount must be 0`);
for (let a=0; a<font.avar.axisCount; a++) {
const positionMapCount = buf.u16;
font.avar.axisSegmentMaps[a] = [];
for (let p=0; p<positionMapCount; p++) {
font.avar.axisSegmentMaps[a].push([ buf.f214, buf.f214 ]);
}
}
// avar2 only
if (font.avar.version[0] == 2) {
font.avar.axisIndexMapOffset = buf.u32;
font.avar.itemVariationStoreOffset = buf.u32; // we use this key, rather that in the spec, so that it will get picked up by the ItemVariationStore decoder along with those of MVAR, HVAR, etc.
if (font.avar.axisIndexMapOffset) {
buf.seek(font.avar.axisIndexMapOffset);
font.avar.axisIndexMap = buf.decodeIndexMap();
}
// else we must use implicit mappings later
}
},
"cmap": (font, buf) => {
// https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
// - this function works in step with SamsaFont.prototype.glyphIdFromUnicode()
const cmap = font.cmap;
cmap.lookup = []; // the main cmap lookup table
cmap.encodings = {};
buf.seek(4); // get to the start of the encodingRecords array (we already read version and numTables in the template)
// step thru the encodingRecords
// - we only process the encodings we care about
const uniqueEncodings = {}; // encodings indexed by subtableOffset (if an encoding is referred to twice, we only parse it once)
for (let t=0; t < cmap.numTables; t++) {
const encodingRecord = buf.decode(FORMATS.EncodingRecord);
const platEnc = (encodingRecord.platformID << 16) + encodingRecord.encodingID; // store each platform/encoding pairs as one uint32 so we can easily match them
cmap.encodings[platEnc] = encodingRecord; // object with "uint32" keys (the key being a platform/encoding pair)
const bufE = new SamsaBuffer(buf.buffer, buf.byteOffset + encodingRecord.subtableOffset);
const encoding = bufE.decode(FORMATS.CharacterEncoding);
if (uniqueEncodings[encodingRecord.subtableOffset] === undefined) {
// uncached
switch (encoding.format) {
case 0: { // "Byte encoding table"
encoding.mapping = [];
for (let c=0; c<256; c++) {
encoding.mapping[c] = bufE.u8;
}
break;
}
case 4: { // "Segment mapping to delta values"
const segCount = bufE.u16 / 2;
bufE.seekr(6); // skip binary search params
encoding.segments = [];
for (let s=0; s<segCount; s++)
encoding.segments[s] = {end: bufE.u16};
bufE.seekr(2); // skip reservedPad
for (let s=0; s<segCount; s++)
encoding.segments[s].start = bufE.u16;
for (let s=0; s<segCount; s++)
encoding.segments[s].idDelta = bufE.u16;
encoding.idRangeOffsetOffset = bufE.tell() + encodingRecord.subtableOffset; // recording this absolutely makes things easier later
for (let s=0; s<segCount; s++)
encoding.segments[s].idRangeOffset = bufE.u16;
break;
}
case 12: { // "Segmented coverage"
const numGroups = bufE.u32;
encoding.groups = [];
for (let grp=0; grp<numGroups; grp++) {
encoding.groups.push({ start: bufE.u32, end: bufE.u32, glyphId: bufE.u32 });
}
break;
}
case 14: { // "Unicode Variation Sequences"
if (platEnc === 0x00000005) { // only valid under platEnc 0,5
//console.log("UVS!")
const numVarSelectorRecords = bufE.u32;
encoding.varSelectors = {};
// we index varSelectors by their varSelector
for (let v=0; v<numVarSelectorRecords; v++) {
const varSelector = bufE.u24;
const defaultUVSOffset = bufE.u32;
const nonDefaultUVSOffset = bufE.u32;
const defaultUVS = [];
const nonDefaultUVS = [];
const tell = bufE.tell();
if (defaultUVSOffset) {
bufE.seek(defaultUVSOffset);
const count = bufE.u32;
for (let r=0; r<count; r++) {
defaultUVS.push({ startUnicodeValue: bufE.u24, additionalCount: bufE.u8 });
}
}
if (nonDefaultUVSOffset) {
bufE.seek(nonDefaultUVSOffset);
const count = bufE.u32;
for (let r=0; r<count; r++) {
nonDefaultUVS.push({ unicodeValue: bufE.u24, glyphId: bufE.u16 });
}
}
encoding.varSelectors[varSelector] = {
defaultUVS: defaultUVS,
nonDefaultUVS: nonDefaultUVS,
};
bufE.seek(tell);
// so now, we have e.g. varSelector == 0xfe0f, encoding.varSelectors[0xfe0f].defaultUVS == an array of { startUnicodeValue, additionalCount }
}
}
break;
}
}
uniqueEncodings[encodingRecord.subtableOffset] = encoding;
}
cmap.encodings[platEnc] = uniqueEncodings[encodingRecord.subtableOffset];
}
},
"COLR": (font, buf) => {
// https://learn.microsoft.com/en-us/typography/opentype/spec/colr
const colr = font.COLR;
if (colr.version <= 1) {
// COLRv0 offsets (it would be ok to look these up live with binary search)
colr.baseGlyphRecords = [];
if (colr.numBaseGlyphRecords) {
buf.seek(colr.baseGlyphRecordsOffset);
for (let i=0; i<colr.numBaseGlyphRecords; i++) {
const glyphId = buf.u16;
colr.baseGlyphRecords[glyphId] = [buf.u16, buf.u16]; // firstLayerIndex, numLayers
}
}
if (colr.version == 1) {
// COLRv1 offsets (it would be ok to look these up live with binary search)
if (colr.baseGlyphListOffset) {
buf.seek(colr.baseGlyphListOffset);
colr.numBaseGlyphPaintRecords = buf.u32;
colr.baseGlyphPaintRecords = [];
for (let i=0; i<colr.numBaseGlyphPaintRecords; i++) {
const glyphId = buf.u16;
colr.baseGlyphPaintRecords[glyphId] = colr.baseGlyphListOffset + buf.u32;
}
}
// COLRv1 layerList
// - from the spec: "The LayerList is only used in conjunction with the BaseGlyphList and, specifically, with PaintColrLayers tables; it is not required if no color glyphs use a PaintColrLayers table. If not used, set layerListOffset to NULL"
colr.layerList = [];
if (colr.layerListOffset) {
buf.seek(colr.layerListOffset);
colr.numLayerListEntries = buf.u32;
for (let lyr=0; lyr < colr.numLayerListEntries; lyr++)
colr.layerList[lyr] = colr.layerListOffset + buf.u32;
}
// COLRv1 varIndexMap
if (colr.varIndexMapOffset) {
buf.seek(colr.varIndexMapOffset);
colr.varIndexMap = buf.decodeIndexMap();
}
}
}
},
"CPAL": (font, buf) => {
// load CPAL table fully
// https://learn.microsoft.com/en-us/typography/opentype/spec/cpal
const cpal = font.CPAL;
cpal.colors = [];
cpal.palettes = [];
cpal.paletteEntryLabels = [];
// decode colorRecords
buf.seek(cpal.colorRecordsArrayOffset);
for (let c=0; c<cpal.numColorRecords; c++) {
cpal.colors[c] = buf.u32; // [blue, green, red, alpha as u32]
}
// decode paletteEntryLabels
if (cpal.paletteEntryLabelsArrayOffset) {
buf.seek(cpal.paletteEntryLabelsArrayOffset);
for (let pel=0; pel<cpal.numPaletteEntries; pel++) {
const nameId = buf.u16;
if (nameId !== 0xffff)
cpal.paletteEntryLabels[pel] = font.names[nameId];
}
}
// decode palettes: name, type, colors
for (let pal=0; pal<cpal.numPalettes; pal++) {
const palette = { name: "", type: 0, colors: [] };
// name
if (cpal.paletteLabelsArrayOffset) {
buf.seek(cpal.paletteLabelsArrayOffset + 2 * pal);
const nameId = buf.u16;
if (nameId !== 0xffff) {
palette.name = font.names[nameId];
}
}
// type
if (cpal.paletteTypesArrayOffset) {
buf.seek(cpal.paletteTypesArrayOffset + 2 * pal);
palette.type = buf.u16;
}
// colors
for (let e=0; e<cpal.numPaletteEntries; e++) {
palette.colors[e] = cpal.colors[cpal.colorRecordIndices[pal] + e];
}
cpal.palettes.push(palette);
}
},
"fvar": (font, buf) => {
// load fvar axes and instances
// https://learn.microsoft.com/en-us/typography/opentype/spec/fvar
const fvar = font.fvar;
fvar.axes = [];
buf.seek(fvar.axesArrayOffset);
for (let a=0; a<fvar.axisCount; a++) {
const axis = buf.decode(FORMATS.VariationAxisRecord);
axis.axisId = a;
axis.name = font.names[axis.axisNameID];
fvar.axes.push(axis);
}
fvar.instances = [];
const includePostScriptNameID = fvar.instanceSize == fvar.axisCount * 4 + 6; // instanceSize determins whether postScriptNameID is included
for (let i=0; i<fvar.instanceCount; i++) {
const instance = buf.decode(FORMATS.InstanceRecord, fvar.axisCount, includePostScriptNameID);
instance.name = font.names[instance.subfamilyNameID];
fvar.instances.push(instance);
}
},
"gvar": (font, buf) => {
// decode gvar’s sharedTuples array, so we can precalculate scalars (leave the rest for JIT)
// https://learn.microsoft.com/en-us/typography/opentype/spec/gvar
const gvar = font.gvar;
buf.seek(gvar.sharedTuplesOffset);
gvar.sharedTuples = [];
for (let t=0; t < gvar.sharedTupleCount; t++) {
const tuple = [];
for (let a=0; a<gvar.axisCount; a++) {
tuple.push(buf.f214); // these are the peaks, we have to create start and end
}
gvar.sharedTuples.push(tuple);
}
// Experimental code, intended to precalculate the scalars for the sharedTuples
// - the issue is that, occasionally (as in Bitter), some sharedTuples are intermediate so need their start and end explicitly read from the TVT for each glyph
// - the intermediate sharedTuples cannot be precalculated
// - the logic needs to be that IF the peak tuple is shared AND the tuple is non-intermediate, THEN we can precalculate the scalar
// - that is equivalent to checking that flag & 0xC000 == 0
//
// this.gvar.sharedRegions = [];
// buf.seek(this.tables["gvar"].offset + this.gvar.sharedTuplesOffset);
// for (let t=0; t < this.gvar.sharedTupleCount; t++) {
// const region = [];
// for (let a=0; a<this.gvar.axisCount; a++) {
// const peak = buf.f214; // only the peak is stored, we create start and end
// if (peak < 0) {
// start = -1;
// end = 0;
// }
// else if (peak > 0) {
// start = 0;
// end = 1;
// }
// region.push([start, peak, end]);
// }
// this.gvar.sharedRegions.push(region);
// }
// get tupleOffsets array (TODO: we could get these offsets JIT)
buf.seek(20);
gvar.tupleOffsets = [];
for (let g=0; g <= font.maxp.numGlyphs; g++) { // <=
gvar.tupleOffsets[g] = gvar.flags & 0x01 ? buf.u32 : buf.u16 * 2;
}
},
"hmtx": (font, buf) => {
// decode horizontal metrics
// https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx
const numberOfHMetrics = font.hhea.numberOfHMetrics;
const hmtx = font.hmtx = [];
let g=0;
while (g<numberOfHMetrics) {
hmtx[g++] = buf.u16;
buf.seekr(2); // skip over lsb, we only record advance width
}
while (g<font.maxp.numGlyphs) {
hmtx[g++] = hmtx[numberOfHMetrics-1];
}
},
"HVAR": (font, buf) => {
// https://learn.microsoft.com/en-us/typography/opentype/spec/hvar
buf.seek(font.HVAR.advanceWidthMappingOffset);
font.HVAR.indexMap = buf.decodeIndexMap();
},
"MVAR": (font, buf) => {
// decode MVAR value records
// https://learn.microsoft.com/en-us/typography/opentype/spec/mvar
font.MVAR.valueRecords = {};
for (let v=0; v<font.MVAR.valueRecordCount; v++) {
buf.seek(12 + v * font.MVAR.valueRecordSize); // we are dutifully using valueRecordSize to calculate offset, but it should always be 8 bytes
font.MVAR.valueRecords[buf.tag] = [buf.u16, buf.u16]; // deltaSetOuterIndex, deltaSetInnerIndex
}
},
"name": (font, buf) => {
// decode name table strings
// https://learn.microsoft.com/en-us/typography/opentype/spec/name
const name = font.name; // font.name is the name table info directly
font.names = []; // font.names is the names ready to use as UTF8 strings, indexed by nameID in the best platformID/encondingID/languageID match
name.nameRecords = [];
for (let r=0; r<name.count; r++) {
name.nameRecords.push(buf.decode(FORMATS.NameRecord));
}
name.nameRecords.forEach(record => {
buf.seek(name.storageOffset + record.stringOffset);
record.string = buf.decodeNameString(record.length);
if (record.platformID == 3 && record.encodingID == 1 && record.languageID == 0x0409) {
font.names[record.nameID] = record.string; // only record 3, 1, 0x0409 for easy use
}
});
},
"post": (font, buf) => {
const post = font.post;
if (post.version === 2.0) {
// this avoids duplicating string data: we will use a function to retrieve the string from the buffer
post.pascalStringIndices = [];
buf.seek(32 + 2 + post.numGlyphs * 2);
while (buf.tell() < buf.byteLength) {
post.pascalStringIndices.push(buf.tell());
if (buf.tell() < buf.byteLength)
buf.seekr(buf.u8);
}
}
},
"vmtx": (font, buf) => {
// decode vertical metrics
// https://learn.microsoft.com/en-us/typography/opentype/spec/vmtx
const numOfLongVerMetrics = font.vhea.numOfLongVerMetrics;
const vmtx = font.vmtx = [];
let g=0;
while (g<numOfLongVerMetrics) {
vmtx[g++] = buf.u16;
buf.seekr(2); // skip over tsb, we only record advance height
}
while (g<font.maxp.numGlyphs) {
vmtx[g++] = vmtx[numOfLongVerMetrics-1];
}
},
"GSUB": (font, buf) => {
// decode GSUB
// https://learn.microsoft.com/en-us/typography/opentype/spec/gsub
buf.decodeGSUBGPOSheader(font.GSUB);
},
"GPOS": (font, buf) => {
// decode GPOS
// https://learn.microsoft.com/en-us/typography/opentype/spec/gpos
buf.decodeGSUBGPOSheader(font.GPOS);
},
"GDEF": (font, buf) => {
// decode GDEF
// https://learn.microsoft.com/en-us/typography/opentype/spec/gdef
const gdef = font.GDEF;
gdef.version = [buf.u16, buf.u16]; // [0] is majorVersion, [1] is minorVersion
if (gdef.version[0] == 1) {
gdef.glyphClassDefOffset = buf.u16;
gdef.attachListOffset = buf.u16;
gdef.ligCaretListOffset = buf.u16;
gdef.markAttachClassDefOffset = buf.u16;
if (gdef.version[1] >= 2) {
gdef.markGlyphSetsDefOffset = buf.u16;
}
if (gdef.version[1] >= 3) {
gdef.itemVariationStoreOffset = buf.u32; // we use this key, rather that in the spec, so that it will get picked up by the ItemVariationStore decoder along with those of MVAR, HVAR, etc.
}
}
},
"STAT": (font, buf) => {
const stat = font.STAT;
if (stat.version[0] === 1) {
if (stat.version[1] > 0) {
stat.elidedFallbackNameID = buf.u16;
}
stat.designAxes = [];
stat.designAxesSorted = [];
stat.axisValueTables = [];
// parse designAxes
for (let a=0; a<stat.designAxisCount; a++) {
buf.seek(stat.designAxesOffset + a * stat.designAxisSize);
const designAxis = {
designAxisID: a, // in case we are enumerating a sorted array
tag: buf.tag,
nameID: buf.u16,
axisOrdering: buf.u16,
};
stat.designAxes.push(designAxis);
stat.designAxesSorted[designAxis.axisOrdering] = designAxis;
}
// parse axisValueTables
for (let a=0; a<stat.axisValueCount; a++) {
buf.seek(stat.offsetToAxisValueOffsets + 2 * a);
const axisValueOffset = buf.u16;
buf.seek(stat.offsetToAxisValueOffsets + axisValueOffset);
const format = buf.u16;
if (format < 1 || format > 4)
continue;
const avt = {
format: format,
axisIndices:[buf.u16],
flags: buf.u16,
nameID: buf.u16,
values: [],
};
switch (avt.format) {
case 1: {
avt.values.push(buf.f1616);
break;
}
case 2: {
avt.values.push(buf.f1616, buf.f1616, buf.f1616); // value, min, max
break;
}
case 3: {
avt.values.push(buf.f1616, buf.f1616); // value, linkedValue
break;
}
case 4: {
let axisCount = avt.axisIndices.pop(); // use the value we pushed earlier, and so empty the array
while (axisCount--) {
avt.axisIndices.push(buf.u16);
avt.values.push(buf.f1616);
}
break;
}
}
stat.axisValueTables.push(avt);
}
}
},
}
SAMSAGLOBAL.TABLE_ENCODERS = {
"avar": (font, avar) => {
// encode avar
// https://learn.microsoft.com/en-us/typography/opentype/spec/avar
// avar1 and avar2
// create avar header
const bufAvarHeader = new SamsaBuffer(new ArrayBuffer(10000));
const majorVersion = avar.axisIndexMap && avar.ivsBuffer ? 2 : 1;
bufAvarHeader.u16_array = [
majorVersion,
0, // minorVersion
0, // reserved
font.fvar.axisCount]; // axisCount (avar 1) or axisSegmentMapCount (avar 2), note that 0 is rejected by Apple here: use axisCount and 0 for each positionMapCount
// avar1 per-axis segment mappings
// - create an empty axisSegmentMaps array if none is supplied
if (!avar.axisSegmentMaps)
avar.axisSegmentMaps = new Array(font.fvar.axisCount).fill([]);
console.assert(avar.axisSegmentMaps.length === font.fvar.axisCount, "avar.axisSegmentMaps.length must match fvar.axisCount");
// - write the axisSegmentMaps
avar.axisSegmentMaps.forEach(axisSegmentMap => {
bufAvarHeader.u16 = axisSegmentMap.length; // write positionMapCount (=0 or >= 3)
axisSegmentMap.forEach(segment => bufAvarHeader.f214_array = segment); // for each axis, write an array of F214 pairs [fromCoordinate, toCoordinate]
});
const avar1Length = bufAvarHeader.tell();
let avar2Length;
// avar2 only
if (majorVersion === 2) {
const axisIndexMapOffsetTell = avar1Length;
const varStoreOffsetTell = avar1Length + 4;
// write axisIndexMap
// - we are using only a single IVD, so the outer index is always zero
// - we keep it simple and always use 2 bytes (U16) for the inner index, even though axisCount is very rarely > 255
// - the index is a simple map, as axis index will be equal to inner index
bufAvarHeader.seek(avar1Length + 8); // skip to where we can start writing data
const axisIndexMapOffset = bufAvarHeader.tell();
bufAvarHeader.u8 = avar.axisIndexMap.format;
bufAvarHeader.u8 = avar.axisIndexMap.entryFormat;
if (avar.axisIndexMap.format === 0)
bufAvarHeader.u16 = avar.axisIndexMap.indices.length;
else if (avar.axisIndexMap.format === 1) {
bufAvarHeader.u32 = avar.axisIndexMap.indices.length;
}
bufAvarHeader.u16_array = avar.axisIndexMap.indices;
// write varStore
const varStoreOffset = bufAvarHeader.tell();
bufAvarHeader.memcpy(avar.ivsBuffer);
avar2Length = bufAvarHeader.tell();
// write the offsets to axisIndexMap and varStore
bufAvarHeader.seek(axisIndexMapOffsetTell);
bufAvarHeader.u32 = axisIndexMapOffset;
bufAvarHeader.seek(varStoreOffsetTell);
bufAvarHeader.u32 = varStoreOffset;
}
const avarFinalSize = majorVersion === 1 ? avar1Length : avar2Length;
// attempt to decode
// TODO: is this test code? if so, remove it
// TODO: return avarFinalSize?
bufAvarHeader.seek(0);
font.avar = bufAvarHeader.decode(FORMATS["avar"])
SAMSAGLOBAL.TABLE_DECODERS["avar"](font, bufAvarHeader);
return new SamsaBuffer(bufAvarHeader.buffer, 0, avarFinalSize);
},
}
// non-exported functions
function endianness (str) {
const buf = new ArrayBuffer(2);
const testArray = new Uint16Array(buf);
const testDataView = new DataView(buf);
testArray[0] = 0x1234; // LE or BE
const result = testDataView.getUint16(0); // BE
const endianness = result == 0x1234 ? "BE" : "LE";
return str === undefined ? endianness : str == endianness;
}
function clamp (num, min, max) {
if (num < min)
num = min;
else if (num > max)
num = max;
return num;
}
function inRange (num, min, max) {
return num >= min && num <= max;
}
function validateTuple (tuple, axisCount) {
if (!Array.isArray(tuple))
return false;
if (tuple.length != axisCount)
return false;
for (let a=0; a < axisCount; a++) {
if (typeof tuple[a] != "number" || !inRange(tuple[a], -1, 1))
return false;
}
return true;
}
function validateTag (tag) {
return (tag.length === 4 && [...tag].every(ch => inRange(ch.charCodeAt(0), 0x20, 0x7e)) && !tag.match(/^.* [^ ]+.*$/)); // 1. Test length; 2. Test ASCII; 3. Test no non-terminating spaces
}
function compareString (a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
// take an object of attributes, and returning a string suitable for insertion into an XML tag (such as <svg> or <path>)
function expandAttrs (attrs) {
let str = "";
for (let attr in attrs) {
if (attrs[attr] !== undefined)
str += ` ${attr}="${attrs[attr]}"`;
}
return str;
}
// for GSUB and GPOS
function coverageIndexForGlyph (coverage, g) {
let coverageIndex = -1;
if (coverage.format === 1) {
coverageIndex = coverage.glyphArray.indexOf(g);
}
else if (coverage.format === 2) {
for (const range of coverage.glyphRanges) {
if (g >= range.startGlyphID && g <= range.endGlyphID) {
coverageIndex = range.startCoverageIndex + g - range.startGlyphID;
break;
}
}
}
return coverageIndex;
}
/*
const PAINT_HANDLERS = {
// handlers for text output
text: (paint) => { console.log(paint); console.log("------------------------"); },
// handlers for SVG output
svg: [
null, // 0 (does not exist)
// 1: PaintColrLayers
(paint, rendering) => {
console.log("PaintColrLayers");
},
// 2: PaintSolid
(paint, rendering) => {
console.log("PaintSolid");
},
null, // 3: PaintVarSolid
// 4: PaintLinearGradient
(paint, rendering) => {
console.log("PaintLinearGradient");
},
null, // 5: PaintVarLinearGradient
// 6: PaintRadialGradient
(paint, rendering) => {
console.log("PaintRadialGradient");
},
null, // 7: PaintVarRadialGradient
// 8: PaintSweepGradient
(paint, rendering) => {
console.log("PaintSweepGradient");
},
null, // 9: PaintVarSweepGradient
// 10: PaintGlyph
(paint, rendering) => {
console.log("PaintGlyph");
},
// 11: PaintColrGlyph
(paint, rendering) => {
console.log("PaintColrGlyph");
},
// 12 : PaintTransform
(paint, rendering) => {
console.log("PaintSweepGradient");
},
null, // 13: PaintVarTransform
// 14 : PaintTranslate
(paint, rendering) => {
console.log("PaintTranslate");
},
null, // 15: PaintVarTranslate
// 16: PaintScale
(paint, rendering) => {
console.log("PaintScale");
},
null, // 17: PaintVarScale
null, // 18: PaintScaleAroundCenter
null, // 19: PaintVarScaleAroundCenter
null, // 20: PaintScaleUniform
null, // 21: PaintVarScaleUniform
null, // 22: PaintScaleUniformAroundCenter
null, // 23: PaintVarScaleUniformAroundCenter
// 24: PaintRotate
(paint, rendering) => {
console.log("PaintRotate");
},
null, // 25: PaintVarRotate
null, // 26: PaintRotateAroundCenter
null, // 27: PaintVarRotateAroundCenter
// 28: PaintSkew
(paint, rendering) => {
console.log("PaintSkew");
},
null, // 29: PaintVarSkew
null, // 30: PaintSkewAroundCenter, PaintVarSkewAroundCenter
null, // 31: PaintSkewAroundCenter, PaintVarSkewAroundCenter
// 32: PaintComposite
(paint, rendering) => {
console.log("PaintComposite");
},
],
// handlers for SVG output
_svg: [
null, // 0 (does not exist)
// 1: PaintColrLayers
(paint, rendering) => {
console.log("PaintColrLayers");
},
// 2: PaintSolid
(paint, rendering) => {
console.log("PaintSolid");
},
null, // 3: PaintVarSolid
// 4: PaintLinearGradient
(paint, rendering) => {
console.log("PaintLinearGradient");
},
null, // 5: PaintVarLinearGradient
// 6: PaintRadialGradient
(paint, rendering) => {
console.log("PaintRadialGradient");
},
null, // 7: PaintVarRadialGradient
// 8: PaintSweepGradient
(paint, rendering) => {
console.log("PaintSweepGradient");
},
null, // 9: PaintVarSweepGradient
// 10: PaintGlyph
(paint, rendering) => {
console.log("PaintGlyph");
},
// 11: PaintColrGlyph
(paint, rendering) => {
console.log("PaintColrGlyph");
},
// 12 : PaintTransform
(paint, rendering) => {
console.log("PaintSweepGradient");
},
null, // 13: PaintVarTransform
// 14 : PaintTranslate
(paint, rendering) => {
console.log("PaintTranslate");
},
null, // 15: PaintVarTranslate
// 16: PaintScale
(paint, rendering) => {
console.log("PaintScale");
},
null, // 17: PaintVarScale
null, // 18: PaintScaleAroundCenter
null, // 19: PaintVarScaleAroundCenter
null, // 20: PaintScaleUniform
null, // 21: PaintVarScaleUniform
null, // 22: PaintScaleUniformAroundCenter
null, // 23: PaintVarScaleUniformAroundCenter
// 24: PaintRotate
(paint, rendering) => {
console.log("PaintRotate");
},
null, // 25: PaintVarRotate
null, // 26: PaintRotateAroundCenter
null, // 27: PaintVarRotateAroundCenter
// 28: PaintSkew
(paint, rendering) => {
console.log("PaintSkew");
},
null, // 29: PaintVarSkew
null, // 30: PaintSkewAroundCenter, PaintVarSkewAroundCenter
null, // 31: PaintSkewAroundCenter, PaintVarSkewAroundCenter
// 32: PaintComposite
(paint, rendering) => {
console.log("PaintComposite");
},
],
};
Object.keys(PAINT_HANDLERS).forEach(key => {
let lastNonNullHandler = null;
for (let h=1; h<PAINT_HANDLERS[key].length; h++) { // skip the first entry, which is always null
if (PAINT_HANDLERS[key][h]) {
lastNonNullHandler = PAINT_HANDLERS[key][h];
}
else {
PAINT_HANDLERS[key][h] = lastNonNullHandler;
}
}
});
*/
// SamsaBuffer is a DataView subclass, constructed from an ArrayBuffer in exactly the same way as DataView
// - the extensions are:
// * it keeps track of a memory pointer, which is incremented on read/write
// * it keeps track of a phase, used for nibble reads/writes
// * it has numerous decode/encode methods for converting complex data structures to and from binary
class SamsaBuffer extends DataView {
constructor(buffer, byteOffset, byteLength) {
super(buffer, byteOffset, byteLength);
this.p = 0; // the memory pointer
this.phase = false; // phase for nibbles
this.getters = [];
this.getters[U4] = () => this.u4;
this.getters[U8] = () => this.u8;
this.getters[I8] = () => this.i8;
this.getters[U16] = () => this.u16;
this.getters[I16] = () => this.i16;
this.getters[U24] = () => this.u24;
this.getters[I24] = () => this.i24;
this.getters[U32] = () => this.u32;
this.getters[I32] = () => this.i32;
this.getters[U64] = () => this.u64;
this.getters[I64] = () => this.i64;
this.getters[F214] = () => this.f214;
this.getters[F1616] = () => this.f1616;
this.getters[TAG] = () => this.tag;
this.getters[STR] = () => this.tag;
this.setters = [];
this.setters[U4] = n => this.u4 = n;
this.setters[U8] = n => this.u8 = n;
this.setters[I8] = n => this.i8 = n;
this.setters[U16] = n => this.u16 = n;
this.setters[I16] = n => this.i16 = n;
this.setters[U24] = n => this.u24 = n;
this.setters[I24] = n => this.i24 = n;
this.setters[U32] = n => this.u32 = n;
this.setters[I32] = n => this.i32 = n;
this.setters[U64] = n => this.u64 = n;
this.setters[I64] = n => this.i64 = n;
this.setters[F214] = n => this.f214 = n;
this.setters[F1616] = n => this.f1616 = n;
this.setters[TAG] = n => this.tag = n;
this.setters[STR] = n => this.tag = n;
return this;
}
// get current position
tell() {
return this.p;
}
// seek to absolute position
seek(p) {
this.p = p;
}
// seek to relative position
seekr(p) {
this.p += p;
}
memcpy(srcSamsaBuffer=this, dstOffset=this.p, srcOffset=0, len=srcSamsaBuffer.byteLength - srcOffset, padding=0, advanceDest=true) {
const dstArray = new Uint8Array(this.buffer, this.byteOffset + dstOffset, len);
const srcArray = new Uint8Array(srcSamsaBuffer.buffer, srcSamsaBuffer.byteOffset + srcOffset, len);
dstArray.set(srcArray);
this.p += len; // advance the destination pointer (will be undone later if advanceDest is false)
if (padding) this.padToModulo(padding);
if (!advanceDest) this.seek(dstOffset);
}
// validate offset
seekValid(p) {
return p >= 0 && p < this.byteLength;
}
padToModulo(n) {
while (this.p % n) {
this.u8 = 0;
}
}
// uint64, int64
set u64(num) {
this.setUint32(this.p, num >> 32);
this.setUint32(this.p+4, num & 0xffffffff);
this.p += 8;
}
get u64() {
const ret = (this.getUint32(this.p) << 32) + this.getUint32(this.p+4);
this.p += 8;
return ret;
}
set i64(num) {
this.setUint32(this.p, num >> 32);
this.setUint32(this.p+4, num & 0xffffffff);
this.p += 8;
}
get i64() {
const ret = (this.getUint32(this.p) << 32) + this.getUint32(this.p+4);
this.p += 8;
return ret;
}
// uint32, int32, f16dot16
set u32(num) {
this.setUint32(this.p, num);
this.p += 4;
}
get u32() {
const ret = this.getUint32(this.p);
this.p += 4;
return ret;
}
set u32_array(arr) {
for (const num of arr) {
this.setUint32(this.p, num);
this.p += 4;
}
}
get u32_pascalArray() {
const arr = [];
let count = this.getUint16(this.p);
this.p+=2;
while (count--) {
arr.push(this.getUint32(this.p));
this.p+=4;
}
return arr;
}
set i32(num) {
this.setInt32(this.p, num);
this.p += 4;
}
set i32_array(arr) {
for (const num of arr) {
this.setInt32(this.p, num);
this.p += 4;
}
}
get i32() {
const ret = this.getInt32(this.p);
this.p += 4;
return ret;
}
set f1616(num) {
this.setInt32(this.p, num * 0x10000);
this.p += 4;
}
get f1616() {
const ret = this.getInt32(this.p) / 0x10000;
this.p += 4;
return ret;
}
// u32 for WOFF2: https://www.w3.org/TR/WOFF2/#UIntBase128
get u32_128() {
let accum = 0;
for (let i = 0; i < 5; i++) {
let data_byte = this.getUint8(this.p++);
if (i == 0 && data_byte == 0x80) return false; // No leading 0's
if (accum & 0xfe000000) return false; // If any of top 7 bits are set then << 7 would overflow
accum = (accum << 7) | (data_byte & 0x7f);
// Spin until most significant bit of data byte is false
if ((data_byte & 0x80) == 0) {
return accum;
}
}
}
// this was defined for VARC table
get u32_var() {
let firstByte = this.u8;
if (firstByte < 0x80) return firstByte;
else if (firstByte < 0xc0) return ((firstByte & 0x3f) << 8) + this.u8;
else if (firstByte < 0xe0) return ((firstByte & 0x1f) << 16) + this.u16;
else if (firstByte < 0xf0) return ((firstByte & 0x0f) << 24) + this.u24;
return this.u32; // firstByte == 0xfx, ignore low 4 bits of firstByte (which should be 0 anyway)
}
// uint24, int24
set u24(num) {
this.setUint16(this.p, num >> 8);
this.setUint8(this.p+2, num & 0xff);
this.p += 3;
}
get u24() {
const ret = (this.getUint16(this.p) << 8) + this.getUint8(this.p+2);
this.p += 3;
return ret;
}
set i24(num) {
this.setUint16(this.p, num >> 8);
this.setUint8(this.p+2, num & 0xff);
this.p += 3;
}
get i24() {
const ret = (this.getInt16(this.p) * 256) + this.getUint8(this.p+2);
this.p += 3;
return ret;
}
// uint16, int16, f2dot14
set u16(num) {
this.setUint16(this.p, num);
this.p += 2;
}
get u16() {
const ret = this.getUint16(this.p);
this.p += 2;
return ret;
}
set u16_array(arr) {
for (const num of arr) {
this.setUint16(this.p, num);
this.p += 2;
}
}
u16_arrayOfLength(count) { // we can’t use a getter as it needs an argument
const arr = [];
while (count--) {
arr.push(this.getUint16(this.p));
this.p+=2;
}
return arr;
}
set u16_pascalArray(arr) {
this.setUint16(arr.length);
this.p += 2;
for (const num of arr) {
this.setUint16(this.p, num);
this.p += 2;
}
}
get u16_pascalArray() {
const arr = [];
let count = this.getUint16(this.p);
this.p+=2;
while (count--) {
arr.push(this.getUint16(this.p));
this.p+=2;
}
return arr;
}
// u16 for WOFF2: https://www.w3.org/TR/WOFF2/#255UInt16
set u16_255(num) {
const oneMoreByteCode1 = 255;
const oneMoreByteCode2 = 254;
const wordCode = 253;
const lowestUCode = 253;
if (num < 253) {
this.u8 = num;
}
else if (num < 506) {
this.u8 = oneMoreByteCode1;
this.u8 = num - lowestUCode;
}
else if (num < 759) {
this.u8 = oneMoreByteCode2;
this.u8 = num - lowestUCode * 2;
}
else {
this.u8 = wordCode;
this.u16 = num;
}
}
get u16_255() {
const oneMoreByteCode1 = 255;
const oneMoreByteCode2 = 254;
const wordCode = 253;
const lowestUCode = 253;
let value;
const code = this.u8;
if (code == wordCode) {
value = this.u8;
value <<= 8;
value |= this.u8;
}
else if (code == oneMoreByteCode1) {
value = this.u8 + lowestUCode;
}
else if (code == oneMoreByteCode2) {
value = this.u8 + lowestUCode*2;
}
else {
value = code;
}
return value;
}
set i16(num) {
this.setInt16(this.p, num);
this.p += 2;
}
get i16() {
const ret = this.getInt16(this.p);
this.p += 2;
return ret;
}
set i16_array(arr) {
for (const num of arr) {
this.setInt16(this.p, num);
this.p += 2;
}
}
i16_arrayOfLength(count) { // we can’t use a getter as it needs an argument
const arr = [];
while (count--) {
arr.push(this.getInt16(this.p));
this.p+=2;
}
return arr;
}
set i16_pascalArray(arr) {
this.setUint16(this.p, arr.length);
this.p += 2;
for (const num of arr) {
this.setInt16(this.p, num);
this.p += 2;
}
}
get i16_pascalArray() {
const arr = [];
let count = this.getUint16(this.p);
this.p+=2;
while (count--) {
arr.push(this.getInt16(this.p));
this.p+=2;
}
return arr;
}
set f214(num) {
this.setInt16(this.p, num * 0x4000);
this