Skip to content

Commit

Permalink
Merge pull request #476 from moyogo/cursFeatureWriter
Browse files Browse the repository at this point in the history
Add cursive attachment feature writer
  • Loading branch information
khaledhosny authored Oct 16, 2021
2 parents bb3bfd7 + 5356bd3 commit c1f706d
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 17 deletions.
8 changes: 7 additions & 1 deletion Lib/ufo2ft/featureCompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from ufo2ft.constants import MTI_FEATURES_PREFIX
from ufo2ft.featureWriters import (
CursFeatureWriter,
GdefFeatureWriter,
KernFeatureWriter,
MarkFeatureWriter,
Expand Down Expand Up @@ -148,7 +149,12 @@ class FeatureCompiler(BaseFeatureCompiler):
Feature File stored in the UFO, using fontTools.feaLib as compiler.
"""

defaultFeatureWriters = [KernFeatureWriter, MarkFeatureWriter, GdefFeatureWriter]
defaultFeatureWriters = [
KernFeatureWriter,
MarkFeatureWriter,
GdefFeatureWriter,
CursFeatureWriter,
]

def __init__(self, ufo, ttFont=None, glyphSet=None, featureWriters=None, **kwargs):
"""
Expand Down
2 changes: 2 additions & 0 deletions Lib/ufo2ft/featureWriters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from ufo2ft.util import _loadPluginFromString

from .baseFeatureWriter import BaseFeatureWriter
from .cursFeatureWriter import CursFeatureWriter
from .gdefFeatureWriter import GdefFeatureWriter
from .kernFeatureWriter import KernFeatureWriter
from .markFeatureWriter import MarkFeatureWriter

__all__ = [
"BaseFeatureWriter",
"CursFeatureWriter",
"GdefFeatureWriter",
"KernFeatureWriter",
"MarkFeatureWriter",
Expand Down
11 changes: 9 additions & 2 deletions Lib/ufo2ft/featureWriters/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@


import collections
import functools
import operator
import re

# we re-export here all the feaLib AST classes so they can be used from
Expand Down Expand Up @@ -91,8 +93,13 @@ def iterClassDefinitions(feaFile, featureTag=None):
}


def makeLookupFlag(name=None, markAttachment=None, markFilteringSet=None):
value = 0 if name is None else LOOKUP_FLAGS[name]
def makeLookupFlag(flags=None, markAttachment=None, markFilteringSet=None):
if isinstance(flags, str):
value = LOOKUP_FLAGS[flags]
elif flags is not None:
value = functools.reduce(operator.or_, [LOOKUP_FLAGS[n] for n in flags], 0)
else:
value = 0

if markAttachment is not None:
assert isinstance(markAttachment, ast.GlyphClassDefinition)
Expand Down
125 changes: 125 additions & 0 deletions Lib/ufo2ft/featureWriters/cursFeatureWriter.py
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
15 changes: 1 addition & 14 deletions Lib/ufo2ft/featureWriters/kernFeatureWriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fontTools import unicodedata

from ufo2ft.featureWriters import BaseFeatureWriter, ast
from ufo2ft.util import classifyGlyphs, quantize
from ufo2ft.util import classifyGlyphs, quantize, unicodeScriptDirection

SIDE1_PREFIX = "public.kern1."
SIDE2_PREFIX = "public.kern2."
Expand Down Expand Up @@ -99,19 +99,6 @@
"Nand", # Nandinagari
}


# we consider the 'Common' and 'Inherited' scripts as neutral for
# determining a kerning pair's horizontal direction
DFLT_SCRIPTS = {"Zyyy", "Zinh"}


def unicodeScriptDirection(uv):
sc = unicodedata.script(chr(uv))
if sc in DFLT_SCRIPTS:
return None
return unicodedata.script_horizontal_direction(sc)


RTL_BIDI_TYPES = {"R", "AL"}
LTR_BIDI_TYPES = {"L", "AN", "EN"}

Expand Down
12 changes: 12 additions & 0 deletions Lib/ufo2ft/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,18 @@ def unicodeInScripts(uv, scripts):
return not sx.isdisjoint(scripts)


# we consider the 'Common' and 'Inherited' scripts as neutral for
# determining a script horizontal direction
DFLT_SCRIPTS = {"Zyyy", "Zinh"}


def unicodeScriptDirection(uv):
sc = unicodedata.script(chr(uv))
if sc in DFLT_SCRIPTS:
return None
return unicodedata.script_horizontal_direction(sc)


def calcCodePageRanges(unicodes):
"""Given a set of Unicode codepoints (integers), calculate the
corresponding OS/2 CodePage range bits.
Expand Down
133 changes: 133 additions & 0 deletions tests/featureWriters/cursFeatureWriter_test.py
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;
"""
)

0 comments on commit c1f706d

Please sign in to comment.