Batch Changing Glyph Layer in Edit View Controller

Hi,

I am new to python and scripting in Glyphs. Here’s the effect I am trying to accomplish: for within the selected text in edit view, set the layer of all instances of a glyph to a specific layer. I noticed myself doing this quite often, as I wanted to compare different versions of a glyph in a string of text—but you will have to do it manually with each instance.

I looked through the documentation, and couldn’t find anything about setting a layer of a glyph to be the active layer in the edit view. I tried to come up with a solution where I remove all instances of the glyph, then re-insert it with the intended layer. However, I couldn’t find a way to insert a glyph at a specific index in the edit view. “Appending” at the end seems to be the only way to add text to the edit view, and I don’t seem to be able to “insert” at a desired index.

Is there any way to achieve this?

You can assign a list of layers to the edit view: Glyphs.app Python Scripting API Documentation — Glyphs.app Python Scripting API 3.0 documentation

if you like to set single layers, you can use this code:

masterId = Font.masters[1].id
range = (1,1)
textStorage = Font.currentTab.graphicView().textStorage()
textStorage.willChangeValueForKey_("text")
textStorage.text().addAttributes_range_({"GSLayerIdAttrib": masterId}, range)
textStorage.didChangeValueForKey_("text")
1 Like

Thank you, I will be trying to work out a solution based on these directions.

I wrote a script that works by setting all layers in the current tab to the desired layers. It worked well when there’s a small amount of text, but when I tested it on a tab with a lot more text it took a significantly large amount of time, even though the amount of glyphs in view are similar.

Here’s a similar amount of snippet that replaces the layer of all instances of ‘a’ to the 2nd one in the layer index:

font = Glyphs.font
tab = font.currentTab
layers = tab.layers

newLayers = []
selectedGlyph = 'a'

for l in layers:
	if l.parent.name == selectedGlyph:
		l = font.glyphs[selectedGlyph].layers[2]
	newLayers.append(l)

tab.layers = list(newLayers)

It works correctly, but the app often comes to a stand-still for a while, which I assume is due to the amount redrawing needed. Is there a way to circumvent this issue? Since deleting and pasting a large amount of text in edit tab are usually quite fast, I assume I am doing something incorrectly here.

I also did not fully understand the snippet you provided, as I couldn’t find the detailed documentation on GSTextStorage. Maybe that could be a better solution.

Thanks!

How many glyphs do you have in that tab? I tried with a 1000 glyphs and that was instantaneous. And 2000 took something like half a second.

I’m not sure what this two lines are supposed to do, but you can write them a tiny bit simpler:

	if l.parent.name == selectedGlyph:
		l = l.parent.layers[2]

That seems odd. I tried out the script on multiple computers and it took way longer than you described. I used one of the proofing text from Hoefler and Co, which is 1671 characters including spaces. Here are the results when running the above script:

  • 8 Core M1 MacBook Pro, Monterey 12.1 = about 15 seconds
  • 8 Core i7 custom desktop Mac, Catalina 10.15.7 = about 25 seconds
  • 2 Core Early 2015 MacBook Pro, Catalina 10.15.7 = about 40 seconds

The Glyphs version on all these machines is 3.04, and uses Python 3.9.1 for Glyphs. I ran these on a fresh glyphs file derived from an Akkurat otf, with some duplicate layers in letter a for the test. I only did so to make sure there wasn’t any problems with my own font files.

For your question, those two lines are there so that only the intended glyph is replaced (“if the glyph name is ‘a’, insert its 2nd layer instead; otherwise, insert the currently selected layer”). Though you are right that it could be simplified.

I don’t know why it ran so much faster on your end, please let me know if you have any insights into that. Thanks!

Consider using the Text Preview when working with large amount of text that you want to preview:

I can’t reproduce this. Maybe you have some other plugins installed that cause this? So can you start Glyphs while holding down the Option and Shift key?

Could you make a spindump of the hang? That can be done with the Activity Monitor app.

I restarted Glyphs with plugins disabled and ran the script on the M1 Mac, it took approximately the same amount of time with other settings unchanged.

Here is the log, I hope I did it correctly.
Spindump.txt (4.0 MB)

Can you send me your full script? And the .glyphs file you are using (with the tab containing the text).

And do you have features active (with this button in the lower left)?

I found the problem. This was unexpected to me since I am not familiar with Python. Turns out I was using the following script by mistake, which caused the speed problem:

font = Glyphs.font
tab = font.currentTab
layers = tab.layers

newLayers = []
selectedGlyph = 'a'

# instead of using `for l in layers`
for i in range(len(layers)):
	l = layers[i]
	if l.parent.name == selectedGlyph:
		l = l.parent.layers[0]
	newLayers.append(l)

tab.layers = list(newLayers)

Which is different than the one posted above. I noticed the discrepancy and made the following changes:

#from:
for i in range(len(layers)):
	l = layers[i]

#to:
for l in layers:

And the script runs extremely quickly, like you reported. I did not expect the first loop to take so much longer, as I see it being used in other Python examples. Is this expected behaviors? Besides that, I found the problem and it has been solved. Thank you for your help!

In the #from sample, you are assigning a mutable object to a variable. This has some performance costs because memory allocation has to be done in the background. If you have many layers, that may become noticeable.

In the #to sample, you may think you are doing the same thing, but you are really stepping through an iterator object. Python creates them on the fly, they are optimized for iterating, and hence efficient performance-wise. Only what is needed is loaded into memory, and only when it is actually needed.

What you cannot do, however, is change an iterable while you are iterating over it, e.g. remove items from a list. In that case you would have to resort to iterating over index numbers, best backwards. This is explained in detail in the last Scripting tutorial, IIRC.

Converting to a list is not necessary because newLayers already is a list.

I had a look at the code and found why it takes so long. But I can’t change it right now.

If you need the access by index, you need to change this line

layers = tab.layers

to this:

layers = list(tab.layers)

Update: I made a script here that have UI options that help streamline the process. Hopefully it runs smoothly for those who decides to give it a try! I have incorporated it into my process and its working well so far.