Skip to content

Commit

Permalink
Build system refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
mishamyrt committed Mar 28, 2023
1 parent 5f55421 commit 20d30b8
Show file tree
Hide file tree
Showing 19 changed files with 355 additions and 249 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/master_ufo/
/Lilex (Autosaved).glyphs
/generator/__pycache__
.ruff_cache
28 changes: 18 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,43 @@ VTTF_FILE = $(VTTF_DIR)/Lilex-VF.ttf
OS := $(shell uname)

define build_font
$(VENV) fontmake \
-g $(GLYPHS_FILE) \
-a \
-o "$(1)" \
--output-dir "$(2)"
$(VENV) python scripts/lilex.py build $(1)
endef

configure: requirements.txt
rm -rf $(VENV_DIR)
make $(VENV_DIR)

.PHONY: lint
lint:
$(VENV) ruff scripts/
$(VENV) pylint scripts/

.PHONY: regenerate
regenerate:
python3 scripts/apply-features.py
$(VENV) python scripts/lilex.py regenerate

build: ttf otf variable_ttf
.PHONY: build
build:
$(call build_font)

.PHONY: bundle
bundle:
rm -rf "$(BUILD_DIR)"
make build
cd "$(BUILD_DIR)"; zip -r Lilex.zip ./*

.PHONY: ttf
ttf:
$(call build_font,ttf,$(TTF_DIR))
$(call build_font,ttf)

.PHONY: otf
otf:
$(call build_font,otf,$(OTF_DIR))
$(call build_font,otf)

.PHONY: variable_ttf
variable_ttf:
$(call build_font,variable,$(VTTF_DIR))
$(call build_font,variable)

install:
make install_$(OS)
Expand Down
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[tool.ruff]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # too complex
]

[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"]
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
fontmake==3.5.1
cu2qu==1.6.7
gftools==0.9.27
glyphsLib
glyphsLib==6.2.1
arrrgs==0.0.5
ruff==0.0.259
pylint==2.17.1
44 changes: 0 additions & 44 deletions scripts/apply-features.py

This file was deleted.

3 changes: 3 additions & 0 deletions scripts/builder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Lilex font builder module"""
from .const import SUPPORTED_FORMATS
from .font import GlyphsFont
8 changes: 8 additions & 0 deletions scripts/builder/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Lilex builder constants"""

UFO_PATH = "master_ufo"
SUPPORTED_FORMATS = [
"ttf",
"otf",
"variable"
]
77 changes: 77 additions & 0 deletions scripts/builder/font.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Glyphs helper"""
from __future__ import annotations

import sys
from typing import Callable, List

from glyphsLib import (
GSClass,
GSFeature,
GSFont,
GSGlyph,
build_masters,
)

from .const import SUPPORTED_FORMATS, UFO_PATH
from .make import make

LIGATURE_SUFFIX = ".liga"
GlyphFilter = Callable[[GSGlyph], bool]

class GlyphsFont:
"""Glyphs font builder"""
_font: GSFont = None
_path: str

def __init__(self, path: str):
self._font = GSFont(path)
self._path = path

def ligatures(self) -> str:
glyphs = self.glyphs(lambda x: x.name.endswith(LIGATURE_SUFFIX))
ligatures = []
for glyph in glyphs:
ligatures.append(glyph.name.replace(LIGATURE_SUFFIX, ""))
return ligatures

def glyphs(self, _filter: GlyphFilter) -> List[str]:
"""Returns a list of glyphs that match filter"""
result = []
for glyph in self._font.glyphs:
if _filter(glyph):
result.append(glyph)
return result

def save(self):
"""Saves the file to the same path from which it was opened"""
self._font.save(self._path)

def save_to(self, path: str) -> None:
"""Saves the file to the specified path"""
self._font.save(path)

def set_classes(self, classes: List[GSClass]):
"""Sets the font classes"""
for cls in classes:
if cls.name in self._font.classes:
self._font.classes[cls.name] = cls
else:
self._font.classes.append(cls)

def set_features(self, features: List[GSFeature]):
"""Sets the font features"""
for fea in features:
if fea.name in self._font.features:
self._font.features[fea.name] = fea
else:
self._font.features.append(fea)

def build(self, formats: List[str], out_dir: str):
print("Generating master UFOs")
build_masters(self._path, UFO_PATH, write_skipexportglyphs=True)
ds_path = f"{UFO_PATH}/{self._font.familyName}.designspace"
for fmt in formats:
if fmt not in SUPPORTED_FORMATS:
print(f"Unsupported format '{fmt}'")
sys.exit(1)
make(ds_path, fmt, f"{out_dir}/{fmt}")
16 changes: 16 additions & 0 deletions scripts/builder/make.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Make helpers"""
import subprocess as sp


def make(ds_path: str, fmt: str, out_dir: str) -> bool:
"""Wrapper for fontmake"""
cmd = " ".join([
"fontmake",
f"-m '{ds_path}'",
f"-o '{fmt}'",
f"--output-dir '{out_dir}'",
"--autohint"
])
with sp.Popen(cmd, shell=True, stdout=sp.PIPE) as child:
child.communicate()
return child.returncode == 0
2 changes: 2 additions & 0 deletions scripts/generator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Lilex font generator"""
from .ligatures import render_ligatures
51 changes: 51 additions & 0 deletions scripts/generator/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Generator constants"""

IGNORE_PREFIXES = {
'parenleft question': [
'colon',
'equal'
'exclaim'
],
'less question': ['equal'],
'parenleft question less': [
'equal',
'exclaim'
],
}

# Replacement ignore templates map
# ignore sub
IGNORE_TEMPLATES = {
2: [
"1 1' 2",
"1' 2 2"
],
3: [
"1 1' 2 3",
"1' 2 3 3"
],
4: [
"1 1' 2 3 4",
"1' 2 3 4 4"
]
}

# Replacement templates map
# sub
REPLACE_TEMPLATES = {
2: [
"LIG 2' by 1_2.liga",
"1' 2 by LIG"
],
3: [
"LIG LIG 3' by 1_2_3.liga",
"LIG 2' 3 by LIG",
"1' 2 3 by LIG"
],
4: [
"LIG LIG LIG 4' by 1_2_3_4.liga",
"LIG LIG 3' 4 by LIG",
"LIG 2' 3 4 by LIG",
"1' 2 3 4 by LIG"
]
}
55 changes: 55 additions & 0 deletions scripts/generator/ligatures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Ligatures feature generator module"""
from typing import List

from .const import IGNORE_PREFIXES, IGNORE_TEMPLATES, REPLACE_TEMPLATES


def render_statements(statements: List[str], prefix: str) -> str:
"""Renders fea statements"""
return '\n'.join(map(lambda x: f' {prefix} {x};', statements))

def render_template(template: str, glyphs: List[str]) -> str:
result = template
for i, glyph in enumerate(glyphs):
result = result.replace(str(i + 1), glyph)
return result

def get_ignore_prefixes(name: str, count: int) -> List[str]:
ignores: List[str] = []
tail = ''
for i in range(count - 1):
tail += f' {i + 1}'
for statement, starts in IGNORE_PREFIXES.items():
for start in starts:
if name.startswith(start):
ignores.append(f"{statement} 1' {tail}")
return ignores

def render_lookup(replace: List[str], ignore: List[str], glyphs: List[str]) -> str:
name = '_'.join(glyphs)
template = (
f"lookup {name}" + " { \n"
f"{render_statements(ignore, 'ignore sub')}"
f"{render_statements(replace, 'sub')}"
"\n} " + f"{name};"
)
return render_template(template, glyphs)

def render_ligature(name: str) -> str:
"""Generates an OpenType feature code that replaces characters with ligatures.
The `name` must be in the format `<glyph>_<glyph>`"""
glyphs = name.split('_')
count = len(glyphs)
ignores = IGNORE_TEMPLATES[count] + get_ignore_prefixes(name, count)
replaces = REPLACE_TEMPLATES[count]
return render_lookup(replaces, ignores, glyphs)

def render_ligatures(items: List[str]) -> str:
"""Renders the list of ligatures in the OpenType feature"""
result = ""
# For the generated code to work correctly,
# it is necessary to sort the list in descending order of the number of glyphs
ligatures = sorted(items, key=lambda x: len(x.split('_')), reverse=True)
for name in ligatures:
result += render_ligature(name) + "\n"
return result
44 changes: 44 additions & 0 deletions scripts/lilex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Lilex helper entrypoint"""
from arrrgs import arg, command, run
from builder import SUPPORTED_FORMATS, GlyphsFont
from generator import render_ligatures
from glyphsLib import GSFeature
from utils import read_classes, read_features, read_files

FONT_FILE = "Lilex.glyphs"
CLASSES_DIR = "./classes"
FEATURES_DIR = "./features"
OUT_DIR = "./build"

@command()
def regenerate(_, font: GlyphsFont):
"""Saves the generated source file with features and classes"""
font.save()
print("☺️ Font source successfully regenerated")

@command(
arg("formats", nargs="*", help="Format list", default=SUPPORTED_FORMATS)
)
def build(args, font: GlyphsFont):
"""Builds a binary font file"""
font.build(args.formats, OUT_DIR)
print("☺️ Font binaries successfully builded")

def generate_calt(font: GlyphsFont) -> GSFeature:
glyphs = font.ligatures()
code = render_ligatures(glyphs) + read_files(f"{FEATURES_DIR}/calt")
return GSFeature("calt", code)

def prepare(args):
font = GlyphsFont(FONT_FILE)

cls = read_classes(CLASSES_DIR)
fea = read_features(FEATURES_DIR)
fea.append(generate_calt(font))

font.set_classes(cls)
font.set_features(fea)
return args, font

if __name__ == "__main__":
run(prepare=prepare)
Loading

0 comments on commit 20d30b8

Please sign in to comment.