diff --git a/core/tools/build_templates b/core/tools/build_templates index e075001019..594c879f22 100755 --- a/core/tools/build_templates +++ b/core/tools/build_templates @@ -4,8 +4,10 @@ set -e CWD=`dirname "$0"` RENDER="$CWD/../vendor/trezor-common/tools/cointool.py render -M T1B1" -# Search both in `core/src` and `core/embed` - Solana templates are excluded since those are handled separately -FIND_TEMPLATES="find $CWD/.. -name *.mako -not -name _proto* -not -path *solana*" +# Search both in `core/src` and `core/embed` +# - Solana templates are excluded since those are handled separately +# - `gen_font.mako` is excluded as it's used from `gen_font.py` +FIND_TEMPLATES="find $CWD/.. -name *.mako -not -name _proto* -not -path *solana* -not -name gen_font.mako" check_results() { CHECK_FAIL=0 diff --git a/core/tools/codegen/gen_font.mako b/core/tools/codegen/gen_font.mako new file mode 100644 index 0000000000..e0a159428f --- /dev/null +++ b/core/tools/codegen/gen_font.mako @@ -0,0 +1,59 @@ +//! This file is generated by core/tools/codegen/gen_font.py +#![cfg_attr(any(), rustfmt::skip)] +#![allow(non_upper_case_globals)] +// Each glyph: +// - first two bytes: width, height +// - next three bytes: advance, bearingX, bearingY +// - rest is packed ${bpp}-bit glyph data + +use crate::ui::display::font::FontInfo; + +% for g in glyphs: +/// '${g["char"]}' (ASCII ${g["ascii"]}) +const ${g["var_name"]}: [u8; ${g["arr_len"]}] = [ ${g["arr_content"]} ]; + +% endfor +/// Nonprintable glyph (inverse colors of '?') +const ${nonprintable["var_name"]}: [u8; ${nonprintable["arr_len"]}] = [ ${nonprintable["arr_content"]} ]; +% if gen_normal: + +/// Array of references for '${name}' normal ASCII glyphs +const Font_${name}: [&[u8]; ${len(glyph_array)}] = [ +% for ref in glyph_array: + ${ref}, +% endfor +]; +% endif +% if gen_upper: + +/// Array of references for '${name}' ASCII glyphs (forced uppercase) +const Font_${name}_upper: [&[u8]; ${len(glyph_array_upper)}] = [ +% for ref in glyph_array_upper: + ${ref}, +% endfor +]; +% endif +% if gen_normal: + +/// FontInfo struct for normal ASCII usage +pub const Font_${name}_info: FontInfo = FontInfo { + translation_blob_idx: ${font_info["translation_blob_idx"]}, + height: ${font_info["height"]}, + max_height: ${font_info["max_height"]}, + baseline: ${font_info["baseline"]}, + glyph_data: &${font_info["glyph_array"]}, + glyph_nonprintable: &${font_info["nonprintable"]}, +}; +% endif +% if gen_upper: + +/// FontInfo struct for forced uppercase usage +pub const Font_${name}_upper_info: FontInfo = FontInfo { + translation_blob_idx: ${font_info_upper["translation_blob_idx"]}, + height: ${font_info_upper["height"]}, + max_height: ${font_info_upper["max_height"]}, + baseline: ${font_info_upper["baseline"]}, + glyph_data: &${font_info_upper["glyph_array"]}, + glyph_nonprintable: &${font_info_upper["nonprintable"]}, +}; +% endif diff --git a/core/tools/codegen/gen_font.py b/core/tools/codegen/gen_font.py index 79a66925b2..49f8b6be2c 100755 --- a/core/tools/codegen/gen_font.py +++ b/core/tools/codegen/gen_font.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 -# script used to generate core/embed/gfx/fonts/font_*_*.c +# script used to generate +# 1) core/embed/gfx/fonts/font_*_*.c for `prodtest` and `bootloader_ci` +# 2) rust/src/ui/layout_*/fonts/font_*_*.rs for `bootloader` and `firmware` from __future__ import annotations @@ -13,6 +15,7 @@ import json # pip install freetype-py import freetype +from mako.template import Template from foreign_chars import all_languages @@ -24,9 +27,12 @@ def _normalize(s: str) -> str: HERE = Path(__file__).parent CORE_ROOT = HERE.parent.parent FONTS_DIR = HERE / "fonts" +RUST_MAKO_TMPL = HERE / "gen_font.mako" OUT_DIR = HERE / ".." / ".." / "embed" / "gfx" / "fonts" C_FONTS_DEST = CORE_ROOT / "embed" / "gfx" / "fonts" JSON_FONTS_DEST = CORE_ROOT / "translations" / "fonts" +RUST_FONTS_DEST = CORE_ROOT / "embed" / "rust" / "src" / "ui" +LAYOUT_NAME = "" MIN_GLYPH = ord(" ") MAX_GLYPH = ord("~") @@ -182,6 +188,7 @@ class Glyph: 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: @@ -275,6 +282,7 @@ class FaceProcessor: 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}" @@ -293,14 +301,27 @@ class FaceProcessor: def _h_file_name(self) -> Path: return C_FONTS_DEST / f"font_{self.fontname}.h" - def write_files(self) -> None: - self.write_c_files() + @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, gen_c: bool = False) -> None: + # C files: + if gen_c: + self.write_c_files() + # 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_c_files(self) -> None: self._write_c_file() @@ -468,19 +489,138 @@ class FaceProcessor: f"extern const font_info_t Font_{self._name_style_size}_upper_info;\n" ) + # -------------------------------------------------------------------- + # 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), + } + ) -def gen_layout_bolt(): - FaceProcessor("Roboto", "Regular", 20).write_files() - FaceProcessor("Roboto", "Bold", 20).write_files() - FaceProcessor("TTHoves", "Regular", 21, ext="otf").write_files() - FaceProcessor("TTHoves", "DemiBold", 21, ext="otf").write_files() + # 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: + font_info = { + "variant": "normal", + "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: + font_info_upper = { + "variant": "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(gen_c: bool = False): + global LAYOUT_NAME + LAYOUT_NAME = "bolt" + FaceProcessor("Roboto", "Regular", 20).write_files(gen_c) + FaceProcessor("Roboto", "Bold", 20).write_files(gen_c) + FaceProcessor("TTHoves", "Regular", 21, ext="otf").write_files(gen_c) + FaceProcessor("TTHoves", "DemiBold", 21, ext="otf").write_files(gen_c) FaceProcessor( "TTHoves", "Bold", 17, ext="otf", gen_normal=False, gen_upper=True - ).write_files() - FaceProcessor("RobotoMono", "Medium", 20).write_files() + ).write_files(gen_c) + FaceProcessor("RobotoMono", "Medium", 20).write_files(gen_c) -def gen_layout_caesar(): +def gen_layout_caesar(gen_c: bool = False): + global LAYOUT_NAME + LAYOUT_NAME = "caesar" FaceProcessor( "PixelOperator", "Regular", @@ -489,7 +629,7 @@ def gen_layout_caesar(): shaveX=1, gen_normal=True, gen_upper=True, - ).write_files() + ).write_files(gen_c) FaceProcessor( "PixelOperator", "Bold", @@ -498,18 +638,23 @@ def gen_layout_caesar(): shaveX=1, gen_normal=True, gen_upper=True, - ).write_files() - FaceProcessor("PixelOperatorMono", "Regular", 8, bpp=1, shaveX=1).write_files() - FaceProcessor("Unifont", "Regular", 16, bpp=1, shaveX=1, ext="otf").write_files() + ).write_files(gen_c) + FaceProcessor("PixelOperatorMono", "Regular", 8, bpp=1, shaveX=1).write_files(gen_c) + FaceProcessor("Unifont", "Regular", 16, bpp=1, shaveX=1, ext="otf").write_files( + gen_c + ) # NOTE: Unifont Bold does not seem to have czech characters - FaceProcessor("Unifont", "Bold", 16, bpp=1, shaveX=1, ext="otf").write_files() + FaceProcessor("Unifont", "Bold", 16, bpp=1, shaveX=1, ext="otf").write_files(gen_c) -def gen_layout_delizia(): - FaceProcessor("TTSatoshi", "DemiBold", 42, ext="otf").write_files() - FaceProcessor("TTSatoshi", "DemiBold", 21, ext="otf").write_files() - FaceProcessor("TTSatoshi", "DemiBold", 18, ext="otf").write_files() - FaceProcessor("RobotoMono", "Medium", 21).write_files() +def gen_layout_delizia(gen_c: bool = False): + global LAYOUT_NAME + LAYOUT_NAME = "delizia" + FaceProcessor("TTSatoshi", "DemiBold", 42, ext="otf").write_files(gen_c) + FaceProcessor("TTSatoshi", "DemiBold", 21, ext="otf").write_files(gen_c) + FaceProcessor("TTSatoshi", "DemiBold", 18, ext="otf").write_files(gen_c) + FaceProcessor("RobotoMono", "Medium", 21).write_files(gen_c) + LAYOUTS = { "bolt": gen_layout_bolt, @@ -517,6 +662,7 @@ LAYOUTS = { "delizia": gen_layout_delizia, } + @click.command() @click.option( "--layout", @@ -531,19 +677,25 @@ LAYOUTS = { default=False, help="Generate character width files", ) -def main(layout: str | None, write_widths: bool): +@click.option( + "--gen-c", + is_flag=True, + default=False, + help="Also generate C font files", +) +def main(layout: str | None, write_widths: bool, gen_c: 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]() + LAYOUTS[layout](gen_c) else: click.echo("Generating all fonts") for layout_name, layout_func in LAYOUTS.items(): click.echo(f"\nGenerating {layout_name} layout:") - layout_func() + layout_func(gen_c) if __name__ == "__main__":