-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #476 from moyogo/cursFeatureWriter
Add cursive attachment feature writer
- Loading branch information
Showing
7 changed files
with
289 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
from fontTools.misc.fixedTools import otRound | ||
|
||
from ufo2ft.featureWriters import BaseFeatureWriter, ast | ||
from ufo2ft.util import classifyGlyphs, unicodeScriptDirection | ||
|
||
|
||
class CursFeatureWriter(BaseFeatureWriter): | ||
"""Generate a curs feature base on glyph anchors. | ||
The default mode is 'skip': i.e. if the 'curs' feature is already present in | ||
the feature file, it is not generated again. | ||
The optional 'append' mode will add extra lookups to an already existing | ||
features, if any. | ||
By default, anchors names 'entry' and 'exit' will be used to connect the | ||
'entry' anchor of a glyph with the 'exit' anchor of the preceding glyph. | ||
""" | ||
|
||
tableTag = "GPOS" | ||
features = frozenset(["curs"]) | ||
|
||
def _makeCursiveFeature(self): | ||
cmap = self.makeUnicodeToGlyphNameMapping() | ||
if any(unicodeScriptDirection(uv) == "LTR" for uv in cmap): | ||
gsub = self.compileGSUB() | ||
dirGlyphs = classifyGlyphs(unicodeScriptDirection, cmap, gsub) | ||
shouldSplit = "LTR" in dirGlyphs | ||
else: | ||
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) | ||
|
||
if lookups: | ||
feature = ast.FeatureBlock("curs") | ||
feature.statements.extend(lookups) | ||
return feature | ||
|
||
def _makeCursiveLookup(self, glyphs, direction=None): | ||
statements = self._makeCursiveStatements(glyphs) | ||
|
||
if not statements: | ||
return | ||
|
||
suffix = "" | ||
if direction == "LTR": | ||
suffix = "_ltr" | ||
elif direction == "RTL": | ||
suffix = "_rtl" | ||
lookup = ast.LookupBlock(name=f"curs{suffix}") | ||
|
||
if direction != "LTR": | ||
lookup.statements.append(ast.makeLookupFlag(("IgnoreMarks", "RightToLeft"))) | ||
else: | ||
lookup.statements.append(ast.makeLookupFlag("IgnoreMarks")) | ||
|
||
lookup.statements.extend(statements) | ||
|
||
return lookup | ||
|
||
def _makeCursiveStatements(self, glyphs): | ||
cursiveAnchors = dict() | ||
statements = [] | ||
for glyph in glyphs: | ||
entryAnchor = exitAnchor = None | ||
for anchor in glyph.anchors: | ||
if entryAnchor and exitAnchor: | ||
break | ||
if anchor.name == "entry": | ||
entryAnchor = ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)) | ||
elif anchor.name == "exit": | ||
exitAnchor = ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)) | ||
|
||
# A glyph can have only one of the cursive anchors (e.g. if it | ||
# attaches on one side only) | ||
if entryAnchor or exitAnchor: | ||
cursiveAnchors[ast.GlyphName(glyph.name)] = (entryAnchor, exitAnchor) | ||
|
||
if cursiveAnchors: | ||
for glyphName, anchors in cursiveAnchors.items(): | ||
statement = ast.CursivePosStatement(glyphName, *anchors) | ||
statements.append(statement) | ||
|
||
return statements | ||
|
||
def _write(self): | ||
feaFile = self.context.feaFile | ||
feature = self._makeCursiveFeature() | ||
|
||
if not feature: | ||
return False | ||
|
||
self._insert(feaFile=feaFile, features=[feature]) | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
from textwrap import dedent | ||
|
||
import pytest | ||
|
||
from ufo2ft.featureWriters.cursFeatureWriter import CursFeatureWriter | ||
|
||
from . import FeatureWriterTest | ||
|
||
|
||
@pytest.fixture | ||
def testufo(FontClass): | ||
ufo = FontClass() | ||
ufo.newGlyph("a").appendAnchor({"name": "exit", "x": 100, "y": 200}) | ||
glyph = ufo.newGlyph("b") | ||
glyph.appendAnchor({"name": "entry", "x": 0, "y": 200}) | ||
glyph.appendAnchor({"name": "exit", "x": 111, "y": 200}) | ||
ufo.newGlyph("c").appendAnchor({"name": "entry", "x": 100, "y": 200}) | ||
return ufo | ||
|
||
|
||
class CursFeatureWriterTest(FeatureWriterTest): | ||
|
||
FeatureWriter = CursFeatureWriter | ||
|
||
def test_curs_feature(self, testufo): | ||
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; | ||
} curs; | ||
""" | ||
) | ||
|
||
def test_curs_feature_LTR(self, testufo): | ||
testufo["a"].unicode = ord("a") | ||
testufo["b"].unicode = ord("b") | ||
testufo["c"].unicode = ord("c") | ||
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; | ||
} curs; | ||
""" | ||
) | ||
|
||
def test_curs_feature_mixed(self, testufo): | ||
testufo["a"].unicode = ord("a") | ||
testufo["b"].unicode = ord("b") | ||
testufo["c"].unicode = ord("c") | ||
glyph = testufo.newGlyph("a.swsh") | ||
glyph.appendAnchor({"name": "entry", "x": 100, "y": 200}) | ||
glyph = testufo.newGlyph("alef") | ||
glyph.unicode = 0x0627 | ||
glyph = testufo.newGlyph("alef.fina") | ||
glyph.appendAnchor({"name": "entry", "x": 300, "y": 10}) | ||
glyph = testufo.newGlyph("meem") | ||
glyph.unicode = 0x0645 | ||
glyph = testufo.newGlyph("meem.init") | ||
glyph.appendAnchor({"name": "exit", "x": 0, "y": 10}) | ||
glyph = testufo.newGlyph("meem.medi") | ||
glyph.appendAnchor({"name": "entry", "x": 500, "y": 10}) | ||
glyph.appendAnchor({"name": "exit", "x": 0, "y": 10}) | ||
glyph = testufo.newGlyph("meem.fina") | ||
glyph.appendAnchor({"name": "entry", "x": 500, "y": 10}) | ||
testufo.features.text = dedent( | ||
"""\ | ||
feature swsh { | ||
sub a by a.swsh; | ||
} swsh; | ||
feature init { | ||
sub meem by meem.init; | ||
} init; | ||
feature medi { | ||
sub meem by meem.medi; | ||
} medi; | ||
feature fina { | ||
sub alef by alef.fina; | ||
sub meem by meem.fina; | ||
} fina; | ||
""" | ||
) | ||
testufo.lib["public.glyphOrder"] = [ | ||
"a", | ||
"b", | ||
"c", | ||
"a.swsh", | ||
"alef", | ||
"alef.fina", | ||
"meem", | ||
"meem.init", | ||
"meem.medi", | ||
"meem.fina", | ||
] | ||
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>; | ||
pos cursive a.swsh <anchor 100 200> <anchor NULL>; | ||
} curs_ltr; | ||
lookup curs_rtl { | ||
lookupflag RightToLeft IgnoreMarks; | ||
pos cursive alef.fina <anchor 300 10> <anchor NULL>; | ||
pos cursive meem.init <anchor NULL> <anchor 0 10>; | ||
pos cursive meem.medi <anchor 500 10> <anchor 0 10>; | ||
pos cursive meem.fina <anchor 500 10> <anchor NULL>; | ||
} curs_rtl; | ||
} curs; | ||
""" | ||
) |