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!


  • admin

    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)


  • admin

    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. :)


  • admin

    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.")
    

Log in to reply