diff --git a/tools/snippets/font_multiplier.py b/tools/snippets/font_multiplier.py new file mode 100644 index 000000000..4aba4ccc7 --- /dev/null +++ b/tools/snippets/font_multiplier.py @@ -0,0 +1,153 @@ +# Helpers to increase the font size from existing font data. +# Thanks to it we can save space by not having to include +# the larger font definitions. + +from __future__ import annotations + +from typing_extensions import Literal +from typing import Tuple + + +Bit = Literal[0, 1] +Point = Tuple[int, int] + + +def magnify_glyph_by_two(width: int, height: int, bytes_data: list[int]) -> list[int]: + """Magnifying the font size by two. + + Input and output are bytes (that is the standard in font files), + but internally works with bits. + """ + bits_data = _bytes_to_bits(bytes_data, height * width) + double_size_data = _double_the_bits(width, bits_data) + return _bits_to_bytes(double_size_data) + + +def _bytes_to_bits(bytes_data: list[int], bits_to_take: int) -> list[Bit]: + """Transform bytes into bits.""" + bits_data = [f"{i:08b}" for i in bytes_data[:-1]] + + # Last element needs to be handled carefully, + # to respect the number of bits to take. + missing_bits = bits_to_take - len(bits_data) * 8 + last_byte = bytes_data[-1] + last_binary = f"{last_byte:08b}" + # Taking either the right or left part of the last byte + if last_byte < 2**missing_bits: + bits_data.append(last_binary[-missing_bits:]) + else: + bits_data.append(last_binary[:missing_bits]) + + return [1 if int(x) else 0 for x in "".join(bits_data)] + + +def _bits_to_bytes(bits_data: list[Bit]) -> list[int]: + """Transform bits into bytes.""" + bits_str = "".join([str(bit) for bit in bits_data]) + bytes_str_list = [bits_str[i : i + 8] for i in range(0, len(bits_str), 8)] + # Last element needs to be right-padded to 8 bits + while len(bytes_str_list[-1]) != 8: + bytes_str_list[-1] += "0" + return [int(byte, 2) for byte in bytes_str_list] + + +def _double_the_bits(width: int, bits_data: list[Bit]) -> list[Bit]: + """Double the dimension of a given glyph.""" + # Allocate space for the new data - 2*2 bigger than the original + # Then fill all the new indexes with appropriate bits + double_size_data: list[Bit] = [0 for _ in range(4 * len(bits_data))] + for original_index, bit in enumerate(bits_data): + for new_index in _corresponding_indexes(original_index, width): + double_size_data[new_index] = bit + return double_size_data + + +def _corresponding_indexes(index: int, width: int) -> list[int]: + """Find the indexes of the four pixels that correspond to the given one.""" + point = _index_to_point(index, width) + points = _scale_by_two(point) + new_width = 2 * width + return [_point_to_index(new_point, new_width) for new_point in points] + + +def _scale_by_two(point: Point) -> tuple[Point, Point, Point, Point]: + """Translate one pixel into four adjacent pixels to visually scale it by two.""" + x, y = point + return ( + (x * 2, y * 2), + (x * 2 + 1, y * 2), + (x * 2, y * 2 + 1), + (x * 2 + 1, y * 2 + 1), + ) + + +def _point_to_index(point: Point, width: int) -> int: + """Translate point to index according to the glyph width.""" + x, y = point + assert 0 <= x < width + return y * width + x + + +def _index_to_point(index: int, width: int) -> Point: + """Translate index to point according to the glyph width.""" + x = index % width + y = index // width + return x, y + + +def print_from_bits(width: int, height: int, bit_data: list[Bit]): + """Print the glyph into terminal from bit data.""" + for line_id in range(height): + line = bit_data[line_id * width : (line_id + 1) * width] + str_line = "".join([str(x) for x in line]) + print(str_line.replace("0", " ")) + print() + + +def print_from_bytes(width: int, height: int, bytes_data: list[int]): + """Print the glyph into terminal from byte data.""" + bits_data = _bytes_to_bits(bytes_data, height * width) + print_from_bits(width, height, bits_data) + + +################################ +# TEST SECTION for pytest +################################ + +# fmt: off +HEIGHT = 7 +K_WIDTH = 5 +K_GLYPH = [140, 169, 138, 74, 32] +K_BITS = [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1] +K_MAGNIFIED = [192, 240, 60, 51, 12, 204, 51, 15, 3, 192, 204, 51, 12, 51, 12, 192, 240, 48] +M_GLYPH = [131, 7, 29, 89, 48, 96, 128] +M_WIDTH = 7 +# fmt: on + + +def test_bits_to_bytes_and_back(): + vectors = ( # height, width, bytes_data + (HEIGHT, K_WIDTH, K_GLYPH), + (HEIGHT, M_WIDTH, M_GLYPH), + ) + + for height, width, bytes_data in vectors: + bits_data = _bytes_to_bits(bytes_data, height * width) + assert _bits_to_bytes(bits_data) == bytes_data + + +def test_bit_to_bytes(): + assert _bytes_to_bits(K_GLYPH, HEIGHT * K_WIDTH) == K_BITS + + +def test_overall_magnify(): + assert magnify_glyph_by_two(K_WIDTH, HEIGHT, K_GLYPH) == K_MAGNIFIED + + +if __name__ == "__main__": + # Print letter K, magnify it and print it also + print("K_GLYPH", K_GLYPH) + print_from_bytes(K_WIDTH, HEIGHT, K_GLYPH) + magnified_data = magnify_glyph_by_two(K_WIDTH, HEIGHT, K_GLYPH) + print("magnified_data", magnified_data) + print_from_bytes(2 * K_WIDTH, 2 * HEIGHT, magnified_data)