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!



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

  • 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
    


  • Awesome, thanks Frederik! Ha, that makes a lot more sense. :)



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

  • admin

    why flipLayers?


  • 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

    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!



  • 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.


Log in to reply