Skip to content

Commit

Permalink
feat: rework build system, fix tables
Browse files Browse the repository at this point in the history
  • Loading branch information
mishamyrt committed Aug 3, 2024
1 parent f7fb6c6 commit e28c9a0
Show file tree
Hide file tree
Showing 41 changed files with 372 additions and 278 deletions.
80 changes: 41 additions & 39 deletions scripts/font.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
#!/usr/bin/env python3
"""Lilex helper entrypoint"""
import sys
import os
import sys
from argparse import BooleanOptionalAction
import yaml
from typing import TypedDict

from multiprocessing import Process
import yaml
from arrrgs import arg, command, global_args, run
from glyphsLib import GSFeature, GSFont
from liblilex import DEFAULT_FORMATS, GlyphsFont, generate_spacers, render_ligatures
from utils import read_classes, read_features, read_files
from typing import TypedDict
from glyphsLib import GSFont
from liblilex import (
FontFormat,
GlyphsFont,
OpenTypeFeatures,
build_family,
generate_spacers,
render_ligatures,
)

CLASSES_DIR = "sources/classes"
FEATURES_DIR = "sources/features"
Expand Down Expand Up @@ -54,34 +59,28 @@ def generate(args, config: AppConfig):
print("🟢 Font source successfully regenerated")

@command(
arg("formats", nargs="*", help="Format list", default=DEFAULT_FORMATS),
arg("formats", nargs="*", help="Format list", default=['ttf', 'variable']),
arg("--store_temp", "-s", action=BooleanOptionalAction,
help="Not to delete the temporary folder after build")
)
def build(args, config: AppConfig):
async def build(args, config: AppConfig):
"""Builds a binary font file"""
if not os.path.exists(config["output"]):
os.makedirs(config["output"])
proc = []
for font in config["fonts"]:
p = Process(target=font.build, args=[
args.formats,
config["output"],
args.store_temp
])
p.start()
proc.append(p)
for p in proc:
p.join()
if p.exitcode != 0:
print("Failed to build font")
sys.exit(1)

formats = []
for fmt in args.formats:
formats.append(FontFormat(fmt))

print("Building font binaries...")
fonts = [font.file for font in config["fonts"]]
await build_family(fonts, config["output"], formats)
print("🟢 Font binaries successfully built")

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

def move_to_calt(font: GSFont, features: list[str]):
for fea in features:
Expand All @@ -98,26 +97,29 @@ def move_to_calt(font: GSFont, features: list[str]):
aalt = font.features["aalt"]
aalt.code = aalt.code.replace(f"feature {fea};\n", "")

def set_features(font: GlyphsFont, cls: list[str], fea: list[GSFeature]):
features = fea.copy()
calt = generate_calt(font)
features.append(calt)
font.set_classes(cls)
font.set_features(features)

def load_font(args):
with open(args.config, mode="r", encoding="utf-8") as file:
config_file = yaml.safe_load(file)
source_dir = config_file["source"]
cls = read_classes(os.path.join(source_dir, "classes"))
fea = read_features(os.path.join(source_dir, "features"))
features = OpenTypeFeatures(source_dir)
config = AppConfig(output=config_file["output"], fonts=[])
for file in config_file["family"]:
font_config = config_file["family"][file]
font = GlyphsFont(os.path.join(source_dir, file))
config["fonts"].append(font)
skips = []
if font_config is not None and "skip-features" in font_config:
skips = font_config["skip-features"]
feats, cls = features.items(
ignore_features=skips,
data={
"calt": render_ligatures(font.ligatures()),
}
)
generate_spacers(font.ligatures(), font.file.glyphs)
if config_file["family"][file] == "all":
set_features(font, cls, fea)
font.set_classes(cls)
font.set_features(feats)
font.set_fea_names()
config["fonts"].append(font)
return args, config

if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions scripts/liblilex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Lilex font library"""
from .build import DEFAULT_FORMATS, SUPPORTED_FORMATS
from .generator import generate_spacers, render_ligatures
from .build import FontFormat, build_family
from .features import OpenTypeFeatures, generate_spacers, render_ligatures
from .glyphs_font import GlyphsFont
4 changes: 2 additions & 2 deletions scripts/liblilex/build/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Lilex font builder module"""
from .const import DEFAULT_FORMATS, SUPPORTED_FORMATS
from .make import make
from .build import build_family
from .constants import FontFormat
67 changes: 67 additions & 0 deletions scripts/liblilex/build/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio
import os
from shutil import rmtree
from tempfile import mkdtemp

from glyphsLib import GSFont, build_masters
from glyphsLib.builder.axes import find_base_style

from .constants import FontFormat
from .fontmake import fontmake
from .post_process import post_process


def _build_design_space(font: GSFont) -> str:
"""Creates temporary designspace"""
temp_dir = mkdtemp(prefix="lilex")
glyphs_file = os.path.join(temp_dir, "source.glyphs")
ufo_dir = os.path.join(temp_dir, "master_ufo")
file_name = font.familyName
base_style = find_base_style(font.masters)
if base_style != "":
file_name = f"{file_name}-{base_style}"
ds_file = os.path.join(ufo_dir, f"{file_name}.designspace")
font.save(glyphs_file)
build_masters(
glyphs_file,
ufo_dir,
write_skipexportglyphs=True,
designspace_path=ds_file,
)
return (temp_dir, ds_file)

async def _build_font(
font: GSFont,
output_dir: str,
formats: list[FontFormat]
) -> list[str]:
"""Builds a font format. Returns a list of output files"""
temp_dir, ds_file = _build_design_space(font)
format_tasks = []
for fmt in formats:
out_dir = os.path.join(output_dir, fmt.value)
format_tasks.append(fontmake(ds_file, out_dir, fmt))
files = await asyncio.gather(*format_tasks)
rmtree(temp_dir)
return files

def _group_by_format(output: list[list[tuple[str, list[str]]]]) -> dict[FontFormat, list[str]]:
result = {}
for design_space in output:
for fmt, files in design_space:
if fmt not in result:
result[fmt] = []
result[fmt].extend(files)
return result

async def build_family(
fonts: list[GSFont],
output_dir: str,
formats: list[FontFormat]):
"""Builds a font family"""
tasks = []
for font in fonts:
tasks.append(_build_font(font, output_dir, formats))
files = await asyncio.gather(*tasks)
await post_process(_group_by_format(files))
return files
13 changes: 0 additions & 13 deletions scripts/liblilex/build/const.py

This file was deleted.

9 changes: 9 additions & 0 deletions scripts/liblilex/build/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Lilex builder constants"""
from enum import Enum


class FontFormat(Enum):
"""Font format enum"""
TTF = "ttf"
OTF = "otf"
VARIABLE = "variable"
45 changes: 45 additions & 0 deletions scripts/liblilex/build/fontmake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Make helpers"""
import asyncio
import os
import re

from .constants import FontFormat
from .path import which


def _format_variable_path(font_dir: str, family_name: str, axis: list[str]) -> str:
return f"{font_dir}/{family_name}[{','.join(axis)}].ttf"

async def fontmake(
design_space_path: str,
out_dir: str,
fmt: FontFormat,
axis: list[str] = None
):
"""Wrapper for fontmake"""
if axis is None:
axis = ["wght"]
cmd = [
which("fontmake"),
f'-m "{design_space_path}"',
f'-o "{fmt.value}"',
"--flatten-components",
"--autohint",
"--filter DecomposeTransformedComponentsFilter"
]
font_name = os.path.basename(design_space_path).split(".")[0]
if fmt == FontFormat.VARIABLE:
cmd.append(f'--output-path "{_format_variable_path(out_dir, font_name, axis)}"')
else:
cmd.append("--interpolate")
cmd.append(f'--output-dir "{out_dir}"')
proc = await asyncio.create_subprocess_shell(" ".join(cmd),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
_, stderr = await proc.communicate()
file_re = re.escape(out_dir) + r"/(.*)"
matches = re.findall(file_re, stderr.decode(), flags=re.MULTILINE)
files = []
for match in matches:
files.append(os.path.join(out_dir, match))
return fmt, list(set(files))
89 changes: 0 additions & 89 deletions scripts/liblilex/build/make.py

This file was deleted.

14 changes: 14 additions & 0 deletions scripts/liblilex/build/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Path helpers"""
import shutil


def which(cmd: str) -> str:
"""shutil.which that throws on None"""
result = shutil.which(cmd)
if result is None:
raise ValueError(f"""
Can't find {cmd}. Make sure you have venv configured with
`make configure` and activated. Alternatively, install {cmd}
externally and check by running `which {cmd}`.
""")
return result
Loading

0 comments on commit e28c9a0

Please sign in to comment.