UNSOLVED Existing script to scale UPM, plus dimensions, glyphs, components, kerning, etc?
-
I'd like to scale the UPM of a UFO from 1000 to 2000 (or to any arbitrary number).
Of course, I can't just scale the
unitsPerEm
and call it good, because I also need to scale vertical metrics, glyphs, components, kerning, etc.I imagine this is a script that has been written before, so it seems worth checking before I put an hour into it and maybe miss something. Jens Kutilek has one on GitHub, but it's written for RF1.8 and relies on robofab.
Have I missed something in the docs, or something available online? Thanks for any pointers!
-
@ArrowType Feel free to check out my extension Slinky on Mechanic!
-
This may be obvious to folks who read the code, but just as a heads up: the script from 24 Oct 2019 doesn’t scale a lot of attributes in the font info, such as vertical metrics like the typo/hhea ascender, etc.
For some reason that isn’t clear, it also didn’t scale the width of composed glyphs like /eacute, on an earlier run. However, after installing RF 4.5, it did seem to scale all glyph widths.
Overally, just be careful and observant if you run this. It works well for fonts in the early stages of design, but you should be cautious on fonts that are closer to release.
-
Thank you all for sharing this!
Would it make sense to also include Italic Slant Offset to the attributes at the bottom of the script? I remember this caused issues for me once after scaling a font.
-
here is a full simplified example – will be added to the docs soon.
'''Scale a font and what's inside it.''' font = CurrentFont() newUpm = 100 oldUpm = font.info.unitsPerEm factor = newUpm / oldUpm for layer in font.layers: for glyph in layer: for contour in glyph: contour.scaleBy(factor) for anchor in glyph.anchors: anchor.scaleBy(factor) for guideline in glyph.guidelines: guideline.scaleBy(factor) glyph.width *= factor font.kerning.scaleBy(factor) for guideline in font.guidelines: guideline.scaleBy(factor) for attr in ['unitsPerEm', 'descender', 'xHeight', 'capHeight', 'ascender']: oldValue = getattr(font.info, attr) newValue = oldValue * factor setattr(font.info, attr, newValue) font.changed()
@ArrowType thanks for the request!
-
and may just loop over all layers instead of ignoring the default layer:
for layer in font.layers: for glyph in layer: scaleGlyph(g, factor)
in RF3 each layer glyph has his own width and does not inherit it from the default layer (like in RF1)
-
why
flipLayers
?
-
Here's my updated script:
# Change upm # Jens Kutilek 2013-01-02 # # updated for RoboFont 3 by Stephen Nixon / @arrowtype 2019-10-22 from mojo.roboFont import version from mojo.UI import AskString def scaleGlyph(glyph, factor): for contour in glyph: contour.transformBy(factor, 0,0,factor,0,0) for anchor in glyph.anchors: anchor.transformBy(factor, 0,0,factor,0,0) for guideline in glyph.guidelines: guideline.transformBy(factor, 0,0,factor,0,0) # and dont transform components def changeUPM(font, factor, roundCoordinates=True): # glyphs for g in font: scaleGlyph(g, factor) # layers mainLayer = "foreground" for layerName in font.layerOrder: if layerName != mainLayer: for g in font: g.flipLayers(mainLayer, layerName) scaleGlyph(g, factor, scaleWidth=False) g.flipLayers(layerName, mainLayer) # kerning if font.kerning: font.kerning.scale(factor) # vertical dimensions font.info.descender = int(round(font.info.descender * factor)) font.info.xHeight = int(round(font.info.xHeight * factor)) font.info.capHeight = int(round(font.info.capHeight * factor)) font.info.ascender = int(round(font.info.ascender * factor)) # finally set new UPM font.info.unitsPerEm = newUpm font.update() if __name__ == "__main__": from mojo.UI import AskString print("Change Units Per Em") if CurrentFont() is not None: oldUpm = CurrentFont().info.unitsPerEm newUpm = CurrentFont().info.unitsPerEm try: newUpm = int(AskString("New units per em size?", oldUpm)) except: pass if newUpm == oldUpm: print(" Not changing upm size.") else: factor = float(newUpm) / oldUpm print(" Scaling all font measurements by", factor) changeUPM(CurrentFont(), factor) else: print(" Open a font first to change upm, please.") print(" Done.")
-
Awesome, thanks Frederik! Ha, that makes a lot more sense. :)
-
the script does way to much... removing components and placing them back? flipLayers around?
for contour in glyph: contour.transformBy(matrix) for anchor in glyph.anchors: anchor.transformBy(matrix) for guideline in glyph.guidelines: guideline.transformBy(matrix) # and dont transform components
-
Well, I've hastily updated Jens's script for now. 😄
I assume someone has a better version (please post if you do!), but here's a start for the next person that searches on the forum.
# Change upm # Jens Kutilek 2013-01-02 # hastily updated for RoboFont 3 by Stephen Nixon / @arrowtype from mojo.roboFont import version from mojo.UI import AskString def scalePoints(glyph, factor): if version == "1.4": # stupid workaround for bug in RoboFont 1.4 for contour in glyph: for point in contour.points: point.x *= factor point.y *= factor glyph.width *= factor else: glyph *= factor def scaleGlyph(glyph, factor, scaleWidth=True, roundCoordinates=True): if not(scaleWidth): oldWidth = glyph.width if len(glyph.components) == 0: glyph.transformBy((factor,0,0,factor,0,0), origin=(0,0)) glyph.width *= factor if roundCoordinates: glyph.round() else: # save components # this may be a tad too convoluted ... components = [] for i in range(len(glyph.components)): components.append(glyph.components[i]) for c in components: glyph.removeComponent(c) glyph.transformBy((factor,0,0,factor,0,0), origin=(0,0)) glyph.width *= factor if roundCoordinates: glyph.round() # restore components for i in range(len(components)): newOffset = (int(round(components[i].offset[0] * factor)), int(round(components[i].offset[1] * factor))) glyph.appendComponent(components[i].baseGlyph, newOffset, components[i].scale) if not(scaleWidth): # restore width glyph.width = oldWidth def changeUPM(font, factor, roundCoordinates=True): # Glyphs for g in font: scaleGlyph(g, factor) for guide in g.guides: guide.x *= factor guide.y *= factor # Glyph layers mainLayer = "foreground" for layerName in font.layerOrder: if layerName != mainLayer: for g in font: g.flipLayers(mainLayer, layerName) scaleGlyph(g, factor, scaleWidth=False) g.flipLayers(layerName, mainLayer) # Kerning if font.kerning: font.kerning.scale(factor) if roundCoordinates: if not version in ["1.4", "1.5", "1.5.1"]: font.kerning.round(1) else: print("WARNING: kerning values cannot be rounded to integer in this RoboFont version") # TODO: Change positioning feature code? # Vertical dimensions font.info.descender = int(round(font.info.descender * factor)) font.info.xHeight = int(round(font.info.xHeight * factor)) font.info.capHeight = int(round(font.info.capHeight * factor)) font.info.ascender = int(round(font.info.ascender * factor)) # Finally set new UPM font.info.unitsPerEm = newUpm font.update() if __name__ == "__main__": from mojo.UI import AskString print("Change Units Per Em") if CurrentFont() is not None: oldUpm = CurrentFont().info.unitsPerEm newUpm = CurrentFont().info.unitsPerEm try: newUpm = int(AskString("New units per em size?", oldUpm)) except: pass if newUpm == oldUpm: print(" Not changing upm size.") else: factor = float(newUpm) / oldUpm print(" Scaling all font measurements by", factor) changeUPM(CurrentFont(), factor) else: print(" Open a font first to change upm, please.") print(" Done.")