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:
workflow_dispatch:
# schedule:
schedule:
# 1st and 15th at 12:00 AM
# - cron: "0 0 1,15 * *"
- cron: "0 0 1,15 * *"
permissions:
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
MonoLisa/*
@ -7,3 +184,4 @@ patched/*
nerd-fonts
.env
font-patcher-log.txt

View File

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

View File

@ -1,7 +1,7 @@
-include .env
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 \
$(ARGS) \
-f MonoLisa/
@ -24,6 +24,8 @@ lint: ## run pre-commit hooks
clean: ## remove patched fonts
@rm -rf patched/*
@rm -f ./font-patcher-log.txt
@rm -f FontPatcher.zip
# depends on daylinmorgan/yartsu
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
REPO_URL='https://github.com/ryanoasis/nerd-fonts.git'
rm -rf nerd-fonts
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
rm -rf font-patcher
wget https://github.com/ryanoasis/nerd-fonts/raw/master/FontPatcher.zip -O FontPatcher.zip
unzip -u FontPatcher.zip -x "readme.md"
echo "don't forget to commit your changes!"

View File

@ -1,14 +1,14 @@
#!/usr/bin/env python
# coding=utf8
# Nerd Fonts Version: 2.3.3
# Nerd Fonts Version: 3.0.0
# Script version is further down
from __future__ import absolute_import, print_function, unicode_literals
# 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"
projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1]
@ -22,6 +22,7 @@ import errno
import subprocess
import json
from enum import Enum
import logging
try:
import configparser
except ImportError:
@ -239,10 +240,10 @@ def force_panose_monospaced(font):
panose = list(font.os2_panose)
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
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)
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
font.os2_panose = tuple(panose)
@ -296,10 +297,22 @@ def get_old_average_x_width(font):
}
for g in weights:
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]
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:
def __init__(self, args):
@ -311,6 +324,7 @@ class font_patcher:
self.font_dim = None # class 'dict'
self.font_extrawide = False
self.source_monospaced = None # Later True or False
self.symbolsonly = False
self.onlybitmaps = 0
self.essential = set()
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
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
# Prevent opening and closing the fontforge font. Makes things faster when patching
@ -345,8 +359,12 @@ class font_patcher:
symfont = None
if not os.path.isdir(self.args.glyphdir):
sys.exit("{}: Can not find symbol glyph directory {} "
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir))
logger.critical("Can not find symbol glyph directory %s "
"(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:
if patch['Enabled']:
@ -356,11 +374,13 @@ class font_patcher:
symfont.close()
symfont = None
if not os.path.isfile(self.args.glyphdir + patch['Filename']):
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format(
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
logger.critical("Can not find symbol source for '%s' (i.e. %s)",
patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format(
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
logger.critical("Can not open symbol source for '%s' (i.e. %s)",
patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull'
@ -402,11 +422,11 @@ class font_patcher:
break
outfile = os.path.normpath(os.path.join(
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)
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
else:
fontname = sourceFont.fullname
fontname = create_filename(sourceFonts)
if not fontname:
fontname = sourceFont.cidfontname
outfile = os.path.normpath(os.path.join(
@ -414,9 +434,11 @@ class font_patcher:
sanitize_filename(fontname) + self.args.extension))
bitmaps = str()
if len(self.sourceFont.bitmapSizes):
if not self.args.quiet:
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
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)
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
@ -426,8 +448,7 @@ class font_patcher:
source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts):
if not self.args.quiet:
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
xwidth_s = ''
xwidth = self.xavgwidth[idx]
if isinstance(xwidth, int):
@ -438,26 +459,23 @@ class font_patcher:
dest_font.find_table([b'OS/2'], idx)
d_xwidth = dest_font.getshort('avgWidth')
if d_xwidth != xwidth:
if not self.args.quiet:
print("Changing xAvgCharWidth from {} to {}{}".format(d_xwidth, xwidth, xwidth_s))
logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum()
source_font.find_head_table(idx)
dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
if not self.args.quiet:
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem:
if not self.args.quiet:
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
if dest_font.modified:
dest_font.reset_full_checksum()
except Exception as error:
print("Can not handle font flags ({})".format(repr(error)))
logger.error("Can not handle font flags (%s)", repr(error))
finally:
try:
source_font.close()
@ -465,12 +483,13 @@ class font_patcher:
except:
pass
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)
if self.args.postprocess:
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):
@ -488,11 +507,8 @@ class font_patcher:
font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["familyname"]
verboseAdditionalFontNameSuffix = " " + projectNameSingular
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later
additionalFontNameSuffix = " " + projectNameAbbreviation
else:
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
verboseAdditionalFontNameSuffix = ""
additionalFontNameSuffix = ""
if not self.args.complete:
# NOTE not all symbol fonts have appended their suffix here
if self.args.fontawesome:
@ -523,17 +539,24 @@ class font_patcher:
additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons"
# if all source glyphs included simplify the name
else:
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
# add mono signifier to end of name
# add mono signifier to beginning of name suffix
if self.args.single:
additionalFontNameSuffix += " M"
verboseAdditionalFontNameSuffix += " Mono"
variant_abbrev = "M"
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 if it is 'equal' to the fontname
if font.fullname:
@ -545,12 +568,14 @@ class font_patcher:
# Gohu fontnames hide the weight, but the file names are ok...
if parser_name.startswith('Gohu'):
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:
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.enable_short_families(True, "Noto")
n.set_for_windows(self.args.windows)
n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
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
@ -598,23 +623,7 @@ class font_patcher:
if len(subFamily) == 0:
subFamily = "Regular"
if self.args.windows:
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"
familyname += " " + projectNameSingular + variant_full
# 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
@ -627,6 +636,10 @@ class font_patcher:
reservedFontNameReplacements = {
'source' : 'sauce',
'Source' : 'Sauce',
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
'BitstreamVeraSansMono' : 'BitstromWera',
'bitstream vera sans mono' : 'bitstrom wera',
'bitstreamverasansmono' : 'bitstromwera',
'hermit' : 'hurmit',
'Hermit' : 'Hurmit',
'hasklig' : 'hasklug',
@ -693,7 +706,7 @@ class font_patcher:
fullname = replace_font_name(fullname, 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:
font.familyname = " ".join(familyname.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('SubFamily'), subFamily)
else:
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation
if self.args.single:
if self.args.windows:
fam_suffix += 'M'
else:
fam_suffix += ' Mono'
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
# inject_suffix(family, ps_fontname, short_family)
n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
n.rename_font(font)
font.comment = projectInfo
@ -726,25 +735,26 @@ class font_patcher:
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
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))
def remove_ligatures(self):
# 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.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"))
for subtable in ligature_subtables:
print("Removing subtable:", subtable)
logger.debug("Removing subtable: %s", subtable)
try:
self.sourceFont.removeLookupSubtable(subtable)
print("Successfully removed subtable:", subtable)
logger.debug("Successfully removed subtable: %s", subtable)
except Exception:
print("Failed to remove subtable:", subtable)
logger.error("Failed to remove subtable: %s", subtable)
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):
@ -756,16 +766,17 @@ class font_patcher:
panose_mono = check_panose_monospaced(self.sourceFont)
# 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):
print(" Warning: Monospaced check: Panose assumed to be wrong")
print(" {} and {}".format(
logger.warning("Monospaced check: Panose assumed to be wrong")
logger.warning(" %s and %s",
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:
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:
print(" Offending char: 0x{:X}".format(offending_char))
logger.warning(" Offending char: %X", offending_char)
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")