448 lines
14 KiB
Plaintext
448 lines
14 KiB
Plaintext
|
// The Substitution object provides utility methods to manipulate
|
||
|
// the GSUB substitution table.
|
||
|
|
||
|
import check from './check';
|
||
|
import Layout from './layout';
|
||
|
|
||
|
/**
|
||
|
* @exports opentype.Substitution
|
||
|
* @class
|
||
|
* @extends opentype.Layout
|
||
|
* @param {opentype.Font}
|
||
|
* @constructor
|
||
|
*/
|
||
|
function Substitution(font) {
|
||
|
Layout.call(this, font, 'gsub');
|
||
|
}
|
||
|
|
||
|
// Check if 2 arrays of primitives are equal.
|
||
|
function arraysEqual(ar1, ar2) {
|
||
|
const n = ar1.length;
|
||
|
if (n !== ar2.length) {
|
||
|
return false;
|
||
|
}
|
||
|
for (let i = 0; i < n; i++) {
|
||
|
if (ar1[i] !== ar2[i]) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Find the first subtable of a lookup table in a particular format.
|
||
|
function getSubstFormat(lookupTable, format, defaultSubtable) {
|
||
|
const subtables = lookupTable.subtables;
|
||
|
for (let i = 0; i < subtables.length; i++) {
|
||
|
const subtable = subtables[i];
|
||
|
if (subtable.substFormat === format) {
|
||
|
return subtable;
|
||
|
}
|
||
|
}
|
||
|
if (defaultSubtable) {
|
||
|
subtables.push(defaultSubtable);
|
||
|
return defaultSubtable;
|
||
|
}
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
Substitution.prototype = Layout.prototype;
|
||
|
|
||
|
/**
|
||
|
* Create a default GSUB table.
|
||
|
* @return {Object} gsub - The GSUB table.
|
||
|
*/
|
||
|
Substitution.prototype.createDefaultTable = function () {
|
||
|
// Generate a default empty GSUB table with just a DFLT script and dflt lang sys.
|
||
|
return {
|
||
|
version: 1,
|
||
|
scripts: [
|
||
|
{
|
||
|
tag: 'DFLT',
|
||
|
script: {
|
||
|
defaultLangSys: {
|
||
|
reserved: 0,
|
||
|
reqFeatureIndex: 0xffff,
|
||
|
featureIndexes: [],
|
||
|
},
|
||
|
langSysRecords: [],
|
||
|
},
|
||
|
},
|
||
|
],
|
||
|
features: [],
|
||
|
lookups: [],
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List all single substitutions (lookup type 1) for a given script, language, and feature.
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
* @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...)
|
||
|
* @return {Array} substitutions - The list of substitutions.
|
||
|
*/
|
||
|
Substitution.prototype.getSingle = function (feature, script, language) {
|
||
|
const substitutions = [];
|
||
|
const lookupTables = this.getLookupTables(script, language, feature, 1);
|
||
|
for (let idx = 0; idx < lookupTables.length; idx++) {
|
||
|
const subtables = lookupTables[idx].subtables;
|
||
|
for (let i = 0; i < subtables.length; i++) {
|
||
|
const subtable = subtables[i];
|
||
|
const glyphs = this.expandCoverage(subtable.coverage);
|
||
|
let j;
|
||
|
if (subtable.substFormat === 1) {
|
||
|
const delta = subtable.deltaGlyphId;
|
||
|
for (j = 0; j < glyphs.length; j++) {
|
||
|
const glyph = glyphs[j];
|
||
|
substitutions.push({ sub: glyph, by: glyph + delta });
|
||
|
}
|
||
|
} else {
|
||
|
const substitute = subtable.substitute;
|
||
|
for (j = 0; j < glyphs.length; j++) {
|
||
|
substitutions.push({ sub: glyphs[j], by: substitute[j] });
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return substitutions;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List all multiple substitutions (lookup type 2) for a given script, language, and feature.
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
* @param {string} feature - 4-character feature name ('ccmp', 'stch')
|
||
|
* @return {Array} substitutions - The list of substitutions.
|
||
|
*/
|
||
|
Substitution.prototype.getMultiple = function (feature, script, language) {
|
||
|
const substitutions = [];
|
||
|
const lookupTables = this.getLookupTables(script, language, feature, 2);
|
||
|
for (let idx = 0; idx < lookupTables.length; idx++) {
|
||
|
const subtables = lookupTables[idx].subtables;
|
||
|
for (let i = 0; i < subtables.length; i++) {
|
||
|
const subtable = subtables[i];
|
||
|
const glyphs = this.expandCoverage(subtable.coverage);
|
||
|
let j;
|
||
|
|
||
|
for (j = 0; j < glyphs.length; j++) {
|
||
|
const glyph = glyphs[j];
|
||
|
const replacements = subtable.sequences[j];
|
||
|
substitutions.push({ sub: glyph, by: replacements });
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return substitutions;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List all alternates (lookup type 3) for a given script, language, and feature.
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
* @param {string} feature - 4-character feature name ('aalt', 'salt'...)
|
||
|
* @return {Array} alternates - The list of alternates
|
||
|
*/
|
||
|
Substitution.prototype.getAlternates = function (feature, script, language) {
|
||
|
const alternates = [];
|
||
|
const lookupTables = this.getLookupTables(script, language, feature, 3);
|
||
|
for (let idx = 0; idx < lookupTables.length; idx++) {
|
||
|
const subtables = lookupTables[idx].subtables;
|
||
|
for (let i = 0; i < subtables.length; i++) {
|
||
|
const subtable = subtables[i];
|
||
|
const glyphs = this.expandCoverage(subtable.coverage);
|
||
|
const alternateSets = subtable.alternateSets;
|
||
|
for (let j = 0; j < glyphs.length; j++) {
|
||
|
alternates.push({ sub: glyphs[j], by: alternateSets[j] });
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return alternates;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List all ligatures (lookup type 4) for a given script, language, and feature.
|
||
|
* The result is an array of ligature objects like { sub: [ids], by: id }
|
||
|
* @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
* @return {Array} ligatures - The list of ligatures.
|
||
|
*/
|
||
|
Substitution.prototype.getLigatures = function (feature, script, language) {
|
||
|
const ligatures = [];
|
||
|
const lookupTables = this.getLookupTables(script, language, feature, 4);
|
||
|
for (let idx = 0; idx < lookupTables.length; idx++) {
|
||
|
const subtables = lookupTables[idx].subtables;
|
||
|
for (let i = 0; i < subtables.length; i++) {
|
||
|
const subtable = subtables[i];
|
||
|
const glyphs = this.expandCoverage(subtable.coverage);
|
||
|
const ligatureSets = subtable.ligatureSets;
|
||
|
for (let j = 0; j < glyphs.length; j++) {
|
||
|
const startGlyph = glyphs[j];
|
||
|
const ligSet = ligatureSets[j];
|
||
|
for (let k = 0; k < ligSet.length; k++) {
|
||
|
const lig = ligSet[k];
|
||
|
ligatures.push({
|
||
|
sub: [startGlyph].concat(lig.components),
|
||
|
by: lig.ligGlyph,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return ligatures;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add or modify a single substitution (lookup type 1)
|
||
|
* Format 2, more flexible, is always used.
|
||
|
* @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
|
||
|
* @param {Object} substitution - { sub: id, by: id } (format 1 is not supported)
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
*/
|
||
|
Substitution.prototype.addSingle = function (
|
||
|
feature,
|
||
|
substitution,
|
||
|
script,
|
||
|
language
|
||
|
) {
|
||
|
const lookupTable = this.getLookupTables(
|
||
|
script,
|
||
|
language,
|
||
|
feature,
|
||
|
1,
|
||
|
true
|
||
|
)[0];
|
||
|
const subtable = getSubstFormat(lookupTable, 2, {
|
||
|
// lookup type 1 subtable, format 2, coverage format 1
|
||
|
substFormat: 2,
|
||
|
coverage: { format: 1, glyphs: [] },
|
||
|
substitute: [],
|
||
|
});
|
||
|
check.assert(
|
||
|
subtable.coverage.format === 1,
|
||
|
'Single: unable to modify coverage table format ' +
|
||
|
subtable.coverage.format
|
||
|
);
|
||
|
const coverageGlyph = substitution.sub;
|
||
|
let pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
|
||
|
if (pos < 0) {
|
||
|
pos = -1 - pos;
|
||
|
subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
|
||
|
subtable.substitute.splice(pos, 0, 0);
|
||
|
}
|
||
|
subtable.substitute[pos] = substitution.by;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add or modify a multiple substitution (lookup type 2)
|
||
|
* @param {string} feature - 4-letter feature name ('ccmp', 'stch')
|
||
|
* @param {Object} substitution - { sub: id, by: [id] } for format 2.
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
*/
|
||
|
Substitution.prototype.addMultiple = function (
|
||
|
feature,
|
||
|
substitution,
|
||
|
script,
|
||
|
language
|
||
|
) {
|
||
|
check.assert(
|
||
|
substitution.by instanceof Array && substitution.by.length > 1,
|
||
|
'Multiple: "by" must be an array of two or more ids'
|
||
|
);
|
||
|
const lookupTable = this.getLookupTables(
|
||
|
script,
|
||
|
language,
|
||
|
feature,
|
||
|
2,
|
||
|
true
|
||
|
)[0];
|
||
|
const subtable = getSubstFormat(lookupTable, 1, {
|
||
|
// lookup type 2 subtable, format 1, coverage format 1
|
||
|
substFormat: 1,
|
||
|
coverage: { format: 1, glyphs: [] },
|
||
|
sequences: [],
|
||
|
});
|
||
|
check.assert(
|
||
|
subtable.coverage.format === 1,
|
||
|
'Multiple: unable to modify coverage table format ' +
|
||
|
subtable.coverage.format
|
||
|
);
|
||
|
const coverageGlyph = substitution.sub;
|
||
|
let pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
|
||
|
if (pos < 0) {
|
||
|
pos = -1 - pos;
|
||
|
subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
|
||
|
subtable.sequences.splice(pos, 0, 0);
|
||
|
}
|
||
|
subtable.sequences[pos] = substitution.by;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add or modify an alternate substitution (lookup type 3)
|
||
|
* @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
|
||
|
* @param {Object} substitution - { sub: id, by: [ids] }
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
*/
|
||
|
Substitution.prototype.addAlternate = function (
|
||
|
feature,
|
||
|
substitution,
|
||
|
script,
|
||
|
language
|
||
|
) {
|
||
|
const lookupTable = this.getLookupTables(
|
||
|
script,
|
||
|
language,
|
||
|
feature,
|
||
|
3,
|
||
|
true
|
||
|
)[0];
|
||
|
const subtable = getSubstFormat(lookupTable, 1, {
|
||
|
// lookup type 3 subtable, format 1, coverage format 1
|
||
|
substFormat: 1,
|
||
|
coverage: { format: 1, glyphs: [] },
|
||
|
alternateSets: [],
|
||
|
});
|
||
|
check.assert(
|
||
|
subtable.coverage.format === 1,
|
||
|
'Alternate: unable to modify coverage table format ' +
|
||
|
subtable.coverage.format
|
||
|
);
|
||
|
const coverageGlyph = substitution.sub;
|
||
|
let pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
|
||
|
if (pos < 0) {
|
||
|
pos = -1 - pos;
|
||
|
subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
|
||
|
subtable.alternateSets.splice(pos, 0, 0);
|
||
|
}
|
||
|
subtable.alternateSets[pos] = substitution.by;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add a ligature (lookup type 4)
|
||
|
* Ligatures with more components must be stored ahead of those with fewer components in order to be found
|
||
|
* @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
|
||
|
* @param {Object} ligature - { sub: [ids], by: id }
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
*/
|
||
|
Substitution.prototype.addLigature = function (
|
||
|
feature,
|
||
|
ligature,
|
||
|
script,
|
||
|
language
|
||
|
) {
|
||
|
const lookupTable = this.getLookupTables(
|
||
|
script,
|
||
|
language,
|
||
|
feature,
|
||
|
4,
|
||
|
true
|
||
|
)[0];
|
||
|
let subtable = lookupTable.subtables[0];
|
||
|
if (!subtable) {
|
||
|
subtable = {
|
||
|
// lookup type 4 subtable, format 1, coverage format 1
|
||
|
substFormat: 1,
|
||
|
coverage: { format: 1, glyphs: [] },
|
||
|
ligatureSets: [],
|
||
|
};
|
||
|
lookupTable.subtables[0] = subtable;
|
||
|
}
|
||
|
check.assert(
|
||
|
subtable.coverage.format === 1,
|
||
|
'Ligature: unable to modify coverage table format ' +
|
||
|
subtable.coverage.format
|
||
|
);
|
||
|
const coverageGlyph = ligature.sub[0];
|
||
|
const ligComponents = ligature.sub.slice(1);
|
||
|
const ligatureTable = {
|
||
|
ligGlyph: ligature.by,
|
||
|
components: ligComponents,
|
||
|
};
|
||
|
let pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
|
||
|
if (pos >= 0) {
|
||
|
// ligatureSet already exists
|
||
|
const ligatureSet = subtable.ligatureSets[pos];
|
||
|
for (let i = 0; i < ligatureSet.length; i++) {
|
||
|
// If ligature already exists, return.
|
||
|
if (arraysEqual(ligatureSet[i].components, ligComponents)) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
// ligature does not exist: add it.
|
||
|
ligatureSet.push(ligatureTable);
|
||
|
} else {
|
||
|
// Create a new ligatureSet and add coverage for the first glyph.
|
||
|
pos = -1 - pos;
|
||
|
subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
|
||
|
subtable.ligatureSets.splice(pos, 0, [ligatureTable]);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* List all feature data for a given script and language.
|
||
|
* @param {string} feature - 4-letter feature name
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
* @return {Array} substitutions - The list of substitutions.
|
||
|
*/
|
||
|
Substitution.prototype.getFeature = function (feature, script, language) {
|
||
|
if (/ss\d\d/.test(feature)) {
|
||
|
// ss01 - ss20
|
||
|
return this.getSingle(feature, script, language);
|
||
|
}
|
||
|
switch (feature) {
|
||
|
case 'aalt':
|
||
|
case 'salt':
|
||
|
return this.getSingle(feature, script, language).concat(
|
||
|
this.getAlternates(feature, script, language)
|
||
|
);
|
||
|
case 'dlig':
|
||
|
case 'liga':
|
||
|
case 'rlig':
|
||
|
return this.getLigatures(feature, script, language);
|
||
|
case 'ccmp':
|
||
|
return this.getMultiple(feature, script, language).concat(
|
||
|
this.getLigatures(feature, script, language)
|
||
|
);
|
||
|
case 'stch':
|
||
|
return this.getMultiple(feature, script, language);
|
||
|
}
|
||
|
return undefined;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add a substitution to a feature for a given script and language.
|
||
|
* @param {string} feature - 4-letter feature name
|
||
|
* @param {Object} sub - the substitution to add (an object like { sub: id or [ids], by: id or [ids] })
|
||
|
* @param {string} [script='DFLT']
|
||
|
* @param {string} [language='dflt']
|
||
|
*/
|
||
|
Substitution.prototype.add = function (feature, sub, script, language) {
|
||
|
if (/ss\d\d/.test(feature)) {
|
||
|
// ss01 - ss20
|
||
|
return this.addSingle(feature, sub, script, language);
|
||
|
}
|
||
|
switch (feature) {
|
||
|
case 'aalt':
|
||
|
case 'salt':
|
||
|
if (typeof sub.by === 'number') {
|
||
|
return this.addSingle(feature, sub, script, language);
|
||
|
}
|
||
|
return this.addAlternate(feature, sub, script, language);
|
||
|
case 'dlig':
|
||
|
case 'liga':
|
||
|
case 'rlig':
|
||
|
return this.addLigature(feature, sub, script, language);
|
||
|
case 'ccmp':
|
||
|
if (sub.by instanceof Array) {
|
||
|
return this.addMultiple(feature, sub, script, language);
|
||
|
}
|
||
|
return this.addLigature(feature, sub, script, language);
|
||
|
}
|
||
|
return undefined;
|
||
|
};
|
||
|
|
||
|
export default Substitution;
|