UNPKG

samsa-core

Version:

Library for processing TrueType font files.

1,793 lines (1,590 loc) 232 kB
"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