[cantarell-fonts/respacing] Update Instantiator to version from fontmake 2.0.0b1
- From: Nikolaus Waxweiler <nwaxweiler src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [cantarell-fonts/respacing] Update Instantiator to version from fontmake 2.0.0b1
- Date: Sun, 11 Aug 2019 18:54:51 +0000 (UTC)
commit de9342e76e481c4981da287a616de6c7f5565877
Author: Nikolaus Waxweiler <madigens gmail com>
Date: Sun Aug 11 19:54:01 2019 +0100
Update Instantiator to version from fontmake 2.0.0b1
scripts/instantiator.py | 544 +++++++++++++++++++++++++++++++-----------------
1 file changed, 349 insertions(+), 195 deletions(-)
---
diff --git a/scripts/instantiator.py b/scripts/instantiator.py
index 73732273..309bb891 100644
--- a/scripts/instantiator.py
+++ b/scripts/instantiator.py
@@ -1,7 +1,24 @@
-#!/bin/env python3
+# This code is based on ufoProcessor code, which is licensed as follows:
+# Copyright (c) 2017-2018 LettError and Erik van Blokland
+# All rights reserved.
#
-# This code is based on ufoProcessor code, see LICENSE_ufoProcessor, and is itself
-# MIT-licensed.
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
"""Module for generating static font instances.
@@ -12,11 +29,11 @@ instance computation directly and exclusively.
At the time of this writing, varLib lacks support for anisotropic (x, y) locations and
extrapolation.
-
-See the `scripts/make-static-fonts.py` script for how to use it.
"""
-from pathlib import Path
+import copy
+import logging
+import typing
from typing import Any, Dict, List, Mapping, Set, Tuple, Union
import attr
@@ -27,13 +44,25 @@ import fontTools.ufoLib as ufoLib
import fontTools.varLib as varLib
import ufoLib2
-FontMathObject = Union[fontMath.MathGlyph, fontMath.MathInfo, fontMath.MathKerning]
-Location = Mapping[str, float]
+logger = logging.getLogger(__name__)
# Use the same rounding function used by varLib to round things for the variable font
# to reduce differences between the variable and static instances.
fontMath.mathFunctions.setRoundIntegerFunction(fontTools.misc.fixedTools.otRound)
+# Stand-in type for any of the fontMath classes we use.
+FontMathObject = Union[fontMath.MathGlyph, fontMath.MathInfo, fontMath.MathKerning]
+
+# MutatorMath-style location mapping type, i.e.
+# `{"wght": 1.0, "wdth": 0.0, "bleep": 0.5}`.
+# LocationKey is a Location turned into a tuple so we can use it as a dict key.
+Location = Mapping[str, float]
+LocationKey = Tuple[Tuple[str, float], ...]
+
+# Type of mapping of axes to their minimum, default and maximum values, i.e.
+# `{"wght": (100.0, 400.0, 900.0), "wdth": (75.0, 100.0, 100.0)}`.
+AxisBounds = Dict[str, Tuple[float, float, float]]
+
# For mapping `wdth` axis user values to the OS2 table's width class field.
WDTH_VALUE_TO_OS2_WIDTH_CLASS = {
50: 1,
@@ -48,16 +77,22 @@ WDTH_VALUE_TO_OS2_WIDTH_CLASS = {
}
+# Custom exception for this module
+class InstantiatorError(Exception):
+ pass
+
+
@attr.s(auto_attribs=True, frozen=True, slots=True)
class Instantiator:
"""Data class that holds all necessary information to generate a static
font instance object at an arbitary location within the design space."""
- axis_bounds: Dict[str, Tuple[float, float, float]] # Design space!
+ axis_bounds: AxisBounds # Design space!
copy_feature_text: str
copy_groups: Mapping[str, List[str]]
copy_info: ufoLib2.objects.Info
copy_lib: Mapping[str, Any]
+ default_design_location: Location
designspace_rules: List[designspaceLib.RuleDescriptor]
glyph_mutators: Mapping[str, "Variator"]
glyph_name_to_unicodes: Dict[str, List[int]]
@@ -65,7 +100,7 @@ class Instantiator:
kerning_mutator: "Variator"
round_geometry: bool
skip_export_glyphs: List[str]
- weight_width_axes: Mapping[str, designspaceLib.AxisDescriptor]
+ special_axes: Mapping[str, designspaceLib.AxisDescriptor]
@classmethod
def from_designspace(
@@ -75,10 +110,16 @@ class Instantiator:
):
"""Instantiates a new data class from a Designspace object."""
if designspace.default is None:
- raise ValueError(
+ raise InstantiatorError(
"Can't generate UFOs from this designspace: no default font."
)
+ if any(anisotropic(instance.location) for instance in designspace.instances):
+ raise InstantiatorError(
+ "The Designspace contains anisotropic instance locations, which are "
+ "not supported by varLib."
+ )
+
designspace.loadSourceFonts(ufoLib2.Font.open)
glyph_names: Set[str] = set()
@@ -86,9 +127,9 @@ class Instantiator:
glyph_names.update(source.font.keys())
# Construct Variators
- axis_bounds: Dict[str, Tuple[float, float, float]] = {} # Design space!
+ axis_bounds: AxisBounds = {} # Design space!
axis_order: List[str] = []
- weight_width_axes = {}
+ special_axes = {}
for axis in designspace.axes:
axis_order.append(axis.name)
axis_bounds[axis.name] = (
@@ -96,8 +137,9 @@ class Instantiator:
axis.map_forward(axis.default),
axis.map_forward(axis.maximum),
)
- if axis.tag in ("wght", "wdth"):
- weight_width_axes[axis.tag] = axis
+ # Some axes relate to existing OpenType fields and get special attention.
+ if axis.tag in {"wght", "wdth", "slnt"}:
+ special_axes[axis.tag] = axis
masters_info = collect_info_masters(designspace, axis_bounds)
info_mutator = Variator.from_masters(masters_info, axis_order)
@@ -119,7 +161,7 @@ class Instantiator:
copy_info: ufoLib2.objects.Info = default_font.info
copy_lib: Mapping[str, Any] = default_font.lib
- # The list of glyphs not to export and decompose where used as a component is
+ # The list of glyphs-not-to-export-and-decompose-where-used-as-a-component is
# supposed to be taken from the Designspace when a Designspace is used as the
# starting point of the compilation process. It should be exported to all
# instance libs, where the ufo2ft compilation functions will pick it up.
@@ -131,6 +173,7 @@ class Instantiator:
copy_groups,
copy_info,
copy_lib,
+ designspace.default.location,
designspace.rules,
glyph_mutators,
glyph_name_to_unicodes,
@@ -138,47 +181,126 @@ class Instantiator:
kerning_mutator,
round_geometry,
skip_export_glyphs,
- weight_width_axes,
+ special_axes,
)
def generate_instance(
self, instance: designspaceLib.InstanceDescriptor
) -> ufoLib2.Font:
- """Generate a font object for an InstanceDescriptor."""
- font = ufoLib2.Font()
-
- location = instance.location
- if anisotropic(location):
- raise ValueError(
+ """Generate an interpolated instance font object for an
+ InstanceDescriptor."""
+ if anisotropic(instance.location):
+ raise InstantiatorError(
f"Instance {instance.familyName}-"
f"{instance.styleName}: Anisotropic location "
f"{instance.location} not supported by varLib."
)
- location_normalized = normalize_design_location(location, self.axis_bounds)
+
+ font = ufoLib2.Font()
+
+ # Instances may leave out locations that match the default source, so merge
+ # default location with the instance's location.
+ location = {**self.default_design_location, **instance.location}
+ location_normalized = varLib.models.normalizeLocation(
+ location, self.axis_bounds
+ )
# Kerning
- if instance.kerning:
- kerning_instance = self.kerning_mutator.instance_at(location_normalized)
- kerning_instance.extractKerning(font)
+ kerning_instance = self.kerning_mutator.instance_at(location_normalized)
+ if self.round_geometry:
+ kerning_instance.round()
+ kerning_instance.extractKerning(font)
# Info
+ self._generate_instance_info(instance, location_normalized, location, font)
+
+ # Groups
+ for key, glyph_names in self.copy_groups.items():
+ font.groups[key] = [name for name in glyph_names]
+
+ # Features
+ font.features.text = self.copy_feature_text
+
+ # Lib
+ # 1. Copy the default lib to the instance.
+ font.lib = typing.cast(dict, copy.deepcopy(self.copy_lib))
+ # 2. Copy the Designspace's skipExportGlyphs list over to the UFO to
+ # make sure it wins over the default UFO one.
+ font.lib["public.skipExportGlyphs"] = [name for name in self.skip_export_glyphs]
+ # 3. Write _design_ location to instance's lib.
+ font.lib["designspace.location"] = [loc for loc in location.items()]
+
+ # Glyphs
+ for glyph_name, glyph_mutator in self.glyph_mutators.items():
+ glyph = font.newGlyph(glyph_name)
+
+ try:
+ glyph_instance = glyph_mutator.instance_at(location_normalized)
+
+ if self.round_geometry:
+ glyph_instance = glyph_instance.round()
+
+ # onlyGeometry=True does not set name and unicodes, in ufoLib2 we can't
+ # modify a glyph's name. Copy unicodes from default font.
+ glyph_instance.extractGlyph(glyph, onlyGeometry=True)
+ except Exception as e:
+ # TODO: Figure out what exceptions fontMath/varLib can throw.
+ # By default, explode if we cannot generate a glyph instance for
+ # whatever reason (usually outline incompatibility)...
+ if glyph_name not in self.skip_export_glyphs:
+ raise InstantiatorError(
+ f"Failed to generate instance of glyph '{glyph_name}'."
+ ) from e
+
+ # ...except if the glyph is in public.skipExportGlyphs and would
+ # therefore be removed from the compiled font anyway. There's not much
+ # we can do except leave it empty in the instance and tell the user.
+ logger.warning(
+ "Failed to generate instance of glyph '%s', which is marked as "
+ "non-exportable. Glyph will be left empty. Failure reason: %s",
+ glyph_name,
+ e,
+ )
+
+ glyph.unicodes = [uv for uv in self.glyph_name_to_unicodes[glyph_name]]
+
+ # Process rules
+ glyph_names_list = self.glyph_mutators.keys()
+ glyph_names_list_renamed = designspaceLib.processRules(
+ self.designspace_rules, location, glyph_names_list
+ )
+ for name_old, name_new in zip(glyph_names_list, glyph_names_list_renamed):
+ if name_old != name_new:
+ swap_glyph_names(font, name_old, name_new)
+
+ return font
+
+ def _generate_instance_info(
+ self,
+ instance: designspaceLib.InstanceDescriptor,
+ location_normalized: Location,
+ location: Location,
+ font: ufoLib2.Font,
+ ) -> None:
+ """Generate fontinfo related attributes.
+
+ Separate, as fontinfo treatment is more extensive than the rest.
+ """
info_instance = self.info_mutator.instance_at(location_normalized)
if self.round_geometry:
info_instance = info_instance.round()
info_instance.extractInfo(font.info)
- # Copy metadata from sources marked with `<copy info="1">` etc.
+ # Copy non-interpolating metadata from the default font.
for attribute in ufoLib.fontInfoAttributesVersion3:
if hasattr(info_instance, attribute):
- continue # Skip mutated attributes.
+ continue # Skip interpolated info attributes.
if hasattr(self.copy_info, attribute):
- setattr(font.info, attribute, getattr(self.copy_info, attribute))
- for key, value in self.copy_lib.items():
- font.lib[key] = value
- font.lib["public.skipExportGlyphs"] = self.skip_export_glyphs
- for key, value in self.copy_groups.items():
- font.groups[key] = value
- font.features.text = self.copy_feature_text
+ setattr(
+ font.info,
+ attribute,
+ copy.deepcopy(getattr(self.copy_info, attribute)),
+ )
# TODO: multilingual names to replace possibly existing name records.
if instance.familyName:
@@ -194,78 +316,49 @@ class Instantiator:
# If the masters haven't set the OS/2 weight and width class, use the
# user-space values ("input") of the axis mapping in the Designspace file for
- # weight and width axes, if they exist.
- if info_instance.openTypeOS2WeightClass is None:
- if "wght" in self.weight_width_axes:
- weight_axis = self.weight_width_axes["wght"]
- weight_axis_instance_location = instance.location[weight_axis.name]
- font.info.openTypeOS2WeightClass = fontTools.misc.fixedTools.otRound(
- weight_axis.map_backward(weight_axis_instance_location)
- )
- if info_instance.openTypeOS2WidthClass is None:
- if "wdth" in self.weight_width_axes:
- width_axis = self.weight_width_axes["wdth"]
- width_axis_instance_location = instance.location[width_axis.name]
- width_value = width_axis.map_backward(width_axis_instance_location)
- width_class = int(
- varLib.models.piecewiseLinearMap(
- width_value, WDTH_VALUE_TO_OS2_WIDTH_CLASS
- )
- )
- font.info.openTypeOS2WidthClass = width_class
-
- # Glyphs
- for glyph_name, glyph_mutator in self.glyph_mutators.items():
- glyph = font.newGlyph(glyph_name)
-
- glyph_instance = glyph_mutator.instance_at(location_normalized)
- if self.round_geometry:
- glyph_instance = glyph_instance.round()
-
- # onlyGeometry=True does not set name and unicodes, in ufoLib2 we can't
- # modify a glyph's name. Copy unicodes from default font.
- glyph_instance.extractGlyph(glyph, onlyGeometry=True)
- glyph.unicodes = self.glyph_name_to_unicodes[glyph_name]
-
- # Process rules
- glyph_names_list = self.glyph_mutators.keys()
- resultNames = designspaceLib.processRules(
- self.designspace_rules, location, glyph_names_list
- )
- for oldName, newName in zip(glyph_names_list, resultNames):
- if oldName != newName:
- swapGlyphNames(font, oldName, newName)
+ # weight and width axes, if they exist. The slnt axis' value maps 1:1 to
+ # italicAngle. Clamp the values to the valid ranges.
+ if info_instance.openTypeOS2WeightClass is None and "wght" in self.special_axes:
+ weight_axis = self.special_axes["wght"]
+ font.info.openTypeOS2WeightClass = weight_class_from_wght_value(
+ weight_axis.map_backward(location[weight_axis.name])
+ )
+ if info_instance.openTypeOS2WidthClass is None and "wdth" in self.special_axes:
+ width_axis = self.special_axes["wdth"]
+ font.info.openTypeOS2WidthClass = width_class_from_wdth_value(
+ width_axis.map_backward(location[width_axis.name])
+ )
+ if info_instance.italicAngle is None and "slnt" in self.special_axes:
+ slant_axis = self.special_axes["slnt"]
+ font.info.italicAngle = italic_angle_from_slnt_value(
+ slant_axis.map_backward(location[slant_axis.name])
+ )
- font.lib["designspace.location"] = list(instance.location.items())
- return font
+def location_to_key(location: Location) -> LocationKey:
+ """Converts a Location into a sorted tuple so it can be used as a dict
+ key."""
+ return tuple(sorted(location.items()))
def anisotropic(location: Location) -> bool:
- for v in location.values():
- if isinstance(v, tuple):
- return True
- return False
-
-
-def normalize_design_location(
- design_space_location: Location,
- design_space_axis_bounds: Dict[str, Tuple[float, float, float]],
-) -> Location:
- return varLib.models.normalizeLocation(
- design_space_location, design_space_axis_bounds
- )
+ """Tests if any single location value is a MutatorMath-style anisotropic
+ value, i.e. is a tuple of (x, y)."""
+ return any(isinstance(v, tuple) for v in location.values())
def collect_info_masters(
- designspace, axis_bounds
+ designspace: designspaceLib.DesignSpaceDocument, axis_bounds: AxisBounds
) -> List[Tuple[Location, FontMathObject]]:
"""Return master Info objects wrapped by MathInfo."""
locations_and_masters = []
for source in designspace.sources:
if source.layerName is not None:
- continue
- normalized_location = normalize_design_location(source.location, axis_bounds)
+ continue # No font info in source layers.
+
+ normalized_location = varLib.models.normalizeLocation(
+ source.location, axis_bounds
+ )
locations_and_masters.append(
(normalized_location, fontMath.MathInfo(source.font.info))
)
@@ -274,142 +367,183 @@ def collect_info_masters(
def collect_kerning_masters(
- designspace, axis_bounds
+ designspace: designspaceLib.DesignSpaceDocument, axis_bounds: AxisBounds
) -> List[Tuple[Location, FontMathObject]]:
"""Return master kerning objects wrapped by MathKerning."""
locations_and_masters = []
for source in designspace.sources:
if source.layerName is not None:
continue # No kerning in source layers.
- if not source.muteKerning:
- # This assumes that groups of all sources are the same.
- normalized_location = normalize_design_location(
- source.location, axis_bounds
- )
- locations_and_masters.append(
- (
- normalized_location,
- fontMath.MathKerning(source.font.kerning, source.font.groups),
- )
+
+ # This assumes that groups of all sources are the same.
+ normalized_location = varLib.models.normalizeLocation(
+ source.location, axis_bounds
+ )
+ locations_and_masters.append(
+ (
+ normalized_location,
+ fontMath.MathKerning(source.font.kerning, source.font.groups),
)
+ )
return locations_and_masters
def collect_glyph_masters(
- designspace, glyph_name, axis_bounds
+ designspace: designspaceLib.DesignSpaceDocument,
+ glyph_name: str,
+ axis_bounds: AxisBounds,
) -> List[Tuple[Location, FontMathObject]]:
- """Return master glyph objects for glyph_name wrapped by MathGlyph."""
+ """Return master glyph objects for glyph_name wrapped by MathGlyph.
+
+ Note: skips empty source glyphs if the default glyph is not empty to almost match
+ what ufoProcessor is doing. In e.g. Mutator Sans, the 'S.closed' glyph is left
+ empty in one source layer. One could treat this as a source error, but ufoProcessor
+ specifically has code to skip that empty glyph and carry on.
+ """
locations_and_masters = []
- for source in designspace.sources:
- if glyph_name in source.mutedGlyphNames:
- continue
+ default_glyph_empty = False
+ other_glyph_empty = False
- if source.layerName is None:
- # Source font.
+ for source in designspace.sources:
+ if source.layerName is None: # Source font.
source_layer = source.font.layers.defaultLayer
- else:
- # Source layer.
+ else: # Source layer.
source_layer = source.font.layers[source.layerName]
- if glyph_name not in source_layer:
- # Sparse source layer, skip for this glyph.
- continue
+ # Sparse fonts do not and layers may not contain every glyph.
if glyph_name not in source_layer:
continue
source_glyph = source_layer[glyph_name]
- # XXX: What to do about a glyphs lib key? Any useful data that should be
- # copied from master to all instances? `public.verticalOrigin`, does it
- # interpolate? `public.postscript.hints`?
- source_glyph.lib.clear()
+ if not (source_glyph.contours or source_glyph.components):
+ if source is designspace.findDefault():
+ default_glyph_empty = True
+ else:
+ other_glyph_empty = True
- normalized_location = normalize_design_location(source.location, axis_bounds)
+ normalized_location = varLib.models.normalizeLocation(
+ source.location, axis_bounds
+ )
locations_and_masters.append(
(normalized_location, fontMath.MathGlyph(source_glyph))
)
+ # Filter out empty glyphs if the default glyph is not empty.
+ if not default_glyph_empty and other_glyph_empty:
+ locations_and_masters = [
+ (l, m) for l, m in locations_and_masters if m.contours or m.components
+ ]
+
return locations_and_masters
-def swapGlyphNames(font, oldName, newName, swapNameExtension="_______________swap"):
- # In font swap the glyphs oldName and newName.
- # Also swap the names in components in order to preserve appearance.
- # Also swap the names in font groups.
- if oldName not in font or newName not in font:
- return
- swapName = oldName + swapNameExtension
- # park the old glyph
- if not swapName in font:
- font.newGlyph(swapName)
- # swap the outlines
- font[swapName].clear()
- p = font[swapName].getPointPen()
- font[oldName].drawPoints(p)
- font[swapName].width = font[oldName].width
- # lib?
- font[oldName].clear()
- p = font[oldName].getPointPen()
- font[newName].drawPoints(p)
- font[oldName].width = font[newName].width
-
- font[newName].clear()
- p = font[newName].getPointPen()
- font[swapName].drawPoints(p)
- font[newName].width = font[swapName].width
-
- # remap the components
- for g in font:
- for c in g.components:
- if c.baseGlyph == oldName:
- c.baseGlyph = swapName
- continue
- for g in font:
- for c in g.components:
- if c.baseGlyph == newName:
- c.baseGlyph = oldName
- continue
+def width_class_from_wdth_value(wdth_user_value) -> int:
+ """Return the OS/2 width class from the wdth axis user value.
+
+ The OpenType 1.8.3 specification states:
+
+ When mapping from 'wdth' values to usWidthClass, interpolate fractional
+ values between the mapped values and then round, and clamp to the range
+ 1 to 9.
+
+ "Mapped values" probably means the in-percent numbers layed out for the OS/2
+ width class, so we are forcing these numerical semantics on the user values
+ of the wdth axis.
+ """
+ width_user_value = min(max(wdth_user_value, 50), 200)
+ width_user_value_mapped = varLib.models.piecewiseLinearMap(
+ width_user_value, WDTH_VALUE_TO_OS2_WIDTH_CLASS
+ )
+ return fontTools.misc.fixedTools.otRound(width_user_value_mapped)
+
+
+def weight_class_from_wght_value(wght_user_value) -> int:
+ """Return the OS/2 weight class from the wght axis user value."""
+ weight_user_value = min(max(wght_user_value, 1), 1000)
+ return fontTools.misc.fixedTools.otRound(weight_user_value)
+
+
+def italic_angle_from_slnt_value(slnt_user_value) -> Union[int, float]:
+ """Return the italic angle from the slnt axis user value."""
+ slant_user_value = min(max(slnt_user_value, -90), 90)
+ return slant_user_value
+
+
+def swap_glyph_names(font: ufoLib2.Font, name_old: str, name_new: str):
+ """Swap two existing glyphs in the default layer of a font (outlines,
+ width, component references, kerning references, group membership).
+
+ The idea behind swapping instead of overwriting is explained in
+ https://github.com/fonttools/fonttools/tree/master/Doc/source/designspaceLib#ufo-instances.
+ We need to keep the old glyph around in case any other glyph references
+ it; glyphs that are not explicitly substituted by rules should not be
+ affected by the rule application.
+
+ The .unicodes are not swapped. The rules mechanism is supposed to swap
+ glyphs, not characters.
+ """
+
+ if name_old not in font or name_new not in font:
+ raise InstantiatorError(
+ f"Cannot swap glyphs '{name_old}' and '{name_new}', as either or both are "
+ "missing."
+ )
+
+ # 1. Swap outlines and glyph width. Ignore lib content and other properties.
+ glyph_swap = ufoLib2.objects.Glyph(name="temporary_swap_glyph")
+ glyph_old = font[name_old]
+ glyph_new = font[name_new]
+
+ p = glyph_swap.getPointPen()
+ glyph_old.drawPoints(p)
+ glyph_swap.width = glyph_old.width
+
+ glyph_old.clear()
+ p = glyph_old.getPointPen()
+ glyph_new.drawPoints(p)
+ glyph_old.width = glyph_new.width
+
+ glyph_new.clear()
+ p = glyph_new.getPointPen()
+ glyph_swap.drawPoints(p)
+ glyph_new.width = glyph_swap.width
+
+ # 2. Remap components.
for g in font:
for c in g.components:
- if c.baseGlyph == swapName:
- c.baseGlyph = newName
+ if c.baseGlyph == name_old:
+ c.baseGlyph = name_new
+ elif c.baseGlyph == name_new:
+ c.baseGlyph = name_old
- # change the names in groups
- # the shapes will swap, that will invalidate the kerning
- # so the names need to swap in the kerning as well.
- newKerning = {}
+ # 3. Swap literal names in kerning.
+ kerning_new = {}
for first, second in font.kerning.keys():
value = font.kerning[(first, second)]
- if first == oldName:
- first = newName
- elif first == newName:
- first = oldName
- if second == oldName:
- second = newName
- elif second == newName:
- second = oldName
- newKerning[(first, second)] = value
- font.kerning.clear()
- font.kerning.update(newKerning)
-
- for groupName, members in font.groups.items():
- newMembers = []
- for name in members:
- if name == oldName:
- newMembers.append(newName)
- elif name == newName:
- newMembers.append(oldName)
+ if first == name_old:
+ first = name_new
+ elif first == name_new:
+ first = name_old
+ if second == name_old:
+ second = name_new
+ elif second == name_new:
+ second = name_old
+ kerning_new[(first, second)] = value
+ font.kerning = kerning_new
+
+ # 4. Swap names in groups.
+ for group_name, group_members in font.groups.items():
+ group_members_new = []
+ for name in group_members:
+ if name == name_old:
+ group_members_new.append(name_new)
+ elif name == name_new:
+ group_members_new.append(name_old)
else:
- newMembers.append(name)
- font.groups[groupName] = newMembers
-
- remove = []
- for g in font:
- if g.name.find(swapNameExtension) != -1:
- remove.append(g.name)
- for r in remove:
- del font[r]
+ group_members_new.append(name)
+ font.groups[group_name] = group_members_new
@attr.s(auto_attribs=True, frozen=True, slots=True)
@@ -424,6 +558,7 @@ class Variator:
"""
masters: List[FontMathObject]
+ location_to_master: Mapping[LocationKey, FontMathObject]
model: varLib.models.VariationModel
@classmethod
@@ -432,12 +567,31 @@ class Variator:
):
masters = []
master_locations = []
+ location_to_master = {}
for normalized_location, master in items:
master_locations.append(normalized_location)
masters.append(master)
+ location_to_master[location_to_key(normalized_location)] = master
model = varLib.models.VariationModel(master_locations, axis_order)
- return cls(masters, model)
+ return cls(masters, location_to_master, model)
def instance_at(self, normalized_location: Location) -> FontMathObject:
+ """Return a FontMathObject for the specified location ready to be
+ inflated.
+
+ If an instance location matches a master location, this method
+ returns the master data instead of running through varLib. This
+ is both an optimization _and_ it enables having a Designspace
+ with instances matching their masters without requiring them to
+ be compatible. Glyphs.app works this way; it will only generate
+ a font from an instance, but compatibility is only required if
+ there is actual interpolation to be done. This enables us to
+ store incompatible bare masters in one Designspace and having
+ arbitrary instance data applied to them.
+ """
+ normalized_location_key = location_to_key(normalized_location)
+ if normalized_location_key in self.location_to_master:
+ return copy.deepcopy(self.location_to_master[normalized_location_key])
+
return self.model.interpolateFromMasters(normalized_location, self.masters)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]