googlefonts / glyphsLib

A bridge from Glyphs source files (.glyphs) to UFOs
Apache License 2.0
178 stars 51 forks source link

Saving to .glyphspackage format? #953

Open arrowtype opened 8 months ago

arrowtype commented 8 months ago

It is awesome that glyphsLib (and fontmake) now support .glyphspackage files, as they are really nice for version control.

However, it looks like so far, glyphsLib only supports reading from .glyphspackage files (thanks to https://github.com/googlefonts/glyphsLib/pull/803), but not writing back to them.

I am hoping to increment the version number of a glyphspackage source during a build process, so I would love it if glyphsLib could write to the new format, too.

As an example, here’s some Python I’m trying to use:

import sys
from glyphsLib import GSFont

glyphsSource = sys.argv[1]
versionMinor = sys.argv[2]

font = GSFont(glyphsSource)

font.versionMinor = int(versionMinor)

font.save(glyphsSource)

Unfortunately, running it currently gives this result:

▶ python source/01-build-scripts/helpers/update-glyphs-source-version.py source/familyname.glyphspackage 2
Traceback (most recent call last):
  File "/Users/stephennixon/type-repos/familyname/source/01-build-scripts/helpers/update-glyphs-source-version.py", line 11, in <module>
    font.save(glyphsSource)
  File "/Users/stephennixon/type-repos/familyname/venv/lib/python3.11/site-packages/glyphsLib/classes.py", line 4533, in save
    with open(path, "w", encoding="utf-8") as fp:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
IsADirectoryError: [Errno 21] Is a directory: 'source/familyname.glyphspackage'

So, partly I am just filing this issue as a feature request, in hopes that saving to glyphspackage might get support soonish.

I do also have two questions:

  1. Have I just missed something, and is this possible already?
  2. If this isn’t a feature yet, realistically, is this likely to be supported soonish? Or, is it a larger scope of work / not really a priority right now? (It’s all good either way, I’m just trying to plan for a couple of projects.)
  3. If it isn’t likely to happen soon, what might be a good way to handle this basic goal (incrementing versions for each build)? I could just use FontTools ttLib or font-v to write versions to font binaries only, or generate UFOs as a step of my build and edit the version number there... but is there another option that I might be missing?

Thanks so much for any insights and/or ideas!

arrowtype commented 8 months ago

Re: question 3... I guess I could save to .glyphs format, then convert that to a glyphspackage. 🤔 I wonder if I’d lose any data that way. There’s one way to find out, I guess!

anthrotype commented 8 months ago

We haven't implemented writing .glyphspackage yet (though it should not be too hard in theory). If you're only interested in changing one field in the global fontinfo.plist just do that, loading the .glyphspackage in glyphsLib is expensive as it loads all the glyphs as well. Use openstep_plist (which glyphsLib itself uses) to load the glyphspackage's fontinfo.plist, change the version and save it back

arrowtype commented 8 months ago

Use openstep_plist (which glyphsLib itself uses) to load the glyphspackage's fontinfo.plist, change the version and save it back

Ah, awesome! I hoped someone might give a smart suggestion like this. Will try this out!

In the long run, yeah, I would love to be able to interact with glyphspackage files more fully via glyphsLib, but for now my need is very simple. Thanks for sharing that insight!

arrowtype commented 8 months ago

In case it helps anyone, here’s the code I ended up with. It seems to work well!

"""
    USAGE:
    python3 <path_to_script>/update-glyphs-source-version.py <path_to_glyphspackage>/familyname.glyphspackage <integer>
"""

import sys
import os
import openstep_plist

glyphsSource = sys.argv[1] # pass this in as first arg
versionMinor = sys.argv[2] # pass this in as second arg

# make path for fontinfo
fontinfoPath = os.path.join(glyphsSource, "fontinfo.plist")

# read the fontinfo.plist
with open(fontinfoPath, "r", encoding="utf-8") as fontinfo:
    data = openstep_plist.load(fontinfo, use_numbers=True)

# update the data from passed-in arg
data['versionMinor'] = int(versionMinor)

# write to the fontinfo.plist
with open(fontinfoPath, "w", encoding="utf-8") as fontinfo:

    # the extra args keep things closer to the formatting from GlyphsApp
    openstep_plist.dump(data, fontinfo, unicode_escape=False, indent=0, single_line_tuples=True)

# print the update
print(f'Updated {data["familyName"]} with version {data["versionMajor"]}.{str(data["versionMinor"]).zfill(3)}')
arrowtype commented 8 months ago

I notice that the one drawback to my code is that it saves the plist with all feature code compressed onto single lines.

Is there a better way to handle that, to avoid meaningless Git commits?

image
anthrotype commented 8 months ago

hm seems like openstep-plist writer always escapes newline characters and writes multi-line strings as a single line. There seems to be no option to pass through literal newlines:

https://github.com/fonttools/openstep-plist/blob/c9acd408ba3a374ba41dd62f595f43fa2e5bfa6f/src/openstep_plist/writer.pyx#L264-L265

I think glyphsLib only uses openstep-plist to parse, but not also to dump. Anyway, apart from the git noise, both representation should be indentical and work the same. Sorry about that

arrowtype commented 8 months ago

Okay, thanks for confirming! I had explored the options, but wasn't getting closer.

I appreciate your help here!

anthrotype commented 8 months ago

If I remember correctly, once upon a time Glyphs.app would write multiline strings (e.g. features) with the newline escaped, even using octal codes \012 if I am not mistaken) then it got changed to write out the newlines as literal characters unescaped. Anyway, openstep-plist will never match Glyphs.app 100% because it does not know about the quirks of the .glyphs format, it just treats everything the same way, cannot write e.g. unicodes one way or custom parameters antother way etc.

schriftgestalt commented 8 months ago

For simple changes like this, you could just read the file as a string and search and replace on it. That would keep the rest of the file unaffected. Only adding new things might be a bit tricky.

arrowtype commented 8 months ago

Ah, yeah, I guess I'm basically doing search and replace to fix things after changing them, so I could simplify it and just add the data the first time in glyphs, then search and replace only that.