mirror of
https://github.com/ryanoasis/nerd-fonts.git
synced 2024-12-13 17:18:37 +02:00
cc2b547703
[why] When the CI triggers a rebuild of the original-source and the font contents is unchanged we do not want to commit the new version back to the repo. But because fontforge puts the creation date into the font file it will always differ on every run, and we would needlessly create commits. [how] We could use the SOURCE_DATE_EPOCH approach and set the dates to the relevant change (commit) times like so: cd src/svgs export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct -- *.svg)" and only afterwards call the generator script / fontforge. But that would need a complete git repo checkout and not just a shallow one (which is faster and thus is used by github action/checkout). Instead we can instruct fontforge to not put any timestamp into the file. The timestamps are anyhow a fontforge proprietary extension. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
176 lines
6.5 KiB
Python
Executable File
176 lines
6.5 KiB
Python
Executable File
#!/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), 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')
|