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")
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:
force_panose_monospaced(self.sourceFont)
@ -781,9 +792,9 @@ class font_patcher:
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
if box_glyphs_target > box_glyphs_current:
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
if not self.args.quiet and box_glyphs_current > 0:
print("INFO: {}/{} box drawing glyphs will be replaced".format(
box_glyphs_current, box_glyphs_target))
if box_glyphs_current > 0:
logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)
box_enabled = True
else:
# 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.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.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.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.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.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.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': 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': 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': 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': 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.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
]
@ -1102,11 +1113,22 @@ class font_patcher:
our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb:
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:
# 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))
our_btb = win_btb
metrics = Metric.WIN
# 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:
# We trust the WIN metric more, see experiments in #1056
logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb)
our_btb = win_btb
metrics = Metric.WIN
# print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname))
@ -1129,6 +1151,7 @@ class font_patcher:
if self.font_dim['height'] == 0:
# This can only happen if the input font is empty
# Assume we are using our prepared templates
self.symbolsonly = True
self.font_dim = {
'xmin' : 0,
'ymin' : -self.sourceFont.descent,
@ -1139,7 +1162,8 @@ class font_patcher:
}
our_btb = self.sourceFont.descent + self.sourceFont.ascent
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
self.sourceFont.os2_typolinegap = 0
@ -1153,12 +1177,13 @@ class font_patcher:
self.sourceFont.os2_use_typo_metrics = 1
(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:
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
# Find the biggest char width and advance width
# 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
for glyph in range(0x21, 0x17f):
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:
self.font_dim['width'] = self.sourceFont[glyph].width
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(
report_advance_widths(self.sourceFont)))
logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
report_advance_widths(self.sourceFont))
warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax
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
# 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 not self.args.quiet:
print("Warning: Font has negative right side bearing in extended glyphs")
logger.debug("Font has negative right side bearing in extended glyphs")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim)
@ -1288,7 +1312,7 @@ class font_patcher:
if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
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
currentSourceFontGlyph = min(possible_codes)
else:
@ -1311,9 +1335,8 @@ class font_patcher:
# check if a glyph already exists in this location
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
if currentSourceFontGlyph in self.sourceFont:
if not self.args.quiet:
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph))
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
# We don't want to touch anything so move to next Glyph
continue
else:
@ -1461,8 +1484,8 @@ class font_patcher:
if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
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(
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap))
logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
# end for
@ -1603,7 +1626,7 @@ def half_gap(gap, top):
gap_top = int(gap / 2)
gap_bottom = gap - gap_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_bottom
@ -1728,8 +1751,8 @@ def check_fontforge_min_version():
# versions tested: 20150612, 20150824
if actualVersion < minimumVersion:
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion))
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion))
logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
logger.critical("Please use at least version: %d", minimumVersion)
sys.exit(1)
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('-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('-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('--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')
@ -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('-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('--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('--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
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')
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)
# --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts
# Possible values with examples:
@ -1819,8 +1854,9 @@ def setup_arguments():
args = parser.parse_args()
if args.makegroups and not FontnameParserOK:
sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName))
if args.makegroups > 0 and not FontnameParserOK:
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 args.complete:
@ -1853,24 +1889,23 @@ def setup_arguments():
font_complete = False
args.complete = font_complete
if args.alsowindows:
args.windows = False
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
make_sure_path_exists(args.outputdir)
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):
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
try:
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)
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:
args.is_variable = False
finally:
@ -1885,16 +1920,20 @@ def setup_arguments():
args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE):
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:
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 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:
sys.exit(projectName + ": --xavgcharwidth takes only numbers up to 16384")
logging.critical("--xavgcharwidth takes only numbers up to 16384")
sys.exit(2)
return args
@ -1902,24 +1941,43 @@ def setup_arguments():
def main():
global version
git_version = check_version_with_git(version)
print("{} Patcher v{} ({}) (ff {}) executing".format(
projectName, git_version if git_version else version, script_version, fontforge.version()))
allversions = "Patcher v{} ({}) (ff {})".format(
git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
if git_version:
version = git_version
check_fontforge_min_version()
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)
sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts):
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:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception:
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format(
projectName, subfont))
logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
subfont)
sys.exit(1)
patcher.patch(sourceFonts[-1])
@ -1928,13 +1986,6 @@ def main():
patcher.setup_font_names(f)
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:
f.close()

View file

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

View file

@ -49,7 +49,7 @@ exhaustive, and do not form part of our licenses.
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
@ -392,4 +392,4 @@ understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
Creative Commons may be contacted at creativecommons.org.

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

@ -1,21 +1,21 @@
Copyright 2013 Kim Silkebækken and other contributors
https://github.com/powerline/powerline
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
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
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
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

@ -82,10 +82,10 @@ Consult the copyright statement(s) in the license for ways to contact the origin
1.19 What do you mean in condition 4 of the OFL's permissions and conditions? Can you provide examples of abusive promotion / endorsement / advertisement vs. normal acknowledgement?
The intent is that the goodwill and reputation of the author(s) should not be used in a way that makes it sound like the original author(s) endorse or approve of a specific Modified Version or software bundle. For example, it would not be right to advertise a word processor by naming the author(s) in a listing of software features, or to promote a Modified Version on a web site by saying "designed by ...". However, it would be appropriate to acknowledge the author(s) if your software package has a list of people who deserve thanks. We realize that this can seem to be a grey area, but the standard used to judge an acknowledgement is that if the acknowledgement benefits the author(s) it is allowed, but if it primarily benefits other parties, or could reflect poorly on the author(s), then it is not.
1.20 I'm writing a small app for mobile platforms, do I need to include the whole package?
1.20 I'm writing a small app for mobile platforms, do I need to include the whole package?
If you bundle a font under the OFL with your mobile app you must comply with the terms of the license. At a minimum you must include the copyright statement, the license notice and the license text. A mention of this information in your About box or Changelog, with a link to where the font package is from, is good practice, and the extra space needed to carry these items is very small. You do not, however, need to include the full contents of the font package - only the fonts you use and the copyright and license that apply to them. For example, if you only use the regular weight in your app, you do not need to include the italic and bold versions.
1.21 What about including OFL fonts by default in my firmware or dedicated operating system?
1.21 What about including OFL fonts by default in my firmware or dedicated operating system?
Many such systems are restricted and turned into appliances so that users cannot study or modify them. Using open fonts to increase quality and language coverage is a great idea, but you need to be aware that if there is a way for users to extract fonts you cannot legally prevent them from doing that. The fonts themselves, including any changes you make to them, must be distributed under the OFL even if your firmware has a more restrictive license. If you do transform the fonts and change their formats when you include them in your firmware you must respect any names reserved by the font authors via the RFN mechanism and pick your own font name. Alternatively if you directly add a font under the OFL to the font folder of your firmware without modifying or optimizing it you are simply bundling the font like with any other software collection, and do not need to make any further changes.
1.22 Can I make and publish CMS themes or templates that use OFL fonts? Can I include the fonts themselves in the themes or templates? Can I sell the whole package?
@ -103,7 +103,7 @@ No. The terms of use of such services cannot replace or restrict the terms of th
NOTE: This section often refers to a separate paper on 'Web Fonts & RFNs'. This is available at http://scripts.sil.org/OFL_web_fonts_and_RFNs
2.1 Can I make webpages using these fonts?
Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Your three best options are:
Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Your three best options are:
- referring directly in your stylesheet to open fonts which may be available on the user's system
- providing links to download the full package of the font - either from your own website or from elsewhere - so users can install it themselves
- using @font-face to distribute the font directly to browsers. This is recommended and explicitly allowed by the licensing model because it is distribution. The font file itself is distributed with other components of the webpage. It is not embedded in the webpage but referenced through a web address which will cause the browser to retrieve and use the corresponding font to render the webpage (see 1.11 and 1.15 for details related to embedding fonts into documents). As you take advantage of the @font-face cross-platform standard, be aware that web fonts are often tuned for a web environment and not intended for installation and use outside a browser. The reasons in favour of using web fonts are to allow design of dynamic text elements instead of static graphics, to make it easier for content to be localized and translated, indexed and searched, and all this with cross-platform open standards without depending on restricted extensions or plugins. You should check the CSS cascade (the order in which fonts are being called or delivered to your users) when testing.
@ -119,12 +119,12 @@ If the original font data or metadata is changed, or the WOFF-specific metadata
Please note that most WOFF conversion tools and online services do not meet the two requirements listed above, and so their output must be considered a Modified Version. So be very careful and check to be sure that the tool or service you're using is compressing unchanged data and completely and accurately reflecting the original font metadata.
2.3 What about other web font formats such as EOT/EOTLite/CWT/etc.?
In most cases these formats alter the original font data more than WOFF, and do not completely support appropriate metadata, so their use must be considered modification and RFNs may not be used. However, there may be certain formats or usage scenarios that may allow the use of RFNs. See http://scripts.sil.org/OFL_web_fonts_and_RFNs
In most cases these formats alter the original font data more than WOFF, and do not completely support appropriate metadata, so their use must be considered modification and RFNs may not be used. However, there may be certain formats or usage scenarios that may allow the use of RFNs. See http://scripts.sil.org/OFL_web_fonts_and_RFNs
2.4 Can I make OFL fonts available through web font online services?
Yes, you are welcome to include OFL fonts in online web font services as long as you properly meet all the conditions of the license. The origin and open status of the font should be clear among the other fonts you are hosting. Authorship, copyright notices and license information must be sufficiently visible to your users or subscribers so they know where the font comes from and the rights granted by the author(s). Make sure the font file contains the needed copyright notice(s) and licensing information in its metadata. Please double-check the accuracy of every field to prevent contradictory information. Other font formats, including EOT/EOTLite/CWT and superior alternatives like WOFF, already provide fields for this information. Remember that if you modify the font within your library or convert it to another format for any reason the OFL restrictions apply and you need to change the names accordingly. Please respect the author's wishes as expressed in the OFL and do not misrepresent original designers and their work. Don't lump quality open fonts together with dubious freeware or public domain fonts. Consider how you can best work with the original designers and foundries, support their efforts and generate goodwill that will benefit your service. (See 1.17 for details related to URL-based access restrictions methods or DRM mechanisms).
2.5 Some web font formats and services provide ways of "optimizing" the font for a particular website or web application; is that allowed?
2.5 Some web font formats and services provide ways of "optimizing" the font for a particular website or web application; is that allowed?
Yes, it is permitted, but remember that these optimized versions are Modified Versions and so must follow OFL requirements like appropriate renaming. Also you need to bear in mind the other important parameters beyond compression, speed and responsiveness: you need to consider the audience of your particular website or web application, as choosing some optimization parameters may turn out to be less than ideal for them. Subsetting by removing certain glyphs or features may seriously limit functionality of the font in various languages that your users expect. It may also introduce degradation of quality in the rendering or specific bugs on the various target platforms compared to the original font from upstream. In other words, remember that one person's optimized font may be another person's missing feature. Various advanced typographic features (OpenType, Graphite or AAT) are also available through CSS and may provide the desired effects without the need to modify the font.
2.6 Is subsetting a web font considered modification?
@ -136,7 +136,7 @@ Yes. If a web font is optimized only in ways that preserve Functional Equivalenc
2.8 How do you know if an optimization to a web font preserves Functional Equivalence?
Functional Equivalence is described in full in the 'Web fonts and RFNs' paper at http://scripts.sil.org/OFL_web_fonts_and_RFNs, in general, an optimized font is deemed to be Functionally Equivalent (FE) to the Original Version if it:
- Supports the same full character inventory. If a character can be properly displayed using the Original Version, then that same character, encoded correctly on a web page, will display properly.
- Supports the same full character inventory. If a character can be properly displayed using the Original Version, then that same character, encoded correctly on a web page, will display properly.
- Provides the same smart font behavior. Any dynamic shaping behavior that works with the Original Version should work when optimized, unless the browser or environment does not support it. There does not need to be guaranteed support in the client, but there should be no forced degradation of smart font or shaping behavior, such as the removal or obfuscation of OpenType, Graphite or AAT tables.
- Presents text with no obvious degradation in visual quality. The lettershapes should be equally (or more) readable, within limits of the rendering platform.
- Preserves original author, project and license metadata. At a minimum, this should include: Copyright and authorship; The license as stated in the Original Version, whether that is the full text of the OFL or a link to the web version; Any RFN declarations; Information already present in the font or documentation that points back to the Original Version, such as a link to the project or the author's website.
@ -178,7 +178,7 @@ No, but please consider sharing your improvements with others. You may find that
3.7 If a trademark is claimed in the OFL font, does that trademark need to remain in modified fonts?
Yes. Any trademark notices must remain in any derivative fonts to respect trademark laws, but you may add any additional trademarks you claim, officially registered or not. For example if an OFL font called "Foo" contains a notice that "Foo is a trademark of Acme", then if you rename the font to "Bar" when creating a Modified Version, the new trademark notice could say "Foo is a trademark of Acme Inc. - Bar is a trademark of Roadrunner Technologies Ltd.". Trademarks work alongside the OFL and are not subject to the terms of the licensing agreement. The OFL does not grant any rights under trademark law. Bear in mind that trademark law varies from country to country and that there are no international trademark conventions as there are for copyright. You may need to significantly invest in registering and defending a trademark for it to remain valid in the countries you are interested in. This may be costly for an individual independent designer.
3.8 If I commit changes to a font (or publish a branch in a DVCS) as part of a public open source software project, do I have to change the internal font names?
3.8 If I commit changes to a font (or publish a branch in a DVCS) as part of a public open source software project, do I have to change the internal font names?
Only if there are declared RFNs. Making a public commit or publishing a public branch is effectively redistributing your modifications, so any change to the font will require that you do not use the RFNs. Even if there are no RFNs, it may be useful to change the name or add a suffix indicating that a particular version of the font is still in development and not released yet. This will clearly indicate to users and fellow designers that this particular font is not ready for release yet. See section 5 for more details.
@ -198,13 +198,13 @@ If you want to release your fonts under the OFL, we recommend you do the followi
4.2.4 Include the relevant practical documentation on the license by adding the current OFL-FAQ.txt file in your package.
4.2.5 If you wish you can use the OFL graphics (http://scripts.sil.org/OFL_logo) on your website.
4.2.5 If you wish you can use the OFL graphics (http://scripts.sil.org/OFL_logo) on your website.
4.3 Will you make my font OFL for me?
We won't do the work for you. We can, however, try to answer your questions, unfortunately we do not have the resources to review and check your font packages for correct use of the OFL. We recommend you turn to designers, foundries or consulting companies with experience in doing open font design to provide this service to you.
We won't do the work for you. We can, however, try to answer your questions, unfortunately we do not have the resources to review and check your font packages for correct use of the OFL. We recommend you turn to designers, foundries or consulting companies with experience in doing open font design to provide this service to you.
4.4 Will you distribute my OFL font for me?
No, although if the font is of sufficient quality and general interest we may include a link to it on our partial list of OFL fonts on the OFL web site. You may wish to consider other open font catalogs or hosting services, such as the Unifont Font Guide (http://unifont.org/fontguide), The League of Movable Type (http://theleagueofmovabletype.com) or the Open Font Library (http://openfontlibrary.org/), which despite the name has no direct relationship to the OFL or SIL. We do not endorse any particular catalog or hosting service - it is your responsibility to determine if the service is right for you and if it treats authors with fairness.
No, although if the font is of sufficient quality and general interest we may include a link to it on our partial list of OFL fonts on the OFL web site. You may wish to consider other open font catalogs or hosting services, such as the Unifont Font Guide (http://unifont.org/fontguide), The League of Movable Type (http://theleagueofmovabletype.com) or the Open Font Library (http://openfontlibrary.org/), which despite the name has no direct relationship to the OFL or SIL. We do not endorse any particular catalog or hosting service - it is your responsibility to determine if the service is right for you and if it treats authors with fairness.
4.5 Why should I use the OFL for my fonts?
- to meet needs for fonts that can be modified to support lesser-known languages
@ -216,15 +216,15 @@ No, although if the font is of sufficient quality and general interest we may in
- to allow your font to be included in Libre Software operating systems like Ubuntu
- to give your font world status and wide, unrestricted distribution
- to educate students about quality typeface and font design
- to expand your test base and get more useful feedback
- to expand your test base and get more useful feedback
- to extend your reach to new markets when users see your metadata and go to your website
- to get your font more easily into one of the web font online services
- to attract attention for your commercial fonts
- to make money through web font services
- to make money by bundling fonts with applications
- to make money adjusting and extending existing open fonts
- to get a better chance that foundations/NGOs/charities/companies who commission fonts will pick you
- to be part of a sharing design and development community
- to get a better chance that foundations/NGOs/charities/companies who commission fonts will pick you
- to be part of a sharing design and development community
- to give back and contribute to a growing body of font sources
@ -237,7 +237,7 @@ These are font names, or portions of font names, that the author has chosen to r
The best way to acknowledge the source of the design is to thank the original authors and any other contributors in the files that are distributed with your revised font (although no acknowledgement is required). The FONTLOG is a natural place to do this. Reserved Font Names ensure that the only fonts that have the original names are the unmodified Original Versions. This allows designers to maintain artistic integrity while allowing collaboration to happen. It eliminates potential confusion and name conflicts. When choosing a name, be creative and avoid names that reuse almost all the same letters in the same order or sound like the original. It will help everyone if Original Versions and Modified Versions can easily be distinguished from one another and from other derivatives. Any substitution and matching mechanism is outside the scope of the license.
5.3 What do you mean by "primary name as presented to the user"? Are you referring to the font menu name?
Yes, this applies to the font menu name and other mechanisms that specify a font in a document. It would be fine, however, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement). Users who install derivatives (Modified Versions) on their systems should not see any of the original Reserved Font Names in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake one font for another and so expect features only another derivative or the Original Version can actually offer.
Yes, this applies to the font menu name and other mechanisms that specify a font in a document. It would be fine, however, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement). Users who install derivatives (Modified Versions) on their systems should not see any of the original Reserved Font Names in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake one font for another and so expect features only another derivative or the Original Version can actually offer.
5.4 Am I not allowed to use any part of the Reserved Font Names?
You may not use individual words from the Reserved Font Names, but you would be allowed to use parts of words, as long as you do not use any word from the Reserved Font Names entirely. We do not recommend using parts of words because of potential confusion, but it is allowed. For example, if "Foobar" was a Reserved Font Name, you would be allowed to use "Foo" or "bar", although we would not recommend it. Such an unfortunate choice would confuse the users of your fonts as well as make it harder for other designers to contribute.
@ -330,12 +330,12 @@ SIL gives permission to publish unofficial translations into other languages pro
"This is an unofficial translation of the SIL Open Font License into <language_name>. It was not published by SIL International, and does not legally state the distribution terms for fonts that use the OFL. A release under the OFL is only valid when using the original English text. However, we recognize that this unofficial translation will help users and designers not familiar with English to better understand and use the OFL. We encourage designers who consider releasing their creation under the OFL to read the OFL-FAQ in their own language if it is available. Please go to http://scripts.sil.org/OFL for the official version of the license and the accompanying OFL-FAQ."
- Keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion.
- Keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion.
If you start such a unofficial translation effort of the OFL and OFL-FAQ please let us know.
8.7 Does the OFL have an explicit expiration term?
No, the implicit intent of the OFL is that the permissions granted are perpetual and irrevocable.
No, the implicit intent of the OFL is that the permissions granted are perpetual and irrevocable.
9 ABOUT SIL INTERNATIONAL
@ -393,7 +393,7 @@ ChangeLog
- Corrected minor typos in the documentation
- Fixed position of combining inverted breve below (U+032F)
- Added OpenType/Graphite smart code for Armenian
- Added Armenian glyphs (U+0531 -> U+0587)
- Added Armenian glyphs (U+0531 -> U+0587)
- Released as "NewWorldFontFamily"
1 Jan 2005 (Joe Smith) GlobalFontFamily Version 1.0
@ -433,4 +433,3 @@ Foobar.org is a distributed community of developers...
Company.com is a small business who likes to support community designers...
University.edu is a renowned educational institution with a strong design department...
-----

View file

@ -22,7 +22,7 @@ with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The