#!/usr/bin/env python3 # script used to generate /embed/extmod/modtrezorui/font_*_*.c from __future__ import annotations from pathlib import Path import freetype HERE = Path(__file__).parent FONTS_DIR = HERE / "fonts" OUT_DIR = HERE / ".." / ".." / "embed" / "lib" / "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, width: int, height: 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: res = [] 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 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 def process_face( name: str, style: str, size: int, bpp: int = 4, shaveX: int = 0, ext: str = "ttf", ) -> None: print("Processing ... %s %s %s" % (name, style, size)) file_name = FONTS_DIR / f"{name}-{style}.{ext}" face = freetype.Face(str(file_name)) face.set_pixel_sizes(0, size) fontname = "%s_%s_%d" % (name.lower(), style.lower(), size) font_ymin = 0 font_ymax = 0 with open(OUT_DIR / f"font_{fontname}.c", "wt") as f: f.write("#include \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" % bpp) for i in range(MIN_GLYPH, MAX_GLYPH + 1): c = chr(i) face.load_char(c, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_NORMAL) 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) ) print( 'Loaded glyph "%c" ... %d x %d @ %d grays (%d bytes, metrics: %d, %d, %d)' % ( c, bitmap.width, bitmap.rows, bitmap.num_grays, len(bitmap.buffer), advance, bearingX, bearingY, ) ) f.write( "/* %c */ static const uint8_t Font_%s_%s_%d_glyph_%d[] = { %d, %d, %d, %d, %d" % (c, name, style, size, i, width, rows, advance, bearingX, bearingY) ) if len(buf) > 0: f.write( ", " + ", ".join(["%d" % x for x in process_bitmap_buffer(buf, bpp, width, rows)]) ) f.write(" };\n") if i == ord("?"): nonprintable = ( "\nconst uint8_t Font_%s_%s_%d_glyph_nonprintable[] = { %d, %d, %d, %d, %d" % (name, style, size, width, rows, advance, bearingX, bearingY) ) nonprintable += ", " + ", ".join( ["%d" % (x ^ 0xFF) for x in process_bitmap_buffer(buf, bpp, width, rows)] ) nonprintable += " };\n" yMin = bearingY - rows yMax = yMin + rows font_ymin = min(font_ymin, yMin) font_ymax = max(font_ymax, yMax) f.write(nonprintable) f.write( "\nconst uint8_t * const Font_%s_%s_%d[%d + 1 - %d] = {\n" % (name, style, size, MAX_GLYPH, MIN_GLYPH) ) for i in range(MIN_GLYPH, MAX_GLYPH + 1): f.write(" Font_%s_%s_%d_glyph_%d,\n" % (name, style, size, i)) f.write("};\n") with open(OUT_DIR / f"font_{fontname}.h", "wt") as f: f.write("#include \n\n") f.write("#if TREZOR_FONT_BPP != %d\n" % bpp) f.write("#error Wrong TREZOR_FONT_BPP (expected %d)\n" % bpp) f.write("#endif\n") f.write("#define Font_%s_%s_%d_HEIGHT %d\n" % (name, style, size, size)) f.write( "#define Font_%s_%s_%d_MAX_HEIGHT %d\n" % (name, style, size, font_ymax - font_ymin) ) f.write("#define Font_%s_%s_%d_BASELINE %d\n" % (name, style, size, -font_ymin)) f.write( "extern const uint8_t* const Font_%s_%s_%d[%d + 1 - %d];\n" % (name, style, size, MAX_GLYPH, MIN_GLYPH) ) f.write( "extern const uint8_t Font_%s_%s_%d_glyph_nonprintable[];\n" % (name, style, size) ) process_face("Roboto", "Regular", 20) process_face("Roboto", "Bold", 20) process_face("TTHoves", "Regular", 21, ext="otf") process_face("TTHoves", "DemiBold", 21, ext="otf") process_face("TTHoves", "Bold", 17, ext="otf") process_face("RobotoMono", "Medium", 20) process_face("PixelOperator", "Regular", 8, bpp=1, shaveX=1) process_face("PixelOperator", "Bold", 8, bpp=1, shaveX=1) process_face("PixelOperatorMono", "Regular", 8, bpp=1, shaveX=1) # For model R process_face("Unifont", "Regular", 16, bpp=1, shaveX=1, ext="otf") process_face("Unifont", "Bold", 16, bpp=1, shaveX=1, ext="otf")