From 0fae1152fe694146f46b9be659955f5e51f2054e Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 19 Jul 2022 11:53:23 +0100 Subject: [PATCH 01/17] Util scaffolding * Using this version of otRound allows us to reuse a lot more code * A couple of variable scalar / designspace utility functions * Allow compileGSUB to know about fvar tables --- .../featureWriters/cursFeatureWriter.py | 8 ++--- .../featureWriters/markFeatureWriter.py | 20 ++++++++--- Lib/ufo2ft/util.py | 33 +++++++++++++++++-- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py index 020b4d8ac..adb6473b0 100644 --- a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py @@ -1,7 +1,5 @@ -from fontTools.misc.fixedTools import otRound - from ufo2ft.featureWriters import BaseFeatureWriter, ast -from ufo2ft.util import classifyGlyphs, unicodeScriptDirection +from ufo2ft.util import classifyGlyphs, unicodeScriptDirection, otRoundIgnoringVariable class CursFeatureWriter(BaseFeatureWriter): @@ -99,9 +97,9 @@ def _makeCursiveStatements(self, glyphs): if entryAnchor and exitAnchor: break if anchor.name == "entry": - entryAnchor = ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)) + entryAnchor = ast.Anchor(x=otRoundIgnoringVariable(anchor.x), y=otRoundIgnoringVariable(anchor.y)) elif anchor.name == "exit": - exitAnchor = ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)) + exitAnchor = ast.Anchor(x=otRoundIgnoringVariable(anchor.x), y=otRoundIgnoringVariable(anchor.y)) # A glyph can have only one of the cursive anchors (e.g. if it # attaches on one side only) diff --git a/Lib/ufo2ft/featureWriters/markFeatureWriter.py b/Lib/ufo2ft/featureWriters/markFeatureWriter.py index d8e620add..eca7639d3 100644 --- a/Lib/ufo2ft/featureWriters/markFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/markFeatureWriter.py @@ -3,8 +3,6 @@ from collections import OrderedDict, defaultdict from functools import partial -from fontTools.misc.fixedTools import otRound - from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import ( @@ -34,7 +32,13 @@ def _filterMarks(self, include): def _marksAsAST(self): return [ - (ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)), anchor.markClass) + ( + ast.Anchor( + x=otRoundIgnoringVariable(anchor.x), + y=otRoundIgnoringVariable(anchor.y), + ), + anchor.markClass, + ) for anchor in sorted(self.marks, key=lambda a: a.name) ] @@ -78,7 +82,13 @@ def _filterMarks(self, include): def _marksAsAST(self): return [ [ - (ast.Anchor(x=otRound(anchor.x), y=otRound(anchor.y)), anchor.markClass) + ( + ast.Anchor( + x=otRoundIgnoringVariable(anchor.x), + y=otRoundIgnoringVariable(anchor.y), + ), + anchor.markClass, + ) for anchor in sorted(component, key=lambda a: a.name) ] for component in self.marks @@ -411,7 +421,7 @@ def _makeMarkClassDefinitions(self): return newDefs def _defineMarkClass(self, glyphName, x, y, className, markClasses): - anchor = ast.Anchor(x=otRound(x), y=otRound(y)) + anchor = ast.Anchor(x=otRoundIgnoringVariable(x), y=otRoundIgnoringVariable(y)) markClass = markClasses.get(className) if markClass is None: markClass = ast.MarkClass(className) diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py index 48d56a79c..348c6b0a2 100644 --- a/Lib/ufo2ft/util.py +++ b/Lib/ufo2ft/util.py @@ -243,13 +243,18 @@ def makeUnicodeToGlyphNameMapping(font, glyphOrder=None): return mapping -def compileGSUB(featureFile, glyphOrder): +def compileGSUB(featureFile, glyphOrder, fvar=None): """Compile and return a GSUB table from `featureFile` (feaLib FeatureFile), using the given `glyphOrder` (list of glyph names). """ font = ttLib.TTFont() font.setGlyphOrder(glyphOrder) - addOpenTypeFeatures(font, featureFile, tables={"GSUB"}) + if fvar: + font["fvar"] = fvar + tables = {"GSUB", "GDEF"} + else: + tables = {"GSUB"} + addOpenTypeFeatures(font, featureFile, tables=tables) return font.get("GSUB") @@ -549,9 +554,18 @@ def _loadPluginFromString(spec, moduleName, isValidFunc): def quantize(number, factor): """Round to a multiple of the given parameter""" + if not isinstance(number, (float, int)): + # Some kind of variable scalar + return number return factor * otRound(number / factor) +def otRoundIgnoringVariable(number): + if not isinstance(number, (float, int)): + return number + return otRound(number) + + def init_kwargs(kwargs, defaults): """Initialise kwargs default values. @@ -673,3 +687,18 @@ def colrClipBoxQuantization(ufo: Any) -> int: """ upem = getAttrWithFallback(ufo.info, "unitsPerEm") return int(round(upem / 10, -1)) + + +def get_userspace_location(designspace, location): + """Map a location from designspace to userspace across all axes.""" + location_user = designspace.map_backward(location) + return {designspace.getAxis(k).tag: v for k, v in location_user.items()} + + +def collapse_varscalar(varscalar, threshold=0): + """Collapse a variable scalar to a plain scalar if all values are similar""" + # This should eventually be a method on the VariableScalar object + values = list(varscalar.values.values()) + if not any(abs(v - values[0]) > threshold for v in values[1:]): + return list(varscalar.values.values())[0] + return varscalar From dadd828f9d47a94583e0dfd11d76ae8dc4294701 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 19 Jul 2022 12:26:27 +0100 Subject: [PATCH 02/17] Here come the variable feature writer implementations --- .../featureWriters/baseFeatureWriter.py | 57 +++- .../featureWriters/cursFeatureWriter.py | 29 +- .../featureWriters/kernFeatureWriter.py | 287 ++++++++++++++++-- .../featureWriters/markFeatureWriter.py | 5 +- 4 files changed, 331 insertions(+), 47 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py index b69f834e8..66d1d929f 100644 --- a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py @@ -2,10 +2,18 @@ from collections import OrderedDict, namedtuple from types import SimpleNamespace +from fontTools.designspaceLib import DesignSpaceDocument +from fontTools.feaLib.variableScalar import VariableScalar + from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY from ufo2ft.errors import InvalidFeaturesData from ufo2ft.featureWriters import ast -from ufo2ft.util import unicodeScriptExtensions +from ufo2ft.util import ( + collapse_varscalar, + get_userspace_location, + quantize, + unicodeScriptExtensions, +) INSERT_FEATURE_MARKER = r"\s*# Automatic Code.*" @@ -107,6 +115,7 @@ def setContext(self, font, feaFile, compiler=None): todo=todo, insertComments=insertComments, existingFeatures=existing, + isVariable=isinstance(font, DesignSpaceDocument), ) return self.context @@ -347,6 +356,10 @@ def getOpenTypeCategories(self): set(), ) openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {}) + # Handle case where we are a variable feature writer + if not openTypeCategories and isinstance(font, DesignSpaceDocument): + font = font.sources[0].font + openTypeCategories = font.lib.get(OPENTYPE_CATEGORIES_KEY, {}) for glyphName, category in openTypeCategories.items(): if category == "unassigned": @@ -414,6 +427,10 @@ def guessFontScripts(self): feaFile = self.context.feaFile single_scripts = set() + # If we're dealing with a Designspace, look at the default source. + if hasattr(font, "findDefault"): + font = font.findDefault().font + # First, detect scripts from the codepoints. for glyph in font: if glyph.name not in glyphSet or glyph.unicodes is None: @@ -428,3 +445,41 @@ def guessFontScripts(self): single_scripts.update(feaScripts.keys()) return single_scripts + + def _getAnchor(self, glyphName, anchorName, anchor=None): + if self.context.isVariable: + designspace = self.context.font + x_value = VariableScalar() + y_value = VariableScalar() + found = False + for source in designspace.sources: + if glyphName not in source.font: + return None + glyph = source.font[glyphName] + for anchor in glyph.anchors: + if anchor.name == anchorName: + location = get_userspace_location(designspace, source.location) + x_value.add_value(location, anchor.x) + y_value.add_value(location, anchor.y) + found = True + if not found: + return None + x, y = collapse_varscalar(x_value), collapse_varscalar(y_value) + else: + if anchor is None: + if glyphName not in self.context.font: + return None + glyph = self.context.font[glyphName] + anchors = [ + anchor for anchor in glyph.anchors if anchor.name == anchorName + ] + if not anchors: + return None + anchor = anchors[0] + + 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 diff --git a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py index adb6473b0..70f4ae4d4 100644 --- a/Lib/ufo2ft/featureWriters/cursFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/cursFeatureWriter.py @@ -1,5 +1,5 @@ from ufo2ft.featureWriters import BaseFeatureWriter, ast -from ufo2ft.util import classifyGlyphs, unicodeScriptDirection, otRoundIgnoringVariable +from ufo2ft.util import classifyGlyphs, otRoundIgnoringVariable, unicodeScriptDirection class CursFeatureWriter(BaseFeatureWriter): @@ -88,19 +88,28 @@ def _makeCursiveLookup(self, glyphs, direction=None): return lookup + def _getAnchors(self, glyphName, glyph=None): + entryAnchor = None + exitAnchor = None + entryAnchorXY = self._getAnchor(glyphName, "entry") + exitAnchorXY = self._getAnchor(glyphName, "exit") + if entryAnchorXY: + entryAnchor = ast.Anchor( + x=otRoundIgnoringVariable(entryAnchorXY[0]), + y=otRoundIgnoringVariable(entryAnchorXY[1]), + ) + if exitAnchorXY: + exitAnchor = ast.Anchor( + x=otRoundIgnoringVariable(exitAnchorXY[0]), + y=otRoundIgnoringVariable(exitAnchorXY[1]), + ) + return entryAnchor, exitAnchor + 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=otRoundIgnoringVariable(anchor.x), y=otRoundIgnoringVariable(anchor.y)) - elif anchor.name == "exit": - exitAnchor = ast.Anchor(x=otRoundIgnoringVariable(anchor.x), y=otRoundIgnoringVariable(anchor.y)) - + entryAnchor, exitAnchor = self._getAnchors(glyph.name, glyph) # A glyph can have only one of the cursive anchors (e.g. if it # attaches on one side only) if entryAnchor or exitAnchor: diff --git a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py index 0ce7c7e85..0713bb8f8 100644 --- a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py @@ -4,9 +4,13 @@ import logging from dataclasses import dataclass from types import SimpleNamespace -from typing import Iterator, Mapping +from typing import Any, Iterator, Mapping from fontTools import unicodedata +from fontTools.designspaceLib import DesignSpaceDocument +from fontTools.feaLib.variableScalar import Location as VariableScalarLocation +from fontTools.feaLib.variableScalar import VariableScalar +from fontTools.ufoLib.kerning import lookupKerningValue from fontTools.unicodedata import script_horizontal_direction from ufo2ft.constants import COMMON_SCRIPT, INDIC_SCRIPTS, USE_SCRIPTS @@ -14,7 +18,9 @@ from ufo2ft.util import ( DFLT_SCRIPTS, classifyGlyphs, + collapse_varscalar, describe_ufo, + get_userspace_location, quantize, unicodeScriptExtensions, ) @@ -60,7 +66,7 @@ class KerningPair: side1: str | tuple[str, ...] side2: str | tuple[str, ...] - value: float + value: float | VariableScalar def __lt__(self, other: KerningPair) -> bool: if not isinstance(other, KerningPair): @@ -205,20 +211,25 @@ def setContext(self, font, feaFile, compiler=None): ctx.glyphSet = self.getOrderedGlyphSet() # Unless we use the legacy append mode (which ignores insertion - # markers), if the font contains kerning and the feaFile contains `kern` - # or `dist` feature blocks, but we have no insertion markers (or they - # were misspelt and ignored), warn the user that the kerning blocks in - # the feaFile take precedence and other kerning is dropped. + # markers), if the font (Designspace: default source) contains kerning + # and the feaFile contains `kern` or `dist` feature blocks, but we have + # no insertion markers (or they were misspelt and ignored), warn the + # user that the kerning blocks in the feaFile take precedence and other + # kerning is dropped. + if hasattr(font, "findDefault"): + default_source = font.findDefault().font + else: + default_source = font if ( self.mode == "skip" - and font.kerning + and default_source.kerning and ctx.existingFeatures & self.features and not ctx.insertComments ): LOGGER.warning( "%s: font has kerning, but also manually written kerning features " "without an insertion comment. Dropping the former.", - describe_ufo(font), + describe_ufo(default_source), ) # Remember which languages are defined for which OT tag, as all @@ -299,41 +310,96 @@ def getKerningData(self): side1Classes={}, side2Classes={}, classDefs={}, pairs=pairs ) - def getKerningGroups(self): - font = self.context.font + def getKerningGroups( + self, + ) -> tuple[Mapping[str, tuple[str, ...]], Mapping[str, tuple[str, ...]]]: allGlyphs = self.context.glyphSet - side1Groups = {} - side2Groups = {} - side1Membership = {} - side2Membership = {} - 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: + + side1Groups: dict[str, tuple[str, ...]] = {} + side1Membership: dict[str, str] = {} + side2Groups: dict[str, tuple[str, ...]] = {} + side2Membership: dict[str, str] = {} + + if isinstance(self.context.font, DesignSpaceDocument): + fonts = [source.font for source in self.context.font.sources] + else: + fonts = [self.context.font] + + for font in fonts: + assert font is not None + for name, members in font.groups.items(): + # prune non-existent or skipped glyphs + members = {g for g in members if g in allGlyphs} # skip empty groups - continue - # skip groups without UFO3 public.kern{1,2} prefix - if name.startswith(SIDE1_PREFIX): - side1Groups[name] = tuple(sorted(members)) - name_truncated = name[len(SIDE1_PREFIX) :] - for member in members: - side1Membership[member] = name_truncated - elif name.startswith(SIDE2_PREFIX): - side2Groups[name] = tuple(sorted(members)) - name_truncated = name[len(SIDE2_PREFIX) :] - for member in members: - side2Membership[member] = name_truncated + if not members: + continue + # skip groups without UFO3 public.kern{1,2} prefix + if name.startswith(SIDE1_PREFIX): + name_truncated = name[len(SIDE1_PREFIX) :] + known_members = members.intersection(side1Membership.keys()) + if known_members: + for glyph_name in known_members: + original_name_truncated = side1Membership[glyph_name] + if name_truncated != original_name_truncated: + log_regrouped_glyph( + "first", + name, + original_name_truncated, + font, + glyph_name, + ) + # Skip the whole group definition if there is any + # overlap problem. + continue + group = side1Groups.get(name) + if group is None: + side1Groups[name] = tuple(sorted(members)) + for member in members: + side1Membership[member] = name_truncated + elif set(group) != members: + log_redefined_group("left", name, group, font, members) + elif name.startswith(SIDE2_PREFIX): + name_truncated = name[len(SIDE2_PREFIX) :] + known_members = members.intersection(side2Membership.keys()) + if known_members: + for glyph_name in known_members: + original_name_truncated = side2Membership[glyph_name] + if name_truncated != original_name_truncated: + log_regrouped_glyph( + "second", + name, + original_name_truncated, + font, + glyph_name, + ) + # Skip the whole group definition if there is any + # overlap problem. + continue + group = side2Groups.get(name) + if group is None: + side2Groups[name] = tuple(sorted(members)) + for member in members: + side2Membership[member] = name_truncated + elif set(group) != members: + log_redefined_group("right", name, group, font, members) self.context.side1Membership = side1Membership self.context.side2Membership = side2Membership return side1Groups, side2Groups - def getKerningPairs(self, side1Classes, side2Classes): + def getKerningPairs( + self, + side1Classes: Mapping[str, tuple[str, ...]], + side2Classes: Mapping[str, tuple[str, ...]], + ) -> list[KerningPair]: + if self.context.isVariable: + return self.getVariableKerningPairs(side1Classes, side2Classes) + glyphSet = self.context.glyphSet font = self.context.font kerning = font.kerning quantization = self.options.quantization - kerning = font.kerning + kerning: Mapping[tuple[str, str], float] = font.kerning result = [] for (side1, side2), value in kerning.items(): firstIsClass, secondIsClass = (side1 in side1Classes, side2 in side2Classes) @@ -356,6 +422,122 @@ def getKerningPairs(self, side1Classes, side2Classes): return result + def getVariableKerningPairs( + self, + side1Classes: Mapping[str, tuple[str, ...]], + side2Classes: Mapping[str, tuple[str, ...]], + ) -> list[KerningPair]: + designspace: DesignSpaceDocument = self.context.font + glyphSet = self.context.glyphSet + quantization = self.options.quantization + + # Gather utility variables for faster kerning lookups. + # TODO: Do we construct these in code elsewhere? + assert not (set(side1Classes) & set(side2Classes)) + unified_groups = {**side1Classes, **side2Classes} + + glyphToFirstGroup = { + glyph_name: group_name # TODO: Is this overwrite safe? User input is adversarial + for group_name, glyphs in side1Classes.items() + for glyph_name in glyphs + } + glyphToSecondGroup = { + glyph_name: group_name + for group_name, glyphs in side2Classes.items() + for glyph_name in glyphs + } + + # Collate every kerning pair in the designspace, as even UFOs that + # provide no entry for the pair must contribute a value at their + # source's location in the VariableScalar. + # NOTE: This is required as the DS+UFO kerning model and the OpenType + # variation model handle the absence of a kerning value at a + # given location differently: + # - DS+UFO: + # If the missing pair excepts another pair, take its value; + # Otherwise, take a value of 0. + # - OpenType: + # Always interpolate from other locations, ignoring more + # general pairs that this one excepts. + # See discussion: https://github.com/googlefonts/ufo2ft/pull/635 + all_pairs: set[tuple[str, str]] = set() + for source in designspace.sources: + if source.layerName is not None: + continue + assert source.font is not None + all_pairs |= set(source.font.kerning) + + kerning_pairs_in_progress: dict[ + tuple[str | tuple[str], str | tuple[str]], VariableScalar + ] = {} + for source in designspace.sources: + # Skip sparse sources, because they can have no kerning. + if source.layerName is not None: + continue + assert source.font is not None + + location = VariableScalarLocation( + get_userspace_location(designspace, source.location) + ) + + kerning: Mapping[tuple[str, str], float] = source.font.kerning + for pair in all_pairs: + side1, side2 = pair + firstIsClass = side1 in side1Classes + secondIsClass = side2 in side2Classes + + # Filter out pairs that reference missing groups or glyphs. + # TODO: Can we do this outside of the loop? We know the pairs already. + if not firstIsClass and side1 not in glyphSet: + continue + if not secondIsClass and side2 not in glyphSet: + continue + + # Get the kerning value for this source and quantize, following + # the DS+UFO semantics described above. + value = quantize( + lookupKerningValue( + pair, + kerning, + unified_groups, + glyphToFirstGroup=glyphToFirstGroup, + glyphToSecondGroup=glyphToSecondGroup, + ), + quantization, + ) + + if firstIsClass: + side1 = side1Classes[side1] + if secondIsClass: + side2 = side2Classes[side2] + + # TODO: Can we instantiate these outside of the loop? We know the pairs already. + var_scalar = kerning_pairs_in_progress.setdefault( + (side1, side2), VariableScalar() + ) + # NOTE: Avoid using .add_value because it instantiates a new + # VariableScalarLocation on each call. + var_scalar.values[location] = value + + # We may need to provide a default location value to the variation + # model, find out where that is. + default_source = designspace.findDefault() + assert default_source is not None + default_location = VariableScalarLocation( + get_userspace_location(designspace, default_source.location) + ) + + result = [] + for (side1, side2), value in kerning_pairs_in_progress.items(): + # TODO: Should we interpolate a default value if it's not in the + # sources, rather than inserting a zero? What would varLib do? + if default_location not in value.values: + value.values[default_location] = 0 + value = collapse_varscalar(value) + result.append(KerningPair(side1, side2, value)) + + return result + def _makePairPosRule(self, pair, side1Classes, side2Classes, rtl=False): enumerated = pair.firstIsClass ^ pair.secondIsClass valuerecord = ast.ValueRecord( @@ -382,6 +564,18 @@ def _makePairPosRule(self, pair, side1Classes, side2Classes, rtl=False): enumerated=enumerated, ) + def _filterSpacingMarks(self, marks): + if self.context.isVariable: + spacing = [] + for mark in marks: + if all( + source.font[mark].width != 0 for source in self.context.font.sources + ): + spacing.append(mark) + return spacing + + return [mark for mark in marks if self.context.font[mark].width != 0] + def _makeKerningLookup(self, name, ignoreMarks=True): lookup = ast.LookupBlock(name) if ignoreMarks and self.options.ignoreMarks: @@ -392,7 +586,7 @@ def _makeKerningLookup(self, name, ignoreMarks=True): spacing = [] if marks: - spacing = [mark for mark in marks if self.context.font[mark].width != 0] + spacing = self._filterSpacingMarks(marks) if not spacing: # Simple case, there are no spacing ("Spacing Combining") marks, # do what we've always done. @@ -816,3 +1010,30 @@ def addClassDefinition( classNames.add(className) classDef = ast.makeGlyphClassDefinition(className, group) classes[group] = classDefs[className] = classDef + + +def log_redefined_group( + side: str, name: str, group: tuple[str, ...], font: Any, members: set[str] +) -> None: + LOGGER.warning( + "incompatible %s groups: %s was previously %s, %s tried to make it %s", + side, + name, + sorted(group), + font, + sorted(members), + ) + + +def log_regrouped_glyph( + side: str, name: str, original_name: str, font: Any, member: str +) -> None: + LOGGER.warning( + "incompatible %s groups: %s tries to put glyph %s in group %s, but it's already in %s, " + "discarding", + side, + font, + member, + name, + original_name, + ) diff --git a/Lib/ufo2ft/featureWriters/markFeatureWriter.py b/Lib/ufo2ft/featureWriters/markFeatureWriter.py index eca7639d3..22fbc16b7 100644 --- a/Lib/ufo2ft/featureWriters/markFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/markFeatureWriter.py @@ -7,7 +7,7 @@ from ufo2ft.featureWriters import BaseFeatureWriter, ast from ufo2ft.util import ( classifyGlyphs, - quantize, + otRoundIgnoringVariable, unicodeInScripts, unicodeScriptExtensions, ) @@ -344,8 +344,7 @@ def _getAnchorLists(self): self.log.warning( "duplicate anchor '%s' in glyph '%s'", anchorName, glyphName ) - x = quantize(anchor.x, self.options.quantization) - y = quantize(anchor.y, self.options.quantization) + x, y = self._getAnchor(glyphName, anchorName, anchor=anchor) a = self.NamedAnchor(name=anchorName, x=x, y=y) anchorDict[anchorName] = a if anchorDict: From f1b3ef4fb67e211011c44cd59a9a3172c1453676 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 5 Dec 2023 12:25:02 +0000 Subject: [PATCH 03/17] Add a VariableFeatureCompiler This is the same as a FeatureCompiler but it knows its designspace and it also loads the RulesFeatureWriter if there are any designspace rules --- Lib/ufo2ft/featureCompiler.py | 39 ++++++++++++++++++- Lib/ufo2ft/featureWriters/__init__.py | 6 ++- .../featureWriters/baseFeatureWriter.py | 8 +++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Lib/ufo2ft/featureCompiler.py b/Lib/ufo2ft/featureCompiler.py index 788e21da4..447a3b9d8 100644 --- a/Lib/ufo2ft/featureCompiler.py +++ b/Lib/ufo2ft/featureCompiler.py @@ -102,6 +102,10 @@ def __init__(self, ufo, ttFont=None, glyphSet=None, extraSubstitutions=None): glyphOrder = ttFont.getGlyphOrder() if glyphSet is not None: + if set(glyphOrder) != set(glyphSet.keys()): + print("Glyph order incompatible") + print("In UFO but not in font:", set(glyphSet.keys()) - set(glyphOrder)) + print("In font but not in UFO:", set(glyphOrder) - set(glyphSet.keys())) assert set(glyphOrder) == set(glyphSet.keys()) else: glyphSet = ufo @@ -241,7 +245,9 @@ def _load_custom_feature_writers(self, featureWriters=None): if writer is ...: if seen_ellipsis: raise ValueError("ellipsis not allowed more than once") - writers = loadFeatureWriters(self.ufo) + writers = loadFeatureWriters( + self.ufo, variable=hasattr(self, "designspace") + ) if writers is not None: result.extend(writers) else: @@ -407,3 +413,34 @@ def warn_about_miscased_insertion_markers( text, pattern_case.pattern, ) + + +class VariableFeatureCompiler(FeatureCompiler): + """Generate a variable feature file and compile OpenType tables from a + designspace file. + """ + + def __init__( + self, + ufo, + designspace, + ttFont=None, + glyphSet=None, + featureWriters=None, + **kwargs, + ): + self.designspace = designspace + super().__init__(ufo, ttFont, glyphSet, featureWriters, **kwargs) + + def setupFeatures(self): + if self.featureWriters: + featureFile = parseLayoutFeatures(self.ufo) + + for writer in self.featureWriters: + writer.write(self.designspace, featureFile, compiler=self) + + # stringify AST to get correct line numbers in error messages + self.features = featureFile.asFea() + else: + # no featureWriters, simply read existing features' text + self.features = self.ufo.features.text or "" diff --git a/Lib/ufo2ft/featureWriters/__init__.py b/Lib/ufo2ft/featureWriters/__init__.py index ed9706c3b..c04b48da2 100644 --- a/Lib/ufo2ft/featureWriters/__init__.py +++ b/Lib/ufo2ft/featureWriters/__init__.py @@ -48,7 +48,7 @@ def write(self, font, feaFile, compiler=None) return True -def loadFeatureWriters(ufo, ignoreErrors=True): +def loadFeatureWriters(ufo, ignoreErrors=True, variable=False): """Check UFO lib for key "com.github.googlei18n.ufo2ft.featureWriters", containing a list of dicts, each having the following key/value pairs: For example: @@ -63,6 +63,10 @@ def loadFeatureWriters(ufo, ignoreErrors=True): the built-in ufo2ft.featureWriters), and instantiate it with the given 'options' dict. + If ``variable`` is true, then the feature writer class is asked if it + has an associated class which works on Designspace files instead of UFOs, + and if so, then this is used instead. + Return the list of feature writer objects. If the 'featureWriters' key is missing from the UFO lib, return None. diff --git a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py index 66d1d929f..256e044f7 100644 --- a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py @@ -1,3 +1,4 @@ +import copy import logging from collections import OrderedDict, namedtuple from types import SimpleNamespace @@ -321,6 +322,8 @@ def compileGSUB(self): from ufo2ft.util import compileGSUB compiler = self.context.compiler + fvar = None + feafile = self.context.feaFile if compiler is not None: # The result is cached in the compiler instance, so if another # writer requests one it is not compiled again. @@ -328,12 +331,15 @@ def compileGSUB(self): return compiler._gsub glyphOrder = compiler.ttFont.getGlyphOrder() + fvar = compiler.ttFont.get("fvar") + if fvar: + feafile = copy.deepcopy(feafile) else: # the 'real' glyph order doesn't matter because the table is not # compiled to binary, only the glyph names are used glyphOrder = sorted(self.context.font.keys()) - gsub = compileGSUB(self.context.feaFile, glyphOrder) + gsub = compileGSUB(feafile, glyphOrder, fvar=fvar) if compiler and not hasattr(compiler, "_gsub"): compiler._gsub = gsub From dd278fc0affa901e561df25d5dcab1b7585ffac1 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 19 Jul 2022 08:55:17 +0100 Subject: [PATCH 04/17] Hook up variable feature compilation to __init__.py --- Lib/ufo2ft/__init__.py | 29 ++++++++++ Lib/ufo2ft/_compilers/baseCompiler.py | 83 ++++++++++++++++++++++++++- Lib/ufo2ft/featureCompiler.py | 23 ++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/Lib/ufo2ft/__init__.py b/Lib/ufo2ft/__init__.py index 89f4ba4a8..5f9e61d78 100644 --- a/Lib/ufo2ft/__init__.py +++ b/Lib/ufo2ft/__init__.py @@ -18,6 +18,11 @@ "compileVariableCFF2s", ] +try: + from ._version import version as __version__ +except ImportError: + __version__ = "0.0.0+unknown" + def compileTTF(ufo, **kwargs): """Create FontTools TrueType font from a UFO. @@ -168,6 +173,30 @@ def compileVariableTTFs(designSpaceDoc, **kwargs): def compileInterpolatableTTFsFromDS(designSpaceDoc, **kwargs): + """Create FontTools TrueType fonts from the DesignSpaceDocument UFO sources + with interpolatable outlines. Cubic curves are converted compatibly to + quadratic curves using the Cu2Qu conversion algorithm. + + If the Designspace contains a "public.skipExportGlyphs" lib key, these + glyphs will not be exported to the final font. If these glyphs are used as + components in any other glyph, those components get decomposed. If the lib + key doesn't exist in the Designspace, all glyphs are exported (keys in + individual UFOs are ignored). UFO groups and kerning will be pruned of + skipped glyphs. + + The DesignSpaceDocument should contain SourceDescriptor objects with 'font' + attribute set to an already loaded defcon.Font object (or compatible UFO + Font class). If 'font' attribute is unset or None, an AttributeError exception + is thrown. + + Return a copy of the DesignSpaceDocument object (or the same one if + inplace=True) with the source's 'font' attribute set to the corresponding + TTFont instance. + + For sources that have the 'layerName' attribute defined, the corresponding TTFont + object will contain only a minimum set of tables ("head", "hmtx", "glyf", "loca", + "maxp", "post" and "vmtx"), and no OpenType layout tables. + """ return InterpolatableTTFCompiler(**kwargs).compile_designspace(designSpaceDoc) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index b2704f8f4..050fb030c 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -4,13 +4,19 @@ from dataclasses import dataclass from typing import Callable, Optional, Type +from fontTools import varLib from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts from fontTools.misc.loggingTools import Timer from fontTools.otlLib.optimize.gpos import GPOS_COMPACT_MODE_ENV_KEY from ufo2ft.constants import MTI_FEATURES_PREFIX from ufo2ft.errors import InvalidDesignSpaceData -from ufo2ft.featureCompiler import FeatureCompiler, MtiFeatureCompiler +from ufo2ft.featureCompiler import ( + FeatureCompiler, + MtiFeatureCompiler, + VariableFeatureCompiler, + _featuresCompatible, +) from ufo2ft.postProcessor import PostProcessor from ufo2ft.util import ( _notdefGlyphFallback, @@ -248,6 +254,14 @@ def _compileNeededSources(self, designSpaceDoc): originalSources = {} + # If the feature files are compatible between the sources, we can save + # time by building a variable feature file right at the end. + can_optimize_features = _featuresCompatible(designSpaceDoc) + if can_optimize_features: + self.logger.info("Features are compatible across masters; building later") + + originalSources = {} + # Compile all needed sources in each interpolable subspace to make sure # they're all compatible; that also ensures that sub-vfs within the same # interpolable sub-space are compatible too. @@ -265,6 +279,7 @@ def _compileNeededSources(self, designSpaceDoc): self.useProductionNames = False save_postprocessor = self.postProcessorClass self.postProcessorClass = None + self.skipFeatureCompilation = can_optimize_features try: ttfDesignSpace = self.compile_designspace(subDoc) finally: @@ -275,9 +290,11 @@ def _compileNeededSources(self, designSpaceDoc): # Stick TTFs back into original big DS for ttfSource in ttfDesignSpace.sources: + if can_optimize_features: + originalSources[ttfSource.name] = sourcesByName[ttfSource.name].font sourcesByName[ttfSource.name].font = ttfSource.font - return vfNameToBaseUfo, originalSources + return vfNameToBaseUfo, can_optimize_features, originalSources def compile_variable(self, designSpaceDoc): if not self.inplace: @@ -285,6 +302,7 @@ def compile_variable(self, designSpaceDoc): ( vfNameToBaseUfo, + buildVariableFeatures, originalSources, ) = self._compileNeededSources(designSpaceDoc) @@ -294,13 +312,74 @@ def compile_variable(self, designSpaceDoc): self.logger.info("Building variable TTF fonts: %s", ", ".join(vfNameToBaseUfo)) excludeVariationTables = self.excludeVariationTables + if buildVariableFeatures: + # Skip generating feature variations in varLib; we are handling + # the feature variations as part of compiling variable features, + # which we'll do later, so we don't need to produce them here. + excludeVariationTables = set(excludeVariationTables) | {"GSUB"} with self.timer("merge fonts to variable"): vfNameToTTFont = self._merge(designSpaceDoc, excludeVariationTables) + if buildVariableFeatures: + self.compile_all_variable_features( + designSpaceDoc, vfNameToTTFont, originalSources + ) for vfName, varfont in list(vfNameToTTFont.items()): vfNameToTTFont[vfName] = self.postprocess( varfont, vfNameToBaseUfo[vfName], glyphSet=None ) return vfNameToTTFont + + def compile_all_variable_features( + self, designSpaceDoc, vfNameToTTFont, originalSources, debugFeatureFile=False + ): + interpolableSubDocs = [ + subDoc for _location, subDoc in splitInterpolable(designSpaceDoc) + ] + for subDoc in interpolableSubDocs: + for vfName, vfDoc in splitVariableFonts(subDoc): + if vfName not in vfNameToTTFont: + continue + ttFont = vfNameToTTFont[vfName] + # vfDoc is now full of TTFs, create a UFO-sourced equivalent + ufoDoc = vfDoc.deepcopyExceptFonts() + for ttfSource, ufoSource in zip(vfDoc.sources, ufoDoc.sources): + ufoSource.font = originalSources[ttfSource.name] + self.compile_variable_features(ufoDoc, ttFont) + + def compile_variable_features(self, designSpaceDoc, ttFont): + default_ufo = designSpaceDoc.findDefault().font + + # Delete anything from the UFO glyphset which didn't make it into the font. + fontglyphs = ttFont.getGlyphOrder() + glyphSet = {g.name: g for g in default_ufo if g.name in fontglyphs} + + # Add anything we added to the TTF without telling the UFO + if ".notdef" not in glyphSet: + glyphSet[".notdef"] = StubGlyph(".notdef", 0, 0, 0, 0) + + featureCompiler = VariableFeatureCompiler( + default_ufo, designSpaceDoc, ttFont=ttFont, glyphSet=glyphSet + ) + featureCompiler.compile() + + if self.debugFeatureFile: + if hasattr(featureCompiler, "writeFeatures"): + featureCompiler.writeFeatures(self.debugFeatureFile) + + # Add back feature variations, as the code above would overwrite them. + designSpaceData = varLib.load_designspace(designSpaceDoc) + featureTag = designSpaceData.lib.get( + varLib.FEAVAR_FEATURETAG_LIB_KEY, + "rclt" if designSpaceData.rulesProcessingLast else "rvrn", + ) + if designSpaceData.rules: + varLib._add_GSUB_feature_variations( + ttFont, + designSpaceData.axes, + designSpaceData.internal_axis_supports, + designSpaceData.rules, + featureTag, + ) diff --git a/Lib/ufo2ft/featureCompiler.py b/Lib/ufo2ft/featureCompiler.py index 447a3b9d8..0d20e90fd 100644 --- a/Lib/ufo2ft/featureCompiler.py +++ b/Lib/ufo2ft/featureCompiler.py @@ -9,6 +9,7 @@ from tempfile import NamedTemporaryFile from fontTools import mtiLib +from fontTools.designspaceLib import DesignSpaceDocument, SourceDescriptor from fontTools.feaLib.builder import addOpenTypeFeaturesFromString from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound from fontTools.feaLib.parser import Parser @@ -444,3 +445,25 @@ def setupFeatures(self): else: # no featureWriters, simply read existing features' text self.features = self.ufo.features.text or "" + + +def _featuresCompatible(designSpaceDoc: DesignSpaceDocument) -> bool: + """Returns whether the features of the individual source UFOs are the same. + + NOTE: Only compares the feature file text inside the source UFO and does not + follow imports. This will suffice as long as no external feature file is + using variable syntax and all sources are stored n the same parent folder + (so the same includes point to the same files). + """ + + assert all(hasattr(source.font, "features") for source in designSpaceDoc.sources) + + def transform(f: SourceDescriptor) -> str: + # Strip comments + text = re.sub("(?m)#.*$", "", f.font.features.text or "") + # Strip extraneous whitespace + text = re.sub(r"\s+", " ", text) + return text + + first = transform(designSpaceDoc.sources[0]) + return all(transform(s) == first for s in designSpaceDoc.sources[1:]) From d0252b690c1e3532d3c497bdeb8883f952d2bfad Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 27 Jul 2022 13:20:17 +0100 Subject: [PATCH 05/17] Some tests for variable feature writing Add test for different feature writers Fix up test expectations This should now test the variable features --- tests/data/TestVarfea-Bold.ufo/fontinfo.plist | 36 + .../glyphs/alef-ar.fina.glif | 31 + .../TestVarfea-Bold.ufo/glyphs/contents.plist | 16 + .../glyphs/dotabove-ar.glif | 31 + .../glyphs/layerinfo.plist | 19 + .../peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif | 77 +++ .../glyphs/peh-ar.init.glif | 84 +++ .../TestVarfea-Bold.ufo/glyphs/space.glif | 13 + tests/data/TestVarfea-Bold.ufo/kerning.plist | 11 + .../TestVarfea-Bold.ufo/layercontents.plist | 10 + tests/data/TestVarfea-Bold.ufo/lib.plist | 80 +++ tests/data/TestVarfea-Bold.ufo/metainfo.plist | 10 + .../TestVarfea-Regular.ufo/fontinfo.plist | 48 ++ .../glyphs/alef-ar.fina.glif | 31 + .../glyphs/contents.plist | 16 + .../glyphs/dotabove-ar.glif | 31 + .../glyphs/layerinfo.plist | 19 + .../peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif | 77 +++ .../glyphs/peh-ar.init.glif | 84 +++ .../TestVarfea-Regular.ufo/glyphs/space.glif | 13 + .../data/TestVarfea-Regular.ufo/kerning.plist | 11 + .../layercontents.plist | 10 + tests/data/TestVarfea-Regular.ufo/lib.plist | 74 ++ .../TestVarfea-Regular.ufo/metainfo.plist | 10 + tests/data/TestVarfea.designspace | 30 + tests/data/TestVarfea.glyphs | 646 ++++++++++++++++++ tests/data/TestVariableFont-CFF2-cffsubr.ttx | 2 +- tests/data/TestVariableFont-CFF2-post3.ttx | 2 +- ...stVariableFont-CFF2-useProductionNames.ttx | 2 +- tests/data/TestVariableFont-CFF2.ttx | 2 +- tests/data/TestVariableFont-TTF-post3.ttx | 2 +- ...estVariableFont-TTF-useProductionNames.ttx | 2 +- tests/data/TestVariableFont-TTF.ttx | 2 +- .../variableFeatureWriter_test.py | 54 ++ tests/integration_test.py | 21 +- 35 files changed, 1597 insertions(+), 10 deletions(-) create mode 100644 tests/data/TestVarfea-Bold.ufo/fontinfo.plist create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif create mode 100644 tests/data/TestVarfea-Bold.ufo/glyphs/space.glif create mode 100644 tests/data/TestVarfea-Bold.ufo/kerning.plist create mode 100644 tests/data/TestVarfea-Bold.ufo/layercontents.plist create mode 100644 tests/data/TestVarfea-Bold.ufo/lib.plist create mode 100644 tests/data/TestVarfea-Bold.ufo/metainfo.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/fontinfo.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif create mode 100644 tests/data/TestVarfea-Regular.ufo/glyphs/space.glif create mode 100644 tests/data/TestVarfea-Regular.ufo/kerning.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/layercontents.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/lib.plist create mode 100644 tests/data/TestVarfea-Regular.ufo/metainfo.plist create mode 100644 tests/data/TestVarfea.designspace create mode 100644 tests/data/TestVarfea.glyphs create mode 100644 tests/featureWriters/variableFeatureWriter_test.py diff --git a/tests/data/TestVarfea-Bold.ufo/fontinfo.plist b/tests/data/TestVarfea-Bold.ufo/fontinfo.plist new file mode 100644 index 000000000..d6b5d7964 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/fontinfo.plist @@ -0,0 +1,36 @@ + + + + + ascender + 600 + capHeight + 700 + descender + -400 + familyName + TestVarfea + italicAngle + 0 + openTypeHeadCreated + 2022/07/26 14:49:29 + openTypeOS2Type + + 3 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleName + Bold + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 0 + xHeight + 500 + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif new file mode 100644 index 000000000..873945f8e --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/alef-ar.fina.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:51:14 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist b/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist new file mode 100644 index 000000000..4572eabab --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/contents.plist @@ -0,0 +1,16 @@ + + + + + alef-ar.fina + alef-ar.fina.glif + dotabove-ar + dotabove-ar.glif + peh-ar.init + peh-ar.init.glif + peh-ar.init.BRACKET.varAlt01 + peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif + space + space.glif + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif new file mode 100644 index 000000000..05dde66a1 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/dotabove-ar.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:50:50 + com.schriftgestaltung.Glyphs.originalWidth + 300 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist b/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist new file mode 100644 index 000000000..82fbc0d82 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/layerinfo.plist @@ -0,0 +1,19 @@ + + + + + lib + + com.schriftgestaltung.layerId + B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2 + com.schriftgestaltung.layerOrderInGlyph.alef-ar.fina + 1 + com.schriftgestaltung.layerOrderInGlyph.dotabove-ar + 1 + com.schriftgestaltung.layerOrderInGlyph.peh-ar.init + 1 + com.schriftgestaltung.layerOrderInGlyph.space + 1 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif new file mode 100644 index 000000000..82cc961e3 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs._originalLayerName + 26 Jul 22, 15:59 + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif new file mode 100644 index 000000000..a8e87507e --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/peh-ar.init.glif @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif b/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif new file mode 100644 index 000000000..fa922f49a --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/glyphs/space.glif @@ -0,0 +1,13 @@ + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/26 15:00:45 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/kerning.plist b/tests/data/TestVarfea-Bold.ufo/kerning.plist new file mode 100644 index 000000000..6bf0bd2ab --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/kerning.plist @@ -0,0 +1,11 @@ + + + + + alef-ar.fina + + alef-ar.fina + 35 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/layercontents.plist b/tests/data/TestVarfea-Bold.ufo/layercontents.plist new file mode 100644 index 000000000..b9c1a4f27 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/tests/data/TestVarfea-Bold.ufo/lib.plist b/tests/data/TestVarfea-Bold.ufo/lib.plist new file mode 100644 index 000000000..d4c4a8cc2 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/lib.plist @@ -0,0 +1,80 @@ + + + + + GSCornerRadius + 15 + GSOffsetHorizontal + -30 + GSOffsetVertical + -25 + com.github.googlei18n.ufo2ft.filters + + + name + eraseOpenCorners + namespace + glyphsLib.filters + pre + + + + com.schriftgestaltung.appVersion + 3217 + com.schriftgestaltung.customParameter.GSFont.DisplayStrings + + اا + /dotabove-ar + + com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment + + com.schriftgestaltung.customParameter.GSFont.useNiceNames + 1 + com.schriftgestaltung.customParameter.GSFontMaster.customValue + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue1 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue2 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue3 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.iconName + Bold + com.schriftgestaltung.customParameter.GSFontMaster.weightValue + 1000 + com.schriftgestaltung.customParameter.GSFontMaster.widthValue + 100 + com.schriftgestaltung.fontMasterID + B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2 + com.schriftgestaltung.fontMasterOrder + 1 + com.schriftgestaltung.keyboardIncrement + 1 + com.schriftgestaltung.weight + Regular + com.schriftgestaltung.weightValue + 1000 + com.schriftgestaltung.width + Regular + com.schriftgestaltung.widthValue + 100 + public.glyphOrder + + alef-ar.fina + peh-ar.init + space + dotabove-ar + + public.postscriptNames + + alef-ar.fina + uniFE8E + dotabove-ar + dotabovear + peh-ar.init + uniFB58 + peh-ar.init.BRACKET.varAlt01 + uniFB58.BRACKET.varAlt01 + + + diff --git a/tests/data/TestVarfea-Bold.ufo/metainfo.plist b/tests/data/TestVarfea-Bold.ufo/metainfo.plist new file mode 100644 index 000000000..7b8b34ac6 --- /dev/null +++ b/tests/data/TestVarfea-Bold.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + com.github.fonttools.ufoLib + formatVersion + 3 + + diff --git a/tests/data/TestVarfea-Regular.ufo/fontinfo.plist b/tests/data/TestVarfea-Regular.ufo/fontinfo.plist new file mode 100644 index 000000000..9f3eadf5e --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/fontinfo.plist @@ -0,0 +1,48 @@ + + + + + ascender + 600 + capHeight + 0 + descender + -400 + familyName + TestVarfea + italicAngle + 0 + openTypeHeadCreated + 2022/07/26 14:49:29 + openTypeOS2Type + + 3 + + postscriptBlueValues + + -16 + 0 + 600 + 616 + + postscriptOtherBlues + + -416 + -400 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleName + Regular + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 0 + xHeight + 0 + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif new file mode 100644 index 000000000..dfd599570 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/alef-ar.fina.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:51:14 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist b/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist new file mode 100644 index 000000000..4572eabab --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/contents.plist @@ -0,0 +1,16 @@ + + + + + alef-ar.fina + alef-ar.fina.glif + dotabove-ar + dotabove-ar.glif + peh-ar.init + peh-ar.init.glif + peh-ar.init.BRACKET.varAlt01 + peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif + space + space.glif + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif new file mode 100644 index 000000000..d9ec3589f --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/dotabove-ar.glif @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2023/11/20 16:50:50 + com.schriftgestaltung.Glyphs.originalWidth + 300 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist b/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist new file mode 100644 index 000000000..e4d188f0a --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/layerinfo.plist @@ -0,0 +1,19 @@ + + + + + lib + + com.schriftgestaltung.layerId + m01 + com.schriftgestaltung.layerOrderInGlyph.alef-ar.fina + 0 + com.schriftgestaltung.layerOrderInGlyph.dotabove-ar + 0 + com.schriftgestaltung.layerOrderInGlyph.peh-ar.init + 0 + com.schriftgestaltung.layerOrderInGlyph.space + 0 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif new file mode 100644 index 000000000..26c13c38c --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.B_R_A_C_K_E_T_.varA_lt01.glif @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs._originalLayerName + 26 Jul 22, 15:58 + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif new file mode 100644 index 000000000..03c6ba3ad --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/peh-ar.init.glif @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/27 08:01:34 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif b/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif new file mode 100644 index 000000000..ad61b901c --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/glyphs/space.glif @@ -0,0 +1,13 @@ + + + + + + + + + com.schriftgestaltung.Glyphs.lastChange + 2022/07/26 15:00:45 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/kerning.plist b/tests/data/TestVarfea-Regular.ufo/kerning.plist new file mode 100644 index 000000000..696a1f403 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/kerning.plist @@ -0,0 +1,11 @@ + + + + + alef-ar.fina + + alef-ar.fina + 15 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/layercontents.plist b/tests/data/TestVarfea-Regular.ufo/layercontents.plist new file mode 100644 index 000000000..b9c1a4f27 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/tests/data/TestVarfea-Regular.ufo/lib.plist b/tests/data/TestVarfea-Regular.ufo/lib.plist new file mode 100644 index 000000000..0b5415f31 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/lib.plist @@ -0,0 +1,74 @@ + + + + + com.github.googlei18n.ufo2ft.filters + + + name + eraseOpenCorners + namespace + glyphsLib.filters + pre + + + + com.schriftgestaltung.appVersion + 3217 + com.schriftgestaltung.customParameter.GSFont.DisplayStrings + + اا + /dotabove-ar + + com.schriftgestaltung.customParameter.GSFont.disablesAutomaticAlignment + + com.schriftgestaltung.customParameter.GSFont.useNiceNames + 1 + com.schriftgestaltung.customParameter.GSFontMaster.customValue + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue1 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue2 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.customValue3 + 0 + com.schriftgestaltung.customParameter.GSFontMaster.iconName + + com.schriftgestaltung.customParameter.GSFontMaster.weightValue + 100 + com.schriftgestaltung.customParameter.GSFontMaster.widthValue + 100 + com.schriftgestaltung.fontMasterID + m01 + com.schriftgestaltung.fontMasterOrder + 0 + com.schriftgestaltung.keyboardIncrement + 1 + com.schriftgestaltung.weight + Regular + com.schriftgestaltung.weightValue + 100 + com.schriftgestaltung.width + Regular + com.schriftgestaltung.widthValue + 100 + public.glyphOrder + + alef-ar.fina + peh-ar.init + space + dotabove-ar + + public.postscriptNames + + alef-ar.fina + uniFE8E + dotabove-ar + dotabovear + peh-ar.init + uniFB58 + peh-ar.init.BRACKET.varAlt01 + uniFB58.BRACKET.varAlt01 + + + diff --git a/tests/data/TestVarfea-Regular.ufo/metainfo.plist b/tests/data/TestVarfea-Regular.ufo/metainfo.plist new file mode 100644 index 000000000..7b8b34ac6 --- /dev/null +++ b/tests/data/TestVarfea-Regular.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + com.github.fonttools.ufoLib + formatVersion + 3 + + diff --git a/tests/data/TestVarfea.designspace b/tests/data/TestVarfea.designspace new file mode 100644 index 000000000..aa7a1a614 --- /dev/null +++ b/tests/data/TestVarfea.designspace @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/TestVarfea.glyphs b/tests/data/TestVarfea.glyphs new file mode 100644 index 000000000..5c0e945d2 --- /dev/null +++ b/tests/data/TestVarfea.glyphs @@ -0,0 +1,646 @@ +{ +.appVersion = "3217"; +.formatVersion = 3; +DisplayStrings = ( +"اا", +"/dotabove-ar" +); +axes = ( +{ +name = Weight; +tag = wght; +} +); +date = "2022-07-26 14:49:29 +0000"; +familyName = TestVarfea; +fontMaster = ( +{ +axesValues = ( +100 +); +id = m01; +metricValues = ( +{ +over = 16; +pos = 600; +}, +{ +over = -16; +}, +{ +over = -16; +pos = -400; +}, +{ +}, +{ +} +); +name = Regular; +}, +{ +axesValues = ( +1000 +); +iconName = Bold; +id = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +metricValues = ( +{ +pos = 600; +}, +{ +}, +{ +pos = -400; +}, +{ +pos = 700; +}, +{ +pos = 500; +} +); +name = Bold; +userData = { +GSCornerRadius = 15; +GSOffsetHorizontal = -30; +GSOffsetVertical = -25; +}; +} +); +glyphs = ( +{ +glyphname = "alef-ar.fina"; +lastChange = "2023-11-20 16:51:14 +0000"; +layers = ( +{ +anchors = ( +{ +name = entry; +pos = (299,97); +}, +{ +name = top; +pos = (211,730); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(270,173,o), +(283,133,o), +(321,118,cs), +(375,97,o), +(403,105,o), +(466,133,c), +(491,13,l), +(427,-19,o), +(381,-25,o), +(336,-25,cs), +(139,-25,o), +(160,121,o), +(160,569,c), +(270,601,l) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = entry; +pos = (330,115); +}, +{ +name = top; +pos = (214,797); +} +); +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +shapes = ( +{ +closed = 1; +nodes = ( +(297,173,o), +(310,133,o), +(348,118,cs), +(402,97,o), +(433,86,o), +(487,123,c), +(565,-41,l), +(501,-73,o), +(400,-76,o), +(355,-76,cs), +(108,-76,o), +(137,121,o), +(137,569,c), +(297,601,l) +); +} +); +width = 600; +} +); +unicode = 1575; +}, +{ +glyphname = "peh-ar.init"; +lastChange = "2022-07-27 08:01:34 +0000"; +layers = ( +{ +anchors = ( +{ +name = exit; +pos = (161,54); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(466,268,l), +(450,61,o), +(400,-33,o), +(291,-56,cs), +(165,-84,o), +(107,-64,o), +(67,22,c), +(67,130,l), +(124,89,o), +(185,67,o), +(241,67,cs), +(332,67,o), +(370,122,o), +(378,268,c) +); +}, +{ +closed = 1; +nodes = ( +(164,-235,ls), +(158,-241,o), +(158,-250,o), +(164,-257,cs), +(250,-347,ls), +(256,-354,o), +(265,-354,o), +(272,-347,cs), +(362,-261,ls), +(368,-255,o), +(368,-246,o), +(362,-239,cs), +(276,-149,ls), +(270,-142,o), +(261,-142,o), +(254,-149,cs) +); +}, +{ +closed = 1; +nodes = ( +(384,-235,ls), +(378,-241,o), +(378,-250,o), +(384,-257,cs), +(470,-347,ls), +(476,-354,o), +(485,-354,o), +(492,-347,cs), +(582,-261,ls), +(588,-255,o), +(588,-246,o), +(582,-239,cs), +(496,-149,ls), +(490,-142,o), +(481,-142,o), +(474,-149,cs) +); +}, +{ +closed = 1; +nodes = ( +(264,-435,ls), +(258,-441,o), +(258,-450,o), +(264,-457,cs), +(350,-547,ls), +(356,-554,o), +(365,-554,o), +(372,-547,cs), +(462,-461,ls), +(468,-455,o), +(468,-446,o), +(462,-439,cs), +(376,-349,ls), +(370,-342,o), +(361,-342,o), +(354,-349,cs) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = exit; +pos = (73,89); +} +); +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +shapes = ( +{ +closed = 1; +nodes = ( +(525,322,l), +(509,19,o), +(513,14,o), +(342,-63,cs), +(232,-113,o), +(142,-102,o), +(61,-87,c), +(61,104,l), +(104,84,o), +(139,75,o), +(167,75,cs), +(252,75,o), +(277,161,o), +(291,322,c) +); +}, +{ +closed = 1; +nodes = ( +(115,-208,ls), +(107,-215,o), +(107,-228,o), +(115,-236,cs), +(221,-347,ls), +(229,-355,o), +(241,-354,o), +(248,-347,cs), +(359,-241,ls), +(367,-233,o), +(367,-222,o), +(359,-213,cs), +(253,-102,ls), +(246,-94,o), +(233,-95,o), +(226,-102,cs) +); +}, +{ +closed = 1; +nodes = ( +(387,-208,ls), +(379,-216,o), +(379,-227,o), +(387,-236,cs), +(493,-347,ls), +(500,-355,o), +(513,-354,o), +(520,-347,cs), +(631,-241,ls), +(639,-234,o), +(639,-221,o), +(631,-213,cs), +(525,-102,ls), +(517,-94,o), +(505,-95,o), +(498,-102,cs) +); +}, +{ +closed = 1; +nodes = ( +(238,-455,ls), +(230,-462,o), +(230,-475,o), +(238,-483,cs), +(345,-594,ls), +(352,-602,o), +(364,-601,o), +(372,-594,cs), +(483,-488,ls), +(491,-481,o), +(491,-468,o), +(483,-460,cs), +(377,-349,ls), +(369,-341,o), +(357,-342,o), +(350,-349,cs) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = exit; +pos = (89,53); +} +); +associatedMasterId = m01; +attr = { +axisRules = ( +{ +min = 600; +} +); +}; +layerId = "8B3F4CCE-5E0D-437E-916C-4646A5030CF3"; +name = "26 Jul 22, 15:58"; +shapes = ( +{ +closed = 1; +nodes = ( +(490,215,l), +(469,-10,o), +(412,-54,o), +(266,-54,cs), +(161,-54,o), +(90,-27,o), +(67,22,c), +(67,130,l), +(137,80,o), +(173,67,o), +(241,67,cs), +(291,67,o), +(315,118,o), +(325,290,c) +); +}, +{ +closed = 1; +nodes = ( +(194,-235,ls), +(188,-241,o), +(188,-250,o), +(194,-257,cs), +(280,-347,ls), +(286,-354,o), +(296,-353,o), +(302,-347,cs), +(369,-283,l), +(430,-347,ls), +(436,-354,o), +(446,-353,o), +(452,-347,cs), +(542,-261,ls), +(548,-255,o), +(548,-246,o), +(542,-239,cs), +(456,-149,ls), +(450,-142,o), +(440,-143,o), +(434,-149,cs), +(367,-213,l), +(306,-149,ls), +(300,-142,o), +(290,-143,o), +(284,-149,cs) +); +}, +{ +closed = 1; +nodes = ( +(264,-435,ls), +(258,-441,o), +(258,-450,o), +(264,-457,cs), +(350,-547,ls), +(356,-554,o), +(366,-553,o), +(372,-547,cs), +(462,-461,ls), +(468,-455,o), +(468,-446,o), +(462,-439,cs), +(376,-349,ls), +(370,-342,o), +(360,-343,o), +(354,-349,cs) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = exit; +pos = (73,85); +} +); +associatedMasterId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +attr = { +axisRules = ( +{ +min = 600; +} +); +}; +layerId = "3CC1DB06-AB7B-409C-841A-4209D835026A"; +name = "26 Jul 22, 15:59"; +shapes = ( +{ +closed = 1; +nodes = ( +(525,322,l), +(509,19,o), +(513,14,o), +(342,-63,cs), +(232,-113,o), +(142,-102,o), +(61,-87,c), +(61,104,l), +(104,84,o), +(139,75,o), +(167,75,cs), +(252,75,o), +(277,161,o), +(291,322,c) +); +}, +{ +closed = 1; +nodes = ( +(153,-223,ls), +(146,-230,o), +(145,-242,o), +(153,-250,cs), +(259,-361,ls), +(267,-370,o), +(279,-369,o), +(287,-361,cs), +(369,-282,l), +(445,-361,ls), +(453,-369,o), +(464,-369,o), +(472,-361,cs), +(583,-255,ls), +(590,-248,o), +(590,-236,o), +(583,-228,cs), +(477,-117,ls), +(469,-108,o), +(457,-109,o), +(449,-117,cs), +(367,-196,l), +(291,-117,ls), +(283,-109,o), +(272,-109,o), +(264,-117,cs) +); +}, +{ +closed = 1; +nodes = ( +(208,-495,ls), +(199,-504,o), +(198,-519,o), +(208,-529,cs), +(339,-666,ls), +(348,-676,o), +(364,-675,o), +(373,-666,cs), +(510,-535,ls), +(520,-526,o), +(520,-511,o), +(510,-501,cs), +(379,-364,ls), +(369,-354,o), +(354,-355,o), +(345,-364,cs) +); +} +); +width = 600; +} +); +unicode = 1662; +}, +{ +glyphname = space; +lastChange = "2022-07-26 15:00:45 +0000"; +layers = ( +{ +layerId = m01; +width = 200; +}, +{ +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +width = 600; +} +); +unicode = 32; +}, +{ +glyphname = "dotabove-ar"; +lastChange = "2023-11-20 16:50:50 +0000"; +layers = ( +{ +anchors = ( +{ +name = _top; +pos = (100,320); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(104,232,l), +(160,271,o), +(187,303,o), +(187,326,cs), +(187,349,o), +(170,372,o), +(109,411,c), +(100,411,l), +(83,400,o), +(30,341,o), +(13,315,c), +(13,306,l), +(40,285,o), +(68,260,o), +(96,232,c) +); +} +); +width = 300; +}, +{ +anchors = ( +{ +name = _top; +pos = (125,416); +} +); +layerId = "B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2"; +shapes = ( +{ +closed = 1; +nodes = ( +(129,292,l), +(180,328,o), +(231,372,o), +(231,409,cs), +(231,445,o), +(196,472,o), +(135,511,c), +(124,511,l), +(105,499,o), +(38,425,o), +(18,393,c), +(18,382,l), +(50,359,o), +(60,350,o), +(120,292,c) +); +} +); +width = 300; +} +); +} +); +kerningLTR = { +m01 = { +"alef-ar.fina" = { +"alef-ar.fina" = 15; +}; +}; +"B1C208F5-A14F-4863-B7E7-1D4BAD4C88B2" = { +"alef-ar.fina" = { +"alef-ar.fina" = 35; +}; +}; +}; +metrics = ( +{ +type = ascender; +}, +{ +type = baseline; +}, +{ +type = descender; +}, +{ +type = "cap height"; +}, +{ +type = "x-height"; +} +); +unitsPerEm = 1000; +versionMajor = 1; +versionMinor = 0; +} diff --git a/tests/data/TestVariableFont-CFF2-cffsubr.ttx b/tests/data/TestVariableFont-CFF2-cffsubr.ttx index a7ad531d6..b2a8804a7 100644 --- a/tests/data/TestVariableFont-CFF2-cffsubr.ttx +++ b/tests/data/TestVariableFont-CFF2-cffsubr.ttx @@ -328,7 +328,7 @@ - + diff --git a/tests/data/TestVariableFont-CFF2-post3.ttx b/tests/data/TestVariableFont-CFF2-post3.ttx index 163c07fd1..d8f17e020 100644 --- a/tests/data/TestVariableFont-CFF2-post3.ttx +++ b/tests/data/TestVariableFont-CFF2-post3.ttx @@ -305,7 +305,7 @@ - + diff --git a/tests/data/TestVariableFont-CFF2-useProductionNames.ttx b/tests/data/TestVariableFont-CFF2-useProductionNames.ttx index b78c88cb1..9bf821052 100644 --- a/tests/data/TestVariableFont-CFF2-useProductionNames.ttx +++ b/tests/data/TestVariableFont-CFF2-useProductionNames.ttx @@ -322,7 +322,7 @@ - + diff --git a/tests/data/TestVariableFont-CFF2.ttx b/tests/data/TestVariableFont-CFF2.ttx index cfb50b2e1..c53ebc96a 100644 --- a/tests/data/TestVariableFont-CFF2.ttx +++ b/tests/data/TestVariableFont-CFF2.ttx @@ -319,7 +319,7 @@ - + diff --git a/tests/data/TestVariableFont-TTF-post3.ttx b/tests/data/TestVariableFont-TTF-post3.ttx index 6b5fa50d0..aa21f3ef5 100644 --- a/tests/data/TestVariableFont-TTF-post3.ttx +++ b/tests/data/TestVariableFont-TTF-post3.ttx @@ -308,7 +308,7 @@ - + diff --git a/tests/data/TestVariableFont-TTF-useProductionNames.ttx b/tests/data/TestVariableFont-TTF-useProductionNames.ttx index fff0a018d..57466c5f5 100644 --- a/tests/data/TestVariableFont-TTF-useProductionNames.ttx +++ b/tests/data/TestVariableFont-TTF-useProductionNames.ttx @@ -325,7 +325,7 @@ - + diff --git a/tests/data/TestVariableFont-TTF.ttx b/tests/data/TestVariableFont-TTF.ttx index 63ff72b0c..a7504b6c1 100644 --- a/tests/data/TestVariableFont-TTF.ttx +++ b/tests/data/TestVariableFont-TTF.ttx @@ -322,7 +322,7 @@ - + diff --git a/tests/featureWriters/variableFeatureWriter_test.py b/tests/featureWriters/variableFeatureWriter_test.py new file mode 100644 index 000000000..92092c65e --- /dev/null +++ b/tests/featureWriters/variableFeatureWriter_test.py @@ -0,0 +1,54 @@ +import io +from textwrap import dedent + +from fontTools import designspaceLib + +from ufo2ft import compileVariableTTF + + +def test_variable_features(FontClass): + tmp = io.StringIO() + designspace = designspaceLib.DesignSpaceDocument.fromfile( + "tests/data/TestVarfea.designspace" + ) + designspace.loadSourceFonts(FontClass) + _ = compileVariableTTF(designspace, debugFeatureFile=tmp) + + assert dedent("\n" + tmp.getvalue()) == dedent( + """ + markClass dotabove-ar @MC_top; + + lookup kern_Arab { + lookupflag IgnoreMarks; + pos alef-ar.fina alef-ar.fina <(wght=100:15 wght=1000:35) 0 (wght=100:15 wght=1000:35) 0>; + } kern_Arab; + + feature kern { + script DFLT; + language dflt; + lookup kern_Arab; + + script arab; + language dflt; + lookup kern_Arab; + } kern; + + feature mark { + lookup mark2base { + pos base alef-ar.fina + mark @MC_top; + } mark2base; + + } mark; + + feature curs { + lookup curs { + lookupflag RightToLeft IgnoreMarks; + pos cursive alef-ar.fina ; + pos cursive peh-ar.init ; + pos cursive peh-ar.init.BRACKET.varAlt01 ; + } curs; + + } curs; +""" # noqa: B950 + ) diff --git a/tests/integration_test.py b/tests/integration_test.py index 05c2ba0dd..0eb2a038f 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -4,6 +4,7 @@ import re import sys from pathlib import Path +from textwrap import dedent import pytest from fontTools.pens.boundsPen import BoundsPen @@ -241,9 +242,23 @@ def test_debugFeatureFile(self, designspace): tmp = io.StringIO() _ = compileVariableTTF(designspace, debugFeatureFile=tmp) - - assert "### LayerFont-Regular ###" in tmp.getvalue() - assert "### LayerFont-Bold ###" in tmp.getvalue() + assert "\n" + tmp.getvalue() == dedent( + """ + markClass dotabovecomb @MC_top; + + feature liga { + sub a e s s by s; + } liga; + + feature mark { + lookup mark2base { + pos base e + mark @MC_top; + } mark2base; + + } mark; + """ # noqa: B950 + ) @pytest.mark.parametrize( "output_format, options, expected_ttx", From f997b245304d76f0883b70e15be171320f81ffc4 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 5 Dec 2023 15:06:33 +0000 Subject: [PATCH 06/17] Slightly improve the subclassing --- Lib/ufo2ft/_compilers/baseCompiler.py | 5 +++++ .../_compilers/interpolatableOTFCompiler.py | 17 ++++++++++------- .../_compilers/interpolatableTTFCompiler.py | 5 ----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 050fb030c..7ea296aff 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -175,6 +175,11 @@ class BaseInterpolatableCompiler(BaseCompiler): "maxp", "post" and "vmtx"), and no OpenType layout tables. """ + def compile_designspace(self, designSpaceDoc): + ufos = self._pre_compile_designspace(designSpaceDoc) + ttfs = self.compile(ufos) + return self._post_compile_designspace(designSpaceDoc, ttfs) + def _pre_compile_designspace(self, designSpaceDoc): ufos, self.layerNames = [], [] for source in designSpaceDoc.sources: diff --git a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py index c9c668e18..63f43ea4d 100644 --- a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py @@ -28,10 +28,13 @@ class InterpolatableOTFCompiler(OTFCompiler, BaseInterpolatableCompiler): skipFeatureCompilation: bool = False excludeVariationTables: tuple = () - def compile_designspace(self, designSpaceDoc): - self._pre_compile_designspace(designSpaceDoc) + # We can't use the same compile method as interpolatableTTFCompiler + # because that has a TTFInterpolatablePreProcessor which preprocesses + # all UFOs together, whereas we need to do the preprocessing one at + # at a time. + def compile(self, ufos): otfs = [] - for source in designSpaceDoc.sources: + for ufo, layerName in zip(ufos, self.layerNames): # There's a Python bug where dataclasses.asdict() doesn't work with # dataclasses that contain a defaultdict. save_extraSubstitutions = self.extraSubstitutions @@ -39,11 +42,11 @@ def compile_designspace(self, designSpaceDoc): args = { **dataclasses.asdict(self), **dict( - layerName=source.layerName, + layerName=layerName, removeOverlaps=False, overlapsBackend=None, optimizeCFF=CFFOptimization.NONE, - _tables=SPARSE_OTF_MASTER_TABLES if source.layerName else None, + _tables=SPARSE_OTF_MASTER_TABLES if layerName else None, ), } # Remove interpolatable-specific args @@ -51,8 +54,8 @@ def compile_designspace(self, designSpaceDoc): del args["excludeVariationTables"] compiler = OTFCompiler(**args) self.extraSubstitutions = save_extraSubstitutions - otfs.append(compiler.compile(source.font)) - return self._post_compile_designspace(designSpaceDoc, otfs) + otfs.append(compiler.compile(ufo)) + return otfs def _merge(self, designSpaceDoc, excludeVariationTables): return varLib.build_many( diff --git a/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py index a15e2e07d..74d24080c 100644 --- a/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py @@ -78,11 +78,6 @@ def compileOutlines(self, ufo, glyphSet, layerName=None): outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) return outlineCompiler.compile() - def compile_designspace(self, designSpaceDoc): - ufos = self._pre_compile_designspace(designSpaceDoc) - ttfs = self.compile(ufos) - return self._post_compile_designspace(designSpaceDoc, ttfs) - def _merge(self, designSpaceDoc, excludeVariationTables): return varLib.build_many( designSpaceDoc, From 5122946ba8aeaf48c52bd19b8e387b8b9f3a54a2 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 5 Dec 2023 15:22:17 +0000 Subject: [PATCH 07/17] Stash and return glyphsets, then find the default glyphset when compiling variable features --- Lib/ufo2ft/_compilers/baseCompiler.py | 38 ++++++++++--------- .../_compilers/interpolatableOTFCompiler.py | 1 + .../_compilers/interpolatableTTFCompiler.py | 4 +- Lib/ufo2ft/_compilers/otfCompiler.py | 1 + 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 7ea296aff..473ecec96 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -181,7 +181,7 @@ def compile_designspace(self, designSpaceDoc): return self._post_compile_designspace(designSpaceDoc, ttfs) def _pre_compile_designspace(self, designSpaceDoc): - ufos, self.layerNames = [], [] + ufos, self.glyphSets, self.layerNames = [], [], [] for source in designSpaceDoc.sources: if source.font is None: raise AttributeError( @@ -257,8 +257,6 @@ def _compileNeededSources(self, designSpaceDoc): if source.name in sourcesToCompile: sourcesByName[source.name] = source - originalSources = {} - # If the feature files are compatible between the sources, we can save # time by building a variable feature file right at the end. can_optimize_features = _featuresCompatible(designSpaceDoc) @@ -266,6 +264,7 @@ def _compileNeededSources(self, designSpaceDoc): self.logger.info("Features are compatible across masters; building later") originalSources = {} + originalGlyphsets = {} # Compile all needed sources in each interpolable subspace to make sure # they're all compatible; that also ensures that sub-vfs within the same @@ -294,12 +293,18 @@ def _compileNeededSources(self, designSpaceDoc): self.useProductionNames = save_production_names # Stick TTFs back into original big DS - for ttfSource in ttfDesignSpace.sources: + for ttfSource, glyphSet in zip(ttfDesignSpace.sources, self.glyphSets): if can_optimize_features: originalSources[ttfSource.name] = sourcesByName[ttfSource.name].font sourcesByName[ttfSource.name].font = ttfSource.font + originalGlyphsets[ttfSource.name] = glyphSet - return vfNameToBaseUfo, can_optimize_features, originalSources + return ( + vfNameToBaseUfo, + can_optimize_features, + originalSources, + originalGlyphsets, + ) def compile_variable(self, designSpaceDoc): if not self.inplace: @@ -309,6 +314,7 @@ def compile_variable(self, designSpaceDoc): vfNameToBaseUfo, buildVariableFeatures, originalSources, + originalGlyphsets, ) = self._compileNeededSources(designSpaceDoc) if not vfNameToBaseUfo: @@ -328,7 +334,7 @@ def compile_variable(self, designSpaceDoc): if buildVariableFeatures: self.compile_all_variable_features( - designSpaceDoc, vfNameToTTFont, originalSources + designSpaceDoc, vfNameToTTFont, originalSources, originalGlyphsets ) for vfName, varfont in list(vfNameToTTFont.items()): vfNameToTTFont[vfName] = self.postprocess( @@ -338,7 +344,12 @@ def compile_variable(self, designSpaceDoc): return vfNameToTTFont def compile_all_variable_features( - self, designSpaceDoc, vfNameToTTFont, originalSources, debugFeatureFile=False + self, + designSpaceDoc, + vfNameToTTFont, + originalSources, + originalGlyphsets, + debugFeatureFile=False, ): interpolableSubDocs = [ subDoc for _location, subDoc in splitInterpolable(designSpaceDoc) @@ -352,19 +363,12 @@ def compile_all_variable_features( ufoDoc = vfDoc.deepcopyExceptFonts() for ttfSource, ufoSource in zip(vfDoc.sources, ufoDoc.sources): ufoSource.font = originalSources[ttfSource.name] - self.compile_variable_features(ufoDoc, ttFont) + defaultGlyphset = originalGlyphsets[ufoDoc.findDefault().name] + self.compile_variable_features(ufoDoc, ttFont, defaultGlyphset) - def compile_variable_features(self, designSpaceDoc, ttFont): + def compile_variable_features(self, designSpaceDoc, ttFont, glyphSet): default_ufo = designSpaceDoc.findDefault().font - # Delete anything from the UFO glyphset which didn't make it into the font. - fontglyphs = ttFont.getGlyphOrder() - glyphSet = {g.name: g for g in default_ufo if g.name in fontglyphs} - - # Add anything we added to the TTF without telling the UFO - if ".notdef" not in glyphSet: - glyphSet[".notdef"] = StubGlyph(".notdef", 0, 0, 0, 0) - featureCompiler = VariableFeatureCompiler( default_ufo, designSpaceDoc, ttFont=ttFont, glyphSet=glyphSet ) diff --git a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py index 63f43ea4d..50e9a2fdc 100644 --- a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py @@ -55,6 +55,7 @@ def compile(self, ufos): compiler = OTFCompiler(**args) self.extraSubstitutions = save_extraSubstitutions otfs.append(compiler.compile(ufo)) + self.glyphSets.append(compiler._glyphSet) return otfs def _merge(self, designSpaceDoc, excludeVariationTables): diff --git a/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py index 74d24080c..a5d8b0fb0 100644 --- a/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py @@ -31,9 +31,9 @@ def compile(self, ufos): if self.layerNames is None: self.layerNames = [None] * len(ufos) assert len(ufos) == len(self.layerNames) - glyphSets = self.preprocess(ufos) + self.glyphSets = self.preprocess(ufos) - for ufo, glyphSet, layerName in zip(ufos, glyphSets, self.layerNames): + for ufo, glyphSet, layerName in zip(ufos, self.glyphSets, self.layerNames): yield self.compile_one(ufo, glyphSet, layerName) def compile_one(self, ufo, glyphSet, layerName): diff --git a/Lib/ufo2ft/_compilers/otfCompiler.py b/Lib/ufo2ft/_compilers/otfCompiler.py index 0e3acfb80..6c76bce7d 100644 --- a/Lib/ufo2ft/_compilers/otfCompiler.py +++ b/Lib/ufo2ft/_compilers/otfCompiler.py @@ -33,4 +33,5 @@ def postprocess(self, font, ufo, glyphSet): kwargs = prune_unknown_kwargs(self.__dict__, postProcessor.process) kwargs["optimizeCFF"] = self.optimizeCFF >= CFFOptimization.SUBROUTINIZE font = postProcessor.process(**kwargs) + self._glyphSet = glyphSet return font From 6473bbb9450743fac42ab2c444c9d1a2d1be9216 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 5 Dec 2023 15:44:00 +0000 Subject: [PATCH 08/17] On reflection these not needed --- Lib/ufo2ft/featureWriters/baseFeatureWriter.py | 3 --- Lib/ufo2ft/util.py | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py index 256e044f7..bfc8d2f4e 100644 --- a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py @@ -1,4 +1,3 @@ -import copy import logging from collections import OrderedDict, namedtuple from types import SimpleNamespace @@ -332,8 +331,6 @@ def compileGSUB(self): glyphOrder = compiler.ttFont.getGlyphOrder() fvar = compiler.ttFont.get("fvar") - if fvar: - feafile = copy.deepcopy(feafile) else: # the 'real' glyph order doesn't matter because the table is not # compiled to binary, only the glyph names are used diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py index 348c6b0a2..d034f0bf0 100644 --- a/Lib/ufo2ft/util.py +++ b/Lib/ufo2ft/util.py @@ -251,10 +251,7 @@ def compileGSUB(featureFile, glyphOrder, fvar=None): font.setGlyphOrder(glyphOrder) if fvar: font["fvar"] = fvar - tables = {"GSUB", "GDEF"} - else: - tables = {"GSUB"} - addOpenTypeFeatures(font, featureFile, tables=tables) + addOpenTypeFeatures(font, featureFile, tables={"GSUB"}) return font.get("GSUB") From a591759933e05e770e9f883aa52e40d91c797b9a Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 5 Dec 2023 15:56:07 +0000 Subject: [PATCH 09/17] Use shiny new fonttools API --- Lib/ufo2ft/_compilers/baseCompiler.py | 14 +------------- setup.py | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 473ecec96..ed991ca14 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -379,16 +379,4 @@ def compile_variable_features(self, designSpaceDoc, ttFont, glyphSet): featureCompiler.writeFeatures(self.debugFeatureFile) # Add back feature variations, as the code above would overwrite them. - designSpaceData = varLib.load_designspace(designSpaceDoc) - featureTag = designSpaceData.lib.get( - varLib.FEAVAR_FEATURETAG_LIB_KEY, - "rclt" if designSpaceData.rulesProcessingLast else "rvrn", - ) - if designSpaceData.rules: - varLib._add_GSUB_feature_variations( - ttFont, - designSpaceData.axes, - designSpaceData.internal_axis_supports, - designSpaceData.rules, - featureTag, - ) + varLib.addGSUBFeatureVariations(ttFont, designSpaceDoc) diff --git a/setup.py b/setup.py index fa193fd44..10f976106 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup_requires=pytest_runner + wheel + ["setuptools_scm"], tests_require=["pytest>=2.8"], install_requires=[ - "fonttools[ufo]>=4.44.3", + "fonttools[ufo]>=4.46.0", "cffsubr>=0.2.8", "booleanOperations>=0.9.0", ], From 30b6be6531bd360b395e4f3019f5ef954722b607 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 7 Dec 2023 11:10:30 +0000 Subject: [PATCH 10/17] otRound the position when building a variable anchor --- Lib/ufo2ft/featureWriters/baseFeatureWriter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py index bfc8d2f4e..32a15e4b4 100644 --- a/Lib/ufo2ft/featureWriters/baseFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/baseFeatureWriter.py @@ -4,6 +4,7 @@ from fontTools.designspaceLib import DesignSpaceDocument from fontTools.feaLib.variableScalar import VariableScalar +from fontTools.misc.fixedTools import otRound from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY from ufo2ft.errors import InvalidFeaturesData @@ -462,8 +463,8 @@ def _getAnchor(self, glyphName, anchorName, anchor=None): for anchor in glyph.anchors: if anchor.name == anchorName: location = get_userspace_location(designspace, source.location) - x_value.add_value(location, anchor.x) - y_value.add_value(location, anchor.y) + x_value.add_value(location, otRound(anchor.x)) + y_value.add_value(location, otRound(anchor.y)) found = True if not found: return None From 809324f4a42104443db8d84674cb1ee224378820 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 7 Dec 2023 11:36:44 +0000 Subject: [PATCH 11/17] Use getAnchor to get variable ligcarets --- Lib/ufo2ft/featureWriters/gdefFeatureWriter.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py b/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py index 14527590d..fcd41f19d 100644 --- a/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py +++ b/Lib/ufo2ft/featureWriters/gdefFeatureWriter.py @@ -1,3 +1,5 @@ +from fontTools.misc.fixedTools import otRound + from ufo2ft.featureWriters import BaseFeatureWriter, ast @@ -55,16 +57,21 @@ def _getLigatureCarets(self): and anchor.name.startswith("caret_") and anchor.x is not None ): - glyphCarets.add(round(anchor.x)) + glyphCarets.add(self._getAnchor(glyphName, anchor.name)[0]) elif ( anchor.name and anchor.name.startswith("vcaret_") and anchor.y is not None ): - glyphCarets.add(round(anchor.y)) + glyphCarets.add(self._getAnchor(glyphName, anchor.name)[1]) if glyphCarets: - carets[glyphName] = sorted(glyphCarets) + if self.context.isVariable: + carets[glyphName] = sorted( + glyphCarets, key=lambda caret: list(caret.values.values())[0] + ) + else: + carets[glyphName] = [otRound(c) for c in sorted(glyphCarets)] return carets From 1f3f179f61fd272dfff848e8348324ad25b61edc Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 7 Dec 2023 13:39:04 +0000 Subject: [PATCH 12/17] Log progress --- Lib/ufo2ft/_compilers/baseCompiler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index ed991ca14..ff2d77a91 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -364,6 +364,7 @@ def compile_all_variable_features( for ttfSource, ufoSource in zip(vfDoc.sources, ufoDoc.sources): ufoSource.font = originalSources[ttfSource.name] defaultGlyphset = originalGlyphsets[ufoDoc.findDefault().name] + self.logger.info(f"Compiling variable features for {vfName}") self.compile_variable_features(ufoDoc, ttFont, defaultGlyphset) def compile_variable_features(self, designSpaceDoc, ttFont, glyphSet): From 91e76cdebfd867d5ac9c8ae4c4b98b0796045725 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 10 Jan 2024 11:48:47 +0000 Subject: [PATCH 13/17] remove unused parameter --- Lib/ufo2ft/_compilers/variableTTFsCompiler.py | 1 - Lib/ufo2ft/featureCompiler.py | 4 +--- Lib/ufo2ft/featureWriters/__init__.py | 6 +----- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Lib/ufo2ft/_compilers/variableTTFsCompiler.py b/Lib/ufo2ft/_compilers/variableTTFsCompiler.py index 50ca7ac9f..77d7fbfa5 100644 --- a/Lib/ufo2ft/_compilers/variableTTFsCompiler.py +++ b/Lib/ufo2ft/_compilers/variableTTFsCompiler.py @@ -21,4 +21,3 @@ class VariableTTFsCompiler(InterpolatableTTFCompiler): autoUseMyMetrics: bool = True dropImpliedOnCurves: bool = False allQuadratic: bool = True - pass diff --git a/Lib/ufo2ft/featureCompiler.py b/Lib/ufo2ft/featureCompiler.py index 0d20e90fd..e1c6bc982 100644 --- a/Lib/ufo2ft/featureCompiler.py +++ b/Lib/ufo2ft/featureCompiler.py @@ -246,9 +246,7 @@ def _load_custom_feature_writers(self, featureWriters=None): if writer is ...: if seen_ellipsis: raise ValueError("ellipsis not allowed more than once") - writers = loadFeatureWriters( - self.ufo, variable=hasattr(self, "designspace") - ) + writers = loadFeatureWriters(self.ufo) if writers is not None: result.extend(writers) else: diff --git a/Lib/ufo2ft/featureWriters/__init__.py b/Lib/ufo2ft/featureWriters/__init__.py index c04b48da2..ed9706c3b 100644 --- a/Lib/ufo2ft/featureWriters/__init__.py +++ b/Lib/ufo2ft/featureWriters/__init__.py @@ -48,7 +48,7 @@ def write(self, font, feaFile, compiler=None) return True -def loadFeatureWriters(ufo, ignoreErrors=True, variable=False): +def loadFeatureWriters(ufo, ignoreErrors=True): """Check UFO lib for key "com.github.googlei18n.ufo2ft.featureWriters", containing a list of dicts, each having the following key/value pairs: For example: @@ -63,10 +63,6 @@ def loadFeatureWriters(ufo, ignoreErrors=True, variable=False): the built-in ufo2ft.featureWriters), and instantiate it with the given 'options' dict. - If ``variable`` is true, then the feature writer class is asked if it - has an associated class which works on Designspace files instead of UFOs, - and if so, then this is used instead. - Return the list of feature writer objects. If the 'featureWriters' key is missing from the UFO lib, return None. From 52ca46a4adaaf11c6cb95f0254c29f6d000a19e5 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 12:21:47 +0000 Subject: [PATCH 14/17] add compile option to force variableFeatures=False this can be useful to debugging, comparing build time of new vs old build pipeline, or if one wants to ensure no binary changes occur when updating the compiler --- Lib/ufo2ft/_compilers/baseCompiler.py | 4 +++- Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py | 4 ++-- Lib/ufo2ft/_compilers/variableCFF2sCompiler.py | 1 + Lib/ufo2ft/_compilers/variableTTFsCompiler.py | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index ff2d77a91..21cd8cb97 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -259,7 +259,9 @@ def _compileNeededSources(self, designSpaceDoc): # If the feature files are compatible between the sources, we can save # time by building a variable feature file right at the end. - can_optimize_features = _featuresCompatible(designSpaceDoc) + can_optimize_features = self.variableFeatures and _featuresCompatible( + designSpaceDoc + ) if can_optimize_features: self.logger.info("Features are compatible across masters; building later") diff --git a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py index 50e9a2fdc..9ebdde9d3 100644 --- a/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py +++ b/Lib/ufo2ft/_compilers/interpolatableOTFCompiler.py @@ -7,6 +7,7 @@ from ufo2ft.constants import SPARSE_OTF_MASTER_TABLES, CFFOptimization from ufo2ft.outlineCompiler import OutlineOTFCompiler from ufo2ft.preProcessor import OTFPreProcessor +from ufo2ft.util import prune_unknown_kwargs from .baseCompiler import BaseInterpolatableCompiler from .otfCompiler import OTFCompiler @@ -50,8 +51,7 @@ def compile(self, ufos): ), } # Remove interpolatable-specific args - del args["variableFontNames"] - del args["excludeVariationTables"] + args = prune_unknown_kwargs(args, OTFCompiler) compiler = OTFCompiler(**args) self.extraSubstitutions = save_extraSubstitutions otfs.append(compiler.compile(ufo)) diff --git a/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py b/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py index 83c8044e3..b79cb543c 100644 --- a/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py +++ b/Lib/ufo2ft/_compilers/variableCFF2sCompiler.py @@ -17,3 +17,4 @@ class VariableCFF2sCompiler(InterpolatableOTFCompiler): cffVersion: int = 2 optimizeCFF: CFFOptimization = CFFOptimization.SPECIALIZE excludeVariationTables: tuple = () + variableFeatures: bool = True diff --git a/Lib/ufo2ft/_compilers/variableTTFsCompiler.py b/Lib/ufo2ft/_compilers/variableTTFsCompiler.py index 77d7fbfa5..e9e75b7cb 100644 --- a/Lib/ufo2ft/_compilers/variableTTFsCompiler.py +++ b/Lib/ufo2ft/_compilers/variableTTFsCompiler.py @@ -21,3 +21,4 @@ class VariableTTFsCompiler(InterpolatableTTFCompiler): autoUseMyMetrics: bool = True dropImpliedOnCurves: bool = False allQuadratic: bool = True + variableFeatures: bool = True From 207f247043b69a8252ac054412bd29c332fbee1c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 12:34:09 +0000 Subject: [PATCH 15/17] be more generic in log message emitted for either TTF or CFF2 VFs --- Lib/ufo2ft/_compilers/baseCompiler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 21cd8cb97..e28cd42ca 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -322,7 +322,12 @@ def compile_variable(self, designSpaceDoc): if not vfNameToBaseUfo: return {} - self.logger.info("Building variable TTF fonts: %s", ", ".join(vfNameToBaseUfo)) + vfNames = list(vfNameToBaseUfo.keys()) + self.logger.info( + "Building variable font%s: %s", + "s" if len(vfNames) > 1 else "", + ", ".join(vfNames), + ) excludeVariationTables = self.excludeVariationTables if buildVariableFeatures: From a37580645cd3aed7c8202c96ac54cdd03330f78e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 13:35:45 +0000 Subject: [PATCH 16/17] remove unused parameter --- Lib/ufo2ft/_compilers/ttfCompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ufo2ft/_compilers/ttfCompiler.py b/Lib/ufo2ft/_compilers/ttfCompiler.py index a5c32f4c7..05bd2653f 100644 --- a/Lib/ufo2ft/_compilers/ttfCompiler.py +++ b/Lib/ufo2ft/_compilers/ttfCompiler.py @@ -21,7 +21,7 @@ class TTFCompiler(BaseCompiler): dropImpliedOnCurves: bool = False allQuadratic: bool = True - def compileOutlines(self, ufo, glyphSet, layerName=None): + def compileOutlines(self, ufo, glyphSet): kwargs = prune_unknown_kwargs(self.__dict__, self.outlineCompilerClass) kwargs["glyphDataFormat"] = 0 if self.allQuadratic else 1 outlineCompiler = self.outlineCompilerClass(ufo, glyphSet=glyphSet, **kwargs) From a782bbfdbb0a115e6b8bd583f22bfe16ee9fdace Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 15:03:46 +0000 Subject: [PATCH 17/17] restore log message that got lost in #801 refactoring --- Lib/ufo2ft/_compilers/baseCompiler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index e28cd42ca..4175cd0fc 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -57,6 +57,7 @@ def compile(self, ufo): with self.timer("preprocess UFO"): glyphSet = self.preprocess(ufo) with self.timer("compile a basic TTF"): + self.logger.info("Building OpenType tables") font = self.compileOutlines(ufo, glyphSet) if self.layerName is None and not self.skipFeatureCompilation: self.compileFeatures(ufo, font, glyphSet=glyphSet)