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

[variable features] [mark fea writer] Instead of skipping a glyph altogether, only skip missing source layers #840

Merged
merged 3 commits into from
May 17, 2024

Conversation

justvanrossum
Copy link
Contributor

Instead of skipping a glyph altogether, only skip missing source layers, so we can still build meaningful variable anchors.

Situation:

  • a set of ufo's is sparse (at least the default ufo contains all glyphs)
  • some ufo sources contain a glyph with anchors (for the mark feature), but others don't

Currently, I get a failure in this situation:

Traceback (most recent call last):
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/fontmake/font_project.py", line 1208, in run_from_designspace
    self._run_from_designspace_interpolatable(
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/fontmake/font_project.py", line 1312, in _run_from_designspace_interpolatable
    self.build_variable_fonts(
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/fontmake/font_project.py", line 450, in build_variable_fonts
    fonts = ufo2ft.compileVariableTTFs(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/__init__.py", line 172, in compileVariableTTFs
    return VariableTTFsCompiler(**kwargs).compile_variable(designSpaceDoc)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/_compilers/baseCompiler.py", line 423, in compile_variable
    self.compile_all_variable_features(
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/_compilers/baseCompiler.py", line 456, in compile_all_variable_features
    self.compile_variable_features(ufoDoc, ttFont, defaultGlyphset)
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/_compilers/baseCompiler.py", line 464, in compile_variable_features
    featureCompiler.compile()
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/featureCompiler.py", line 148, in compile
    self.setupFeatures()
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/featureCompiler.py", line 439, in setupFeatures
    writer.write(self.designspace, featureFile, compiler=self)
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/featureWriters/baseFeatureWriter.py", line 146, in write
    self.setContext(font, feaFile, compiler=compiler)
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/featureWriters/markFeatureWriter.py", line 325, in setContext
    ctx.anchorLists = self._getAnchorLists()
                      ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/just/code/git/BlackFoundry/fontra/venv/lib/python3.12/site-packages/ufo2ft/featureWriters/markFeatureWriter.py", line 359, in _getAnchorLists
    x, y = self._getAnchor(glyphName, anchorName, anchor=anchor)
    ^^^^
TypeError: cannot unpack non-iterable NoneType object

This is because _getAnchor() returns None for such a glyph in the self.context.isVariable code path.

However, it is trivial to make this work by simply skipping the sources that do not contain the glyph. That's what this PR does.

…rs, so we can still build meaningful variable anchors
@justvanrossum
Copy link
Contributor Author

@anthrotype, what do you think?

@anthrotype
Copy link
Member

it makes sense, however I think _getAnchorLists should still handle the possibility that _getAnchor returns None, I see there are other places where it does return None

@justvanrossum
Copy link
Contributor Author

Something like this?

diff --git a/Lib/ufo2ft/featureWriters/markFeatureWriter.py b/Lib/ufo2ft/featureWriters/markFeatureWriter.py
index 9a79bb4..427b752 100644
--- a/Lib/ufo2ft/featureWriters/markFeatureWriter.py
+++ b/Lib/ufo2ft/featureWriters/markFeatureWriter.py
@@ -356,9 +356,11 @@ class MarkFeatureWriter(BaseFeatureWriter):
                     self.log.warning(
                         "duplicate anchor '%s' in glyph '%s'", anchorName, glyphName
                     )
-                x, y = self._getAnchor(glyphName, anchorName, anchor=anchor)
-                a = self.NamedAnchor(name=anchorName, x=x, y=y)
-                anchorDict[anchorName] = a
+                anchorCoord = self._getAnchor(glyphName, anchorName, anchor=anchor)
+                if anchorCoord is not None:
+                    x, y = anchorCoord
+                    a = self.NamedAnchor(name=anchorName, x=x, y=y)
+                    anchorDict[anchorName] = a
             if anchorDict:
                 result[glyphName] = list(anchorDict.values())
         return result

I can gladly add that to this PR, but I don't really have means to properly test it.

@anthrotype
Copy link
Member

anthrotype commented May 17, 2024

actually I think the None never triggers. The way _getAnchor gets called from _getAnchorLists makes it impossible for None to be returned; in a variable context, the default source will contain a glyphName with that anchorName because caller got them from itself; and in non-variable context, the anchor parameter is not None (unused in the variable context branch; unsure where it ever is None to be honest).

Maybe @simoncozens could also take a look, I think he wrote that code.

It's kind of strange that this code was not originally written with a continue like you propose, instead of this return None, makes me think it was deliberate for some reasons.

Adding the continue will essentially allow "sparse" anchor definitions, which aligns with the intent of the sparse glyph layers

@anthrotype
Copy link
Member

I don't really have means to properly test it

ideally we should add a test like the ones in tests/featureWriters/markFeatureWriter_test.py

@anthrotype
Copy link
Member

anthrotype commented May 17, 2024

Instead of skipping...

it's more like "crashing" -- which I agree is not ideal. I wonder why we haven't found this early, must be a very common situation.

Ah.. I see! You are using whole UFOs (their default foreground layer) as sparse sources, not additional sparse layers within a
UFO, which I believe ufo2ft treat differently for OTL feature generation (the sparse layers are assumed to not contribute feature any features of their own, whereas whole UFO do come with a features.fea of their own)

@justvanrossum
Copy link
Contributor Author

You are using whole UFOs (their default foreground layer) as sparse sources

Indeed. Which is a very natural thing to do in my context, and I think it would be nice if this were better supported. Especially in cases that involve a two-line fix :)

My context: merging several projects, but not all support support the same variation axes. For example, one subproject supports opsz, and has (non-sparse) masters for that, but that leaves the other subproject's glyphs "sparse" compared to the whole system. But I still need to build its mark feature.

@simoncozens
Copy link
Contributor

I think the PR as it is would work fine.

I've never quite understood why we use layers for sparse masters; we end up having to associate a sparse master with an arbitrary non-sparse master. It doesn't make sense to me that e.g. "wght=450" is "part" of Regular. The plan in babelfont is just to promote them to masters and deal with it.

@anthrotype
Copy link
Member

I've never quite understood why we use layers for sparse masters; we end up having to associate a sparse master with an arbitrary non-sparse master. It doesn't make sense to me that e.g. "wght=450" is "part" of Regular.

It comes from Glyphs.app, I think, where intermediate (sparse) layers are associated with some "master" layer.

I'm in favour of better support for sparse UFO as sources. We can merge this PR as soon as we have a little test.
I see variable feature writers are tested all in here:
https://github.com/googlefonts/ufo2ft/blob/main/tests/featureWriters/variableFeatureWriter_test.py

maybe modify the TestVarfea.designspace or the source UFOs it references to add sparse UFO or make one of the existing one sparse, put some anchors and voila

@justvanrossum
Copy link
Contributor Author

maybe modify the TestVarfea.designspace or the source UFOs it references to add sparse UFO or make one of the existing one sparse, put some anchors and voila

Done. The test indeed confirms the effectiveness of the fix: without it we observe the crash.

Although the result of my added glyphs is not a variable mark, I think it still proves the fix is correct.

@justvanrossum
Copy link
Contributor Author

(Apologies for the irrelevant fontinfo.plist change, I can roll that back if you want.)

@anthrotype
Copy link
Member

it's ok
let's merge!

@anthrotype anthrotype merged commit bb3b2cf into googlefonts:main May 17, 2024
7 checks passed
@justvanrossum justvanrossum deleted the better-sparse-masters branch May 17, 2024 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants