Repository: googlefonts/ufo2ft
Pull request #918 (2025-06-11 08:57:53)

Copy attributes from instance lib element
TL;DR
ufo2ft's instancer doesn't copy over attributes that have been set using the instance lib element. This PR adds this functionality.

I'm currently working on a monospace family where I need to set the panose for each instance in the designspace. I'm defining the panose for each instance using the lib element, https://fonttools.readthedocs.io/en/stable/designspaceLib/xml.html#lib-element-instance:

```XML
  <instances>
    <instance name="Intel One Mono Light" familyname="Intel One Mono" stylename="Light" filename="instance_ufos/IntelOneMono-Light.ufo" stylemapstylename="regular" stylemapfamilyname="Intel One Mono Light">
      <location>
        <dimension name="Weight" xvalue="60"/>
      </location>
          <lib>
      <dict>
        <key>public.fontInfo</key>
        <dict>
          <key>openTypeOS2Panose</key>
          <array>
            <integer>2</integer>
            <integer>11</integer>
            <integer>4</integer>
            <integer>9</integer>
            <integer>2</integer>
            <integer>2</integer>
            <integer>3</integer>
            <integer>9</integer>
            <integer>2</integer>
            <integer>4</integer>
          </array>
        </dict>
      </dict>
    </lib>
    </instance>
```
When I run `fontmake -m IntelOneMono.designspace -i -o ufo` it produces a set of instance ufos that have the placeholder panose numbers only. You may be asking why the above fontmake command? This is currently used in the gftools builder.



Patch:
diff --git a/Lib/ufo2ft/instantiator.py b/Lib/ufo2ft/instantiator.py
index fcc7f97..26b6c62 100644
--- a/Lib/ufo2ft/instantiator.py
+++ b/Lib/ufo2ft/instantiator.py
@@ -596,6 +596,21 @@ class Instantiator:
                     copy.deepcopy(getattr(self.copy_info, attribute)),
                 )
 
+        # Copy instance-specific attributes from the InstanceDescriptor.
+        if hasattr(instance, "lib") and "public.fontInfo" in instance.lib:
+            for key, value in instance.lib["public.fontInfo"].items():
+                if hasattr(font.info, key):
+                    setattr(font.info, key, value)
+                else:
+                    logger.warning(
+                        "Instance %s at location %s has an unknown font info "
+                        "attribute %s with value %s. This will be ignored.",
+                        instance.familyName,
+                        location,
+                        key,
+                        value,
+                    )
+
         # TODO: multilingual names to replace possibly existing name records.
         if instance.familyName:
             font.info.familyName = instance.familyName


Test patch:
diff --git a/tests/instantiator_test.py b/tests/instantiator_test.py
index fa67331..3302fc3 100644
--- a/tests/instantiator_test.py
+++ b/tests/instantiator_test.py
@@ -477,6 +477,34 @@ def test_instance_no_attributes(ufo_module, data_dir, caplog):
     assert instance_font.info.styleMapStyleName is None
 
 
+def test_instance_lib_attributes(ufo_module, data_dir):
+    designspace = designspaceLib.DesignSpaceDocument.fromfile(
+        data_dir / "MutatorSans" / "MutatorSans.designspace"
+    )
+    designspace.instances[0].lib["public.fontInfo"] = {}
+    designspace.instances[0].lib["public.fontInfo"]["openTypeOS2Panose"] = [
+        2,
+        11,
+        5,
+        4,
+        2,
+        2,
+        2,
+        2,
+        2,
+        4,
+    ]
+    designspace.loadSourceFonts(openFontFactory(ufo_module=ufo_module))
+    generator = ufo2ft.instantiator.Instantiator.from_designspace(
+        designspace, round_geometry=True
+    )
+    instance_font = generator.generate_instance(designspace.instances[0])
+    assert instance_font.info.openTypeOS2Panose == [2, 11, 5, 4, 2, 2, 2, 2, 2, 4]
+
+    instance_font2 = generator.generate_instance(designspace.instances[1])
+    assert instance_font2.info.openTypeOS2Panose is None
+
+
 def test_axis_mapping(ufo_module, data_dir):
     designspace = designspaceLib.DesignSpaceDocument.fromfile(
         data_dir / "DesignspaceTest" / "DesignspaceTest-wght-wdth.designspace"