From 378b6ebc1dd7afc28ffb2e7abe2c8fbf7f1e7ade Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Thu, 6 Jun 2024 23:49:43 +0200 Subject: [PATCH] add font converter Signed-off-by: Peter Siegmund --- ePaper-ESP-IDF/Makefile | 3 +- ePaper-ESP-IDF/fontconvert.py | 150 ++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 ePaper-ESP-IDF/fontconvert.py diff --git a/ePaper-ESP-IDF/Makefile b/ePaper-ESP-IDF/Makefile index 838bff2..5d946eb 100644 --- a/ePaper-ESP-IDF/Makefile +++ b/ePaper-ESP-IDF/Makefile @@ -1,3 +1,2 @@ -convert: curl -o ./data/staticmap.png https://maps.googleapis.com/maps/api/staticmap\?center\=53.541962,%209.993402\&zoom\=15\&size\=540x540\&key\=AIzaSyARgP_FKFXsrcgVd_HVWoIfH5N8-a88wlQ\&map_id\=2f371c2346218fe8 - python imgconvert.py -i ./data/staticmap.png -o components/mapView/staticmap.h -n staticmap + python imgconvert.py -i ./data/staticmap.png -o components/mapView/include/staticmap.h -n staticmap diff --git a/ePaper-ESP-IDF/fontconvert.py b/ePaper-ESP-IDF/fontconvert.py new file mode 100644 index 0000000..7601b49 --- /dev/null +++ b/ePaper-ESP-IDF/fontconvert.py @@ -0,0 +1,150 @@ +#!python3 +import freetype +import zlib +import sys +import re +import math +import argparse +from collections import namedtuple + +parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.") +parser.add_argument("name", action="store", help="name of the font.") +parser.add_argument("size", type=int, help="font size to use.") +parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.") +parser.add_argument("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.") +args = parser.parse_args() + +GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"]) + +font_stack = [freetype.Face(f) for f in args.fontstack] +compress = args.compress +size = args.size +font_name = args.name + +# inclusive unicode code point intervals +# must not overlap and be in ascending order +intervals = [ + (32, 126), + (160, 255), + #(0x2500, 0x259F), + #(0x2700, 0x27BF), + # # powerline symbols + #(0xE0A0, 0xE0A2), + #(0xE0B0, 0xE0B3), + #(0x1F600, 0x1F680), +] + + +def norm_floor(val): + return int(math.floor(val / (1 << 6))) + +def norm_ceil(val): + return int(math.ceil(val / (1 << 6))) + +for face in font_stack: + # shift by 6 bytes, because sizes are given as 6-bit fractions + # the display has about 150 dpi. + face.set_char_size(size << 6, size << 6, 150, 150) + +def chunks(l, n): + for i in range(0, len(l), n): + yield l[i:i + n] + +total_size = 0 +total_packed = 0 +all_glyphs = [] + +def load_glyph(code_point): + face_index = 0 + while face_index < len(font_stack): + face = font_stack[face_index] + glyph_index = face.get_char_index(code_point) + if glyph_index > 0: + face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER) + return face + break + face_index += 1 + print (f"falling back to font {face_index} for {chr(code_point)}.", file=sys.stderr) + raise ValueError(f"code point {code_point} not found in font stack!") + +for i_start, i_end in intervals: + for code_point in range(i_start, i_end + 1): + face = load_glyph(code_point) + bitmap = face.glyph.bitmap + pixels = [] + px = 0 + for i, v in enumerate(bitmap.buffer): + y = i / bitmap.width + x = i % bitmap.width + if x % 2 == 0: + px = (v >> 4) + else: + px = px | (v & 0xF0) + pixels.append(px); + px = 0 + # eol + if x == bitmap.width - 1 and bitmap.width % 2 > 0: + pixels.append(px) + px = 0 + + packed = bytes(pixels); + total_packed += len(packed) + compressed = packed + if compress: + compressed = zlib.compress(packed) + + glyph = GlyphProps( + width = bitmap.width, + height = bitmap.rows, + advance_x = norm_floor(face.glyph.advance.x), + left = face.glyph.bitmap_left, + top = face.glyph.bitmap_top, + compressed_size = len(compressed), + data_offset = total_size, + code_point = code_point, + ) + total_size += len(compressed) + all_glyphs.append((glyph, compressed)) + +# pipe seems to be a good heuristic for the "real" descender +face = load_glyph(ord('|')) + +glyph_data = [] +glyph_props = [] +for index, glyph in enumerate(all_glyphs): + props, compressed = glyph + glyph_data.extend([b for b in compressed]) + glyph_props.append(props) + +print("total", total_packed, file=sys.stderr) +print("compressed", total_size, file=sys.stderr) + +print("#pragma once") +print("#include \"epd_driver.h\"") +print(f"const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{") +for c in chunks(glyph_data, 16): + print (" " + " ".join(f"0x{b:02X}," for b in c)) +print ("};"); + +print(f"const GFXglyph {font_name}Glyphs[] = {{") +for i, g in enumerate(glyph_props): + print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else ''}") +print ("};"); + +print(f"const UnicodeInterval {font_name}Intervals[] = {{") +offset = 0 +for i_start, i_end in intervals: + print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},") + offset += i_end - i_start + 1 +print ("};"); + +print(f"const GFXfont {font_name} = {{") +print(f" (uint8_t*){font_name}Bitmaps,") +print(f" (GFXglyph*){font_name}Glyphs,") +print(f" (UnicodeInterval*){font_name}Intervals,") +print(f" {len(intervals)},") +print(f" {1 if compress else 0},") +print(f" {norm_ceil(face.size.height)},") +print(f" {norm_ceil(face.size.ascender)},") +print(f" {norm_floor(face.size.descender)},") +print("};")