Last active
September 22, 2026 19:21
-
-
Save ryanbugden/2d3d3b1f1a9c08cb451d009f57771229 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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