mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-07 15:18:08 +00:00

WIP - refactor and extend font generation for non-ascii characters WIP - add czech characters mapping between UTF8 value and index WIP - regenerate font files with czech characters WIP - shorten czech button text, it was causing SHUTDOWN for some reason WIP - support UTF8 characters in fonts.c WIP - account for translation in tests WIP - small fixes WIP - fix last test WIP - support UTF8 also in Rust font operations WIP - add a script to find non-translated english strings in micropython code WIP - add a validator script for checking missing micropython translations WIP - translate remaining altcoins and other apps in core (fido, sdcard, TT layouts, ...) WIP - generate czech glyphs for TT fonts WIP - modify gen_font.py to account for negative bearing czech characters WIP - extend translation validation scripts, move them into core/tools WIP - translate TT layouts in Rust WIP - fix tests WIP - fix inverse coloring of nonprintable glyph WIP - add build and test pipelines for Czech language WIP - merge both JSON files together WIP - run new isort WIP - unify all the translation in Rust, expose to micropython TEMP - leave en_merged.json file, so it is accessible by translators with old link WIP - fixes WIP - add french characters and translation via Google Translator WIP - skip rustfmt in mako-created files WIP - revert all the font height changes causing false-positive UI diff WIP - fixes after rebase WIP - fix broken translations WIP - revert some wording changes causing UI diff WIP - improve validation and translate scripts, translate missing strings WIP - sort all keys alphabetically WIP - remove any usage of translation in bootloader WIP - add newline at the end of JSON file WIP - fix bitcoin-only strings check WIP - fix python support check WIP - add some missing translations WIP - fix SD card device test WIP - fix pystyle WIP - fix rust unittests WIP - fix click tests WIP - flag errors in french translations WIP - add script transferring translations data into a byte blob WIP - regenerate fr.rs WIP - store and read language translations from flash WIP - storing language name in storage WIP - sending language_data in apply_settings protobuf message WIP - separate protobuf message for translations, fixes WIP - set up translations area for TT as well WIP - get rid of TREZOR_LANG env variable during build WIP - make the firmware buildable for TT WIP - add basic device tests WIP - set language for tests WIP - counting with language when writing fixtures WIP - add todos WIP - fix CI WIP - unify translations, make titles CAPITAL WIP - translate missing english WIP - skip translations messages for T1 WIP - not changing tests names for english WIP - fix flake8 WIP - no test language setting for T1 WIP - clippy lint about complex data type WIP - fix some english UI diff for TR WIP - fix cstyle WIP - minimize the usage of #[cfg(feature = "micropython")] outside translations module WIP - minimize TT's UI diff WIP - fix ruststyle WIP - fix TR build WIP - advanced Shamir text change WIP - storing the language name as the first item in the translation data WIP - modify and extend tests after storing language name WIP - modify checklist sentence WIP - add TEST_LANG into Makefile for all the emu tests WIP - default arguments WIP - reimplement default arguments remove unneeded pub from get_info function WIP - Rust handling of object attributes lookups from upy - thanks Matejcik! WIP - generate mock interface for attribute-based translations lookups WIP - change function calls to object attributes WIP - symbolic link for unix/translations.c WIP - fix and improve the reading of translations - thanks Matejcik! WIP - add support for multiple languages in removing missing tests WIP - fix multiple-accounts warning in tests WIP - fix encoding of newlines in translations WIP - fix czech tutorial text WIP - fix czech click tests WIP - do not translate wire error messages WIP - add language options to click tests as well WIP - setup czech device tests in CI WIP - setup czech click tests in CI WIP - record czech device tests for TR WIP - record czech click tests for TR WIP - record czech device tests for TT WIP - record czech click tests for TT WIP - pystyle WIP - cstyle WIP - fix Rust micropython import dependency WIP - fix czech recordings WIP - support french translations in tests WIP - shorten some french words to fix the tests WIP - fix micropython cfg compilation WIP - record french click tests for TR WIP - record french device tests for TR WIP - record french device tests for TT WIP - record french click tests for TT WIP - fix french translations - shorten them WIP - translate missing french words WIP - fix click tests WIP - add french tests into CI WIP - pystyle WIP - allow for czech/french tests in update script WIP - update czech fixtures WIP - update french fixtures WIP - ruststyle WIP - disallow MPU to run it on hardware WIP - cstyle WIP - change translations delimiter from * to \x00 WIP - change translations protobufs WIP - remove language handling from storage WIP - add header into JSON files WIP - count with header in translations blob WIP - yml style fixes WIP - fix proto gen WIP - verify version and data hash WIP - fix loading test translations feat(core): allow access to translations area in firmware [no changelog] WIP - fixes after rebase WIP - increase the TT's translations area to 3 sectors WIP - dynamically read the maximum translations size WIP - record non-english tests from CI WIP - loading font data from translations blob WIP - bump translations version WIP - include czech and french glyph data WIP - whitelist another negative-bearing glyph WIP - remove czech/french glyphs from common font files WIP - fix language tests WIP - specific fonts for specific models WIP - revert the non-ascii font hardcoding WIP - include missing BIG font into nonprintable logic WIP - minor Rust code improvements WIP - include newlines at the end of json files WIP - move glyph Rust function to librust_fonts.h WIP - add all fonts into translations file WIP - move fonts into its own dir WIP - reflect separate dir for fonts WIP - not putting translations trezorhal into bootloader WIP - write and read multiple fonts into translations data WIP - silence pyright issue/notissue WIP - delete no more used translations/*.py imports WIP - fix bootloader builds by introducing translations feature and TRANSLATIONS flag WIP - fix TT's bootloader Rust build WIP - fix tests in non-english languages WIP - not search for UTF-8 when there are no translations data WIP - add colons to strings where missing WIP - fix language loading in tests WIP - fix signmessage input flow to work in all languages WIP - create offset table for translation strings WIP - code improvements WIP - record foreign language fixtures + sync with main in english WIP - do alignment check before reading u16 data WIP - allocate blob in RAM for translations data WIP - add TODO for blob generation WIP - record non-english device tests WIP - use bytes.align_to instead of messing with pointers WIP - fixtures WIP - remove unused import WIP - add order.py WIP - add order.json WIP - take order.json into account in creating general.rs WIP - take order.json into account in generating the blob WIP - style WIP - sort the language files WIP - remove unused file WIP - code improvements WIP - add TODO for homescreen notification WIP - translate plural forms WIP - translate time intervals WIP - sign translations with dev keys, validate signatures, improve robustness WIP - improve tests for translations WIP - add `trezorctl utils sign-translations` for production signing of the blob WIP - pyright fix WIP - changing TR progress loader offset - it was colliding with title WIP - show indeterminate loader when loading translations data WIP - record new and updated language tests WIP - show the change language title/prompt in the target language WIP - sort keys WIP - add crowdin-cli into shell.nix WIP - add crowdin sync script
393 lines
12 KiB
Python
Executable File
393 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# script used to generate /embed/extmod/modtrezorui/font_*_*.c
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import TextIO, Any
|
|
import json
|
|
|
|
# pip install freetype-py
|
|
import freetype
|
|
|
|
from foreign_chars import all_languages
|
|
|
|
HERE = Path(__file__).parent
|
|
FONTS_DIR = HERE / "fonts"
|
|
|
|
MIN_GLYPH = ord(" ")
|
|
MAX_GLYPH = ord("~")
|
|
|
|
# metrics explanation: https://www.freetype.org/freetype2/docs/glyphs/metrics.png
|
|
|
|
|
|
def process_bitmap_buffer(buf: list[int], bpp: int) -> list[int]:
|
|
res = buf[:]
|
|
if bpp == 1:
|
|
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:
|
|
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:
|
|
if len(res) % 2 > 0:
|
|
res.append(0)
|
|
res = [
|
|
((a & 0xF0) | (b >> 4))
|
|
for a, b in [res[i : i + 2] for i in range(0, len(res), 2)]
|
|
]
|
|
elif bpp == 8:
|
|
pass
|
|
else:
|
|
raise ValueError
|
|
return res
|
|
|
|
|
|
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:
|
|
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 below)
|
|
# not using negative bearingX makes life so much easier; add it to advance instead
|
|
if bearingX < 0:
|
|
if c in "ÀÂÆÎÏîïÿÝŸÁýAXYjxy}),/_":
|
|
advance += -bearingX
|
|
bearingX = 0
|
|
else:
|
|
raise ValueError("Negative bearingX for character '%s'" % c)
|
|
bearingY = metrics.horiBearingY // 64
|
|
assert advance >= 0 and advance <= 255
|
|
assert bearingX >= 0 and bearingX <= 255
|
|
if bearingY < 0: # HACK
|
|
print("normalizing bearingY %d for '%s'" % (bearingY, 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(
|
|
'Glyph "%c": removed %d pixel columns from the left' % (c, remove_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(
|
|
'Loaded glyph "%c" ... %d x %d @ %d grays (%d bytes, metrics: %d, %d, %d)'
|
|
% (
|
|
self.char,
|
|
self.width,
|
|
self.rows,
|
|
self.num_grays,
|
|
len(self.buf),
|
|
self.advance,
|
|
self.bearingX,
|
|
self.bearingY,
|
|
)
|
|
)
|
|
|
|
def process_byte(self, b: int) -> int:
|
|
if self.inverse_colors:
|
|
return b ^ 0xFF
|
|
else:
|
|
return b
|
|
|
|
def get_definition_line(
|
|
self, name_style_size: str, bpp: int, i: int | str, static: bool = True
|
|
) -> str:
|
|
line = (
|
|
"/* %c */ static const uint8_t Font_%s_glyph_%s[] = { %d, %d, %d, %d, %d"
|
|
% (
|
|
self.char,
|
|
name_style_size,
|
|
i,
|
|
self.width,
|
|
self.rows,
|
|
self.advance,
|
|
self.bearingX,
|
|
self.bearingY,
|
|
)
|
|
)
|
|
|
|
if len(self.buf) > 0:
|
|
line = line + (
|
|
", "
|
|
+ ", ".join(
|
|
[
|
|
"%d" % self.process_byte(x)
|
|
for x in process_bitmap_buffer(self.buf, bpp)
|
|
]
|
|
)
|
|
)
|
|
|
|
line = line + " };\n"
|
|
|
|
if not static:
|
|
line = line.replace("static const", "const")
|
|
|
|
return line
|
|
|
|
def get_json_list(self, bpp: int) -> list[int]:
|
|
infos = [
|
|
self.width,
|
|
self.rows,
|
|
self.advance,
|
|
self.bearingX,
|
|
self.bearingY,
|
|
]
|
|
data = [self.process_byte(x) for x in process_bitmap_buffer(self.buf, bpp)]
|
|
|
|
return infos + data
|
|
|
|
|
|
class FaceProcessor:
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
style: str,
|
|
size: int,
|
|
bpp: int = 4,
|
|
shaveX: int = 0,
|
|
ext: str = "ttf",
|
|
):
|
|
print("Processing ... %s %s %s" % (name, style, size))
|
|
self.name = name
|
|
self.style = style
|
|
self.size = size
|
|
self.bpp = bpp
|
|
self.shaveX = shaveX
|
|
self.ext = ext
|
|
self.face = freetype.Face(str(FONTS_DIR / f"{name}-{style}.{ext}"))
|
|
self.face.set_pixel_sizes(0, size) # type: ignore
|
|
self.fontname = "%s_%s_%d" % (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 _c_file_name(self) -> str:
|
|
return f"font_{self.fontname}.c"
|
|
|
|
@property
|
|
def _h_file_name(self) -> str:
|
|
return f"font_{self.fontname}.h"
|
|
|
|
def write_files(self) -> None:
|
|
self.write_c_files()
|
|
self.write_foreign_json()
|
|
|
|
def write_c_files(self) -> None:
|
|
self._write_c_file()
|
|
self._write_h_file()
|
|
|
|
def write_foreign_json(self) -> None:
|
|
def int_list_to_hex(int_list: list[int]) -> str:
|
|
return "".join(f"{x:02x}" for x in int_list)
|
|
|
|
for language in all_languages:
|
|
all_objects: list[dict[str, Any]] = []
|
|
for item in language["data"]:
|
|
c = item[0]
|
|
self._load_char(c)
|
|
glyph = Glyph.from_face(self.face, c, self.shaveX)
|
|
glyph.print_metrics()
|
|
json_list = glyph.get_json_list(self.bpp)
|
|
obj = {
|
|
"char": c,
|
|
"utf8": item[1],
|
|
"data": int_list_to_hex(json_list),
|
|
}
|
|
print("obj", obj)
|
|
all_objects.append(obj)
|
|
filename = f"font_{self.fontname}_{language['name']}.json"
|
|
with open(filename, "w", encoding="utf-8") as f:
|
|
json_content = json.dumps(all_objects, indent=2, ensure_ascii=False)
|
|
f.write(json_content + "\n")
|
|
|
|
def _write_c_file(self) -> None:
|
|
with open(self._c_file_name, "wt") as f:
|
|
self._write_c_file_header(f)
|
|
self._write_c_file_content(f)
|
|
|
|
def _write_c_file_content(self, f: TextIO) -> None:
|
|
# Write "normal" ASCII characters
|
|
for i in range(MIN_GLYPH, MAX_GLYPH + 1):
|
|
c = chr(i)
|
|
self._write_char_definition(f, c, i)
|
|
|
|
# Write non-printable character
|
|
f.write("\n")
|
|
nonprintable = self._get_nonprintable_definition_line()
|
|
f.write(nonprintable)
|
|
|
|
# Write array of all glyphs
|
|
f.write("\n")
|
|
f.write(
|
|
"const uint8_t * const Font_%s[%d + 1 - %d] = {\n"
|
|
% (self._name_style_size, MAX_GLYPH, MIN_GLYPH)
|
|
)
|
|
for i in range(MIN_GLYPH, MAX_GLYPH + 1):
|
|
f.write(" Font_%s_glyph_%d,\n" % (self._name_style_size, i))
|
|
f.write("};\n")
|
|
|
|
def _write_char_definition(self, f: TextIO, c: str, i: int) -> None:
|
|
self._load_char(c)
|
|
glyph = Glyph.from_face(self.face, c, self.shaveX)
|
|
glyph.print_metrics()
|
|
definition_line = glyph.get_definition_line(self._name_style_size, self.bpp, i)
|
|
f.write(definition_line)
|
|
|
|
# Update mix/max metrics
|
|
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)
|
|
|
|
def _write_c_file_header(self, f: TextIO) -> None:
|
|
f.write("#include <stdint.h>\n\n")
|
|
f.write("// clang-format off\n\n")
|
|
f.write("// - the first two bytes are width and height of the glyph\n")
|
|
f.write(
|
|
"// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph\n"
|
|
)
|
|
f.write("// - the rest is packed %d-bit glyph data\n\n" % self.bpp)
|
|
|
|
def _get_nonprintable_definition_line(self) -> str:
|
|
c = "?"
|
|
self._load_char(c)
|
|
glyph = Glyph.from_face(self.face, c, self.shaveX, inverse_colors=True)
|
|
return glyph.get_definition_line(
|
|
self._name_style_size, self.bpp, "nonprintable", static=False
|
|
)
|
|
|
|
def _load_char(self, c: str) -> None:
|
|
self.face.load_char(c, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_NORMAL) # type: ignore
|
|
|
|
def _write_h_file(self) -> None:
|
|
with open(self._h_file_name, "wt") as f:
|
|
f.write("#include <stdint.h>\n\n")
|
|
f.write("#if TREZOR_FONT_BPP != %d\n" % self.bpp)
|
|
f.write("#error Wrong TREZOR_FONT_BPP (expected %d)\n" % self.bpp)
|
|
f.write("#endif\n")
|
|
|
|
f.write("#define Font_%s_HEIGHT %d\n" % (self._name_style_size, self.size))
|
|
f.write(
|
|
"#define Font_%s_MAX_HEIGHT %d\n"
|
|
% (self._name_style_size, self.font_ymax - self.font_ymin)
|
|
)
|
|
f.write(
|
|
"#define Font_%s_BASELINE %d\n"
|
|
% (self._name_style_size, -self.font_ymin)
|
|
)
|
|
f.write(
|
|
"extern const uint8_t* const Font_%s[%d + 1 - %d];\n"
|
|
% (self._name_style_size, MAX_GLYPH, MIN_GLYPH)
|
|
)
|
|
f.write(
|
|
"extern const uint8_t Font_%s_glyph_nonprintable[];\n"
|
|
% (self._name_style_size)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
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()
|
|
FaceProcessor("TTHoves", "Bold", 17, ext="otf").write_files()
|
|
FaceProcessor("RobotoMono", "Medium", 20).write_files()
|
|
|
|
FaceProcessor("PixelOperator", "Regular", 8, bpp=1, shaveX=1).write_files()
|
|
|
|
FaceProcessor("PixelOperator", "Bold", 8, bpp=1, shaveX=1).write_files()
|
|
FaceProcessor("PixelOperatorMono", "Regular", 8, bpp=1, shaveX=1).write_files()
|
|
|
|
# For model R
|
|
FaceProcessor("Unifont", "Regular", 16, bpp=1, shaveX=1, ext="otf").write_files()
|
|
# NOTE: Unifont Bold does not seem to have czech characters
|
|
FaceProcessor("Unifont", "Bold", 16, bpp=1, shaveX=1, ext="otf").write_files()
|