maple-font/build.py
2024-03-08 14:44:57 +08:00

261 lines
7.7 KiB
Python

from enum import Enum, unique
import hashlib
import importlib.util
import json
import platform
import shutil
import subprocess
from os import environ, listdir, makedirs, path, remove, walk
import sys
from urllib.request import urlopen
from zipfile import ZIP_DEFLATED, ZipFile
from fontTools.ttLib import TTFont
@unique
class Status(Enum):
DISABLE = "0"
ENABLE = "1"
IGNORE = "2"
# whether to archieve fonts
release_mode = True
# whether to build nerd font
build_nerd_font = True
build_config = {
"family_name": "Maple Mono",
"nerd_font": {
# whether to make icon width fixed
"mono": Status.DISABLE,
# whether to generate Nerd Font patch based on hinted ttf
"use_hinted": Status.ENABLE,
},
}
package_name = "foundryToolsCLI"
package_installed = importlib.util.find_spec(package_name) is not None
family_name = build_config["family_name"]
family_name_compact = family_name.replace(" ", "")
if not package_installed:
print(f"{package_name} is not found. Please run `pip install foundrytools-cli`")
exit(1)
# run command
def run(cli: str | list[str], extra_args: list[str] = []) -> None:
subprocess.run((cli.split(" ") if isinstance(cli, str) else cli) + extra_args)
# compress folder and return sha1
def compress_folder(source_file_or_dir_path: str, target_parent_dir_path: str) -> str:
source_folder_name = path.basename(source_file_or_dir_path)
zip_path = path.join(
target_parent_dir_path, f"{family_name_compact}-{source_folder_name}.zip"
)
with ZipFile(zip_path, "w", compression=ZIP_DEFLATED, compresslevel=5) as zip_file:
for root, _, files in walk(source_file_or_dir_path):
for file in files:
file_path = path.join(root, file)
zip_file.write(
file_path, path.relpath(file_path, source_file_or_dir_path)
)
zip_file.close()
sha1 = hashlib.sha1()
with open(zip_path, "rb") as zip_file:
while True:
data = zip_file.read(1024)
if not data:
break
sha1.update(data)
return sha1.hexdigest()
def check_font_patcher():
if path.exists("FontPatcher"):
return
version = "3.1.1"
url = f"https://github.com/ryanoasis/nerd-fonts/releases/download/v{version}/FontPatcher.zip"
try:
zip_path = "FontPatcher.zip"
if not path.exists(zip_path):
print(f"NerdFont Patcher does not exist, download from {url}")
with urlopen(url) as response, open(zip_path, "wb") as out_file:
shutil.copyfileobj(response, out_file)
with ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall("FontPatcher")
remove(zip_path)
except Exception as e:
print(
f"\nFail to get NerdFont Patcher. Please download it manually from {url}, then put downloaded 'FontPatcher.zip' into project's root and run this script again. \n\tError: {e}"
)
exit(1)
input_files = [
"src-font/MapleMono[wght]-VF.ttf",
"src-font/MapleMono-Italic[wght]-VF.ttf",
]
output_dir = "fonts"
output_otf = path.join(output_dir, "otf")
output_ttf = path.join(output_dir, "ttf")
output_ttf_autohint = path.join(output_dir, "ttf-autohint")
output_variable = path.join(output_dir, "variable")
output_woff2 = path.join(output_dir, "woff2")
output_nf = path.join(output_dir, "nf")
shutil.rmtree(output_dir, ignore_errors=True)
shutil.rmtree("woff2", ignore_errors=True)
makedirs(output_dir)
makedirs(output_variable)
print("=== [build start] ===")
conf = json.dumps(
build_config,
default=lambda x: x.name if isinstance(x, Status) else None,
indent=4,
)
print(conf)
for input_file in input_files:
shutil.copy(input_file, output_variable)
run(f"ftcli converter vf2i {input_file} -out {output_ttf}")
# fix font name
for f in listdir(output_ttf):
_path = path.join(output_ttf, f)
font = TTFont(_path)
def set_name(name: str, id: int):
font["name"].setName(name, nameID=id, platformID=1, platEncID=0, langID=0x0)
font["name"].setName(name, nameID=id, platformID=3, platEncID=1, langID=0x409)
def del_name(id: int):
font["name"].removeNames(nameID=id)
style_name = f[10:-4]
if style_name.endswith("Italic") and style_name[0] != "I":
style_name = style_name[:-6] + " Italic"
set_name(family_name, 1)
set_name(style_name, 2)
set_name(f"{family_name} {style_name}", 4)
set_name(f"{family_name_compact}-{f[10:-4]}", 6)
del_name(16)
del_name(17)
font.save(_path)
font.close()
run(f"ftcli converter ttf2otf {output_ttf} -out {output_otf}")
run(f"ftcli converter ft2wf {output_ttf} -out {output_woff2} -f woff2")
run(f"ftcli ttf autohint {output_ttf} -out {output_ttf_autohint}")
if build_nerd_font:
system = platform.uname()[0]
check_font_patcher()
# get fontforge python executable path
ffpy = ""
if "Windows" in system:
ffpy = path.join(
environ.get("ProgramFiles(x86)"), "FontForgeBuilds\\bin\\ffpython.exe"
)
elif "Darwin" in system:
ffpy = "/Applications/FontForge.app/Contents/MacOS/FFPython"
if ffpy == "" or not path.exists(ffpy):
ffpy = sys.executable
font_dir = (
output_ttf_autohint
if build_config["nerd_font"]["use_hinted"] == Status.ENABLE
else output_ttf
)
# full args: https://github.com/ryanoasis/nerd-fonts#font-patcher
nf_args = [
ffpy,
"FontPatcher/font-patcher",
"-l",
"-c",
"--careful",
"--outputdir",
output_nf,
]
if build_config["nerd_font"]["mono"] == Status.ENABLE:
nf_args += ["--mono"]
print(f"FontPatcher args: {nf_args}")
for f in listdir(output_ttf):
print(f"generate NerdFont for {f}")
run(nf_args + [path.join(font_dir, f)])
# format font name
nf_file_name = "NerdFont"
if build_config["nerd_font"]["mono"] == Status.ENABLE:
nf_file_name += "Mono"
_path = path.join(output_nf, f.replace("-", f"{nf_file_name}-"))
nf_font = TTFont(_path)
def set_nf_name(name: str, id: int):
nf_font["name"].setName(
name, nameID=id, platformID=1, platEncID=0, langID=0x0
)
nf_font["name"].setName(
name, nameID=id, platformID=3, platEncID=1, langID=0x409
)
def del_nf_name(id: int):
nf_font["name"].removeNames(nameID=id)
style_name = f[10:-4]
if style_name.endswith("Italic") and style_name[0] != "I":
style_name = style_name[:-6] + " Italic"
set_nf_name(f"{family_name} NF", 1)
set_nf_name(style_name, 2)
set_nf_name(f"{family_name} NF {style_name}", 4)
set_nf_name(f"{family_name_compact}-NF-{f[10:-4]}", 6)
del_nf_name(16)
del_nf_name(17)
nf_font.save(_path)
nf_font.close()
# rename file name
shutil.move(_path, path.join(output_nf, f.replace("-", "-NF-")))
# write config to output path
with open(path.join(output_dir, "build-config.json"), "w") as config_file:
config_file.write(conf)
if release_mode:
print("=== [Release Mode] ===")
# archieve fonts
release_dir = path.join(output_dir, "release")
makedirs(release_dir)
hash_map = {}
for f in listdir(output_dir):
if f == "release" or f.endswith(".json"):
continue
hash_map[f] = compress_folder(path.join(output_dir, f), release_dir)
print(f"archieve: {f}")
# write sha1
with open(path.join(release_dir, "sha1.json"), "w") as hash_file:
hash_file.write(json.dumps(hash_map, indent=4))
shutil.rmtree("woff2", ignore_errors=True)
shutil.copytree(output_woff2, "woff2")
print("copy woff2 to root")