@@ -1011,8 +1011,8 @@ def test_data_independence(ufo_module, data_dir):
10111011
10121012 assert generator .copy_info .openTypeOS2Panose == [2 , 11 , 5 , 4 , 2 , 2 , 2 , 2 , 2 , 4 ]
10131013 generator .copy_info .openTypeOS2Panose .append (1000 )
1014- assert instance_font1 .info .openTypeOS2Panose is None
1015- assert instance_font2 .info .openTypeOS2Panose is None
1014+ assert instance_font1 .info .openTypeOS2Panose == [ 2 , 11 , 5 , 4 , 2 , 2 , 2 , 2 , 2 , 4 ]
1015+ assert instance_font2 .info .openTypeOS2Panose == [ 2 , 11 , 5 , 4 , 2 , 2 , 2 , 2 , 2 , 4 ]
10161016
10171017 # copy_feature_text not tested because it is a(n immutable) string
10181018
@@ -1039,7 +1039,6 @@ def test_skipped_fontinfo_attributes():
10391039 "openTypeNameUniqueID" ,
10401040 "openTypeNameWWSFamilyName" ,
10411041 "openTypeNameWWSSubfamilyName" ,
1042- "openTypeOS2Panose" ,
10431042 "postscriptFontName" ,
10441043 "postscriptFullName" ,
10451044 "postscriptUniqueID" ,
@@ -1070,6 +1069,237 @@ def test_designspace_v5_discrete_axis_raises_error(data_dir):
10701069 ufo2ft .instantiator .Instantiator .from_designspace (designspace )
10711070
10721071
1072+ def test_opentype_os2_panose_merging (ufo_module ):
1073+ """Test that openTypeOS2Panose values are properly merged across sources.
1074+
1075+ Only panose values that are identical across all sources should be copied
1076+ to instances. Different values should be set to 0.
1077+ """
1078+ d = designspaceLib .DesignSpaceDocument ()
1079+ d .addAxisDescriptor (
1080+ name = "Weight" , tag = "wght" , minimum = 300 , default = 300 , maximum = 900
1081+ )
1082+
1083+ font1 = ufo_module .Font ()
1084+ font1 .info .openTypeOS2Panose = [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ] # default source
1085+
1086+ font2 = ufo_module .Font ()
1087+ font2 .info .openTypeOS2Panose = [
1088+ 2 ,
1089+ 11 ,
1090+ 8 , # different at index 2
1091+ 2 ,
1092+ 4 ,
1093+ 5 ,
1094+ 4 ,
1095+ 2 ,
1096+ 2 ,
1097+ 4 ,
1098+ ]
1099+
1100+ font3 = ufo_module .Font ()
1101+ font3 .info .openTypeOS2Panose = [
1102+ 2 ,
1103+ 11 ,
1104+ 5 ,
1105+ 3 , # different at index 3
1106+ 4 ,
1107+ 5 ,
1108+ 4 ,
1109+ 2 ,
1110+ 2 ,
1111+ 4 ,
1112+ ]
1113+
1114+ d .addSourceDescriptor (location = {"Weight" : 300 }, font = font1 )
1115+ d .addSourceDescriptor (location = {"Weight" : 600 }, font = font2 )
1116+ d .addSourceDescriptor (location = {"Weight" : 900 }, font = font3 )
1117+ d .addInstanceDescriptor (styleName = "Regular" , location = {"Weight" : 400 })
1118+ d .addInstanceDescriptor (styleName = "Bold" , location = {"Weight" : 700 })
1119+
1120+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1121+
1122+ instance1 = generator .generate_instance (d .instances [0 ])
1123+ instance2 = generator .generate_instance (d .instances [1 ])
1124+
1125+ # Indices 2 and 3 differ across sources, so they should be 0
1126+ # All other indices are the same across sources
1127+ expected_panose = [2 , 11 , 0 , 0 , 4 , 5 , 4 , 2 , 2 , 4 ]
1128+
1129+ assert instance1 .info .openTypeOS2Panose == expected_panose
1130+ assert instance2 .info .openTypeOS2Panose == expected_panose
1131+
1132+
1133+ def test_opentype_os2_panose_all_different (ufo_module ):
1134+ """Test that when all panose values differ, the attribute is deleted."""
1135+ d = designspaceLib .DesignSpaceDocument ()
1136+ d .addAxisDescriptor (
1137+ name = "Weight" , tag = "wght" , minimum = 300 , default = 300 , maximum = 900
1138+ )
1139+
1140+ # Create fonts with completely different panose values
1141+ font1 = ufo_module .Font ()
1142+ font1 .info .openTypeOS2Panose = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ]
1143+
1144+ font2 = ufo_module .Font ()
1145+ font2 .info .openTypeOS2Panose = [10 , 9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 ]
1146+
1147+ d .addSourceDescriptor (location = {"Weight" : 300 }, font = font1 )
1148+ d .addSourceDescriptor (location = {"Weight" : 900 }, font = font2 )
1149+ d .addInstanceDescriptor (styleName = "Regular" , location = {"Weight" : 600 })
1150+
1151+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1152+ instance = generator .generate_instance (d .instances [0 ])
1153+
1154+ # All values differ, so openTypeOS2Panose should be unset
1155+ assert instance .info .openTypeOS2Panose is None
1156+
1157+
1158+ def test_opentype_os2_panose_missing_in_some_sources (ufo_module ):
1159+ """Test handling when some sources don't have panose values."""
1160+ d = designspaceLib .DesignSpaceDocument ()
1161+ d .addAxisDescriptor (
1162+ name = "Weight" , tag = "wght" , minimum = 300 , default = 300 , maximum = 900
1163+ )
1164+
1165+ font1 = ufo_module .Font ()
1166+ font1 .info .openTypeOS2Panose = [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1167+
1168+ # Second font doesn't have panose, doesn't contribute
1169+ font2 = ufo_module .Font ()
1170+ assert font2 .info .openTypeOS2Panose is None
1171+
1172+ font3 = ufo_module .Font ()
1173+ font3 .info .openTypeOS2Panose = [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1174+
1175+ d .addSourceDescriptor (location = {"Weight" : 300 }, font = font1 )
1176+ d .addSourceDescriptor (location = {"Weight" : 600 }, font = font2 )
1177+ d .addSourceDescriptor (location = {"Weight" : 900 }, font = font3 )
1178+ d .addInstanceDescriptor (styleName = "Regular" , location = {"Weight" : 400 })
1179+
1180+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1181+ instance = generator .generate_instance (d .instances [0 ])
1182+
1183+ # The default source's panose should be copied since font2 has None and
1184+ # font3 has same panose as font1
1185+ assert instance .info .openTypeOS2Panose == [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1186+
1187+
1188+ def test_opentype_os2_panose_single_source (ufo_module ):
1189+ """Test that panose values are preserved as-is when there's only one source.
1190+
1191+ Cf. https://github.com/googlefonts/fontc/issues/1609
1192+ """
1193+ d = designspaceLib .DesignSpaceDocument ()
1194+ d .addAxisDescriptor (
1195+ name = "Weight" , tag = "wght" , minimum = 300 , default = 400 , maximum = 900
1196+ )
1197+
1198+ font1 = ufo_module .Font ()
1199+ font1 .info .openTypeOS2Panose = [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1200+
1201+ d .addSourceDescriptor (location = {"Weight" : 400 }, font = font1 )
1202+ d .addInstanceDescriptor (styleName = "Regular" , location = {"Weight" : 400 })
1203+
1204+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1205+
1206+ instance = generator .generate_instance (d .instances [0 ])
1207+
1208+ # With only one source, panose should be preserved as-is
1209+ assert instance .info .openTypeOS2Panose == [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1210+
1211+
1212+ def test_opentype_os2_panose_no_mutation_of_default (ufo_module ):
1213+ """Test that processing panose values doesn't mutate the default source."""
1214+ d = designspaceLib .DesignSpaceDocument ()
1215+ d .addAxisDescriptor (
1216+ name = "Weight" , tag = "wght" , minimum = 300 , default = 300 , maximum = 900
1217+ )
1218+
1219+ font1 = ufo_module .Font ()
1220+ original_panose = [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1221+ font1 .info .openTypeOS2Panose = original_panose .copy ()
1222+
1223+ font2 = ufo_module .Font ()
1224+ font2 .info .openTypeOS2Panose = [
1225+ 2 ,
1226+ 11 ,
1227+ 8 , # different at index 2
1228+ 2 ,
1229+ 4 ,
1230+ 5 ,
1231+ 4 ,
1232+ 2 ,
1233+ 2 ,
1234+ 4 ,
1235+ ]
1236+
1237+ d .addSourceDescriptor (location = {"Weight" : 300 }, font = font1 )
1238+ d .addSourceDescriptor (location = {"Weight" : 900 }, font = font2 )
1239+ d .addInstanceDescriptor (styleName = "Regular" , location = {"Weight" : 600 })
1240+
1241+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1242+ instance = generator .generate_instance (d .instances [0 ])
1243+
1244+ # The instance should have merged panose with 0 at index 2
1245+ assert instance .info .openTypeOS2Panose == [2 , 11 , 0 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ]
1246+
1247+ # The default source should remain unchanged
1248+ assert font1 .info .openTypeOS2Panose == original_panose
1249+ assert d .default .font .info .openTypeOS2Panose == original_panose
1250+
1251+
1252+ @pytest .mark .parametrize (
1253+ "panose_data,expected" ,
1254+ [
1255+ pytest .param (
1256+ [[2 , 11 , 5 ], [2 , 11 , 8 , 2 , 4 , 5 , 4 , 2 , 2 , 4 , 99 , 88 , 77 , 66 , 55 ]],
1257+ [2 , 11 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ],
1258+ id = "zero-padded" , # At least one source has length < 10 (will be padded)
1259+ ),
1260+ pytest .param (
1261+ [
1262+ [2 , 11 , 5 , 2 , 4 , 5 , 4 , 2 , 2 , 4 , 99 , 88 ],
1263+ [2 , 11 , 8 , 2 , 4 , 5 , 4 , 2 , 2 , 4 , 77 , 66 , 55 ],
1264+ ],
1265+ [2 , 11 , 0 , 2 , 4 , 5 , 4 , 2 , 2 , 4 ],
1266+ id = "truncated" , # All sources have length > 10 (will be truncated)
1267+ ),
1268+ ],
1269+ )
1270+ def test_opentype_os2_panose_malformed_lengths (panose_data , expected , caplog ):
1271+ """Test handling of panose values with incorrect lengths (ufoLib2 only)."""
1272+ ufo_module = pytest .importorskip ("ufoLib2" )
1273+
1274+ d = designspaceLib .DesignSpaceDocument ()
1275+ d .addAxisDescriptor (
1276+ name = "Weight" , tag = "wght" , minimum = 300 , default = 300 , maximum = 900
1277+ )
1278+
1279+ fonts = []
1280+ for i , panose_values in enumerate (panose_data ):
1281+ font = ufo_module .Font ()
1282+ font .info .openTypeOS2Panose = panose_values
1283+ fonts .append (font )
1284+ d .addSourceDescriptor (location = {"Weight" : 300 + i * 600 }, font = font )
1285+
1286+ d .addInstanceDescriptor (styleName = "Regular" , location = {"Weight" : 600 })
1287+
1288+ with caplog .at_level (logging .WARNING ):
1289+ generator = ufo2ft .instantiator .Instantiator .from_designspace (d )
1290+ instance = generator .generate_instance (d .instances [0 ])
1291+
1292+ # Should log a warning about invalid length
1293+ assert (
1294+ "openTypeOS2Panose values in designspace sources have invalid length"
1295+ in caplog .text
1296+ )
1297+
1298+ # Should produce a valid result with exactly 10 values
1299+ assert instance .info .openTypeOS2Panose == expected
1300+ assert len (instance .info .openTypeOS2Panose ) == 10
1301+
1302+
10731303def test_strict_math_glyph (ufo_module , data_dir ):
10741304 designspace = designspaceLib .DesignSpaceDocument .fromfile (
10751305 data_dir / "InstantiatorStrictMathGlyph" / "StrictMathGlyph.designspace"
0 commit comments