Skip to content

Commit

Permalink
Merge pull request #863 from googlefonts/multiple-cursive-anchors
Browse files Browse the repository at this point in the history
cursFeatureWriter: Support multiple entry/exit anchor pairs
  • Loading branch information
khaledhosny authored Aug 6, 2024
2 parents 3459681 + da19495 commit 92a7023
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 40 deletions.
111 changes: 71 additions & 40 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 @@ -29,54 +48,64 @@ def _makeCursiveFeature(self):
shouldSplit = False

lookups = []
ordereredGlyphSet = self.getOrderedGlyphSet().items()
if shouldSplit:
# Make LTR lookup
LTRlookup = self._makeCursiveLookup(
(
glyph
for (glyphName, glyph) in ordereredGlyphSet
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 ordereredGlyphSet
if glyphName not in dirGlyphs["LTR"]
),
direction="RTL",
)
if RTLlookup:
lookups.append(RTLlookup)
else:
lookup = self._makeCursiveLookup(
(glyph for (glyphName, glyph) in ordereredGlyphSet)
)
if lookup:
lookups.append(lookup)
orderedGlyphSet = self.getOrderedGlyphSet().items()
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, glyph=None):
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, glyph=None):
)
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, glyph)
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 92a7023

Please sign in to comment.