nerd-fonts/bin/scripts/generate-original-source.py
2024-04-12 14:31:51 +00:00

179 lines
6.8 KiB
Python
Executable file

#!/usr/bin/env python3
# Nerd Fonts Version: 3.2.1
# Script Version: 1.0.1
# 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 = "3.2.1"
start_codepoint = 0xE4FA # with shift this is 0xE5FA
end_codepoint = 0xE5FF # Next set starts at 0xE700 - 0x0100 shift = 0xE600
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 = [53, 0, 1000 - 53, 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():
if codepoint not in range(start_codepoint, end_codepoint + 1):
print('FATAL: We are leaving the allocated codepoint range with "{}", bailing out'.format(data[0][0]))
exit(1)
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), flags=("no-FFTM-table",))
# 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')