Skip to content

Instantly share code, notes, and snippets.

@ryanbugden
Last active September 22, 2026 19:21
Show Gist options
  • Select an option

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

Select an option

Save ryanbugden/2d3d3b1f1a9c08cb451d009f57771229 to your computer and use it in GitHub Desktop.
# menuTitle: Start Tabular Glyphs
# author: Tal Leming
from vanilla import (
FloatingWindow,
Button,
TextBox,
List,
CheckBox,
EditText,
HorizontalLine,
)
from mojo.UI import AskString, Message, ProgressBar
"""
Start tabular glyphs.
Version 0.2 (Updated by Ryan Bugden, sponsored by La Police)
— Updates the UI to Vanilla, replaces deprecated code,
and includes general code cleanup.
"""
"""
This script gives you a UI and some options for building tabular
figures. The generated glyphs contain duplicates of the source
glyphs, but their widths and sidebearings have been changed.
This does not attempt to squeeze or stretch the outlines, rather
it simply shifts them around.
-----------------------------------------------------------------
Groups List:
Groups of glyphs that should all have the same width.
The groups can have a fixed width, or the width for the group
can be created by averaging the width of all source glyphs in
the group. If you elect for a fixed width, you can enter it as
a set number of units (ie 250) or as a percentage (ie 50%).
Percentage values will be calculated as a percentage of the
overall tabular width.
Glyphs List:
List of glyphs in the selected group. The glyphs in this list
are the sources that will be used to generate the tabular glyphs.
-----------------------------------------------------------------
Use glyphs with suffix:
An optional suffix that will be appended to the source glyph
names when searching for source glyphs. For example, if you want
to generate old style tabular and your old style glyphs have a
".oldstyle" suffix, enter ".oldstyle" in this field.
Add suffix:
The suffix to be added to the tabular glyph names.
-----------------------------------------------------------------
Overwrite existing glyphs:
Enabling this will cause any glyphs with the same name as the
new tabular glyphs to be overwritten.
-----------------------------------------------------------------
Width:
The width for the tabular glyphs.
Apply to all open fonts:
Enabling this will add the tabular glyphs to all open fonts.
Apply to current font:
Enabling this will add the tabular glyphs only to the current font.
-----------------------------------------------------------------
Calculator
This is a simple dialog that averages the widths of selected
glyphs in specified fonts.
-----------------------------------------------------------------
Glyphs whose adjusted sidebearings become negative
are marked in red in the UI for easy identification.
"""
DEFAULT_GROUPS = {
"figures": {
"glyphs": "zero one two three four five six seven eight nine".split(" "),
"width": "100%",
"useAverage": False,
},
"math": {
"glyphs": "plus minus plusminus multiply divide equal notequal approxequal greater less greaterequal lessequal numbersign".split(
" "
),
"width": "100%",
"useAverage": False,
},
"currency": {
"glyphs": "dollar cent sterling section yen florin Euro won".split(" "),
"width": "100%",
"useAverage": False,
},
"half": {
"glyphs": "space colon semicolon comma period".split(" "),
"width": "50%",
"useAverage": False,
},
"containers": {
"glyphs": "parenleft parenright bracketleft bracketright braceleft braceright".split(
" "
),
"width": "75%",
"useAverage": False,
},
"percent": {
"glyphs": "percent".split(" "),
"width": "100%",
"useAverage": False,
},
"perthousand": {
"glyphs": "perthousand".split(" "),
"width": "125%",
"useAverage": False,
},
}
DEFAULT_GROUP = {"glyphs": [], "width": "100%", "useAverage": False}
def guess_average_tab_width(font):
glyph_names = [
"zero",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
]
widths = []
for glyph_name in glyph_names:
if glyph_name in font:
widths.append(font[glyph_name].width)
return int(sum(widths) / len(widths))
def interpretGroupWidth(widthString, tabularWidth):
widthString = widthString.strip()
try:
if widthString.endswith("%"):
widthString = widthString.replace("%", "")
percentage = int(widthString) * 0.01
width = tabularWidth * percentage
else:
width = int(widthString)
return width
except ValueError:
return None
def findAverageWidth(fonts, glyphNames):
widths = []
for font in fonts:
for glyphName in glyphNames:
if glyphName in font.keys():
widths.append(font[glyphName].width)
return sum(widths) / len(widths)
def makeTabular(fonts, glyphNames, width, inputSuffix, outputSuffix, overwriteExisting):
for font in fonts:
for glyphName in glyphNames:
# glyph = None
inputName = glyphName + inputSuffix
outputName = glyphName + outputSuffix
if inputName not in font.keys():
print(
"skipping %s because %s does not exist" % (outputName, inputName)
)
continue
inputGlyph = font[inputName]
if outputName in font.keys() and not overwriteExisting:
print("skipping %s because the glyph already exists" % outputName)
continue
diff = width - inputGlyph.width
offset = int(round(diff / 2.0))
outputGlyph = font.newGlyph(outputName, clear=True)
outputGlyph.appendComponent(inputName, offset=(offset, 0))
outputGlyph.width = width
if width < inputGlyph.width:
outputGlyph.markColor = (1, 0, 0, 0.5)
outputGlyph.note = "non-tabular width: %s\ndifference: %s" % (
str(inputGlyph.width),
str(diff),
)
outputGlyph.changed()
class TabularInterface(object):
def __init__(self):
self.groups = DEFAULT_GROUPS
#
self.w = FloatingWindow(
(339, 515),
"Make Tabular Figures",
)
#
self.w.groupTitle = TextBox((12, 15, 50, 17), "Groups")
self.w.groupAddButton = Button(
(99, 10, 28, 20), "+", callback=self.callback_addGroup
)
self.w.groupRemoveButton = Button(
(134, 10, 28, 20), "-", callback=self.callback_removeGroup
)
self.w.groupList = List(
(12, 40, 150, 100), [], selectionCallback=self.callback_groupSelection
)
self.w.groupUseWidth = CheckBox(
(12, 145, 100, 27),
"Fixed width:",
value=True,
callback=self.callback_groupWidthToggle,
)
self.w.groupWidthEntry = EditText(
(115, 145, 47, 27), "100%", callback=self.callback_groupWidthEntry
)
self.w.groupUseAverage = CheckBox(
(12, 170, 150, 27),
"Group average width",
callback=self.callback_groupWidthToggle,
)
#
self.w.glyphsTitle = TextBox((177, 15, 50, 17), "Glyphs")
self.w.glyphsAddButton = Button(
(264, 10, 28, 20), "+", callback=self.callback_addGlyph
)
self.w.glyphsRemoveButton = Button(
(299, 10, 28, 20), "-", callback=self.callback_removeGlyph
)
self.w.glyphList = List((177, 40, 150, 155), [])
#
self.w.divider1 = HorizontalLine((12, 210, 315, 1))
#
self.w.inputSuffixTitle = TextBox((12, 225, 150, 17), "Use glyphs with suffix:")
self.w.inputSuffixEntry = EditText((12, 245, 150, 27))
self.w.outputSuffixTitle = TextBox((177, 225, 150, 17), "Add suffix:")
self.w.outputSuffixEntry = EditText((177, 245, 150, 27), ".tnum")
#
self.w.divider2 = HorizontalLine((12, 285, 315, 1))
#
self.w.overwriteExisting = CheckBox(
(12, 295, 315, 27), "Overwrite existing glyphs", value=True
)
#
self.w.divider3 = HorizontalLine((12, 330, 315, 1))
#
self.w.widthTitle = TextBox((12, 350, 60, 17), "Width:")
self.w.widthEntry = EditText((55, 345, 107, 27))
#
self.w.divider4 = HorizontalLine((12, 385, 315, 1))
#
self.w.applyToAll = CheckBox(
(12, 400, 315, 27),
"Apply to all open fonts",
callback=self.callback_applyToToggle,
)
self.w.applyToCurrent = CheckBox(
(12, 425, 315, 27),
"Apply to current font",
value=True,
callback=self.callback_applyToToggle,
)
#
self.w.calculatorButton = Button(
(12, -35, 100, 20), "Calculator", callback=self.callback_showCalculator
)
#
self.w.okButton = Button((262, -35, 65, 20), "OK", callback=self.callback_ok)
self.w.cancelButton = Button(
(177, -35, 65, 20), "Cancel", callback=self.callback_cancel
)
self.populateGroupList()
#
# Make a guess about width
f = CurrentFont()
if f and guess_average_tab_width(f):
self.w.widthEntry.set(guess_average_tab_width(f))
self.w.open()
def populateGroupList(self):
groupNames = list(self.groups.keys())
groupNames.sort()
self.w.groupList.set(groupNames)
def getSelectedGroup(self):
selection = self.w.groupList.getSelection()
if len(selection) != 1:
return None
index = selection[0]
title = self.w.groupList.get()[index]
return self.groups[title]
def callback_cancel(self, sender):
self.w.close()
def callback_addGroup(self, sender):
from copy import deepcopy
title = AskString("Group title:")
if title:
if title in self.groups:
return
self.groups[title] = deepcopy(DEFAULT_GROUP)
self.populateGroupList()
def callback_removeGroup(self, sender):
selection = self.w.groupList.getSelection()
if selection:
for index in selection:
title = self.w.groupList.get()[index]
del self.groups[title]
self.populateGroupList()
def callback_groupSelection(self, sender):
group = self.getSelectedGroup()
if group is None:
self.w.glyphList.set([])
return
self.w.glyphList.set(group["glyphs"])
self.w.groupWidthEntry.set(group["width"])
if group["useAverage"]:
self.w.groupUseWidth.set(False)
self.w.groupWidthEntry.enable(False)
self.w.groupUseAverage.set(True)
else:
self.w.groupUseWidth.set(True)
self.w.groupWidthEntry.enable(True)
self.w.groupUseAverage.set(False)
def packGroupWidthData(self):
group = self.getSelectedGroup()
if not group:
return
group["width"] = self.w.groupWidthEntry.get()
group["useAverage"] = self.w.groupUseAverage.get()
def callback_groupWidthEntry(self, sender):
# validate the entry
widthString = sender.get()
if interpretGroupWidth(widthString, 100) is None:
Message(
"Invalid entry. The value must be a percentage (example: 50%) or a fixed value (example 100)."
)
else:
self.packGroupWidthData()
def callback_groupWidthToggle(self, sender):
opposite = not sender.get()
if sender == self.w.groupUseWidth:
self.w.groupUseAverage.set(opposite)
else:
self.w.groupUseWidth.set(opposite)
self.w.groupWidthEntry.enable(not self.w.groupUseAverage.get())
self.packGroupWidthData()
def callback_addGlyph(self, sender):
group = self.getSelectedGroup()
if group is None:
return
glyphName = AskString("Glyph name:")
if glyphName:
if glyphName in group["glyphs"]:
return
group["glyphs"].append(glyphName)
self.callback_groupSelection(self.w.groupList)
def callback_removeGlyph(self, sender):
group = self.getSelectedGroup()
if group is None:
return
selection = self.w.glyphList.getSelection()
if selection:
glyphNames = [self.w.glyphList[index] for index in selection]
for glyphName in glyphNames:
group["glyphs"].remove(glyphName)
self.callback_groupSelection(self.w.groupList)
def callback_applyToToggle(self, sender):
opposite = not sender.get()
if sender == self.w.applyToAll:
self.w.applyToCurrent.set(opposite)
else:
self.w.applyToAll.set(opposite)
def callback_showCalculator(self, sender):
CalculatorInterface()
def callback_ok(self, sender):
incompleteData = False
#
width = self.w.widthEntry.get()
try:
width = int(width)
except ValueError:
incompleteData = True
#
inputSuffix = self.w.inputSuffixEntry.get()
outputSuffix = self.w.outputSuffixEntry.get()
if not inputSuffix and not outputSuffix:
incompleteData = True
#
overwriteExisting = self.w.overwriteExisting.get()
#
if not self.groups:
incompleteData = True
#
if incompleteData:
Message("Can not proceed: Incomplete data.")
else:
if self.w.applyToAll.get():
fonts = AllFonts()
else:
fonts = [CurrentFont()]
tickCount = len(self.groups)
progressBar = ProgressBar(title="Processing...", ticks=tickCount)
for group in self.groups.values():
glyphs = group["glyphs"]
groupWidth = interpretGroupWidth(group["width"], width)
useAverage = group["useAverage"]
if useAverage:
groupWidth = findAverageWidth(fonts, glyphs)
makeTabular(
fonts,
glyphs,
groupWidth,
inputSuffix,
outputSuffix,
overwriteExisting,
)
progressBar.tick()
for font in fonts:
font.changed()
progressBar.close()
class CalculatorInterface(object):
def __init__(self):
self.allFonts = AllFonts()
self.currentFont = CurrentFont()
#
self.w = FloatingWindow((174, 300), "Average Width")
#
self.w.glyphTitle = TextBox((12, 15, 50, 17), "Glyphs")
self.w.glyphAddButton = Button(
(99, 10, 28, 20), "+", callback=self.callback_addGlyph
)
self.w.glyphRemoveButton = Button(
(134, 10, 28, 20), "-", callback=self.callback_removeGlyph
)
self.w.glyphList = List((12, 40, 150, 100), [])
#
self.w.allFonts = CheckBox(
(12, 155, 150, 27), "All open fonts", callback=self.callback_fontToggle
)
self.w.currentFont = CheckBox(
(12, 180, 150, 27),
"Current font",
value=True,
callback=self.callback_fontToggle,
)
#
self.w.outputField = TextBox((12, 220, 150, 17), "Average: 0")
#
self.w.open()
def calculate(self):
if self.w.allFonts.get():
fonts = self.allFonts
else:
fonts = [self.currentFont]
widths = []
for glyphName in self.w.glyphList.get():
for font in fonts:
if glyphName in font.keys():
widths.append(font[glyphName].width)
if not len(widths):
self.w.outputField.set("Average: 0")
else:
average = sum(widths) / len(widths)
self.w.outputField.set("Average: %d" % average)
def callback_addGlyph(self, sender):
glyphName = AskString("Glyph name:")
if not glyphName:
return
if glyphName not in self.w.glyphList.get():
glyphs = self.w.glyphList.get()
glyphs.append(glyphName)
self.w.glyphList.set(glyphs)
self.calculate()
def callback_removeGlyph(self, sender):
selection = self.w.glyphList.getSelection()
if selection:
selection.sort()
selection.reverse()
for index in selection:
glyphs = self.w.glyphList.get()
del glyphs[index]
self.w.glyphList.set(glyphs)
self.calculate()
def callback_fontToggle(self, sender):
opposite = not sender.get()
if sender == self.w.allFonts:
self.w.currentFont.set(opposite)
else:
self.w.allFonts.set(opposite)
self.calculate()
TabularInterface()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment