UNPKG

opentype.js

Version:
977 lines (856 loc) 28.5 kB
// Data types used in the OpenType font file. // All OpenType fonts use Motorola-style byte ordering (Big Endian) import check from './check'; const LIMIT16 = 32768; // The limit at which a 16-bit number switches signs == 2^15 const LIMIT32 = 2147483648; // The limit at which a 32-bit number switches signs == 2 ^ 31 /** * @exports opentype.decode * @class */ const decode = {}; /** * @exports opentype.encode * @class */ const encode = {}; /** * @exports opentype.sizeOf * @class */ const sizeOf = {}; // Return a function that always returns the same value. function constant(v) { return function() { return v; }; } // OpenType data types ////////////////////////////////////////////////////// /** * Convert an 8-bit unsigned integer to a list of 1 byte. * @param {number} * @returns {Array} */ encode.BYTE = function(v) { check.argument(v >= 0 && v <= 255, 'Byte value should be between 0 and 255.'); return [v]; }; /** * @constant * @type {number} */ sizeOf.BYTE = constant(1); /** * Convert a 8-bit signed integer to a list of 1 byte. * @param {string} * @returns {Array} */ encode.CHAR = function(v) { return [v.charCodeAt(0)]; }; /** * @constant * @type {number} */ sizeOf.CHAR = constant(1); /** * Convert an ASCII string to a list of bytes. * @param {string} * @returns {Array} */ encode.CHARARRAY = function(v) { if (typeof v === 'undefined') { v = ''; console.warn('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.'); } const b = []; for (let i = 0; i < v.length; i += 1) { b[i] = v.charCodeAt(i); } return b; }; /** * @param {Array} * @returns {number} */ sizeOf.CHARARRAY = function(v) { if (typeof v === 'undefined') { return 0; } return v.length; }; /** * Convert a 16-bit unsigned integer to a list of 2 bytes. * @param {number} * @returns {Array} */ encode.USHORT = function(v) { return [(v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.USHORT = constant(2); /** * Convert a 16-bit signed integer to a list of 2 bytes. * @param {number} * @returns {Array} */ encode.SHORT = function(v) { // Two's complement if (v >= LIMIT16) { v = -(2 * LIMIT16 - v); } return [(v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.SHORT = constant(2); /** * Convert a 24-bit unsigned integer to a list of 3 bytes. * @param {number} * @returns {Array} */ encode.UINT24 = function(v) { return [(v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.UINT24 = constant(3); /** * Convert a 32-bit unsigned integer to a list of 4 bytes. * @param {number} * @returns {Array} */ encode.ULONG = function(v) { return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.ULONG = constant(4); /** * Convert a 32-bit unsigned integer to a list of 4 bytes. * @param {number} * @returns {Array} */ encode.LONG = function(v) { // Two's complement if (v >= LIMIT32) { v = -(2 * LIMIT32 - v); } return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.LONG = constant(4); encode.FIXED = encode.ULONG; sizeOf.FIXED = sizeOf.ULONG; encode.FWORD = encode.SHORT; sizeOf.FWORD = sizeOf.SHORT; encode.UFWORD = encode.USHORT; sizeOf.UFWORD = sizeOf.USHORT; /** * Convert a 32-bit Apple Mac timestamp integer to a list of 8 bytes, 64-bit timestamp. * @param {number} * @returns {Array} */ encode.LONGDATETIME = function(v) { return [0, 0, 0, 0, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.LONGDATETIME = constant(8); /** * Convert a 4-char tag to a list of 4 bytes. * @param {string} * @returns {Array} */ encode.TAG = function(v) { check.argument(v.length === 4, 'Tag should be exactly 4 ASCII characters.'); return [v.charCodeAt(0), v.charCodeAt(1), v.charCodeAt(2), v.charCodeAt(3)]; }; /** * @constant * @type {number} */ sizeOf.TAG = constant(4); // CFF data types /////////////////////////////////////////////////////////// encode.Card8 = encode.BYTE; sizeOf.Card8 = sizeOf.BYTE; encode.Card16 = encode.USHORT; sizeOf.Card16 = sizeOf.USHORT; encode.OffSize = encode.BYTE; sizeOf.OffSize = sizeOf.BYTE; encode.SID = encode.USHORT; sizeOf.SID = sizeOf.USHORT; // Convert a numeric operand or charstring number to a variable-size list of bytes. /** * Convert a numeric operand or charstring number to a variable-size list of bytes. * @param {number} * @returns {Array} */ encode.NUMBER = function(v) { if (v >= -107 && v <= 107) { return [v + 139]; } else if (v >= 108 && v <= 1131) { v = v - 108; return [(v >> 8) + 247, v & 0xFF]; } else if (v >= -1131 && v <= -108) { v = -v - 108; return [(v >> 8) + 251, v & 0xFF]; } else if (v >= -32768 && v <= 32767) { return encode.NUMBER16(v); } else { return encode.NUMBER32(v); } }; /** * @param {number} * @returns {number} */ sizeOf.NUMBER = function(v) { return encode.NUMBER(v).length; }; /** * Convert a signed number between -32768 and +32767 to a three-byte value. * This ensures we always use three bytes, but is not the most compact format. * @param {number} * @returns {Array} */ encode.NUMBER16 = function(v) { return [28, (v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.NUMBER16 = constant(3); /** * Convert a signed number between -(2^31) and +(2^31-1) to a five-byte value. * This is useful if you want to be sure you always use four bytes, * at the expense of wasting a few bytes for smaller numbers. * @param {number} * @returns {Array} */ encode.NUMBER32 = function(v) { return [29, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; }; /** * @constant * @type {number} */ sizeOf.NUMBER32 = constant(5); /** * @param {number} * @returns {Array} */ encode.REAL = function(v) { let value = v.toString(); // Some numbers use an epsilon to encode the value. (e.g. JavaScript will store 0.0000001 as 1e-7) // This code converts it back to a number without the epsilon. const m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value); if (m) { const epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length)); value = (Math.round(v * epsilon) / epsilon).toString(); } let nibbles = ''; for (let i = 0, ii = value.length; i < ii; i += 1) { const c = value[i]; if (c === 'e') { nibbles += value[++i] === '-' ? 'c' : 'b'; } else if (c === '.') { nibbles += 'a'; } else if (c === '-') { nibbles += 'e'; } else { nibbles += c; } } nibbles += (nibbles.length & 1) ? 'f' : 'ff'; const out = [30]; for (let i = 0, ii = nibbles.length; i < ii; i += 2) { out.push(parseInt(nibbles.substr(i, 2), 16)); } return out; }; /** * @param {number} * @returns {number} */ sizeOf.REAL = function(v) { return encode.REAL(v).length; }; encode.NAME = encode.CHARARRAY; sizeOf.NAME = sizeOf.CHARARRAY; encode.STRING = encode.CHARARRAY; sizeOf.STRING = sizeOf.CHARARRAY; /** * @param {DataView} data * @param {number} offset * @param {number} numBytes * @returns {string} */ decode.UTF8 = function(data, offset, numBytes) { const codePoints = []; const numChars = numBytes; for (let j = 0; j < numChars; j++, offset += 1) { codePoints[j] = data.getUint8(offset); } return String.fromCharCode.apply(null, codePoints); }; /** * @param {DataView} data * @param {number} offset * @param {number} numBytes * @returns {string} */ decode.UTF16 = function(data, offset, numBytes) { const codePoints = []; const numChars = numBytes / 2; for (let j = 0; j < numChars; j++, offset += 2) { codePoints[j] = data.getUint16(offset); } return String.fromCharCode.apply(null, codePoints); }; /** * Convert a JavaScript string to UTF16-BE. * @param {string} * @returns {Array} */ encode.UTF16 = function(v) { const b = []; for (let i = 0; i < v.length; i += 1) { const codepoint = v.charCodeAt(i); b[b.length] = (codepoint >> 8) & 0xFF; b[b.length] = codepoint & 0xFF; } return b; }; /** * @param {string} * @returns {number} */ sizeOf.UTF16 = function(v) { return v.length * 2; }; // Data for converting old eight-bit Macintosh encodings to Unicode. // This representation is optimized for decoding; encoding is slower // and needs more memory. The assumption is that all opentype.js users // want to open fonts, but saving a font will be comparatively rare // so it can be more expensive. Keyed by IANA character set name. // // Python script for generating these strings: // // s = u''.join([chr(c).decode('mac_greek') for c in range(128, 256)]) // print(s.encode('utf-8')) /** * @private */ const eightBitMacEncodings = { 'x-mac-croatian': // Python: 'mac_croatian' 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®Š™´¨≠ŽØ∞±≤≥∆µ∂∑∏š∫ªºΩžø' + '¿¡¬√ƒ≈ƫȅ ÀÃÕŒœĐ—“”‘’÷◊©⁄€‹›Æ»–·‚„‰ÂćÁčÈÍÎÏÌÓÔđÒÚÛÙıˆ˜¯πË˚¸Êæˇ', 'x-mac-cyrillic': // Python: 'mac_cyrillic' 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°Ґ£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµґЈЄєЇїЉљЊњ' + 'јЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю', 'x-mac-gaelic': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/GAELIC.TXT 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØḂ±≤≥ḃĊċḊḋḞḟĠġṀæø' + 'ṁṖṗɼƒſṠ«»… ÀÃÕŒœ–—“”‘’ṡẛÿŸṪ€‹›Ŷŷṫ·Ỳỳ⁊ÂÊÁËÈÍÎÏÌÓÔ♣ÒÚÛÙıÝýŴŵẄẅẀẁẂẃ', 'x-mac-greek': // Python: 'mac_greek' 'Ĺ²É³ÖÜ΅àâä΄¨çéèê룙î‰ôö¦€ùûü†ΓΔΘΛΞΠß®©ΣΪ§≠°·Α±≤≥¥ΒΕΖΗΙΚΜΦΫΨΩ' + 'άΝ¬ΟΡ≈Τ«»… ΥΧΆΈœ–―“”‘’÷ΉΊΌΎέήίόΏύαβψδεφγηιξκλμνοπώρστθωςχυζϊϋΐΰ\u00AD', 'x-mac-icelandic': // Python: 'mac_iceland' 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûüݰ¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€ÐðÞþý·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ', 'x-mac-inuit': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/INUIT.TXT 'ᐃᐄᐅᐆᐊᐋᐱᐲᐳᐴᐸᐹᑉᑎᑏᑐᑑᑕᑖᑦᑭᑮᑯᑰᑲᑳᒃᒋᒌᒍᒎᒐᒑ°ᒡᒥᒦ•¶ᒧ®©™ᒨᒪᒫᒻᓂᓃᓄᓅᓇᓈᓐᓯᓰᓱᓲᓴᓵᔅᓕᓖᓗ' + 'ᓘᓚᓛᓪᔨᔩᔪᔫᔭ… ᔮᔾᕕᕖᕗ–—“”‘’ᕘᕙᕚᕝᕆᕇᕈᕉᕋᕌᕐᕿᖀᖁᖂᖃᖄᖅᖏᖐᖑᖒᖓᖔᖕᙱᙲᙳᙴᙵᙶᖖᖠᖡᖢᖣᖤᖥᖦᕼŁł', 'x-mac-ce': // Python: 'mac_latin2' 'ÄĀāÉĄÖÜáąČäčĆć鏟ĎíďĒēĖóėôöõúĚěü†°Ę£§•¶ß®©™ę¨≠ģĮįĪ≤≥īĶ∂∑łĻļĽľĹĺŅ' + 'ņѬ√ńŇ∆«»… ňŐÕőŌ–—“”‘’÷◊ōŔŕŘ‹›řŖŗŠ‚„šŚśÁŤťÍŽžŪÓÔūŮÚůŰűŲųÝýķŻŁżĢˇ', macintosh: // Python: 'mac_roman' 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ', 'x-mac-romanian': // Python: 'mac_romanian' 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ĂȘ∞±≤≥¥µ∂∑∏π∫ªºΩăș' + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›Țț‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ', 'x-mac-turkish': // Python: 'mac_turkish' 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸĞğİıŞş‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙˆ˜¯˘˙˚¸˝˛ˇ' }; /** * Decodes an old-style Macintosh string. Returns either a Unicode JavaScript * string, or 'undefined' if the encoding is unsupported. For example, we do * not support Chinese, Japanese or Korean because these would need large * mapping tables. * @param {DataView} dataView * @param {number} offset * @param {number} dataLength * @param {string} encoding * @returns {string} */ decode.MACSTRING = function(dataView, offset, dataLength, encoding) { const table = eightBitMacEncodings[encoding]; if (table === undefined) { return undefined; } let result = ''; for (let i = 0; i < dataLength; i++) { const c = dataView.getUint8(offset + i); // In all eight-bit Mac encodings, the characters 0x00..0x7F are // mapped to U+0000..U+007F; we only need to look up the others. if (c <= 0x7F) { result += String.fromCharCode(c); } else { result += table[c & 0x7F]; } } return result; }; // Helper function for encode.MACSTRING. Returns a dictionary for mapping // Unicode character codes to their 8-bit MacOS equivalent. This table // is not exactly a super cheap data structure, but we do not care because // encoding Macintosh strings is only rarely needed in typical applications. const macEncodingTableCache = typeof WeakMap === 'function' && new WeakMap(); let macEncodingCacheKeys; const getMacEncodingTable = function (encoding) { // Since we use encoding as a cache key for WeakMap, it has to be // a String object and not a literal. And at least on NodeJS 2.10.1, // WeakMap requires that the same String instance is passed for cache hits. if (!macEncodingCacheKeys) { macEncodingCacheKeys = {}; for (let e in eightBitMacEncodings) { /*jshint -W053 */ // Suppress "Do not use String as a constructor." macEncodingCacheKeys[e] = new String(e); } } const cacheKey = macEncodingCacheKeys[encoding]; if (cacheKey === undefined) { return undefined; } // We can't do "if (cache.has(key)) {return cache.get(key)}" here: // since garbage collection may run at any time, it could also kick in // between the calls to cache.has() and cache.get(). In that case, // we would return 'undefined' even though we do support the encoding. if (macEncodingTableCache) { const cachedTable = macEncodingTableCache.get(cacheKey); if (cachedTable !== undefined) { return cachedTable; } } const decodingTable = eightBitMacEncodings[encoding]; if (decodingTable === undefined) { return undefined; } const encodingTable = {}; for (let i = 0; i < decodingTable.length; i++) { encodingTable[decodingTable.charCodeAt(i)] = i + 0x80; } if (macEncodingTableCache) { macEncodingTableCache.set(cacheKey, encodingTable); } return encodingTable; }; /** * Encodes an old-style Macintosh string. Returns a byte array upon success. * If the requested encoding is unsupported, or if the input string contains * a character that cannot be expressed in the encoding, the function returns * 'undefined'. * @param {string} str * @param {string} encoding * @returns {Array} */ encode.MACSTRING = function(str, encoding) { const table = getMacEncodingTable(encoding); if (table === undefined) { return undefined; } const result = []; for (let i = 0; i < str.length; i++) { let c = str.charCodeAt(i); // In all eight-bit Mac encodings, the characters 0x00..0x7F are // mapped to U+0000..U+007F; we only need to look up the others. if (c >= 0x80) { c = table[c]; if (c === undefined) { // str contains a Unicode character that cannot be encoded // in the requested encoding. return undefined; } } result[i] = c; // result.push(c); } return result; }; /** * @param {string} str * @param {string} encoding * @returns {number} */ sizeOf.MACSTRING = function(str, encoding) { const b = encode.MACSTRING(str, encoding); if (b !== undefined) { return b.length; } else { return 0; } }; // Helper for encode.VARDELTAS function isByteEncodable(value) { return value >= -128 && value <= 127; } // Helper for encode.VARDELTAS function encodeVarDeltaRunAsZeroes(deltas, pos, result) { let runLength = 0; const numDeltas = deltas.length; while (pos < numDeltas && runLength < 64 && deltas[pos] === 0) { ++pos; ++runLength; } result.push(0x80 | (runLength - 1)); return pos; } // Helper for encode.VARDELTAS function encodeVarDeltaRunAsBytes(deltas, offset, result) { let runLength = 0; const numDeltas = deltas.length; let pos = offset; while (pos < numDeltas && runLength < 64) { const value = deltas[pos]; if (!isByteEncodable(value)) { break; } // Within a byte-encoded run of deltas, a single zero is best // stored literally as 0x00 value. However, if we have two or // more zeroes in a sequence, it is better to start a new run. // Fore example, the sequence of deltas [15, 15, 0, 15, 15] // becomes 6 bytes (04 0F 0F 00 0F 0F) when storing the zero // within the current run, but 7 bytes (01 0F 0F 80 01 0F 0F) // when starting a new run. if (value === 0 && pos + 1 < numDeltas && deltas[pos + 1] === 0) { break; } ++pos; ++runLength; } result.push(runLength - 1); for (let i = offset; i < pos; ++i) { result.push((deltas[i] + 256) & 0xff); } return pos; } // Helper for encode.VARDELTAS function encodeVarDeltaRunAsWords(deltas, offset, result) { let runLength = 0; const numDeltas = deltas.length; let pos = offset; while (pos < numDeltas && runLength < 64) { const value = deltas[pos]; // Within a word-encoded run of deltas, it is easiest to start // a new run (with a different encoding) whenever we encounter // a zero value. For example, the sequence [0x6666, 0, 0x7777] // needs 7 bytes when storing the zero inside the current run // (42 66 66 00 00 77 77), and equally 7 bytes when starting a // new run (40 66 66 80 40 77 77). if (value === 0) { break; } // Within a word-encoded run of deltas, a single value in the // range (-128..127) should be encoded within the current run // because it is more compact. For example, the sequence // [0x6666, 2, 0x7777] becomes 7 bytes when storing the value // literally (42 66 66 00 02 77 77), but 8 bytes when starting // a new run (40 66 66 00 02 40 77 77). if (isByteEncodable(value) && pos + 1 < numDeltas && isByteEncodable(deltas[pos + 1])) { break; } ++pos; ++runLength; } result.push(0x40 | (runLength - 1)); for (let i = offset; i < pos; ++i) { const val = deltas[i]; result.push(((val + 0x10000) >> 8) & 0xff, (val + 0x100) & 0xff); } return pos; } /** * Encode a list of variation adjustment deltas. * * Variation adjustment deltas are used in ‘gvar’ and ‘cvar’ tables. * They indicate how points (in ‘gvar’) or values (in ‘cvar’) get adjusted * when generating instances of variation fonts. * * @see https://www.microsoft.com/typography/otspec/gvar.htm * @see https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html * @param {Array} * @return {Array} */ encode.VARDELTAS = function(deltas) { let pos = 0; const result = []; while (pos < deltas.length) { const value = deltas[pos]; if (value === 0) { pos = encodeVarDeltaRunAsZeroes(deltas, pos, result); } else if (value >= -128 && value <= 127) { pos = encodeVarDeltaRunAsBytes(deltas, pos, result); } else { pos = encodeVarDeltaRunAsWords(deltas, pos, result); } } return result; }; // Convert a list of values to a CFF INDEX structure. // The values should be objects containing name / type / value. /** * @param {Array} l * @returns {Array} */ encode.INDEX = function(l) { //var offset, offsets, offsetEncoder, encodedOffsets, encodedOffset, data, // i, v; // Because we have to know which data type to use to encode the offsets, // we have to go through the values twice: once to encode the data and // calculate the offsets, then again to encode the offsets using the fitting data type. let offset = 1; // First offset is always 1. const offsets = [offset]; const data = []; for (let i = 0; i < l.length; i += 1) { const v = encode.OBJECT(l[i]); Array.prototype.push.apply(data, v); offset += v.length; offsets.push(offset); } if (data.length === 0) { return [0, 0]; } const encodedOffsets = []; const offSize = (1 + Math.floor(Math.log(offset) / Math.log(2)) / 8) | 0; const offsetEncoder = [undefined, encode.BYTE, encode.USHORT, encode.UINT24, encode.ULONG][offSize]; for (let i = 0; i < offsets.length; i += 1) { const encodedOffset = offsetEncoder(offsets[i]); Array.prototype.push.apply(encodedOffsets, encodedOffset); } return Array.prototype.concat(encode.Card16(l.length), encode.OffSize(offSize), encodedOffsets, data); }; /** * @param {Array} * @returns {number} */ sizeOf.INDEX = function(v) { return encode.INDEX(v).length; }; /** * Convert an object to a CFF DICT structure. * The keys should be numeric. * The values should be objects containing name / type / value. * @param {Object} m * @returns {Array} */ encode.DICT = function(m) { let d = []; const keys = Object.keys(m); const length = keys.length; for (let i = 0; i < length; i += 1) { // Object.keys() return string keys, but our keys are always numeric. const k = parseInt(keys[i], 0); const v = m[k]; // Value comes before the key. d = d.concat(encode.OPERAND(v.value, v.type)); d = d.concat(encode.OPERATOR(k)); } return d; }; /** * @param {Object} * @returns {number} */ sizeOf.DICT = function(m) { return encode.DICT(m).length; }; /** * @param {number} * @returns {Array} */ encode.OPERATOR = function(v) { if (v < 1200) { return [v]; } else { return [12, v - 1200]; } }; /** * @param {Array} v * @param {string} * @returns {Array} */ encode.OPERAND = function(v, type) { let d = []; if (Array.isArray(type)) { for (let i = 0; i < type.length; i += 1) { check.argument(v.length === type.length, 'Not enough arguments given for type' + type); d = d.concat(encode.OPERAND(v[i], type[i])); } } else { if (type === 'SID') { d = d.concat(encode.NUMBER(v)); } else if (type === 'offset') { // We make it easy for ourselves and always encode offsets as // 4 bytes. This makes offset calculation for the top dict easier. d = d.concat(encode.NUMBER32(v)); } else if (type === 'number') { d = d.concat(encode.NUMBER(v)); } else if (type === 'real') { d = d.concat(encode.REAL(v)); } else { throw new Error('Unknown operand type ' + type); // FIXME Add support for booleans } } return d; }; encode.OP = encode.BYTE; sizeOf.OP = sizeOf.BYTE; // memoize charstring encoding using WeakMap if available const wmm = typeof WeakMap === 'function' && new WeakMap(); /** * Convert a list of CharString operations to bytes. * @param {Array} * @returns {Array} */ encode.CHARSTRING = function(ops) { // See encode.MACSTRING for why we don't do "if (wmm && wmm.has(ops))". if (wmm) { const cachedValue = wmm.get(ops); if (cachedValue !== undefined) { return cachedValue; } } let d = []; const length = ops.length; for (let i = 0; i < length; i += 1) { const op = ops[i]; d = d.concat(encode[op.type](op.value)); } if (wmm) { wmm.set(ops, d); } return d; }; /** * @param {Array} * @returns {number} */ sizeOf.CHARSTRING = function(ops) { return encode.CHARSTRING(ops).length; }; // Utility functions //////////////////////////////////////////////////////// /** * Convert an object containing name / type / value to bytes. * @param {Object} * @returns {Array} */ encode.OBJECT = function(v) { const encodingFunction = encode[v.type]; check.argument(encodingFunction !== undefined, 'No encoding function for type ' + v.type); return encodingFunction(v.value); }; /** * @param {Object} * @returns {number} */ sizeOf.OBJECT = function(v) { const sizeOfFunction = sizeOf[v.type]; check.argument(sizeOfFunction !== undefined, 'No sizeOf function for type ' + v.type); return sizeOfFunction(v.value); }; /** * Convert a table object to bytes. * A table contains a list of fields containing the metadata (name, type and default value). * The table itself has the field values set as attributes. * @param {opentype.Table} * @returns {Array} */ encode.TABLE = function(table) { let d = []; const length = table.fields.length; const subtables = []; const subtableOffsets = []; for (let i = 0; i < length; i += 1) { const field = table.fields[i]; const encodingFunction = encode[field.type]; check.argument(encodingFunction !== undefined, 'No encoding function for field type ' + field.type + ' (' + field.name + ')'); let value = table[field.name]; if (value === undefined) { value = field.value; } const bytes = encodingFunction(value); if (field.type === 'TABLE') { subtableOffsets.push(d.length); d = d.concat([0, 0]); subtables.push(bytes); } else { d = d.concat(bytes); } } for (let i = 0; i < subtables.length; i += 1) { const o = subtableOffsets[i]; const offset = d.length; check.argument(offset < 65536, 'Table ' + table.tableName + ' too big.'); d[o] = offset >> 8; d[o + 1] = offset & 0xff; d = d.concat(subtables[i]); } return d; }; /** * @param {opentype.Table} * @returns {number} */ sizeOf.TABLE = function(table) { let numBytes = 0; const length = table.fields.length; for (let i = 0; i < length; i += 1) { const field = table.fields[i]; const sizeOfFunction = sizeOf[field.type]; check.argument(sizeOfFunction !== undefined, 'No sizeOf function for field type ' + field.type + ' (' + field.name + ')'); let value = table[field.name]; if (value === undefined) { value = field.value; } numBytes += sizeOfFunction(value); // Subtables take 2 more bytes for offsets. if (field.type === 'TABLE') { numBytes += 2; } } return numBytes; }; encode.RECORD = encode.TABLE; sizeOf.RECORD = sizeOf.TABLE; // Merge in a list of bytes. encode.LITERAL = function(v) { return v; }; sizeOf.LITERAL = function(v) { return v.length; }; export { decode, encode, sizeOf };