@@ -1090,6 +1090,100 @@ def test_static_font_default_instance_inherits_all_fontinfo(data_dir, ufo_module
10901090 assert instance_font .info .postscriptFontName == "MyFont-Light"
10911091
10921092
1093+ def test_sparse_master_instance_at_non_default_location (ufo_module ):
1094+ """Designspaces with sparse/virtual masters (e.g. those Glyphs.app emits for a
1095+ "Virtual Master" custom parameter) have only one source carrying fontinfo
1096+ even though sources exist at non-default locations in design space. Instances
1097+ at non-default locations must be generatable without tripping the
1098+ is_static_font() assertion in _generate_instance_info.
1099+
1100+ https://github.com/googlefonts/ufo2ft/issues/981
1101+ """
1102+ d = designspaceLib .DesignSpaceDocument ()
1103+ d .addAxisDescriptor (
1104+ name = "Pixel Shape" , tag = "PXSH" , minimum = 1 , default = 1 , maximum = 100
1105+ )
1106+
1107+ # Default source: carries fontinfo, default layer holds the glyph.
1108+ default_font = ufo_module .Font ()
1109+ default_font .info .familyName = "Geist Pixel"
1110+ default_font .info .styleName = "Regular"
1111+ default_font .info .unitsPerEm = 1000
1112+ default_font .info .ascender = 800
1113+ default_font .info .descender = - 200
1114+ default_font .info .xHeight = 500
1115+ default_font .info .capHeight = 700
1116+ # instance-specific attribute that the multi-master path must NOT inherit
1117+ default_font .info .postscriptFontName = "GeistPixel-Regular"
1118+ default_glyph = default_font .newGlyph ("A" )
1119+ default_glyph .width = 600
1120+ pen = default_glyph .getPen ()
1121+ pen .moveTo ((0 , 0 ))
1122+ pen .lineTo ((600 , 0 ))
1123+ pen .lineTo ((600 , 700 ))
1124+ pen .lineTo ((0 , 700 ))
1125+ pen .closePath ()
1126+
1127+ # Sparse source: shares the same font object but points at a non-default
1128+ # layer. collect_info_masters() will skip it because layerName is not None,
1129+ # so info_mutator ends up with a single master.
1130+ sparse_layer = default_font .newLayer ("{100}" )
1131+ sparse_glyph = sparse_layer .newGlyph ("A" )
1132+ sparse_glyph .width = 600
1133+ pen = sparse_glyph .getPen ()
1134+ pen .moveTo ((50 , 50 ))
1135+ pen .lineTo ((550 , 50 ))
1136+ pen .lineTo ((550 , 650 ))
1137+ pen .lineTo ((50 , 650 ))
1138+ pen .closePath ()
1139+
1140+ d .addSourceDescriptor (
1141+ name = "Geist Pixel Regular" ,
1142+ familyName = "Geist Pixel" ,
1143+ styleName = "Regular" ,
1144+ location = {"Pixel Shape" : 1 },
1145+ font = default_font ,
1146+ )
1147+ d .addSourceDescriptor (
1148+ name = "Geist Pixel Regular {100}" ,
1149+ familyName = "Geist Pixel" ,
1150+ styleName = "Regular {100}" ,
1151+ layerName = "{100}" ,
1152+ location = {"Pixel Shape" : 100 },
1153+ font = default_font ,
1154+ )
1155+ d .addInstanceDescriptor (
1156+ familyName = "Geist Pixel" ,
1157+ styleName = "Circle" ,
1158+ location = {"Pixel Shape" : 20 },
1159+ )
1160+
1161+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1162+
1163+ # info_mutator was built from only the default source (the sparse source was
1164+ # skipped by collect_info_masters), so it reports as a single-master Variator.
1165+ assert generator .info_mutator .is_static_font ()
1166+
1167+ # The bug: generating an instance at a non-default location currently trips
1168+ # `assert all(v == 0.0 for v in location_normalized.values())`.
1169+ instance_font = generator .generate_instance (d .instances [0 ])
1170+
1171+ # Instance-specific attributes must NOT be inherited from the default source
1172+ # when the instance is at a non-default location. (Only the static-font
1173+ # path that runs at the default location is allowed to inherit them.)
1174+ assert instance_font .info .postscriptFontName is None
1175+
1176+ # The interpolating fontinfo (a no-op with a single info master) reduces to
1177+ # the default master's values, which is what we want.
1178+ assert instance_font .info .unitsPerEm == 1000
1179+ assert instance_font .info .ascender == 800
1180+ assert instance_font .info .descender == - 200
1181+
1182+ # Instance-level overrides still apply.
1183+ assert instance_font .info .familyName == "Geist Pixel"
1184+ assert instance_font .info .styleName == "Circle"
1185+
1186+
10931187def test_designspace_v5_discrete_axis_raises_error (data_dir ):
10941188 designspace = designspaceLib .DesignSpaceDocument .fromfile (
10951189 data_dir / "MutatorSansLite" / "MutatorFamily_v5_discrete_axis.designspace"
0 commit comments