Skip to content

Commit

Permalink
Here come the variable feature writer implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
simoncozens committed Jul 19, 2022
1 parent 37eea61 commit 2979873
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 0 deletions.
32 changes: 32 additions & 0 deletions Lib/ufo2ft/featureWriters/variableCursWriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from fontTools.feaLib.variableScalar import VariableScalar
from ufo2ft.featureWriters import CursFeatureWriter


class VariableCursFeatureWriter(CursFeatureWriter):
def _getAnchors(self, glyphName):
entry_anchor = None
exit_anchor = None
entry_x_value = VariableScalar()
entry_y_value = VariableScalar()
exit_x_value = VariableScalar()
exit_y_value = VariableScalar()
for source in self.context.font.sources:
glyph = source.font[glyphName]
for anchor in glyph.anchors:
if anchor.name == "entry":
location = get_userspace_location(
self.context.font, source.location
)
entry_x_value.add_value(location, anchor.x)
entry_y_value.add_value(location, anchor.y)
if entry_anchor is None:
entry_anchor = ast.Anchor(x=entry_x_value, y=entry_y_value)
if anchor.name == "exit":
location = get_userspace_location(
self.context.font, source.location
)
exit_x_value.add_value(location, anchor.x)
exit_y_value.add_value(location, anchor.y)
if exit_anchor is None:
exit_anchor = ast.Anchor(x=exit_x_value, y=exit_y_value)
return entry_anchor, exit_anchor
98 changes: 98 additions & 0 deletions Lib/ufo2ft/featureWriters/variableKernWriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from ufo2ft.featureWriters import KernFeatureWriter
from ufo2ft.featureWriters.kernFeatureWriter import (
SIDE1_PREFIX,
SIDE2_PREFIX,
KerningPair,
)
from ufo2ft.util import get_userspace_location, collapse_varscalar
from fontTools.feaLib.variableScalar import VariableScalar
import logging

log = logging.getLogger(__file__)


class VariableKernFeatureWriter(KernFeatureWriter):
@staticmethod
def getKerningGroups(designspace, glyphSet=None):
if glyphSet:
allGlyphs = set(glyphSet.keys())
else:
allGlyphs = set(designspace.findDefault().font.keys())
side1Groups = {}
side2Groups = {}
for source in designspace.sources:
font = source.font
for name, members in font.groups.items():
# prune non-existent or skipped glyphs
members = [g for g in members if g in allGlyphs]
if not members:
# skip empty groups
continue
# skip groups without UFO3 public.kern{1,2} prefix
if name.startswith(SIDE1_PREFIX):
if name in side1Groups and side1Groups[name] != members:
log.warning(
"incompatible left groups: %s was previously %s, %s tried to make it %s",
name,
side1Groups[name],
font,
members,
)
continue
side1Groups[name] = members
elif name.startswith(SIDE2_PREFIX):
if name in side2Groups and side2Groups[name] != members:
log.warning(
"incompatible right groups: %s was previously %s, %s tried to make it %s",
name,
side2Groups[name],
font,
members,
)
continue
side2Groups[name] = members
return side1Groups, side2Groups

@staticmethod
def getKerningPairs(designspace, side1Classes, side2Classes, glyphSet=None):
default_font = designspace.findDefault().font
if glyphSet:
allGlyphs = set(glyphSet.keys())
else:
allGlyphs = set(default_font)

pairsByFlags = {}
for source in designspace.sources:
for (side1, side2) in source.font.kerning:
# filter out pairs that reference missing groups or glyphs
if side1 not in side1Classes and side1 not in allGlyphs:
continue
if side2 not in side2Classes and side2 not in allGlyphs:
continue
flags = (side1 in side1Classes, side2 in side2Classes)
pairsByFlags.setdefault(flags, set()).add((side1, side2))

result = []
for flags, pairs in sorted(pairsByFlags.items()):
for side1, side2 in sorted(pairs):
value = VariableScalar()
for source in designspace.sources:
if (side1, side2) in source.font.kerning:
location = get_userspace_location(designspace, source.location)
value.add_value(location, source.font.kerning[side1, side2])
elif source.font == default_font:
# Need to establish a default master value for the kern
location = get_userspace_location(designspace, source.location)
value.add_value(location, 0)
values = list(value.values.values())
value = collapse_varscalar(value)
if all(flags) and value == 0:
# ignore zero-valued class kern pairs
continue
firstIsClass, secondIsClass = flags
if firstIsClass:
side1 = side1Classes[side1]
if secondIsClass:
side2 = side2Classes[side2]
result.append(KerningPair(side1, side2, value))
return result
70 changes: 70 additions & 0 deletions Lib/ufo2ft/featureWriters/variableMarkWriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from ufo2ft.featureWriters import MarkFeatureWriter
from types import SimpleNamespace
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.feaLib import ast
from collections import OrderedDict, defaultdict
from ufo2ft.util import get_userspace_location, collapse_varscalar


class VariableMarkFeatureWriter(MarkFeatureWriter):
def setContext(self, *args, **kwargs):
# Rename "font" to "designspace" to avoid confusion
super(MarkFeatureWriter, self).setContext(*args, **kwargs)
self.context = SimpleNamespace(
designspace=self.context.font,
feaFile=self.context.feaFile,
compiler=self.context.compiler,
todo=self.context.todo,
insertComments=self.context.insertComments,
font=self.context.font.findDefault().font,
)
self.context.gdefClasses = self.getGDEFGlyphClasses()
self.context.anchorLists = self._getAnchorLists()
self.context.anchorPairs = self._getAnchorPairs()

return self.context

def _getAnchor(self, glyphName, anchorName):
x_value = VariableScalar()
y_value = VariableScalar()
for source in self.context.designspace.sources:
glyph = source.font[glyphName]
for anchor in glyph.anchors:
if anchor.name == anchorName:
location = get_userspace_location(
self.context.designspace, source.location
)
x_value.add_value(location, anchor.x)
y_value.add_value(location, anchor.y)
return collapse_varscalar(x_value), collapse_varscalar(y_value)

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)
a = self.NamedAnchor(name=anchorName, x=x, y=y)
anchorDict[anchorName] = a
if anchorDict:
result[glyphName] = list(anchorDict.values())
return result
54 changes: 54 additions & 0 deletions Lib/ufo2ft/featureWriters/variableRulesWriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from ufo2ft.featureWriters import BaseFeatureWriter
from types import SimpleNamespace
from fontTools.feaLib.variableScalar import VariableScalar
from fontTools.feaLib import ast
from collections import OrderedDict, defaultdict


class VariableRulesFeatureWriter(BaseFeatureWriter):
def write(self, font, feaFile, compiler=None):
"""Write features and class definitions for this font to a feaLib
FeatureFile object.
Returns True if feature file was modified, False if no new features
were generated.
"""
self.setContext(font, feaFile, compiler=compiler)
return self._write()

def _write(self):
self._designspace = self.context.font
self._axis_map = {axis.name: axis.tag for axis in self._designspace.axes}

feaFile = self.context.feaFile
self._conditionsets = []
for r in self._designspace.rules:
conditionsets = [self.rearrangeConditionSet(c) for c in r.conditionSets]
for conditionset in conditionsets:
if conditionset not in self._conditionsets:
cs_name = "ConditionSet%i" % (len(self._conditionsets) + 1)
feaFile.statements.append(
ast.ConditionsetStatement(cs_name, conditionset)
)
self._conditionsets.append(conditionset)
else:
cs_name = "ConditionSet%i" % self._conditionsets.index(conditionset)
# XXX
block = ast.VariationBlock("rvrn", cs_name)
for sub in r.subs:
block.statements.append(
ast.SingleSubstStatement(
[ast.GlyphName(sub[0])],
[ast.GlyphName(sub[1])],
[],
[],
False,
)
)
feaFile.statements.append(block)

def rearrangeConditionSet(self, condition):
return {
self._axis_map[rule["name"]]: (int(rule["minimum"]),
int(rule["maximum"]))
for rule in condition
}

0 comments on commit 2979873

Please sign in to comment.