opentype.features.js
Version:
Library to list opentype fonts optional features.
480 lines (479 loc) • 18.6 kB
JavaScript
"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;