-
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.
Here come the variable feature writer implementations
- Loading branch information
1 parent
37eea61
commit 2979873
Showing
4 changed files
with
254 additions
and
0 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
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 |
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,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 |
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,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 |
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,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 | ||
} |