sachac-hand
sachac-hand copied to clipboard
Working on a handwriting font
#+OPTIONS: toc:2 #+PROPERTY: header-args python :noweb no-export :dir "./files" :exports both :results output :colnames no :eval never-export
- Summary
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 |
- License
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.
- Blog post :PROPERTIES: :ID: o2b:cbd413ee-7c20-47da-9cda-666a2909b0d0 :POST_DATE: [2020-06-05 Fri 00:20] :POSTID: 29568 :BLOG: sacha :END:
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!
- Things I needed to install
=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.
** Errors fixed along the way
- =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
- General font code ** Parameters and common functions
#+NAME: params #+begin_src python :results none :tangle "files/params.py" 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))
#+end_src
** Generate guidelines *** Code to make the template
#+NAME: def_make_template #+begin_src python :results none 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'] #+end_src
*** Actually make the templates
#+begin_src python :results output
<
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 }) #+end_src
#+RESULTS: :results: template-sample.png :end:
** Cut into glyphs
#+NAME: def_cut_glyphs #+begin_src python :eval no 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) #+end_src
** Import SVG outlines into font
#+NAME: def_import_glyphs #+BEGIN_SRC python :results output :eval no 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 #+END_SRC
** Adjust bearings
#+NAME: def_set_bearings #+begin_src python :eval no 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 #+end_src
** Kern the font
*** Kern by classes
NOTE: This removes the old kerning table.
#+NAME: def_kern_classes #+begin_src python :eval no 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
#+end_src
*** Kern by character
While trying to figure out kerning, I came across this issue that described how you sometimes need a [[https://www.dafont.com/forum/read/405813/the-kerning-is-set-in-a-way-that-doesn-t-work-at-dafont-we-use-the-gd-library-of-php][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.
#+NAME: def_kern_by_char #+begin_src python :eval no 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 #+end_src
** Hand-tweak the glyphs
#+NAME: def_copy_glyphs #+begin_src python :eval no def copy_glyphs(font, edited): edited.selection.all() edited.copy() font.selection.all() font.paste() return font #+end_src
- Generate fonts
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.
#+NAME: def_kern_existing_font #+begin_src python :eval no 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 #+end_src
#+NAME: def_all #+begin_src python :eval no <<def_cut_glyphs>> <<def_import_glyphs>> <<def_set_bearings>> <<def_kern_classes>> <<def_kern_by_char>> <<def_kern_existing_font>> <<def_test_font_html>> #+end_src
** Generate sachacHand Light
#+NAME: light_bearings | | 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.
#+NAME: light_kerning_matrix | | 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 | |
#+begin_src python :var bearings=light_bearings :var kerning_matrix=light_kerning_matrix :eval yes <<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) #+end_src
#+RESULTS: : Old table removed. : [None, ['f'], ['t'], ['i'], ['r'], ['k'], ['l'], ['v'], ['b', 'o', 'p'], ['n', 'm'], ['a'], ['W'], ['T'], ['F'], ['P'], ['g'], ['q', 'd', 'h', 'y', 'j'], ['c', 'e', 's', 'u', 'w', 'x', 'z'], ['V'], ['A'], ['Y'], ['M', 'N', 'H', 'I'], ['O', 'Q', 'D', 'U'], ['J'], ['C'], ['E'], ['L'], ['P'], ['K', 'R'], ['G'], ['B', 'S', 'X', 'Z']] : [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']] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -61, -20, 0, 0, 0, 0, 0, 0, -150, 0, -70, 0, -50, -41, -20, 0, 0, 0, 0, 0, 0, -150, 0, -10, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, -150, 0, 0, 0, -32, -40, 0, 0, 0, 0, 0, 0, 0, -170, 0, 29, 0, -10, -50, 0, 0, 0, 0, 0, 0, -48, -150, 0, -79, 0, -10, -20, 0, 0, 0, 0, 0, 0, 0, -110, 0, -20, 0, -40, -35, -15, 0, 0, 0, 0, 0, 0, -170, 0, 30, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, -170, 0, 43, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, -170, 0, 0, 0, -23, -30, 0, 0, 0, 0, 0, 0, 0, -170, 0, 7, 0, -40, -30, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -150, -120, -120, -30, -40, -130, 0, -100, -80, 0, 0, 0, 0, -90, -90, -70, -30, 0, -70, 0, -50, -80, -40, 0, 0, 0, -100, -70, -50, 0, 0, -70, 0, -30, -80, -20, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, -120, 0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 0, -100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -120, 0, 0, 0, -70, 30, 30, 0, -80, -20, 0, -40, -40, -10, 0, 0, 0, 30, 60, 30, 30, 0, 20, 40, 20, -80, -120, 20, 20, 0, 20, 60, 30, 30, 0, 20, 20, 40, 20, -10, 0, 0, 0, 20, 10, 40, 30, 0, 10, 20, 20, 0, 0, 0, 0, 0, 0, 50, 40, 30, -20, 30, 20, 30, 0, -70, 0, 0, 0, 0, 40, 20, 20, -20, 10, 10, 30, 0, -30, 0, 0, 0, 10, 40, 10, 30, 0, 30, 30, 20, 0, -30, 0, 0, 0, -10, 50, 0, 10, -20, 10, 0, 20, 0, 0, 0, 0, 0, -10, -10, 0, 0, -30, 0, 0, 20, 0, -90, 0, 0, 0, -50, 30, 20, 20, 0, 0, 20, 20, 0, -30, 0, 0, 0, 20, 20, 20, 10, 0, 20, 20, 20, 0, -60, 0, 0, 0, 20, 40, 30, 30, 0, 20, 20, 20, 0, -100, 10, 0, 0, 20, 40, 30, 30, 0, 20, 20, 20, 20, -20, 10, 0]
** Generate sachacHand Regular
#+NAME: regular_bearings | | 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 |
#+NAME: regular_kerning_matrix | | 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 | | | | | | | |
#+NAME: make_regular #+begin_src python :var bearings=regular_bearings :session out :var kerning_matrix=regular_kerning_matrix :results output <<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) #+end_src
** Generate sachacHand from iPad sample
For cutting the glyphs:
#+NAME: font_params
#+begin_src python
<
Kerning:
#+NAME: new_bearings | | 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 | | | | | |
#+NAME: new_kerning_matrix | | 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 | | | | | | | | | | | | |
#+begin_src python <<font_params>> cut_glyphs(**params) #+end_src
#+RESULTS:
:results:
:end:
#+NAME: make_new
#+begin_src python :var bearings=new_bearings :var kerning_matrix=new_kerning_matrix :results output :tangle make_new.py
<<font_params>>
import psMat
if os.path.exists(params['new_font_file']):
font = fontforge.open(params['new_font_file'])
else:
font = fontforge.font()
fontforge.loadNamelist(params['name_list'])
font = import_glyphs(font, **params)
font.fontname = 'sachacHand-ipad'
font.familyname = 'sachacHand'
font.fullname = 'sachacHand ipad'
font.os2_weight = 400
font.os2_family_class = 10 * 256 + 8
font.encoding = "UnicodeFull"
font.os2_vendor = 'SC83'
kern_existing_font(font=font, bearings=bearings, kerning_matrix=kerning_matrix, filename=params['new_font_file'])
font.generate(font.fontname + '.woff')
font.generate(font.fontname + '.woff2')
font.generateFeatureFile(font.fontname + '.feature')
#save_font(font)
with open("test-%s.html" % font.fontname, 'w') as f:
f.write(test_font_html(font.fontname + '.woff'))
#+end_src
#+RESULTS: make_new :results: Old table removed. Saving sachacHand-ipad.sfd :end:
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/
** Import the glyphs for variant1 and variant2
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
#+begin_src python :eval yes :results output :session "out" :var bearings=regular_bearings :var kerning_matrix=regular_kerning_matrix
<<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)) #+end_src
#+RESULTS:
: Bad name when parsing aglfn for unicode 41
: Old table removed.
: 1 | 1 @
Okay, why isn't it triggering when we start off with 0?
#+RESULTS:
: Bad name when parsing aglfn for unicode 41
: 1 | 1 @
** Okay, how do I space and kern the variants more efficiently? font-feature-settings: "calt" 0; turns off variants. Works in Chrome, too.
- Test the fonts 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:
- http://famira.com/article/letterproef
- http://ninastoessinger.com/stringmaker/index.php
#+NAME: test_fonts | 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 |
#+RESULTS: : [['test-regular.html', 'sachacHand.woff', 'regular'], ['test-bold.html', 'sachacHandBold.woff', 'bold'], ['test-black.html', 'sachacHandBlack.woff', 'black']] : [{'output': 'test-regular.html', 'font_filename': 'sachacHand.woff', 'klass': 'regular'}, {'output': 'test-bold.html', 'font_filename': 'sachacHandBold.woff', 'klass': 'bold'}, {'output': 'test-black.html', 'font_filename': 'sachacHandBlack.woff', 'klass': 'black'}]
#+NAME: def_test_font_html #+begin_src python 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('n%s%sn' % (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('') doc.asis('') 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() #+end_src
#+RESULTS: def_test_font_html :results: :end:
#+NAME: test_html #+begin_src python :results output :session "out" :eval yes <<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('') 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()) #+end_src
#+RESULTS: test_html :results: :end:
Oh, can I get livereload working? There's a =python3-livereload=... Ah, it's as simple as running =livereload=.
- Ideas
** DONE Copy glyphs from hand-edited font
CLOSED: [2020-06-06 Sat 22:33]
:LOGBOOK:
- State "DONE" from "TODO" [2020-06-06 Sat 22:33] :END: ** TODO Alternate glyphs ** TODO Ligatures ** TODO Accents ** Generating a zero-width version? *** Export glyphs, autotrace them, and load them into a different font
#+begin_src python
import os
<
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"}) #+end_src
#+RESULTS: : None
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
#+RESULTS:
https://superuser.com/questions/1337567/how-do-i-convert-a-ttf-into-individual-png-character-images *** TODO Manually edit the glyphs to make them look okay *** TODO Double up the paths and close them
https://wiki.inkscape.org/wiki/index.php/CalligraphedOutlineFill ? #+begin_src python import inkex #+end_src
#+RESULTS: ** TODO Make a font for A-
#+begin_src python
<
#+RESULTS: : None
- Extra stuff
- Get information from my blog database
#+begin_src sh :eval no cd ~/code/docker/blog docker-compose up mysql #+end_src
** Figure out what glyphs I want based on my blog headings
#+NAME: connect-to-db #+begin_src python :eval no 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() #+end_src
** Make test page with blog headings
#+begin_src python :eval no
<
#+RESULTS:
** Check glyphs
#+begin_src python :results table :eval no
<
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() #+end_src
#+RESULTS: |  | 65 | | à | 57 | | ‚ | 39 | | ƒ | 33 | | ’ | 13 | | £ | 8 | | \x81 | 4 | | ¤ | 4 | | » | 4 | | ¦ | 3 | | ¿ | 3 | | – | 3 | | — | 2 | | ¥ | 2 | | ¨ | 2 | | € | 2 | | ō | 2 | | % | 2 | | \t | 1 | | „ | 1 | | Ÿ | 1 | | Š | 1 | | œ | 1 | | ¬ | 1 | | ª | 1 | | ž | 1 | | < | 1 | | > | 1 | | ¹ | 1 | | … | 1 | | § | 1 | | ¸ | 1 | | Ž | 1 | | ¼ | 1 | | Œ | 1 | | \xa0 | 1 | | \x8d | 1 | | † | 1 | | « | 1 | | ā | 1 | | ē | 1 | | č | 1 |
** Look up posts with weird glyphs
#+NAME: check-posts
#+begin_src python :results output :var char="–" :eval no
<
#+RESULTS: check-posts : id post_title : 0 7059 Wiki organization challenge – thinking out loud : 1 7330 Setting up my new tablet PC – apps, config, etc. : 2 22038 Work on the business from the outside, not in ...
** Get frequency of pairs of characters
#+NAME: digrams
#+begin_src python :results value scalar :cache yes :eval no
<
#+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
** Copy metrics from my edited font
*** Get the glyph bearings
#+begin_src python :results table :eval no 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())) #+end_src
#+RESULTS: | a | 39.0 | 38.0 | | b | 39.0 | 38.59677350874102 | | c | 38.807172523099524 | 39.0 | | d | 38.853036079593494 | 37.70218462414317 | | e | 23.0 | 39.0 | | f | 22.0 | 28.0 | | g | 39.0 | 38.839263397187665 | | h | 42.44897959183673 | 32.244897959183675 | | i | 39.0 | 39.0 | | j | 29.0 | 37.07269908475212 | | k | 38.7232 | 38.0 | | l | 38.849996883261696 | 24.0 | | m | 38.88120540762966 | 61.872974804436524 | | n | 38.41699749411689 | 50.09722712588024 | | o | 38.861850745445174 | 38.36155030599474 | | p | 38.72189349112426 | 38.806185204215126 | | q | 38.635016803781454 | 38.0 | | r | 39.183503419072274 | 39.0 | | s | 39.0 | 38.0 | | t | 39.0 | 39.0 | | u | 38.68004732178092 | 38.39916483580083 | | v | 39.0 | 39.0 | | w | 38.5881853639986 | 38.21114561800016 | | x | 39.0 | 39.0 | | y | -25.0 | 36.43496760281849 | | z | 39.0 | 39.0 | | A | 39.38789400666183 | 39.0 | | B | 39.0 | 37.98737993209943 | | C | 39.16280761404536 | 38.0 | | D | 39.0 | 39.51459156482764 | | E | 39.0 | 39.0 | | F | 39.0 | 38.0 | | G | 39.0 | 38.966489765633526 | | H | 39.0 | 38.0 | | I | 38.96694214876033 | 39.25 | | J | 39.0 | 38.464468801750854 | | K | 38.59617220614814 | 38.0 | | L | 39.0 | 38.0 | | M | 38.745166004060955 | 38.0 | | N | 38.73987423309397 | 38.115654115187624 | | O | 38.98891966759004 | 38.81665596263048 | | P | 39.107438016528924 | 38.65155124501666 | | Q | 39.08006855188009 | 38.01570072979803 | | R | 39.0 | 38.0 | | S | 39.0 | 37.81373873377618 | | T | 39.0 | 38.0 | | U | 38.75 | 37.93218925782895 | | V | 38.64979175001243 | 38.0 | | W | 39.0 | 38.97697312351511 | | X | 39.0 | 39.0 | | Y | 39.2011995420152 | 38.493344292403606 | | Z | 38.920094771357476 | 39.0 | | zero | 39.02557980683008 | 38.934353847767 | | one | 39.0 | 37.86668813070091 | | two | 39.0 | 38.0 | | three | 39.0 | 38.30090715487154 | | four | 38.61480785064145 | 38.0 | | five | 39.0 | 38.759568693514495 | | six | 39.2019689704218 | 38.50115350183796 | | seven | 39.0 | 39.45880036173975 | | eight | 39.30732386691426 | 38.81767097798502 | | nine | 39.04800948718441 | 37.956930045381114 | | question | 39.35264826217293 | 38.26531143335521 | | colon | 38.5 | 38.70624687253556 | | semicolon | 39.0 | 39.27324858612964 | | hyphen | 39.0 | 38.0 | | equal | 39.0 | 38.0 | | exclam | 38.783020821373505 | 39.0 | | quotesingle | 39.0 | -1.7598547334076642 | | at | 39.229928128979466 | 38.0 | | slash | 39.0 | 38.0 | | backslash | 39.0 | 39.0 | | quotedbl | 38.86626375007093 | 37.95034254612182 | | asciitilde | 38.68727157672891 | 38.0 | | underscore | 39.0 | 39.0 | | numbersign | 39.0 | 38.740379553133494 | | dollar | 39.0 | 38.734693877551024 | | percent | 39.200007286174 | 38.10774096287298 | | ampersand | 38.96710425694502 | 38.68428307198798 | | parenleft | 39.286819706621706 | 39.0 | | parenright | 39.0 | 39.05824335912013 | | asterisk | 39.0 | 38.0 | | plus | 39.0 | 38.0 | | comma | 38.96546178699183 | 38.55278640450004 | | period | 38.83875395420776 | 37.87092262792087 | | less | 38.97840529870042 | 39.0 | | greater | 39.0 | 37.69246464578106 | | bracketleft | 38.788380868145794 | 38.0 | | bracketright | 39.0 | 39.0 | | asciicircum | 39.0 | 38.0 | | grave | 39.0 | 39.0 | | braceleft | 38.7827057593821 | 39.0 | | bar | 39.0 | 38.406427221172024 | | braceright | 39.0 | 38.206693605650514 | | space | 0.0 | 243.0 |
*** Get the kerning information
#+NAME: def_show_kerning_classes
#+begin_src python :eval no
<
#+begin_src python :results output drawer :var font="/home/sacha/code/font/files/SachaHandEdited.sfd" :eval no import fontforge <<def_show_kerning_classes>> show_kerning_classes(fontforge.open(font)) #+end_src #+RESULTS: :results: :end:
** Copy it to my website
#+begin_src sh :eval yes scp sachacHand-Regular.woff web:~/sacha-v3/ #+end_src
#+RESULTS:
- Other resources
http://ctan.localhost.net.ar/fonts/amiri/tools/build.py
#+begin_export html
#+end_export