From 9269eced6d5b48b5b2a27ff285781b826aaf1263 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 6 Aug 2024 14:13:38 +0300 Subject: [PATCH 1/3] cursFeatureWriter: Typo --- Lib/ufo2ft/featureWriters/cursFeatureWriter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py index 70f4ae4d..9cce3a6e 100644 --- a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py @@ -29,13 +29,13 @@ def _makeCursiveFeature(self): shouldSplit = False lookups = [] - ordereredGlyphSet = self.getOrderedGlyphSet().items() + orderedGlyphSet = self.getOrderedGlyphSet().items() if shouldSplit: # Make LTR lookup LTRlookup = self._makeCursiveLookup( ( glyph - for (glyphName, glyph) in ordereredGlyphSet + for (glyphName, glyph) in orderedGlyphSet if glyphName in dirGlyphs["LTR"] ), direction="LTR", @@ -47,7 +47,7 @@ def _makeCursiveFeature(self): RTLlookup = self._makeCursiveLookup( ( glyph - for (glyphName, glyph) in ordereredGlyphSet + for (glyphName, glyph) in orderedGlyphSet if glyphName not in dirGlyphs["LTR"] ), direction="RTL", @@ -56,7 +56,7 @@ def _makeCursiveFeature(self): lookups.append(RTLlookup) else: lookup = self._makeCursiveLookup( - (glyph for (glyphName, glyph) in ordereredGlyphSet) + (glyph for (glyphName, glyph) in orderedGlyphSet) ) if lookup: lookups.append(lookup) From 49941a06d552cdd979c492865327e6cea3e63ae5 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 6 Aug 2024 14:34:13 +0300 Subject: [PATCH 2/3] cursFeatureWrite: Unused method parameter --- Lib/ufo2ft/featureWriters/cursFeatureWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py index 9cce3a6e..51518e59 100644 --- a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py @@ -88,7 +88,7 @@ def _makeCursiveLookup(self, glyphs, direction=None): return lookup - def _getAnchors(self, glyphName, glyph=None): + def _getAnchors(self, glyphName): entryAnchor = None exitAnchor = None entryAnchorXY = self._getAnchor(glyphName, "entry") @@ -109,7 +109,7 @@ def _makeCursiveStatements(self, glyphs): cursiveAnchors = dict() statements = [] for glyph in glyphs: - entryAnchor, exitAnchor = self._getAnchors(glyph.name, glyph) + entryAnchor, exitAnchor = self._getAnchors(glyph.name) # A glyph can have only one of the cursive anchors (e.g. if it # attaches on one side only) if entryAnchor or exitAnchor: From da1949599cadaab2a0f439201c311c272c4e8f4a Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 6 Aug 2024 15:08:07 +0300 Subject: [PATCH 3/3] cursFeatureWriter: Support multiple entry/exit anchor pairs Support dot suffixed entry and exit anchor names, in addition to unsuffixed names. Each anchor pair creates a new lookups. Lookups are sorted by anchor names. --- .../featureWriters/cursFeatureWriter.py | 109 +++++++----- .../featureWriters/cursFeatureWriter_test.py | 165 ++++++++++++++++++ 2 files changed, 235 insertions(+), 39 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py index 51518e59..86ac75cb 100644 --- a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py @@ -18,6 +18,25 @@ class CursFeatureWriter(BaseFeatureWriter): tableTag = "GPOS" features = frozenset(["curs"]) + @staticmethod + def _getCursiveAnchorPairs(glyphs): + anchors = set() + for _, glyph in glyphs: + anchors.update(a.name for a in glyph.anchors) + + anchorPairs = [] + if "entry" in anchors and "exit" in anchors: + anchorPairs.append(("entry", "exit")) + for anchor in anchors: + if anchor.startswith("entry.") and f"exit.{anchor[6:]}" in anchors: + anchorPairs.append((anchor, f"exit.{anchor[6:]}")) + + return sorted(anchorPairs) + + @staticmethod + def _hasAnchor(glyph, anchorName): + return any(a.name == anchorName for a in glyph.anchors) + def _makeCursiveFeature(self): cmap = self.makeUnicodeToGlyphNameMapping() if any(unicodeScriptDirection(uv) == "LTR" for uv in cmap): @@ -30,53 +49,63 @@ def _makeCursiveFeature(self): lookups = [] orderedGlyphSet = self.getOrderedGlyphSet().items() - if shouldSplit: - # Make LTR lookup - LTRlookup = self._makeCursiveLookup( - ( - glyph - for (glyphName, glyph) in orderedGlyphSet - if glyphName in dirGlyphs["LTR"] - ), - direction="LTR", - ) - if LTRlookup: - lookups.append(LTRlookup) - - # Make RTL lookup with other glyphs - RTLlookup = self._makeCursiveLookup( - ( - glyph - for (glyphName, glyph) in orderedGlyphSet - if glyphName not in dirGlyphs["LTR"] - ), - direction="RTL", - ) - if RTLlookup: - lookups.append(RTLlookup) - else: - lookup = self._makeCursiveLookup( - (glyph for (glyphName, glyph) in orderedGlyphSet) - ) - if lookup: - lookups.append(lookup) + cursiveAnchorsPairs = self._getCursiveAnchorPairs(orderedGlyphSet) + for entryName, exitName in cursiveAnchorsPairs: + if shouldSplit: + # Make LTR lookup + LTRlookup = self._makeCursiveLookup( + ( + glyph + for (glyphName, glyph) in orderedGlyphSet + if glyphName in dirGlyphs["LTR"] + ), + entryName, + exitName, + direction="LTR", + ) + if LTRlookup: + lookups.append(LTRlookup) + + # Make RTL lookup with other glyphs + RTLlookup = self._makeCursiveLookup( + ( + glyph + for (glyphName, glyph) in orderedGlyphSet + if glyphName not in dirGlyphs["LTR"] + ), + entryName, + exitName, + direction="RTL", + ) + if RTLlookup: + lookups.append(RTLlookup) + else: + lookup = self._makeCursiveLookup( + (glyph for (glyphName, glyph) in orderedGlyphSet), + entryName, + exitName, + ) + if lookup: + lookups.append(lookup) if lookups: feature = ast.FeatureBlock("curs") feature.statements.extend(lookups) return feature - def _makeCursiveLookup(self, glyphs, direction=None): - statements = self._makeCursiveStatements(glyphs) + def _makeCursiveLookup(self, glyphs, entryName, exitName, direction=None): + statements = self._makeCursiveStatements(glyphs, entryName, exitName) if not statements: return suffix = "" + if entryName != "entry": + suffix = f"_{entryName[6:]}" if direction == "LTR": - suffix = "_ltr" + suffix += "_ltr" elif direction == "RTL": - suffix = "_rtl" + suffix += "_rtl" lookup = ast.LookupBlock(name=f"curs{suffix}") if direction != "LTR": @@ -86,13 +115,15 @@ def _makeCursiveLookup(self, glyphs, direction=None): lookup.statements.extend(statements) + print(str(lookup)) + return lookup - def _getAnchors(self, glyphName): + def _getAnchors(self, glyphName, entryName, exitName): entryAnchor = None exitAnchor = None - entryAnchorXY = self._getAnchor(glyphName, "entry") - exitAnchorXY = self._getAnchor(glyphName, "exit") + entryAnchorXY = self._getAnchor(glyphName, entryName) + exitAnchorXY = self._getAnchor(glyphName, exitName) if entryAnchorXY: entryAnchor = ast.Anchor( x=otRoundIgnoringVariable(entryAnchorXY[0]), @@ -105,11 +136,11 @@ def _getAnchors(self, glyphName): ) return entryAnchor, exitAnchor - def _makeCursiveStatements(self, glyphs): + def _makeCursiveStatements(self, glyphs, entryName, exitName): cursiveAnchors = dict() statements = [] for glyph in glyphs: - entryAnchor, exitAnchor = self._getAnchors(glyph.name) + entryAnchor, exitAnchor = self._getAnchors(glyph.name, entryName, exitName) # A glyph can have only one of the cursive anchors (e.g. if it # attaches on one side only) if entryAnchor or exitAnchor: diff --git a/tests/featureWriters/cursFeatureWriter_test.py b/tests/featureWriters/cursFeatureWriter_test.py index 76a3730b..157bb783 100644 --- a/tests/featureWriters/cursFeatureWriter_test.py +++ b/tests/featureWriters/cursFeatureWriter_test.py @@ -130,3 +130,168 @@ def test_curs_feature_mixed(self, testufo): } curs; """ ) + + def test_curs_feature_multiple_anchors(self, testufo): + glyph = testufo.newGlyph("d") + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph = testufo.newGlyph("e") + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph = testufo.newGlyph("f") + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 400}) + glyph = testufo.newGlyph("g") + glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) + generated = self.writeFeatures(testufo) + + assert str(generated) == dedent( + """\ + feature curs { + lookup curs { + lookupflag RightToLeft IgnoreMarks; + pos cursive a ; + pos cursive b ; + pos cursive c ; + } curs; + + lookup curs_1 { + lookupflag RightToLeft IgnoreMarks; + pos cursive d ; + pos cursive e ; + pos cursive f ; + } curs_1; + + lookup curs_2 { + lookupflag RightToLeft IgnoreMarks; + pos cursive f ; + pos cursive g ; + } curs_2; + + } curs; + """ + ) + + def test_curs_feature_multiple_anchors_LTR(self, testufo): + testufo["a"].unicode = ord("a") + testufo["b"].unicode = ord("b") + testufo["c"].unicode = ord("c") + glyph = testufo.newGlyph("d") + glyph.unicode = ord("d") + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph = testufo.newGlyph("e") + glyph.unicode = ord("e") + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph = testufo.newGlyph("f") + glyph.unicode = ord("f") + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 400}) + glyph = testufo.newGlyph("g") + glyph.unicode = ord("g") + glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) + generated = self.writeFeatures(testufo) + + assert str(generated) == dedent( + """\ + feature curs { + lookup curs_ltr { + lookupflag IgnoreMarks; + pos cursive a ; + pos cursive b ; + pos cursive c ; + } curs_ltr; + + lookup curs_1_ltr { + lookupflag IgnoreMarks; + pos cursive d ; + pos cursive e ; + pos cursive f ; + } curs_1_ltr; + + lookup curs_2_ltr { + lookupflag IgnoreMarks; + pos cursive f ; + pos cursive g ; + } curs_2_ltr; + + } curs; + """ + ) + + def test_curs_feature_multiple_anchors_mixed(self, testufo): + testufo["a"].unicode = ord("a") + testufo["b"].unicode = ord("b") + testufo["c"].unicode = ord("c") + glyph = testufo.newGlyph("d") + glyph.unicode = ord("d") + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph = testufo.newGlyph("e") + glyph.unicode = ord("e") + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph = testufo.newGlyph("f") + glyph.unicode = ord("f") + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 400}) + glyph = testufo.newGlyph("g") + glyph.unicode = ord("g") + glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) + glyph = testufo.newGlyph("alef-ar") + glyph.appendAnchor({"name": "entry", "x": 100, "y": 200}) + glyph.appendAnchor({"name": "exit", "x": 0, "y": 200}) + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 300}) + glyph = testufo.newGlyph("beh-ar") + glyph.unicode = 0x0628 + glyph.appendAnchor({"name": "entry.1", "x": 100, "y": 200}) + glyph.appendAnchor({"name": "exit.1", "x": 0, "y": 200}) + glyph.appendAnchor({"name": "exit.2", "x": 0, "y": 100}) + glyph = testufo.newGlyph("hah-ar") + glyph.unicode = 0x0647 + glyph.appendAnchor({"name": "entry", "x": 100, "y": 100}) + glyph.appendAnchor({"name": "entry.2", "x": 100, "y": 200}) + generated = self.writeFeatures(testufo) + + assert str(generated) == dedent( + """\ + feature curs { + lookup curs_ltr { + lookupflag IgnoreMarks; + pos cursive a ; + pos cursive b ; + pos cursive c ; + } curs_ltr; + + lookup curs_rtl { + lookupflag RightToLeft IgnoreMarks; + pos cursive alef-ar ; + pos cursive hah-ar ; + } curs_rtl; + + lookup curs_1_ltr { + lookupflag IgnoreMarks; + pos cursive d ; + pos cursive e ; + pos cursive f ; + } curs_1_ltr; + + lookup curs_1_rtl { + lookupflag RightToLeft IgnoreMarks; + pos cursive alef-ar ; + pos cursive beh-ar ; + } curs_1_rtl; + + lookup curs_2_ltr { + lookupflag IgnoreMarks; + pos cursive f ; + pos cursive g ; + } curs_2_ltr; + + lookup curs_2_rtl { + lookupflag RightToLeft IgnoreMarks; + pos cursive beh-ar ; + pos cursive hah-ar ; + } curs_2_rtl; + + } curs; + """ + )