Compare commits

...

6 commits

Author SHA1 Message Date
56a06a32c9
fix(#4): refactor to reduce some boilerplate 2023-03-20 01:40:55 -05:00
cdb6bacf37
build: drop flake8 for ruff and update pre-commit repos 2023-03-20 01:40:55 -05:00
a8175ab675
chore: add licensing info 2023-03-20 01:40:35 -05:00
github-actions
32a93386bd
chore: change batteries 2023-03-20 01:40:35 -05:00
github-actions
04081cc370
chore: change batteries 2023-03-20 01:40:35 -05:00
github-actions
718ba2179f
chore: change batteries 2023-03-20 01:40:34 -05:00
9 changed files with 446 additions and 161 deletions

View file

@ -1,23 +1,23 @@
exclude: "^(src/.*|bin/font-patcher)" exclude: "^(src/.*|bin/font-patcher)"
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0 rev: v4.4.0
hooks: hooks:
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: 5.9.3 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.12.0 rev: 23.1.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 4.0.1 rev: 'v0.0.257'
hooks: hooks:
- id: flake8 - id: ruff
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.2 rev: v0.9.0.2
hooks: hooks:

152
LICENSE Normal file
View file

@ -0,0 +1,152 @@
MIT License
Copyright (c) 2023 Daylin Morgan
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.
-------
See below for licensing information related to ./src/ and ./bin/font-patcher.
# Nerd Fonts Licensing
There are various sources used under various licenses:
* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
* Nerd Fonts original source code files (such as `.sh`, `.py`, `font-patcher` and others) are licensed under the MIT License (MIT) (see below).
* Many other licenses are present in this project for even more detailed breakdown see: [License Audit](https://github.com/ryanoasis/nerd-fonts/blob/-/license-audit.md).
## Source files not in folders containing an explicit license are using the MIT License (MIT)
The MIT License (MIT)
Copyright (c) 2014 Ryan L McIntyre
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.
## Various Fonts, Patched Fonts, SVGs, Glyph Fonts, and any files in a folder with explicit SIL OFL 1.1 License
Copyright (c) 2014, Ryan L McIntyre (https://ryanlmcintyre.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
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,
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
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -1,13 +1,10 @@
-include .env -include .env
ARGS ?= -c ARGS ?= -c
NF_SRC := $(shell ./bin/get-font-files src)
FONT_FLAGS := $(shell ./bin/get-font-files MonoLisa 'otf,ttf,woff,woff2')
patch: ./bin/font-patcher ## apply nerd fonts patch |> -gs b_magenta -ms bold patch: ./bin/font-patcher ## apply nerd fonts patch |> -gs b_magenta -ms bold
@./bin/patch-monolisa \ @./patch-monolisa \
$(FONT_FLAGS) \ $(ARGS) \
$(ARGS) -f MonoLisa/
update-fonts: ## move fonts and update fc-cache update-fonts: ## move fonts and update fc-cache
$(call msg,Adding Fonts To System) $(call msg,Adding Fonts To System)

View file

@ -11,7 +11,7 @@ tested w/ MonoLisa v2.003
## Dependencies ## Dependencies
- `python` - `python`
- `make` - `make` (optional)
- `fontforge` OR `docker` - `fontforge` OR `docker`
@ -52,7 +52,7 @@ you can easily apply the nerd font patches with `make`.
To patch all font types use the default `patch` rule. To patch all font types use the default `patch` rule.
```bash ```bash
make make # or ./patch-monolisa -f MonoLisa -c
``` ```
By default the complete (`-c`) flag is passed to the font-patcher script to include all icons/symbols. By default the complete (`-c`) flag is passed to the font-patcher script to include all icons/symbols.
@ -62,7 +62,7 @@ You can change this by specifying the `ARGS` at runtime.
ARGS="-c -w" make patch ARGS="-c -w" make patch
``` ```
See `./bin/patch-monolisa --help` and `./bin/font-patcher --help` for available `ARGS`. See `./patch-monolisa --help` and `./bin/font-patcher --help` for available `ARGS`.
You can find your patched fonts in the `patched/` directory You can find your patched fonts in the `patched/` directory

View file

@ -6,7 +6,7 @@
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.5.2" script_version = "3.6.1"
version = "2.3.3" version = "2.3.3"
projectName = "Nerd Fonts" projectName = "Nerd Fonts"
@ -21,6 +21,7 @@ from argparse import RawTextHelpFormatter
import errno import errno
import subprocess import subprocess
import json import json
from enum import Enum
try: try:
import configparser import configparser
except ImportError: except ImportError:
@ -130,7 +131,7 @@ class TableHEADWriter:
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 """
if type(where) is str: if isinstance(where, str):
positions = {'checksumAdjustment': 2+2+4, positions = {'checksumAdjustment': 2+2+4,
'flags': 2+2+4+4+4, 'flags': 2+2+4+4+4,
'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2, 'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2,
@ -194,6 +195,14 @@ def panose_check_to_text(value, panose = False):
return "Panose says \"monospaced\"" return "Panose says \"monospaced\""
return "Panose is invalid" + (" ({})".format(list(panose)) if panose else "") return "Panose is invalid" + (" ({})".format(list(panose)) if panose else "")
def panose_proportion_to_text(value):
""" Interpret a Panose proportion value (4th value) for family 2 (latin text) """
proportion = {
0: "Any", 1: "No Fit", 2: "Old Style", 3: "Modern", 4: "Even Width",
5: "Extended", 6: "Condensed", 7: "Very Extended", 8: "Very Condensed",
9: "Monospaced" }
return proportion.get(value, "??? {}".format(value))
def is_monospaced(font): def is_monospaced(font):
""" Check if a font is probably monospaced """ """ Check if a font is probably monospaced """
# Some fonts lie (or have not any Panose flag set), spot check monospaced: # Some fonts lie (or have not any Panose flag set), spot check monospaced:
@ -222,6 +231,20 @@ def is_monospaced(font):
# We believe our own check more then Panose ;-D # We believe our own check more then Panose ;-D
return (width_mono, None if width_mono else glyph) return (width_mono, None if width_mono else glyph)
def force_panose_monospaced(font):
""" Forces the Panose flag to monospaced if they are unset or halfway ok already """
# For some Windows applications (e.g. 'cmd'), they seem to honour the Panose table
# https://forum.high-logic.com/postedfiles/Panose.pdf
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')")
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])))
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
font.os2_panose = tuple(panose)
def get_advance_width(font, extended, minimum): def get_advance_width(font, extended, minimum):
""" Get the maximum/minimum advance width in the extended(?) range """ """ Get the maximum/minimum advance width in the extended(?) range """
width = 0 width = 0
@ -248,6 +271,17 @@ def report_advance_widths(font):
get_advance_width(font, True, True), get_advance_width(font, False, True), get_advance_width(font, True, True), get_advance_width(font, False, True),
get_advance_width(font, False, False), get_advance_width(font, True, False)) get_advance_width(font, False, False), get_advance_width(font, True, False))
def get_btb_metrics(font):
""" Get the baseline to baseline distance for all three metrics """
hhea_height = font.hhea_ascent - font.hhea_descent
typo_height = font.os2_typoascent - font.os2_typodescent
win_height = font.os2_winascent + font.os2_windescent
win_gap = max(0, font.hhea_linegap - win_height + hhea_height)
hhea_btb = hhea_height + font.hhea_linegap
typo_btb = typo_height + font.os2_typolinegap
win_btb = win_height + win_gap
return (hhea_btb, typo_btb, win_btb, win_gap)
class font_patcher: class font_patcher:
def __init__(self, args): def __init__(self, args):
@ -258,6 +292,7 @@ class font_patcher:
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.font_extrawide = False self.font_extrawide = False
self.source_monospaced = None # Later True or False
self.onlybitmaps = 0 self.onlybitmaps = 0
self.essential = set() self.essential = set()
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
@ -267,7 +302,6 @@ class font_patcher:
self.setup_version() self.setup_version()
self.get_essential_references() self.get_essential_references()
self.setup_name_backup(font) self.setup_name_backup(font)
if self.args.single:
self.assert_monospace() self.assert_monospace()
self.remove_ligatures() self.remove_ligatures()
self.setup_patch_set() self.setup_patch_set()
@ -280,13 +314,6 @@ class font_patcher:
# 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.
# This needs to be done on all characters, as some information seems to be lost from the original font file. # This needs to be done on all characters, as some information seems to be lost from the original font file.
self.set_sourcefont_glyph_widths() self.set_sourcefont_glyph_widths()
# For some Windows applications (e.g. 'cmd') that is not enough. But they seem to honour the Panose table
# https://forum.high-logic.com/postedfiles/Panose.pdf
panose = list(self.sourceFont.os2_panose)
if panose[0] == 0 or panose[0] == 2: # 0 (1st value) = family kind; 0 = any (default); 2 = latin text and display
panose[0] = 2 # Assert kind
panose[3] = 9 # 3 (4th value) = propotion; 9 = monospaced
self.sourceFont.os2_panose = tuple(panose)
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs # For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs
if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2: if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
@ -316,6 +343,7 @@ class font_patcher:
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format( sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format(
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename'])) symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull'
# Match the symbol font size to the source font size # Match the symbol font size to the source font size
symfont.em = self.sourceFont.em symfont.em = self.sourceFont.em
@ -422,7 +450,9 @@ class font_patcher:
def setup_font_names(self, font): def setup_font_names(self, font):
font.fontname = font.persistent["fontname"] font.fontname = font.persistent["fontname"]
if isinstance(font.persistent["fullname"], str):
font.fullname = font.persistent["fullname"] font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["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
@ -470,7 +500,7 @@ class font_patcher:
verboseAdditionalFontNameSuffix += " Mono" verboseAdditionalFontNameSuffix += " Mono"
if FontnameParserOK and self.args.makegroups: if FontnameParserOK and self.args.makegroups:
use_fullname = type(font.fullname) == str # Usually the fullname is better to parse use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname # Use fullname if it is 'equal' to the fontname
if font.fullname: if font.fullname:
use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower() use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower()
@ -686,6 +716,9 @@ class font_patcher:
def assert_monospace(self): def assert_monospace(self):
# Check if the sourcefont is monospaced # Check if the sourcefont is monospaced
width_mono, offending_char = is_monospaced(self.sourceFont) width_mono, offending_char = is_monospaced(self.sourceFont)
self.source_monospaced = width_mono
if self.args.nonmono:
return
panose_mono = check_panose_monospaced(self.sourceFont) panose_mono = check_panose_monospaced(self.sourceFont)
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown' # The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown'
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1): if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
@ -693,19 +726,48 @@ class font_patcher:
print(" {} and {}".format( print(" {} and {}".format(
report_advance_widths(self.sourceFont), report_advance_widths(self.sourceFont),
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))) panose_check_to_text(panose_mono, self.sourceFont.os2_panose)))
if not width_mono: if self.args.single and not width_mono:
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
if offending_char is not None: if offending_char is not None:
print(" Offending char: 0x{:X}".format(offending_char)) print(" Offending char: 0x{:X}".format(offending_char))
if self.args.single <= 1: if self.args.single <= 1:
sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching") sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
if width_mono:
force_panose_monospaced(self.sourceFont)
def setup_patch_set(self): def setup_patch_set(self):
""" Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """ """ Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """
# Supported params: overlap | careful
box_enabled = self.source_monospaced # Box glyph only for monospaced
if box_enabled:
self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f)
box_glyphs_target = len(list(self.sourceFont.selection))
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
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))
box_keep = False
box_enabled = True
else:
box_keep = True # just scale do not copy
box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this
# Stretch 'xz' or 'pa' (preserve aspect ratio)
# Supported params: overlap | careful | xy-ratio | dont_copy
# Overlap value is used horizontally but vertically limited to 0.01 # Overlap value is used horizontally but vertically limited to 0.01
# Powerline dividers # Careful does not overwrite/modify existing glyphs
# The xy-ratio limits the x-scale for a given y-scale to make the ratio <= this value (to prevent over-wide glyphs)
# '1' means occupu 1 cell (default for 'xy')
# '2' means occupy 2 cells (default for 'pa')
# '!' means do the 'pa' scaling even with non mono fonts (else it just scales down, never up)
# Dont_copy does not overwrite existing glyphs but rescales the preexisting ones
SYM_ATTR_DEFAULT = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}
}
SYM_ATTR_POWERLINE = { SYM_ATTR_POWERLINE = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}, 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
@ -722,16 +784,16 @@ class font_patcher:
0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}}, 0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}},
# Bottom Triangles # Bottom Triangles
0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}}, 0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, 0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}}, 0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, 0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
# Top Triangles # Top Triangles
0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}}, 0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, 0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}}, 0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, 0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
# Flames # Flames
0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, 0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
@ -740,36 +802,34 @@ class font_patcher:
0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, 0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}},
# Small squares # Small squares
0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}},
0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}},
# Bigger squares # Bigger squares
0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}},
0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}},
# Waveform # Waveform
0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, 0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
0xe0ca: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, 0xe0ca: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
# Hexagons # Hexagons
0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, 0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02, 'xy-ratio': 0.85}},
0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'xy-ratio': 0.865}},
# Legos # Legos
0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}},
0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, 0xe0d0: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}},
0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}},
# Top and bottom trapezoid # Top and bottom trapezoid
0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}, 0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}},
0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}} 0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}
} }
SYM_ATTR_TRIGRAPH = {
SYM_ATTR_DEFAULT = { 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa1!', 'params': {'overlap': -0.10, 'careful': True}}
# 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}
} }
SYM_ATTR_FONTA = { SYM_ATTR_FONTA = {
# 'pa' == preserve aspect ratio # 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}, 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
@ -779,7 +839,16 @@ class font_patcher:
0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}, 0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}},
0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}} 0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}
} }
SYM_ATTR_HEAVYBRACKETS = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {'careful': True}}
}
SYM_ATTR_BOX = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'dont_copy': box_keep}},
# No overlap with checkered greys (commented out because that raises problems on rescaling clients)
# 0x2591: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}},
# 0x2592: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}},
# 0x2593: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}},
}
CUSTOM_ATTR = { CUSTOM_ATTR = {
# 'pa' == preserve aspect ratio # 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}} 'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}}
@ -828,6 +897,20 @@ class font_patcher:
# For historic reasons ScaleGroups is sometimes called 'new method' and ScaleGlyph 'old'. # For historic reasons ScaleGroups is sometimes called 'new method' and ScaleGlyph 'old'.
# The codepoints mentioned here are symbol-font-codepoints. # The codepoints mentioned here are symbol-font-codepoints.
BOX_SCALE_LIST = {'ScaleGroups': [
[*range(0x2500, 0x2570 + 1), *range(0x2574, 0x257f + 1)], # box drawing
range(0x2571, 0x2573 + 1), # diagonals
[*range(0x2580, 0x2590 + 1), 0x2594, 0x2595], # blocks
range(0x2591, 0x2593 + 1), # greys
range(0x2594, 0x259f + 1), # quards (Note: quard 2597 in Hack is wrong, scales like block!)
]}
CODI_SCALE_LIST = {'ScaleGroups': [
range(0xea99, 0xeaa1 + 1), # arrows
range(0xeb6e, 0xeb71 + 1), # triangles
range(0xeab4, 0xeab7 + 1), # chevrons
[0xea71, *range(0xeaa6, 0xeaab + 1), 0xeabc, 0xeb18, 0xeb87, 0xeb88, 0xeb8a, 0xeb8c, 0xebb4], # cicles
[0xeacc, 0xeaba], # dash
]}
DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo
'GlyphsToScale': [ 'GlyphsToScale': [
(0xe6bd, 0xe6c3) # very small things (0xe6bd, 0xe6c3) # very small things
@ -863,21 +946,32 @@ class font_patcher:
0xf0ca, # dash 0xf0ca, # dash
]} ]}
WEATH_SCALE_LIST = {'ScaleGroups': [ WEATH_SCALE_LIST = {'ScaleGroups': [
[0xf03c, 0xf042, 0xf045 ], # degree signs
[0xf043, 0xf044, 0xf048, 0xf04b, 0xf04c, 0xf04d, 0xf057, 0xf058, 0xf087, 0xf088], # arrows
range(0xf053, 0xf055 + 1), # thermometers
[*range(0xf059, 0xf061 + 1), 0xf0b1], # wind directions
range(0xf089, 0xf094 + 1), # clocks
range(0xf095, 0xf0b0 + 1), # moon phases range(0xf095, 0xf0b0 + 1), # moon phases
range(0xf0b7, 0xf0c3 + 1), # wind strengths range(0xf0b7, 0xf0c3 + 1), # wind strengths
range(0xf053, 0xf055 + 1), # thermometer [0xf06e, 0xf070 ], # solar/lunar eclipse
[0xf06e, 0xf070 ], # solar eclipse # Note: Codepoints listed before that are also in the following range
[0xf042, 0xf045 ], # degree sign # will take the scaling of the previous group (the ScaleGroups are
]} # searched through in definition order).
MDI_SCALE_LIST = {'ScaleGlyph': 0xf068d, # 'solid' fills complete design space # But be careful, the combined bounding box for the following group
'GlyphsToScale+': [ # _will_ include all glyphs in its definition: Make sure the exempt
(0xf0000, 0xfffff) # all because they are very well scaled already # glyphs from above are smaller (do not extend) the combined bounding
# box of this range:
range(0xf000, 0xf0cb + 1), # lots of clouds and other (Please read note above!)
]} ]}
MDI_SCALE_LIST = None # Maybe later add some selected ScaleGroups
# 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': 0xE5AA, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5FF, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': True, 'Name': "Heavy Angle Brackets", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x276C, 'SymEnd': 0x2771, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_HEAVYBRACKETS},
{'Enabled': box_enabled, 'Name': "Box Drawing", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x2500, 'SymEnd': 0x259F, 'SrcStart': None, 'ScaleRules': BOX_SCALE_LIST, 'Attributes': SYM_ATTR_BOX},
{'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleRules': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleRules': 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, 'ScaleRules': 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, 'ScaleRules': 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, 'ScaleRules': 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, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
@ -885,6 +979,7 @@ class font_patcher:
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0x2630, 'SymEnd': 0x2630, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_TRIGRAPH},
{'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleRules': 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, 'ScaleRules': 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, 'ScaleRules': 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, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize {'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize
@ -898,7 +993,7 @@ class font_patcher:
{'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': 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': 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.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'ScaleRules': 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, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR} {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
] ]
@ -928,9 +1023,10 @@ class font_patcher:
self.add_glyphrefs_to_essential(altcode) self.add_glyphrefs_to_essential(altcode)
# From fontforge documentation: # From fontforge documentation:
# glyph.references return a tuple of tuples containing, for each reference in foreground, # glyph.references return a tuple of tuples containing, for each reference in foreground,
# a glyph name, a transformation matrix, and whether the reference is currently selected. # a glyph name, a transformation matrix, and (depending on ff version) whether the
# reference is currently selected.
references = self.sourceFont[unicode].references references = self.sourceFont[unicode].references
for refcode in [ self.sourceFont[n].unicode for n, m, s in references ]: for refcode in [ self.sourceFont[n].unicode for n, *_ in references ]: # tuple of 2 or 3 depending on ff version
if refcode not in self.essential and refcode >= 0: if refcode not in self.essential and refcode >= 0:
self.add_glyphrefs_to_essential(refcode) self.add_glyphrefs_to_essential(refcode)
@ -961,39 +1057,37 @@ class font_patcher:
# and we try to sort this out here # and we try to sort this out here
# See also https://glyphsapp.com/learn/vertical-metrics # See also https://glyphsapp.com/learn/vertical-metrics
# See also https://github.com/source-foundry/font-line # See also https://github.com/source-foundry/font-line
hhea_height = self.sourceFont.hhea_ascent - self.sourceFont.hhea_descent (hhea_btb, typo_btb, win_btb, win_gap) = get_btb_metrics(self.sourceFont)
typo_height = self.sourceFont.os2_typoascent - self.sourceFont.os2_typodescent
win_height = self.sourceFont.os2_winascent + self.sourceFont.os2_windescent
win_gap = max(0, self.sourceFont.hhea_linegap - win_height + hhea_height)
hhea_btb = hhea_height + self.sourceFont.hhea_linegap
typo_btb = typo_height + self.sourceFont.os2_typolinegap
win_btb = win_height + win_gap
use_typo = self.sourceFont.os2_use_typo_metrics != 0 use_typo = self.sourceFont.os2_use_typo_metrics != 0
Metric = Enum('Metric', ['HHEA', 'TYPO', 'WIN'])
# We use either TYPO (1) or WIN (2) and compare with HHEA # We use either TYPO (1) or WIN (2) and compare with HHEA
# and use HHEA (0) if the fonts seems broken # and use HHEA (0) if the fonts seems broken - no WIN, see #1056
our_btb = typo_btb if use_typo else win_btb our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb: if our_btb == hhea_btb:
metrics = 1 if use_typo else 2 # conforming font metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
else: else:
# We trust the WIN metric more, see experiments in #1056 # We trust the WIN metric more, see experiments in #1056
print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb)) print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb))
our_btb = win_btb our_btb = win_btb
metrics = 1 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)) # print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname))
self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0} self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0}
if metrics == 0: if metrics == Metric.HHEA:
self.font_dim['ymin'] = self.sourceFont.hhea_descent + half_gap(self.sourceFont.hhea_linegap, False) self.font_dim['ymin'] = self.sourceFont.hhea_descent - half_gap(self.sourceFont.hhea_linegap, False)
self.font_dim['ymax'] = self.sourceFont.hhea_ascent + half_gap(self.sourceFont.hhea_linegap, True) self.font_dim['ymax'] = self.sourceFont.hhea_ascent + half_gap(self.sourceFont.hhea_linegap, True)
elif metrics == 1: elif metrics == Metric.TYPO:
self.font_dim['ymin'] = self.sourceFont.os2_typodescent + half_gap(self.sourceFont.os2_typolinegap, False) self.font_dim['ymin'] = self.sourceFont.os2_typodescent - half_gap(self.sourceFont.os2_typolinegap, False)
self.font_dim['ymax'] = self.sourceFont.os2_typoascent + half_gap(self.sourceFont.os2_typolinegap, True) self.font_dim['ymax'] = self.sourceFont.os2_typoascent + half_gap(self.sourceFont.os2_typolinegap, True)
else: elif metrics == Metric.WIN:
self.font_dim['ymin'] = -self.sourceFont.os2_windescent + half_gap(win_gap, False) self.font_dim['ymin'] = -self.sourceFont.os2_windescent - half_gap(win_gap, False)
self.font_dim['ymax'] = self.sourceFont.os2_winascent + half_gap(win_gap, True) self.font_dim['ymax'] = self.sourceFont.os2_winascent + half_gap(win_gap, True)
else:
pass # Will fail the metrics check some line later
# Calculate font height # Calculate font height
self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax'] self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax']
@ -1008,6 +1102,7 @@ class font_patcher:
'width' : self.sourceFont.em, 'width' : self.sourceFont.em,
'height': self.sourceFont.descent + self.sourceFont.ascent, 'height': self.sourceFont.descent + self.sourceFont.ascent,
} }
our_btb = self.sourceFont.descent + self.sourceFont.ascent
elif self.font_dim['height'] < 0: elif self.font_dim['height'] < 0:
sys.exit("{}: Can not detect sane font height".format(projectName)) sys.exit("{}: Can not detect sane font height".format(projectName))
@ -1021,11 +1116,15 @@ class font_patcher:
self.sourceFont.hhea_descent = self.sourceFont.os2_typodescent self.sourceFont.hhea_descent = self.sourceFont.os2_typodescent
self.sourceFont.hhea_linegap = self.sourceFont.os2_typolinegap self.sourceFont.hhea_linegap = self.sourceFont.os2_typolinegap
self.sourceFont.os2_use_typo_metrics = 1 self.sourceFont.os2_use_typo_metrics = 1
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont)
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))
# Step 2 # Step 2
# Find the biggest char width and advance width # Find the biggest char width and advance width
# 0x00-0x17f is the Latin Extended-A range # 0x00-0x17f is the Latin Extended-A range
warned = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target warned1 = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target
warned2 = warned1
for glyph in range(0x21, 0x17f): for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF) or glyph in [ if glyph in range(0x7F, 0xBF) or glyph in [
0x132, 0x133, # IJ, ij (in Overpass Mono) 0x132, 0x133, # IJ, ij (in Overpass Mono)
@ -1041,30 +1140,39 @@ class font_patcher:
# print("WIDTH {:X} {} ({} {})".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("WIDTH {:X} {} ({} {})".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.sourceFont[glyph].width: if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width self.font_dim['width'] = self.sourceFont[glyph].width
if not warned and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format( print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format(
report_advance_widths(self.sourceFont))) report_advance_widths(self.sourceFont)))
warned = True warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']: if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax self.font_dim['xmax'] = xmax
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Info: 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)) # 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")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim) # print("FINAL", self.font_dim)
def get_target_width(self, stretch):
""" Get the target width (1 or 2 'cell') for a given stretch parameter """
# For monospaced fonts all chars need to be maximum 'one' space wide
# other fonts allows double width glyphs for 'pa' or if requested with '2'
if self.args.single or ('pa' not in stretch and '2' not in stretch) or '1' in stretch:
return 1
return 2
def get_scale_factors(self, sym_dim, stretch): def get_scale_factors(self, sym_dim, stretch):
""" Get scale in x and y as tuple """ """ Get scale in x and y as tuple """
# It is possible to have empty glyphs, so we need to skip those. # It is possible to have empty glyphs, so we need to skip those.
if not sym_dim['width'] or not sym_dim['height']: if not sym_dim['width'] or not sym_dim['height']:
return (1.0, 1.0) return (1.0, 1.0)
# For monospaced fonts all chars need to be maximum 'one' space wide target_width = self.font_dim['width'] * self.get_target_width(stretch)
# other fonts allows double width glyphs for 'pa' or if requested with '2'
if self.args.single or (stretch != 'pa' and '2' not in stretch):
relative_width = 1.0
else:
relative_width = 2.0
target_width = self.font_dim['width'] * relative_width
scale_ratio_x = target_width / sym_dim['width'] scale_ratio_x = target_width / sym_dim['width']
# font_dim['height'] represents total line height, keep our symbols sized based upon font's em # font_dim['height'] represents total line height, keep our symbols sized based upon font's em
@ -1072,10 +1180,10 @@ class font_patcher:
target_height = self.font_dim['height'] target_height = self.font_dim['height']
scale_ratio_y = target_height / sym_dim['height'] scale_ratio_y = target_height / sym_dim['height']
if stretch == 'pa': if 'pa' in stretch:
# We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit # We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit
scale_ratio_x = min(scale_ratio_x, scale_ratio_y) scale_ratio_x = min(scale_ratio_x, scale_ratio_y)
if not self.args.single: if not self.args.single and not '!' in stretch:
# non monospaced fonts just scale down on 'pa', not up # non monospaced fonts just scale down on 'pa', not up
scale_ratio_x = min(scale_ratio_x, 1.0) scale_ratio_x = min(scale_ratio_x, 1.0)
scale_ratio_y = scale_ratio_x scale_ratio_y = scale_ratio_x
@ -1093,7 +1201,6 @@ class font_patcher:
""" Copies symbol glyphs into self.sourceFont """ """ Copies symbol glyphs into self.sourceFont """
progressText = '' progressText = ''
careful = False careful = False
glyphSetLength = 0
sourceFontCounter = 0 sourceFontCounter = 0
if self.args.careful: if self.args.careful:
@ -1114,14 +1221,14 @@ class font_patcher:
glyphSetLength = len(symbolFontSelection) glyphSetLength = len(symbolFontSelection)
if not self.args.quiet: if not self.args.quiet:
sys.stdout.write("Adding " + str(max(1, glyphSetLength)) + " Glyphs from " + setName + " Set \n") modify = attributes['default']['params'].get('dont_copy')
sys.stdout.write("{} {} Glyphs from {} Set\n".format(
"Adding" if not modify else "Rescaling", glyphSetLength, setName))
currentSourceFontGlyph = -1 # initialize for the exactEncoding case currentSourceFontGlyph = -1 # initialize for the exactEncoding case
width_warning = False width_warning = False
for index, sym_glyph in enumerate(symbolFontSelection): for index, sym_glyph in enumerate(symbolFontSelection):
index = max(1, index)
sym_attr = attributes.get(sym_glyph.unicode) sym_attr = attributes.get(sym_glyph.unicode)
if sym_attr is None: if sym_attr is None:
sym_attr = attributes['default'] sym_attr = attributes['default']
@ -1176,6 +1283,12 @@ class font_patcher:
if currentSourceFontGlyph in self.sourceFont: if currentSourceFontGlyph in self.sourceFont:
self.sourceFont[currentSourceFontGlyph].removePosSub("*") self.sourceFont[currentSourceFontGlyph].removePosSub("*")
dont_copy = sym_attr['params'].get('dont_copy')
if dont_copy:
# Just prepare scaling of existing glyphs
glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, self.sourceFont, currentSourceFontGlyph) if scaleRules is not None else None
else:
# This will destroy any content currently in currentSourceFontGlyph, so do it first # This will destroy any content currently in currentSourceFontGlyph, so do it first
glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, symbolFont, currentSourceFontGlyph) if scaleRules is not None else None glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, symbolFont, currentSourceFontGlyph) if scaleRules is not None else None
@ -1196,6 +1309,8 @@ class font_patcher:
if glyph_scale_data is not None: if glyph_scale_data is not None:
if glyph_scale_data[1] is not None: if glyph_scale_data[1] is not None:
sym_dim = glyph_scale_data[1] # Use combined bounding box sym_dim = glyph_scale_data[1] # Use combined bounding box
(scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, sym_attr['stretch'])
else:
# This is roughly alike get_scale_factors(glyph_scale_data[1], 'pa') # This is roughly alike get_scale_factors(glyph_scale_data[1], 'pa')
# Except we do not have glyph_scale_data[1] always... # Except we do not have glyph_scale_data[1] always...
(scale_ratio_x, scale_ratio_y) = (glyph_scale_data[0], glyph_scale_data[0]) (scale_ratio_x, scale_ratio_y) = (glyph_scale_data[0], glyph_scale_data[0])
@ -1252,17 +1367,24 @@ class font_patcher:
x_align_distance += (self.font_dim['width'] / 2) - (sym_dim['width'] / 2) x_align_distance += (self.font_dim['width'] / 2) - (sym_dim['width'] / 2)
elif sym_attr['align'] == 'r': elif sym_attr['align'] == 'r':
# Right align # Right align
x_align_distance += self.font_dim['width'] - sym_dim['width'] x_align_distance += self.font_dim['width'] * self.get_target_width(sym_attr['stretch']) - sym_dim['width']
if not self.args.single and '2' in sym_attr['stretch']: # If symbol glyph is wider than target font cell, just left-align
x_align_distance += self.font_dim['width'] x_align_distance = max(self.font_dim['xmin'] - sym_dim['xmin'], x_align_distance)
if overlap: if overlap:
overlap_width = self.font_dim['width'] * overlap overlap_width = self.font_dim['width'] * overlap
if sym_attr['align'] == 'l': if sym_attr['align'] == 'l':
x_align_distance -= overlap_width x_align_distance -= overlap_width
if sym_attr['align'] == 'r' and not self.args.nonmono: elif sym_attr['align'] == 'c':
# Nonmono is 'left aligned' per definition, translation does not help here if overlap_width > 0:
x_align_distance += overlap_width x_align_distance -= overlap_width / 2
elif sym_attr['align'] == 'r':
# Check and correct overlap; it can go wrong if we have a xy-ratio limit
target_xmax = (self.font_dim['xmin'] + self.font_dim['width']) * self.get_target_width(sym_attr['stretch'])
target_xmax += overlap_width
glyph_xmax = sym_dim['xmax'] + x_align_distance
correction = target_xmax - glyph_xmax
x_align_distance += correction
align_matrix = psMat.translate(x_align_distance, y_align_distance) align_matrix = psMat.translate(x_align_distance, y_align_distance)
self.sourceFont[currentSourceFontGlyph].transform(align_matrix) self.sourceFont[currentSourceFontGlyph].transform(align_matrix)
@ -1485,7 +1607,7 @@ def get_multiglyph_boundingBox(glyphs, destGlyph = None):
if glyph is None: if glyph is None:
# Glyph has been in defining range but is not in the actual font # Glyph has been in defining range but is not in the actual font
continue continue
if destGlyph: if destGlyph and glyph.font != destGlyph.font:
glyph.font.selection.select(glyph) glyph.font.selection.select(glyph)
glyph.font.copy() glyph.font.copy()
destGlyph.font.selection.select(destGlyph) destGlyph.font.selection.select(destGlyph)
@ -1571,6 +1693,34 @@ 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 check_version_with_git(version):
""" Upgraded the version to the current git tag version (starting with 'v') """
git = subprocess.run("git describe --tags",
cwd=os.path.dirname(__file__),
shell=True,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
).stdout.decode('utf-8')
if len(git) == 0:
return False
tag = git.strip()
if len(tag) == 0 or not tag.startswith('v'):
return False
tag = tag[1:]
r = re.search('(.*?)(-[0-9]+)-g[0-9a-fA-F]+$', tag)
if r:
tag = r.group(1)
patchlevel = r.group(2)
else:
patchlevel = ""
# Inspired by Phaxmohdem's versiontuple https://stackoverflow.com/a/28568003
versiontuple = lambda v: tuple( p.zfill(8) for p in v.split(".") )
if versiontuple(tag) > versiontuple(version):
return tag + patchlevel
if versiontuple(tag) == versiontuple(version) and len(patchlevel) > 0:
return tag + patchlevel
return False
def setup_arguments(): def setup_arguments():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=( description=(
@ -1699,7 +1849,12 @@ def setup_arguments():
def main(): def main():
print("{} Patcher v{} ({}) executing".format(projectName, version, script_version)) 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()))
if git_version:
version = git_version
check_fontforge_min_version() check_fontforge_min_version()
args = setup_arguments() args = setup_arguments()
patcher = font_patcher(args) patcher = font_patcher(args)

View file

@ -1,30 +0,0 @@
#!/usr/bin/env python3
import sys
from pathlib import Path
EXTS = ["otf", "ttf", "woff", "woff2"]
def find_files(search_path, exts=None):
return (
[f for ext in exts for f in search_path.glob(f"**/*.{ext}")]
if exts
else [f for f in search_path.rglob("*") if f.is_file()]
)
def main():
if len(sys.argv) == 1:
print("please specify directory to search")
exit(1)
search_path = Path(sys.argv[1])
exts = sys.argv[2].split(",") if len(sys.argv) == 3 else EXTS
for f in find_files(search_path, exts):
sys.stdout.write(f"-f '{f}' ")
if __name__ == "__main__":
main()

View file

@ -10,7 +10,7 @@ import time
from pathlib import Path from pathlib import Path
from typing import List, Set from typing import List, Set
FONT_SRC = (Path(__file__).parent.parent / "MonoLisa").absolute() FONT_SRC = (Path(__file__).parent / "MonoLisa").absolute()
class Color: class Color:
@ -159,7 +159,6 @@ def patch_single_font(
f"{color.yellow}:::{color.end} Patching font " f"{color.yellow}:::{color.end} Patching font "
f"{color.bold}{font_path.name}{color.end}... " f"{color.bold}{font_path.name}{color.end}... "
): ):
run_cmd(cmd, font_path, verbose) run_cmd(cmd, font_path, verbose)
echo(f"{rel_path} patched!", hue="green") echo(f"{rel_path} patched!", hue="green")
@ -205,6 +204,17 @@ def patch_font_dir_docker(
echo(f"{font_dir_path.name}/ fonts patched!", hue="green") echo(f"{font_dir_path.name}/ fonts patched!", hue="green")
def get_font_files(p):
exts = ["otf", "ttf", "woff", "woff2"]
if p.is_file():
return (p,)
if p.is_dir():
return (f for ext in exts for f in p.glob(f"**/*.{ext}"))
return ()
def echo(msg: str, header=False, hue="cyan") -> None: def echo(msg: str, header=False, hue="cyan") -> None:
if header: if header:
print(f"==>{color.magenta} {msg} {color.end}<==") print(f"==>{color.magenta} {msg} {color.end}<==")
@ -213,7 +223,6 @@ def echo(msg: str, header=False, hue="cyan") -> None:
def main(): def main():
echo("MonoLisa NerdFont Patcher", header=True) echo("MonoLisa NerdFont Patcher", header=True)
args, fp_args = get_args() args, fp_args = get_args()
fp_args = " ".join(fp_args) fp_args = " ".join(fp_args)
@ -223,12 +232,15 @@ def main():
echo("Patching the following files") echo("Patching the following files")
for fontfile in args.font_path: for fontfile in args.font_path:
sys.stdout.write(f" {color.magenta}->{color.end} {fontfile}\n") sys.stdout.write(f" {color.magenta}->{color.end} {fontfile}\n")
fontfiles = [f for p in args.font_path for f in get_font_files(p)]
if args.docker: if args.docker:
echo("==> DOCKER MODE ENABLED") echo("==> DOCKER MODE ENABLED")
for font_dir in collect_files_by_dir(args.font_path): for font_dir in collect_files_by_dir(fontfiles):
patch_font_dir_docker(font_dir, args.output, fp_args, args.verbose) patch_font_dir_docker(font_dir, args.output, fp_args, args.verbose)
else: else:
for fontfile in args.font_path: for fontfile in fontfiles:
patch_single_font(Path(fontfile), args.output, fp_args, args.verbose) patch_single_font(Path(fontfile), args.output, fp_args, args.verbose)
echo("fonts are patched", hue="green") echo("fonts are patched", hue="green")
@ -236,6 +248,5 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
color = Color() color = Color()
main() main()

Binary file not shown.

BIN
src/glyphs/extraglyphs.sfd Normal file

Binary file not shown.