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

Split the UFO builder into 1 file per "feature" #235

Merged
merged 6 commits into from
Oct 23, 2017

Conversation

belluzj
Copy link
Collaborator

@belluzj belluzj commented Oct 17, 2017

This is a work-in-progress to implement the round-trip between Glyphs and UFO+designspace.

Proposed plan:

  1. Split the current builder (Glyphs->UFO) into small files, one per "feature" (anchors, guidelines, kerning...)
  2. For each feature, implement the other side (UFO->Glyphs) in the same file
  3. Add features related to the designspace document (instances and glyph layers named with brackets https://glyphsapp.com/tutorials/alternating-glyph-shapes and other related tricks)
  4. Add smart components, probably by instantiating each of their uses as a normal UFO components + metadata to recover the "smartness" in the other direction.

After reading the code and working on step 1, I have a few questions:

  1. In a few places, the objects from Glyphs are "parsed"/decomposed into tuples, and then the tuples are used to fill in the UFO objects. I believe that this is a leftover from the previous builder that used raw dictionaries instead of nice objects, and that the builder should now use the objects directly, and get rid of the intermediary tuples?

  2. It looks like the two modules "interpolation.py" and "util.py" are mostly related to the builder. Maybe they should be moved into the "builder" module? (for interpolation: I say that because the builder should be able to read/fill in a designspace document to link the produced UFOs together)

  3. For testing and for the builder's "robustness", I see three levels of round-trip:

    1. Glyphs->UFO->Glyphs without user intervention. If that works, it means that we have persisted all the info from Glyphs into the UFOs
    2. Glyphs->UFO, user intervention, UFO->Glyphs, user intervention -> etc. I suppose that it is what most users of the lib will want to do? The goals are:
      • to recover as much as possible of the initial Glyphs "smartness" while still taking into account the user modifications. E.g. the user modifies a UFO component that was previously a smart component: maybe we cannot bring that one back as a smart component, but all the other untouched instances of smart components should come back as "smart" in Glyphs.
      • to keep special/custom UFO data from other UFO editing tools that will be used in the middle
      • to minimize the textual diffs both in the various versions of the .glyphs files and of the UFO files
      • to guard against the various UFOs getting out of sync, e.g. the user modifies the family name in one of the UFOs but not the others = what do we do when converting back to glyphs?
    3. Random UFO from anywhere, any tool -> Glyphs. Is anyone going to want that? In that case, we won't be able to introduce any "smartness"? Also, could we encounter exotic stuff that cannot be modeled in Glyphs? Is this part of the goals of this library? Or is there already a UFO import tool in Glyphs? (maybe this third case is not very different from the second one)
  4. I saw that some Glyphs data is stored as text in the features.fea file. In order to import it back, I guess that I should use a feature file parser. Can I use fonttools.feaLib for that?

@schriftgestalt
Copy link
Collaborator

Glyphs can read and write UFOs. So if someone has a UFO and needs to convert it to a .glyphs file, he most likely has Glyphs around and can just open the UFO.

One use/test case for the Glyphs<->UFO code could be to use it from within Glyphs to read/write UFO/designspace files. That would test the API compatibility/modularity quite well.

@schriftgestalt
Copy link
Collaborator

If you find ways to store more smart stuff in UFOs, I might use the same structure when exporting from Glyphs. I’m interested how you implement this. I’m not familiar enough with how private data is supposed to be stored in UFO to be able to connect it back to the base object. One simple problem is extra info for components, like the alignment setting. How to decide that the data needs to be used or not when the base object has changes.

@anthrotype
Copy link
Member

anthrotype commented Oct 18, 2017

Thanks Jany for the detailed plan you proposed.
I think the step 1. (splitting the current glyphs->ufo builder into smaller modules, and leaving the reverse to_glyphs_* functions as not-yet-implemented stubs) should be the object of an initial PR (e.g. this one), while the rest of the plan can be implemented in another PR. This way, we could merge this and release 2.0 now-ish, and then can keep working on the rest of the UFOs->glyphs builder, and aim to ship that in the following release.
There are lots of changes already on master, and I think to fully develop this ufos->glyphs builder it may take some time, and I'd like to be able to tag a release before that.

In a few places, the objects from Glyphs are "parsed"/decomposed into tuples, and then the tuples are used to fill in the UFO objects...

we can get rid of that, and use Glyphs objects directly

"interpolation.py" and "util.py" are mostly related to the builder... should be moved into the "builder"

ok

the user modifies the family name in one of the UFOs but not the others = what do we do when converting back

I guess it's ok to nicely fail in these cases

exotic stuff... random stuff that cannot be modeled in Glyphs

We do what we can do. Let's start with the easy part and see how far we get :)

Thanks!

@belluzj
Copy link
Collaborator Author

belluzj commented Oct 18, 2017

Thanks for your feedback. I pushed my latest code even though it has conflicts and probably does not run, just to show that I have further split the builder.

Tomorrow I will cleanup this PR so that the only diffs are the existing code moving around and the stub functions, and I will submit the new code for UFO->Glyphs in another PR.

@moyogo
Copy link
Collaborator

moyogo commented Oct 18, 2017

Sounds good!

@belluzj belluzj force-pushed the ufo_roundtrip branch 2 times, most recently from 9cbfb9c to 57780e1 Compare October 19, 2017 12:14
@belluzj belluzj changed the title WIP UFO roundtrip Split the UFO builder into 1 file per "feature" Oct 19, 2017
@belluzj belluzj mentioned this pull request Oct 19, 2017
@belluzj
Copy link
Collaborator Author

belluzj commented Oct 19, 2017

@anthrotype @moyogo I have moved the new code in another PR, this one is ready to be reviewed

Copy link
Member

@anthrotype anthrotype left a comment

Choose a reason for hiding this comment

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

I think True and 1 are equal in 🐍.

@belluzj
Copy link
Collaborator Author

belluzj commented Oct 20, 2017

Ugh, I need to un-learn Ruby

@belluzj
Copy link
Collaborator Author

belluzj commented Oct 20, 2017

I'm glad you pointed that out, because the test I just added was really moot


# Existing
from .kerning import add_glyph_to_groups, add_groups_to_ufo, load_kerning
from .anchors import propagate_font_anchors, propagate_glyph_anchors
Copy link
Member

Choose a reason for hiding this comment

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

propagate_glyph_anchors is imported but not used here. I think it's a private function only used by propagate_font_anchros.

if varfont_origin:
instance_data[varfont_origin_key] = varfont_origin
if debug:
return clear_data(font)
Copy link
Member

@anthrotype anthrotype Oct 22, 2017

Choose a reason for hiding this comment

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

I think we need to remove this debug argument as well as this clear_data function.
They no longer work with the class-based interface, as clear_data expects a dict or a list from which values are popped and the ones that are left are thus "unused".

for anchor in anchors:
x, y = anchor.position
anchor_dict = {'name': anchor.name, 'x': x, 'y': y}
glyph.appendAnchor(glyph.anchorClass(anchorDict=anchor_dict))
Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure defcon's appendAnchor also accepts a raw dict directly. It will automatically instantiate it to the respective anchor class for you.

unicode_literals)


def to_ufo_blue_values(_context, ufo, master):
Copy link
Member

Choose a reason for hiding this comment

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

why _context with an underscore? is it because it is not used in this function? I think it's fine to always call it context.

@anthrotype
Copy link
Member

anthrotype commented Oct 22, 2017

for the merge conflict, I think it should be fine to git rm Lib/glyphsLib/builder/ufo.py file, as the only change occurred with #249 was to cast appVersion to int, which was already the case in this PR.

self.defcon = defcon

self.ufos = OrderedDict()
"""
Copy link
Member

Choose a reason for hiding this comment

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

any reasons why here you use """ multi-line string literals """ instead of simple # inline comment?

Copy link
Collaborator Author

@belluzj belluzj Oct 23, 2017

Choose a reason for hiding this comment

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

Because I read in a PEP that it was a way to document class attributes: https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring

String literals occurring immediately after a simple assignment at the top level of a module, class, or init method are called "attribute docstrings".

Is there another way?

Copy link
Member

Choose a reason for hiding this comment

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

Interesting, I didn't know that...
However, if you try to do help(...) on that class, it only shows the first multi-line string.

Copy link
Member

Choose a reason for hiding this comment

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

maybe auto docs generators like sphinx will be able to retrieve those. I'm fine if we leave them. We should polish the documentation at some point.

from glyphsLib.util import bin_to_int_list
from .filters import parse_glyphs_filter
from .constants import GLYPHS_PREFIX, PUBLIC_PREFIX, CODEPAGE_RANGES, \
UFO2FT_FILTERS_KEY
Copy link
Member

Choose a reason for hiding this comment

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

let's stick to either parentheses or backslashes for multi-line import statements. I prefer the former

from __future__ import (print_function, division, absolute_import,
unicode_literals)

from glyphsLib.types import point
Copy link
Member

Choose a reason for hiding this comment

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

point is imported but unused

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will remove it.

It's here because it was used in the WIP implementation of to_glyphs_guidelines that I replaced with pass. Is it still the plan to have this MR merged quickly with stub functions for everything UFO -> Glyphs?

Copy link
Member

Choose a reason for hiding this comment

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

Is it still the plan to have this MR merged quickly with stub functions for everything UFO -> Glyphs?

yes!

"""Builder context for Glyphs to UFO + designspace."""

def __init__(self, font, defcon):
"""Create a context for the Glyphs to UFO + designspace builder.
Copy link
Member

@anthrotype anthrotype Oct 22, 2017

Choose a reason for hiding this comment

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

I was thinking, instead of passing around a "context" object as argument to the to_ufo_* and to_glyphs_* functions, we could define two classes e.g. UFOBuilder and GlyphsBuilder that have those functions as instance methods and use self as the context.

We could still keep this modularized structure with the builder functions in their respective modules, and then when we define the builder classes, we would set these functions as class attributes, and they will be bound to a builder instance just like regular methods.

WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Using classes makes sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I will do that

@belluzj
Copy link
Collaborator Author

belluzj commented Oct 23, 2017

I fixed all the comments and rebased onto master.

class UFOBuilder(object):
"""Builder for Glyphs to UFO + designspace."""

def __init__(self, font, defcon, family_name=None, propagate_anchors=True):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe ufo_module instead of defcon. And default to defcon when None.



@property
def master_ufos(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just masters instead of master_ufos, then it will be the same as instances and designspace.



# Implementation is spit into one file per feature
from .anchors import to_ufo_propagate_font_anchors, to_ufo_glyph_anchors
Copy link
Member

Choose a reason for hiding this comment

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

I was also thinking.. perhaps instead of naming the methods UFOBuilder.to_ufo_*, and GlyphsBuilder.to_glyphs_*, which seems a bit redundant, we could replace the to_ufo_* and to_glyphs_* prefixes with a generic build_*, using the as keyword in the import statements.
E.g. inside UFOBuilder scope, you would do from .anchors import to_ufo_glyph_anchors as build_glyph_anchors, whereas inside GlyphsBuilder, from .anchors import to_glyphs_glyph_anchors as build_glyph_anchors.
WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't mind the redundant naming because I think it makes things very clear inside the code: those names will only be used inside the implementations of the builders, with self. in front of it, and if you're looking at a file and stumble upon self.build_anchors, you will have to look around to find out in which direction the current function is going, whereas here it will always be very clear. Also having two names for the same function sounds confusing.

Copy link
Member

Choose a reason for hiding this comment

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

ok, let's keep it like that

return result


def to_glyphs(ufos, designspace=None, classes=classes):
Copy link
Collaborator

Choose a reason for hiding this comment

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

glyphs_module instead of classes would be clearer, like ufo_module.

Copy link
Member

@anthrotype anthrotype left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@belluzj belluzj merged commit cbc3d7b into googlefonts:master Oct 23, 2017
@belluzj belluzj deleted the ufo_roundtrip branch October 23, 2017 14:56
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.

4 participants