Skip to content

Commit aed50f0

Browse files
authored
Merge pull request #983 from googlefonts/fix-instantiator-sparse-master-assertion
[instantiator] Fix assertion error in instantiator with sparse/virtual masters
2 parents 023a7d8 + ddedb12 commit aed50f0

2 files changed

Lines changed: 104 additions & 5 deletions

File tree

Lib/ufo2ft/instantiator.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -722,11 +722,16 @@ def _generate_instance_info(
722722
if self.round_geometry:
723723
info_instance = info_instance.round()
724724

725-
# If there is only one master (static font), the instance can only be at the
726-
# default location after the latter has been normalized. It's OK for it to
727-
# inherit ALL the fontinfo from the default source.
728-
if self.info_mutator.is_static_font():
729-
assert all(v == 0.0 for v in location_normalized.values())
725+
# If there is only one info master AND the instance is at the default
726+
# location, it's OK for it to inherit ALL the fontinfo from the default
727+
# source. This covers true static fonts as well as variable fonts with
728+
# sparse/virtual masters (where collect_info_masters skips the
729+
# non-default layer-only sources) when the instance happens to be at
730+
# the default. Instances at non-default locations fall through to the
731+
# multi-master path, which correctly skips instance-specific attributes
732+
# like postscriptFontName, styleName, openTypeNameUniqueID, etc.
733+
is_at_default = all(v == 0.0 for v in location_normalized.values())
734+
if self.info_mutator.is_static_font() and is_at_default:
730735
for attribute in ufoLib.fontInfoAttributesVersion3:
731736
if (value := getattr(self.copy_info, attribute, None)) is not None:
732737
setattr(font.info, attribute, copy.deepcopy(value))

tests/instantiator_test.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
10931187
def 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

Comments
 (0)