Skip to content

Commit

Permalink
markFeatureWriter: Use new ufo2ft MarkFeatureWriter
Browse files Browse the repository at this point in the history
The code here was merged back to ufo2ft’s MarkFeatureWriter. Keeping
ContextualMarkFeatureWriter as stub just in case someone is using it
directly. Keeping the tests too, to exercise exporting anchor libData to
UFO.
  • Loading branch information
khaledhosny committed Sep 19, 2024
1 parent 6b9c4ee commit d436f8f
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 306 deletions.
5 changes: 1 addition & 4 deletions Lib/glyphsLib/builder/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,7 @@
DEFAULT_FEATURE_WRITERS = [
{"class": "CursFeatureWriter"},
{"class": "KernFeatureWriter"},
{
"module": "glyphsLib.featureWriters.markFeatureWriter",
"class": "ContextualMarkFeatureWriter",
},
{"class": "MarkFeatureWriter"},
{"class": "GdefFeatureWriter"},
]

Expand Down
277 changes: 2 additions & 275 deletions Lib/glyphsLib/featureWriters/markFeatureWriter.py
Original file line number Diff line number Diff line change
@@ -1,278 +1,5 @@
from collections import OrderedDict, defaultdict
import re

from glyphsLib.builder.constants import OBJECT_LIBS_KEY
from ufo2ft.featureWriters import ast
from ufo2ft.featureWriters.markFeatureWriter import (
MARK_PREFIX,
LIGA_SEPARATOR,
LIGA_NUM_RE,
MarkFeatureWriter,
MarkToBasePos,
NamedAnchor,
)
from ufo2ft.util import quantize


class ContextuallyAwareNamedAnchor(NamedAnchor):
__slots__ = (
"name",
"x",
"y",
"isMark",
"key",
"number",
"markClass",
"isContextual",
"isIgnorable",
"libData",
)

@classmethod
def parseAnchorName(
cls,
anchorName,
markPrefix=MARK_PREFIX,
ligaSeparator=LIGA_SEPARATOR,
ligaNumRE=LIGA_NUM_RE,
ignoreRE=None,
):
"""Parse anchor name and return a tuple that specifies:
1) whether the anchor is a "mark" anchor (bool);
2) the "key" name of the anchor, i.e. the name after stripping all the
prefixes and suffixes, which identifies the class it belongs to (str);
3) An optional number (int), starting from 1, which identifies that index
of the ligature component the anchor refers to.
The 'ignoreRE' argument is an optional regex pattern (str) identifying
sub-strings in the anchor name that should be ignored when parsing the
three elements above.
"""
number = None
isContextual = False
if ignoreRE is not None:
anchorName = re.sub(ignoreRE, "", anchorName)

if anchorName[0] == "*":
isContextual = True
anchorName = anchorName[1:]
anchorName = re.sub(r"\..*", "", anchorName)

m = ligaNumRE.match(anchorName)
if not m:
key = anchorName
else:
number = m.group(1)
key = anchorName.rstrip(number)
separator = ligaSeparator
if key.endswith(separator):
assert separator
key = key[: -len(separator)]
number = int(number)
else:
# not a valid ligature anchor name
key = anchorName
number = None

if anchorName.startswith(markPrefix) and key:
if number is not None:
raise ValueError("mark anchor cannot be numbered: %r" % anchorName)
isMark = True
key = key[len(markPrefix) :]
if not key:
raise ValueError("mark anchor key is nil: %r" % anchorName)
else:
isMark = False

isIgnorable = not key[0].isalpha()

return isMark, key, number, isContextual, isIgnorable

def __init__(self, name, x, y, markClass=None, libData=None):
self.name = name
self.x = x
self.y = y
isMark, key, number, isContextual, isIgnorable = self.parseAnchorName(
name,
markPrefix=self.markPrefix,
ligaSeparator=self.ligaSeparator,
ligaNumRE=self.ligaNumRE,
ignoreRE=self.ignoreRE,
)
if number is not None:
if number < 1:
raise ValueError("ligature component indexes must start from 1")
else:
assert key, name
self.isMark = isMark
self.key = key
self.number = number
self.markClass = markClass
self.isContextual = isContextual
self.isIgnorable = isIgnorable
self.libData = libData
from ufo2ft.featureWriters.markFeatureWriter import MarkFeatureWriter


class ContextualMarkFeatureWriter(MarkFeatureWriter):
NamedAnchor = ContextuallyAwareNamedAnchor

def _getAnchor(self, glyphName, anchorName, anchor=None):
# the variable FEA aware method is defined with ufo2ft v3; make sure we don't
# fail but continue to work unchanged with older ufo2ft MarkFeatureWriter API.
try:
getter = super()._getAnchor
except AttributeError:
x = anchor.x
y = anchor.y
if hasattr(self.options, "quantization"):
x = quantize(x, self.options.quantization)
y = quantize(y, self.options.quantization)
return x, y
else:
return getter(glyphName, anchorName, anchor=anchor)

def _getAnchorLists(self):
gdefClasses = self.context.gdefClasses
if gdefClasses.base is not None:
# only include the glyphs listed in the GDEF.GlyphClassDef groups
include = gdefClasses.base | gdefClasses.ligature | gdefClasses.mark
else:
# no GDEF table defined in feature file, include all glyphs
include = None
result = OrderedDict()
for glyphName, glyph in self.getOrderedGlyphSet().items():
if include is not None and glyphName not in include:
continue
anchorDict = OrderedDict()
for anchor in glyph.anchors:
anchorName = anchor.name
if not anchorName:
self.log.warning(
"unnamed anchor discarded in glyph '%s'", glyphName
)
continue
if anchorName in anchorDict:
self.log.warning(
"duplicate anchor '%s' in glyph '%s'", anchorName, glyphName
)
x, y = self._getAnchor(glyphName, anchorName, anchor=anchor)
libData = None
if anchor.identifier:
libData = glyph.lib[OBJECT_LIBS_KEY].get(anchor.identifier)
a = self.NamedAnchor(name=anchorName, x=x, y=y, libData=libData)
if a.isContextual and not libData:
continue
if a.isIgnorable:
continue
anchorDict[anchorName] = a
if anchorDict:
result[glyphName] = list(anchorDict.values())
return result

def _makeFeatures(self):
features = super()._makeFeatures()
# Now do the contextual ones

# Arrange by context
by_context = defaultdict(list)
markGlyphNames = self.context.markGlyphNames

for glyphName, anchors in sorted(self.context.anchorLists.items()):
if glyphName in markGlyphNames:
continue
for anchor in anchors:
if not anchor.isContextual:
continue
anchor_context = anchor.libData["GPOS_Context"].strip()
by_context[anchor_context].append((glyphName, anchor))
if not by_context:
return features, []

# Pull the lookups from the feature and replace them with lookup references,
# to ensure the order is correct
lookups = features["mark"].statements
features["mark"].statements = [
ast.LookupReferenceStatement(lu) for lu in lookups
]

dispatch_lookups = {}
# We sort the full context by longest first. This isn't perfect
# but it gives us the best chance that more specific contexts
# (typically longer) will take precedence over more general ones.
for ix, (fullcontext, glyph_anchor_pair) in enumerate(
sorted(by_context.items(), key=lambda x: -len(x[0]))
):
# Make the contextual lookup
lookupname = "ContextualMark_%i" % ix
if ";" in fullcontext:
before, after = fullcontext.split(";")
# I know it's not really a comment but this is the easiest way
# to get the lookup flag in there without reparsing it.
else:
after = fullcontext
before = ""
after = after.strip()
if before not in dispatch_lookups:
dispatch_lookups[before] = ast.LookupBlock(
"ContextualMarkDispatch_%i" % len(dispatch_lookups.keys())
)
if before:
dispatch_lookups[before].statements.append(
ast.Comment(f"{before};")
)
features["mark"].statements.append(
ast.LookupReferenceStatement(dispatch_lookups[before])
)
lkp = dispatch_lookups[before]
lkp.statements.append(ast.Comment(f"# {after}"))
lookup = ast.LookupBlock(lookupname)
for glyph, anchor in glyph_anchor_pair:
lookup.statements.append(MarkToBasePos(glyph, [anchor]).asAST())
lookups.append(lookup)

# Insert mark glyph names after base glyph names if not specified otherwise.
if "&" not in after:
after = after.replace("*", "* &")

# Group base glyphs by anchor
glyphs = {}
for glyph, anchor in glyph_anchor_pair:
glyphs.setdefault(anchor.key, [anchor, []])[1].append(glyph)

for anchor, bases in glyphs.values():
bases = " ".join(bases)
marks = ast.GlyphClass(
self.context.markClasses[anchor.key].glyphs.keys()
).asFea()

# Replace * with base glyph names
contextual = after.replace("*", f"[{bases}]")

# Replace & with mark glyph names
contextual = contextual.replace("&", f"{marks}' lookup {lookupname}")
lkp.statements.append(ast.Comment(f"pos {contextual}; # {anchor.name}"))

lookups.extend(dispatch_lookups.values())

return features, lookups

def _write(self):
self._pruneUnusedAnchors()

newClassDefs = self._makeMarkClassDefinitions()
self._setBaseAnchorMarkClasses()

features, lookups = self._makeFeatures()
if not features:
return False

feaFile = self.context.feaFile

self._insert(
feaFile=feaFile,
markClassDefs=newClassDefs,
features=[features[tag] for tag in sorted(features.keys())],
lookups=lookups,
)

return True
pass
2 changes: 1 addition & 1 deletion requirements-dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ fonttools[ufo,unicode] >= 4.38.0
# extras
ufoNormalizer>=0.3.2
defcon>=0.6.0
ufo2ft>=3.0.0b1
ufo2ft>=3.3.0
skia-pathops
Loading

0 comments on commit d436f8f

Please sign in to comment.