Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix "Axis Mappings" interpretation #1040

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions Lib/glyphsLib/builder/axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,22 @@ def to_designspace_axes(self):
):
axis_wanted = True

# Build the mapping from the instances because they have both
# a user location and a design location; we build this here
# because we use it to check if a custom mapping needs to be inverted.
instance_mapping = {}
update_mapping_from_instances(
instance_mapping,
self.font.instances,
axis_def,
minimize_glyphs_diffs=self.minimize_glyphs_diffs,
)

# See https://github.com/googlefonts/glyphsLib/issues/568
if custom_mapping:
if axis.tag in custom_mapping:
mapping = {float(k): v for k, v in custom_mapping[axis.tag].items()}
mapping = _potentially_invert_mapping(self, mapping, instance_mapping)
regularDesignLoc = axis_def.get_design_loc(regular_master)
reverse_mapping = {dl: ul for ul, dl in sorted(mapping.items())}
regularUserLoc = piecewiseLinearMap(regularDesignLoc, reverse_mapping)
Expand Down Expand Up @@ -230,16 +242,6 @@ def to_designspace_axes(self):
regularDesignLoc = axis_def.get_design_loc(regular_master)
regularUserLoc = axis_def.get_user_loc(regular_master)
else:
# Build the mapping from the instances because they have both
# a user location and a design location.
instance_mapping = {}
update_mapping_from_instances(
instance_mapping,
self.font.instances,
axis_def,
minimize_glyphs_diffs=self.minimize_glyphs_diffs,
)

master_mapping = {}
for master in self.font.masters:
# Glyphs masters don't have a user location
Expand Down Expand Up @@ -322,6 +324,51 @@ def font_uses_axis_locations(font):
)


def _potentially_invert_mapping(self, mapping, instance_mapping):
# Fontmake introduced an Axis Mapping custom parameter before Glyphs did;
# then Glyphs introduced it, but with an inverted interpretation. Then
# Glyphs changed the interpretation to match fontmake's. Try and figure out
simoncozens marked this conversation as resolved.
Show resolved Hide resolved
# what's going on.
# See https://github.com/googlefonts/glyphsLib/issues/745

# We want to return a userspace->designspace map for our purposes
inverted = {v: k for k, v in mapping.items()}
if int(self.font.appVersion) >= 3168:
# Mapping is *definitely* designspace->userspace, invert
return inverted
Copy link
Member

@anthrotype anthrotype Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm confused, I thought older versions (< 3168?) would interpret it as design->user and thus require flip, whereas newer versions have aligned with fontmake's interpretation and already have user->design? This block seems to contradict my understanding, for it's using an inverted mapping if the version is more recent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Interpret as" is perhaps not helpful here. :-) I just opened Nunito source, converted to Glyphs 3 file format and saved under Glyphs 3310. The custom parameter is stored as:

wght = {
42 = 200;
61 = 300;
81 = 400;
91 = 500;
101 = 600;
125 = 700;
151 = 800;
178 = 900;
208 = 1000;
};

i.e. designspace to userspace.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now I am extremely confused

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thankfully the code has comments and the tests are passing. :-) (And I can compile fonts that were not compiling before.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, I for one don't understand this whole mess. It would be nice to have a recap.

Copy link
Member

@anthrotype anthrotype Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can compile fonts that were not compiling before

good, what I'm worried about are the other fonts that were compiling before and may not after this.
Right now (tested with 3324) on a newly created glyphs3 file, the Axis Mappings is already stored in the source as user->design (or external->internal, as Glyphs calls them), so it is correct and matches fontmake. Wouldn't this PR invert it? I'm seeing

if int(self.font.appVersion) >= 3168:
    return inverted

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are going to love this.

Glyphs now stores user->design for new fonts, but it "upgrades" old fonts by simply inverting them. Take your new font, manually edit "appVersion" to (say) 3110, open it in Glyphs and save it again - the mappings will be flipped.

So I guess this means we can't trust the app version number, and we have to probe against the instance mapping.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have went with my suggestion (back in #745) and made glyphsLib match (old) GlyphsApp behavior and fixed the few fonts that would have been broken by this. Now it is even a bigger mess...

Copy link
Collaborator Author

@simoncozens simoncozens Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't deny that it's stupid and wrong and there may have been questionable choices on both sides, but in 95% of cases the interpretation is completely obvious. Do you have a weight uservalue of 7? You did something wrong. Did all the master locations suddenly fall outside of the design space and you have no valid masters any more? You got the axis mappings backwards. Compare against the instance locations, because we know which way round they are, and if the ranges are wildly different, switch it. It's a mess, but it's not a hard mess to clean up.

if self.font.format_version != 3:
# Glyphs wasn't using this custom parameter, only we were
return mapping
# Let's get heuristical
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm i'm a bit skeptical we need to go "heuristical".. Can't we simply check the version and do accordingly? If the source file doesn't match the interpretation for the version it was saved with, we can say it's not our problem

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there were some fonts that would set it "wrong" in Glyphs to make it work with fontMake.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about fixing the actual sources instead?

if min(mapping.keys()) == min(mapping.values()) or max(mapping.keys()) == max(
mapping.values()
):
# No way of knowing
logger.warning(
"Axis %s: The Axis Mappings custom parameter is ambiguous, "
"assuming it is in userspace->designspace format. Please check the mapping "
"and save in a recent version of Glyphs to avoid this warning."
)
return mapping
if min(mapping.keys()) == min(instance_mapping.keys()) and max(
mapping.keys()
) == max(instance_mapping.keys()):
# Looks like userspace->designspace
return mapping
if min(mapping.keys()) == min(instance_mapping.values()) and max(
mapping.values()
) == max(instance_mapping.keys()):
# Looks like designspace->userspace
return inverted
# No idea
logger.warning(
"Axis %s: The Axis Mappings custom parameter is ambiguous, "
"assuming it is in userspace->designspace format. Please check the mapping "
"and save in a recent version of Glyphs to avoid this warning."
)
return mapping


def to_glyphs_axes(self):
axes_parameter = []
for axis in self.designspace.axes:
Expand Down
44 changes: 44 additions & 0 deletions tests/builder/axes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,47 @@ def test_virtual_masters_extend_min_max_for_unmapped_axis(ufo_module, datadir):
assert [
cp.value for cp in font2.customParameters if cp.name == "Virtual Master"
] == virtual_masters


def test_axis_mappings_different_interpretations(ufo_module, datadir):
# https://github.com/googlefonts/glyphsLib/issues/859
font = GSFont(datadir.join("gf", "Rubik.glyphs")) # Saved under Glyphs 3079
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this Rubik.glyphs seems to already have a correct user->design mapping even if it was saved with an old version:

https://github.com/googlefonts/glyphsLib/blob/1cb4fc5ae2cf385df95d2b7768e7ab4eb60a5ac3/tests/data/gf/Rubik.glyphs#L49-L59

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, because it was a format 2 file.

expected_map = [
(300.0, 60),
(400.0, 90),
(500.0, 125),
(600.0, 142),
(700.0, 160),
(800.0, 190),
(900.0, 220),
]
assert "Axis Mappings" in font.customParameters
ds = to_designspace(font, ufo_module=ufo_module)
assert ds.axes[0].minimum == 300
assert ds.axes[0].maximum == 900
assert ds.axes[0].map == expected_map

font.format_version = 3 # Ambiguous, work out interpretation hueristically
ds = to_designspace(font, ufo_module=ufo_module)
assert ds.axes[0].minimum == 300
assert ds.axes[0].maximum == 900
assert ds.axes[0].map == expected_map

# Pretend we saved it under a new version
font.appVersion = 3217
font.customParameters["Axis Mappings"] = {
"wght": {
60: 300,
90: 400,
125: 500,
142: 600,
160: 700,
190: 800,
220: 900,
}
}

ds = to_designspace(font, ufo_module=ufo_module)
assert ds.axes[0].minimum == 300
assert ds.axes[0].maximum == 900
assert ds.axes[0].map == expected_map
Loading