// The Layout object is the prototype of Substitution objects, and provides // utility methods to manipulate common layout tables (GPOS, GSUB, GDEF...) import check from './check'; function searchTag(arr, tag) { /* jshint bitwise: false */ let imin = 0; let imax = arr.length - 1; while (imin <= imax) { const imid = (imin + imax) >>> 1; const val = arr[imid].tag; if (val === tag) { return imid; } else if (val < tag) { imin = imid + 1; } else { imax = imid - 1; } } // Not found: return -1-insertion point return -imin - 1; } function binSearch(arr, value) { /* jshint bitwise: false */ let imin = 0; let imax = arr.length - 1; while (imin <= imax) { const imid = (imin + imax) >>> 1; const val = arr[imid]; if (val === value) { return imid; } else if (val < value) { imin = imid + 1; } else { imax = imid - 1; } } // Not found: return -1-insertion point return -imin - 1; } // binary search in a list of ranges (coverage, class definition) function searchRange(ranges, value) { // jshint bitwise: false let range; let imin = 0; let imax = ranges.length - 1; while (imin <= imax) { const imid = (imin + imax) >>> 1; range = ranges[imid]; const start = range.start; if (start === value) { return range; } else if (start < value) { imin = imid + 1; } else { imax = imid - 1; } } if (imin > 0) { range = ranges[imin - 1]; if (value > range.end) return 0; return range; } } /** * @exports opentype.Layout * @class */ function Layout(font, tableName) { this.font = font; this.tableName = tableName; } Layout.prototype = { /** * Binary search an object by "tag" property * @instance * @function searchTag * @memberof opentype.Layout * @param {Array} arr * @param {string} tag * @return {number} */ searchTag: searchTag, /** * Binary search in a list of numbers * @instance * @function binSearch * @memberof opentype.Layout * @param {Array} arr * @param {number} value * @return {number} */ binSearch: binSearch, /** * Get or create the Layout table (GSUB, GPOS etc). * @param {boolean} create - Whether to create a new one. * @return {Object} The GSUB or GPOS table. */ getTable: function (create) { let layout = this.font.tables[this.tableName]; if (!layout && create) { layout = this.font.tables[this.tableName] = this.createDefaultTable(); } return layout; }, /** * Returns the best bet for a script name. * Returns 'DFLT' if it exists. * If not, returns 'latn' if it exists. * If neither exist, returns undefined. */ getDefaultScriptName: function () { let layout = this.getTable(); if (!layout) { return; } let hasLatn = false; for (let i = 0; i < layout.scripts.length; i++) { const name = layout.scripts[i].tag; if (name === 'DFLT') return name; if (name === 'latn') hasLatn = true; } if (hasLatn) return 'latn'; }, /** * Returns all LangSysRecords in the given script. * @instance * @param {string} [script='DFLT'] * @param {boolean} create - forces the creation of this script table if it doesn't exist. * @return {Object} An object with tag and script properties. */ getScriptTable: function (script, create) { const layout = this.getTable(create); if (layout) { script = script || 'DFLT'; const scripts = layout.scripts; const pos = searchTag(layout.scripts, script); if (pos >= 0) { return scripts[pos].script; } else if (create) { const scr = { tag: script, script: { defaultLangSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [], }, langSysRecords: [], }, }; scripts.splice(-1 - pos, 0, scr); return scr.script; } } }, /** * Returns a language system table * @instance * @param {string} [script='DFLT'] * @param {string} [language='dlft'] * @param {boolean} create - forces the creation of this langSysTable if it doesn't exist. * @return {Object} */ getLangSysTable: function (script, language, create) { const scriptTable = this.getScriptTable(script, create); if (scriptTable) { if (!language || language === 'dflt' || language === 'DFLT') { return scriptTable.defaultLangSys; } const pos = searchTag(scriptTable.langSysRecords, language); if (pos >= 0) { return scriptTable.langSysRecords[pos].langSys; } else if (create) { const langSysRecord = { tag: language, langSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [], }, }; scriptTable.langSysRecords.splice(-1 - pos, 0, langSysRecord); return langSysRecord.langSys; } } }, /** * Get a specific feature table. * @instance * @param {string} [script='DFLT'] * @param {string} [language='dlft'] * @param {string} feature - One of the codes listed at https://www.microsoft.com/typography/OTSPEC/featurelist.htm * @param {boolean} create - forces the creation of the feature table if it doesn't exist. * @return {Object} */ getFeatureTable: function (script, language, feature, create) { const langSysTable = this.getLangSysTable(script, language, create); if (langSysTable) { let featureRecord; const featIndexes = langSysTable.featureIndexes; const allFeatures = this.font.tables[this.tableName].features; // The FeatureIndex array of indices is in arbitrary order, // even if allFeatures is sorted alphabetically by feature tag. for (let i = 0; i < featIndexes.length; i++) { featureRecord = allFeatures[featIndexes[i]]; if (featureRecord.tag === feature) { return featureRecord.feature; } } if (create) { const index = allFeatures.length; // Automatic ordering of features would require to shift feature indexes in the script list. check.assert( index === 0 || feature >= allFeatures[index - 1].tag, 'Features must be added in alphabetical order.' ); featureRecord = { tag: feature, feature: { params: 0, lookupListIndexes: [] }, }; allFeatures.push(featureRecord); featIndexes.push(index); return featureRecord.feature; } } }, /** * Get the lookup tables of a given type for a script/language/feature. * @instance * @param {string} [script='DFLT'] * @param {string} [language='dlft'] * @param {string} feature - 4-letter feature code * @param {number} lookupType - 1 to 9 * @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables. * @return {Object[]} */ getLookupTables: function (script, language, feature, lookupType, create) { const featureTable = this.getFeatureTable( script, language, feature, create ); const tables = []; if (featureTable) { let lookupTable; const lookupListIndexes = featureTable.lookupListIndexes; const allLookups = this.font.tables[this.tableName].lookups; // lookupListIndexes are in no particular order, so use naive search. for (let i = 0; i < lookupListIndexes.length; i++) { lookupTable = allLookups[lookupListIndexes[i]]; if (lookupTable.lookupType === lookupType) { tables.push(lookupTable); } } if (tables.length === 0 && create) { lookupTable = { lookupType: lookupType, lookupFlag: 0, subtables: [], markFilteringSet: undefined, }; const index = allLookups.length; allLookups.push(lookupTable); lookupListIndexes.push(index); return [lookupTable]; } } return tables; }, /** * Find a glyph in a class definition table * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table * @param {object} classDefTable - an OpenType Layout class definition table * @param {number} glyphIndex - the index of the glyph to find * @returns {number} -1 if not found */ getGlyphClass: function (classDefTable, glyphIndex) { switch (classDefTable.format) { case 1: if ( classDefTable.startGlyph <= glyphIndex && glyphIndex < classDefTable.startGlyph + classDefTable.classes.length ) { return classDefTable.classes[ glyphIndex - classDefTable.startGlyph ]; } return 0; case 2: const range = searchRange(classDefTable.ranges, glyphIndex); return range ? range.classId : 0; } }, /** * Find a glyph in a coverage table * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-table * @param {object} coverageTable - an OpenType Layout coverage table * @param {number} glyphIndex - the index of the glyph to find * @returns {number} -1 if not found */ getCoverageIndex: function (coverageTable, glyphIndex) { switch (coverageTable.format) { case 1: const index = binSearch(coverageTable.glyphs, glyphIndex); return index >= 0 ? index : -1; case 2: const range = searchRange(coverageTable.ranges, glyphIndex); return range ? range.index + glyphIndex - range.start : -1; } }, /** * Returns the list of glyph indexes of a coverage table. * Format 1: the list is stored raw * Format 2: compact list as range records. * @instance * @param {Object} coverageTable * @return {Array} */ expandCoverage: function (coverageTable) { if (coverageTable.format === 1) { return coverageTable.glyphs; } else { const glyphs = []; const ranges = coverageTable.ranges; for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; const start = range.start; const end = range.end; for (let j = start; j <= end; j++) { glyphs.push(j); } } return glyphs; } }, }; export default Layout;