UNPKG

opentype.js

Version:
369 lines (350 loc) 14.2 kB
// 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;