Skip to content

Commit

Permalink
Devicons: Prepare update
Browse files Browse the repository at this point in the history
This adds the current mapping file (which has been hand-crafted) and the
scripts to update the Devicons.

This also fixes Vorillaz' typo 'rasberry_pi' -> 'raspberry_pi'.

Signed-off-by: Fini Jastrow <[email protected]>
  • Loading branch information
Finii committed Aug 27, 2024
1 parent 71e7ae0 commit f375743
Show file tree
Hide file tree
Showing 11 changed files with 546 additions and 7 deletions.
15 changes: 8 additions & 7 deletions src/glyphs/devicons/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Devicons

For more information have a look at the upstream website: https://github.com/vorillaz/devicons
From the Devicons the non-linemark versions are selected and assembled into a custom
icon font. This font guarantees that the codepoints of existing icons do not change
when other icons are added or removed.

This is taken directly from the repository default branch, which is ahead of release 1.8.0.
We call it 1.8.1 here, but there is no such release.
For more information have a look at the upstream website: https://github.com/devicons/devicon

## Source bugs fixed
The helper scripts need to be called in this order (note the individual prerequisites):
* `analyze`
* `generate` (possibly via `fontforge`)

Glyph 0xE6B6 is defective in the original font. We hand optimized and fixed that.

Version: 1.8.1
Version: 2.16.0.custom
163 changes: 163 additions & 0 deletions src/glyphs/devicons/analyze
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
# coding=utf8

# Create a new mapping file by combining the information from
# the old mapping and checking which icons got dropped; are new;
# or get a different svg file.

# PREREQUISITES:
# $ curl -OL https://github.com/devicons/devicon/archive/refs/tags/v2.16.0.tar.gz
# $ tar zxf v2.16.0.tar.gz
# $ mv devicon-*/icons .
# $ cp -r vorillaz icons

import re, os, sys

vectorsdir = 'icons'

def filename_from_name(filename):
""" Some icons have a name that is not the svg filename """
# Returns '-' if the icon is to be removed
# Giving a full pathname selects a certain svg variant
return {
'aws': 'amazonwebservices',
'backbone': 'backbonejs',
'bower': 'bower/bower-line.svg',
'clojure': 'clojure/clojure-line.svg',
'composer': 'composer/composer-line.svg',
'css3_full': 'css3/css3-plain-wordmark.svg',
'digital_ocean': 'digitalocean',
'dotnet': 'dot-net',
'ghost': 'ghost/ghost-original-wordmark.svg',
'ghost_small': 'ghost',
'github': '-',
'github_alt': '-',
'github_badge': 'github',
'github_full': 'github/github-original-wordmark.svg',
'go': 'go/go-line.svg',
'grunt': 'grunt/grunt-line.svg',
'ie': 'ie10',
'javascript_badge': 'javascript',
'javascript': '-',
'jekyll_small': 'jekyll',
'krakenjs': '-',
'krakenjs_badge': 'krakenjs',
'meteorfull': 'meteor/meteor-plain-wordmark.svg',
'nodejs': 'nodejs/nodejs-plain-wordmark.svg',
'nodejs_small': 'nodejs',
'raspberry_pi': 'raspberrypi',
'ruby_on_rails': 'rails',
'symfony': '-',
'symfony_badge': 'symfony',
'unity_small': 'unity',
'windows': 'windows8',
}.get(filename, filename)

def get_aliases(names):
""" For some icons we would like to have aliases """
# Returns a list with aliases, keep main name first element
name = names[0]
alias = {
'digital_ocean': [ 'digitalocean' ],
'github_badge': [ 'github' ],
'javascript_badge': [ 'javascript' ],
'jekyll_small': [ 'jekyll' ],
'krakenjs_badge': [ 'krakenjs' ],
'ruby_on_rails': [ 'rails' ],
'symfony_badge': [ 'symfony' ],
'unifiedmodelinglanguage': [ 'uml' ],
}.get(name, [ ])
while name in alias:
alias.remove(name)
return [ name , *alias ]

def file_with_ending(files, ending):
""" Return the (first) file out of a list of files that has the desired ending """
# Returns False if no match at all
matches = [ file for file in files if file.endswith(ending) ]
if not matches:
return False
return matches[0]

def suggest_new_filename(name):
""" Return a specific svg filename for one icon, preferring some svg filename endings """
name = filename_from_name(name)
subdir = vectorsdir + '/' + name
if not os.path.exists(subdir):
return False
if os.path.isfile(subdir):
return name
svgs = os.listdir(subdir)
filename = file_with_ending(svgs, 'plain.svg')
if not filename:
filename = file_with_ending(svgs, 'original.svg')
if not filename:
filename = file_with_ending(svgs, 'plain-wordmark.svg')
if not filename:
filename = file_with_ending(svgs, 'original-wordmark.svg')
if not filename:
return False
return name + '/' + filename

remix_mapping = []
with open('mapping', 'r') as f:
for line in f.readlines():
if line.startswith('#'):
continue
c1, c2, n, *f = re.split(' +', line.strip())
remix_mapping.append((int(c1, 16), int(c2, 16), n, *f))

new_names = os.listdir(vectorsdir)
new_names.sort()
new_names.remove('vorillaz') # If this fails one prerequisite step is missing

notes1 = ''
notes2 = ''
mapping = []
for orig_point, dest_point, filename, *names in remix_mapping:
if not os.path.isfile(vectorsdir + '/' + filename):
newfilename = suggest_new_filename(names[0])
if newfilename:
notes1 += '# SVG change: code: {:04X} name: {}, old: {}, new: {}\n'.format(
orig_point, names[0], filename, newfilename)
filename = newfilename
if filename:
mapping.append((orig_point, dest_point, filename, *names))
dirname = os.path.dirname(filename)
if dirname in new_names:
new_names.remove(dirname)
continue

notes2 += '# Icon dropped: code: {:04X} name: {}\n'.format(
orig_point, names[0])

index = 0xE700
taken_codes = set([ e[1] for e in mapping ])
for iconname in new_names:
filename = suggest_new_filename(iconname)
if not filename:
sys.exit('Can not find svg for "{}"'.format(iconname))
while index in taken_codes:
index = index + 1
mapping.append((index - 0x0100, index, filename, iconname))
taken_codes.add(index)

with open('mapping', 'w', encoding = 'utf8') as f:
f.write('# Devicons mapping file\n')
f.write('#\n')
f.write('# DEV-code NF-code filename name [alias [...]]\n')
f.write('#\n')

mapping.sort(key=(lambda x: x[1]))
unique_names = set()
for orig_point, dest_point, filename, *names in mapping:
aliases = get_aliases(names)
for n in aliases:
if n not in unique_names:
unique_names.add(n)
else:
sys.exit('ERROR name duplicate found: {}'.format(n))
f.write("{:04X} {:04X} {} {}\n".format(orig_point, dest_point, filename, ' '.join(aliases)))

print(notes1)
print(notes2)
120 changes: 120 additions & 0 deletions src/glyphs/devicons/generate
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# coding=utf8

# PREREQUISITES:
# Have a correct and up to date mappings file, updated maybe with analyze

import sys
import os
import re
import subprocess
import fontforge

# Double-quotes required here, for version-bump.sh:
# version-bump.sh is not working here, need to adjust manually!
version = "3.3.0"

dev_version = 'v2.16.0'
archive = '{}.tar.gz'.format(dev_version)

vectorsdir = 'icons'
fontdir = '.'
fontfile = 'devicons.otf'
glyphsetfile = 'i_dev.sh'
glyphsetsdir = '../../../bin/scripts/lib'

def addIcon(codepoint, name, filename):
""" Add one outline file and rescale/move """
filename = os.path.join(vectorsdir, filename)
glyph = font.createChar(codepoint, name)
glyph.importOutlines(filename)
xmin, ymin, xmax, ymax = glyph.boundingBox()
glyph.width = int(xmax + xmin) # make left and right bearings equal
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_dev_loaded" && return || __i_dev_loaded=1\n')
for _, codepoint, _, *name in icon_datasets:
codepoint = int(codepoint, 16)
f.write(u"i='{}' i_dev_{}=$i\n".format(chr(codepoint), name[0]))
for more_names in name[1:]:
f.write(u" i_dev_{}=$i\n".format(more_names))
f.write(u'unset i\n')

print('\nReading mapping file')
mapping = []
with open('mapping', 'r') as f:
for line in f.readlines():
line = line.strip()
if line.startswith('#') or len(line) < 1:
continue
mapping.append(tuple(re.split(' +', line.strip())))
print('Found {} entries'.format(len(mapping)))
mapping.sort(key=(lambda x: x[1]))

if not os.path.isfile(archive):
print('Fetching Devicons archive "{}"\n'.format(archive))
if False and subprocess.call('curl -OL https://github.com/devicons/devicon/archive/refs/tags/' + archive, shell=True):
sys.exit('Error fetching Devicons archive')

print('\nUnpacking Devicons archive')
if subprocess.call('rm -rf devicon-* icons && tar zxf ' + archive + \
' && mv devicon-*/icons . && rm -rf v*.tar.gz', shell=True):
sys.exit('Error unpacking archive')

print('\nMixing Vorillaz Devicons in')
if subprocess.call('cp -r vorillaz icons', shell=True):
sys.exit('Error mixing ...')


svg_dirs = os.listdir(vectorsdir)
svgs = []
for d in svg_dirs:
svgs += os.listdir(vectorsdir + '/' + d)
print('Found {} svgs'.format(len(svgs)))

font = fontforge.font()
font.fontname = 'Devicons-NerdFont-Regular'
font.fullname = 'Devicons Nerd Font Regular'
font.familyname = 'Devicons Nerd Font'
font.ascent = 1000
font.descent = 200
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 = 'Fonticons, Inc'
font.appendSFNTName('English (US)', 'Version', archive + '; ' + version)
font.appendSFNTName('English (US)', 'Vendor URL', 'https://github.com/ryanoasis/nerd-fonts')
font.appendSFNTName('English (US)', 'Copyright', 'See https://github.com/devicons/devicon')

for _, codepoint, file, *names in mapping:
codepoint = int(codepoint, 16)
addIcon(codepoint, names[0], file)

num_icons = len(mapping)

print('Generating {} with {} glyphs'.format(fontfile, num_icons))
font.ascent = 1100
font.descent = 300
font.generate(os.path.join(fontdir, fontfile), flags=("no-FFTM-table",))

codepoints = [ int(p, 16) for _, p, *_ in mapping ]
aliases = [ len(n) - 1 for _, _, _, *n in mapping ]
intro = u'# Devicons (version {}, {} icons, {} aliases)\n'.format(dev_version, num_icons, sum(aliases))
intro += u'# Does not include all icons of the release\n'
intro += u'# Codepoints: {:X}-{:X} with gaps\n'.format(min(codepoints), max(codepoints))
intro += u'# Nerd Fonts Version: {}\n'.format(version)

print('Generating GlyphInfo {}'.format(glyphsetfile))
createGlyphInfo(mapping, os.path.join(glyphsetsdir, glyphsetfile), intro)
print('Finished')
Loading

0 comments on commit f375743

Please sign in to comment.