UNPKG

font-ligatures

Version:

Find ligature replacements for any system font

239 lines 9.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const opentype = require("opentype.js"); const lru = require("lru-cache"); const merge_1 = require("./merge"); const walk_1 = require("./walk"); const mergeRange_1 = require("./mergeRange"); const _6_1_1 = require("./processors/6-1"); const _6_2_1 = require("./processors/6-2"); const _6_3_1 = require("./processors/6-3"); const _8_1_1 = require("./processors/8-1"); const flatten_1 = require("./flatten"); class FontImpl { constructor(font, options) { this._lookupTrees = []; this._glyphLookups = {}; this._font = font; if (options.cacheSize > 0) { this._cache = new lru({ max: options.cacheSize, length: ((val, key) => key.length) }); } const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter(f => f.tag === 'calt') || []; const lookupIndices = caltFeatures .reduce((acc, val) => [...acc, ...val.feature.lookupListIndexes], []); const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || []; const lookupGroups = allLookups.filter((l, i) => lookupIndices.some(idx => idx === i)); for (const [index, lookup] of lookupGroups.entries()) { const trees = []; switch (lookup.lookupType) { case 6: for (const [index, table] of lookup.subtables.entries()) { switch (table.substFormat) { case 1: trees.push(_6_1_1.default(table, allLookups, index)); break; case 2: trees.push(_6_2_1.default(table, allLookups, index)); break; case 3: trees.push(_6_3_1.default(table, allLookups, index)); break; } } break; case 8: for (const [index, table] of lookup.subtables.entries()) { trees.push(_8_1_1.default(table, index)); } break; } const tree = flatten_1.default(merge_1.default(trees)); this._lookupTrees.push({ tree, processForward: lookup.lookupType !== 8 }); for (const glyphId of Object.keys(tree)) { if (!this._glyphLookups[glyphId]) { this._glyphLookups[glyphId] = []; } this._glyphLookups[glyphId].push(index); } } } findLigatures(text) { const cached = this._cache && this._cache.get(text); if (cached && !Array.isArray(cached)) { return cached; } const glyphIds = []; for (const char of text) { glyphIds.push(this._font.charToGlyphIndex(char)); } // If there are no lookup groups, there's no point looking for // replacements. This gives us a minor performance boost for fonts with // no ligatures if (this._lookupTrees.length === 0) { return { inputGlyphs: glyphIds, outputGlyphs: glyphIds, contextRanges: [] }; } const result = this._findInternal(glyphIds.slice()); const finalResult = { inputGlyphs: glyphIds, outputGlyphs: result.sequence, contextRanges: result.ranges }; if (this._cache) { this._cache.set(text, finalResult); } return finalResult; } findLigatureRanges(text) { // Short circuit the process if there are no possible ligatures in the // font if (this._lookupTrees.length === 0) { return []; } const cached = this._cache && this._cache.get(text); if (cached) { return Array.isArray(cached) ? cached : cached.contextRanges; } const glyphIds = []; for (const char of text) { glyphIds.push(this._font.charToGlyphIndex(char)); } const result = this._findInternal(glyphIds); if (this._cache) { this._cache.set(text, result.ranges); } return result.ranges; } _findInternal(sequence) { const ranges = []; let nextLookup = this._getNextLookup(sequence, 0); while (nextLookup.index !== null) { const lookup = this._lookupTrees[nextLookup.index]; if (lookup.processForward) { let lastGlyphIndex = nextLookup.last; for (let i = nextLookup.first; i < lastGlyphIndex; i++) { const result = walk_1.default(lookup.tree, sequence, i, i); if (result) { for (let j = 0; j < result.substitutions.length; j++) { const sub = result.substitutions[j]; if (sub !== null) { sequence[i + j] = sub; } } mergeRange_1.default(ranges, result.contextRange[0] + i, result.contextRange[1] + i); // Substitutions can end up extending the search range if (i + result.length >= lastGlyphIndex) { lastGlyphIndex = i + result.length + 1; } i += result.length - 1; } } } else { // We don't need to do the lastGlyphIndex tracking here because // reverse processing isn't allowed to replace more than one // character at a time. for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) { const result = walk_1.default(lookup.tree, sequence, i, i); if (result) { for (let j = 0; j < result.substitutions.length; j++) { const sub = result.substitutions[j]; if (sub !== null) { sequence[i + j] = sub; } } mergeRange_1.default(ranges, result.contextRange[0] + i, result.contextRange[1] + i); i -= result.length - 1; } } } nextLookup = this._getNextLookup(sequence, nextLookup.index + 1); } return { sequence, ranges }; } /** * Returns the lookup and glyph range for the first lookup that might * contain a match. * * @param sequence Input glyph sequence * @param start The first input to try */ _getNextLookup(sequence, start) { const result = { index: null, first: Infinity, last: -1 }; // Loop through each glyph and find the first valid lookup for it for (let i = 0; i < sequence.length; i++) { const lookups = this._glyphLookups[sequence[i]]; if (!lookups) { continue; } for (let j = 0; j < lookups.length; j++) { const lookupIndex = lookups[j]; if (lookupIndex >= start) { // Update the lookup information if it's the one we're // storing or earlier than it. if (result.index === null || lookupIndex <= result.index) { result.index = lookupIndex; if (result.first > i) { result.first = i; } result.last = i + 1; } break; } } } return result; } } /** * Load the font with the given name. The returned value can be used to find * ligatures for the font. * * @param name Font family name for the font to load */ async function load(name, options) { // We just grab the first font variant we find for now. // TODO: allow users to specify information to pick a specific variant const [fontInfo] = await Promise.resolve().then(() => require('font-finder')).then(fontFinder => fontFinder.listVariants(name)); if (!fontInfo) { throw new Error(`Font ${name} not found`); } return loadFile(fontInfo.path, options); } exports.load = load; /** * Load the font at the given file path. The returned value can be used to find * ligatures for the font. * * @param path Path to the file containing the font */ async function loadFile(path, options) { const font = await Promise.resolve().then(() => require('util')).then(util => util.promisify(opentype.load)(path)); return new FontImpl(font, Object.assign({ cacheSize: 0 }, options)); } exports.loadFile = loadFile; /** * Load the font from it's binary data. The returned value can be used to find * ligatures for the font. * * @param buffer ArrayBuffer of the font to load */ function loadBuffer(buffer, options) { const font = opentype.parse(buffer); return new FontImpl(font, Object.assign({ cacheSize: 0 }, options)); } exports.loadBuffer = loadBuffer; //# sourceMappingURL=index.js.map