Repository: googlefonts/glyphsLib
Pull request #618 (2020-08-03 12:14:45)

Support 'Axis Mappings' customParameter
Support the 'Axis Mappings' customParameter.

If this customParameter has been defined, it takes precedence over all our other methods to assemble the designspace axes.


Todo:
- Possibly add a more informative comment
- regularDesignLoc and regularUserLoc are copy pasta. We may want to refactor this.


Patch:
diff --git a/Lib/glyphsLib/builder/axes.py b/Lib/glyphsLib/builder/axes.py
index 811f0f81..4e82077b 100644
--- a/Lib/glyphsLib/builder/axes.py
+++ b/Lib/glyphsLib/builder/axes.py
@@ -140,6 +140,8 @@ def to_designspace_axes(self):
     regular_master = get_regular_master(self.font)
     assert isinstance(regular_master, classes.GSFontMaster)
 
+    custom_mapping = self.font.customParameters["Axis Mappings"]
+
     for axis_def in get_axis_definitions(self.font):
         axis = self.designspace.newAxisDescriptor()
         axis.tag = axis_def.tag
@@ -157,8 +159,21 @@ def to_designspace_axes(self):
         ):
             axis_wanted = True
 
+        # See https://github.com/googlefonts/glyphsLib/issues/568
+        if custom_mapping:
+            if axis.tag in custom_mapping:
+                mapping = {float(k): v for k, v in custom_mapping[axis.tag].items()}
+                regularDesignLoc = axis_def.get_design_loc(regular_master)
+                reverse_mapping = [(dl, ul) for ul, dl in sorted(mapping.items())]
+                regularUserLoc = interp(reverse_mapping, regularDesignLoc)
+            else:
+                logger.debug(
+                    f"Skipping {axis.tag} since it hasn't been defined "
+                    "in the Axis Mapping."
+                )
+                continue
         # See https://github.com/googlefonts/glyphsLib/issues/280
-        if font_uses_new_axes(self.font):
+        elif font_uses_axis_locations(self.font):
             # Build the mapping from the "Axis Location" of the masters
             # TODO: (jany) use Virtual Masters as well?
             mapping = {}
@@ -174,7 +189,7 @@ def to_designspace_axes(self):
             regularDesignLoc = axis_def.get_design_loc(regular_master)
             regularUserLoc = axis_def.get_user_loc(regular_master)
         else:
-            # Build the mapping from the isntances because they have both
+            # Build the mapping from the instances because they have both
             # a user location and a design location.
             instance_mapping = {}
             for instance in self.font.instances:
@@ -242,7 +257,7 @@ def to_designspace_axes(self):
         )
 
 
-def font_uses_new_axes(font):
+def font_uses_axis_locations(font):
     # It's possible for fonts to have the 'Axes' parameter but to NOT specify
     # the master locations using 'Axis Location', in which case we have to
     # resort to using instances or other old tricks to get the mapping.
@@ -281,21 +296,11 @@ def to_glyphs_axes(self):
     if axes_parameter and not _is_subset_of_default_axes(axes_parameter):
         self.font.customParameters["Axes"] = axes_parameter
 
-    if self.minimize_ufo_diffs:
-        # TODO: (jany) later, when Glyphs can manage general designspace axes
-        # self.font.userData[AXES_KEY] = [
-        #     dict(
-        #         tag=axis.tag,
-        #         name=axis.name,
-        #         minimum=axis.minimum,
-        #         maximum=axis.maximum,
-        #         default=axis.default,
-        #         hidden=axis.hidden,
-        #         labelNames=axis.labelNames,
-        #     )
-        #     for axis in self.designspace.axes
-        # ]
-        pass
+    if any(a.map for a in self.designspace.axes):
+        mapping = {
+            axis.tag: {str(k): v for k, v in axis.map} for axis in self.designspace.axes
+        }
+        self.font.customParameters["Axis Mappings"] = mapping
 
 
 class AxisDefinition:
diff --git a/Lib/glyphsLib/builder/masters.py b/Lib/glyphsLib/builder/masters.py
index 75b0e236..2a8682e4 100644
--- a/Lib/glyphsLib/builder/masters.py
+++ b/Lib/glyphsLib/builder/masters.py
@@ -15,7 +15,7 @@
 
 import os
 
-from .axes import font_uses_new_axes, get_axis_definitions
+from .axes import font_uses_axis_locations, get_axis_definitions
 from .constants import GLYPHS_PREFIX, GLYPHLIB_PREFIX, UFO_FILENAME_CUSTOM_PARAM
 
 MASTER_ID_LIB_KEY = GLYPHS_PREFIX + "fontMasterID"
@@ -63,7 +63,7 @@ def to_ufo_master_attributes(self, source, master):
         if custom_value:
             ufo.lib[GLYPHS_PREFIX + "customValue" + number] = custom_value
 
-    if font_uses_new_axes(self.font):
+    if font_uses_axis_locations(self.font):
         # Set the OS/2 weightClass and widthClas according the this master's
         # user location ("Axis Location" parameter)
         for axis in get_axis_definitions(self.font):
diff --git a/Lib/glyphsLib/builder/sources.py b/Lib/glyphsLib/builder/sources.py
index 4b623652..8907994f 100644
--- a/Lib/glyphsLib/builder/sources.py
+++ b/Lib/glyphsLib/builder/sources.py
@@ -21,7 +21,12 @@ import fontTools.designspaceLib
 from glyphsLib.util import build_ufo_path
 
 from .masters import UFO_FILENAME_KEY
-from .axes import get_axis_definitions, get_regular_master, font_uses_new_axes, interp
+from .axes import (
+    get_axis_definitions,
+    get_regular_master,
+    font_uses_axis_locations,
+    interp,
+)
 from .constants import UFO_FILENAME_CUSTOM_PARAM
 
 
@@ -227,7 +232,7 @@ def _to_glyphs_source(self, master):
             continue
 
         axis_def.set_design_loc(master, design_location)
-        if font_uses_new_axes(self.font):
+        if font_uses_axis_locations(self.font):
             # The user location can be found by reading the mapping backwards
             mapping = []
             for axis in self.designspace.axes:


Test patch:
diff --git a/tests/builder/axes_test.py b/tests/builder/axes_test.py
index 47b1a0f0..06423ac4 100644
--- a/tests/builder/axes_test.py
+++ b/tests/builder/axes_test.py
@@ -305,3 +305,50 @@ def test_single_master_default_weight_400(ufo_module):
 
     assert len(font2.masters) == 1
     assert font2.masters[0].weightValue == 400
+
+
+def test_axis_mapping(ufo_module):
+    font = to_glyphs(
+        [ufo_module.Font(), ufo_module.Font(), ufo_module.Font(), ufo_module.Font()]
+    )
+    font.masters[0].weightValue = 0
+    font.masters[0].widthValue = 100
+    font.masters[1].weightValue = 1000
+    font.masters[1].widthValue = 100
+
+    font.masters[2].weightValue = 0
+    font.masters[2].widthValue = 75
+    font.masters[3].weightValue = 1000
+    font.masters[3].widthValue = 75
+
+    wght_mapping = [(100, 0), (400, 350), (900, 1000)]
+    wdth_mapping = [(75, 75), (100, 100)]
+
+    axis_mappings = {
+        "wght": {str(float(k)): v for k, v in wght_mapping},
+        "wdth": {str(float(k)): v for k, v in wdth_mapping},
+    }
+
+    font.customParameters["Axis Mappings"] = axis_mappings
+    # When we convert to a designspace, the wdth mapping is removed because
+    # it isn't needed.
+    doc = to_designspace(font, ufo_module=ufo_module)
+
+    assert doc.axes[0].name == "Weight"
+    assert doc.axes[0].minimum == 100
+    assert doc.axes[0].default == 100
+    assert doc.axes[0].maximum == 900
+    assert doc.axes[0].map == wght_mapping
+
+    assert doc.axes[1].name == "Width"
+    assert doc.axes[1].minimum == 75
+    assert doc.axes[0].default == 100
+    assert doc.axes[1].maximum == 100
+    assert doc.axes[1].map != wdth_mapping
+    assert doc.axes[1].map == []
+
+    # When we convert back to glyphs, the wdth mapping isn't present because
+    # it was removed during the designspace conversion.
+    font = to_glyphs(doc)
+    axis_mappings["wdth"] = {}
+    assert font.customParameters["Axis Mappings"] == axis_mappings