diff --git a/src/opentype/GlyphIterator.js b/src/opentype/GlyphIterator.js index b22ad30b..54b1468d 100644 --- a/src/opentype/GlyphIterator.js +++ b/src/opentype/GlyphIterator.js @@ -4,10 +4,11 @@ export default class GlyphIterator { this.reset(options); } - reset(options = {}, index = 0) { + reset(options = {}, index = 0, markFilteringSet = null) { this.options = options; this.flags = options.flags || {}; this.markAttachmentType = options.markAttachmentType || 0; + this.markFilteringSet = markFilteringSet; this.index = index; } @@ -16,6 +17,12 @@ export default class GlyphIterator { } shouldIgnore(glyph) { + // useMarkFilteringSet (lookup flag 0x10) overrides ignoreMarks and + // markAttachmentType for marks: a mark is considered only if it is in + // the filtering set, otherwise it is skipped. + if (this.flags.useMarkFilteringSet && glyph.isMark) { + return !this.markFilteringSet?.has(glyph.id); + } return (this.flags.ignoreMarks && glyph.isMark) || (this.flags.ignoreBaseGlyphs && glyph.isBase) || (this.flags.ignoreLigatures && glyph.isLigature) || diff --git a/src/opentype/OTProcessor.js b/src/opentype/OTProcessor.js index 80d1ce69..2a57a2d7 100644 --- a/src/opentype/OTProcessor.js +++ b/src/opentype/OTProcessor.js @@ -191,7 +191,7 @@ export default class OTProcessor { for (let { feature, lookup } of lookups) { this.currentFeature = feature; - this.glyphIterator.reset(lookup.flags); + this.glyphIterator.reset(lookup.flags, 0, this.getMarkFilteringSet(lookup)); while (this.glyphIterator.index < glyphs.length) { if (!(feature in this.glyphIterator.cur.features)) { @@ -217,16 +217,17 @@ export default class OTProcessor { applyLookupList(lookupRecords) { let options = this.glyphIterator.options; + let markFilteringSet = this.glyphIterator.markFilteringSet; let glyphIndex = this.glyphIterator.index; for (let lookupRecord of lookupRecords) { // Reset flags and find glyph index for this lookup record - this.glyphIterator.reset(options, glyphIndex); + this.glyphIterator.reset(options, glyphIndex, markFilteringSet); this.glyphIterator.increment(lookupRecord.sequenceIndex); // Get the lookup and setup flags for subtables let lookup = this.table.lookupList.get(lookupRecord.lookupListIndex); - this.glyphIterator.reset(lookup.flags, this.glyphIterator.index); + this.glyphIterator.reset(lookup.flags, this.glyphIterator.index, this.getMarkFilteringSet(lookup)); // Apply lookup subtables until one matches for (let table of lookup.subTables) { @@ -236,10 +237,34 @@ export default class OTProcessor { } } - this.glyphIterator.reset(options, glyphIndex); + this.glyphIterator.reset(options, glyphIndex, markFilteringSet); return true; } + getMarkFilteringSet(lookup) { + if (!lookup.flags.flags.useMarkFilteringSet) { + return null; + } + let coverage = this.font.GDEF?.markGlyphSetsDef?.coverage?.[lookup.markFilteringSet]; + if (!coverage) { + return null; + } + let cache = (this._coverageSetCache ??= new Map()); + let set = cache.get(coverage); + if (!set) { + set = new Set(); + if (coverage.glyphs) { + for (let id of coverage.glyphs) set.add(id); + } else if (coverage.rangeRecords) { + for (let { start, end } of coverage.rangeRecords) { + for (let id = start; id <= end; id++) set.add(id); + } + } + cache.set(coverage, set); + } + return set; + } + coverageIndex(coverage, glyph) { if (glyph == null) { glyph = this.glyphIterator.cur.id; diff --git a/test/shaping.js b/test/shaping.js index dc005ea0..337b4b29 100644 --- a/test/shaping.js +++ b/test/shaping.js @@ -56,6 +56,18 @@ describe('shaping', function () { '218+545|11+1781|94+1362|26@35,0+1139|34+564|32+1250|3+532|9+1904|96+1088|93+1383|51+569|8+1904|' + '3+532|33+1225|21+1470|3+532|96+1088|17+1496|96+1088|17+1496|32+1250|3+532|9+1904|95+1104|12+1781|39+1052'); + // Exercises the useMarkFilteringSet lookup flag: Mada's `rclt` chain + // rule fires for [Jeem-form, ar1Dot.below, kasra] and substitutes the + // kasra with a positioned variant (`uni0650.alt`). The lookup carries + // a mark filtering set containing only the below-marks, so when an + // above-mark (fatha) sits between the dot and the kasra the iterator + // must skip it. Without the flag honoured, fontkit sees the fatha as + // the immediate predecessor of the kasra, the backtrack fails to + // match and the kasra is left unsubstituted. + test('should honour useMarkFilteringSet during contextual matching', + 'Mada/Mada-VF.ttf', 'جَِ', + '86@873,-335+0|83@253,495+0|111@273,-95+0|8+568'); + test('should shape N\'Ko text', 'NotoSans/NotoSansNKo-Regular.ttf', 'ߞߊ߬ ߞߐߕߐ߮ ߞߎߘߊ ߘߏ߫ ߘߊߦߟߍ߬ ߸ ߏ߬', '52@10,-300+0|23+1128|3+532|64+985|3+532|52@150,-300+0|84+1268|139+1184|160+1067|76+543|119+1622|3+532|51@10,-300+0|90+1128' + '|119+1622|3+532|75+543|118+1622|88+1212|137+1114|3+532|54@170,0+0|93+1321|109+1155|94+1321|137+1114|3+532|52@-210,0+0|75+543|137+1114');