How might I trigger "Instance as master" from the API?


#1

Hello, I’m hoping to make a master from an extrapolated instance, via script.

Question 1:

So, I’d hope to replicate the functionality discussed and revealed in Instance as master!, but purely from a script. Is there an existing method, or might I have to do something like generating the instance, then importing this as a master? I’m not finding it in the API doc.

Question 2:

Does the generate() method not work with extrapolated instances, or have I made a dumb mistake here? I’m following the example in the docs, but when I use…

exportFolder = '/Users/stephennixon/Desktop'

for instance in Glyphs.font.instances:
  # my extrapolated instance is at 1350.0
  if instance.weightValue == 1350.0:
    # this works fine	
    print instance.name
    # this line fails (see below)
    instance.generate(FontPath = exportFolder)
		
Glyphs.showNotification('Export fonts', 'The export of %s was successful.' % (Glyphs.font.familyName))

…I get the following error:

Traceback (most recent call last):
  File "<string>", line 6, in <module>
  File "GlyphsApp/GlyphsApp/__init__.py", line 3728, in __Instance_Export__
TypeError: 'NSKVONotifying_GSFont' object is not callable

Just in case it’s useful

I’m in Glyphs Version 2.5.1 (1141), on macOS 10.14.


Thanks for any insight on either or both questions!


Accessing layer and associatedMasterId in remote script / Core API
#2

If I recall correctly, I believe the issue you ran into in Question 2 may have been fixed in one of the 2.5.2 cutting edge releases. fyi.


#3

Jeff is right.

And for the first question. That is very easy with a script. Make a new instance, put in the name and the interpolation values and add it to the font.


#4

Thanks for the replies! Glad that issue was fixed. :slight_smile:

As to the first question … I’m trying to follow your directions, @GeorgSeifert, but I’m not quite finding the proper way to do it in the API Doc. I have a couple of follow-up questions.

My main challenge comes down to setting attributes in instances and masters – I’m probably missing something simple, or potentially this might be a bug. Please let me know if you spot what’s happening!

How do I set values of a new instance?

I’ve tried to copy from a few examples, but setting attributes doesn’t yet seem to work for me.

I am able to make new instances, by working from the basis of an example of adding a new layer I’ve tried:

newInstance = GSInstance()
f.instances.append(newInstance)

I’ve also tried copying the example of adding a new glyph:

newInstance = f.instances[1].copy()
f.instances.append(newInstance)

These both work! However, if I try to set any attribute values, I run into a problem. When I try to copy lines 217–234 of a MekkaBlue script that inserts Instances

newInstance = GSInstance()
newInstance.name = "newRegular"
newInstance.weight = 435
f.instances.append(newInstance)
newInstance.updateInterpolationValues()

…I get:

AttributeError: 'GSInstance' object attribute 'name' is read-only

How do I create a new master from an instance? Alternatively, how do I create a new master and set & reinterpolate to its weight?

My assumption is that I could create a new master from an instance with something like:

newMaster = f.instances[0].copy()
f.masters.append(newMaster)

However, this also gives me an error:

Traceback (most recent call last):
  File "add-new-master-at-interp-weight-WIP.py", line 29, in <module>
    f.masters.append(newMaster)
  File "GlyphsApp/GlyphsApp/__init__.py", line 1491, in append
ValueError: NSInvalidArgumentException - -[GSInstance id]: unrecognized selector sent to instance 0x60000158cea0

Alternatively, I can copy a master and copy glyphs to the new layer:

newMaster = f.masters[0].copy()
f.masters.append(newMaster)

for glyph in f.glyphs:
    glyph.layers[-1] = glyph.layers[0].copy()

I assume there is some way to set the master weight in order to go through layers to reinterpolate as seems to be advised in this thread, like:

newMaster = f.masters[0].copy()
f.masters.append(newMaster)

f.masters[-1].weight = 435

for glyph in f.glyphs:
    glyph.layers[-1] = glyph.layers[0].copy()
    glyph.layers[0].reinterpolate()

But again, when I try to do so, I get a similar error to the ones above:

AttributeError: 'GSFontMaster' object attribute 'weight' is read-only

Thanks for any insight!


#5

Update:

A colleague helped me see one obvious solution: I was using f.masters[-1].weight = 435 where I needed to use f.masters.weightValue = 435.

I’m still having some of the other issues, however, so more help is still appreciated! I’ll post an update if I can figure out more. :slight_smile:


#6

I can make a new master with a weightValue with:

f = Glyphs.font
newMaster = f.masters[0].copy()
f.masters.append(newMaster)

…then separately, I can copy some of the glyphs with:

f = Glyphs.font

import copy

for glyph in f.glyphs:
    glyph.layers[-1] = glyph.layers[0].copy()
    if len(glyph.layers[0].components) > 0:
        glyph.layers[-1].components = copy.copy(glyph.layers[0].components)
    glyph.layers[-1].reinterpolate()

But somehow, these don’t work the same when run in the same script. Also, glyphs stop copying at l, don’t copy components, and I get this error:

Traceback (most recent call last):
  File "<string>", line 6, in <module>
  File "GlyphsApp/GlyphsApp/__init__.py", line 2002, in __setitem__
AttributeError: 'NoneType' object has no attribute 'id'

I may come back to this later. Thanks for any pointers in the meantime, if you see how I can make this work better!


"Instance as master" through Core API?
#7

The second line doesn’t make any sense. The masters property can’t handle .weight.

This is the code you are looking for:

instanceFont = Font.instances[2].interpolatedFont
Font.masters.append(instanceFont.masters[0])
newMasterID = instanceFont.masters[0].id
for glyph in Font.glyphs:
	instanceGlyph = instanceFont.glyphs[glyph.name]
	glyph.layers[newMasterID] = instanceGlyph.layers[newMasterID]
Font.kerning[newMasterID] = instanceFont.kerning[newMasterID]

#8

Amazing, thanks so much for the specific code, Georg!

If it helps anyone else, here’s the full, working code I’m using to adjust my master weight:

#MenuTitle: Adjust Master weight
# -*- coding: utf-8 -*-
__doc__="""
Add new master at interpolated position, then delete existing master. Good for subtle adjustments in a build process. Set variables in script to configure.
"""
#MenuTitle: Adjust Master weight
# -*- coding: utf-8 -*-
__doc__="""
Add new master at interpolated position, then delete existing master. Good for subtle adjustments in a build process. Set variables in script to configure.
"""

### set vars ##########################

oldMasterWeightValue = 400.0
newMasterWeightValue = 440.0

#######################################

f = Glyphs.font

# create new instance
newInstance = GSInstance()
f.instances.append(newInstance)

# set new instance weight value to variable from above
newInstance.weightValue = newMasterWeightValue

# make an interpolated font of the new instance
instanceFont = f.instances[-1].interpolatedFont
# add the master of the interpolated font to the masters
f.masters.append(instanceFont.masters[0])
# get the id from the master of the interpolated font
newMasterID = instanceFont.masters[0].id

for glyph in f.glyphs:
    # make variable for glyph of interpolated font
	instanceGlyph = instanceFont.glyphs[glyph.name]
    # bring glyph data into glyph of new master
	glyph.layers[newMasterID] = instanceGlyph.layers[newMasterID]

# bring kerning in from interpolated font
f.kerning[newMasterID] = instanceFont.kerning[newMasterID]

for i, instance in enumerate(f.instances):
    # delete generated instance
    if instance.weightValue == newMasterWeightValue:
        print("delete " + str(instance))
        del f.instances[i]

for i, master in enumerate(f.masters):
    # delete old master
    if master.weightValue == oldMasterWeightValue:
        print("delete " + str(master))
        del f.masters[i]
    # set new master value as old master value (round to nearest integer to match)
    if round(master.weightValue) == round(newMasterWeightValue):
        print("update weight value of " + str(master))
        f.masters[i].weightValue = oldMasterWeightValue

UPDATE, Nov 21, 11:42: When I prevent the NoneType error, it prevents the new master from updating its weightValue. I’m not quite sure why that warning is being triggered, but I’ve updated the above script to the version that works as expected, albeit with a warning:

Traceback (most recent call last):
  File "add-new-master-at-interp-weight.py", line 53, in <module>
    if round(master.weightValue) == round(newMasterWeightValue):
AttributeError: 'NoneType' object has no attribute 'weightValue'

#9

That happens when the master is deleted. Add a continue after del f.masters[i].


#10

Hmmm. Generally not a good idea to delete an item of a list while you are iterating over it. It messes up the index number.

What I do to avoid this, is to iterate backwards over the range, like this:

for i in range(len(items)-1, -1, -1):
	print items[i]

Or like this, same thing:

for i in range(len(items))[::-1]:
	print items[i]

Then you can delete items without messing up your iterator variable.


#11

The second might be slower when you have a big list as it makes a new list.