UNPKG

opentype.features.js

Version:

Library to list opentype fonts optional features.

480 lines (479 loc) 18.6 kB
"use strict"; /** * Query a feature by some of it's properties to lookup a glyph substitution. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SubstitutionAction = exports.FeatureQuery = void 0; var char_1 = require("../char"); /** * Create feature query instance * @param {Font} font opentype font instance */ function FeatureQuery(font) { this.font = font; this.features = {}; } exports.FeatureQuery = FeatureQuery; /** * @typedef SubstitutionAction * @type Object * @property {number} id substitution type * @property {string} tag feature tag * @property {any} substitution substitution value(s) */ /** * Create a substitution action instance * @param {SubstitutionAction} action */ function SubstitutionAction(action) { this.id = action.id; this.tag = action.tag; this.substitution = action.substitution; } exports.SubstitutionAction = SubstitutionAction; /** * Lookup a coverage table * @param {number} glyphIndex glyph index * @param {CoverageTable} coverage coverage table */ function lookupCoverage(glyphIndex, coverage) { if (!glyphIndex) return -1; switch (coverage.format) { case 1: return coverage.glyphs.indexOf(glyphIndex); case 2: var ranges = coverage.ranges; for (var i = 0; i < ranges.length; i++) { var range = ranges[i]; if (glyphIndex >= range.start && glyphIndex <= range.end) { var offset = glyphIndex - range.start; return range.index + offset; } } break; default: return -1; // not found } return -1; } /** * Handle a single substitution - format 1 * @param {ContextParams} contextParams context params to lookup */ function singleSubstitutionFormat1(glyphIndex, subtable) { var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage); if (substituteIndex === -1) return null; return glyphIndex + subtable.deltaGlyphId; } /** * Handle a single substitution - format 2 * @param {ContextParams} contextParams context params to lookup */ function singleSubstitutionFormat2(glyphIndex, subtable) { var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage); if (substituteIndex === -1) return null; return subtable.substitute[substituteIndex]; } /** * Lookup a list of coverage tables * @param {any} coverageList a list of coverage tables * @param {ContextParams} contextParams context params to lookup */ function lookupCoverageList(coverageList, contextParams) { var lookupList = []; for (var i = 0; i < coverageList.length; i++) { var coverage = coverageList[i]; var glyphIndex = contextParams.current; glyphIndex = Array.isArray(glyphIndex) ? glyphIndex[0] : glyphIndex; var lookupIndex = lookupCoverage(glyphIndex, coverage); if (lookupIndex !== -1) { lookupList.push(lookupIndex); } } if (lookupList.length !== coverageList.length) return -1; return lookupList; } /** * Handle alternate substitution - format 1 * @param {ContextParams} contextParams context params to lookup */ function alternateSubstitutionFormat1(glyphIndex, subtable) { return [1, 2, 3]; } /** * Handle chaining context substitution - format 3 * @param {ContextParams} contextParams context params to lookup */ function chainingSubstitutionFormat3(contextParams, subtable) { var lookupsCount = (subtable.inputCoverage.length + subtable.lookaheadCoverage.length + subtable.backtrackCoverage.length); if (contextParams.context.length < lookupsCount) return []; // INPUT LOOKUP // var inputLookups = lookupCoverageList(subtable.inputCoverage, contextParams); if (inputLookups === -1) return []; // LOOKAHEAD LOOKUP // var lookaheadOffset = subtable.inputCoverage.length - 1; if (contextParams.lookahead.length < subtable.lookaheadCoverage.length) return []; var lookaheadContext = contextParams.lookahead.slice(lookaheadOffset); while (lookaheadContext.length && (0, char_1.isTashkeelArabicChar)(lookaheadContext[0].char)) { lookaheadContext.shift(); } var lookaheadParams = new ContextParams(lookaheadContext, 0); var lookaheadLookups = lookupCoverageList(subtable.lookaheadCoverage, lookaheadParams); // BACKTRACK LOOKUP // var backtrackContext = [].concat(contextParams.backtrack); backtrackContext.reverse(); while (backtrackContext.length && (0, char_1.isTashkeelArabicChar)(backtrackContext[0].char)) { backtrackContext.shift(); } if (backtrackContext.length < subtable.backtrackCoverage.length) return []; var backtrackParams = new ContextParams(backtrackContext, 0); var backtrackLookups = lookupCoverageList(subtable.backtrackCoverage, backtrackParams); var contextRulesMatch = (inputLookups.length === subtable.inputCoverage.length && lookaheadLookups.length === subtable.lookaheadCoverage.length && backtrackLookups.length === subtable.backtrackCoverage.length); var substitutions = []; if (contextRulesMatch) { for (var i = 0; i < subtable.lookupRecords.length; i++) { var lookupRecord = subtable.lookupRecords[i]; var lookupListIndex = lookupRecord.lookupListIndex; var lookupTable = this.getLookupByIndex(lookupListIndex); for (var s = 0; s < lookupTable.subtables.length; s++) { var subtable_1 = lookupTable.subtables[s]; var lookup = this.getLookupMethod(lookupTable, subtable_1); var substitutionType = this.getSubstitutionType(lookupTable, subtable_1); if (substitutionType === '12') { for (var n = 0; n < inputLookups.length; n++) { var glyphIndex = contextParams.get(n); var substitution = lookup(glyphIndex); if (substitution) substitutions.push(substitution); } } } } } return substitutions; } /** * Handle ligature substitution - format 1 * @param {ContextParams} contextParams context params to lookup */ function ligatureSubstitutionFormat1(contextParams, subtable) { // COVERAGE LOOKUP // var glyphIndex = contextParams.current; var ligSetIndex = lookupCoverage(glyphIndex, subtable.coverage); if (ligSetIndex === -1) return null; // COMPONENTS LOOKUP // (!) note, components are ordered in the written direction. var ligature; var ligatureSet = subtable.ligatureSets[ligSetIndex]; for (var s = 0; s < ligatureSet.length; s++) { ligature = ligatureSet[s]; for (var l = 0; l < ligature.components.length; l++) { var lookaheadItem = contextParams.lookahead[l]; var component = ligature.components[l]; if (lookaheadItem !== component) break; if (l === ligature.components.length - 1) return ligature; } } return null; } /** * Handle decomposition substitution - format 1 * @param {number} glyphIndex glyph index * @param {any} subtable subtable */ function decompositionSubstitutionFormat1(glyphIndex, subtable) { var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage); if (substituteIndex === -1) return null; return subtable.sequences[substituteIndex]; } /** * Get default script features indexes */ FeatureQuery.prototype.getDefaultScriptFeaturesIndexes = function () { var scripts = this.font.tables.gsub.scripts; for (var s = 0; s < scripts.length; s++) { var script = scripts[s]; if (script.tag === 'DFLT') return (script.script.defaultLangSys.featureIndexes); } return []; }; /** * Get feature indexes of a specific script * @param {string} scriptTag script tag */ FeatureQuery.prototype.getScriptFeaturesIndexes = function (scriptTag) { var tables = this.font.tables; if (!tables.gsub) return []; if (!scriptTag) return this.getDefaultScriptFeaturesIndexes(); var scripts = this.font.tables.gsub.scripts; for (var i = 0; i < scripts.length; i++) { var script = scripts[i]; if (script.tag === scriptTag && script.script.defaultLangSys) { return script.script.defaultLangSys.featureIndexes; } else { var langSysRecords = script.langSysRecords; if (!!langSysRecords) { for (var j = 0; j < langSysRecords.length; j++) { var langSysRecord = langSysRecords[j]; if (langSysRecord.tag === scriptTag) { var langSys = langSysRecord.langSys; return langSys.featureIndexes; } } } } } return this.getDefaultScriptFeaturesIndexes(); }; /** * Map a feature tag to a gsub feature * @param {any} features gsub features * @param {string} scriptTag script tag */ FeatureQuery.prototype.mapTagsToFeatures = function (features, scriptTag) { var tags = {}; for (var i = 0; i < features.length; i++) { var tag = features[i].tag; var feature = features[i].feature; tags[tag] = feature; } this.features[scriptTag].tags = tags; }; /** * Get features of a specific script * @param {string} scriptTag script tag */ FeatureQuery.prototype.getScriptFeatures = function (scriptTag) { var features = this.features[scriptTag]; if (this.features.hasOwnProperty(scriptTag)) return features; var featuresIndexes = this.getScriptFeaturesIndexes(scriptTag); if (!featuresIndexes) return null; var gsub = this.font.tables.gsub; features = featuresIndexes.map(function (index) { return gsub.features[index]; }); this.features[scriptTag] = features; this.mapTagsToFeatures(features, scriptTag); return features; }; /** * Get substitution type * @param {any} lookupTable lookup table * @param {any} subtable subtable */ FeatureQuery.prototype.getSubstitutionType = function (lookupTable, subtable) { var lookupType = lookupTable.lookupType.toString(); var substFormat = subtable.substFormat.toString(); return lookupType + substFormat; }; /** * Get lookup method * @param {any} lookupTable lookup table * @param {any} subtable subtable */ FeatureQuery.prototype.getLookupMethod = function (lookupTable, subtable) { var _this = this; var substitutionType = this.getSubstitutionType(lookupTable, subtable); switch (substitutionType) { case '11': return function (glyphIndex) { return singleSubstitutionFormat1.apply(_this, [glyphIndex, subtable]); }; case '12': return function (glyphIndex) { return singleSubstitutionFormat2.apply(_this, [glyphIndex, subtable]); }; case '63': return function (contextParams) { return chainingSubstitutionFormat3.apply(_this, [contextParams, subtable]); }; case '41': return function (contextParams) { return ligatureSubstitutionFormat1.apply(_this, [contextParams, subtable]); }; case '21': return function (glyphIndex) { return decompositionSubstitutionFormat1.apply(_this, [glyphIndex, subtable]); }; case '31': return function (glyphIndex) { return alternateSubstitutionFormat1.apply(_this, [glyphIndex, subtable]); }; default: throw new Error("lookupType: ".concat(lookupTable.lookupType, " - ") + "substFormat: ".concat(subtable.substFormat, " ") + "is not yet supported"); } }; /** * [ LOOKUP TYPES ] * ------------------------------- * Single 1; * Multiple 2; * Alternate 3; * Ligature 4; * Context 5; * ChainingContext 6; * ExtensionSubstitution 7; * ReverseChainingContext 8; * ------------------------------- * */ /** * @typedef FQuery * @type Object * @param {string} tag feature tag * @param {string} script feature script * @param {ContextParams} contextParams context params */ /** * Lookup a feature using a query parameters * @param {FQuery} query feature query */ FeatureQuery.prototype.lookupFeature = function (query) { var contextParams = query.contextParams; var currentIndex = contextParams.index; var feature = this.getFeature({ tag: query.tag, script: query.script }); if (!feature) return new Error("font '".concat(this.font.names.fullName.en, "' ") + "doesn't support feature '".concat(query.tag, "' ") + "for script '".concat(query.script, "'.")); var lookups = this.getFeatureLookups(feature); var substitutions = [].concat(contextParams.context); for (var l = 0; l < lookups.length; l++) { var lookupTable = lookups[l]; var subtables = this.getLookupSubtables(lookupTable); for (var s = 0; s < subtables.length; s++) { var subtable = subtables[s]; var substType = this.getSubstitutionType(lookupTable, subtable); var lookup = this.getLookupMethod(lookupTable, subtable); var substitution = void 0; switch (substType) { case '11': substitution = lookup(contextParams.current); if (substitution) { substitutions.splice(currentIndex, 1, new SubstitutionAction({ id: 11, tag: query.tag, substitution: substitution })); } break; case '12': substitution = lookup(contextParams.current); if (substitution) { substitutions.splice(currentIndex, 1, new SubstitutionAction({ id: 12, tag: query.tag, substitution: substitution })); } break; case '63': substitution = lookup(contextParams); if (Array.isArray(substitution) && substitution.length) { substitutions.splice(currentIndex, 1, new SubstitutionAction({ id: 63, tag: query.tag, substitution: substitution })); } break; case '41': substitution = lookup(contextParams); if (substitution) { substitutions.splice(currentIndex, 1, new SubstitutionAction({ id: 41, tag: query.tag, substitution: substitution })); } break; case '21': substitution = lookup(contextParams.current); if (substitution) { substitutions.splice(currentIndex, 1, new SubstitutionAction({ id: 21, tag: query.tag, substitution: substitution })); } break; case '31': substitution = lookup(contextParams.current); if (substitution) { substitutions.splice(currentIndex, 1, new SubstitutionAction({ id: 31, tag: query.tag, substitution: substitution })); } break; } contextParams = new ContextParams(substitutions, currentIndex); if (Array.isArray(substitution) && !substitution.length) continue; substitution = null; } } return substitutions.length ? substitutions : null; }; /** * Checks if a font supports a specific features * @param {FQuery} query feature query object */ FeatureQuery.prototype.supports = function (query) { if (!query.script) return false; this.getScriptFeatures(query.script); var supportedScript = this.features.hasOwnProperty(query.script); if (!query.tag) return supportedScript; var supportedFeature = (this.features[query.script].some(function (feature) { return feature.tag === query.tag; })); return supportedScript && supportedFeature; }; /** * Get lookup table subtables * @param {any} lookupTable lookup table */ FeatureQuery.prototype.getLookupSubtables = function (lookupTable) { return lookupTable.subtables || null; }; /** * Get lookup table by index * @param {number} index lookup table index */ FeatureQuery.prototype.getLookupByIndex = function (index) { var lookups = this.font.tables.gsub.lookups; return lookups[index] || null; }; /** * Get lookup tables for a feature * @param {string} feature */ FeatureQuery.prototype.getFeatureLookups = function (feature) { // TODO: memoize return feature.lookupListIndexes.map(this.getLookupByIndex.bind(this)); }; /** * Query a feature by it's properties * @param {any} query an object that describes the properties of a query */ FeatureQuery.prototype.getFeature = function getFeature(query) { if (!this.font) return { FAIL: "No font was found" }; if (!this.features.hasOwnProperty(query.script)) { this.getScriptFeatures(query.script); } var scriptFeatures = this.features[query.script]; if (!scriptFeatures) return ({ FAIL: "No feature for script ".concat(query.script) }); if (!scriptFeatures.tags[query.tag]) return null; return this.features[query.script].tags[query.tag]; }; exports.default = FeatureQuery;