Skip to content

Instantly share code, notes, and snippets.

@ryanbugden
Last active April 22, 2024 21:54
Show Gist options
  • Select an option

  • Save ryanbugden/2e2ca71efc57f867c9fb079cfa4a91e5 to your computer and use it in GitHub Desktop.

Select an option

Save ryanbugden/2e2ca71efc57f867c9fb079cfa4a91e5 to your computer and use it in GitHub Desktop.
A Font Overview contextual menu that allows you to monospace selected glyphs. Tries to handle components and anchors intelligently.
# menuTitle : (Menu) Monospace Glyphs
from mojo.subscriber import Subscriber, registerFontOverviewSubscriber
from mojo.UI import PostBannerNotification, AskString
from fontTools.misc.fixedTools import otRound
from statistics import median
MARK_COLOR = (1, 0, 0.2234, 0.8)
class MonospaceGlyphsMenu(Subscriber):
'''
2020.07.24
2024.04.22
Ryan Bugden
'''
def build(self):
self.f = None
def fontOverviewWantsContextualMenuItems(self, info):
self.f = CurrentFont()
if self.f.selectedGlyphNames and len(self.f.selectedGlyphNames) != len(self.f.keys()):
self.message = "Monospace Selected Glyphs"
self.span = self.f.selectedGlyphNames
self.suggested_width = self.get_median_width()
else:
self.message = "Monospace All Glyphs"
self.span = self.f.keys()
self.suggested_width = self.get_control_glyph_width()
menu_items = [
(self.message, [
('Keep Relative Spacing', self.monospace_proportionate),
('Force Centered', self.monospace_centered)
]
)
]
info['itemDescriptions'].extend(menu_items)
def get_median_width(self):
ws = []
for g_name in self.span:
ws.append(self.f[g_name].width)
try:
return otRound(median(ws))
except:
return 1000
def get_control_glyph_width(self):
ws = []
for g_name in ['n', 'o']:
if g_name in self.f.keys():
ws.append(self.f[g_name].width)
if ws:
try:
return otRound(median(ws))
except:
return 1000
else:
return self.get_median_width()
def space_glyph_lsb(self, g, side_value):
# Move contours, anchors, guidelines, image
stuff_to_move = [c for c in g.contours] + [a for a in g.anchors] + [g for g in g.guidelines] + [g.image]
for element in stuff_to_move:
if element:
element.moveBy((side_value, 0))
# Handle components that are transformed, and only if they're being manipulated by this script as well.
for comp in g.components:
# If the component’s base glyph isn't in the selection, then move it normally.
if comp.baseGlyph not in self.span:
comp.moveBy((side_value, 0))
# If it is, then compensate for whatever changes were made to the base glyph
else:
# If the component is rotated at all
if comp.transformation[0] < 0:
# Move it horizontally, according to how much it's rotated
comp.moveBy((-comp.transformation[0]*side_value*2, 0))
# Adjust vertical offset if vertically squished
comp.moveBy((0, -comp.transformation[1]*side_value))
def monospace_proportionate(self, sender):
self.monospace_glyphs("proportionate")
def monospace_centered(self, sender):
self.monospace_glyphs("centered")
def monospace_glyphs(self, style):
set_width = int(AskString(
"Choose width:",
f"{self.suggested_width}",
self.message
))
width_changed = []
names_and_deltas = {}
for g_name in self.span:
g = self.f[g_name]
if g.markColor == MARK_COLOR:
g.markColor = None
# Collect the glyphs that will have changed, and mark them
if g.width != set_width:
width_changed.append(g.name)
g.markColor = MARK_COLOR
# If there are contours or components in the glyph
min_anchors = None
if g.bounds:
content_width = g.bounds[2] - g.bounds[0]
# If there are anchors
elif g.anchors:
a_xs = [a.x for a in g.anchors]
min_anchors, max_anchors = min(a_xs), max(a_xs)
content_width = max_anchors - min_anchors
if style == "centered":
desired_left = otRound((set_width - content_width) / 2)
if g.angledLeftMargin:
left_delta = desired_left - g.angledLeftMargin
elif min_anchors:
left_delta = desired_left - min_anchors
else:
left_delta = 0
elif style == "proportionate":
left_delta = otRound((set_width - g.width) / 2)
# Store all changes beforehand, in order to protect component changes
if left_delta:
names_and_deltas[g] = left_delta
# Actually change the spacing
for g, left_delta in names_and_deltas.items():
with g.undo(f"Monospace glyph {g.name}"):
if left_delta:
self.space_glyph_lsb(g, left_delta)
g.width = set_width
self.f.changed()
PostBannerNotification("Monospaced Glyphs", f"Width: {set_width}")
print(f"\nMonospaced glyphs with width: {set_width}")
print("Glyphs requested: ", self.span)
print("Glyphs that changed width: ", width_changed)
#===================
if __name__ == "__main__":
registerFontOverviewSubscriber(MonospaceGlyphsMenu)
@peterlaxalt
Copy link

Hey man - adjusted your script slightly to prompt if you want to reset the left/right margins to center all glyphs in selection automatically. Currently working on a CJK font and found it useful. The double prompt isn't as elegant but gets the job done. Thanks for all your work here. 🔨

# menuTitle : Monospace Glyphs Menu

from mojo.subscriber import Subscriber, registerFontOverviewSubscriber
from mojo.UI import PostBannerNotification
from mojo.UI import AskString
from mojo.UI import AskYesNoCancel
from mojo.roboFont import version
from statistics import median

class monospaceGlyphsMenu(Subscriber):
    
    '''
    Monospace glyphs menu (median)
    
    2020.07.24
    Ryan Bugden
    '''
    
    maxTitleLength = 20


    def build(self):
        self.f = None
        
    def fontOverviewWantsContextualMenuItems(self, info):
        self.f = CurrentFont()
        if self.f.selectedGlyphNames:
            myMenuItems = [
                ("Monospace glyphs", self.monospaceCallback)
            ]
            info['itemDescriptions'].extend(myMenuItems)
        else:
            pass
        
    def monospaceCallback(self, sender):

        # if no selection, do all
        if self.f.selectedGlyphNames == ():
            sel = self.f.keys()
        else:
            sel = self.f.selectedGlyphNames
            
        ws = []
        
        for g_name in sel:
            g = self.f[g_name]
            ws.append(g.width)

        try: 
            des = int(median(ws))
        except:
            des  = 1000
            
        set_width = int(AskString(
            "Set width:",
            f"{des}",
            "Monospace Selected Glyphs"
            ))
        reset_left_right_margins = AskYesNoCancel(
            "Reset left/right margins?", 
            title='Margin control', 
            default=0, 
            informativeText='This will center your glyph in your new width and remove margins'
            )
            
        if reset_left_right_margins == -1:
            print("\nMargins reset cancelled, bailing", reset_left_right_margins)
            return

        for g_name in sel:
            g = self.f[g_name]
    
            # for Robofont 3+
            if version >= "3.0":
                print("\nVersion >= 3.0")
                if g.bounds:
                    
                    print("\nMargins reset", reset_left_right_margins)
                    
                    if reset_left_right_margins == 1:
                        print("\nResetting left/right margins to 0")
                        g.leftMargin = 0
                        g.rightMargin = 0    
                        
                    print("\nBounds detected")
                    print("\nLeft margin:", g.leftMargin)
                    print("\nRight margin:", g.rightMargin)
                    print("\nBounds:", g.bounds)
                    print("\nWidth:", g.width)
                    diff = set_width - g.width
                    g.leftMargin = diff/2
                    g.rightMargin = diff/2
                    g.width = set_width
        
                else:
                    g.width = set_width
                g.changed()
                self.f.changed()
            
            # for Robofont 1
            else:
                if g.box:
                    print("\nSetting bounds (legacy)")
                    diff = set_width - g.width
                    g.leftMargin += diff/2
                    g.rightMargin += diff/2
                    g.width = set_width
        
                else:
                    print("\nNot setting bounds")
                    g.width = set_width
                g.changed()
                self.f.changed()

        PostBannerNotification("Monospaced selected glyphs", f"Width: {set_width}")

        print("\nMonospaced selected glyphs with width: " + str(set_width))
        print("Selected glyphs included: ", self.f.selectedGlyphNames)
        
        
#===================
        
if __name__ == "__main__":    
    registerFontOverviewSubscriber(monospaceGlyphsMenu)

@ryanbugden
Copy link
Author

Hi @peterlaxalt , I like your idea! I rewrote it to give two submenu options, and added some other stuff to take care of components. Only lightly tested. Give it a shot and let me know what you think.

@peterlaxalt
Copy link

Nicely done - working well. Thanks @ryanbugden .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment