I wanted to make my own handwriting font. I also wanted to be able to generate fonts quickly from the handwriting samples I can draw on my tablet.
https://pages.sachachua.com/sachac-hand/README.html | This README as HTML |
https://github.com/sachac/sachac-hand | Github repo |
./files/test.html | Test pages |
Feel free to use the font under the SIL Open Font License. That means you can freely create and distribute things that use the font.
Feel free to use the code under the GNU GPL v3+ license.
See LICENSE for more details.
I wanted to make a font based on my handwriting using only free software. It turns out that FontForge can be scripted with Python. I know just a little about Python and even less about typography, but I managed to hack together something that worked for me. If you’re reading this on my blog at https://sachachua.com/blog/ , you’ll probably see the new font being used on the blog post titles. Whee!
My rough notes are at https://github.com/sachac/sachac-hand/ . I wanted to write it as a literate program using Org Babel blocks. It’s not really fully reproducible yet, but it might be a handy starting point. The basic workflow was:
- Generate a template using other fonts as the base.
- Import the template into Medibang Paint on my phone and draw
letters on a different layer. (I almost forgot the letter
q
, so I had to add it at the last minute.) - Export just the layer with my writing.
- Cut the image into separate glyphs using Python and autotrace each one.
- Import each glyph into FontForge as an SVG and a PNG.
- Set the left side and right side bearing, overriding as needed based on a table.
- Figure out kerning classes.
- Hand-tweak the contours and kerning.
- Use
sfnt2woff
to export the web font file for use on my blog, and modify the stylesheet to include it.
I really liked being able to specify kerning classes through an Org Mode table like this:
None | o,a,c,e,d,g,q,w | f,t,x,v,y,z | h,b,l,i,k | j | m,n,p,r,u | s | T | zero | |
None | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
f | 0 | -102 | -61 | -30 | 0 | -60 | 0 | -120 | -70 |
t | 0 | -70 | -41 | -25 | 0 | 0 | 0 | -120 | -10 |
r | 0 | -82 | -41 | -25 | 0 | -20 | 0 | -120 | 29 |
k | 0 | -50 | -81 | -20 | 0 | -20 | -48 | -120 | -79 |
l | 0 | -41 | -50 | 0 | 0 | 0 | 0 | -120 | -52 |
v | 0 | -40 | -35 | -30 | 0 | 0 | 0 | -120 | 30 |
b,o,p | 0 | -20 | -80 | 0 | 0 | 0 | 0 | -120 | 43 |
a | 0 | -23 | -60 | 0 | 0 | 0 | 0 | -120 | 7 |
W | 0 | -40 | -30 | -20 | 0 | 0 | 0 | -120 | 17 |
T | 0 | -190 | -120 | -60 | 0 | -130 | 0 | 0 | -188 |
F | 0 | -100 | -90 | -60 | 0 | -70 | -100 | -40 | -166 |
two | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -53 |
I had a hard time defining classes using the FontForge interface because I occasionally ended up clearing my glyph selection, so it was great being able to just edit my columns and rows.
Clearly my kerning is still very rough–no actual values for j, for example–but it’s a start. Also, I can probably figure out how to combine this with character pair kerning and have two tables for easier tweaking.
A- insisted on tracing my handwriting template a few times, so I might actually be able to go through the same process to convert her handwriting into a font. Whee!
sudo apt-get install fontforge python3-fontforge python3-numpy python3-sqlalchemy python3-pandas python3-pymysql python3-nltk woff-tools woff2 python3-yattag python3-livereload
I compiled autotrace based on my fork at https://github.com/sachac/autotrace so that it uses Graphicsmagick instead of Imagemagick.
I also needed (setenv "LD_LIBRARY_PATH" "/usr/local/lib")
. There are probably a bunch of other prerequisites I’ve forgotten to write down.
FileNotFoundError: [Errno 2] No such file or directory: '/home/sacha/.local/lib/python3.8/site-packages/aglfn/agl-aglfn/aglfn.txt'
- symlink or copy the one from /usr/share/aglfn to the right place
import numpy as np
import pandas as pd
import aglfn
import fontforge
import subprocess
params = {'template': 'template-256.png',
'sample_file': 'sample.png',
'name_list': '/usr/share/aglfn/glyphlist.txt',
'new_font_file': 'sachacHand.sfd',
'new_otf': 'sachacHand.otf',
'new_font_name': 'sachacHand',
'new_family_name': 'sachacHand',
'new_full_name': 'sachacHand',
'text_color': 'lightgray',
'glyph_dir': 'glyphs/',
'letters': 'HOnodpagscebhklftijmnruwvxyzCGABRDLEFIJKMNPQSTUVWXYZ0123456789?:;-–—=!\'’"“”@/\\~_#$%&()*+,.<>[]^`{|}q',
'direction': 'vertical',
'rows': 10,
'columns': 10,
'x_height': 368,
'em': 1000,
'em_width': 1000,
'row_padding': 0,
'ascent': 800,
'descent': 200,
'height': 500,
'width': 500,
'caps': 650,
'line_width': 3,
'text': "Python+FontForge+Org: I made a font based on my handwriting!"
}
params['font_size'] = int(params['em'])
params['baseline'] = params['em'] - params['descent']
def transpose_letters(letters, width, height):
return ''.join(np.reshape(list(letters.ljust(width * height)), (height, width)).transpose().reshape(-1))
# Return glyph name of s, or s if none (possibly variant)
def glyph_name(s):
return aglfn.name(s) or s
def get_glyph(font, g):
pos = font.findEncodingSlot(g)
if pos == -1 or not pos in font:
return font.createChar(ord(aglfn.to_glyph(g)), g)
else:
return font[pos]
def glyph_matrix(font=None, matrix=None, letters=None, rows=0, columns=0, direction='horizontal', **kwargs):
if matrix:
if isinstance(matrix[0], str):
# Split each
matrix = [x.split(',') for x in matrix]
else:
matrix = matrix[:] # copy the list
else:
matrix = np.reshape(list(letters.ljust(rows * columns))[0:rows * columns], (rows, columns))
if direction == 'vertical':
matrix = matrix.transpose()
if hasattr(font, 'findEncodingSlot'):
matrix = [[glyph_name(x) if x != 'None' else None for x in row] for row in matrix]
if font:
for r, row in enumerate(matrix):
for c, col in enumerate(row):
if col is None: continue
matrix[r][c] = get_glyph(font, col)
return matrix
def glyph_filename_base(glyph_name):
try:
return 'uni%s-%s' % (hex(ord(aglfn.to_glyph(glyph_name))).replace('0x', '').zfill(4), glyph_name)
except:
return glyph_name
def load_font(params):
if type(params) == str:
return fontforge.open(params)
else:
return fontforge.open(params['new_font_file'])
def save_font(font, font_filename=None, **kwargs):
if font_filename is None:
font_filename = font.fontname + '.sfd'
font.save(font_filename)
#font = fontforge.open(font_filename)
#font.generate(font_filename.replace('.sfd', '.otf'))
#font.generate(font_filename.replace('.sfd', '.woff'))
import orgbabelhelper as ob
def out(df, **kwargs):
print(ob.dataframe_to_orgtable(df, **kwargs))
from PIL import Image, ImageFont, ImageDraw
#LETTERS = 'abcd'
# Baseline is red
# Top of glyph is light blue
# Bottom of glyph is blue
def draw_letter(column, row, letter, params):
draw = params['draw']
sized_padding = int(params['row_padding'] * params['em'] / params['height'])
origin = (column * params['em_width'], row * (params['em'] + sized_padding))
draw.line((origin[0], origin[1], origin[0] + params['em_width'], origin[1]), fill='lightblue', width=params['line_width'])
draw.line((origin[0], origin[1], origin[0], origin[1] + params['em']), fill='lightgray', width=params['line_width'])
draw.line((origin[0], origin[1] + params['ascent'] - params['x_height'], origin[0] + params['em_width'], origin[1] + params['ascent'] - params['x_height']), fill='lightgray', width=params['line_width'])
draw.line((origin[0], origin[1] + params['ascent'], origin[0] + params['em_width'], origin[1] + params['ascent']), fill='red', width=params['line_width'])
draw.line((origin[0], origin[1] + params['ascent'] - params['caps'], origin[0] + params['em_width'], origin[1] + params['ascent'] - params['caps']), fill='lightgreen', width=params['line_width'])
draw.line((origin[0], origin[1] + params['em'], origin[0] + params['em_width'], origin[1] + params['em']), fill='blue', width=params['line_width'])
width, height = draw.textsize(letter, font=params['font'])
draw.text((origin[0] + (params['em_width'] - width) / 2, origin[1]), letter, font=params['font'], fill=params['text_color'])
def make_template(params):
sized_padding = int(params['row_padding'] * params['em'] / params['height'])
img = Image.new('RGB', (params['columns'] * params['em_width'], params['rows'] * (params['em'] + sized_padding)), 'white')
params['draw'] = ImageDraw.Draw(img)
params['font'] = ImageFont.truetype(params['font_name'], params['font_size'])
matrix = glyph_matrix(**params)
for r, row in enumerate(matrix):
for c, ch in enumerate(row):
draw_letter(c, r, ch, params)
img.thumbnail((params['columns'] * params['width'], params['rows'] * (params['height'] + params['row_padding'])))
img.save(params['template'])
return params['template']
<<params>>
<<def_make_template>>
#make_template({**params, 'font_name': '/home/sacha/.fonts/Romochka.otf', 'template': 'template-romochka.png', 'row_padding': 15})
#make_template({**params, 'font_name': '/home/sacha/.fonts/Breip.ttf', 'template': 'template-breip.png', 'row_padding': 15})
# make_template({**params, 'font_name': 'sachacHand-Regular.otf', 'template': 'template-sachacHand.png', 'row_padding': 50})
make_template({**params, 'font_name': 'sachacHand.otf', 'template': 'template-sample.png', 'direction': 'horizontal', 'height': 1000, 'width': 1000, 'row_padding': 100 })
#return make_template({**params, 'font_name': 'sachacHand.otf', 'template': 'template-sample.png', 'direction': 'horizontal', 'rows': 4, 'columns': 4, 'height': 100, 'width': 100, 'row_padding': 100 })
import os
import libxml2
from PIL import Image, ImageOps
import subprocess
def cut_glyphs(sample_file="", letters="", direction="", columns=0, rows=0, height=0, width=0, row_padding=0, glyph_dir='glyphs', matrix=None, force=False, **kwargs):
im = Image.open(sample_file).convert('L')
thresh = 200
fn = lambda x : 255 if x > thresh else 0
im = im.point(fn, mode='1')
if not os.path.exists(glyph_dir):
os.makedirs(glyph_dir)
matrix = glyph_matrix(matrix=matrix, letters=letters, direction=direction, columns=columns, rows=rows)
for r, row in enumerate(matrix):
top = r * (height + row_padding)
bottom = top + height
for c, ch in enumerate(row):
if ch is None: continue
filename = os.path.join(glyph_dir, glyph_filename_base(aglfn.name(ch)) + '.pbm')
if os.path.exists(filename) and not force: continue
left = c * width
right = left + width
small = im.crop((left, top, right, bottom))
small.save(filename)
svg = filename.replace('.pbm', '.svg')
png = filename.replace('.pbm', '.png')
small.save(png)
subprocess.call(['autotrace', '-output-file', svg, filename])
doc = libxml2.parseFile(svg)
root = doc.children
child = root.children
child.next.unlinkNode()
doc.saveFile(svg)
import fontforge
import os
import aglfn
import psMat
def set_up_font_info(font, new_family_name="", new_font_name="", new_full_name="", em=1000, descent=200, ascent=800, **kwargs):
font.encoding = 'UnicodeFull'
font.fontname = new_font_name
font.familyname = new_family_name
font.fullname = new_full_name
font.em = em
font.descent = descent
font.ascent = ascent
return font
def import_glyphs(font, glyph_dir='glyphs', letters=None, columns=None, rows=None, direction=None, matrix=None, height=0, **kwargs):
old_em = font.em
font.em = height
matrix = glyph_matrix(font=font, matrix=matrix, letters=letters, columns=columns, rows=rows, direction=direction)
if params['scale']:
scale = psMat.scale(params['scale'])
for row in matrix:
for g in row:
if g is None: continue
try:
base = glyph_filename_base(g.glyphname)
svg_filename = os.path.join(glyph_dir, base + '.svg')
png_filename = os.path.join(glyph_dir, base + '.png')
g.clear()
#g.importOutlines(png_filename)
g.importOutlines(svg_filename)
if params['scale']:
g.transform(scale)
except Exception as e:
print("Error with ", g, e)
font.em = old_em
return font
import re
# Return glyph name without .suffix
def glyph_base_name(x):
m = re.match(r"([^.]+)\..+", x)
return m.group(1) if m else x
def glyph_suffix(x):
m = re.match(r"([^.]+)\.(.+)", x)
return m.group(2) if m else ''
def set_bearings(font, bearings, **kwargs):
bearing_dict = {}
for row in bearings[1:]:
bearing_dict[row[0]] = row
for g in font:
key = g
m = glyph_base_name(key)
if not key in bearing_dict:
if m and m in bearing_dict:
key = m
else:
key = 'Default'
if bearing_dict[key][1] != '':
font[g].left_side_bearing = int(bearing_dict[key][1] * (params['scale'] or 1))
else:
font[g].left_side_bearing = int(bearing_dict['Default'][1] * (params['scale'] or 1))
if bearing_dict[key][2] != '':
font[g].right_side_bearing = int(bearing_dict[key][2] * (params['scale'] or 1))
else:
font[g].right_side_bearing = int(bearing_dict['Default'][2] * (params['scale'] or 1))
if 'space' not in bearing_dict:
space = font.createMappedChar('space')
space.width = int(font.em / 5)
return font
NOTE: This removes the old kerning table.
def get_classes(row):
result = []
for x in row:
if x == "" or x == "None" or x is None:
result.append(None)
elif isinstance(x, str):
result.append(x.split(','))
else:
result.append(x)
return result
def kern_classes(font, kerning_matrix):
try:
font.removeLookup('kern')
print("Old table removed.")
except:
print("Starting from scratch")
font.addLookup("kern", "gpos_pair", 0, [["kern",[["latn",["dflt"]]]]])
offsets = np.asarray(kerning_matrix)
classes_right = [None if (x == "" or x == "None") else x.split(",") for x in offsets[0,1:]]
classes_left = [None if (x == "" or x == "None") else x.split(',') for x in offsets[1:,0]]
offset_list = [0 if x == "" else (int(int(x) * (params['scale'] or 1))) for x in offsets[1:,1:].reshape(-1)]
#print('left', len(classes_left), classes_left)
#print('right', len(classes_right), classes_right)
#print('offset', len(offset_list), offset_list)
font.addKerningClass("kern", "kern-1", classes_left, classes_right, offset_list)
return font
While trying to figure out kerning, I came across this issue that described how you sometimes need a character-pair kern table instead of just class-based kerning. Since I had figured out character-based kerning before I figured out class-based kerning, it was easy to restore my Python code that takes the same kerning matrix and generates character pairs. Here’s what that code looks like.
def kern_by_char(font, kerning_matrix):
# Add kerning by character as backup
font.addLookupSubtable("kern", "kern-2")
offsets = np.asarray(kerning_matrix)
classes_right = [None if (x == "" or x == "None") else x.split(",") for x in offsets[0,1:]]
classes_left = [None if (x == "" or x == "None") else x.split(',') for x in offsets[1:,0]]
for r, row in enumerate(classes_left):
if row is None: continue
for first_letter in row:
g = font.createMappedChar(first_letter)
for c, column in enumerate(classes_right):
if column is None: continue
for second_letter in column:
if kerning_matrix[r + 1][c + 1]:
g.addPosSub("kern-2", second_letter, 0, 0, int((params['scale'] or 1) * kerning_matrix[r + 1][c + 1]), 0, 0, 0, 0, 0)
return font
def copy_glyphs(font, edited):
edited.selection.all()
edited.copy()
font.selection.all()
font.paste()
return font
I wanted to be able to easily compare different versions of my font:
my original glyphs versus my tweaked glyphs, simple spacing versus
kerned. This was a hassle with FontForge, since I had to open
different font files in different Metrics windows. If I execute a
little bit of source code in my Org Mode, though, I can use my test
web page to view all the different versions. By arranging my Emacs
windows a certain way and adding :eval no
to the Org Babel blocks
I’m not currently using, I can easily change the relevant table
entries and evaluate the whole buffer to regenerate the font versions,
including exports to OTF and WOFF.
This code helps me update my hand-edited fonts.
def kern_existing_font(filename=None, font=None, bearings=None, kerning_matrix=None, **kwargs):
if font is None:
font = load_font(filename)
font = set_bearings(font, bearings)
font = kern_classes(font, kerning_matrix)
font = kern_by_char(font, kerning_matrix)
print("Saving %s" % filename)
save_font(font, font_filename=filename)
#with open("test-%s.html" % font.fontname, 'w') as f:
# f.write(test_font_html(font.fontname + '.woff'))
return font
<<def_cut_glyphs>>
<<def_import_glyphs>>
<<def_set_bearings>>
<<def_kern_classes>>
<<def_kern_by_char>>
<<def_kern_existing_font>>
<<def_test_font_html>>
Left | Right | |
---|---|---|
Default | 60 | 60 |
A | 60 | -50 |
B | 60 | 0 |
C | 60 | -30 |
c | 40 | |
b | 40 | |
D | 10 | |
d | 30 | 30 |
e | 30 | 40 |
E | 70 | 10 |
F | 70 | 0 |
f | 0 | -20 |
G | 60 | 30 |
g | 20 | 60 |
H | 80 | 80 |
h | 40 | 40 |
I | 80 | 50 |
i | 30 | |
J | 40 | 30 |
j | -70 | 40 |
k | 40 | 20 |
K | 80 | 0 |
H | 10 | |
L | 80 | 10 |
l | 0 | |
M | 60 | 30 |
m | 40 | |
N | 70 | 10 |
O | 70 | 10 |
o | 40 | 40 |
P | 70 | 0 |
p | 40 | |
Q | 70 | 10 |
q | 20 | 30 |
R | 70 | -10 |
r | 40 | |
S | 60 | 60 |
s | 20 | 40 |
T | -10 | |
t | -10 | 20 |
U | 70 | 20 |
u | 40 | 40 |
V | -10 | |
v | 20 | 20 |
W | 70 | 20 |
w | 40 | 40 |
X | -10 | |
x | 10 | 20 |
y | 20 | 30 |
Y | 40 | 0 |
Z | -10 | |
z | 10 | 20 |
Rows are first characters, columns are second characters.
None | o,a,c,e,d,g,q,w | f,t | x,v,z | h,b,l,i | j | m,n,p,r,u | k | y | s | T | F | zero | |
None | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |||
f | 0 | -30 | -61 | -20 | 0 | 0 | -150 | -70 | |||||
t | 0 | -50 | -41 | -20 | 0 | 0 | 0 | -150 | -10 | ||||
i | -40 | -150 | |||||||||||
r | 0 | -32 | -40 | 0 | 0 | -170 | 29 | ||||||
k | 0 | -10 | -50 | 0 | -48 | -150 | -79 | ||||||
l | 0 | -10 | -20 | 0 | 0 | 0 | 0 | -110 | -20 | ||||
v | 0 | -40 | -35 | -15 | 0 | 0 | 0 | -170 | 30 | ||||
b,o,p | 0 | -40 | 0 | 0 | 0 | 0 | -170 | 43 | |||||
n,m | -30 | -170 | |||||||||||
a | 0 | -23 | -30 | 0 | 0 | 0 | 0 | -170 | 7 | ||||
W | 0 | -40 | -30 | -10 | 0 | 0 | 0 | ||||||
T | 0 | -150 | -120 | -120 | -30 | -40 | -130 | -100 | -80 | 0 | |||
F | 0 | -90 | -90 | -70 | -30 | 0 | -70 | -50 | -80 | -40 | |||
P | 0 | -100 | -70 | -50 | 0 | -70 | -30 | -80 | -20 | ||||
g | 40 | -120 | |||||||||||
q,d,h,y,j | 30 | 30 | 30 | 30 | 30 | -100 | |||||||
c,e,s,u,w,x,z | -120 | ||||||||||||
V | -70 | 30 | 30 | -80 | -20 | -40 | -40 | -10 | |||||
A | 30 | 60 | 30 | 30 | 20 | 40 | 20 | -80 | -120 | 20 | 20 | ||
Y | 20 | 60 | 30 | 30 | 20 | 20 | 40 | 20 | -10 | ||||
M,N,H,I | 20 | 10 | 40 | 30 | 10 | 20 | 20 | ||||||
O,Q,D,U | 50 | 40 | 30 | -20 | 30 | 20 | 30 | -70 | |||||
J | 40 | 20 | 20 | -20 | 10 | 10 | 30 | -30 | |||||
C | 10 | 40 | 10 | 30 | 30 | 30 | 20 | -30 | |||||
E | -10 | 50 | 10 | -20 | 10 | 20 | |||||||
L | -10 | -10 | -30 | 20 | -90 | ||||||||
P | -50 | 30 | 20 | 20 | 20 | 20 | -30 | ||||||
K,R | 20 | 20 | 20 | 10 | 20 | 20 | 20 | -60 | |||||
G | 20 | 40 | 30 | 30 | 20 | 20 | 20 | -100 | 10 | ||||
B,S,X,Z | 20 | 40 | 30 | 30 | 20 | 20 | 20 | 20 | -20 | 10 |
<<def_all>>
font = fontforge.open('sachacHandLightEdited.sfd')
font.fontname = 'sachacHand-Light'
font.familyname = 'sachacHand'
font.fullname = 'sachacHand Light'
font.os2_weight = 200
font.os2_family_class = 10 * 256 + 8
font.os2_vendor = 'SC83'
#with open('../LICENSE', 'r') as file:
# font.copyright = file.read()
kern_existing_font(font=font, bearings=bearings, kerning_matrix=kerning_matrix)
Left | Right | |
---|---|---|
Default | 30 | 30 |
A | 40 | -90 |
B | 20 | 0 |
C | 40 | -30 |
b | 40 | |
D | 60 | 10 |
d | -10 | |
e | 20 | |
E | 60 | 20 |
F | 70 | 20 |
f | -50 | -10 |
G | 40 | 30 |
g | 20 | 40 |
I | 70 | 50 |
i | 30 | |
J | -10 | 30 |
j | -40 | 50 |
k | 40 | 20 |
K | 50 | 0 |
H | 50 | 30 |
L | 60 | 10 |
l | 40 | 40 |
M | 70 | 40 |
m | 40 | |
N | 70 | 30 |
O | 40 | 10 |
P | 60 | 0 |
p | 20 | |
Q | 40 | 10 |
q | 20 | 30 |
R | 50 | -10 |
S | 20 | 30 |
s | 20 | 40 |
T | -10 | |
t | -40 | 0 |
U | 60 | 10 |
u | 20 | |
V | -10 | |
v | 20 | 20 |
W | 50 | 20 |
X | -10 | |
x | 10 | 20 |
y | 20 | 30 |
Y | 40 | 20 |
Z | -10 | |
z | 10 | 20 |
None | m,n,p,r | h,b,l,i,k | o,a,c,e,d,g,q,w,u | f,t | x,v,z | j | y | s | T | J | F,B,D,E,H,I,K,L,M,N,P,R | V | A,C,G,K,O,Q,S,W | U | X | Y | Z | zero | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
None | 110 | ||||||||||||||||||
f | -10 | 20 | -60 | 0 | -90 | -40 | -190 | -80 | 20 | ||||||||||
t | 20 | -20 | 10 | -70 | -100 | 10 | |||||||||||||
i | -30 | 10 | -90 | -160 | -20 | -20 | |||||||||||||
r | -10 | -80 | -90 | -40 | -190 | -100 | -10 | -50 | -50 | -10 | -50 | ||||||||
k | -10 | -10 | -20 | -10 | -90 | -100 | 10 | -30 | -30 | -10 | |||||||||
l | -20 | 10 | -50 | -20 | -100 | 10 | -20 | -30 | -30 | ||||||||||
v | -30 | 10 | -50 | -100 | 10 | -30 | -30 | -20 | |||||||||||
b,o,p | -20 | 10 | -90 | -100 | 10 | -10 | -30 | -30 | -30 | -10 | |||||||||
n,m | 10 | -90 | -100 | 10 | -10 | -20 | -30 | -10 | |||||||||||
a | -30 | -20 | -90 | -10 | -140 | -30 | -60 | -40 | -20 | -40 | |||||||||
W | 20 | -100 | 10 | -20 | |||||||||||||||
T | -70 | -30 | -100 | -70 | -90 | -120 | -30 | -80 | -100 | -50 | |||||||||
F | -50 | -70 | -100 | -50 | |||||||||||||||
g | -10 | 10 | -50 | -140 | 10 | -20 | |||||||||||||
d | 10 | 10 | 20 | 10 | -50 | 10 | -100 | 10 | 10 | ||||||||||
h,q,y,j | 10 | 20 | 10 | -50 | 10 | -130 | 10 | -20 | 10 | ||||||||||
c,e,s,u,w,x,z | -20 | 10 | 10 | -50 | -130 | 10 | -40 | -40 | -20 | ||||||||||
V | -20 | -70 | 30 | 30 | -80 | -40 | -40 | -30 | 0 | ||||||||||
A | 20 | 30 | 30 | 60 | 50 | 20 | 20 | -10 | 60 | 20 | 20 | 20 | 20 | ||||||
Y | 20 | 30 | 20 | 60 | 30 | -50 | 40 | 20 | -10 | 40 | 30 | ||||||||
M,N,H,I | 20 | 20 | 0 | 50 | 30 | -50 | 20 | 40 | 30 | ||||||||||
O,Q,D,U | 30 | 40 | 50 | 40 | -20 | 30 | -70 | 40 | 20 | ||||||||||
J | 10 | 20 | 40 | 20 | -20 | 30 | -30 | 80 | 20 | ||||||||||
C | 30 | 30 | 10 | 40 | 10 | 20 | -30 | 80 | 20 | ||||||||||
E | 10 | 10 | -10 | 50 | -20 | 20 | 110 | ||||||||||||
L | -10 | -10 | -30 | 20 | -90 | 20 | |||||||||||||
P | 20 | -50 | 30 | 20 | 20 | -30 | 80 | ||||||||||||
K,R | 20 | 10 | 20 | 20 | 20 | 20 | -60 | 50 | |||||||||||
G | 20 | 30 | 20 | 40 | 30 | 20 | -100 | 10 | 10 | ||||||||||
B,S,X,Z | 20 | 30 | 20 | 40 | 30 | 20 | 20 | -20 | 90 | 10 |
<<def_all>>
font = fontforge.open('sachacHandRegularEdited.sfd')
font.fontname = 'sachacHand-Regular'
font.familyname = 'sachacHand'
font.fullname = 'sachacHand Regular'
font.os2_weight = 400
font.os2_family_class = 10 * 256 + 8
font.os2_vendor = 'SC83'
with open('../LICENSE', 'r') as file:
font.copyright = file.read()
kern_existing_font(filename="sachacHandRegularEdited.sfd",bearings=bearings, kerning_matrix=kerning_matrix)
For cutting the glyphs:
<<params>>
<<def_all>>
params = {**params,
'sample_file': '../samples/output.png',
'direction': 'horizontal',
'height': 1000, 'width': 1000, 'row_padding': 100,
'scale': 1.2,
'name_list': 'glyphlist.txt',
'direction': 'horizontal',
'new_font_file': 'sachacHand-ipad.sfd',
'new_otf': 'sachacHand-ipad.otf'}
Kerning:
Left | Right | |
---|---|---|
Default | 30 | 30 |
A | 30 | -4 |
B | 60 | 0 |
C | 20 | -30 |
b | 40 | |
D | 40 | 10 |
d | 13 | |
e | 20 | |
E | 50 | 20 |
F | 50 | 0 |
f | -50 | -80 |
G | 40 | 30 |
g | 20 | 40 |
H | 50 | 50 |
h | 14 | 14 |
I | 60 | 50 |
i | 30 | |
J | -10 | 30 |
j | -20 | 30 |
k | 40 | 20 |
K | 70 | 0 |
H | 10 | |
L | 60 | 10 |
l | 0 | |
M | 60 | |
m | 40 | |
N | 60 | 10 |
n | 35 | |
o | 5 | |
O | 40 | 10 |
P | 60 | 0 |
p | 8 | 15 |
Q | 40 | 10 |
q | 20 | 30 |
R | 50 | -10 |
S | 30 | 30 |
s | 20 | 40 |
T | -10 | |
t | -40 | 0 |
U | 60 | 20 |
u | 20 | |
V | -10 | |
v | 20 | 20 |
W | 50 | 20 |
X | -10 | |
x | 10 | 20 |
y | 20 | 30 |
Y | 40 | 0 |
Z | -10 | |
z | 10 | 20 |
exclam | 50 | |
None | o,a,c,e,d,g,q,w | f,t | x,v,z | h,b,l,i | j | m,n,p,r,u | k | y | s | T | F | V | zero | |
None | 20 | |||||||||||||
n,m | 20 | -90 | -100 | -100 | ||||||||||
f | -10 | 0 | 20 | -90 | 10 | 20 | -40 | -190 | 20 | |||||
t | -20 | 10 | -70 | 20 | 20 | -100 | ||||||||
i | -30 | 10 | -90 | -160 | ||||||||||
r | -70 | -10 | -90 | -60 | -220 | |||||||||
k | -20 | -10 | -10 | -90 | -10 | -100 | -10 | |||||||
l | 10 | 20 | -100 | |||||||||||
v | -30 | 10 | -50 | -100 | ||||||||||
b,o,p | 10 | -90 | -100 | |||||||||||
a | -90 | -10 | -100 | |||||||||||
W | 20 | -100 | ||||||||||||
T | -120 | -70 | -90 | -30 | -120 | -70 | -30 | -30 | -80 | -100 | ||||
F | -90 | -70 | -100 | |||||||||||
g | 10 | -50 | -100 | |||||||||||
q,d,h,y,j | 20 | 10 | -50 | 10 | 10 | 10 | -100 | 10 | ||||||
c,e,s,u,w,x,z | -20 | 10 | 10 | -10 | -50 | -100 | ||||||||
V | -70 | 30 | 30 | -80 | -20 | -40 | -40 | -10 | ||||||
A | 30 | 60 | 30 | 30 | 20 | 40 | 20 | 20 | -10 | 20 | 20 | |||
Y | 20 | 60 | 30 | 30 | 20 | 20 | 40 | 20 | -10 | |||||
M,N,H,I | 20 | 50 | 40 | 30 | 10 | 20 | 20 | |||||||
O,Q,D,U | 50 | 40 | 30 | -20 | 30 | 20 | 30 | -70 | ||||||
J | 40 | 20 | 20 | -20 | 10 | 10 | 30 | -30 | ||||||
C | 10 | 40 | 10 | 30 | 30 | 30 | 20 | -30 | ||||||
E | -10 | 50 | 10 | -20 | 10 | 20 | ||||||||
L | -10 | -10 | -30 | 20 | -90 | |||||||||
P | -50 | 30 | 20 | 20 | 20 | 20 | -30 | |||||||
K,R | 20 | 20 | 20 | 10 | 20 | 20 | 20 | -60 | ||||||
G | 20 | 40 | 30 | 30 | 20 | 20 | 20 | -100 | 10 | |||||
B,S,X,Z | 20 | 40 | 30 | 30 | 20 | 20 | 20 | 20 | -20 | 10 | ||||
W | -70 |
<<font_params>>
cut_glyphs(**params)
https://www.youtube.com/watch?v=WqSQU7nuTsc https://www.tug.org/TUGboat/tb24-3/williams.pdf https://typedrawers.com/discussion/1357/how-can-i-randomize-letters-in-a-typeface http://learn.scannerlicker.net/2015/06/12/making-a-font-maximal-part-iii/
Expanding the kerning matrix:
- Specify list of variant glyphs to add to existing classes if not specified
- Specify suffixes, try each glyph to see if it exists
- Check the font to see what other glyphs are specified, add to those classes
<<def_all>>
def get_stylistic_set(font, suffix):
return [g for g in font if suffix in g]
def add_character_variants(font, sets):
if not 'calt' in font.gsub_lookups:
font.addLookup('calt', 'gsub_contextchain', 0, [['calt', [['latn', ['dflt']]]]])
prev_tag = ''
for i, sub in enumerate(sets):
if not sub in font.gsub_lookups:
font.addLookup(sub, 'gsub_single', 0, [])
font.addLookupSubtable(sub, sub + '-1')
alt_set = get_stylistic_set(font, sub)
for g in alt_set:
get_glyph(font, glyph_base_name(g)).addPosSub(sub + '-1', g)
default = [glyph_base_name(g) for g in alt_set]
prev_set = [glyph_base_name(g) + prev_tag for g in alt_set]
print('%d | %d @<ss%02d>' % (i + 1, 1, i + 1))
print(default)
default = default + ['0']
try: font.removeLookupSubtable('calt-%d' % (i + 1))
except Exception: pass
print(prev_set)
if i == 0:
font.addContextualSubtable('calt', 'calt-%d' % (i + 1), 'class', '%d | %d @<ss%02d>' % (i + 1, 1, i + 1),
bclasses=(None, default), mclasses=(None, default))
else:
font.addContextualSubtable('calt', 'calt-%d' % (i + 1), 'class', '%d | %d @<ss%02d>' % (i + 1, 1, i + 1),
bclasses=(None, default, prev_set), mclasses=(None, default, prev_set))
prev_tag = '.' + sub
return font
font = fontforge.open('sachacHand-Regular-V.sfd')
params = {**params,
'row_padding': 50,
'sample_file': 'sample-sachacHand-regular-variant1.png',
'new_font_file': 'sachacHandRegular-Variants.sfd',
'new_otf': 'sachacHandRegular-Variants.otf',
'letters': None,
'matrix':
['H.ss01,e.ss01,q.ss01,A.ss01,M.ss01,Y.ss01,eight.ss01,quotesingle.ss01,numbersign.ss01,less.ss01',
'O.ss01,b.ss01,r.ss01,B.ss01,N.ss01,Z.ss01,nine.ss01,quoteright.ss01,dollar.ss01,greater.ss01',
'n.ss01,h.ss01,u.ss01,R.ss01,P.ss01,zero.crossed,question.ss01,quotedbl.ss01,bracketleft.ss01',
'o.ss01,k.ss01,w.ss01,D.ss01,Q.ss01,one.ss01,colon.ss01,quotedblleft.ss01,ampersand,bracketright.ss01',
'd.ss01,l.ss01,v.ss01,L.ss01,S.ss01,two.ss01,semicolon.ss01,quotedblright.ss01,parenleft.ss01,asciicircum.ss01',
'p.ss01,f.ss01,x.ss01,E.ss01,T.ss01,three.ss01,hyphen.ss01,at.ss01,parenright.ss01,grave.ss01',
'a.ss01,t.ss01,y.ss01,F.ss01,U.ss01,four.ss01,endash.ss01,slash.ss01,asterisk.ss01,braceleft.ss01',
'g.ss01,i.ss01,z.ss01,I.ss01,V.ss01,five.ss01,emdash.ss01,backslash.ss01,plus.ss01,bar.ss01',
's.ss01,j.ss01,C.ss01,J.ss01,W.ss01,six.ss01,equal.ss01,asciitilde.ss01,comma.ss01,braceright.ss01',
'c.ss01,m.ss01,G.ss01,K.ss01,X.ss01,seven.ss01,exclam.ss01,underscore.ss01,period.ss01,zero.ss01']}
cut_glyphs(**params)
matrix = glyph_matrix(font=font, matrix=params['matrix'])
import_glyphs(font, **params)
# params = {**params,
# 'sample_file': 'sample-sachacHand-bold.png',
# 'matrix':
# ['H.ss02,e.ss02,q.ss02,A.ss02,M.ss02,Y.ss02,eight.ss02,quotesingle.ss02,numbersign.ss02,less.ss02',
# 'O.ss02,b.ss02,r.ss02,B.ss02,N.ss02,Z.ss02,nine.ss02,quoteright.ss02,dollar.ss02,greater.ss02',
# 'n.ss02,h.ss02,u.ss02,R.ss02,P.ss02,zero.ss02,question.ss02,quotedbl.ss02,bracketleft.ss02',
# 'o.ss02,k.ss02,w.ss02,D.ss02,Q.ss02,one.ss02,colon.ss02,quotedblleft.ss02,ampersand,bracketright.ss02',
# 'd.ss02,l.ss02,v.ss02,L.ss02,S.ss02,two.ss02,semicolon.ss02,quotedblright.ss02,parenleft.ss02,asciicircum.ss02',
# 'p.ss02,f.ss02,x.ss02,E.ss02,T.ss02,three.ss02,hyphen.ss02,at.ss02,parenright.ss02,grave.ss02',
# 'a.ss02,t.ss02,y.ss02,F.ss02,U.ss02,four.ss02,endash.ss02,slash.ss02,asterisk.ss02,braceleft.ss02',
# 'g.ss02,i.ss02,z.ss02,I.ss02,V.ss02,five.ss02,emdash.ss02,backslash.ss02,plus.ss02,bar.ss02',
# 's.ss02,j.ss02,C.ss02,J.ss02,W.ss02,six.ss02,equal.ss02,asciitilde.ss02,comma.ss02,braceright.ss02',
# 'c.ss02,m.ss02,G.ss02,K.ss02,X.ss02,seven.ss02,exclam.ss02,underscore.ss02,period.ss02,None']}
# cut_glyphs(**params)
# import_glyphs(font, **params)
# set_bearings(font, bearings)
# variants = ['ss01', 'ss02']
def expand_classes(array, new_glyphs):
not_found = []
for g in new_glyphs:
found_exact = None
found_base = None
base = glyph_base_name(g)
for i, class_glyphs in enumerate(array):
if class_glyphs is None: continue
if isinstance(class_glyphs, str):
class_glyphs = class_glyphs.split(',')
array[i] = class_glyphs
for glyph in class_glyphs:
if glyph == g:
found_exact = i
break
if glyph == base:
found_base = i
break
if found_exact: continue
elif found_base: array[found_base].append(g)
else: not_found.append(g)
return ([','.join(x) for x in array], not_found)
# def expand_kerning_matrix(font=font, kerning_matrix=kerning_matrix, new_glyphs=[]):
# classes_right = [None if (x == "" or x == "None") else x.split(",") for x in offsets[0,1:]]
# classes_left = [None if (x == "" or x == "None") else x.split(',') for x in offsets[1:,0]]
# right_glyphs = np.asarray(offsets[0,1:]).reshape(-1)
# # Expand all the right glyphs
# for i, c in enumerate(kerning_matrix[0]):
# if c is None: continue
# glyphs = c.split(',')
# for g in glyphs:
alt_set = get_stylistic_set(font, 'ss02')
(classes_right, not_found) = expand_classes(list(kerning_matrix[0]), alt_set)
(classes_left, not_found) = expand_classes([x[0] for x in kerning_matrix], alt_set)
kerning_matrix[0] = classes_right
for i, c in enumerate(classes_left):
kerning_matrix[i][0] = c
font = kern_classes(font, kerning_matrix)
font = kern_by_char(font, kerning_matrix)
add_character_variants(font, variants)
#font.mergeFeature('sachacHand-Regular-V.fea')
font.familyname = 'sachacHand'
font.fullname = 'sachacHand Regular Variants'
font.os2_weight = 400
font.os2_family_class = 10 * 256 + 8
font.os2_vendor = 'SC83'
font.fontname = 'sachacHand-Regular-V'
font.buildOrReplaceAALTFeatures()
# TODO Just plop them into different fonts, darn it.
save_font(font)
with open("test-%s.html" % font.fontname, 'w') as f:
f.write(test_font_html(font.fontname + '.woff', variants=variants))
Okay, why isn’t it triggering when we start off with 0?
font-feature-settings: “calt” 0; turns off variants. Works in Chrome, too.
This lets me quickly try text with different versions of my font. I can also look at lots of kerning pairs at the same time.
Resources:
Output | Font filename | Class |
---|---|---|
test-regular.html | sachacHand.woff | regular |
test-bold.html | sachacHandBold.woff | bold |
test-black.html | sachacHandBlack.woff | black |
test-new.html | sachacHand-New.woff2 | new |
strings = ["hhhhnnnnnnhhhhhnnnnnn",
"ooonoonnonnn",
"nnannnnbnnnncnnnndnnnnennnnfnnnngnnnnhnnnninnnnjnn",
"nnknnnnlnnnnmnnnnnnnnnonnnnpnnnnqnnnnrnnnnsnnnntnn",
"nnunnnnvnnnnwnnnnxnnnnynnnnznn",
"HHHOHHOOHOOO",
"HHAHHHHBHHHHCHHHHDHHHHEHHHHFHHHHGHHHHHHHHHIHHHHJHH",
"HHKHHHHLHHHHMHHHHNHHHHOHHHHPHHHHQHHHHRHHHHSHHHHTHH",
"HHUHHHHVHHHHWHHHHXHHHHYHHHHZHH",
"Having fun kerning using Org Mode and FontForge",
"Python+FontForge+Org: I made a font based on my handwriting!",
"Monthly review: May 2020",
"Emacs News 2020-06-01",
"Projects"]
def test_strings(strings, font, variants=None):
doc, tag, text, line = Doc().ttl()
line('h2', 'Test strings')
if variants:
for s in strings:
with tag('table'):
with tag('tr'):
with tag('td', 'nocalt'):
text(s)
for v in variants:
with tag('tr'):
line('td', v)
with tag('td', klass=v + ' nocalt'):
text(s)
else:
with tag('table'):
for s in strings:
with tag('tr'):
with tag('td'):
text(s)
return doc.getvalue()
def test_kerning_matrix(font):
sub = font.getLookupSubtables(font.gpos_lookups[0])
doc, tag, text, line = Doc().ttl()
for s in sub:
if font.isKerningClass(s):
(classes_left, classes_right, array) = font.getKerningClass(s)
kerning = np.array(array).reshape(len(classes_left), len(classes_right))
with tag('table', style='border-collapse: collapse'):
for r, row in enumerate(classes_left):
if row is None: continue
for j, first_letter in enumerate(row):
if first_letter == None: continue
style = "border-top: 1px solid gray" if j == 0 else ""
g1 = aglfn.to_glyph(glyph_base_name(first_letter))
c1 = glyph_suffix(first_letter)
with tag('tr', style=style):
line('td', first_letter)
for c, column in enumerate(classes_right):
if column is None: continue
for i, second_letter in enumerate(column):
if second_letter is None: continue
g2 = aglfn.to_glyph(glyph_base_name(second_letter))
c2 = glyph_suffix(second_letter)
klass = "kerned" if kerning[r][c] else "default"
style = "border-left: 1px solid gray" if i == 0 else ""
with tag('td', klass=klass, style=style):
doc.asis('<span class="base">n</span><span class="%s" title="%s">%s</span><span class="%s" title="%s">%s</span><span class="base">n</span>' % (c1, first_letter, g1, c2, second_letter, g2))
return doc.getvalue()
from yattag import Doc
import numpy as np
import fontforge
import aglfn
def test_glyphs(font, count=1):
return ''.join([(aglfn.to_glyph(g) or "") * count for g in font if (font[g].isWorthOutputting() and font[g].unicode > -1)])
def test_font_html(font_filename=None, variants=None):
doc, tag, text, line = Doc().ttl()
font = fontforge.open(font_filename)
name = font.fontname
with tag('html'):
with tag('head'):
doc.asis('<link rel="stylesheet" type="text/css" href="style.css" />')
doc.asis('<meta charset="UTF-8">')
with tag('style'):
doc.asis("@font-face { font-family: '%s'; src: url('%s'); }\n" % (name, font_filename))
doc.asis("body { font-family: '%s'; }\n" % name)
doc.asis(".bold { font-weight: bold } .italic { font-style: italic } .oblique { font-style: oblique }")
doc.asis(".small-caps { font-variant: small-caps }")
if variants:
for v in variants:
doc.asis('.%s { font-feature-settings: "calt" off, "%s" on; }' % (v, v))
with tag('body'):
with tag('a', href='index.html'):
text('Back to index')
with tag('div', style='float: right'):
with tag('a', href=font.fullname + '.woff'):
text('WOFF')
text(' | ')
with tag('a', href=font.fullname + '.otf'):
text('OTF')
line('h1', font.fullname)
line('h2', 'Glyphs and sizes')
with tag('table'):
for size in [10, 14, 20, 24, 36, 72]:
with tag('tr', style='font-size: %dpt' % size):
line('td', size)
line('td', test_glyphs(font))
if variants:
line('h2', 'Variants')
line('div', test_glyphs(font, 4))
with tag('table', klass='nocalt'):
for v in variants:
with tag('tr'):
line('td', v)
with tag('td', klass=v):
text(test_glyphs(font))
line('h2', 'Transformations')
with tag('table'):
for t in ['normal', 'bold', 'italic', 'oblique', 'bold italic', 'bold oblique', 'small-caps', 'bold small-caps']:
with tag('tr', klass=t):
line('td', t)
line('td', test_glyphs(font))
line('h2', 'Size')
with tag('div'):
line('span', "Hello world")
line('span', "Hello world", klass='basefont')
with tag('table'):
with tag('tr'):
line('td', test_glyphs(font))
line('td.base', test_glyphs(font))
doc.asis(test_strings(strings, font, variants))
line('h2', 'Kerning matrix')
with tag('div', klass='nocalt'):
doc.asis(test_kerning_matrix(font))
line('h2', 'License')
with tag('pre', klass='license'):
text(font.copyright)
# http://famira.com/article/letterproef
font.close()
return doc.getvalue()
<<def_test_html>>
font_files = ['sachacHand-Light.woff', 'sachacHand-Regular.woff', 'sachacHand-Bold.woff']
fonts = {}
# Write the main page
with open('index.html', 'w') as f:
doc, tag, text, line = Doc().ttl()
with tag('html'):
with tag('head'):
doc.asis('<link rel="stylesheet" type="text/css" href="style.css" />')
with tag('style'):
for p in font_files:
fonts[p] = fontforge.open(p)
doc.asis("@font-face { font-family: '%s'; src: url('%s'); }\n" % (fonts[p].fontname, p))
doc.asis(".%s { font-family: '%s'; }" % (fonts[p].fontname, fonts[p].fontname))
with tag('body'):
with tag('a', href='https://github.com/sachac/sachac-hand'):
text('View source code on Github')
line('h1', 'Summary')
line('h2', 'Glyphs')
with tag('table'):
for p in fonts:
with tag('tr', klass=fonts[p].fontname):
with tag('td'):
with tag('a', href='test-%s.html' % fonts[p].fontname):
text(fonts[p].fullname)
line('td', test_glyphs(fonts[p]))
line('h2', 'Strings')
with tag('table', style='border-bottom: 1px solid gray; width: 100%; border-collapse: collapse'):
for s in strings:
for i, p in enumerate(fonts):
style = 'border-top: 1px solid gray' if (i == 0) else ""
with tag('tr', klass=fonts[p].fontname, style=style):
with tag('td'):
with tag('a', href='test-%s.html' % fonts[p].fontname):
text(fonts[p].fullname)
line('td', s)
f.write(doc.getvalue())
Oh, can I get livereload working? There’s a python3-livereload
… Ah, it’s as simple as running livereload
.
- State “DONE” from “TODO” [2020-06-06 Sat 22:33]
import os
<<params>>
def export_glyphs(font, directory):
for g in font:
if font[g].isWorthOutputting():
filename = os.path.join(directory, g)
font[g].export(filename + ".png", params['em'], 1)
subprocess.call(["convert", filename + ".png", filename + ".pbm"])
subprocess.call(["autotrace", "-centerline", "-output-file", filename + ".svg", filename + ".pbm"])
def zero_glyphs(font, directory):
for g in font:
glyph = font[g]
if glyph.isWorthOutputting():
glyph.clear()
glyph.importOutlines(os.path.join(directory, g + '.svg'))
return font
font = load_font(params['new_font_file'])
directory = 'exported-glyphs'
# export_glyphs(font, directory)
font = zero_glyphs(font, directory)
font.fontname = 'sachacHand-Zero'
font.fullname = 'sachacHand Zero'
font.weight = 'Zero'
save_font(font, {**params, "new_font_file": "sachacHandZero.sfd", "new_otf": "sachacHandZero.otf"})
Huh. I want the latest version so that I can pass keyword arguments.
1023,/home/sacha/vendor/fontforge% cd build cmake -GNinja .. -DENABLE_FONTFORGE_EXTRAS=ON ninja ninja install
https://superuser.com/questions/1337567/how-do-i-convert-a-ttf-into-individual-png-character-images
https://wiki.inkscape.org/wiki/index.php/CalligraphedOutlineFill ?
import inkex
<<params>>
params = {**params,
'sample_file': 'a-kiddo-sample.png',
'new_font_file': 'aKiddoHand.sfd',
'new_otf': 'aKiddoHand.otf',
'new_font_name': 'aKiddoHand',
'new_family_name': 'aKiddoHand',
'new_full_name': 'aKiddoHand'}
cd ~/code/docker/blog
docker-compose up mysql
from dotenv import load_dotenv
from sqlalchemy import create_engine
import os
import pandas as pd
import pymysql
load_dotenv(dotenv_path="/home/sacha/code/docker/blog/.env", verbose=True)
sqlEngine = create_engine('mysql+pymysql://' + os.getenv('PYTHON_DB'), pool_recycle=3600)
dbConnection = sqlEngine.connect()
<<connect-to-db>>
from yattag import Doc, indent
doc, tag, text, line = Doc().ttl()
with tag('html'):
with tag('head'):
doc.asis('<link rel="stylesheet" type="text/css" href="style.css" />')
with tag('body', klass="blog-heading"):
result = dbConnection.execute("select id, post_title from wp_posts WHERE post_type='post' AND post_status='publish' AND post_password='' order by id desc")
for row in result:
with tag('h2'):
with tag('a', href="https://sachachua.com/blog/p/%s" % row['id']):
text(row['post_title'])
dbConnection.close()
with open('test-blog.html', 'w') as f:
f.write(indent(doc.getvalue(), indent_text=True))
<<connect-to-db>>
df = pd.read_sql("select post_title from wp_posts WHERE post_type='post' AND post_status='publish'", dbConnection);
# Debugging
#q = df[~df['post_title'].str.match('^[A-Za-z0-9\? "\'(),\-:\.\*;/@\!\[\]=_&\?\$\+#^{}\~]+$')]
#print(q)
from collections import Counter
df['filtered'] = df.post_title.str.replace('[A-Za-z0-9\? "\'(),\-:\.\*;/@\!\[\]=_&\?\$\+#^{}\~]+', '')
#print(df['filtered'].apply(list).sum())
res = Counter(df.filtered.apply(list).sum())
return res.most_common()
<<connect-to-db>>
df = pd.read_sql("select id, post_title from wp_posts WHERE post_type='post' AND post_status='publish' AND post_title LIKE %(char)s limit 10;", dbConnection, params={"char": '%' + char + '%'});
print(df)
<<connect-to-db>>
df = pd.read_sql("select post_title from wp_posts WHERE post_type='post' AND post_status='publish'", dbConnection);
from collections import Counter
s = df.post_title.apply(list).sum()
res = Counter('{}{}'.format(a, b) for a, b in zip(s, s[1:]))
common = res.most_common(100)
return ''.join([x[0] for x in common])
#+RESULTS[5a3f821b4bbfcb462cebc176c66bcb697c6bf4f2]: digrams
innge g s treeron aanesy entit orndthn ee: ted atarr hetont, acstou o fekne rieWe smaalewo 20roea mle w 2itvi e pk rimedietioomchev cly01edlil ve i braisseha Wotdece dcotahih looouticurel laseccssila
import fontforge
import numpy as np
import pandas as pd
f = fontforge.open("/home/sacha/code/font/files/SachaHandEdited.sfd")
return list(map(lambda g: [g.glyphname, g.left_side_bearing, g.right_side_bearing], f.glyphs()))
<<params>>
def show_kerning_classes(f):
kern_name = f.gpos_lookups[0]
lookup_info = f.getLookupInfo(kern_name)
sub = f.getLookupSubtables(kern_name)
for subtable in sub:
(classes_left, classes_right, array) = f.getKerningClass(subtable)
classes_left = list(map(lambda x: 'None' if x is None else ','.join(x), classes_left))
classes_right = list(map(lambda x: 'None' if x is None else ','.join(x), classes_right))
kerning = np.array(array).reshape(len(classes_left), len(classes_right))
df = pd.DataFrame(data=kerning, index=classes_left, columns=classes_right)
out(df)
import fontforge
<<def_show_kerning_classes>>
show_kerning_classes(fontforge.open(font))
scp sachacHand-Regular.woff web:~/sacha-v3/