Skip to content

Commit

Permalink
Merge pull request #980 from verbosus/non-default-layer
Browse files Browse the repository at this point in the history
Instantiator support for non-default layer sources
  • Loading branch information
anthrotype authored Feb 21, 2023
2 parents b55aabd + bd6ad41 commit eef7c40
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Lib/fontmake/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def parse_mutually_exclusive_inputs(parser, args):
):
ufo_paths.append(filename)
else:
parser.error(f"Unknown input file extension: '{filename}'")
parser.error(f"Unknown input file extension: {filename!r}")

count = sum(bool(p) for p in (glyphs_path, ufo_paths, designspace_path))
if count == 0:
Expand Down
2 changes: 1 addition & 1 deletion Lib/fontmake/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, msg, source_file):

def __str__(self):
trail = " -> ".join(
f"'{str(_try_relative_path(s))}'"
f"{str(_try_relative_path(s))!r}"
for s in reversed(self.source_trail)
if s is not None
)
Expand Down
7 changes: 5 additions & 2 deletions Lib/fontmake/font_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ def save_otfs(
warnings.warn(
"the 'subroutinize' argument is deprecated, use 'optimize_cff'",
UserWarning,
stacklevel=2,
)
if subroutinize:
optimize_cff = CFFOptimization.SUBROUTINIZE
Expand Down Expand Up @@ -909,13 +910,15 @@ def interpolate_instance_ufos(
if include is not None and not fullmatch(include, instance.name):
continue

logger.info(f'Generating instance UFO for "{instance.name}"')
logger.info("Generating instance UFO for {!r}".format(instance.name))

try:
instance.font = generator.generate_instance(instance)
except instantiator.InstantiatorError as e:
raise FontmakeError(
f"Interpolating instance '{instance.styleName}' failed.",
"Interpolating instance {!r} failed.".format(
instance.styleName
),
designspace.path,
) from e

Expand Down
50 changes: 37 additions & 13 deletions Lib/fontmake/instantiator.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,20 +224,41 @@ def from_designspace(
# because the math behind varLib and MutatorMath uses the default font as the
# point of reference for all data.
default_font = designspace.default.font
non_default_layer_name = designspace.default.layerName

glyph_names: Set[str] = set(default_font.keys())

if non_default_layer_name is not None:
try:
layer = default_font.layers[non_default_layer_name]
except KeyError as e:
raise InstantiatorError(
f"Layer {non_default_layer_name!r} not found "
f"in {designspace.default.filename}"
) from e
layer = default_font.layers[non_default_layer_name]
glyph_names = layer.keys()
logger.info(f"Building from layer {layer.name}")

for source in designspace.sources:
other_names = set(source.font.keys())
diff_names = other_names - glyph_names
if diff_names:
max_diff_glyphs = 10
logger.warning(
"The source %s (%s) contains glyphs that are missing from the "
"default source, which will be ignored: %s. If this is unintended, "
"The source %s (%s)%s contains glyphs that are missing from the "
"default source, which will be ignored: %s%s; if this is unintended, "
"check that these glyphs have the exact same name as the "
"corresponding glyphs in the default source.",
source.name,
source.filename,
", ".join(sorted(diff_names)),
f" [layer: {source.layerName}]"
if non_default_layer_name is not None
else "",
", ".join(sorted(diff_names)[0:max_diff_glyphs]),
f"... ({len(diff_names)} total)"
if len(diff_names) > max_diff_glyphs
else "",
)

# Construct Variators
Expand Down Expand Up @@ -279,7 +300,7 @@ def from_designspace(
glyph_mutators[glyph_name] = Variator.from_masters(items, axis_order)
except varLib.errors.VarLibError as e:
raise InstantiatorError(
f"Cannot set up glyph '{glyph_name}' for interpolation: {e}'"
f"Cannot set up glyph {glyph_name} for interpolation: {e}'"
) from e
glyph_name_to_unicodes[glyph_name] = default_font[glyph_name].unicodes

Expand Down Expand Up @@ -382,7 +403,7 @@ def generate_instance(
# whatever reason (usually outline incompatibility)...
if glyph_name not in self.skip_export_glyphs:
raise InstantiatorError(
f"Failed to generate instance of glyph '{glyph_name}': "
f"Failed to generate instance of glyph {glyph_name!r}: "
f"{str(e)}. (Note: the most common cause for an error here is "
"that the glyph outlines are not point-for-point compatible or "
"have the same starting point or are in the same order in all "
Expand Down Expand Up @@ -494,9 +515,9 @@ def _error_msg_no_default(designspace: designspaceLib.DesignSpaceDocument) -> st

return (
"Can't generate UFOs from this Designspace because there is no default "
f"master source at location '{default_location}'. Check that all 'default' "
"master source at location {!r}. Check that all 'default' "
"values of all axes together point to a single actual master source. "
f"{bonus_msg}"
"{!s}".format(default_location, bonus_msg)
)


Expand All @@ -517,9 +538,10 @@ def collect_info_masters(
) -> List[Tuple[Location, FontMathObject]]:
"""Return master Info objects wrapped by MathInfo."""
locations_and_masters = []

for source in designspace.sources:
if source.layerName is not None:
continue # No font info in source layers.
if source.layerName is not None and source is not designspace.default:
continue # No font info in non-default source layers.

normalized_location = varLib.models.normalizeLocation(
source.location, axis_bounds
Expand All @@ -541,9 +563,10 @@ def collect_kerning_masters(
groups = designspace.default.font.groups

locations_and_masters = []

for source in designspace.sources:
if source.layerName is not None:
continue # No kerning in source layers.
if source.layerName is not None and source is not designspace.default:
continue # No kerning in non-default source layers.

# If a source has groups, they should match the default's.
if source.font.groups and source.font.groups != groups:
Expand Down Expand Up @@ -665,8 +688,9 @@ def swap_glyph_names(font: ufoLib2.Font, name_old: str, name_new: str):

if name_old not in font or name_new not in font:
raise InstantiatorError(
f"Cannot swap glyphs '{name_old}' and '{name_new}', as either or both are "
"missing."
"Cannot swap glyphs {!r} and {!r}, as either or both are missing".format(
name_old, name_new
)
)

# 1. Swap outlines and glyph width. Ignore lib content and other properties.
Expand Down
29 changes: 29 additions & 0 deletions tests/data/MutatorSans/MutatorSans-non-default-layer.designspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="4.0">
<axes>
<axis tag="wdth" name="width" minimum="0" maximum="1000" default="0"/>
<axis tag="wght" name="weight" minimum="0" maximum="1000" default="0"/>
</axes>
<sources>
<source filename="MutatorSansLightCondensed.ufo" name="master.MutatorMathTest.LightCondensed.0" familyname="MutatorMathTest" stylename="LightCondensed" layer="support">
<lib copy="1"/>
<groups copy="1"/>
<features copy="1"/>
<info copy="1"/>
<location>
<dimension name="width" xvalue="0"/>
<dimension name="weight" xvalue="0"/>
</location>
</source>
</sources>
<instances>
<instance familyname="MutatorMathTest" stylename="LightCondensed" filename="instances/MutatorMathTest-LightCondensed.ufo" postscriptfontname="MutatorMathTest-LightCondensed">
<location>
<dimension name="width" xvalue="0"/>
<dimension name="weight" xvalue="0"/>
</location>
<kerning/>
<info/>
</instance>
</instances>
</designspace>
12 changes: 12 additions & 0 deletions tests/test_instantiator.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,18 @@ def test_interpolation_masters_as_instances(data_dir):
assert instance_font["l"].width == 280


def test_non_default_layer(data_dir, caplog):
designspace = designspaceLib.DesignSpaceDocument.fromfile(
data_dir / "MutatorSans" / "MutatorSans-non-default-layer.designspace"
)
designspace.loadSourceFonts(ufoLib2.Font.open)
generator = fontmake.instantiator.Instantiator.from_designspace(
designspace, round_geometry=True
)
instance_font = generator.generate_instance(designspace.instances[0])
assert {g.name for g in instance_font} == {"A", "S", "W"}


def test_instance_attributes(data_dir):
designspace = designspaceLib.DesignSpaceDocument.fromfile(
data_dir / "DesignspaceTest" / "DesignspaceTest-instance-attrs.designspace"
Expand Down

0 comments on commit eef7c40

Please sign in to comment.