Skip to content

Commit

Permalink
cursFeatureWriter: Support multiple entry/exit anchor pairs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
khaledhosny committed Aug 6, 2024
1 parent 49941a0 commit da19495
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 39 deletions.
109 changes: 70 additions & 39 deletions Lib/ufo2ft/featureWriters/cursFeatureWriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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":
Expand All @@ -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]),
Expand All @@ -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:
Expand Down
165 changes: 165 additions & 0 deletions tests/featureWriters/cursFeatureWriter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <anchor NULL> <anchor 100 200>;
pos cursive b <anchor 0 200> <anchor 111 200>;
pos cursive c <anchor 100 200> <anchor NULL>;
} curs;
lookup curs_1 {
lookupflag RightToLeft IgnoreMarks;
pos cursive d <anchor 100 200> <anchor 0 300>;
pos cursive e <anchor 100 200> <anchor NULL>;
pos cursive f <anchor NULL> <anchor 0 300>;
} curs_1;
lookup curs_2 {
lookupflag RightToLeft IgnoreMarks;
pos cursive f <anchor NULL> <anchor 0 400>;
pos cursive g <anchor 100 200> <anchor NULL>;
} 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 <anchor NULL> <anchor 100 200>;
pos cursive b <anchor 0 200> <anchor 111 200>;
pos cursive c <anchor 100 200> <anchor NULL>;
} curs_ltr;
lookup curs_1_ltr {
lookupflag IgnoreMarks;
pos cursive d <anchor 100 200> <anchor 0 300>;
pos cursive e <anchor 100 200> <anchor NULL>;
pos cursive f <anchor NULL> <anchor 0 300>;
} curs_1_ltr;
lookup curs_2_ltr {
lookupflag IgnoreMarks;
pos cursive f <anchor NULL> <anchor 0 400>;
pos cursive g <anchor 100 200> <anchor NULL>;
} 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 <anchor NULL> <anchor 100 200>;
pos cursive b <anchor 0 200> <anchor 111 200>;
pos cursive c <anchor 100 200> <anchor NULL>;
} curs_ltr;
lookup curs_rtl {
lookupflag RightToLeft IgnoreMarks;
pos cursive alef-ar <anchor 100 200> <anchor 0 200>;
pos cursive hah-ar <anchor 100 100> <anchor NULL>;
} curs_rtl;
lookup curs_1_ltr {
lookupflag IgnoreMarks;
pos cursive d <anchor 100 200> <anchor 0 300>;
pos cursive e <anchor 100 200> <anchor NULL>;
pos cursive f <anchor NULL> <anchor 0 300>;
} curs_1_ltr;
lookup curs_1_rtl {
lookupflag RightToLeft IgnoreMarks;
pos cursive alef-ar <anchor NULL> <anchor 0 300>;
pos cursive beh-ar <anchor 100 200> <anchor 0 200>;
} curs_1_rtl;
lookup curs_2_ltr {
lookupflag IgnoreMarks;
pos cursive f <anchor NULL> <anchor 0 400>;
pos cursive g <anchor 100 200> <anchor NULL>;
} curs_2_ltr;
lookup curs_2_rtl {
lookupflag RightToLeft IgnoreMarks;
pos cursive beh-ar <anchor NULL> <anchor 0 100>;
pos cursive hah-ar <anchor 100 200> <anchor NULL>;
} curs_2_rtl;
} curs;
"""
)

0 comments on commit da19495

Please sign in to comment.