Skip to content

Instantly share code, notes, and snippets.

@typesupply
Created May 10, 2022 19:45
Show Gist options
  • Save typesupply/247b3f59143e7dc94d2c3e4bbcdb9165 to your computer and use it in GitHub Desktop.
Save typesupply/247b3f59143e7dc94d2c3e4bbcdb9165 to your computer and use it in GitHub Desktop.
Start tabular figures.
"""Create tabular figures. Version 0.1"""
"""
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 sufix 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 figures to be overwritten.
-----------------------------------------------------------------
Width:
The width for the tabular glyphs.
-----------------------------------------------------------------
Apply to all open fonts:
Enabling tis 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.
"""
from fontParts.ui import AskString, Message, ProgressBar
from dialogKit import *
defaultGroups = {
"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" : "70%",
"useAverage" : False,
},
"percent" : {"glyphs" : "percent".split(" "),
"width" : "100%",
"useAverage" : False,
},
"perthousand" : {"glyphs" : "perthousand".split(" "),
"width" : "100%",
"useAverage" : False,
}
}
defaultGroup = {"glyphs" : [],
"width" : "100%",
"useAverage" : False
}
def interpretGroupWidth(widthString, tabularWidth):
widthString = widthString.strip()
try:
if widthString.endswith("%"):
widthString = widthString.replace("%", "")
percentage = int(widthString) * .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 font.has_key(glyphName):
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 not font.has_key(inputName):
print("skipping %s because %s does not exists" % (outputName, inputName))
continue
inputGlyph = font[inputName]
if font.has_key(outputName) 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.mark = (1, 0, 0, 0.5)
outputGlyph.note = "non-tabular width: %s\ndifference: %s" % (str(inputGlyph.width), str(diff))
outputGlyph.update()
class TabularInterface(object):
def __init__(self):
self.groups = defaultGroups
#
self.w = ModalDialog((339, 515), okCallback=self.callback_ok)
#
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), [], callback=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), ".tab")
#
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.populateGroupList()
#
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[index]
return self.groups[title]
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(defaultGroup)
self.populateGroupList()
def callback_removeGroup(self, sender):
selection = self.w.groupList.getSelection()
if selection:
for index in selection:
title = self.w.groupList[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 NameError:
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.update()
progressBar.close()
class CalculatorInterface(object):
def __init__(self):
self.allFonts = AllFonts()
self.currentFont = CurrentFont()
#
self.w = ModalDialog((174, 300))
#
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 font.has_key(glyphName):
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:
self.w.glyphList.append(glyphName)
self.calculate()
def callback_removeGlyph(self, sender):
selection = self.w.glyphList.getSelection()
if selection:
selection.sort()
selection.reverse()
for index in selection:
del self.w.glyphList[index]
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()
@typoman
Copy link

typoman commented Apr 17, 2023

Thank you for sharing this. I was checking this out and I realised I don't have dialogKit. Is this a built in package in RF?

@typesupply
Copy link
Author

I think Frederik removed dialogKit from the latest RF beta. It was a very old UI toolkit that worked in FontLab that I made a vanilla version of so that old scripts could work in RF. Here's the old vanilla wrapper:

https://github.com/typesupply/dialogKit/blob/master/Lib/dialogKit/_dkVanilla.py

You should be able to convert everything to vanilla except ModalDialog. Just make that a Window.

Ideally this tool would be updated to ezui. :)

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