Compare commits

...

2 commits

Author SHA1 Message Date
2d64ecab8b update .task.mk 2022-10-13 10:41:38 -05:00
9eb507579a update NF batteries 2022-10-13 10:33:48 -05:00
4 changed files with 523 additions and 362 deletions

243
.task.mk
View file

@ -1,7 +1,7 @@
# }> [github.com/daylinmorgan/task.mk] <{ # # }> [github.com/daylinmorgan/task.mk] <{ #
# Copyright (c) 2022 Daylin Morgan # Copyright (c) 2022 Daylin Morgan
# MIT License # MIT License
# version: v22.9.19-5-g5f593e3-dev # version: v22.9.28-dev
# #
# task.mk should be included at the bottom of your Makefile with `-include .task.mk` # task.mk should be included at the bottom of your Makefile with `-include .task.mk`
# See below for the standard configuration options that should be set prior to including this file. # See below for the standard configuration options that should be set prior to including this file.
@ -12,12 +12,13 @@ ACCENT_STYLE ?= b_yellow
PARAMS_STYLE ?= $(ACCENT_STYLE) PARAMS_STYLE ?= $(ACCENT_STYLE)
GOAL_STYLE ?= $(ACCENT_STYLE) GOAL_STYLE ?= $(ACCENT_STYLE)
MSG_STYLE ?= faint MSG_STYLE ?= faint
DIVIDER_STYLE ?= default
DIVIDER ?= DIVIDER ?=
DIVIDER_STYLE ?= default
HELP_SEP ?= HELP_SEP ?=
WRAP ?= 100
# python f-string literals # python f-string literals
EPILOG ?= EPILOG ?=
USAGE ?={ansi.$(HEADER_STYLE)}usage{ansi.end}:\n make <recipe>\n USAGE ?={ansi.header}usage{ansi.end}:\n make <recipe>\n
INHERIT_SHELL ?= INHERIT_SHELL ?=
# ---- [builtin recipes] ---- # # ---- [builtin recipes] ---- #
ifeq (help,$(firstword $(MAKECMDGOALS))) ifeq (help,$(firstword $(MAKECMDGOALS)))
@ -25,10 +26,8 @@ ifeq (help,$(firstword $(MAKECMDGOALS)))
export HELP_ARGS export HELP_ARGS
endif endif
## h, help | show this help ## h, help | show this help
.PHONY: help h h help:
help h: $(call py,help_py) || { echo "exiting early!"; exit 1; }
$(call py,help_py)
.PHONY: _help
_help: export SHOW_HIDDEN=true _help: export SHOW_HIDDEN=true
_help: help _help: help
ifdef PRINT_VARS ifdef PRINT_VARS
@ -40,20 +39,21 @@ endif
### | args: -ws --hidden ### | args: -ws --hidden
### task.mk builtins: | args: -d --hidden ### task.mk builtins: | args: -d --hidden
## _print-ansi | show all possible ansi color code combinations ## _print-ansi | show all possible ansi color code combinations
.PHONY:
_print-ansi: _print-ansi:
$(call py,print_ansi_py) $(call py,print_ansi_py)
# functions to take f-string literals and pass to python print # functions to take f-string literals and pass to python print
tprint = $(call py,info_py,$(1)) tprint = $(call py,print_py,$(1))
tprint-sh = $(call pysh,info_py,$(1)) tprint-sh = $(call pysh,print_py,$(1))
tconfirm = $(call py,confirm_py,$(1)) tconfirm = $(call py,confirm_py,$(1))
## _update-task.mk | downloads latest development version of task.mk ## _update-task.mk | downloads latest development version of task.mk
_update-task.mk: _update-task.mk:
$(call tprint,{a.b_cyan}Updating task.mk{a.end}) $(call tprint,{a.b_cyan}Updating task.mk{a.end})
curl https://raw.githubusercontent.com/daylinmorgan/task.mk/main/task.mk -o .task.mk curl https://raw.githubusercontent.com/daylinmorgan/task.mk/main/task.mk -o .task.mk
export MAKEFILE_LIST .PHONY: h help _help _print-ansi _update-task.mk
TASK_MAKEFILE_LIST := $(filter-out $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)),$(MAKEFILE_LIST))
export MAKEFILE_LIST MAKE TASK_MAKEFILE_LIST
ifndef INHERIT_SHELL ifndef INHERIT_SHELL
SHELL := /bin/bash SHELL := $(shell which bash)
endif endif
# ---- [python/bash script runner] ---- # # ---- [python/bash script runner] ---- #
define _newline define _newline
@ -84,8 +84,10 @@ import argparse
from collections import namedtuple from collections import namedtuple
import os import os
import re import re
$(ansi_py) import subprocess
$(quit_make_py) import sys
from textwrap import wrap
$(utils_py)
MaxLens = namedtuple("MaxLens", "goal msg") MaxLens = namedtuple("MaxLens", "goal msg")
pattern = re.compile( pattern = re.compile(
r"^## (?P<goal>.*?) \| (?P<msg>.*?)(?:\s?\| args: (?P<msgargs>.*?))?$$|^### (?P<rawmsg>.*?)?(?:\s?\| args: (?P<rawargs>.*?))?$$" r"^## (?P<goal>.*?) \| (?P<msg>.*?)(?:\s?\| args: (?P<msgargs>.*?))?$$|^### (?P<rawmsg>.*?)?(?:\s?\| args: (?P<rawargs>.*?))?$$"
@ -100,9 +102,11 @@ def parseargs(argstring):
parser.add_argument("-gs", "--goal-style", type=str) parser.add_argument("-gs", "--goal-style", type=str)
parser.add_argument("--hidden", action="store_true") parser.add_argument("--hidden", action="store_true")
return parser.parse_args(argstring.split()) return parser.parse_args(argstring.split())
def divider(len):
return ansi.style(f" {cfg.div*len}", "div_style")
def gen_makefile(): def gen_makefile():
makefile = "" makefile = ""
for file in os.getenv("MAKEFILE_LIST").split(): for file in os.getenv("MAKEFILE_LIST", "").split():
with open(file, "r") as f: with open(file, "r") as f:
makefile += f.read() + "\n\n" makefile += f.read() + "\n\n"
return makefile return makefile
@ -132,58 +136,80 @@ def recipe_help_header(goal):
item[0].get("msgargs", ""), item[0].get("msgargs", ""),
) )
else: else:
return f" {ansi.style(goal,'$(GOAL_STYLE)')}:" return f" {ansi.style(goal,'goal')}"
def get_goal_deps(goal="task.mk"):
make = os.getenv("MAKE", "make")
cmd = [make, "-p", "-n", "-i"]
for file in os.getenv("TASK_MAKEFILE_LIST", "").split():
cmd.extend(["-f", file])
database = subprocess.check_output(cmd, universal_newlines=True)
dep_pattern = re.compile(r"^" + goal + ":(.*)?")
for line in database.splitlines():
match = dep_pattern.search(line)
if match and match.groups()[0]:
return wrap(
f"{ansi.style('deps','default')}: {ansi.style(match.groups()[0].strip(),'msg')}",
width=cfg.wrap,
initial_indent=" ",
subsequent_indent=" ",
)
def parse_goal(file, goal): def parse_goal(file, goal):
goals = goal_pattern.findall(file) goals = goal_pattern.findall(file)
matched_goal = [i for i in goals if goal in i.split()] matched_goal = [i for i in goals if goal in i.split()]
output = [] output = []
if matched_goal: if matched_goal:
output.append(recipe_help_header(matched_goal[0])) output.append(recipe_help_header(matched_goal[0]))
deps = get_goal_deps(matched_goal[0])
if deps:
output.extend(deps)
lines = file.splitlines() lines = file.splitlines()
loc = [n for n, l in enumerate(lines) if l.startswith(f"{matched_goal[0]}:")][0] loc = [n for n, l in enumerate(lines) if l.startswith(f"{matched_goal[0]}:")][0]
recipe = [] recipe = []
for line in lines[loc + 1 :]: for line in lines[loc + 1 :]:
if not line.startswith("\t"): if not line.startswith("\t"):
break break
recipe.append(line) recipe.append(f" {line.strip()}")
output.append(divider(max((len(l) for l in recipe)) + 5)) output.append(divider(max((len(l.strip()) for l in recipe))))
output.append("\n".join(recipe) + "\n") output.append("\n".join(recipe))
else: else:
output.append(f"{ansi.b_red}ERROR{ansi.end} Failed to find goal: {goal}") deps = get_goal_deps(goal)
if deps:
output.append(recipe_help_header(goal))
output.extend(deps)
if not output:
output.append(f"{ansi.style('ERROR','b_red')}: Failed to find goal: {goal}")
return output return output
def fmt_goal(goal, msg, max_goal_len, argstr): def fmt_goal(goal, msg, max_goal_len, argstr):
args = parseargs(argstr) args = parseargs(argstr)
goal_style = args.goal_style.strip() if args.goal_style else "$(GOAL_STYLE)" goal_style = args.goal_style.strip() if args.goal_style else "goal"
msg_style = args.msg_style.strip() if args.msg_style else "$(MSG_STYLE)" msg_style = args.msg_style.strip() if args.msg_style else "msg"
return ( return (
ansi.style(f" {goal:>{max_goal_len}}", goal_style) ansi.style(f" {goal:>{max_goal_len}}", goal_style)
+ f" $(HELP_SEP) " + f" $(HELP_SEP) "
+ ansi.style(msg, msg_style) + ansi.style(msg, msg_style)
) )
def divider(len):
return ansi.style(f" {'$(DIVIDER)'*len}", "$(DIVIDER_STYLE)")
def fmt_rawmsg(msg, argstr, maxlens): def fmt_rawmsg(msg, argstr, maxlens):
args = parseargs(argstr) args = parseargs(argstr)
lines = [] lines = []
msg_style = args.msg_style.strip() if args.msg_style else "$(MSG_STYLE)" msg_style = args.msg_style.strip() if args.msg_style else "msg"
if not os.getenv("SHOW_HIDDEN") and args.hidden: if not os.getenv("SHOW_HIDDEN") and args.hidden:
return [] return []
if msg: if msg:
if args.align == "sep": if args.align == "sep":
lines.append( lines.append(
f"{' '*(maxlens.goal+len('$(HELP_SEP)')+4)}{ansi.style(msg,msg_style)}" f"{' '*(maxlens.goal+len(cfg.sep)+4)}{ansi.style(msg,msg_style)}"
) )
elif args.align == "center": elif args.align == "center":
lines.append(f" {ansi.style(msg.center(sum(maxlens)),msg_style)}") lines.append(f" {ansi.style(msg.center(sum(maxlens)),msg_style)}")
else: else:
lines.append(f" {ansi.style(msg,msg_style)}") lines.append(f" {ansi.style(msg,msg_style)}")
if args.divider: if args.divider:
lines.append(divider(len("$(HELP_SEP)") + sum(maxlens) + 2)) lines.append(divider(len(cfg.sep) + sum(maxlens) + 2))
if args.whitespace: if args.whitespace:
lines.append("\n") lines.append("\n")
return lines return lines
def print_help(): def print_help():
lines = [f"""$(USAGE)"""] lines = [cfg.usage]
items = list(parse_help(gen_makefile())) items = list(parse_help(gen_makefile()))
maxlens = MaxLens( maxlens = MaxLens(
*(max((len(item[x]) for item in items if x in item)) for x in ["goal", "msg"]) *(max((len(item[x]) for item in items if x in item)) for x in ["goal", "msg"])
@ -197,25 +223,73 @@ def print_help():
) )
if "rawmsg" in item: if "rawmsg" in item:
lines.extend(fmt_rawmsg(item["rawmsg"], item.get("rawargs", ""), maxlens)) lines.extend(fmt_rawmsg(item["rawmsg"], item.get("rawargs", ""), maxlens))
lines.append(f"""$(EPILOG)""") lines.append(cfg.epilog)
print("\n".join(lines)) print("\n".join(lines))
def print_arg_help(help_args): def print_arg_help(help_args):
print(f"{ansi.style('task.mk recipe help','header')}\n")
for arg in help_args.split(): for arg in help_args.split():
print(f"{ansi.style('task.mk recipe help','$(HEADER_STYLE)')}\n")
print("\n".join(parse_goal(gen_makefile(), arg))) print("\n".join(parse_goal(gen_makefile(), arg)))
print()
def main(): def main():
help_args = os.getenv("HELP_ARGS") help_args = os.getenv("HELP_ARGS")
if help_args: if help_args:
quit_make()
print_arg_help(help_args) print_arg_help(help_args)
sys.exit(1)
else: else:
print_help() print_help()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
endef endef
define ansi_py define print_py
$(utils_py)
sys.stderr.write(f"""$(2)\n""")
endef
define print_ansi_py
$(utils_py)
sep = f"$(HELP_SEP)"
codes_names = {getattr(ansi, attr): attr for attr in ansi.__dict__}
for code in sorted(codes_names.keys(), key=lambda item: (len(item), item)):
print(f"{codes_names[code]:>20} {sep} {code+'*****'+ansi.end} {sep} {repr(code)}")
endef
define vars_py
import os
$(utils_py)
vars = "$2".split()
length = max((len(v) for v in vars))
print(f"{ansi.header}vars{ansi.end}:\n")
for v in vars:
print(f" {ansi.params}{v:<{length}}{ansi.end} = {os.getenv(v)}")
print()
endef
define confirm_py
import sys
$(utils_py)
def confirm():
"""
Ask user to enter Y or N (case-insensitive).
:return: True if the answer is Y.
:rtype: bool
"""
answer = ""
while answer not in ["y", "n"]:
answer = input(f"""$(2) {a.b_red}[Y/n]{a.end} """).lower()
return answer == "y"
if confirm():
sys.exit()
else:
sys.exit(1)
endef
define utils_py
import os import os
import sys import sys
from dataclasses import dataclass
@dataclass
class Config:
div: str
sep: str
epilog: str
usage: str
wrap: int
color2byte = dict( color2byte = dict(
black=0, black=0,
red=1, red=1,
@ -247,6 +321,7 @@ class Ansi:
) )
for name, byte in state2byte.items(): for name, byte in state2byte.items():
self.setcode(name, f"\033[{byte}m") self.setcode(name, f"\033[{byte}m")
self.add_cfg()
def setcode(self, name, escape_code): def setcode(self, name, escape_code):
"""create attr for style and escape code""" """create attr for style and escape code"""
if not sys.stdout.isatty() or os.getenv("NO_COLOR", False): if not sys.stdout.isatty() or os.getenv("NO_COLOR", False):
@ -256,27 +331,41 @@ class Ansi:
def custom(self, fg=None, bg=None): def custom(self, fg=None, bg=None):
"""use custom color""" """use custom color"""
code, end = "\033[", "m" code, end = "\033[", "m"
if fg: if not sys.stdout.isatty() or os.getenv("NO_COLOR", False):
if isinstance(fg, int): return ""
code += f"38;5;{fg}" else:
elif (isinstance(fg, list) or isinstance(fg, tuple)) and len(fg) == 1: if fg:
code += f"38;5;{fg[0]}" if isinstance(fg, int):
elif (isinstance(fg, list) or isinstance(fg, tuple)) and len(fg) == 3: code += f"38;5;{fg}"
code += f"38;2;{';'.join((str(i) for i in fg))}" elif (isinstance(fg, list) or isinstance(fg, tuple)) and len(fg) == 1:
else: code += f"38;5;{fg[0]}"
print("Expected one or three values for fg as a list") elif (isinstance(fg, list) or isinstance(fg, tuple)) and len(fg) == 3:
sys.exit(1) code += f"38;2;{';'.join((str(i) for i in fg))}"
if bg: else:
if isinstance(bg, int): print("Expected one or three values for fg as a list")
code += f"{';' if fg else ''}48;5;{bg}" sys.exit(1)
elif (isinstance(bg, list) or isinstance(bg, tuple)) and len(bg) == 1: if bg:
code += f"{';' if fg else ''}48;5;{bg[0]}" if isinstance(bg, int):
elif (isinstance(bg, list) or isinstance(bg, tuple)) and len(bg) == 3: code += f"{';' if fg else ''}48;5;{bg}"
code += f"{';' if fg else ''}48;2;{';'.join((str(i) for i in bg))}" elif (isinstance(bg, list) or isinstance(bg, tuple)) and len(bg) == 1:
else: code += f"{';' if fg else ''}48;5;{bg[0]}"
print("Expected one or three values for bg as a list") elif (isinstance(bg, list) or isinstance(bg, tuple)) and len(bg) == 3:
sys.exit(1) code += f"{';' if fg else ''}48;2;{';'.join((str(i) for i in bg))}"
return code + end else:
print("Expected one or three values for bg as a list")
sys.exit(1)
return code + end
def add_cfg(self):
cfg_styles = {
"header": "$(HEADER_STYLE)",
"accent": "$(ACCENT_STYLE)",
"params": "$(PARAMS_STYLE)",
"goal": "$(GOAL_STYLE)",
"msg": "$(MSG_STYLE)",
"div_style": "$(DIVIDER_STYLE)",
}
for name, style in cfg_styles.items():
self.setcode(name, getattr(self, style))
def style(self, text, style): def style(self, text, style):
if style not in self.__dict__: if style not in self.__dict__:
print(f"unknown style: {style}") print(f"unknown style: {style}")
@ -284,49 +373,7 @@ class Ansi:
else: else:
return f"{self.__dict__[style]}{text}{self.__dict__['end']}" return f"{self.__dict__[style]}{text}{self.__dict__['end']}"
a = ansi = Ansi() a = ansi = Ansi()
endef cfg = Config(
define info_py "$(DIVIDER)", "$(HELP_SEP)", f"""$(EPILOG)""", f"""$(USAGE)""", int("$(WRAP)")
$(ansi_py) )
print(f"""$(2)""")
endef
define print_ansi_py
$(ansi_py)
sep = f"$(HELP_SEP)"
codes_names = {getattr(ansi, attr): attr for attr in ansi.__dict__}
for code in sorted(codes_names.keys(), key=lambda item: (len(item), item)):
print(f"{codes_names[code]:>20} {sep} {code+'*****'+ansi.end} {sep} {repr(code)}")
endef
define vars_py
import os
$(ansi_py)
vars = "$2".split()
length = max((len(v) for v in vars))
print(f"{ansi.$(HEADER_STYLE)}vars:{ansi.end}\n")
for v in vars:
print(f" {ansi.b_magenta}{v:<{length}}{ansi.end} = {os.getenv(v)}")
print()
endef
define confirm_py
import sys
$(ansi_py)
$(quit_make_py)
def confirm():
"""
Ask user to enter Y or N (case-insensitive).
:return: True if the answer is Y.
:rtype: bool
"""
answer = ""
while answer not in ["y", "n"]:
answer = input(f"""$(2) {a.b_red}[Y/n]{a.end} """).lower()
return answer == "y"
if confirm():
sys.exit()
else:
quit_make()
endef
define quit_make_py
import os, signal
def quit_make():
os.kill(os.getppid(), signal.SIGQUIT)
endef endef

View file

@ -26,7 +26,7 @@ update-fonts:
## check | check fc-list for MonoLisa ## check | check fc-list for MonoLisa
.PHONY: check .PHONY: check
check: check:
$(call msg, Checking System for Fonts) $(call msg,Checking System for Fonts)
@fc-list | grep "MonoLisa" @fc-list | grep "MonoLisa"
## update-src | update nerd fonts source ## update-src | update nerd fonts source
@ -45,5 +45,5 @@ lint:
clean: clean:
@rm -r patched/* @rm -r patched/*
USAGE = {a.b_green}Update MonoLisa with Nerd Fonts! {a.end}\n\n{a.$(HEADER_STYLE)}usage{a.end}:\n make <recipe>\n USAGE = {a.b_green}Update MonoLisa with Nerd Fonts! {a.end}\n\n{a.header}usage{a.end}:\n make <recipe>\n
-include .task.mk -include .task.mk

View file

@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf8 # coding=utf8
# Nerd Fonts Version: 2.2.2 # Nerd Fonts Version: 2.3.0-RC
# Script version is further down # Script version is further down
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
# Change the script version when you edit this script: # Change the script version when you edit this script:
script_version = "3.0.6" script_version = "3.1.1"
version = "2.2.2" version = "2.3.0-RC"
projectName = "Nerd Fonts" projectName = "Nerd Fonts"
projectNameAbbreviation = "NF" projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1] projectNameSingular = projectName[:-1]
@ -82,15 +82,32 @@ class TableHEADWriter:
i += 4 i += 4
extra = 0 extra = 0
for j in range(4): for j in range(4):
extra = extra << 8
if i + j <= end: if i + j <= end:
extra += ord(self.f.read(1)) extra += ord(self.f.read(1))
extra = extra << 8
checksum = (checksum + extra) & 0xFFFFFFFF checksum = (checksum + extra) & 0xFFFFFFFF
return checksum return checksum
def find_head_table(self): def find_head_table(self, idx):
""" Search all tables for the HEAD table and store its metadata """ """ Search all tables for the HEAD table and store its metadata """
self.f.seek(4) # Use font with index idx if this is a font collection file
self.f.seek(0, 0)
tag = self.f.read(4)
if tag == b'ttcf':
self.f.seek(2*2, 1)
self.num_fonts = self.getlong()
if (idx >= self.num_fonts):
raise Exception('Trying to access subfont index {} but have only {} fonts'.format(idx, num_fonts))
for _ in range(idx + 1):
offset = self.getlong()
self.f.seek(offset, 0)
elif idx != 0:
raise Exception('Trying to access subfont but file is no collection')
else:
self.f.seek(0, 0)
self.num_fonts = 1
self.f.seek(4, 1)
numtables = self.getshort() numtables = self.getshort()
self.f.seek(3*2, 1) self.f.seek(3*2, 1)
@ -102,7 +119,7 @@ class TableHEADWriter:
self.tab_length = self.getlong() self.tab_length = self.getlong()
if tab_name == b'head': if tab_name == b'head':
return return
raise Exception('No HEAD table found') raise Exception('No HEAD table found in font idx {}'.format(idx))
def goto(self, where): def goto(self, where):
""" Go to a named location in the file or to the specified index """ """ Go to a named location in the file or to the specified index """
@ -146,60 +163,97 @@ class TableHEADWriter:
self.modified = False self.modified = False
self.f = open(filename, 'r+b') self.f = open(filename, 'r+b')
self.find_head_table() self.find_head_table(0)
self.flags = self.getshort('flags') self.flags = self.getshort('flags')
self.lowppem = self.getshort('lowestRecPPEM') self.lowppem = self.getshort('lowestRecPPEM')
self.checksum_adj = self.getlong('checksumAdjustment') self.checksum_adj = self.getlong('checksumAdjustment')
def check_panose_monospaced(font):
""" Check if the font's Panose flags say it is monospaced """
# https://forum.high-logic.com/postedfiles/Panose.pdf
panose = list(font.os2_panose)
if panose[0] < 2 or panose[0] > 5:
return -1 # invalid Panose info
panose_mono = ((panose[0] == 2 and panose[3] == 9) or
(panose[0] == 3 and panose[3] == 3))
return 1 if panose_mono else 0
def is_monospaced(font):
""" Check if a font is probably monospaced """
# Some fonts lie (or have not any Panose flag set), spot check monospaced:
width = -1
width_mono = True
for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', '.'
if not glyph in font:
# A 'strange' font, believe Panose
return check_panose_monospaced(font) == 1
# print(" -> {} {}".format(glyph, font[glyph].width))
if width < 0:
width = font[glyph].width
continue
if font[glyph].width != width:
# Exception for fonts like Code New Roman Regular or Hermit Light/Bold:
# Allow small 'i' and dot to be smaller than normal
# I believe the source fonts are buggy
if glyph in [ 0x69, 0x2E ]:
if width > font[glyph].width:
continue
(xmin, _, xmax, _) = font[glyph].boundingBox()
if width > xmax - xmin:
continue
width_mono = False
break
# We believe our own check more then Panose ;-D
return width_mono
def get_advance_width(font, extended, minimum):
""" Get the maximum/minimum advance width in the extended(?) range """
width = 0
if extended:
end = 0x17f
else:
end = 0x07e
for glyph in range(0x21, end):
if not glyph in font:
continue
if glyph in range(0x7F, 0xBF):
continue # ignore special characters like '1/4' etc
if width == 0:
width = font[glyph].width
continue
if not minimum and width < font[glyph].width:
width = font[glyph].width
elif minimum and width > font[glyph].width:
width = font[glyph].width
return width
class font_patcher: class font_patcher:
def __init__(self): def __init__(self, args):
self.args = None # class 'argparse.Namespace' self.args = args # class 'argparse.Namespace'
self.sym_font_args = [] self.sym_font_args = []
self.config = None # class 'configparser.ConfigParser' self.config = None # class 'configparser.ConfigParser'
self.sourceFont = None # class 'fontforge.font' self.sourceFont = None # class 'fontforge.font'
self.octiconsExactEncodingPosition = True
self.patch_set = None # class 'list' self.patch_set = None # class 'list'
self.font_dim = None # class 'dict' self.font_dim = None # class 'dict'
self.onlybitmaps = 0 self.onlybitmaps = 0
self.extension = ""
self.essential = set() self.essential = set()
self.setup_arguments()
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
if not os.path.isfile(self.args.font):
sys.exit("{}: Font file does not exist: {}".format(projectName, self.args.font)) def patch(self, font):
if not os.access(self.args.font, os.R_OK): self.sourceFont = font
sys.exit("{}: Can not open font file for reading: {}".format(projectName, self.args.font))
if len(fontforge.fontsInFile(self.args.font)) > 1:
sys.exit("{}: Font file contains {} fonts, can only handle single font files".format(projectName,
len(fontforge.fontsInFile(self.args.font))))
try:
self.sourceFont = fontforge.open(self.args.font, 1) # 1 = ("fstypepermitted",))
except Exception:
sys.exit(projectName + ": Can not open font, try to open with fontforge interactively to get more information")
self.setup_version() self.setup_version()
self.get_essential_references() self.get_essential_references()
self.setup_name_backup() self.setup_name_backup(font)
if self.args.single:
self.assert_monospace()
self.remove_ligatures() self.remove_ligatures()
make_sure_path_exists(self.args.outputdir)
self.check_position_conflicts()
self.setup_patch_set() self.setup_patch_set()
self.setup_line_dimensions() self.setup_line_dimensions()
self.get_sourcefont_dimensions() self.get_sourcefont_dimensions()
self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available
self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used
if self.args.extension == "":
self.extension = os.path.splitext(self.args.font)[1]
else:
self.extension = '.' + self.args.extension
if re.match("\.ttc$", self.extension, re.IGNORECASE):
sys.exit(projectName + ": Can not create True Type Collections")
def patch(self):
print("{} Patcher v{} ({}) executing\n".format(projectName, version, script_version))
if self.args.single: if self.args.single:
# Force width to be equal on all glyphs to ensure the font is considered monospaced on Windows. # Force width to be equal on all glyphs to ensure the font is considered monospaced on Windows.
@ -241,14 +295,11 @@ class font_patcher:
symfont.em = self.sourceFont.em symfont.em = self.sourceFont.em
PreviousSymbolFilename = patch['Filename'] PreviousSymbolFilename = patch['Filename']
# If patch table doesn't include a source start and end, re-use the symbol font values # If patch table doesn't include a source start, re-use the symbol font values
SrcStart = patch['SrcStart'] SrcStart = patch['SrcStart']
SrcEnd = patch['SrcEnd']
if not SrcStart: if not SrcStart:
SrcStart = patch['SymStart'] SrcStart = patch['SymStart']
if not SrcEnd: self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleGlyph'], patch['Name'], patch['Attributes'])
SrcEnd = patch['SymEnd']
self.copy_glyphs(SrcStart, SrcEnd, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleGlyph'], patch['Name'], patch['Attributes'])
if symfont: if symfont:
symfont.close() symfont.close()
@ -262,30 +313,49 @@ class font_patcher:
self.sourceFont["grave"].glyphclass="baseglyph" self.sourceFont["grave"].glyphclass="baseglyph"
def generate(self): def generate(self, sourceFonts):
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. sourceFont = sourceFonts[0]
if self.sourceFont.fullname != None: if len(sourceFonts) > 1:
outfile = self.args.outputdir + "/" + self.sourceFont.fullname + self.extension layer = None
self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) # use first non-background layer
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile) for l in sourceFont.layers:
if not sourceFont.layers[l].is_background:
layer = l
break
outfile = os.path.normpath(os.path.join(self.args.outputdir, sourceFont.familyname + ".ttc"))
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'.
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=(str('opentype'), str('PfEd-comments')), layer=layer)
message = "\nGenerated: {} fonts in '{}'".format(len(sourceFonts), outfile)
else: else:
outfile = self.args.outputdir + "/" + self.sourceFont.cidfontname + self.extension fontname = sourceFont.fullname
self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) if not fontname:
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fontname, outfile) fontname = sourceFont.cidfontname
outfile = os.path.normpath(os.path.join(self.args.outputdir, fontname + self.args.extension))
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'.
bitmaps = str()
if len(self.sourceFont.bitmapSizes):
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
bitmaps = str('otf') # otf/ttf, both is bf_ttf
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=(str('opentype'), str('PfEd-comments')))
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile)
# Adjust flags that can not be changed via fontforge # Adjust flags that can not be changed via fontforge
try: try:
source_font = TableHEADWriter(self.args.font) source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile) dest_font = TableHEADWriter(outfile)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: for idx in range(source_font.num_fonts):
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08)) print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int' source_font.find_head_table(idx)
if source_font.lowppem != dest_font.lowppem: dest_font.find_head_table(idx)
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem)) if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM') print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
if dest_font.modified: dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
dest_font.reset_table_checksum() if source_font.lowppem != dest_font.lowppem:
dest_font.reset_full_checksum() print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
dest_font.reset_full_checksum()
except Exception as error: except Exception as error:
print("Can not handle font flags ({})".format(repr(error))) print("Can not handle font flags ({})".format(repr(error)))
finally: finally:
@ -301,130 +371,19 @@ class font_patcher:
print("\nPost Processed: {}".format(outfile)) print("\nPost Processed: {}".format(outfile))
def setup_arguments(self): def setup_name_backup(self, font):
parser = argparse.ArgumentParser(
description=(
'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n'
'* Website: https://www.nerdfonts.com\n'
'* Version: ' + version + '\n'
'* Development Website: https://github.com/ryanoasis/nerd-fonts\n'
'* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/master/changelog.md'),
formatter_class=RawTextHelpFormatter
)
# optional arguments
parser.add_argument('font', help='The path to the font to patch (e.g., Inconsolata.otf)')
parser.add_argument('-v', '--version', action='version', version=projectName + ": %(prog)s (" + version + ")")
parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='store_true', 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')
parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing')
parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)')
parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font. All new glyphs will be copied, with no scaling applied.')
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('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
# 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('--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')
# symbol fonts to include arguments
sym_font_group = parser.add_argument_group('Symbol Fonts')
sym_font_group.add_argument('--fontawesome', dest='fontawesome', default=False, action='store_true', help='Add Font Awesome Glyphs (http://fontawesome.io/)')
sym_font_group.add_argument('--fontawesomeextension', dest='fontawesomeextension', default=False, action='store_true', help='Add Font Awesome Extension Glyphs (https://andrelzgava.github.io/font-awesome-extension/)')
sym_font_group.add_argument('--fontlogos', '--fontlinux', dest='fontlogos', default=False, action='store_true', help='Add Font Logos Glyphs (https://github.com/Lukas-W/font-logos)')
sym_font_group.add_argument('--octicons', dest='octicons', default=False, action='store_true', help='Add Octicons Glyphs (https://octicons.github.com)')
sym_font_group.add_argument('--codicons', dest='codicons', default=False, action='store_true', help='Add Codicons Glyphs (https://github.com/microsoft/vscode-codicons)')
sym_font_group.add_argument('--powersymbols', dest='powersymbols', default=False, action='store_true', help='Add IEC Power Symbols (https://unicodepowersymbol.com/)')
sym_font_group.add_argument('--pomicons', dest='pomicons', default=False, action='store_true', help='Add Pomicon Glyphs (https://github.com/gabrielelana/pomicons)')
sym_font_group.add_argument('--powerline', dest='powerline', default=False, action='store_true', help='Add Powerline Glyphs')
sym_font_group.add_argument('--powerlineextra', dest='powerlineextra', default=False, action='store_true', help='Add Powerline Glyphs (https://github.com/ryanoasis/powerline-extra-symbols)')
sym_font_group.add_argument('--material', '--materialdesignicons', '--mdi', dest='material', default=False, action='store_true', help='Add Material Design Icons (https://github.com/templarian/MaterialDesign)')
sym_font_group.add_argument('--weather', '--weathericons', dest='weather', default=False, action='store_true', help='Add Weather Icons (https://github.com/erikflowers/weather-icons)')
self.args = parser.parse_args()
if self.args.makegroups and not FontnameParserOK:
sys.exit(projectName + ": FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName))
# if you add a new font, set it to True here inside the if condition
if self.args.complete:
self.args.fontawesome = True
self.args.fontawesomeextension = True
self.args.fontlogos = True
self.args.octicons = True
self.args.codicons = True
self.args.powersymbols = True
self.args.pomicons = True
self.args.powerline = True
self.args.powerlineextra = True
self.args.material = True
self.args.weather = True
if not self.args.complete:
# add the list of arguments for each symbol font to the list self.sym_font_args
for action in sym_font_group._group_actions:
self.sym_font_args.append(action.__dict__['option_strings'])
# determine whether or not all symbol fonts are to be used
font_complete = True
for sym_font_arg_aliases in self.sym_font_args:
found = False
for alias in sym_font_arg_aliases:
if alias in sys.argv:
found = True
if found is not True:
font_complete = False
self.args.complete = font_complete
if self.args.alsowindows:
self.args.windows = False
if self.args.nonmono and self.args.single:
print("Warniung: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
self.args.nonmono = False
# this one also works but it needs to be updated every time a font is added
# it was a conditional in self.setup_font_names() before, but it was missing
# a symbol font, so it would name the font complete without being so sometimes.
# that's why i did the above.
#
# if you add a new font, put it in here too, as the others are
# self.args.complete = all([
# self.args.fontawesome is True,
# self.args.fontawesomeextension is True,
# self.args.fontlogos is True,
# self.args.octicons is True,
# self.args.powersymbols is True,
# self.args.pomicons is True,
# self.args.powerline is True,
# self.args.powerlineextra is True,
# self.args.material is True,
# self.args.weather is True
# ])
def setup_name_backup(self):
""" Store the original font names to be able to rename the font multiple times """ """ Store the original font names to be able to rename the font multiple times """
self.original_fontname = self.sourceFont.fontname font.persistent = {
self.original_fullname = self.sourceFont.fullname "fontname": font.fontname,
self.original_familyname = self.sourceFont.familyname "fullname": font.fullname,
"familyname": font.familyname,
}
def setup_font_names(self): def setup_font_names(self, font):
self.sourceFont.fontname = self.original_fontname font.fontname = font.persistent["fontname"]
self.sourceFont.fullname = self.original_fullname font.fullname = font.persistent["fullname"]
self.sourceFont.familyname = self.original_familyname font.familyname = font.persistent["familyname"]
verboseAdditionalFontNameSuffix = " " + projectNameSingular verboseAdditionalFontNameSuffix = " " + projectNameSingular
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later
additionalFontNameSuffix = " " + projectNameAbbreviation additionalFontNameSuffix = " " + projectNameAbbreviation
@ -471,14 +430,14 @@ class font_patcher:
verboseAdditionalFontNameSuffix += " Mono" verboseAdditionalFontNameSuffix += " Mono"
if FontnameParserOK and self.args.makegroups: if FontnameParserOK and self.args.makegroups:
use_fullname = type(self.sourceFont.fullname) == str # Usually the fullname is better to parse use_fullname = type(font.fullname) == str # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname # Use fullname if it is 'equal' to the fontname
if self.sourceFont.fullname: if font.fullname:
use_fullname |= self.sourceFont.fontname.lower() == FontnameTools.postscript_char_filter(self.sourceFont.fullname).lower() use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower()
# Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks) # Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks)
for hit in [ 'Meslo' ]: for hit in [ 'Meslo' ]:
use_fullname |= self.sourceFont.fontname.lower().startswith(hit.lower()) use_fullname |= font.fontname.lower().startswith(hit.lower())
parser_name = self.sourceFont.fullname if use_fullname else self.sourceFont.fontname parser_name = font.fullname if use_fullname else font.fontname
# Gohu fontnames hide the weight, but the file names are ok... # Gohu fontnames hide the weight, but the file names are ok...
if parser_name.startswith('Gohu'): if parser_name.startswith('Gohu'):
parser_name = os.path.splitext(os.path.basename(self.args.font))[0] parser_name = os.path.splitext(os.path.basename(self.args.font))[0]
@ -496,16 +455,16 @@ class font_patcher:
# have an internal style defined (in sfnt_names) # have an internal style defined (in sfnt_names)
# using '([^-]*?)' to get the item before the first dash "-" # using '([^-]*?)' to get the item before the first dash "-"
# using '([^-]*(?!.*-))' to get the item after the last dash "-" # using '([^-]*(?!.*-))' to get the item after the last dash "-"
fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", self.sourceFont.fontname).groups() fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", font.fontname).groups()
# dont trust 'sourceFont.familyname' # dont trust 'font.familyname'
familyname = fontname familyname = fontname
# fullname (filename) can always use long/verbose font name, even in windows # fullname (filename) can always use long/verbose font name, even in windows
if self.sourceFont.fullname != None: if font.fullname != None:
fullname = self.sourceFont.fullname + verboseAdditionalFontNameSuffix fullname = font.fullname + verboseAdditionalFontNameSuffix
else: else:
fullname = self.sourceFont.cidfontname + verboseAdditionalFontNameSuffix fullname = font.cidfontname + verboseAdditionalFontNameSuffix
fontname = fontname + additionalFontNameSuffix.replace(" ", "") fontname = fontname + additionalFontNameSuffix.replace(" ", "")
@ -513,13 +472,13 @@ class font_patcher:
# parse fontname if it fails: # parse fontname if it fails:
try: try:
# search tuple: # search tuple:
subFamilyTupleIndex = [x[1] for x in self.sourceFont.sfnt_names].index("SubFamily") subFamilyTupleIndex = [x[1] for x in font.sfnt_names].index("SubFamily")
# String ID is at the second index in the Tuple lists # String ID is at the second index in the Tuple lists
sfntNamesStringIDIndex = 2 sfntNamesStringIDIndex = 2
# now we have the correct item: # now we have the correct item:
subFamily = self.sourceFont.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex] subFamily = font.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex]
except IndexError: except IndexError:
sys.stderr.write("{}: Could not find 'SubFamily' for given font, falling back to parsed fontname\n".format(projectName)) sys.stderr.write("{}: Could not find 'SubFamily' for given font, falling back to parsed fontname\n".format(projectName))
subFamily = fallbackStyle subFamily = fallbackStyle
@ -629,22 +588,22 @@ class font_patcher:
if not (FontnameParserOK and self.args.makegroups): if not (FontnameParserOK and self.args.makegroups):
# replace any extra whitespace characters: # replace any extra whitespace characters:
self.sourceFont.familyname = " ".join(familyname.split()) font.familyname = " ".join(familyname.split())
self.sourceFont.fullname = " ".join(fullname.split()) font.fullname = " ".join(fullname.split())
self.sourceFont.fontname = " ".join(fontname.split()) font.fontname = " ".join(fontname.split())
self.sourceFont.appendSFNTName(str('English (US)'), str('Preferred Family'), self.sourceFont.familyname) font.appendSFNTName(str('English (US)'), str('Preferred Family'), font.familyname)
self.sourceFont.appendSFNTName(str('English (US)'), str('Family'), self.sourceFont.familyname) font.appendSFNTName(str('English (US)'), str('Family'), font.familyname)
self.sourceFont.appendSFNTName(str('English (US)'), str('Compatible Full'), self.sourceFont.fullname) font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname)
self.sourceFont.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
else: else:
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation
fam_suffix += ' Mono' if self.args.single else '' fam_suffix += ' Mono' if self.args.single else ''
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix) n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
n.rename_font(self.sourceFont) n.rename_font(font)
self.sourceFont.comment = projectInfo font.comment = projectInfo
self.sourceFont.fontlog = projectInfo font.fontlog = projectInfo
def setup_version(self): def setup_version(self):
@ -678,10 +637,19 @@ class font_patcher:
print("No configfile given, skipping configfile related actions") print("No configfile given, skipping configfile related actions")
def check_position_conflicts(self): def assert_monospace(self):
# Prevent glyph encoding position conflicts between glyph sets # Check if the sourcefont is monospaced
if self.args.fontawesome and self.args.octicons: width_mono = is_monospaced(self.sourceFont)
self.octiconsExactEncodingPosition = False 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(" Glyph widths {} / {} - {} and Panose says \"monospace {}\" ({})".format(get_advance_width(self.sourceFont, False, True),
get_advance_width(self.sourceFont, False, False), get_advance_width(self.sourceFont, True, False), panose_mono, list(self.sourceFont.os2_panose)))
if not width_mono:
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
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")
def setup_patch_set(self): def setup_patch_set(self):
@ -805,28 +773,28 @@ class font_patcher:
# Define the character ranges # Define the character ranges
# Symbol font ranges # Symbol font ranges
self.patch_set = [ self.patch_set = [
{'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE534, 'SrcStart': 0xE5FA, 'SrcEnd': 0xE634, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE534, 'SrcStart': 0xE5FA, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'SrcEnd': 0xE7C5, 'ScaleGlyph': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleGlyph': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xF000, 'SymEnd': 0xF2E0, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA}, {'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xF000, 'SymEnd': 0xF2E0, 'SrcStart': None, 'ScaleGlyph': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA},
{'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'SrcEnd': 0xE2A9, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize {'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleGlyph': 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, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleGlyph': 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, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off)
{'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'SrcEnd': 0xFD46, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleGlyph': None, '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, 'SrcEnd': 0xE3EB, 'ScaleGlyph': None, '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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'SrcEnd': None , 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'SrcEnd': 0xF505, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'SrcEnd': 0xF4A9, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop
{'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': 0x0000, 'SrcEnd': 0x0000, 'ScaleGlyph': None, 'Attributes': CUSTOM_ATTR} {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': CUSTOM_ATTR}
] ]
def setup_line_dimensions(self): def setup_line_dimensions(self):
@ -846,11 +814,6 @@ class font_patcher:
self.sourceFont.hhea_ascent = self.sourceFont.os2_winascent self.sourceFont.hhea_ascent = self.sourceFont.os2_winascent
self.sourceFont.hhea_descent = -self.sourceFont.os2_windescent self.sourceFont.hhea_descent = -self.sourceFont.os2_windescent
# Line gap add extra space on the bottom of the line which
# doesn't allow the powerline glyphs to fill the entire line.
self.sourceFont.hhea_linegap = 0
self.sourceFont.os2_typolinegap = 0
def get_essential_references(self): def get_essential_references(self):
"""Find glyphs that are needed for the basic glyphs""" """Find glyphs that are needed for the basic glyphs"""
# Sometimes basic glyphs are constructed from multiple other glyphs. # Sometimes basic glyphs are constructed from multiple other glyphs.
@ -877,6 +840,38 @@ class font_patcher:
self.font_dim['ymin'] = self.sourceFont.os2_typodescent self.font_dim['ymin'] = self.sourceFont.os2_typodescent
self.font_dim['ymax'] = self.sourceFont.os2_typoascent self.font_dim['ymax'] = self.sourceFont.os2_typoascent
# Calculate font height
self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax']
if self.font_dim['height'] == 0:
# This can only happen if the input font is empty
# Assume we are using our prepared templates
self.font_dim = {
'xmin' : 0,
'ymin' : -self.sourceFont.descent,
'xmax' : self.sourceFont.em,
'ymax' : self.sourceFont.ascent,
'width' : self.sourceFont.em,
'height': self.sourceFont.descent + self.sourceFont.ascent,
}
# Line gap add extra space on the bottom of the line which
# doesn't allow the powerline glyphs to fill the entire line.
# Put half of the gap into the 'cell', each top and bottom
gap = max(self.sourceFont.hhea_linegap, self.sourceFont.os2_typolinegap) # TODO probably wrong
if self.sourceFont.os2_use_typo_metrics:
gap = self.sourceFont.os2_typolinegap
self.sourceFont.hhea_linegap = 0
self.sourceFont.os2_typolinegap = 0
if gap > 0:
gap_top = int(gap / 2)
gap_bottom = gap - gap_top
self.font_dim['ymin'] -= gap_bottom
self.font_dim['ymax'] += gap_top
self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax']
self.sourceFont.os2_typoascent = self.sourceFont.os2_typoascent + gap_top
self.sourceFont.os2_typodescent = self.sourceFont.os2_typodescent - gap_bottom
# TODO Check what to do with win and hhea values
# Find the biggest char width # Find the biggest char width
# Ignore the y-values, os2_winXXXXX values set above are used for line height # Ignore the y-values, os2_winXXXXX values set above are used for line height
# #
@ -890,22 +885,10 @@ class font_patcher:
continue continue
if self.font_dim['width'] < self.sourceFont[glyph].width: if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width self.font_dim['width'] = self.sourceFont[glyph].width
# print("New MAXWIDTH-A {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax))
if xmax > self.font_dim['xmax']: if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax self.font_dim['xmax'] = xmax
# print("New MAXWIDTH-B {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax))
# Calculate font height
self.font_dim['height'] = abs(self.font_dim['ymin']) + self.font_dim['ymax']
if self.font_dim['height'] == 0:
# This can only happen if the input font is empty
# Assume we are using our prepared templates
self.font_dim = {
'xmin' : 0,
'ymin' : -self.sourceFont.descent,
'xmax' : self.sourceFont.em,
'ymax' : self.sourceFont.ascent,
'width' : self.sourceFont.em,
'height': abs(self.sourceFont.descent) + self.sourceFont.ascent,
}
def get_scale_factor(self, sym_dim): def get_scale_factor(self, sym_dim):
@ -921,19 +904,16 @@ class font_patcher:
return scale_ratio return scale_ratio
def copy_glyphs(self, sourceFontStart, sourceFontEnd, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleGlyph, setName, attributes): def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleGlyph, setName, attributes):
""" Copies symbol glyphs into self.sourceFont """ """ Copies symbol glyphs into self.sourceFont """
progressText = '' progressText = ''
careful = False careful = False
glyphSetLength = 0 glyphSetLength = 0
sourceFontCounter = 0
if self.args.careful: if self.args.careful:
careful = True careful = True
if exactEncoding is False:
sourceFontList = list(range(sourceFontStart, sourceFontEnd + 1))
sourceFontCounter = 0
# Create glyphs from symbol font # Create glyphs from symbol font
# #
# If we are going to copy all Glyphs, then assume we want to be careful # If we are going to copy all Glyphs, then assume we want to be careful
@ -974,8 +954,8 @@ class font_patcher:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
currentSourceFontGlyph = min(possible_codes) currentSourceFontGlyph = min(possible_codes)
else: else:
# use source font defined hex values based on passed in start and end # use source font defined hex values based on passed in start (fills gaps; symbols are packed)
currentSourceFontGlyph = sourceFontList[sourceFontCounter] currentSourceFontGlyph = sourceFontStart + sourceFontCounter
sourceFontCounter += 1 sourceFontCounter += 1
if self.args.quiet is False: if self.args.quiet is False:
@ -1292,19 +1272,153 @@ def check_fontforge_min_version():
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion)) sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion))
sys.exit(1) sys.exit(1)
def setup_arguments():
parser = argparse.ArgumentParser(
description=(
'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n'
'* Website: https://www.nerdfonts.com\n'
'* Version: ' + version + '\n'
'* Development Website: https://github.com/ryanoasis/nerd-fonts\n'
'* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/master/changelog.md'),
formatter_class=RawTextHelpFormatter
)
# optional arguments
parser.add_argument('font', help='The path to the font to patch (e.g., Inconsolata.otf)')
parser.add_argument('-v', '--version', action='version', version=projectName + ": %(prog)s (" + version + ")")
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')
parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing')
parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)')
parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font. All new glyphs will be copied, with no scaling applied.')
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('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
# 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('--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')
# symbol fonts to include arguments
sym_font_group = parser.add_argument_group('Symbol Fonts')
sym_font_group.add_argument('--fontawesome', dest='fontawesome', default=False, action='store_true', help='Add Font Awesome Glyphs (http://fontawesome.io/)')
sym_font_group.add_argument('--fontawesomeextension', dest='fontawesomeextension', default=False, action='store_true', help='Add Font Awesome Extension Glyphs (https://andrelzgava.github.io/font-awesome-extension/)')
sym_font_group.add_argument('--fontlogos', '--fontlinux', dest='fontlogos', default=False, action='store_true', help='Add Font Logos Glyphs (https://github.com/Lukas-W/font-logos)')
sym_font_group.add_argument('--octicons', dest='octicons', default=False, action='store_true', help='Add Octicons Glyphs (https://octicons.github.com)')
sym_font_group.add_argument('--codicons', dest='codicons', default=False, action='store_true', help='Add Codicons Glyphs (https://github.com/microsoft/vscode-codicons)')
sym_font_group.add_argument('--powersymbols', dest='powersymbols', default=False, action='store_true', help='Add IEC Power Symbols (https://unicodepowersymbol.com/)')
sym_font_group.add_argument('--pomicons', dest='pomicons', default=False, action='store_true', help='Add Pomicon Glyphs (https://github.com/gabrielelana/pomicons)')
sym_font_group.add_argument('--powerline', dest='powerline', default=False, action='store_true', help='Add Powerline Glyphs')
sym_font_group.add_argument('--powerlineextra', dest='powerlineextra', default=False, action='store_true', help='Add Powerline Glyphs (https://github.com/ryanoasis/powerline-extra-symbols)')
sym_font_group.add_argument('--material', '--materialdesignicons', '--mdi', dest='material', default=False, action='store_true', help='Add Material Design Icons (https://github.com/templarian/MaterialDesign)')
sym_font_group.add_argument('--weather', '--weathericons', dest='weather', default=False, action='store_true', help='Add Weather Icons (https://github.com/erikflowers/weather-icons)')
args = parser.parse_args()
if args.makegroups and not FontnameParserOK:
sys.exit(projectName + ": FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName))
# if you add a new font, set it to True here inside the if condition
if args.complete:
args.fontawesome = True
args.fontawesomeextension = True
args.fontlogos = True
args.octicons = True
args.codicons = True
args.powersymbols = True
args.pomicons = True
args.powerline = True
args.powerlineextra = True
args.material = True
args.weather = True
if not args.complete:
sym_font_args = []
# add the list of arguments for each symbol font to the list sym_font_args
for action in sym_font_group._group_actions:
sym_font_args.append(action.__dict__['option_strings'])
# determine whether or not all symbol fonts are to be used
font_complete = True
for sym_font_arg_aliases in sym_font_args:
found = False
for alias in sym_font_arg_aliases:
if alias in sys.argv:
found = True
if found is not True:
font_complete = False
args.complete = font_complete
if args.alsowindows:
args.windows = False
if args.nonmono and args.single:
print("Warniung: 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))
if not os.access(args.font, os.R_OK):
sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font))
is_ttc = len(fontforge.fontsInFile(args.font)) > 1
if args.extension == "":
args.extension = os.path.splitext(args.font)[1]
else:
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")
else:
if is_ttc:
sys.exit(projectName + ": Can not create single font files from True Type Collections")
return args
def main(): def main():
print("{} Patcher v{} ({}) executing".format(projectName, version, script_version))
check_fontforge_min_version() check_fontforge_min_version()
patcher = font_patcher() args = setup_arguments()
patcher.patch() patcher = font_patcher(args)
sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts):
print("\n{}: Processing {} ({}/{})".format(projectName, 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))
patcher.patch(sourceFonts[-1])
print("\nDone with Patch Sets, generating font...\n") print("\nDone with Patch Sets, generating font...\n")
patcher.setup_font_names() for f in sourceFonts:
patcher.generate() patcher.setup_font_names(f)
patcher.generate(sourceFonts)
# This mainly helps to improve CI runtime # This mainly helps to improve CI runtime
if patcher.args.alsowindows: if patcher.args.alsowindows:
patcher.args.windows = True patcher.args.windows = True
patcher.setup_font_names() for f in sourceFonts:
patcher.generate() patcher.setup_font_names(f)
patcher.generate(sourceFonts)
for f in sourceFonts:
f.close()
if __name__ == "__main__": if __name__ == "__main__":

Binary file not shown.