Add original-source.otf generator script

[why]
When we add a custom glyph (or want to update Seti) the process is
rather laborious and we are needed to change the font and the
accompanying `i_seti.sh` in sync.

[how]
We use a data file to map icon (svg) filenames to codepoints and
readable names.

That file is parsed and the font and info file is created (overwritten
in the repo); and could then be easily committed. This can be a CI
workflow.

Having a dedicated mapping file (`icons.tsv`) enables us to have stable
codepoints for the same symbol over time. Changes in codepoint
allocation can be checked in git.

Having the font autogenerated help guarantee that the icons are all
likely scaled. We rescale them all to the same size and mid-position.
That is not needed for font-patcher, because it rescales and shifts
again based on to-be-patched font metrics. But it certainly is better
for a view into the original-source font.

Sizes and position are still roughly equivalent to the hand positioned
glyphs.

Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
This commit is contained in:
Fini Jastrow 2022-09-17 16:44:47 +02:00
parent 5a9b44749f
commit 328b8a2d22
2 changed files with 252 additions and 0 deletions

View file

@ -0,0 +1,175 @@
#!/usr/bin/env python3
# Nerd Fonts Version: 2.2.2
# Script Version: 1.0.0
# Generates original-source.otf from individual glyphs
#
# Idea & original code taken from
# https://github.com/lukas-w/font-logos/blob/v1.0.1/scripts/generate-font.py
import os
import re
import fontforge
import psMat
# Double-quotes required here, for version-bump.sh:
version = "2.2.2"
start_codepoint = 0xE4FA
codepoint_shift = 0x0100 # shift introduced by font-patcher
vector_datafile = 'icons.tsv'
vectorsdir = '../../src/svgs'
fontfile = 'original-source.otf'
fontdir = '../../src/glyphs'
glyphsetfile = 'i_seti.sh'
glyphsetsdir = 'lib'
def hasGaps(data, start_codepoint):
""" Takes a list of integers and checks that it contains no gaps """
for i in range(min(data) + 1, max(data)):
if not i in data:
print("Gap at offset {}".format(i - start_codepoint))
return True
return False
def iconFileLineOk(parts):
""" Check one line for course errors, decide if it shall be skipped """
if parts[0].startswith('#'):
# Comment lines start with '#'
return False
if len(parts) != 2 and len(parts) != 3:
print('Unexpected data on the line "{}"'.format(line.strip()))
return False
if int(parts[0]) < 0:
print('Offset must be positive on line "{}", ignoring'.format(line.strip()))
return False
return True
def addLineToData(data, parts, codepoint):
""" Add one line to the data. Return (success, is_alias) """
ali = False
if codepoint in data:
data[codepoint][0] += [ parts[1] ]
if len(parts) > 2 and data[codepoint][1] != parts[2]:
print('Conflicting filename for {}, ignoring {}'.format(codepoint, parts[2]))
return False, False
ali = True
else:
data[codepoint] = [[parts[1]], parts[2]]
return True, ali
def readIconFile(filename, start_codepoint):
""" Read the database with codepoints, names and files """
# First line of the file is the header, it is ignored
# All other lines are one line for one glyph
# Elements in each line are tab separated (any amount consecutive of tabs)
# First element is the offset, 2nd is name, 3rd is filename
# For aliases the 3rd can be ommited on an additional line
data = {}
num = 0
ali = 0
with open(filename, 'r') as f:
for line in f.readlines():
parts = re.split('\t+', line.strip())
if not iconFileLineOk(parts):
continue
offset = int(parts[0])
codepoint = start_codepoint + offset
if re.search('[^a-zA-Z0-9_]', parts[1]):
print('Invalid characters in name: "{}" replaced by "_"'.format(parts[1]))
parts[1] = re.sub('[^a-zA-Z0-9_]', '_', parts[1])
added = addLineToData(data, parts, codepoint)
if not added[0]:
continue
num += 1
if added[1]:
ali += 1
print('Read glyph data successfully with {} entries ({} aliases)'.format(num, ali))
return (data, num, ali)
def widthFromBB(bb):
""" Calculate glyph width from BoundingBox data """
return bb[2] - bb[0]
def heightFromBB(bb):
""" Calculate glyph height from BoundingBox data """
return bb[3] - bb[1]
def calcShift(left1, width1, left2, width2):
""" Calculate shift needed to center '2' in '1' """
return width1 / 2 + left1 - width2 / 2 - left2
def addIcon(codepoint, name, filename):
""" Add one outline file and rescale/move """
dBB = [120, 0, 1000-120, 900] # just some nice sizes
filename = os.path.join(vectorsdir, filename)
glyph = font.createChar(codepoint, name)
glyph.importOutlines(filename)
gBB = glyph.boundingBox()
scale_x = widthFromBB(dBB) / widthFromBB(gBB)
scale_y = heightFromBB(dBB) / heightFromBB(gBB)
scale = scale_y if scale_y < scale_x else scale_x
glyph.transform(psMat.scale(scale, scale))
gBB = glyph.boundingBox() # re-get after scaling (rounding errors)
glyph.transform(psMat.translate(
calcShift(dBB[0], widthFromBB(dBB), gBB[0], widthFromBB(gBB)),
calcShift(dBB[1], heightFromBB(dBB), gBB[1], heightFromBB(gBB))))
glyph.width = int(dBB[2] + dBB[0])
glyph.manualHints = True
def createGlyphInfo(icon_datasets, filepathname, into):
""" Write the glyphinfo file """
with open(filepathname, 'w', encoding = 'utf8') as f:
f.write(u'#!/usr/bin/env bash\n')
f.write(intro)
f.write(u'# Script Version: (autogenerated)\n')
f.write(u'test -n "$__i_seti_loaded" && return || __i_seti_loaded=1\n')
for codepoint, data in icon_datasets.items():
f.write(u"i='{}' {}=$i\n".format(chr(codepoint),data[0][0]))
for alias in data[0][1:]:
f.write(u" {}=${}\n".format(alias, data[0][0]))
f.write(u'unset i\n')
### Lets go
print('\n[Nerd Fonts] Glyph collection font generator {}\n'.format(version))
font = fontforge.font()
font.fontname = 'NerdFontFileTypes-Regular'
font.fullname = 'Nerd Font File Types Regular'
font.familyname = 'Nerd Font File Types'
font.em = 1024
font.encoding = 'UnicodeFull'
# Add valid space glyph to avoid "unknown character" box on IE11
glyph = font.createChar(32)
glyph.width = 200
font.sfntRevision = None # Auto-set (refreshed) by fontforge
font.version = version
font.copyright = 'Nerd Fonts'
font.appendSFNTName('English (US)', 'Version', version)
font.appendSFNTName('English (US)', 'Vendor URL', 'https://github.com/ryanoasis/nerd-fonts')
font.appendSFNTName('English (US)', 'Copyright', 'Nerd Fonts')
icon_datasets, _, num_aliases = readIconFile(os.path.join(vectorsdir, vector_datafile), start_codepoint)
gaps = ' (with gaps)' if hasGaps(icon_datasets.keys(), start_codepoint) else ''
for codepoint, data in icon_datasets.items():
addIcon(codepoint, data[0][0], data[1])
num_icons = len(icon_datasets)
print('Generating {} with {} glyphs'.format(fontfile, num_icons))
font.generate(os.path.join(fontdir, fontfile))
# We create the font, but ... patch it in on other codepoints :-}
icon_datasets = { code + codepoint_shift : data for (code, data) in icon_datasets.items() }
intro = u'# Seti-UI + Custom ({} icons, {} aliases)\n'.format(num_icons, num_aliases)
intro += u'# Codepoints: {:X}-{:X}{}\n'.format(min(icon_datasets.keys()), max(icon_datasets.keys()), gaps)
intro += u'# Nerd Fonts Version: {}\n'.format(version)
print('Generating GlyphInfo {}'.format(glyphsetfile))
createGlyphInfo(icon_datasets, os.path.join(glyphsetsdir, glyphsetfile), intro)
print('Finished')

77
src/svgs/icons.tsv Normal file
View file

@ -0,0 +1,77 @@
# This file defines the codepoints for the individual glyphs in
# original-source.otf and their names in the CSS and on the cheat
# sheet (via i_seti.sh).
#
# If you add a svg to the directory you need to add also a line here.
# Keep the numbers consecutive.
# You can add aliases by adding a new line with the same offset but
# different name; omit the filename on those lines.
# Use generate-original-source.py to regenerate the font used to patch.
#
# offset name (in i_seti.sh) filename (.svg)
#
0 i_custom_folder_npm npm-folder.svg
1 i_custom_folder_git git-folder.svg
1 i_custom_folder_git_branch
2 i_custom_folder_config config-folder.svg
3 i_custom_folder_github octocat-folder.svg
4 i_custom_folder_open open-folder.svg
5 i_custom_folder folder.svg
6 i_seti_stylus stylus.svg
7 i_seti_project project.svg
8 i_seti_play_arrow play-arrow.svg
9 i_seti_sass sass.svg
10 i_seti_rails rails.svg
11 i_seti_ruby ruby.svg
12 i_seti_python python.svg
13 i_seti_heroku heroku.svg
14 i_seti_php php.svg
15 i_seti_markdown markdown.svg
16 i_seti_license license.svg
17 i_seti_json less.svg
17 i_seti_less
18 i_seti_javascript javascript.svg
19 i_seti_image image.svg
20 i_seti_html html.svg
21 i_seti_mustache handlebars.svg
22 i_seti_gulp gulp.svg
23 i_seti_grunt grunt.svg
24 i_seti_default file.svg
24 i_seti_text
25 i_seti_folder seti-folder.svg
26 i_seti_css css.svg
27 i_seti_config config.svg
28 i_seti_npm npm.svg
29 i_seti_home home.svg
30 i_seti_ejs html.svg
31 i_seti_xml rss.svg
32 i_seti_bower bower.svg
33 i_seti_coffee coffeescript.svg
33 i_seti_cjsx
34 i_seti_twig twig.svg
35 i_custom_cpp c++.svg
36 i_custom_c c.svg
37 i_seti_haskell haskell.svg
38 i_seti_lua lua.svg
39 i_indent_line separator.svg
39 i_indentation_line
39 i_indent_dotted_guide
40 i_seti_karma karma.svg
41 i_seti_favicon favourite.svg
42 i_seti_julia julia.svg
43 i_seti_react react.svg
44 i_custom_go go2.svg
45 i_seti_go go.svg
46 i_seti_typescript typescript.svg
47 i_custom_msdos ms-dos.svg
48 i_custom_windows windows.svg
49 i_custom_vim vim.svg
50 i_custom_elm elm.svg
51 i_custom_elixir elixir.svg
52 i_custom_electron electron.svg
53 i_custom_crystal crystal.svg
54 i_custom_purescript purescript.svg
55 i_custom_puppet puppet.svg
56 i_custom_emacs emacs.svg
57 i_custom_orgmode orgmode.svg
58 i_custom_kotlin kotlin.svg
Can't render this file because it has a wrong number of fields in line 11.