|
|
import AATStateMachine from './AATStateMachine';
|
|
|
import AATLookupTable from './AATLookupTable';
|
|
|
import {cache} from '../decorators';
|
|
|
|
|
|
// indic replacement flags
|
|
|
const MARK_FIRST = 0x8000;
|
|
|
const MARK_LAST = 0x2000;
|
|
|
const VERB = 0x000F;
|
|
|
|
|
|
// contextual substitution and glyph insertion flag
|
|
|
const SET_MARK = 0x8000;
|
|
|
|
|
|
// ligature entry flags
|
|
|
const SET_COMPONENT = 0x8000;
|
|
|
const PERFORM_ACTION = 0x2000;
|
|
|
|
|
|
// ligature action masks
|
|
|
const LAST_MASK = 0x80000000;
|
|
|
const STORE_MASK = 0x40000000;
|
|
|
const OFFSET_MASK = 0x3FFFFFFF;
|
|
|
|
|
|
const VERTICAL_ONLY = 0x800000;
|
|
|
const REVERSE_DIRECTION = 0x400000;
|
|
|
const HORIZONTAL_AND_VERTICAL = 0x200000;
|
|
|
|
|
|
// glyph insertion flags
|
|
|
const CURRENT_IS_KASHIDA_LIKE = 0x2000;
|
|
|
const MARKED_IS_KASHIDA_LIKE = 0x1000;
|
|
|
const CURRENT_INSERT_BEFORE = 0x0800;
|
|
|
const MARKED_INSERT_BEFORE = 0x0400;
|
|
|
const CURRENT_INSERT_COUNT = 0x03E0;
|
|
|
const MARKED_INSERT_COUNT = 0x001F;
|
|
|
|
|
|
export default class AATMorxProcessor {
|
|
|
constructor(font) {
|
|
|
this.processIndicRearragement = this.processIndicRearragement.bind(this);
|
|
|
this.processContextualSubstitution = this.processContextualSubstitution.bind(this);
|
|
|
this.processLigature = this.processLigature.bind(this);
|
|
|
this.processNoncontextualSubstitutions = this.processNoncontextualSubstitutions.bind(this);
|
|
|
this.processGlyphInsertion = this.processGlyphInsertion.bind(this);
|
|
|
this.font = font;
|
|
|
this.morx = font.morx;
|
|
|
this.inputCache = null;
|
|
|
}
|
|
|
|
|
|
// Processes an array of glyphs and applies the specified features
|
|
|
// Features should be in the form of {featureType:{featureSetting:true}}
|
|
|
process(glyphs, features = {}) {
|
|
|
for (let chain of this.morx.chains) {
|
|
|
let flags = chain.defaultFlags;
|
|
|
|
|
|
// enable/disable the requested features
|
|
|
for (let feature of chain.features) {
|
|
|
let f;
|
|
|
if ((f = features[feature.featureType]) && f[feature.featureSetting]) {
|
|
|
flags &= feature.disableFlags;
|
|
|
flags |= feature.enableFlags;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for (let subtable of chain.subtables) {
|
|
|
if (subtable.subFeatureFlags & flags) {
|
|
|
this.processSubtable(subtable, glyphs);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// remove deleted glyphs
|
|
|
let index = glyphs.length - 1;
|
|
|
while (index >= 0) {
|
|
|
if (glyphs[index].id === 0xffff) {
|
|
|
glyphs.splice(index, 1);
|
|
|
}
|
|
|
|
|
|
index--;
|
|
|
}
|
|
|
|
|
|
return glyphs;
|
|
|
}
|
|
|
|
|
|
processSubtable(subtable, glyphs) {
|
|
|
this.subtable = subtable;
|
|
|
this.glyphs = glyphs;
|
|
|
if (this.subtable.type === 4) {
|
|
|
this.processNoncontextualSubstitutions(this.subtable, this.glyphs);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.ligatureStack = [];
|
|
|
this.markedGlyph = null;
|
|
|
this.firstGlyph = null;
|
|
|
this.lastGlyph = null;
|
|
|
this.markedIndex = null;
|
|
|
|
|
|
let stateMachine = this.getStateMachine(subtable);
|
|
|
let process = this.getProcessor();
|
|
|
|
|
|
let reverse = !!(this.subtable.coverage & REVERSE_DIRECTION);
|
|
|
return stateMachine.process(this.glyphs, reverse, process);
|
|
|
}
|
|
|
|
|
|
@cache
|
|
|
getStateMachine(subtable) {
|
|
|
return new AATStateMachine(subtable.table.stateTable);
|
|
|
}
|
|
|
|
|
|
getProcessor() {
|
|
|
switch (this.subtable.type) {
|
|
|
case 0:
|
|
|
return this.processIndicRearragement;
|
|
|
case 1:
|
|
|
return this.processContextualSubstitution;
|
|
|
case 2:
|
|
|
return this.processLigature;
|
|
|
case 4:
|
|
|
return this.processNoncontextualSubstitutions;
|
|
|
case 5:
|
|
|
return this.processGlyphInsertion;
|
|
|
default:
|
|
|
throw new Error(`Invalid morx subtable type: ${this.subtable.type}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
processIndicRearragement(glyph, entry, index) {
|
|
|
if (entry.flags & MARK_FIRST) {
|
|
|
this.firstGlyph = index;
|
|
|
}
|
|
|
|
|
|
if (entry.flags & MARK_LAST) {
|
|
|
this.lastGlyph = index;
|
|
|
}
|
|
|
|
|
|
reorderGlyphs(this.glyphs, entry.flags & VERB, this.firstGlyph, this.lastGlyph);
|
|
|
}
|
|
|
|
|
|
processContextualSubstitution(glyph, entry, index) {
|
|
|
let subsitutions = this.subtable.table.substitutionTable.items;
|
|
|
if (entry.markIndex !== 0xffff) {
|
|
|
let lookup = subsitutions.getItem(entry.markIndex);
|
|
|
let lookupTable = new AATLookupTable(lookup);
|
|
|
glyph = this.glyphs[this.markedGlyph];
|
|
|
var gid = lookupTable.lookup(glyph.id);
|
|
|
if (gid) {
|
|
|
this.glyphs[this.markedGlyph] = this.font.getGlyph(gid, glyph.codePoints);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (entry.currentIndex !== 0xffff) {
|
|
|
let lookup = subsitutions.getItem(entry.currentIndex);
|
|
|
let lookupTable = new AATLookupTable(lookup);
|
|
|
glyph = this.glyphs[index];
|
|
|
var gid = lookupTable.lookup(glyph.id);
|
|
|
if (gid) {
|
|
|
this.glyphs[index] = this.font.getGlyph(gid, glyph.codePoints);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (entry.flags & SET_MARK) {
|
|
|
this.markedGlyph = index;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
processLigature(glyph, entry, index) {
|
|
|
if (entry.flags & SET_COMPONENT) {
|
|
|
this.ligatureStack.push(index);
|
|
|
}
|
|
|
|
|
|
if (entry.flags & PERFORM_ACTION) {
|
|
|
let actions = this.subtable.table.ligatureActions;
|
|
|
let components = this.subtable.table.components;
|
|
|
let ligatureList = this.subtable.table.ligatureList;
|
|
|
|
|
|
let actionIndex = entry.action;
|
|
|
let last = false;
|
|
|
let ligatureIndex = 0;
|
|
|
let codePoints = [];
|
|
|
let ligatureGlyphs = [];
|
|
|
|
|
|
while (!last) {
|
|
|
let componentGlyph = this.ligatureStack.pop();
|
|
|
codePoints.unshift(...this.glyphs[componentGlyph].codePoints);
|
|
|
|
|
|
let action = actions.getItem(actionIndex++);
|
|
|
last = !!(action & LAST_MASK);
|
|
|
let store = !!(action & STORE_MASK);
|
|
|
let offset = (action & OFFSET_MASK) << 2 >> 2; // sign extend 30 to 32 bits
|
|
|
offset += this.glyphs[componentGlyph].id;
|
|
|
|
|
|
let component = components.getItem(offset);
|
|
|
ligatureIndex += component;
|
|
|
|
|
|
if (last || store) {
|
|
|
let ligatureEntry = ligatureList.getItem(ligatureIndex);
|
|
|
this.glyphs[componentGlyph] = this.font.getGlyph(ligatureEntry, codePoints);
|
|
|
ligatureGlyphs.push(componentGlyph);
|
|
|
ligatureIndex = 0;
|
|
|
codePoints = [];
|
|
|
} else {
|
|
|
this.glyphs[componentGlyph] = this.font.getGlyph(0xffff);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Put ligature glyph indexes back on the stack
|
|
|
this.ligatureStack.push(...ligatureGlyphs);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
processNoncontextualSubstitutions(subtable, glyphs, index) {
|
|
|
let lookupTable = new AATLookupTable(subtable.table.lookupTable);
|
|
|
|
|
|
for (index = 0; index < glyphs.length; index++) {
|
|
|
let glyph = glyphs[index];
|
|
|
if (glyph.id !== 0xffff) {
|
|
|
let gid = lookupTable.lookup(glyph.id);
|
|
|
if (gid) { // 0 means do nothing
|
|
|
glyphs[index] = this.font.getGlyph(gid, glyph.codePoints);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
_insertGlyphs(glyphIndex, insertionActionIndex, count, isBefore) {
|
|
|
let insertions = [];
|
|
|
while (count--) {
|
|
|
let gid = this.subtable.table.insertionActions.getItem(insertionActionIndex++);
|
|
|
insertions.push(this.font.getGlyph(gid));
|
|
|
}
|
|
|
|
|
|
if (!isBefore) {
|
|
|
glyphIndex++;
|
|
|
}
|
|
|
|
|
|
this.glyphs.splice(glyphIndex, 0, ...insertions);
|
|
|
}
|
|
|
|
|
|
processGlyphInsertion(glyph, entry, index) {
|
|
|
if (entry.flags & SET_MARK) {
|
|
|
this.markedIndex = index;
|
|
|
}
|
|
|
|
|
|
if (entry.markedInsertIndex !== 0xffff) {
|
|
|
let count = (entry.flags & MARKED_INSERT_COUNT) >>> 5;
|
|
|
let isBefore = !!(entry.flags & MARKED_INSERT_BEFORE);
|
|
|
this._insertGlyphs(this.markedIndex, entry.markedInsertIndex, count, isBefore);
|
|
|
}
|
|
|
|
|
|
if (entry.currentInsertIndex !== 0xffff) {
|
|
|
let count = (entry.flags & CURRENT_INSERT_COUNT) >>> 5;
|
|
|
let isBefore = !!(entry.flags & CURRENT_INSERT_BEFORE);
|
|
|
this._insertGlyphs(index, entry.currentInsertIndex, count, isBefore);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
getSupportedFeatures() {
|
|
|
let features = [];
|
|
|
for (let chain of this.morx.chains) {
|
|
|
for (let feature of chain.features) {
|
|
|
features.push([feature.featureType, feature.featureSetting]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return features;
|
|
|
}
|
|
|
|
|
|
generateInputs(gid) {
|
|
|
if (!this.inputCache) {
|
|
|
this.generateInputCache();
|
|
|
}
|
|
|
|
|
|
return this.inputCache[gid] || [];
|
|
|
}
|
|
|
|
|
|
generateInputCache() {
|
|
|
this.inputCache = {};
|
|
|
|
|
|
for (let chain of this.morx.chains) {
|
|
|
let flags = chain.defaultFlags;
|
|
|
|
|
|
for (let subtable of chain.subtables) {
|
|
|
if (subtable.subFeatureFlags & flags) {
|
|
|
this.generateInputsForSubtable(subtable);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
generateInputsForSubtable(subtable) {
|
|
|
// Currently, only supporting ligature subtables.
|
|
|
if (subtable.type !== 2) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
let reverse = !!(subtable.coverage & REVERSE_DIRECTION);
|
|
|
if (reverse) {
|
|
|
throw new Error('Reverse subtable, not supported.');
|
|
|
}
|
|
|
|
|
|
this.subtable = subtable;
|
|
|
this.ligatureStack = [];
|
|
|
|
|
|
let stateMachine = this.getStateMachine(subtable);
|
|
|
let process = this.getProcessor();
|
|
|
|
|
|
let input = [];
|
|
|
let stack = [];
|
|
|
this.glyphs = [];
|
|
|
|
|
|
stateMachine.traverse({
|
|
|
enter: (glyph, entry) => {
|
|
|
let glyphs = this.glyphs;
|
|
|
stack.push({
|
|
|
glyphs: glyphs.slice(),
|
|
|
ligatureStack: this.ligatureStack.slice()
|
|
|
});
|
|
|
|
|
|
// Add glyph to input and glyphs to process.
|
|
|
let g = this.font.getGlyph(glyph);
|
|
|
input.push(g);
|
|
|
glyphs.push(input[input.length - 1]);
|
|
|
|
|
|
// Process ligature substitution
|
|
|
process(glyphs[glyphs.length - 1], entry, glyphs.length - 1);
|
|
|
|
|
|
// Add input to result if only one matching (non-deleted) glyph remains.
|
|
|
let count = 0;
|
|
|
let found = 0;
|
|
|
for (let i = 0; i < glyphs.length && count <= 1; i++) {
|
|
|
if (glyphs[i].id !== 0xffff) {
|
|
|
count++;
|
|
|
found = glyphs[i].id;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (count === 1) {
|
|
|
let result = input.map(g => g.id);
|
|
|
let cache = this.inputCache[found];
|
|
|
if (cache) {
|
|
|
cache.push(result);
|
|
|
} else {
|
|
|
this.inputCache[found] = [result];
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
|
|
|
exit: () => {
|
|
|
({glyphs: this.glyphs, ligatureStack: this.ligatureStack} = stack.pop());
|
|
|
input.pop();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// swaps the glyphs in rangeA with those in rangeB
|
|
|
// reverse the glyphs inside those ranges if specified
|
|
|
// ranges are in [offset, length] format
|
|
|
function swap(glyphs, rangeA, rangeB, reverseA = false, reverseB = false) {
|
|
|
let end = glyphs.splice(rangeB[0] - (rangeB[1] - 1), rangeB[1]);
|
|
|
if (reverseB) {
|
|
|
end.reverse();
|
|
|
}
|
|
|
|
|
|
let start = glyphs.splice(rangeA[0], rangeA[1], ...end);
|
|
|
if (reverseA) {
|
|
|
start.reverse();
|
|
|
}
|
|
|
|
|
|
glyphs.splice(rangeB[0] - (rangeA[1] - 1), 0, ...start);
|
|
|
return glyphs;
|
|
|
}
|
|
|
|
|
|
function reorderGlyphs(glyphs, verb, firstGlyph, lastGlyph) {
|
|
|
let length = lastGlyph - firstGlyph + 1;
|
|
|
switch (verb) {
|
|
|
case 0: // no change
|
|
|
return glyphs;
|
|
|
|
|
|
case 1: // Ax => xA
|
|
|
return swap(glyphs, [firstGlyph, 1], [lastGlyph, 0]);
|
|
|
|
|
|
case 2: // xD => Dx
|
|
|
return swap(glyphs, [firstGlyph, 0], [lastGlyph, 1]);
|
|
|
|
|
|
case 3: // AxD => DxA
|
|
|
return swap(glyphs, [firstGlyph, 1], [lastGlyph, 1]);
|
|
|
|
|
|
case 4: // ABx => xAB
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 0]);
|
|
|
|
|
|
case 5: // ABx => xBA
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 0], true, false);
|
|
|
|
|
|
case 6: // xCD => CDx
|
|
|
return swap(glyphs, [firstGlyph, 0], [lastGlyph, 2]);
|
|
|
|
|
|
case 7: // xCD => DCx
|
|
|
return swap(glyphs, [firstGlyph, 0], [lastGlyph, 2], false, true);
|
|
|
|
|
|
case 8: // AxCD => CDxA
|
|
|
return swap(glyphs, [firstGlyph, 1], [lastGlyph, 2]);
|
|
|
|
|
|
case 9: // AxCD => DCxA
|
|
|
return swap(glyphs, [firstGlyph, 1], [lastGlyph, 2], false, true);
|
|
|
|
|
|
case 10: // ABxD => DxAB
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 1]);
|
|
|
|
|
|
case 11: // ABxD => DxBA
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 1], true, false);
|
|
|
|
|
|
case 12: // ABxCD => CDxAB
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2]);
|
|
|
|
|
|
case 13: // ABxCD => CDxBA
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], true, false);
|
|
|
|
|
|
case 14: // ABxCD => DCxAB
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], false, true);
|
|
|
|
|
|
case 15: // ABxCD => DCxBA
|
|
|
return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], true, true);
|
|
|
|
|
|
default:
|
|
|
throw new Error(`Unknown verb: ${verb}`);
|
|
|
}
|
|
|
}
|
|
|
|