feat: upgrade to nf v3.0.0

This commit is contained in:
Daylin Morgan 2023-05-02 14:57:12 -05:00
parent 1850f3b2b5
commit 2a0e39720f
Signed by: daylin
GPG key ID: C1E52E7DD81DF79F
29 changed files with 1835 additions and 315 deletions

View file

@ -2,9 +2,9 @@ name: "Update Nerd Fonts"
on: on:
workflow_dispatch: workflow_dispatch:
# schedule: schedule:
# 1st and 15th at 12:00 AM # 1st and 15th at 12:00 AM
# - cron: "0 0 1,15 * *" - cron: "0 0 1,15 * *"
permissions: permissions:
contents: write contents: write

178
.gitignore vendored
View file

@ -1,3 +1,180 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python
#
*.zip *.zip
MonoLisa/* MonoLisa/*
@ -7,3 +184,4 @@ patched/*
nerd-fonts nerd-fonts
.env .env
font-patcher-log.txt

View file

@ -1,4 +1,4 @@
exclude: "^(src/.*|bin/font-patcher)" exclude: "^(src/.*|bin/scripts|font-patcher)"
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
@ -6,10 +6,6 @@ repos:
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 23.1.0
hooks: hooks:

View file

@ -1,7 +1,7 @@
-include .env -include .env
ARGS ?= -c ARGS ?= -c
patch: ./bin/font-patcher ## apply nerd fonts patch |> -gs b_magenta -ms bold patch: ## apply nerd fonts patch |> -gs b_magenta -ms bold
@./patch-monolisa \ @./patch-monolisa \
$(ARGS) \ $(ARGS) \
-f MonoLisa/ -f MonoLisa/
@ -24,6 +24,8 @@ lint: ## run pre-commit hooks
clean: ## remove patched fonts clean: ## remove patched fonts
@rm -rf patched/* @rm -rf patched/*
@rm -f ./font-patcher-log.txt
@rm -f FontPatcher.zip
# depends on daylinmorgan/yartsu # depends on daylinmorgan/yartsu
assets/help.svg: assets/help.svg:

View file

@ -0,0 +1,418 @@
#!/usr/bin/env python
# coding=utf8
import re
from FontnameTools import FontnameTools
class FontnameParser:
"""Parse a font name and generate all kinds of names"""
def __init__(self, filename, logger):
"""Parse a font filename and store the results"""
self.parse_ok = False
self.use_short_families = (
False,
False,
False,
) # ( camelcase name, short styles, aggressive )
self.keep_regular_in_family = None # None = auto, True, False
self.suppress_preferred_if_identical = True
self.family_suff = ""
self.ps_fontname_suff = ""
self.short_family_suff = ""
self.name_subst = []
[
self.parse_ok,
self._basename,
self.weight_token,
self.style_token,
self.other_token,
self._rest,
] = FontnameTools.parse_font_name(filename)
self.basename = self._basename
self.rest = self._rest
self.add_name_substitution_table(FontnameTools.SIL_TABLE)
self.rename_oblique = True
self.logger = logger
def _make_ps_name(self, n, is_family):
"""Helper to limit font name length in PS names"""
fam = "family " if is_family else ""
limit = 31 if is_family else 63
if len(n) <= limit:
return n
r = re.search("(.*)(-.*)", n)
if not r:
new_n = n[:limit]
else:
q = limit - len(r.groups()[1])
if q < 1:
q = 1
self.logger.error(
"====-< Shortening too long PS {}name: Garbage warning".format(fam)
)
new_n = r.groups()[0][:q] + r.groups()[1]
if new_n != n:
self.logger.error(
"====-< Shortening too long PS {}name: {} -> {}".format(fam, n, new_n)
)
return new_n
def _shortened_name(self):
"""Return a blank free basename-rest combination"""
if not self.use_short_families[0]:
return (self.basename, self.rest)
else:
return (FontnameTools.concat(self.basename, self.rest).replace(" ", ""), "")
def set_keep_regular_in_family(self, keep):
"""Familyname may contain 'Regular' where it should normally be suppressed"""
self.keep_regular_in_family = keep
def set_expect_no_italic(self, noitalic):
"""Prevents rewriting Oblique as family name part"""
# To prevent naming clashes usually Oblique is moved out in the family name
# because some fonts have Italic and Oblique, and we want to generate pure
# RIBBI families in ID1/2.
# But some fonts have Oblique instead of Italic, here the prevential movement
# is not needed, or rather contraproductive. This can not be detected on a
# font file level but needs to be specified per family from the outside.
# Returns true if setting was successful.
if "Italic" in self.style_token:
self.rename_oblique = True
return not noitalic
self.rename_oblique = not noitalic
return True
def set_suppress_preferred(self, suppress):
"""Suppress ID16/17 if it is identical to ID1/2 (True is default)"""
self.suppress_preferred_if_identical = suppress
def inject_suffix(self, family, ps_fontname, short_family):
"""Add a custom additonal string that shows up in the resulting names"""
self.family_suff = family.strip()
self.ps_fontname_suff = ps_fontname.replace(" ", "")
self.short_family_suff = short_family.strip()
return self
def enable_short_families(self, camelcase_name, prefix, aggressive):
"""Enable short styles in Family when (original) font name starts with prefix; enable CamelCase basename in (Typog.) Family"""
# camelcase_name is boolean
# prefix is either a string or False/True
if isinstance(prefix, str):
prefix = self._basename.startswith(prefix)
self.use_short_families = (camelcase_name, prefix, aggressive)
return self
def add_name_substitution_table(self, table):
"""Have some fonts renamed, takes list of tuples (regex, replacement)"""
# The regex will be anchored to name begin and used case insensitive
# Replacement can have regex matches, mind to catch the correct source case
self.name_subst = table
self.basename = self._basename
self.rest = self._rest
for regex, replacement in self.name_subst:
base_and_rest = self.basename + (" " + self.rest if len(self.rest) else "")
m = re.match(regex, base_and_rest, re.IGNORECASE)
if not m:
continue
i = len(self.basename) - len(m.group(0))
if i < 0:
self.basename = m.expand(replacement).rstrip()
self.rest = self.rest[-(i + 1) :].lstrip()
else:
self.basename = m.expand(replacement) + self.basename[len(m.group(0)) :]
return self
def drop_for_powerline(self):
"""Remove 'for Powerline' from all names (can not be undone)"""
if "Powerline" in self.other_token:
idx = self.other_token.index("Powerline")
self.other_token.pop(idx)
if idx > 0 and self.other_token[idx - 1] == "For":
self.other_token.pop(idx - 1)
self._basename = re.sub(
r"(\b|for\s?)?powerline\b", "", self._basename, 1, re.IGNORECASE
).strip()
self.add_name_substitution_table(self.name_subst) # re-evaluate
return self
### Following the creation of the name parts:
#
# Relevant websites
# https://www.fonttutorials.com/how-to-name-font-family/
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fss
# https://docs.microsoft.com/en-us/typography/opentype/spec/head#macstyle
# Example (mind that they group 'semibold' as classic-group-of-4 Bold, while we will always only take bold as Bold):
# Adobe Caslon Pro Regular ID1: Adobe Caslon Pro ID2: Regular
# Adobe Caslon Pro Italic ID1: Adobe Caslon Pro ID2: Italic
# Adobe Caslon Pro Semibold ID1: Adobe Caslon Pro ID2: Bold ID16: Adobe Caslon Pro ID17: Semibold
# Adobe Caslon Pro Semibold Italic ID1: Adobe Caslon Pro ID2: Bold Italic ID16: Adobe Caslon Pro ID17: Semibold Italic
# Adobe Caslon Pro Bold ID1: Adobe Caslon Pro Bold ID2: Regular ID16: Adobe Caslon Pro ID17: Bold
# Adobe Caslon Pro Bold Italic ID1: Adobe Caslon Pro Bold ID2: Italic ID16: Adobe Caslon Pro ID17: Bold Italic
# fontname === preferred_family + preferred_styles
# fontname === family + subfamily
#
# familybase = basename + rest + other (+ suffix)
# ID 1/2 just have self.style in the subfamily, all the rest ends up in the family
# ID 16/17 have self.style and self.weight in the subfamily, the rest ends up in the family
def fullname(self):
"""Get the SFNT Fullname (ID 4)"""
styles = self.style_token
weights = self.weight_token
if self.keep_regular_in_family == None:
keep_regular = FontnameTools.is_keep_regular(
self._basename + " " + self._rest
)
else:
keep_regular = self.keep_regular_in_family
if "Regular" in styles and (
not keep_regular or len(self.weight_token) > 0
): # This is actually a malformed font name
styles = list(self.style_token)
styles.remove("Regular")
# For naming purposes we want Oblique to be part of the styles
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
(name, rest) = self._shortened_name()
if self.use_short_families[1]:
[weights, styles] = FontnameTools.short_styles(
[weights, styles], self.use_short_families[2]
)
return FontnameTools.concat(
name, rest, self.other_token, self.short_family_suff, weights, styles
)
def psname(self):
"""Get the SFNT PostScriptName (ID 6)"""
# This is almost self.family() + '-' + self.subfamily()
(name, rest) = self._shortened_name()
styles = self.style_token
weights = self.weight_token
if self.use_short_families[1]:
styles = FontnameTools.short_styles(styles, self.use_short_families[2])
weights = FontnameTools.short_styles(weights, self.use_short_families[2])
fam = FontnameTools.camel_casify(
FontnameTools.concat(name, rest, self.other_token, self.ps_fontname_suff)
)
sub = FontnameTools.camel_casify(FontnameTools.concat(weights, styles))
if len(sub) > 0:
sub = "-" + sub
fam = FontnameTools.postscript_char_filter(fam)
sub = FontnameTools.postscript_char_filter(sub)
return self._make_ps_name(fam + sub, False)
def preferred_family(self):
"""Get the SFNT Preferred Familyname (ID 16)"""
(name, rest) = self._shortened_name()
pfn = FontnameTools.concat(name, rest, self.other_token, self.family_suff)
if self.suppress_preferred_if_identical and pfn == self.family():
# Do not set if identical to ID 1
return ""
return pfn
def preferred_styles(self):
"""Get the SFNT Preferred Styles (ID 17)"""
styles = self.style_token
weights = self.weight_token
# For naming purposes we want Oblique to be part of the styles
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
pfs = FontnameTools.concat(weights, styles)
if self.suppress_preferred_if_identical and pfs == self.subfamily():
# Do not set if identical to ID 2
return ""
return pfs
def family(self):
"""Get the SFNT Familyname (ID 1)"""
# We use the short form of the styles to save on number of chars
(name, rest) = self._shortened_name()
other = self.other_token
weights = self.weight_token
aggressive = self.use_short_families[2]
if not self.rename_oblique:
(weights, styles) = FontnameTools.make_oblique_style(weights, [])
if self.use_short_families[1]:
[other, weights] = FontnameTools.short_styles([other, weights], aggressive)
weights = [w if w != "Oblique" else "Obl" for w in weights]
return FontnameTools.concat(name, rest, other, self.short_family_suff, weights)
def subfamily(self):
"""Get the SFNT SubFamily (ID 2)"""
styles = self.style_token
weights = self.weight_token
if not self.rename_oblique:
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
if len(styles) == 0:
if "Oblique" in weights:
return FontnameTools.concat(styles, "Italic")
return "Regular"
if "Oblique" in weights and not "Italic" in styles:
return FontnameTools.concat(styles, "Italic")
return FontnameTools.concat(styles)
def ps_familyname(self):
"""Get the PS Familyname"""
fam = self.preferred_family()
if len(fam) < 1:
fam = self.family()
return self._make_ps_name(fam, True)
def macstyle(self, style):
"""Modify a given macStyle value for current name, just bits 0 and 1 touched"""
b = style & (~3)
b |= 1 if "Bold" in self.style_token else 0
b |= 2 if "Italic" in self.style_token else 0
return b
def fs_selection(self, fs):
"""Modify a given fsSelection value for current name, bits 0, 5, 6, 8, 9 touched"""
ITALIC = 1 << 0
BOLD = 1 << 5
REGULAR = 1 << 6
WWS = 1 << 8
OBLIQUE = 1 << 9
b = fs & (~(ITALIC | BOLD | REGULAR | WWS | OBLIQUE))
if "Bold" in self.style_token:
b |= BOLD
# Ignore Italic if we have Oblique
if "Oblique" in self.weight_token:
b |= OBLIQUE
elif "Italic" in self.style_token:
b |= ITALIC
# Regular is just the basic weight
if len(self.weight_token) == 0:
b |= REGULAR
b |= WWS # We assert this by our naming process
return b
def checklen(self, max_len, entry_id, name):
"""Check the length of a name string and report violations"""
if len(name) <= max_len:
self.logger.debug(
"=====> {:18} ok ({:2} <={:2}): {}".format(
entry_id, len(name), max_len, name
)
)
else:
self.logger.error(
"====-< {:18} too long ({:2} > {:2}): {}".format(
entry_id, len(name), max_len, name
)
)
return name
def rename_font(self, font):
"""Rename the font to include all information we found (font is fontforge font object)"""
font.fondname = None
font.fontname = self.psname()
font.fullname = self.fullname()
font.familyname = self.ps_familyname()
# We have to work around several issues in fontforge:
#
# a. Remove some entries from SFNT table; fontforge has no API function for that
#
# b. Fontforge does not allow to set SubFamily (and other) to any value:
#
# Fontforge lets you set any value, unless it is the default value. If it
# is the default value it does not set anything. It also does not remove
# a previously existing non-default value. Why it is done this way is
# unclear:
# fontforge/python.c SetSFNTName() line 11431
# return( 1 ); /* If they set it to the default, there's nothing to do */
#
# Then is the question: What is the default? It is taken from the
# currently set fontname (??!). The fontname is parsed and everything
# behind the dash is the default SubFamily:
# fontforge/tottf.c DefaultTTFEnglishNames()
# fontforge/splinefont.c _GetModifiers()
#
# To fix this without touching Fontforge we need to set the SubFamily
# directly in the SFNT table:
#
# c. Fontforge has the bug that it allows to write empty-string to a SFNT field
# and it is actually embedded as empty string, but empty strings are not
# shown if you query the sfnt_names *rolleyes*
version_tag = ""
sfnt_list = []
TO_DEL = [
"Family",
"SubFamily",
"Fullname",
"PostScriptName",
"Preferred Family",
"Preferred Styles",
"Compatible Full",
"WWS Family",
"WWS Subfamily",
"UniqueID",
"CID findfont Name",
]
# Remove these entries in all languages and add (at least the vital ones) some
# back, but only as 'English (US)'. This makes sure we do not leave contradicting
# names over different languages.
for l, k, v in list(font.sfnt_names):
if not k in TO_DEL:
sfnt_list += [(l, k, v)]
if k == "Version" and l == "English (US)":
version_tag = " " + v.split()[-1]
sfnt_list += [
(
"English (US)",
"Family",
self.checklen(31, "Family (ID 1)", self.family()),
)
] # 1
sfnt_list += [
(
"English (US)",
"SubFamily",
self.checklen(31, "SubFamily (ID 2)", self.subfamily()),
)
] # 2
sfnt_list += [("English (US)", "UniqueID", self.fullname() + version_tag)] # 3
sfnt_list += [
(
"English (US)",
"Fullname",
self.checklen(63, "Fullname (ID 4)", self.fullname()),
)
] # 4
sfnt_list += [
(
"English (US)",
"PostScriptName",
self.checklen(63, "PSN (ID 6)", self.psname()),
)
] # 6
p_fam = self.preferred_family()
if len(p_fam):
sfnt_list += [
(
"English (US)",
"Preferred Family",
self.checklen(31, "PrefFamily (ID 16)", p_fam),
)
] # 16
p_sty = self.preferred_styles()
if len(p_sty):
sfnt_list += [
(
"English (US)",
"Preferred Styles",
self.checklen(31, "PrefStyles (ID 17)", p_sty),
)
] # 17
font.sfnt_names = tuple(sfnt_list)
font.macstyle = self.macstyle(0)
font.os2_stylemap = self.fs_selection(0)

View file

@ -0,0 +1,441 @@
#!/usr/bin/env python
# coding=utf8
import re
import sys
class FontnameTools:
"""Deconstruct a font filename to get standardized name parts"""
@staticmethod
def front_upper(word):
"""Capitalize a string (but keep case of subsequent chars)"""
return word[:1].upper() + word[1:]
@staticmethod
def camel_casify(word):
"""Remove blanks and use CamelCase for the new word"""
return "".join(map(FontnameTools.front_upper, word.split(" ")))
@staticmethod
def camel_explode(word):
"""Explode CamelCase -> Camel Case"""
# But do not explode "JetBrains" etc at string start...
excludes = [
"JetBrains",
"DejaVu",
"OpenDyslexicAlta",
"OpenDyslexicMono",
"OpenDyslexic",
"DaddyTimeMono",
"InconsolataGo",
"ProFontWindows",
"ProFont",
"ProggyClean",
]
m = re.match("(" + "|".join(excludes) + ")(.*)", word)
(prefix, word) = m.group(1, 2) if m != None else ("", word)
if len(word) == 0:
return prefix
parts = re.split("(?<=[a-z0-9])(?=[A-Z])", word)
if len(prefix):
parts.insert(0, prefix)
return " ".join(parts)
@staticmethod
def drop_empty(l):
"""Remove empty strings from list of strings"""
return [x for x in l if len(x) > 0]
@staticmethod
def concat(*all_things):
"""Flatten list of (strings or lists of strings) to a blank-separated string"""
all = []
for thing in all_things:
if type(thing) is not list:
all.append(thing)
else:
all += thing
return " ".join(FontnameTools.drop_empty(all))
@staticmethod
def unify_style_names(style_name):
"""Substitude some known token with standard wording"""
known_names = {
# Source of the table is the current sourcefonts
# Left side needs to be lower case
"-": "",
"book": "",
"text": "",
"ce": "CE",
#'semibold': 'Demi',
"ob": "Oblique",
"it": "Italic",
"i": "Italic",
"b": "Bold",
"normal": "Regular",
"c": "Condensed",
"r": "Regular",
"m": "Medium",
"l": "Light",
}
if style_name in known_names:
return known_names[style_name.lower()]
return style_name
@staticmethod
def find_in_dicts(key, dicts):
"""Find an entry in a list of dicts, return entry and in which list it was"""
for i, d in enumerate(dicts):
if key in d:
return (d[key], i)
return (None, 0)
@staticmethod
def get_shorten_form_idx(aggressive, prefix, form_if_prefixed):
"""Get the tuple index of known_* data tables"""
if aggressive:
return 0
if len(prefix):
return form_if_prefixed
return 1
@staticmethod
def shorten_style_name(name, aggressive):
"""Substitude some known styles to short form"""
# If aggressive is False create the mild short form
# aggressive == True: Always use first form of everything
# aggressive == False:
# - has no modifier: use the second form
# - has modifier: use second form of mod plus first form of weights2
# - has modifier: use second form of mod plus second form of widths
name_rest = name
name_pre = ""
form = FontnameTools.get_shorten_form_idx(aggressive, "", 0)
for mod in FontnameTools.known_modifiers:
if name.startswith(mod) and len(name) > len(
mod
): # Second condition specifically for 'Demi'
name_pre = FontnameTools.known_modifiers[mod][form]
name_rest = name[len(mod) :]
break
subst, i = FontnameTools.find_in_dicts(
name_rest, [FontnameTools.known_weights2, FontnameTools.known_widths]
)
form = FontnameTools.get_shorten_form_idx(aggressive, name_pre, i)
if isinstance(subst, tuple):
return name_pre + subst[form]
if not len(name_pre):
# The following sets do not allow modifiers
subst, _ = FontnameTools.find_in_dicts(
name_rest, [FontnameTools.known_weights1, FontnameTools.known_slopes]
)
if isinstance(subst, tuple):
return subst[form]
return name
@staticmethod
def short_styles(lists, aggressive):
"""Shorten all style names in a list or a list of lists"""
if not len(lists) or not isinstance(lists[0], list):
return list(
map(lambda x: FontnameTools.shorten_style_name(x, aggressive), lists)
)
return [
list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), styles))
for styles in lists
]
@staticmethod
def make_oblique_style(weights, styles):
"""Move "Oblique" from weights to styles for font naming purposes"""
if "Oblique" in weights:
weights = list(weights)
weights.remove("Oblique")
styles = list(styles)
styles.append("Oblique")
return (weights, styles)
@staticmethod
def get_name_token(name, tokens, allow_regex_token=False):
"""Try to find any case insensitive token from tokens in the name, return tuple with found token-list and rest"""
# The default mode (allow_regex_token = False) will try to find any verbatim string in the
# tokens list (case insensitive matching) and give that tokens list item back with
# unchanged case (i.e. [ 'Bold' ] will match "bold" and return it as [ 'Bold', ]
# In the regex mode (allow_regex_token = True) it will use the tokens elements as
# regexes and return the original (i.e. from name) case.
#
# Token are always used in a regex and may not capture, use non capturing
# grouping if needed (?: ... )
lower_tokens = [t.lower() for t in tokens]
not_matched = ""
all_tokens = []
j = 1
regex = re.compile("(.*?)(" + "|".join(tokens) + ")(.*)", re.IGNORECASE)
while j:
j = regex.match(name)
if not j:
break
if len(j.groups()) != 3:
sys.exit("Malformed regex in FontnameTools.get_name_token()")
not_matched += (
" " + j.groups()[0]
) # Blanc prevents unwanted concatenation of unmatched substrings
tok = j.groups()[1].lower()
if tok in lower_tokens:
tok = tokens[lower_tokens.index(tok)]
tok = FontnameTools.unify_style_names(tok)
if len(tok):
all_tokens.append(tok)
name = j.groups()[2] # Recurse rest
not_matched += " " + name
return (not_matched.strip(), all_tokens)
@staticmethod
def postscript_char_filter(name):
"""Filter out characters that are not allowed in Postscript names"""
# The name string must be restricted to the printable ASCII subset, codes 33 to 126,
# except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'
out = ""
for c in name:
if c in "[](){}<>/%" or ord(c) < 33 or ord(c) > 126:
continue
out += c
return out
SIL_TABLE = [
("(a)nonymous", r"\1nonymice"),
("(b)itstream( ?)(v)era( ?sans ?mono)?", r"\1itstrom\2Wera"),
("(s)ource", r"\1auce"),
("(h)ermit", r"\1urmit"),
("(h)asklig", r"\1asklug"),
("(s)hare", r"\1hure"),
("IBM[- ]?plex", r"Blex"), # We do not keep the case here
("(t)erminus", r"\1erminess"),
("(l)iberation", r"\1iteration"),
("iA([- ]?)writer", r"iM\1Writing"),
("(a)nka/(c)oder", r"\1na\2onder"),
("(c)ascadia( ?)(c)ode", r"\1askaydia\2\3ove"),
("(c)ascadia( ?)(m)ono", r"\1askaydia\2\3ono"),
("(m)( ?)plus", r"\1+"), # Added this, because they use a plus symbol :->
("Gohufont", r"GohuFont"), # Correct to CamelCase
# Noone cares that font names starting with a digit are forbidden:
("IBM 3270", r"3270"), # for historical reasons and 'IBM' is a TM or something
# Some name parts that are too long for us
("(.*sans ?m)ono", r"\1"), # Various SomenameSansMono fonts
("(.*code ?lat)in Expanded", r"\1X"), # for 'M PLUS Code Latin Expanded'
("(.*code ?lat)in", r"\1"), # for 'M PLUS Code Latin'
("(b)ig( ?)(b)lue( ?)(t)erminal", r"\1ig\3lue\5erm"), # Shorten BigBlueTerminal
("(.*)437TT", r"\g<1>437"), # Shorten BigBlueTerminal 437 TT even further
("(.*dyslexic ?alt)a", r"\1"), # Open Dyslexic Alta -> Open Dyslexic Alt
("(.*dyslexic ?m)ono", r"\1"), # Open Dyslexic Mono -> Open Dyslexic M
("(overpass ?m)ono", r"\1"), # Overpass Mono -> Overpass M
("(proggyclean) ?tt", r"\1"), # Remove TT from ProggyClean
(
"(terminess) ?\(ttf\)",
r"\1",
), # Remove TTF from Terminus (after renamed to Terminess)
("(im ?writing ?q)uattro", r"\1uat"), # Rename iM Writing Quattro to Quat
(
"(im ?writing ?(mono|duo|quat)) ?s",
r"\1",
), # Remove S from all iM Writing styles
]
# From https://adobe-type-tools.github.io/font-tech-notes/pdfs/5088.FontNames.pdf
# The first short variant is from the linked table.
# The second (longer) short variant is from diverse fonts like Noto.
# We can
# - use the long form
# - use the very short form (first)
# - use mild short form:
# - has no modifier: use the second form
# - has modifier: use second form of mod plus first form of weights2
# - has modifier: use second form of mod plus second form of widths
# This is encoded in get_shorten_form_idx()
known_weights1 = { # can not take modifiers
"Medium": ("Md", "Med"),
"Nord": ("Nd", "Nord"),
"Book": ("Bk", "Book"),
"Poster": ("Po", "Poster"),
"Demi": (
"Dm",
"Demi",
), # Demi is sometimes used as a weight, sometimes as a modifier
"Regular": ("Rg", "Reg"),
"Display": ("DS", "Disp"),
"Super": ("Su", "Sup"),
"Retina": ("Rt", "Ret"),
}
known_weights2 = { # can take modifiers
"Black": ("Blk", "Black"),
"Bold": ("Bd", "Bold"),
"Heavy": ("Hv", "Heavy"),
"Thin": ("Th", "Thin"),
"Light": ("Lt", "Light"),
" ": (), # Just for CodeClimate :-/
}
known_widths = { # can take modifiers
"Compressed": ("Cm", "Comp"),
"Extended": ("Ex", "Extd"),
"Condensed": ("Cn", "Cond"),
"Narrow": ("Nr", "Narrow"),
"Compact": ("Ct", "Compact"),
}
known_slopes = { # can not take modifiers
"Inclined": ("Ic", "Incl"),
"Oblique": ("Obl", "Obl"),
"Italic": ("It", "Italic"),
"Upright": ("Up", "Uprght"),
"Kursiv": ("Ks", "Kurs"),
"Sloped": ("Sl", "Slop"),
}
known_modifiers = {
"Demi": ("Dm", "Dem"),
"Ultra": ("Ult", "Ult"),
"Semi": ("Sm", "Sem"),
"Extra": ("X", "Ext"),
}
@staticmethod
def is_keep_regular(basename):
"""This has been decided by the font designers, we need to mimic that (for comparison purposes)"""
KEEP_REGULAR = [
"Agave",
"Arimo",
"Aurulent",
"Cascadia",
"Cousine",
"Fantasque",
"Fira",
"Overpass",
"Lilex",
"Inconsolata$", # not InconsolataGo
"IAWriter",
"Meslo",
"Monoid",
"Mononoki",
"Hack",
"JetBrains Mono",
"Noto Sans",
"Noto Serif",
"Victor",
]
for kr in KEEP_REGULAR:
if (basename.rstrip() + "$").startswith(kr):
return True
return False
@staticmethod
def _parse_simple_font_name(name):
"""Parse a filename that does not follow the 'FontFamilyName-FontStyle' pattern"""
# No dash in name, maybe we have blanc separated filename?
if " " in name:
return FontnameTools.parse_font_name(name.replace(" ", "-"))
# Do we have a number-name boundary?
p = re.split("(?<=[0-9])(?=[a-zA-Z])", name)
if len(p) > 1:
return FontnameTools.parse_font_name("-".join(p))
# Or do we have CamelCase?
n = FontnameTools.camel_explode(name)
if n != name:
return FontnameTools.parse_font_name(n.replace(" ", "-"))
return (False, FontnameTools.camel_casify(name), [], [], [], "")
@staticmethod
def parse_font_name(name):
"""Expects a filename following the 'FontFamilyName-FontStyle' pattern and returns ... parts"""
name = re.sub(
r"\bsemi-condensed\b", "SemiCondensed", name, 1, re.IGNORECASE
) # Just for "3270 Semi-Condensed" :-/
name = re.sub("[_\s]+", " ", name)
matches = re.match(r"([^-]+)(?:-(.*))?", name)
familyname = FontnameTools.camel_casify(matches.group(1))
style = matches.group(2)
if not style:
return FontnameTools._parse_simple_font_name(name)
# These are the FontStyle keywords we know, in three categories
# Weights end up as Typographic Family parts ('after the dash')
# Styles end up as Family parts (for classic grouping of four)
# Others also end up in Typographic Family ('before the dash')
weights = (
[
m + s
for s in list(FontnameTools.known_weights2)
+ list(FontnameTools.known_widths)
for m in list(FontnameTools.known_modifiers) + [""]
if m != s
]
+ list(FontnameTools.known_weights1)
+ list(FontnameTools.known_slopes)
)
styles = [
"Bold",
"Italic",
"Regular",
"Normal",
]
weights = [w for w in weights if w not in styles]
# Some font specialities:
other = [
"-",
"Book",
"For",
"Powerline",
"Text", # Plex
"IIx", # Profont IIx
"LGC", # Inconsolata LGC
r"\bCE\b", # ProggycleanTT CE
r"[12][cmp]n?", # MPlus
r"(?:uni-)?1[14]", # GohuFont uni
]
# Sometimes used abbreviations
weight_abbrevs = [
"ob",
"c",
"m",
"l",
]
style_abbrevs = [
"it",
"r",
"b",
"i",
]
(style, weight_token) = FontnameTools.get_name_token(style, weights)
(style, style_token) = FontnameTools.get_name_token(style, styles)
(style, other_token) = FontnameTools.get_name_token(style, other, True)
if (
len(style) < 4 and style.lower() != "pro"
): # Prevent 'r' of Pro to be detected as style_abbrev
(style, weight_token_abbrevs) = FontnameTools.get_name_token(
style, weight_abbrevs
)
(style, style_token_abbrevs) = FontnameTools.get_name_token(
style, style_abbrevs
)
weight_token += weight_token_abbrevs
style_token += style_token_abbrevs
while "Regular" in style_token and len(style_token) > 1:
# Correct situation where "Regular" and something else is given
style_token.remove("Regular")
# Recurse to see if unmatched stuff between dashes can belong to familyname
matches2 = re.match(r"(\w+)-(.*)", style)
if matches2:
return FontnameTools.parse_font_name(
familyname + matches2.group(1) + "-" + matches2.group(2)
)
style = re.sub(
r"(^|\s)\d+(\.\d+)+(\s|$)", r"\1\3", style
) # Remove (free standing) version numbers
style_parts = FontnameTools.drop_empty(style.split(" "))
style = " ".join(map(FontnameTools.front_upper, style_parts))
familyname = FontnameTools.camel_explode(familyname)
return (True, familyname, weight_token, style_token, other_token, style)

View file

@ -1,15 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
REPO_URL='https://github.com/ryanoasis/nerd-fonts.git' rm -rf font-patcher
wget https://github.com/ryanoasis/nerd-fonts/raw/master/FontPatcher.zip -O FontPatcher.zip
rm -rf nerd-fonts unzip -u FontPatcher.zip -x "readme.md"
git clone --filter=blob:none --no-checkout --depth 1 --sparse $REPO_URL
cd nerd-fonts || exit
git sparse-checkout add src/glyphs
git checkout
cp font-patcher ../bin/font-patcher
cp src/glyphs/** ../src/glyphs -r
echo "don't forget to commit your changes!" echo "don't forget to commit your changes!"

View file

@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf8 # coding=utf8
# Nerd Fonts Version: 2.3.3 # Nerd Fonts Version: 3.0.0
# Script version is further down # Script version is further down
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
# Change the script version when you edit this script: # Change the script version when you edit this script:
script_version = "3.7.1" script_version = "4.1.2"
version = "2.3.3" version = "3.0.0"
projectName = "Nerd Fonts" projectName = "Nerd Fonts"
projectNameAbbreviation = "NF" projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1] projectNameSingular = projectName[:-1]
@ -22,6 +22,7 @@ import errno
import subprocess import subprocess
import json import json
from enum import Enum from enum import Enum
import logging
try: try:
import configparser import configparser
except ImportError: except ImportError:
@ -239,10 +240,10 @@ def force_panose_monospaced(font):
panose = list(font.os2_panose) panose = list(font.os2_panose)
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default) if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
panose[0] = 2 # make kind latin text and display panose[0] = 2 # make kind latin text and display
print(" Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
font.os2_panose = tuple(panose) font.os2_panose = tuple(panose)
if panose[0] == 2 and panose[3] != 9: if panose[0] == 2 and panose[3] != 9:
print(" Setting Panose 'Proportion' to 'Monospaced' (was '{}')".format(panose_proportion_to_text(panose[3]))) logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3]))
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
font.os2_panose = tuple(panose) font.os2_panose = tuple(panose)
@ -296,10 +297,22 @@ def get_old_average_x_width(font):
} }
for g in weights: for g in weights:
if g not in font: if g not in font:
sys.exit("{}: Can not determine ancient style xAvgCharWidth".format(projectName)) logger.critical("Can not determine ancient style xAvgCharWidth")
sys.exit(1)
s += font[g].width * weights[g] s += font[g].width * weights[g]
return int(s / 1000) return int(s / 1000)
def create_filename(fonts):
""" Determine filename from font object(s) """
sfnt = { k: v for l, k, v in fonts[0].sfnt_names }
sfnt_pfam = sfnt.get('Preferred Family', sfnt['Family'])
sfnt_psubfam = sfnt.get('Preferred Styles', sfnt['SubFamily'])
if len(fonts) > 1:
return sfnt_pfam
if len(sfnt_psubfam) > 0:
sfnt_psubfam = '-' + sfnt_psubfam
return (sfnt_pfam + sfnt_psubfam).replace(' ', '')
class font_patcher: class font_patcher:
def __init__(self, args): def __init__(self, args):
@ -311,6 +324,7 @@ class font_patcher:
self.font_dim = None # class 'dict' self.font_dim = None # class 'dict'
self.font_extrawide = False self.font_extrawide = False
self.source_monospaced = None # Later True or False self.source_monospaced = None # Later True or False
self.symbolsonly = False
self.onlybitmaps = 0 self.onlybitmaps = 0
self.essential = set() self.essential = set()
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
@ -336,7 +350,7 @@ class font_patcher:
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs # For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs
if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2: if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
print("Very wide and short font, disabling 2 cell Powerline glyphs") logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs")
self.font_extrawide = True self.font_extrawide = True
# Prevent opening and closing the fontforge font. Makes things faster when patching # Prevent opening and closing the fontforge font. Makes things faster when patching
@ -345,8 +359,12 @@ class font_patcher:
symfont = None symfont = None
if not os.path.isdir(self.args.glyphdir): if not os.path.isdir(self.args.glyphdir):
sys.exit("{}: Can not find symbol glyph directory {} " logger.critical("Can not find symbol glyph directory %s "
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir)) "(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir)
sys.exit(1)
if self.args.dry_run:
return
for patch in self.patch_set: for patch in self.patch_set:
if patch['Enabled']: if patch['Enabled']:
@ -356,11 +374,13 @@ class font_patcher:
symfont.close() symfont.close()
symfont = None symfont = None
if not os.path.isfile(self.args.glyphdir + patch['Filename']): if not os.path.isfile(self.args.glyphdir + patch['Filename']):
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format( logger.critical("Can not find symbol source for '%s' (i.e. %s)",
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK): if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format( logger.critical("Can not open symbol source for '%s' (i.e. %s)",
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename'])) symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull' symfont.encoding = 'UnicodeFull'
@ -402,11 +422,11 @@ class font_patcher:
break break
outfile = os.path.normpath(os.path.join( outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True), sanitize_filename(self.args.outputdir, True),
sanitize_filename(sourceFont.familyname) + ".ttc")) sanitize_filename(create_filename(sourceFonts)) + ".ttc"))
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer) sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer)
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile) message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
else: else:
fontname = sourceFont.fullname fontname = create_filename(sourceFonts)
if not fontname: if not fontname:
fontname = sourceFont.cidfontname fontname = sourceFont.cidfontname
outfile = os.path.normpath(os.path.join( outfile = os.path.normpath(os.path.join(
@ -414,9 +434,11 @@ class font_patcher:
sanitize_filename(fontname) + self.args.extension)) sanitize_filename(fontname) + self.args.extension))
bitmaps = str() bitmaps = str()
if len(self.sourceFont.bitmapSizes): if len(self.sourceFont.bitmapSizes):
if not self.args.quiet: logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
bitmaps = str('otf') # otf/ttf, both is bf_ttf bitmaps = str('otf') # otf/ttf, both is bf_ttf
if self.args.dry_run:
logger.debug("=====> Filename '{}'".format(outfile))
return
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags) sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags)
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile) message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
@ -426,8 +448,7 @@ class font_patcher:
source_font = TableHEADWriter(self.args.font) source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile) dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts): for idx in range(source_font.num_fonts):
if not self.args.quiet: logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
xwidth_s = '' xwidth_s = ''
xwidth = self.xavgwidth[idx] xwidth = self.xavgwidth[idx]
if isinstance(xwidth, int): if isinstance(xwidth, int):
@ -438,26 +459,23 @@ class font_patcher:
dest_font.find_table([b'OS/2'], idx) dest_font.find_table([b'OS/2'], idx)
d_xwidth = dest_font.getshort('avgWidth') d_xwidth = dest_font.getshort('avgWidth')
if d_xwidth != xwidth: if d_xwidth != xwidth:
if not self.args.quiet: logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
print("Changing xAvgCharWidth from {} to {}{}".format(d_xwidth, xwidth, xwidth_s))
dest_font.putshort(xwidth, 'avgWidth') dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum() dest_font.reset_table_checksum()
source_font.find_head_table(idx) source_font.find_head_table(idx)
dest_font.find_head_table(idx) dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
if not self.args.quiet: logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int' dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem: if source_font.lowppem != dest_font.lowppem:
if not self.args.quiet: logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM') dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified: if dest_font.modified:
dest_font.reset_table_checksum() dest_font.reset_table_checksum()
if dest_font.modified: if dest_font.modified:
dest_font.reset_full_checksum() dest_font.reset_full_checksum()
except Exception as error: except Exception as error:
print("Can not handle font flags ({})".format(repr(error))) logger.error("Can not handle font flags (%s)", repr(error))
finally: finally:
try: try:
source_font.close() source_font.close()
@ -465,12 +483,13 @@ class font_patcher:
except: except:
pass pass
if self.args.is_variable: if self.args.is_variable:
print("Warning: Source font is a variable open type font (VF) and the patch results will most likely not be what you want") logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
print(message) print(message)
if self.args.postprocess: if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile]) subprocess.call([self.args.postprocess, outfile])
print("\nPost Processed: {}".format(outfile)) print("\n")
logger.info("Post Processed: %s", outfile)
def setup_name_backup(self, font): def setup_name_backup(self, font):
@ -488,11 +507,8 @@ class font_patcher:
font.fullname = font.persistent["fullname"] font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str): if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["familyname"] font.familyname = font.persistent["familyname"]
verboseAdditionalFontNameSuffix = " " + projectNameSingular verboseAdditionalFontNameSuffix = ""
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later additionalFontNameSuffix = ""
additionalFontNameSuffix = " " + projectNameAbbreviation
else:
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
if not self.args.complete: if not self.args.complete:
# NOTE not all symbol fonts have appended their suffix here # NOTE not all symbol fonts have appended their suffix here
if self.args.fontawesome: if self.args.fontawesome:
@ -523,17 +539,24 @@ class font_patcher:
additionalFontNameSuffix += " WEA" additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons" verboseAdditionalFontNameSuffix += " Plus Weather Icons"
# if all source glyphs included simplify the name # add mono signifier to beginning of name suffix
else:
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
# add mono signifier to end of name
if self.args.single: if self.args.single:
additionalFontNameSuffix += " M" variant_abbrev = "M"
verboseAdditionalFontNameSuffix += " Mono" variant_full = " Mono"
elif self.args.nonmono and not self.symbolsonly:
variant_abbrev = "P"
variant_full = " Propo"
else:
variant_abbrev = ""
variant_full = ""
if FontnameParserOK and self.args.makegroups: ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix
# add 'Nerd Font' to beginning of name suffix
verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix
additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix
if FontnameParserOK and self.args.makegroups > 0:
use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname # Use fullname if it is 'equal' to the fontname
if font.fullname: if font.fullname:
@ -545,12 +568,14 @@ class font_patcher:
# Gohu fontnames hide the weight, but the file names are ok... # Gohu fontnames hide the weight, but the file names are ok...
if parser_name.startswith('Gohu'): if parser_name.startswith('Gohu'):
parser_name = os.path.splitext(os.path.basename(self.args.font))[0] parser_name = os.path.splitext(os.path.basename(self.args.font))[0]
n = FontnameParser(parser_name) n = FontnameParser(parser_name, logger)
if not n.parse_ok: if not n.parse_ok:
print("Have only minimal naming information, check resulting name. Maybe omit --makegroups option") logger.warning("Have only minimal naming information, check resulting name. Maybe specify --makegroups 0")
n.drop_for_powerline() n.drop_for_powerline()
n.enable_short_families(True, "Noto") n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
n.set_for_windows(self.args.windows) if not n.set_expect_no_italic(self.args.noitalic):
logger.critical("Detected 'Italic' slant but --has-no-italic specified")
sys.exit(1)
# All the following stuff is ignored in makegroups-mode # All the following stuff is ignored in makegroups-mode
@ -598,23 +623,7 @@ class font_patcher:
if len(subFamily) == 0: if len(subFamily) == 0:
subFamily = "Regular" subFamily = "Regular"
if self.args.windows: familyname += " " + projectNameSingular + variant_full
maxFamilyLength = 31
maxFontLength = maxFamilyLength - len('-' + subFamily)
familyname += " " + projectNameAbbreviation
if self.args.single:
familyname += "M"
fullname += " Windows Compatible"
# now make sure less than 32 characters name length
if len(fontname) > maxFontLength:
fontname = fontname[:maxFontLength]
if len(familyname) > maxFamilyLength:
familyname = familyname[:maxFamilyLength]
else:
familyname += " " + projectNameSingular
if self.args.single:
familyname += " Mono"
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with # Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with
# the same name as the same font, even if subFamily is different. Make sure to # the same name as the same font, even if subFamily is different. Make sure to
@ -627,6 +636,10 @@ class font_patcher:
reservedFontNameReplacements = { reservedFontNameReplacements = {
'source' : 'sauce', 'source' : 'sauce',
'Source' : 'Sauce', 'Source' : 'Sauce',
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
'BitstreamVeraSansMono' : 'BitstromWera',
'bitstream vera sans mono' : 'bitstrom wera',
'bitstreamverasansmono' : 'bitstromwera',
'hermit' : 'hurmit', 'hermit' : 'hurmit',
'Hermit' : 'Hurmit', 'Hermit' : 'Hurmit',
'hasklig' : 'hasklug', 'hasklig' : 'hasklug',
@ -693,7 +706,7 @@ class font_patcher:
fullname = replace_font_name(fullname, additionalFontNameReplacements2) fullname = replace_font_name(fullname, additionalFontNameReplacements2)
fontname = replace_font_name(fontname, additionalFontNameReplacements2) fontname = replace_font_name(fontname, additionalFontNameReplacements2)
if not (FontnameParserOK and self.args.makegroups): if not (FontnameParserOK and self.args.makegroups > 0):
# replace any extra whitespace characters: # replace any extra whitespace characters:
font.familyname = " ".join(familyname.split()) font.familyname = " ".join(familyname.split())
font.fullname = " ".join(fullname.split()) font.fullname = " ".join(fullname.split())
@ -704,13 +717,9 @@ class font_patcher:
font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname) font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname)
font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
else: else:
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
if self.args.single: # inject_suffix(family, ps_fontname, short_family)
if self.args.windows: n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
fam_suffix += 'M'
else:
fam_suffix += ' Mono'
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
n.rename_font(font) n.rename_font(font)
font.comment = projectInfo font.comment = projectInfo
@ -726,25 +735,26 @@ class font_patcher:
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version) self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version)
# The Version SFNT name is later reused by the NameParser for UniqueID
# print("Version now is {}".format(sourceFont.version)) # print("Version now is {}".format(sourceFont.version))
def remove_ligatures(self): def remove_ligatures(self):
# let's deal with ligatures (mostly for monospaced fonts) # let's deal with ligatures (mostly for monospaced fonts)
# the tables have been removed from the repo with >this< commit # Usually removes 'fi' ligs that end up being only one cell wide, and 'ldot'
if self.args.configfile and self.config.read(self.args.configfile): if self.args.configfile and self.config.read(self.args.configfile):
if self.args.removeligatures: if self.args.removeligatures:
print("Removing ligatures from configfile `Subtables` section") logger.info("Removing ligatures from configfile `Subtables` section")
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures")) ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
for subtable in ligature_subtables: for subtable in ligature_subtables:
print("Removing subtable:", subtable) logger.debug("Removing subtable: %s", subtable)
try: try:
self.sourceFont.removeLookupSubtable(subtable) self.sourceFont.removeLookupSubtable(subtable)
print("Successfully removed subtable:", subtable) logger.debug("Successfully removed subtable: %s", subtable)
except Exception: except Exception:
print("Failed to remove subtable:", subtable) logger.error("Failed to remove subtable: %s", subtable)
elif self.args.removeligatures: elif self.args.removeligatures:
print("Unable to read configfile, unable to remove ligatures") logger.error("Unable to read configfile, unable to remove ligatures")
def assert_monospace(self): def assert_monospace(self):
@ -756,16 +766,17 @@ class font_patcher:
panose_mono = check_panose_monospaced(self.sourceFont) panose_mono = check_panose_monospaced(self.sourceFont)
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown' # The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown'
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1): if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
print(" Warning: Monospaced check: Panose assumed to be wrong") logger.warning("Monospaced check: Panose assumed to be wrong")
print(" {} and {}".format( logger.warning(" %s and %s",
report_advance_widths(self.sourceFont), report_advance_widths(self.sourceFont),
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))) panose_check_to_text(panose_mono, self.sourceFont.os2_panose))
if self.args.single and not width_mono: if self.args.single and not width_mono:
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
if offending_char is not None: if offending_char is not None:
print(" Offending char: 0x{:X}".format(offending_char)) logger.warning(" Offending char: %X", offending_char)
if self.args.single <= 1: if self.args.single <= 1:
sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching") logger.critical("Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
sys.exit(1)
if width_mono: if width_mono:
force_panose_monospaced(self.sourceFont) force_panose_monospaced(self.sourceFont)
@ -781,9 +792,9 @@ class font_patcher:
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs)) box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
if box_glyphs_target > box_glyphs_current: if box_glyphs_target > box_glyphs_current:
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing) # Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
if not self.args.quiet and box_glyphs_current > 0: if box_glyphs_current > 0:
print("INFO: {}/{} box drawing glyphs will be replaced".format( logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)) box_glyphs_current, box_glyphs_target)
box_enabled = True box_enabled = True
else: else:
# Sourcefont does have all of these glyphs # Sourcefont does have all of these glyphs
@ -1020,14 +1031,14 @@ class font_patcher:
{'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize {'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off)
{'Enabled': self.args.material, 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF305, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR} {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
] ]
@ -1102,9 +1113,20 @@ class font_patcher:
our_btb = typo_btb if use_typo else win_btb our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb: if our_btb == hhea_btb:
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
elif abs(our_btb - hhea_btb) / our_btb < 0.03:
logger.info("Font vertical metrics slightly off (%.1f%)", (our_btb - hhea_btb) / our_btb * 100.0)
metrics = Metric.TYPO if use_typo else Metric.WIN
else:
# Try the other metric
our_btb = typo_btb if not use_typo else win_btb
if our_btb == hhea_btb:
logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", 'True' if not use_typo else 'False')
use_typo = not use_typo
self.sourceFont.os2_use_typo_metrics = 1 if use_typo else 0
metrics = Metric.TYPO if use_typo else Metric.WIN
else: else:
# We trust the WIN metric more, see experiments in #1056 # We trust the WIN metric more, see experiments in #1056
print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb)) logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb)
our_btb = win_btb our_btb = win_btb
metrics = Metric.WIN metrics = Metric.WIN
@ -1129,6 +1151,7 @@ class font_patcher:
if self.font_dim['height'] == 0: if self.font_dim['height'] == 0:
# This can only happen if the input font is empty # This can only happen if the input font is empty
# Assume we are using our prepared templates # Assume we are using our prepared templates
self.symbolsonly = True
self.font_dim = { self.font_dim = {
'xmin' : 0, 'xmin' : 0,
'ymin' : -self.sourceFont.descent, 'ymin' : -self.sourceFont.descent,
@ -1139,7 +1162,8 @@ class font_patcher:
} }
our_btb = self.sourceFont.descent + self.sourceFont.ascent our_btb = self.sourceFont.descent + self.sourceFont.ascent
elif self.font_dim['height'] < 0: elif self.font_dim['height'] < 0:
sys.exit("{}: Can not detect sane font height".format(projectName)) logger.critical("Can not detect sane font height")
sys.exit(1)
# Make all metrics equal # Make all metrics equal
self.sourceFont.os2_typolinegap = 0 self.sourceFont.os2_typolinegap = 0
@ -1153,12 +1177,13 @@ class font_patcher:
self.sourceFont.os2_use_typo_metrics = 1 self.sourceFont.os2_use_typo_metrics = 1
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont) (check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont)
if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb: if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb:
sys.exit("{}: Error in baseline to baseline code detected".format(projectName)) logger.critical("Error in baseline to baseline code detected")
sys.exit(1)
# Step 2 # Step 2
# Find the biggest char width and advance width # Find the biggest char width and advance width
# 0x00-0x17f is the Latin Extended-A range # 0x00-0x17f is the Latin Extended-A range
warned1 = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target warned1 = self.args.nonmono # Do not warn if proportional target
warned2 = warned1 warned2 = warned1
for glyph in range(0x21, 0x17f): for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF) or glyph in [ if glyph in range(0x7F, 0xBF) or glyph in [
@ -1176,19 +1201,18 @@ class font_patcher:
if self.font_dim['width'] < self.sourceFont[glyph].width: if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width self.font_dim['width'] = self.sourceFont[glyph].width
if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format( logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
report_advance_widths(self.sourceFont))) report_advance_widths(self.sourceFont))
warned1 = True warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']: if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax self.font_dim['xmax'] = xmax
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Info: Extended glyphs wider bounding box than basic glyphs") logger.debug("Extended glyphs wider bounding box than basic glyphs")
warned2 = True warned2 = True
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.font_dim['xmax']: if self.font_dim['width'] < self.font_dim['xmax']:
if not self.args.quiet: logger.debug("Font has negative right side bearing in extended glyphs")
print("Warning: Font has negative right side bearing in extended glyphs")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim) # print("FINAL", self.font_dim)
@ -1288,7 +1312,7 @@ class font_patcher:
if sym_glyph.altuni: if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
if len(possible_codes) == 0: if len(possible_codes) == 0:
print(" Can not determine codepoint of {:X}. Skipping...".format(sym_glyph.unicode)) logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
continue continue
currentSourceFontGlyph = min(possible_codes) currentSourceFontGlyph = min(possible_codes)
else: else:
@ -1311,9 +1335,8 @@ class font_patcher:
# check if a glyph already exists in this location # check if a glyph already exists in this location
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential: if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
if currentSourceFontGlyph in self.sourceFont: if currentSourceFontGlyph in self.sourceFont:
if not self.args.quiet:
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing' careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph)) logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
# We don't want to touch anything so move to next Glyph # We don't want to touch anything so move to next Glyph
continue continue
else: else:
@ -1461,8 +1484,8 @@ class font_patcher:
if self.args.single: if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox() (xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)): if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)):
print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format( logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)) currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
# end for # end for
@ -1603,7 +1626,7 @@ def half_gap(gap, top):
gap_top = int(gap / 2) gap_top = int(gap / 2)
gap_bottom = gap - gap_top gap_bottom = gap - gap_top
if top: if top:
print("Redistributing line gap of {} ({} top and {} bottom)".format(gap, gap_top, gap_bottom)) logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom)
return gap_top return gap_top
return gap_bottom return gap_bottom
@ -1728,8 +1751,8 @@ def check_fontforge_min_version():
# versions tested: 20150612, 20150824 # versions tested: 20150612, 20150824
if actualVersion < minimumVersion: if actualVersion < minimumVersion:
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion)) logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion)) logger.critical("Please use at least version: %d", minimumVersion)
sys.exit(1) sys.exit(1)
def check_version_with_git(version): def check_version_with_git(version):
@ -1777,7 +1800,6 @@ def setup_arguments():
parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)') parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)')
parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)') parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)')
parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output') parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output')
parser.add_argument('-w', '--windows', dest='windows', default=False, action='store_true', help='Limit the internal font name to 31 characters (for Windows compatibility)')
parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs') parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs')
parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected') parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected')
parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file') parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
@ -1787,15 +1809,28 @@ def setup_arguments():
parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)') parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)')
parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to') parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching') parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
parser.add_argument('--makegroups', dest='makegroups', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)') parser.add_argument('--makegroups', dest='makegroups', default=1, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=1, choices=range(0, 6 + 1))
# --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'.
# Original font name: Hugo Sans Mono ExtraCondensed Light Italic
# NF Fam agg.
# 0 turned off, use old naming scheme [-] [-] [-]
# 1 HugoSansMono Nerd Font ExtraCondensed Light Italic [ ] [ ] [ ]
# 2 HugoSansMono Nerd Font ExtCn Light Italic [ ] [X] [ ]
# 3 HugoSansMono Nerd Font XCn Lt It [ ] [X] [X]
# 4 HugoSansMono NF ExtraCondensed Light Italic [X] [ ] [ ]
# 5 HugoSansMono NF ExtCn Light Italic [X] [X] [ ]
# 6 HugoSansMono NF XCn Lt It [X] [X] [X]
parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")') parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
parser.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique)')
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse # progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False) progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set') progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)')
progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set') progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set')
parser.set_defaults(progressbars=True) parser.set_defaults(progressbars=True)
parser.add_argument('--also-windows', dest='alsowindows', default=False, action='store_true', help='Create two fonts, the normal and the --windows version') parser.add_argument('--debug', dest='debugmode', default=False, action='store_true', help='Verbose mode')
parser.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming')
parser.add_argument('--xavgcharwidth', dest='xavgwidth', default=None, type=int, nargs='?', help='Adjust xAvgCharWidth (optional: concrete value)', const=True) parser.add_argument('--xavgcharwidth', dest='xavgwidth', default=None, type=int, nargs='?', help='Adjust xAvgCharWidth (optional: concrete value)', const=True)
# --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts # --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts
# Possible values with examples: # Possible values with examples:
@ -1819,8 +1854,9 @@ def setup_arguments():
args = parser.parse_args() args = parser.parse_args()
if args.makegroups and not FontnameParserOK: if args.makegroups > 0 and not FontnameParserOK:
sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName)) logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0")
sys.exit(1)
# if you add a new font, set it to True here inside the if condition # if you add a new font, set it to True here inside the if condition
if args.complete: if args.complete:
@ -1853,24 +1889,23 @@ def setup_arguments():
font_complete = False font_complete = False
args.complete = font_complete args.complete = font_complete
if args.alsowindows:
args.windows = False
if args.nonmono and args.single: if args.nonmono and args.single:
print("Warning: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.") logging.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
args.nonmono = False args.nonmono = False
make_sure_path_exists(args.outputdir) make_sure_path_exists(args.outputdir)
if not os.path.isfile(args.font): if not os.path.isfile(args.font):
sys.exit("{}: Font file does not exist: {}".format(projectName, args.font)) logging.critical("Font file does not exist: %s", args.font)
sys.exit(1)
if not os.access(args.font, os.R_OK): if not os.access(args.font, os.R_OK):
sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font)) logging.critical("Can not open font file for reading: %s", args.font)
sys.exit(1)
is_ttc = len(fontforge.fontsInFile(args.font)) > 1 is_ttc = len(fontforge.fontsInFile(args.font)) > 1
try: try:
source_font_test = TableHEADWriter(args.font) source_font_test = TableHEADWriter(args.font)
args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0) args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0)
if args.is_variable: if args.is_variable:
print(" Warning: Source font is a variable open type font (VF), opening might fail...") logging.warning("Source font is a variable open type font (VF), opening might fail...")
except: except:
args.is_variable = False args.is_variable = False
finally: finally:
@ -1885,16 +1920,20 @@ def setup_arguments():
args.extension = '.' + args.extension args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE): if re.match("\.ttc$", args.extension, re.IGNORECASE):
if not is_ttc: if not is_ttc:
sys.exit(projectName + ": Can not create True Type Collections from single font files") logging.critical("Can not create True Type Collections from single font files")
sys.exit(1)
else: else:
if is_ttc: if is_ttc:
sys.exit(projectName + ": Can not create single font files from True Type Collections") logging.critical("Can not create single font files from True Type Collections")
sys.exit(1)
if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool): if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool):
if args.xavgwidth < 0: if args.xavgwidth < 0:
sys.exit(projectName + ": --xavgcharwidth takes no negative numbers") logging.critical("--xavgcharwidth takes no negative numbers")
sys.exit(2)
if args.xavgwidth > 16384: if args.xavgwidth > 16384:
sys.exit(projectName + ": --xavgcharwidth takes only numbers up to 16384") logging.critical("--xavgcharwidth takes only numbers up to 16384")
sys.exit(2)
return args return args
@ -1902,24 +1941,43 @@ def setup_arguments():
def main(): def main():
global version global version
git_version = check_version_with_git(version) git_version = check_version_with_git(version)
print("{} Patcher v{} ({}) (ff {}) executing".format( allversions = "Patcher v{} ({}) (ff {})".format(
projectName, git_version if git_version else version, script_version, fontforge.version())) git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
if git_version: if git_version:
version = git_version version = git_version
check_fontforge_min_version() check_fontforge_min_version()
args = setup_arguments() args = setup_arguments()
global logger
logger = logging.getLogger(os.path.basename(args.font))
logger.setLevel(logging.DEBUG)
f_handler = logging.FileHandler('font-patcher-log.txt')
f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s'))
logger.addHandler(f_handler)
logger.debug(allversions)
logger.debug("Options %s", repr(sys.argv[1:]))
c_handler = logging.StreamHandler(stream=sys.stdout)
c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
if not args.debugmode:
c_handler.setLevel(logging.INFO)
logger.addHandler(c_handler)
logger.debug("Naming mode %d", args.makegroups)
patcher = font_patcher(args) patcher = font_patcher(args)
sourceFonts = [] sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font) all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts): for i, subfont in enumerate(all_fonts):
if len(all_fonts) > 1: if len(all_fonts) > 1:
print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts))) print("\n")
logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts))
try: try:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",)) sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception: except Exception:
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format( logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
projectName, subfont)) subfont)
sys.exit(1)
patcher.patch(sourceFonts[-1]) patcher.patch(sourceFonts[-1])
@ -1928,13 +1986,6 @@ def main():
patcher.setup_font_names(f) patcher.setup_font_names(f)
patcher.generate(sourceFonts) patcher.generate(sourceFonts)
# This mainly helps to improve CI runtime
if patcher.args.alsowindows:
patcher.args.windows = True
for f in sourceFonts:
patcher.setup_font_names(f)
patcher.generate(sourceFonts)
for f in sourceFonts: for f in sourceFonts:
f.close() f.close()

View file

@ -140,7 +140,7 @@ def patch_single_font(
cmd = [ cmd = [
"fontforge", "fontforge",
"-script", "-script",
"./bin/font-patcher", "./font-patcher",
"--glyphdir", "--glyphdir",
"./src/glyphs/", "./src/glyphs/",
"-out", "-out",
@ -182,8 +182,6 @@ def patch_font_dir_docker(
f"{font_dir_path}:/in", f"{font_dir_path}:/in",
"-v", "-v",
f"{output_path}:/out", f"{output_path}:/out",
"-u",
f"{os.getuid()}:{os.getegid()}",
"nerdfonts/patcher", "nerdfonts/patcher",
] ]

Binary file not shown.

View file

@ -1,59 +0,0 @@
SplineFontDB: 3.0
FontName: Symbols-1000-em
FullName: Symbols-1000-em
FamilyName: Symbols
Weight: Regular
Copyright: Copyright (c) 2016, Ryan McIntyre
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -100
UnderlineWidth: 50
Ascent: 800
Descent: 200
InvalidEm: 0
LayerCount: 2
Layer: 0 0 "Back" 1
Layer: 1 0 "Fore" 0
XUID: [1021 913 -638292798 6571593]
FSType: 0
OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1480466430
ModificationTime: 1480467813
PfmFamily: 17
TTFWeight: 400
TTFWidth: 5
LineGap: 90
VLineGap: 0
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
OS2TypoDOffset: 1
OS2TypoLinegap: 90
OS2WinAscent: 0
OS2WinAOffset: 1
OS2WinDescent: 0
OS2WinDOffset: 1
HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2Vendor: 'PfEd'
MarkAttachClasses: 1
DEI: 91125
LangName: 1033
Encoding: UnicodeFull
UnicodeInterp: none
NameList: AGL For New Fonts
DisplaySize: -72
AntiAlias: 1
FitToEm: 0
WinInfo: 64 8 8
OnlyBitmaps: 1
BeginPrivate: 0
EndPrivate
TeXData: 1 0 0 346030 173015 115343 0 1048576 115343 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144
BeginChars: 1114112 0
EndChars
EndSplineFont

View file

@ -1,59 +0,0 @@
SplineFontDB: 3.0
FontName: Symbols-2048-em
FullName: Symbols-2048-em
FamilyName: Symbols
Weight: Regular
Copyright: Copyright (c) 2016, Ryan McIntyre
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -204
UnderlineWidth: 102
Ascent: 1638
Descent: 410
InvalidEm: 0
LayerCount: 2
Layer: 0 0 "Back" 1
Layer: 1 0 "Fore" 0
XUID: [1021 913 -638292798 6571593]
FSType: 0
OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1480466430
ModificationTime: 1480467841
PfmFamily: 17
TTFWeight: 400
TTFWidth: 5
LineGap: 184
VLineGap: 0
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
OS2TypoDOffset: 1
OS2TypoLinegap: 184
OS2WinAscent: 0
OS2WinAOffset: 1
OS2WinDescent: 0
OS2WinDOffset: 1
HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2Vendor: 'PfEd'
MarkAttachClasses: 1
DEI: 91125
LangName: 1033
Encoding: UnicodeFull
UnicodeInterp: none
NameList: AGL For New Fonts
DisplaySize: -72
AntiAlias: 1
FitToEm: 0
WinInfo: 64 8 8
OnlyBitmaps: 1
BeginPrivate: 0
EndPrivate
TeXData: 1 0 0 346030 173015 115343 0 1048576 115343 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144
BeginChars: 1114112 0
EndChars
EndSplineFont

Binary file not shown.

0
src/glyphs/devicons.ttf Executable file → Normal file
View file

Binary file not shown.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# coding=utf8
# This extracts the names and source and destination codepoints
# of the old octicons font file to keep their codepoints stable
#
# You do not need to redo it, the result is in the repo
#
# Usage:
# fontforge analyze_octicons > mapping
import fontforge
octi_orig = "octicons.ttf"
current_cp = 0xF400
print("# Examining {}".format(octi_orig))
font = fontforge.open(octi_orig)
for glyph in font.glyphs("encoding"):
point = glyph.unicode
if point < 0:
continue
desti = glyph.unicode
if point < 0xF000:
desti = point
else:
desti = current_cp
current_cp += 1
print("{:X} {:X} {}".format(point, desti, glyph.glyphname))
font.close()

201
src/glyphs/octicons/generate Executable file
View file

@ -0,0 +1,201 @@
#!/usr/bin/env python3
# coding=utf8
import sys
import os
import re
import subprocess
import fontforge
# Double-quotes required here, for version-bump.sh:
version = "2.3.3"
archive = "v18.3.0.tar.gz"
vectorsdir = "icons"
fontdir = "."
fontfile = "octicons.ttf"
glyphsetfile = "i_oct.sh"
glyphsetsdir = "../../../bin/scripts/lib"
subset = "-16" # use 16 px subset if possible
subset_other = "-24" # use 24 px subset otherwise
def renamer(old_name):
"""Return new equivalent icon name"""
return {
"trashcan": "trash",
"cloud-download": "download",
"cloud-upload": "upload",
"clippy": "paste",
"mail-read": "read",
"primitive-dot": "dot-fill",
"primitive-square": "square-fill",
"settings": "sliders",
"dashboard": "meter",
"paintcan": "paintbrush",
}.get(old_name, old_name)
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)
glyph.manualHints = True
def createGlyphInfo(icon_datasets, filepathname, into):
"""Write the glyphinfo file"""
with open(filepathname, "w", encoding="utf8") as f:
f.write("#!/usr/bin/env bash\n")
f.write(intro)
f.write("# Script Version: (autogenerated)\n")
f.write('test -n "$__i_oct_loaded" && return || __i_oct_loaded=1\n')
for _, codepoint, name in icon_datasets:
codepoint = int(codepoint, 16)
f.write(
"i='{}' i_oct_{}=$i\n".format(chr(codepoint), name.replace("-", "_"))
)
f.write("unset i\n")
print("\nReading mapping file")
old_mapping = []
with open("mapping", "r") as f:
for line in f.readlines():
if line.startswith("#"):
continue
old_mapping.append(tuple(re.split(" +", line.strip())))
print("Found {} entries".format(len(old_mapping)))
old_mapping.sort(key=(lambda x: x[0]))
print('Fetching octicons archive "{}"\n'.format(archive))
if subprocess.call(
"curl -OL https://github.com/primer/octicons/archive/" + archive, shell=True
):
sys.exit("Error fetching octicons archive")
print("\nUnpacking octicons archive")
if subprocess.call(
"rm -rf icons octicons-* && tar zxf *.gz && mv octicons-*/icons . && rm -rf octicons-*",
shell=True,
):
sys.exit("Error unpacking archive")
svgs = os.listdir(vectorsdir)
print("Found {} svgs".format(len(svgs)))
names = {
s[0 : -len("-xx.svg")]
for s in svgs
if s.endswith(subset + ".svg") or s.endswith(subset_other + ".svg")
}
print("Found {} icons after de-duplicating\n".format(len(names)))
num_found = 0
num_missing = 0
misslist = ""
renamelist = ""
freeslots = []
new_mapping = []
for i, j, old_n in old_mapping:
if old_n in names:
names.remove(old_n)
new_mapping.append((i, j, old_n))
num_found += 1
continue
new_n = renamer(old_n)
if new_n in names:
renamelist += "Renamed {} -> {}\n".format(old_n, new_n)
names.remove(new_n)
new_mapping.append((i, j, new_n))
num_found += 1
continue
misslist += "Missing {}\n".format(old_n)
freeslots.append((i, j))
num_missing += 1
print(renamelist)
print(misslist)
print(
"Found {} (of {}, missing {}) and new {}".format(
num_found, len(old_mapping), num_missing, len(names)
)
)
names = list(names)
names.sort()
for n in list(names):
if len(freeslots) == 0:
break
i, j = freeslots[0]
new_mapping.append((i, j, n))
names.remove(n)
freeslots = freeslots[1:]
print("Filled in missing, remaining new {}".format(len(names)))
i_max = 0
j_max = 0
for i, j, _ in new_mapping:
i = int(i, 16)
j = int(j, 16)
if i > i_max:
i_max = i
if j > j_max:
j_max = j
for n in names:
i_max += 1
j_max += 1
new_mapping.append(("{:X}".format(i_max), "{:X}".format(j_max), n))
print("Appended remaining new, total new mapping {}".format(len(new_mapping)))
new_mapping.sort(key=(lambda x: x[0]))
with open("mapping", "w") as f:
for i, j, n in new_mapping:
f.write("{} {} {}\n".format(i, j, n))
font = fontforge.font()
font.fontname = "OcticonsNerdFont-Regular"
font.fullname = "Octicons Nerd Font Regular"
font.familyname = "Octicons Nerd Font"
font.em = 2048
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 = "GitHub 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", "GitHub Inc.")
for codepoint, _, name in new_mapping:
codepoint = int(codepoint, 16)
filename = name + subset + ".svg"
if filename not in svgs:
filename = name + subset_other + ".svg"
addIcon(codepoint, name, filename)
num_icons = len(new_mapping)
print("Generating {} with {} glyphs".format(fontfile, num_icons))
font.generate(os.path.join(fontdir, fontfile), flags=("no-FFTM-table",))
codepoints = [int(p, 16) for _, p, _ in new_mapping]
intro = "# Octicons ({} icons)\n".format(num_icons)
intro += "# Codepoints: {:X}-{:X} with gaps\n".format(min(codepoints), max(codepoints))
intro += "# Nerd Fonts Version: {}\n".format(version)
print("Generating GlyphInfo {}".format(glyphsetfile))
createGlyphInfo(new_mapping, os.path.join(glyphsetsdir, glyphsetfile), intro)
print("Finished")

309
src/glyphs/octicons/mapping Normal file
View file

@ -0,0 +1,309 @@
2665 2665 heart
26A1 26A1 zap
F000 F400 light-bulb
F001 F401 repo
F002 F402 repo-forked
F005 F403 repo-push
F006 F404 repo-pull
F007 F405 book
F008 F406 accessibility
F009 F407 git-pull-request
F00A F408 mark-github
F00B F409 download
F00C F40A upload
F00D F40B accessibility-inset
F00E F40C alert-fill
F010 F40D file-code
F011 F40E apps
F012 F40F file-media
F013 F410 file-zip
F014 F411 archive
F015 F412 tag
F016 F413 file-directory
F017 F414 file-submodule
F018 F415 person
F019 F416 arrow-both
F01F F417 git-commit
F020 F418 git-branch
F023 F419 git-merge
F024 F41A mirror
F026 F41B issue-opened
F027 F41C issue-reopened
F028 F41D issue-closed
F02A F41E star
F02B F41F comment
F02C F420 question
F02D F421 alert
F02E F422 search
F02F F423 gear
F030 F424 arrow-down-left
F031 F425 tools
F032 F426 sign-out
F033 F427 rocket
F034 F428 rss
F035 F429 paste
F036 F42A sign-in
F037 F42B organization
F038 F42C device-mobile
F039 F42D unfold
F03A F42E check
F03B F42F mail
F03C F430 read
F03D F431 arrow-up
F03E F432 arrow-right
F03F F433 arrow-down
F040 F434 arrow-left
F041 F435 pin
F042 F436 gift
F043 F437 graph
F044 F438 triangle-left
F045 F439 credit-card
F046 F43A clock
F047 F43B ruby
F048 F43C broadcast
F049 F43D key
F04A F43E arrow-down-right
F04C F43F repo-clone
F04D F440 diff
F04E F441 eye
F04F F442 comment-discussion
F051 F443 arrow-switch
F052 F444 dot-fill
F053 F445 square-fill
F056 F446 device-camera
F057 F447 device-camera-video
F058 F448 pencil
F059 F449 info
F05A F44A triangle-right
F05B F44B triangle-down
F05C F44C link
F05D F44D plus
F05E F44E three-bars
F05F F44F code
F060 F450 location
F061 F451 list-unordered
F062 F452 list-ordered
F063 F453 quote
F064 F454 versions
F068 F455 calendar
F06A F456 lock
F06B F457 diff-added
F06C F458 diff-removed
F06D F459 diff-modified
F06E F45A diff-renamed
F070 F45B horizontal-rule
F071 F45C arrow-up-left
F075 F45D milestone
F076 F45E checklist
F077 F45F megaphone
F078 F460 chevron-right
F07B F461 bookmark
F07C F462 sliders
F07D F463 meter
F07E F464 history
F07F F465 link-external
F080 F466 mute
F081 F467 x
F084 F468 circle-slash
F085 F469 pulse
F087 F46A sync
F088 F46B telescope
F08C F46C arrow-up-right
F08D F46D home
F08F F46E stop
F091 F46F bug
F092 F470 logo-github
F094 F471 file-binary
F096 F472 database
F097 F473 server
F099 F474 diff-ignored
F09A F475 ellipsis
F09C F476 bell-fill
F09D F477 hubot
F09F F478 bell-slash
F0A0 F479 blocked
F0A1 F47A bookmark-fill
F0A2 F47B chevron-up
F0A3 F47C chevron-down
F0A4 F47D chevron-left
F0AA F47E triangle-up
F0AC F47F git-compare
F0AD F480 logo-gist
F0B0 F481 file-symlink-file
F0B1 F482 bookmark-slash
F0B2 F483 squirrel
F0B6 F484 globe
F0BA F485 unmute
F0BE F486 mention
F0C4 F487 package
F0C5 F488 browser
F0C8 F489 terminal
F0C9 F48A markdown
F0CA F48B dash
F0CC F48C fold
F0CF F48D inbox
F0D0 F48E trash
F0D1 F48F paintbrush
F0D2 F490 flame
F0D3 F491 briefcase
F0D4 F492 plug
F0D6 F493 bookmark-slash-fill
F0D7 F494 mortar-board
F0D8 F495 law
F0DA F496 thumbsup
F0DB F497 thumbsdown
F0DC F498 desktop-download
F0DD F499 beaker
F0DE F49A bell
F0E0 F49B cache
F0E1 F49C shield
F0E2 F49D bold
F0E3 F49E check-circle
F0E4 F49F italic
F0E5 F4A0 tasklist
F0E6 F4A1 verified
F0E7 F4A2 smiley
F0E8 F4A3 unverified
F101 F4A4 check-circle-fill
F102 F4A5 file
F103 F4A6 grabber
F104 F4A7 checkbox
F105 F4A8 reply
F27C F4A9 device-desktop
F27D F4AA circle
F27E F4AB clock-fill
F27F F4AC cloud
F280 F4AD cloud-offline
F281 F4AE code-of-conduct
F282 F4AF code-review
F283 F4B0 code-square
F284 F4B1 codescan
F285 F4B2 codescan-checkmark
F286 F4B3 codespaces
F287 F4B4 columns
F288 F4B5 command-palette
F289 F4B6 commit
F28A F4B7 container
F28B F4B8 copilot
F28C F4B9 copilot-error
F28D F4BA copilot-warning
F28E F4BB copy
F28F F4BC cpu
F290 F4BD cross-reference
F291 F4BE dependabot
F292 F4BF diamond
F293 F4C0 discussion-closed
F294 F4C1 discussion-duplicate
F295 F4C2 discussion-outdated
F296 F4C3 dot
F297 F4C4 duplicate
F298 F4C5 eye-closed
F299 F4C6 feed-discussion
F29A F4C7 feed-forked
F29B F4C8 feed-heart
F29C F4C9 feed-merged
F29D F4CA feed-person
F29E F4CB feed-repo
F29F F4CC feed-rocket
F2A0 F4CD feed-star
F2A1 F4CE feed-tag
F2A2 F4CF feed-trophy
F2A3 F4D0 file-added
F2A4 F4D1 file-badge
F2A5 F4D2 file-diff
F2A6 F4D3 file-directory-fill
F2A7 F4D4 file-directory-open-fill
F2A8 F4D5 file-moved
F2A9 F4D6 file-removed
F2AA F4D7 filter
F2AB F4D8 fiscal-host
F2AC F4D9 fold-down
F2AD F4DA fold-up
F2AE F4DB git-merge-queue
F2AF F4DC git-pull-request-closed
F2B0 F4DD git-pull-request-draft
F2B1 F4DE goal
F2B2 F4DF hash
F2B3 F4E0 heading
F2B4 F4E1 heart-fill
F2B5 F4E2 home-fill
F2B6 F4E3 hourglass
F2B7 F4E4 id-badge
F2B8 F4E5 image
F2B9 F4E6 infinity
F2BA F4E7 issue-draft
F2BB F4E8 issue-tracked-by
F2BC F4E9 issue-tracks
F2BD F4EA iterations
F2BE F4EB kebab-horizontal
F2BF F4EC key-asterisk
F2C0 F4ED log
F2C1 F4EE moon
F2C2 F4EF move-to-bottom
F2C3 F4F0 move-to-end
F2C4 F4F1 move-to-start
F2C5 F4F2 move-to-top
F2C6 F4F3 multi-select
F2C7 F4F4 no-entry
F2C8 F4F5 north-star
F2C9 F4F6 note
F2CA F4F7 number
F2CB F4F8 package-dependencies
F2CC F4F9 package-dependents
F2CD F4FA paper-airplane
F2CE F4FB paperclip
F2CF F4FC passkey-fill
F2D0 F4FD people
F2D1 F4FE person-add
F2D2 F4FF person-fill
F2D3 F500 play
F2D4 F501 plus-circle
F2D5 F502 project
F2D6 F503 project-roadmap
F2D7 F504 project-symlink
F2D8 F505 project-template
F2D9 F506 rel-file-path
F2DA F507 repo-deleted
F2DB F508 repo-locked
F2DC F509 repo-template
F2DD F50A report
F2DE F50B rows
F2DF F50C screen-full
F2E0 F50D screen-normal
F2E1 F50E share
F2E2 F50F share-android
F2E3 F510 shield-check
F2E4 F511 shield-lock
F2E5 F512 shield-slash
F2E6 F513 shield-x
F2E7 F514 sidebar-collapse
F2E8 F515 sidebar-expand
F2E9 F516 single-select
F2EA F517 skip
F2EB F518 skip-fill
F2EC F519 sort-asc
F2ED F51A sort-desc
F2EE F51B sparkle-fill
F2EF F51C sponsor-tiers
F2F0 F51D square
F2F1 F51E stack
F2F2 F51F star-fill
F2F3 F520 stopwatch
F2F4 F521 strikethrough
F2F5 F522 sun
F2F6 F523 tab
F2F7 F524 tab-external
F2F8 F525 table
F2F9 F526 telescope-fill
F2FA F527 trophy
F2FB F528 typography
F2FC F529 unlink
F2FD F52A unlock
F2FE F52B unread
F2FF F52C video
F300 F52D webhook
F301 F52E workflow
F302 F52F x-circle
F303 F530 x-circle-fill
F304 F531 zoom-in
F305 F532 zoom-out

Binary file not shown.

View file

@ -433,4 +433,3 @@ Foobar.org is a distributed community of developers...
Company.com is a small business who likes to support community designers... Company.com is a small business who likes to support community designers...
University.edu is a renowned educational institution with a strong design department... University.edu is a renowned educational institution with a strong design department...
----- -----