Skip to content

Instantly share code, notes, and snippets.

@jmsole
Last active April 29, 2023 19:52
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmsole/464c7a01d2de89204f38286fd8519489 to your computer and use it in GitHub Desktop.
Save jmsole/464c7a01d2de89204f38286fd8519489 to your computer and use it in GitHub Desktop.
Kern proof generator with Drawbot and drawbotgrid
from drawBotGrid import BaselineGrid, columnBaselineGridTextBox
import os
import datetime
import itertools
import unicodedata
# When you add more fonts, it stacks the strings for each
# weight to enable easy comparisons.
fonts = ('Helvetica Neue Thin', 'Helvetica Neue', 'Helvetica Neue Bold')
#fonts = ('Helvetica Neue Thin', 'Helvetica Neue')
fontnumber = len(fonts)
familyname = 'Helvetica Neue'
# These are the strings fo characters that will be mashed together
# to generate the kerning strings. You do have to take care with
# escaping certain characters.
ucchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZÆĐÞƠƯ'
lcchars = 'abcdefghijklmnopqrstuvwxyzßďľðơư'
numchars = '0123456789'
punctchars = '.,:;-_¿?¡!\/()[]{}‹›@\'‘’*&§¶°†‡'
ucpunctchars = '¡¿·•\/-(){}[]‹›@'
mathchars = '∕+−×÷=≠><≥≤±≈~¬^%∅∞'
currchars = '€¢¤$₫₴₤₺₱₽₹£₩¥'
topleftclash = 'FĽƠTƯVWYďđfíĭǐîïīĩĵľſťấẩếềểốồổ7'
toprightclash = 'TVWYằàðhĭǐîïìīĩ'
bottomleftclash = 'ĘỊĮQąęịįjļḷțṯỵ'
bottomrightclash = 'ỊJgịjļpţṯ'
# For ligatures you store each combination of letters that
# generates a ligature and then toggle them with OT features
#ss01right = ('AI', 'AV', 'AW', 'FI', 'FO', 'LA', 'LD', 'LE', 'LI', 'LL', 'LO', 'LR', 'RI', 'FA', 'TE', 'TO', 'UE', 'VE', 'WE', 'WR', 'QU', 'RA', 'RE', 'RO', 'RU')
#ss01left = ('OR', 'OU')
# Template characters to use as context for strings
uc1 = 'H'
uc2 = 'O'
lc1 = 'n'
lc2 = 'o'
num1 = '0'
# This function helps with making the lines of strings
# have a specific amount of *words* per line.
def wrap_by_word(s, n):
'''returns a string where \\n is inserted between every n words'''
a = s.split()
ret = ''
for i in range(0, len(a), n):
ret += ' '.join(a[i:i+n]) + '\n'
return ret
# This function generates the kern strings by mashing up
# groups of characters. If you give it only one list, it
# will mash it up against itself but will avoid repeating
# them, so thge further you progress the fewer combinations
# you will see. If you give it two separate strings to
# mash up, it will do all against all, every
# combination (mathematical product).
def makeKern (section, linelen, *args):
kernstring = ''
if len(args) == 1:
product = itertools.combinations_with_replacement(args[0], 2)
elif len(args) == 2:
product = itertools.product(args[0], args[1])
else:
print('Too many arguments!')
for pair in product:
strpair = ''.join(pair)
if section == 'UC x LC' or section == 'LC x UC':
makestr = f'{strpair}{lc1}{lc1}{lc1} '
elif 'LIGA' in section:
makestr = f'{uc1}{uc1}{strpair}{uc1}{uc1} '
elif 'UC' in section:
makestr = f'{uc1}{strpair}{uc1}{uc1}{strpair[::-1]}{uc1} '
elif 'LC' in section:
makestr = f'{lc1}{strpair}{lc1}{lc1}{strpair[::-1]}{lc1} '
elif 'NUM' in section:
makestr = f'{num1}{strpair}{num1}{num1}{strpair[::-1]}{num1} '
elif 'CLASHERS' in section:
makestr = f'{lc1}{lc1}{strpair}{lc1}{lc1} '
else:
makestr = f'{uc1}{strpair}{uc1}{uc1}{strpair[::-1]}{uc1} '
kernstring += makestr
kernstring = wrap_by_word(kernstring, linelen)
return kernstring
# Make a footer per page. This comes from DJR's simple proof script.
def drawFooter(section):
with savedState():
folio = FormattedString(str(pageCount()),
font='Courier',
fontSize=9,
lineHeight=9,
align='right'
)
today = datetime.date.today()
# assemble footer text
footerText = f'{today} | {familyname} | {section}'
# and display formatted string
footer = FormattedString(footerText,
font='Courier',
fontSize=9,
lineHeight=9
)
folio = FormattedString(str(pageCount()),
font='Courier',
fontSize=9,
lineHeight=9,
align="right"
)
textBox(footer, (40, 25, width()-25*2, 9))
textBox(folio, (40, 25, width()-50*2, 9))
# This is the main function that draws the pages.
# It uses baseline grids and grid text boxes using
# Mathieu Reguer's drawbot grid. That means you can
# very easily switch to a multiple colum layout.
# And when stacking multiple lines, using the same
# baseline grid ensures it's all aligned and evenly linespaced.
# The name of the section is passed to the makeKern function
# Which has a few ifs to assign different control characters.
# The linelen argument is to assign independent linelengths to
# each section. Probably there's a way to automate that.
# args are the groups of character to mash up.
# For now kwargs takes an optional OT feature dict to toggle
# different features on and off.
def drawSection(section, linelen, *args, **kwargs):
text = makeKern(section, linelen, *args)
# I keep this print statement here in case I also want to
# use the strings in a font editor.
#print(text)
while text:
newPage('A4')
drawFooter(section)
fill(0)
fontSize(15)
lineHeight(20*fontnumber)
# Parse the otfea dict to toggle feature per section
if 'otfea' in kwargs:
otfea = kwargs.get('otfea')
openTypeFeatures(**otfea)
else:
openTypeFeatures(liga=False, kern=True)
hyphenation(False)
baselines = BaselineGrid.from_margins((0, -40, 0, -40), 5)
# Uncomment the line below to see the baseline grid.
#baselines.draw()
# The code below is what draw the text to the page. Because
# of how the while text loop works, I need to make a copy of
# the text for each font I want to use, otherwise the text
# gets used up by the first font, and the next font would use
# the next line. I also use sa vertical shift to shift the text
# boxes for each font after the first one.
vshift = 0
textprerun = text
runs = 0
for fi, f in enumerate(fonts, start=1):
font(f)
text = columnBaselineGridTextBox(text, (40, 40 - vshift, width()-80, height()-80), baselines, subdivisions=1, gutter=20, draw_grid=False)
vshift = vshift + 20
runs += 1
if runs == fontnumber:
continue
else:
text = textprerun
# Finally this is where you configure each section. If you
# want to have your own names for the sections, make sure to
# change the parsing logic on the makeKern function as well.
# The number is the amount of *words* per line for each section.
# You can give one or two groups of character to be mashed up.
# The otfea dict is optional and should be used to generate
# kern string involving glyphs that can only be accessed
# through OT features.
drawSection('UC x UC', 4, ucchars)
drawSection('LC x LC', 4, lcchars)
drawSection('UC x LC', 6, ucchars, lcchars)
drawSection('LC x UC', 6, lcchars, ucchars)
drawSection('PUNCT x UC', 4, punctchars, ucchars)
# Uncomment the line below if you have UC punctuation in
# a case feature.
#drawSection('UCPUNCT x UC', 4, ucpunctchars, ucchars, otfea=dict(liga=False, case=True, kern=True))
drawSection('PUNCT x LC', 5, punctchars, lcchars)
drawSection('NUM x NUM', 6, numchars)
# Old-style numbers
#drawSection('ONUM x ONUM', 6, numchars, otfea=dict(liga=False, onum=True, kern=True))
drawSection('PUNCT x NUM', 5, punctchars, numchars)
#drawSection('PUNCT x ONUM', 5, punctchars, numchars, otfea=dict(liga=False, onum=True, kern=True))
drawSection('MATH x NUM', 5, mathchars, numchars)
#drawSection('MATH x ONUM', 5, mathchars, numchars, otfea=dict(liga=False, onum=True, kern=True))
drawSection('CURR x NUM', 5, currchars, numchars)
#drawSection('CURR x ONUM', 5, currchars, numchars, otfea=dict(liga=False, onum=True, kern=True))
# I made groups of glyphs that can normally clash so
# I can make sure they behave the way I want.
drawSection('TOP CLASHERS', 7, topleftclash, toprightclash)
drawSection('BOTTOM CLASHERS', 7, bottomleftclash, bottomrightclash)
# Examples for testing stylistic set ligatures
#drawSection('SS01LIGA x UC', 5, ss03right, ucchars, otfea=dict(liga=False, ss01=True, kern=True))
#drawSection('UC x SS01LIGA', 5, ucchars, ss03left, otfea=dict(liga=False, ss01=True, kern=True))
saveImage('kern-proof.pdf')
@jmsole
Copy link
Author

jmsole commented Apr 21, 2023

You need to have drawbotgrid installed in Drawbot. Instructions are in that repo.

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