mirror of
https://github.com/slavfox/Cozette.git
synced 2026-03-11 08:54:33 +00:00
The Regular and Bold fonts have different names, so programs treat them as different fonts. When using the font in a terminal, it cannot find a bold variant for the regular one, and does not render bold text as such. Set the same name for both fonts, and properly set the style of each one so that they both form part of the font family. Fixes: https://github.com/the-moonwitch/Cozette/issues/183
335 lines
10 KiB
Python
335 lines
10 KiB
Python
import argparse
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from pickle import BUILD
|
|
from shlex import quote
|
|
from shutil import rmtree
|
|
from typing import Optional, Sequence, cast
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
import crayons # type: ignore
|
|
|
|
from cozette_builder.changeloggen import get_changelog, get_last_ver
|
|
from cozette_builder.hidpi import double_size
|
|
from cozette_builder.imagegen import (
|
|
add_margins,
|
|
read_sample,
|
|
save_charlist,
|
|
save_sample,
|
|
)
|
|
from cozette_builder.scanner import (
|
|
find_missing_codepoints,
|
|
print_codepoints_for_changelog,
|
|
scan_for_codepoints,
|
|
)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent
|
|
BUILD_DIR = REPO_ROOT / "build"
|
|
FONTNAME = "Cozette"
|
|
SFDPATH = REPO_ROOT / "Cozette" / "Cozette.sfd"
|
|
|
|
|
|
@dataclass
|
|
class Generate:
|
|
filename: str
|
|
bitmap_fmt: Optional[str] = None
|
|
|
|
def __str__(self):
|
|
return (
|
|
f'Generate("{self.filename}", "{self.bitmap_fmt}")'
|
|
if self.bitmap_fmt
|
|
else f'Generate("{self.filename}")'
|
|
)
|
|
|
|
|
|
def save_images():
|
|
print(crayons.yellow("Saving character map"))
|
|
save_charlist(FONTNAME, SFDPATH, REPO_ROOT / "img")
|
|
|
|
print(crayons.yellow("Saving sample image"))
|
|
save_sample(
|
|
FONTNAME,
|
|
read_sample(REPO_ROOT / "img" / "sample.txt"),
|
|
REPO_ROOT / "img" / "sample.png",
|
|
)
|
|
add_margins(REPO_ROOT / "img" / "sample.png")
|
|
|
|
|
|
def fontforge(open: Path, generate: Sequence[Generate]):
|
|
BUILD_DIR.mkdir(exist_ok=True)
|
|
script = "; ".join(
|
|
[
|
|
f'Open("{open}")',
|
|
# 'RenameGlyphs("AGL with PUA")',
|
|
# 'Reencode("unicode")',
|
|
]
|
|
+ [str(gen) for gen in generate]
|
|
)
|
|
# No idea why this doesn't work without shell=True
|
|
subprocess.run(
|
|
[f"fontforge -lang ff -c {quote(script)}"], cwd=BUILD_DIR, shell=True
|
|
)
|
|
|
|
|
|
def rename_single(dir: Path, pattern: str, newname: str) -> Path:
|
|
return cast(Path, next(dir.glob(pattern)).rename(dir / newname))
|
|
|
|
|
|
def gen_bitmap_formats(sfd_path: Path, prefix: str) -> Path:
|
|
fontforge(
|
|
open=sfd_path,
|
|
generate=[
|
|
Generate(f"{prefix}.", bitmap_fmt="bdf"),
|
|
],
|
|
)
|
|
bdf_path = rename_single(BUILD_DIR, f"{prefix}-*.bdf", f"{prefix}.bdf")
|
|
fontforge(
|
|
open=sfd_path,
|
|
generate=[
|
|
Generate(f"{prefix}.", "otb"),
|
|
Generate(f"{prefix}.", "psf"),
|
|
Generate(f"{prefix}.", "fnt"),
|
|
Generate(f"{prefix}.dfont", "sbit"),
|
|
],
|
|
)
|
|
rename_single(BUILD_DIR, f"{prefix}-*.fnt", f"{prefix}.fnt")
|
|
return bdf_path
|
|
|
|
|
|
def fix_ttf(ttfpath: Path, name: str):
|
|
print(crayons.yellow(f"Generating TTF for {name}..."))
|
|
version = "1.0"
|
|
with SFDPATH.open() as f:
|
|
for line in f.readlines():
|
|
if line.startswith("Version "):
|
|
version = line.split()[1]
|
|
break
|
|
|
|
if name.endswith("Bold"):
|
|
family_name = name.removesuffix("Bold")
|
|
style_name = "Bold"
|
|
weight = 700
|
|
else:
|
|
family_name = name
|
|
style_name = "Regular"
|
|
weight = 400
|
|
|
|
with NamedTemporaryFile() as sfd:
|
|
subprocess.run(
|
|
[
|
|
f"fontforge -c '"
|
|
f'f = open("{ttfpath}"); '
|
|
f"f.os2_version = 4; "
|
|
f"f.os2_weight_width_slope_only = True; "
|
|
f'f.save("{sfd.name}")\''
|
|
],
|
|
cwd=BUILD_DIR,
|
|
shell=True,
|
|
check=True,
|
|
)
|
|
script = ";\n".join(
|
|
[
|
|
f'Open("{sfd.name}")',
|
|
"SelectWorthOutputting()",
|
|
"RemoveOverlap()",
|
|
"CorrectDirection()",
|
|
"ScaleToEm(2048)",
|
|
'RenameGlyphs("AGL with PUA")',
|
|
'Reencode("unicode")',
|
|
f'SetTTFName(0x409, 1, "{family_name}")',
|
|
f'SetTTFName(0x409, 2, "{style_name}")',
|
|
f'SetTTFName(0x409, 3, "{family_name} {style_name}")', # Unique font identifier
|
|
f'SetTTFName(0x409, 4, "{family_name} {style_name}")', # Full font name
|
|
f'SetTTFName(0x409, 5, "{version}")',
|
|
f'SetTTFName(0x409, 6, "{family_name}-{style_name}")', # PostScript name
|
|
f'SetTTFName(0x409, 8, "Ines <ines@moonwit.ch>")',
|
|
f'SetTTFName(0x409, 9, "Ines <ines@moonwit.ch>")',
|
|
f'SetTTFName(0x409, 11, "https://github.com/the-moonwitch/Cozette")',
|
|
f'SetTTFName(0x409, 13, LoadStringFromFile({repr(str((REPO_ROOT / "LICENSE").resolve()))}))',
|
|
'SetTTFName(0x409, 14, "https://github.com/the-moonwitch/Cozette/blob/master/LICENSE")',
|
|
f'SetOS2Value("Weight", {weight})',
|
|
f'Generate("{name}.dfont")',
|
|
f'Generate("{name}.otf")',
|
|
f'Generate("{name}.ttf")',
|
|
f'Generate("{name}.woff")',
|
|
f'Generate("{name}.woff2")',
|
|
]
|
|
)
|
|
with NamedTemporaryFile(mode="w+", suffix=".pe") as f:
|
|
print(f.name)
|
|
f.write(script)
|
|
f.flush()
|
|
f.seek(0)
|
|
subprocess.run(
|
|
[f"fontforge -script {f.name}"],
|
|
cwd=BUILD_DIR,
|
|
shell=True,
|
|
check=True,
|
|
)
|
|
# No idea why this doesn't work without shell=True
|
|
ttfpath.unlink()
|
|
|
|
|
|
def make_hidpi(bdf_path: Path, out_path: Path):
|
|
print(crayons.yellow("Generating hidpi font..."))
|
|
with bdf_path.open() as i:
|
|
with out_path.open("w") as o:
|
|
double_size(i, o)
|
|
print(crayons.green("Done!"))
|
|
|
|
|
|
BDF_CODEPOINT_RE = re.compile(r"BDFChar: \S+ (\d+)")
|
|
|
|
|
|
def variant(
|
|
sfd_path: Path, variant_name: str, variant_source: Path
|
|
) -> tuple[Path, str]:
|
|
outpath = BUILD_DIR / f"{variant_name}.sfd"
|
|
replacements = {}
|
|
with variant_source.open() as f:
|
|
for line in f:
|
|
if codepoint := BDF_CODEPOINT_RE.match(line):
|
|
replacements[int(codepoint.group(1))] = next(f)
|
|
|
|
with outpath.open("w") as f:
|
|
with sfd_path.open() as src:
|
|
replacement = None
|
|
for line in src:
|
|
if replacement is not None:
|
|
f.write(replacement)
|
|
replacement = None
|
|
elif line.startswith("FAMILY_NAME"):
|
|
f.write(line)
|
|
else:
|
|
f.write(line.replace("Cozette", variant_name))
|
|
if codepoint := BDF_CODEPOINT_RE.match(line):
|
|
if int(codepoint.group(1)) in replacements:
|
|
replacement = replacements[int(codepoint.group(1))]
|
|
|
|
return (outpath, variant_name)
|
|
|
|
|
|
def gen_versions(bdf_path: Path, font_name: str, filename_prefix: str):
|
|
hidpi_path = BUILD_DIR / f"{filename_prefix}_hidpi.bdf"
|
|
|
|
def bnp_invoc_ttf(name: str, format: str):
|
|
return [
|
|
REPO_ROOT / "bitsnpicas.sh",
|
|
"convertbitmap",
|
|
"-f",
|
|
format,
|
|
"-o",
|
|
BUILD_DIR / f"{name}_tmp.{format}",
|
|
"-s",
|
|
"Cozette",
|
|
"-r",
|
|
name,
|
|
"-T",
|
|
]
|
|
|
|
subprocess.run(
|
|
[
|
|
BUILD_DIR.parent / "bitsnpicas.sh",
|
|
"convertbitmap",
|
|
"-f",
|
|
"psf",
|
|
"-o",
|
|
BUILD_DIR / f"{filename_prefix}.psf",
|
|
bdf_path,
|
|
],
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
bnp_invoc_ttf(f"{font_name}Vector", "ttf") + [bdf_path], check=True
|
|
)
|
|
subprocess.run(
|
|
bnp_invoc_ttf(f"{font_name}VectorBold", "ttf") + ["-b", bdf_path],
|
|
check=True,
|
|
)
|
|
print(crayons.yellow("Fixing TTF variants..."))
|
|
fix_ttf(BUILD_DIR / f"{font_name}Vector_tmp.ttf", f"{font_name}Vector")
|
|
fix_ttf(
|
|
BUILD_DIR / f"{font_name}VectorBold_tmp.ttf", f"{font_name}VectorBold"
|
|
)
|
|
print(crayons.green("Done!"))
|
|
make_hidpi(bdf_path, hidpi_path)
|
|
fontforge(
|
|
open=hidpi_path,
|
|
generate=[
|
|
Generate(f"{hidpi_path.stem}.", "otb"),
|
|
Generate(f"{hidpi_path.stem}.", "fnt"),
|
|
Generate(f"{hidpi_path.stem}.dfont", "sbit"),
|
|
],
|
|
)
|
|
rename_single(BUILD_DIR, "*-26.fnt", f"{filename_prefix}_hidpi.fnt")
|
|
subprocess.run(
|
|
[
|
|
BUILD_DIR.parent / "bitsnpicas.sh",
|
|
"convertbitmap",
|
|
"-f",
|
|
"psf",
|
|
"-o",
|
|
hidpi_path.with_suffix(".psf"),
|
|
hidpi_path,
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(dest="action")
|
|
images = subparsers.add_parser("images")
|
|
changelog = subparsers.add_parser("changelog")
|
|
fonts = subparsers.add_parser("fonts")
|
|
scan = subparsers.add_parser("scan")
|
|
# noinspection PyTypeChecker
|
|
scan.add_argument("path", type=Path) # type: ignore
|
|
scan.add_argument("-s", "--print-source-files", action="store_true")
|
|
scan.add_argument("-r", "--reverse", action="store_true")
|
|
args = parser.parse_args()
|
|
if args.action == "scan":
|
|
missing_codepoints = find_missing_codepoints(
|
|
SFDPATH,
|
|
scan_for_codepoints(args.path),
|
|
)
|
|
if missing_codepoints:
|
|
print_codepoints_for_changelog(
|
|
missing_codepoints,
|
|
print_source_files=args.print_source_files,
|
|
reverse=args.reverse,
|
|
)
|
|
else:
|
|
print(
|
|
crayons.green(
|
|
f"All codepoints under {args.path} already "
|
|
f"supported by Cozette."
|
|
)
|
|
)
|
|
if args.action == "images":
|
|
print(crayons.blue("Saving sample images..."))
|
|
save_images()
|
|
print(crayons.green("Done!", bold=True))
|
|
elif args.action == "fonts":
|
|
rmtree(BUILD_DIR, ignore_errors=True)
|
|
BUILD_DIR.mkdir(exist_ok=True)
|
|
for sfd_path, font_name in (
|
|
(SFDPATH, "Cozette"),
|
|
variant(
|
|
SFDPATH,
|
|
"CozetteCrossedSeven",
|
|
SFDPATH.parent / "variants" / "CozetteCrossedSeven.sfd",
|
|
),
|
|
):
|
|
print(crayons.blue(f"Building bitmap formats for {font_name}..."))
|
|
bdf_path = gen_bitmap_formats(sfd_path, font_name.lower())
|
|
print(crayons.green("Done!", bold=True))
|
|
print(crayons.blue(f"Building versions for {font_name}..."))
|
|
gen_versions(bdf_path, font_name, font_name.lower())
|
|
print(crayons.green("Done!", bold=True))
|
|
elif args.action == "changelog":
|
|
get_changelog()
|
|
else:
|
|
parser.print_usage()
|