1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-18 18:42:06 +00:00
trezor-firmware/core/tools/codegen/gen_font.py

584 lines
18 KiB
Python
Raw Permalink Normal View History

2016-03-26 22:46:18 +00:00
#!/usr/bin/env python3
# script used to generate FontInfo in `rust/src/ui/layout_*/fonts/font_*_*.rs`
2023-05-04 12:28:20 +00:00
from __future__ import annotations
import unicodedata
from dataclasses import dataclass
2023-05-04 12:28:20 +00:00
from pathlib import Path
import click
import json
# pip install freetype-py
2016-03-26 22:46:18 +00:00
import freetype
from mako.template import Template
2016-03-26 22:46:18 +00:00
from foreign_chars import all_languages
def _normalize(s: str) -> str:
return unicodedata.normalize("NFKC", s)
2023-05-04 12:28:20 +00:00
HERE = Path(__file__).parent
CORE_ROOT = HERE.parent.parent
2023-05-04 12:28:20 +00:00
FONTS_DIR = HERE / "fonts"
RUST_MAKO_TMPL = HERE / "gen_font.mako"
JSON_FONTS_DEST = CORE_ROOT / "translations" / "fonts"
RUST_FONTS_DEST = CORE_ROOT / "embed" / "rust" / "src" / "ui"
LAYOUT_NAME = ""
2023-05-04 12:28:20 +00:00
2018-07-31 09:35:09 +00:00
MIN_GLYPH = ord(" ")
MAX_GLYPH = ord("~")
2016-03-26 22:46:18 +00:00
WRITE_WIDTHS = False
# characters for which bearingX is negative, but we choose to make it zero and modify
# advance instead
MODIFY_BEARING_X = [
_normalize(c)
for c in (
"Ä",
"À",
"Â",
"Ã",
"Æ",
"Î",
"Ï",
"Ì",
"î",
"ï",
"ì",
"ÿ",
"Ý",
"Ÿ",
"Á",
"ý",
"A",
"X",
"Y",
"j",
"x",
"y",
"}",
")",
",",
"/",
"_",
)
]
2016-10-18 13:05:55 +00:00
# metrics explanation: https://www.freetype.org/freetype2/docs/glyphs/metrics.png
2017-06-13 14:50:03 +00:00
def process_bitmap_buffer(
buf: list[int], bpp: int, width: int, height: int
) -> list[int]:
res = buf[:]
if bpp == 1:
if len(res) % 8 != 0:
# add padding if needed
for _ in range(8 - len(res) % 8):
res.append(0)
res = [
(
(a & 0x80)
| ((b & 0x80) >> 1)
| ((c & 0x80) >> 2)
| ((d & 0x80) >> 3)
| ((e & 0x80) >> 4)
| ((f & 0x80) >> 5)
| ((g & 0x80) >> 6)
| ((h & 0x80) >> 7)
)
for a, b, c, d, e, f, g, h in [
res[i : i + 8] for i in range(0, len(res), 8)
]
]
elif bpp == 2:
if len(res) % 4 != 0:
# add padding if needed
for _ in range(4 - len(res) % 4):
res.append(0)
res = [
((a & 0xC0) | ((b & 0xC0) >> 2) | ((c & 0xC0) >> 4) | ((d & 0xC0) >> 6))
for a, b, c, d in [res[i : i + 4] for i in range(0, len(res), 4)]
]
elif bpp == 4:
res: list[int] = []
for y in range(0, height):
row = buf[y * width : (y + 1) * width]
for a, b in zip(row[::2], row[1::2]):
res.append(((b & 0xF0) | (a >> 4)))
if width & 1 != 0:
res.append(row[-1] >> 4)
elif bpp == 8:
pass
else:
raise ValueError
return res
2023-05-04 12:28:20 +00:00
def drop_left_columns(buf: list[int], width: int, drop: int) -> list[int]:
res: list[int] = []
for i in range(len(buf)):
if i % width >= drop:
res.append(buf[i])
return res
@dataclass
class Glyph:
char: str
width: int
rows: int
advance: int
bearingX: int
bearingY: int
buf: list[int]
num_grays: int
inverse_colors: bool = False
@classmethod
def from_face(
cls, face: freetype.Face, c: str, shaveX: int, inverse_colors: bool = False
) -> Glyph:
assert len(c) == 1
bitmap = face.glyph.bitmap
metrics = face.glyph.metrics
assert metrics.width // 64 == bitmap.width
assert metrics.height // 64 == bitmap.rows
assert metrics.width % 64 == 0
assert metrics.height % 64 == 0
assert metrics.horiAdvance % 64 == 0
assert metrics.horiBearingX % 64 == 0
assert metrics.horiBearingY % 64 == 0
assert bitmap.width == bitmap.pitch
assert len(bitmap.buffer) == bitmap.pitch * bitmap.rows
width = bitmap.width
rows = bitmap.rows
advance = metrics.horiAdvance // 64
bearingX = metrics.horiBearingX // 64
remove_left = shaveX
# discard space on the left side
if shaveX > 0:
diff = min(advance, bearingX, shaveX)
advance -= diff
bearingX -= diff
remove_left -= diff
# the following code is here just for some letters (listed at start)
# not using negative bearingX makes life so much easier; add it to advance instead
if bearingX < 0:
if c in MODIFY_BEARING_X:
advance += -bearingX
bearingX = 0
else:
raise ValueError(f"Negative bearingX for character '{c}'")
bearingY = metrics.horiBearingY // 64
assert advance >= 0 and advance <= 255
assert bearingX >= 0 and bearingX <= 255
if bearingY < 0: # HACK
print(f"normalizing bearingY {bearingY} for '{c}'")
bearingY = 0
assert bearingY >= 0 and bearingY <= 255
buf = list(bitmap.buffer)
# discard non-space pixels on the left side
if remove_left > 0 and width > 0:
assert bearingX == 0
buf = drop_left_columns(buf, width, remove_left)
assert width > remove_left
width -= remove_left
assert advance > remove_left
advance -= remove_left
print(f'Glyph "{c}": removed {remove_left} pixel columns from the left')
return Glyph(
char=c,
width=width,
rows=rows,
advance=advance,
bearingX=bearingX,
bearingY=bearingY,
buf=buf,
num_grays=bitmap.num_grays,
inverse_colors=inverse_colors,
)
def print_metrics(self) -> None:
print(
f'Loaded glyph "{self.char}" ... {self.width} x {self.rows} @ {self.num_grays} grays ({len(self.buf)} bytes, metrics: {self.advance}, {self.bearingX}, {self.bearingY})'
)
def process_byte(self, b: int) -> int:
if self.inverse_colors:
return b ^ 0xFF
else:
return b
def to_bytes(self, bpp: int) -> bytes:
infos = [
self.width,
self.rows,
self.advance,
self.bearingX,
self.bearingY,
]
if self.buf:
data = [
self.process_byte(x)
for x in process_bitmap_buffer(self.buf, bpp, self.width, self.rows)
]
return bytes(infos + data)
else:
return bytes(infos)
class FaceProcessor:
def __init__(
self,
name: str,
style: str,
size: int,
bpp: int = 4,
shaveX: int = 0,
ext: str = "ttf",
gen_normal: bool = True, # generate font with all the letters
gen_upper: bool = False, # generate font with only upper-cased letters
font_idx: int | None = None, # idx to UTF-8 foreign chars data
font_idx_upper: int | None = None, # idx to UTF-8 upper-cased foreign chars
):
if gen_normal is False and gen_upper is False:
raise ValueError(
"At least one must be selected from normal glyphs or only uppercased glyphs."
)
print(f"Processing ... {name} {style} {size}")
self.name = name
self.style = style
self.size = size
self.font_idx = font_idx
self.font_idx_upper = font_idx_upper
self.bpp = bpp
self.shaveX = shaveX
self.ext = ext
self.gen_normal = gen_normal
self.gen_upper = gen_upper
self.face = freetype.Face(str(FONTS_DIR / f"{name}-{style}.{ext}"))
self.face.set_pixel_sizes(0, size) # type: ignore
self.fontname = f"{name.lower()}_{style.lower()}_{size}"
self.font_ymin = 0
self.font_ymax = 0
@property
def _name_style_size(self) -> str:
return f"{self.name}_{self.style}_{self.size}"
@property
def _rs_file_name(self) -> Path:
return (
RUST_FONTS_DEST
/ f"layout_{LAYOUT_NAME}"
/ "fonts"
/ f"font_{self.fontname}.rs"
)
def write_files(self) -> None:
# JSON files:
if self.gen_normal:
self.write_foreign_json(upper_cased=False)
if self.gen_upper:
self.write_foreign_json(upper_cased=True)
if WRITE_WIDTHS:
self.write_char_widths_files()
self.write_rust_file()
def write_foreign_json(self, upper_cased=False) -> None:
for lang, language_chars in all_languages.items():
fontdata = {}
for item in language_chars:
c = _normalize(item)
map_from = c
if c.islower() and upper_cased and c != "ß":
# FIXME not sure how to properly handle the german "ß"
c = c.upper()
assert len(c) == 1
assert len(map_from) == 1
self._load_char(c)
glyph = Glyph.from_face(self.face, c, self.shaveX)
glyph.print_metrics()
fontdata[map_from] = glyph.to_bytes(self.bpp).hex()
file = (
JSON_FONTS_DEST
/ f"font_{self.fontname}{'_upper' if upper_cased else ''}_{lang}.json"
)
json_content = json.dumps(fontdata, indent=2, ensure_ascii=False)
file.write_text(json_content + "\n")
def write_char_widths_files(self) -> None:
chars: set[str] = set()
widths: dict[str, int] = {}
# "normal" ASCII characters
for i in range(MIN_GLYPH, MAX_GLYPH + 1):
c = chr(i)
if c.islower() and not self.gen_normal:
c = c.upper()
chars.add(c)
# foreign language data
for _lang, lang_chars in all_languages.items():
for c in lang_chars:
chars.add(c)
for c in sorted(chars):
self._load_char(c)
glyph = Glyph.from_face(self.face, c, self.shaveX)
widths[c] = glyph.advance
filename = f"font_widths_{self.fontname}.json"
with open(filename, "w", encoding="utf-8") as f:
json_content = json.dumps(widths, indent=2, ensure_ascii=False)
f.write(json_content + "\n")
def _load_char(self, c: str) -> None:
self.face.load_char(c, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_NORMAL) # type: ignore
# --------------------------------------------------------------------
# Rust code generation
# --------------------------------------------------------------------
def write_rust_file(self) -> None:
"""
Write a Rust source file using a Mako template.
"""
# Build a dict with all data needed by the template.
# 1) Gather ASCII glyph definitions.
glyphs = []
for i in range(MIN_GLYPH, MAX_GLYPH + 1):
c = chr(i)
if c.islower() and not self.gen_normal:
continue
self._load_char(c)
glyph = Glyph.from_face(self.face, c, self.shaveX)
arr_bytes = glyph.to_bytes(self.bpp)
glyphs.append(
{
"ascii": i,
"char": glyph.char,
"var_name": f"Font_{self._name_style_size}_glyph_{i}",
"arr_len": len(arr_bytes),
"arr_content": ", ".join(str(n) for n in arr_bytes),
}
)
# 2) Nonprintable glyph.
self._load_char("?")
glyph_np = Glyph.from_face(self.face, "?", self.shaveX, inverse_colors=True)
arr_bytes_np = glyph_np.to_bytes(self.bpp)
nonprintable = {
"var_name": f"Font_{self._name_style_size}_glyph_nonprintable",
"arr_len": len(arr_bytes_np),
"arr_content": ", ".join(str(n) for n in arr_bytes_np),
}
# 3) Build arrays of glyph references.
glyph_array = []
glyph_array_upper = []
if self.gen_normal:
glyph_array = [
f"&Font_{self._name_style_size}_glyph_{i}"
for i in range(MIN_GLYPH, MAX_GLYPH + 1)
]
if self.gen_upper:
for i in range(MIN_GLYPH, MAX_GLYPH + 1):
if chr(i).islower():
c_to = chr(i).upper()
i_mapped = ord(c_to)
glyph_array_upper.append(
f"&Font_{self._name_style_size}_glyph_{i_mapped}, // {chr(i)} -> {c_to}"
)
else:
glyph_array_upper.append(f"&Font_{self._name_style_size}_glyph_{i}")
# 4) Recompute font_ymin and font_ymax.
self.font_ymin = 0
self.font_ymax = 0
for i in range(MIN_GLYPH, MAX_GLYPH + 1):
c = chr(i)
if c.islower() and not self.gen_normal:
continue
self._load_char(c)
glyph = Glyph.from_face(self.face, c, self.shaveX)
yMin = glyph.bearingY - glyph.rows
yMax = yMin + glyph.rows
self.font_ymin = min(self.font_ymin, yMin)
self.font_ymax = max(self.font_ymax, yMax)
# 5) Build FontInfo definitions.
font_info = None
font_info_upper = None
if self.gen_normal:
if self.font_idx is None:
raise ValueError(
f"font_idx must be set when generating FontInfo for {self._name_style_size}"
)
font_info = {
"variant": "normal",
"translation_blob_idx": self.font_idx,
"height": self.size,
"max_height": self.font_ymax - self.font_ymin,
"baseline": -self.font_ymin,
"glyph_array": f"Font_{self._name_style_size}",
"nonprintable": f"Font_{self._name_style_size}_glyph_nonprintable",
}
if self.gen_upper:
if self.font_idx_upper is None:
raise ValueError(
f"font_idx_upper must be set when generating `only_upper` FontInfo for {self._name_style_size}"
)
font_info_upper = {
"variant": "upper",
"translation_blob_idx": self.font_idx_upper,
"height": self.size,
"max_height": self.font_ymax - self.font_ymin,
"baseline": -self.font_ymin,
"glyph_array": f"Font_{self._name_style_size}_upper",
"nonprintable": f"Font_{self._name_style_size}_glyph_nonprintable",
}
data = {
"bpp": self.bpp,
"name": self._name_style_size,
"glyphs": glyphs,
"nonprintable": nonprintable,
"glyph_array": glyph_array,
"glyph_array_upper": glyph_array_upper,
"gen_normal": self.gen_normal,
"gen_upper": self.gen_upper,
"font_info": font_info,
"font_info_upper": font_info_upper,
}
# Load the Mako template from the same directory.
with open(RUST_MAKO_TMPL, "r") as f:
template_content = f.read()
template = Template(template_content)
rendered = template.render(**data)
# Write the rendered template into the Rust file.
with open(self._rs_file_name, "wt") as f:
f.write(rendered)
def gen_layout_bolt():
global LAYOUT_NAME
LAYOUT_NAME = "bolt"
FaceProcessor("TTHoves", "Regular", 21, ext="otf", font_idx=1).write_files()
FaceProcessor("TTHoves", "DemiBold", 21, ext="otf", font_idx=5).write_files()
FaceProcessor(
"TTHoves",
"Bold",
17,
ext="otf",
gen_normal=False,
gen_upper=True,
font_idx_upper=7,
).write_files()
FaceProcessor("RobotoMono", "Medium", 20, font_idx=3).write_files()
def gen_layout_caesar():
global LAYOUT_NAME
LAYOUT_NAME = "caesar"
FaceProcessor(
"PixelOperator",
"Regular",
8,
bpp=1,
shaveX=1,
gen_normal=True,
gen_upper=True,
font_idx=1,
font_idx_upper=6,
).write_files()
FaceProcessor(
"PixelOperator",
"Bold",
8,
bpp=1,
shaveX=1,
gen_normal=True,
gen_upper=True,
font_idx=2,
font_idx_upper=7,
).write_files()
FaceProcessor(
"PixelOperatorMono", "Regular", 8, bpp=1, shaveX=1, font_idx=3
).write_files()
FaceProcessor(
"Unifont", "Regular", 16, bpp=1, shaveX=1, ext="otf", font_idx=4
).write_files()
# NOTE: Unifont Bold does not seem to have czech characters
FaceProcessor(
"Unifont", "Bold", 16, bpp=1, shaveX=1, ext="otf", font_idx=5
).write_files()
def gen_layout_delizia():
global LAYOUT_NAME
LAYOUT_NAME = "delizia"
# FIXME: BIG font id not needed
FaceProcessor("TTSatoshi", "DemiBold", 42, ext="otf", font_idx=1).write_files()
FaceProcessor("TTSatoshi", "DemiBold", 21, ext="otf", font_idx=1).write_files()
FaceProcessor("TTSatoshi", "DemiBold", 18, ext="otf", font_idx=8).write_files()
FaceProcessor("RobotoMono", "Medium", 21, font_idx=3).write_files()
FaceProcessor(
"TTHoves",
"Bold",
17,
ext="otf",
gen_normal=False,
gen_upper=True,
font_idx_upper=7,
).write_files()
LAYOUTS = {
"bolt": gen_layout_bolt,
"caesar": gen_layout_caesar,
"delizia": gen_layout_delizia,
}
@click.command()
@click.option(
"--layout",
"-l",
help="Generate fonts only for specified layout",
type=click.Choice(list(LAYOUTS.keys())),
)
@click.option(
"--write-widths",
"-w",
is_flag=True,
default=False,
help="Generate character width files",
)
def main(layout: str | None, write_widths: bool):
"""Generate font files for Trezor firmware."""
global WRITE_WIDTHS
WRITE_WIDTHS = write_widths
if layout:
click.echo(f"Generating fonts for layout: {layout}")
LAYOUTS[layout]()
else:
click.echo("Generating all fonts")
for layout_name, layout_func in LAYOUTS.items():
click.echo(f"\nGenerating {layout_name} layout:")
layout_func()
if __name__ == "__main__":
main()