#!/usr/bin/python # -*- coding: utf-8 -*- # ************************************************************************************************* # Arm 2D project # @file ttf2c.py # @brief TrueTypeFont to C-array converter # # ************************************************************************************************* # # * Copyright (C) 2025 ARM Limited or its affiliates. All rights reserved. # * # * SPDX-License-Identifier: Apache-2.0 # * # * Licensed under the Apache License, Version 2.0 (the License); you may # * not use this file except in compliance with the License. # * You may obtain a copy of the License at # * # * www.apache.org/licenses/LICENSE-2.0 # * # * Unless required by applicable law or agreed to in writing, software # * distributed under the License is distributed on an AS IS BASIS, WITHOUT # * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # * See the License for the specific language governing permissions and # * limitations under the License. # */ import os import sys import argparse import freetype import numpy as np import math import binascii import tempfile c_head_string=""" /* generated with ttf2c.py (v2.4.1) */ #include "arm_2d_helper.h" #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunknown-warning-option" # pragma clang diagnostic ignored "-Wreserved-identifier" # pragma clang diagnostic ignored "-Wmissing-variable-declarations" # pragma clang diagnostic ignored "-Wcast-qual" # pragma clang diagnostic ignored "-Wmissing-declarations" # pragma clang diagnostic ignored "-Wgnu-variable-sized-type-not-at-end" # pragma clang diagnostic ignored "-Wimplicit-int-conversion" # pragma clang diagnostic ignored "-Wdeclaration-after-statement" # pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" # pragma clang diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers" # pragma clang diagnostic ignored "-Wpadded" # pragma clang diagnostic ignored "-Wunused-parameter" # pragma clang diagnostic ignored "-Wunused-variable" # pragma clang diagnostic ignored "-Wsign-conversion" #elif defined(__IS_COMPILER_GCC__) # pragma GCC diagnostic ignored "-Wdiscarded-qualifiers" # pragma GCC diagnostic ignored "-Wunused-variable" #elif defined(__IS_COMPILER_ARM_COMPILER_5__) # pragma diag_suppress=1296,1,64,177 #endif typedef struct { uint16_t hwIndex; arm_2d_size_t tCharSize; int16_t iAdvance; int16_t iBearingX; int16_t iBearingY; uint8_t chCodeLength; uint8_t chUTF8[4]; } const __ttf_char_descriptor_t; """ c_body_string=""" ARM_SECTION(\"arm2d.tile.c_tileUTF8{0}A{5}Mask\") static const arm_2d_tile_t c_tileUTF8{0}A{5}Mask = {{ .tRegion = {{ .tSize = {{ .iWidth = {1}, .iHeight = {4}, }}, }}, .tInfo = {{ .bIsRoot = true, .bHasEnforcedColour = true, .tColourInfo = {{ .chScheme = ARM_2D_COLOUR_{5}BIT, }}, }}, .pchBuffer = (uint8_t *)c_bmpUTF8{0}A{5}Font, }}; #define __UTF8_FONT_SIZE_{5}__ static IMPL_FONT_GET_CHAR_DESCRIPTOR(__utf8_a{5}_font_get_char_descriptor) {{ assert(NULL != ptFont); assert(NULL != ptDescriptor); assert(NULL != pchCharCode); arm_2d_user_font_t *ptThis = (arm_2d_user_font_t *)ptFont; ARM_2D_UNUSED(ptThis); memset(ptDescriptor, 0, sizeof(arm_2d_char_descriptor_t)); ptDescriptor->tileChar.ptParent = (arm_2d_tile_t *)&ptFont->tileFont; ptDescriptor->tileChar.tInfo.bDerivedResource = true; /* use the white space as the default char */ __ttf_char_descriptor_t *ptUTF8Char = (__ttf_char_descriptor_t *)&c_tUTF8{0}LookUpTableA{5}[dimof(c_tUTF8{0}LookUpTableA{5})-1]; arm_foreach(__ttf_char_descriptor_t, c_tUTF8{0}LookUpTableA{5}, ptChar) {{ if (0 == strncmp( (char *)pchCharCode, (char *)ptChar->chUTF8, ptChar->chCodeLength)) {{ /* found the UTF8 char */ ptUTF8Char = ptChar; break; }} }} ptDescriptor->chCodeLength = ptUTF8Char->chCodeLength; ptDescriptor->tileChar.tRegion.tSize = ptUTF8Char->tCharSize; ptDescriptor->tileChar.tRegion.tLocation.iY = (int16_t)ptUTF8Char->hwIndex; ptDescriptor->iAdvance = ptUTF8Char->iAdvance; ptDescriptor->iBearingX= ptUTF8Char->iBearingX; ptDescriptor->iBearingY= ptUTF8Char->iBearingY; return ptDescriptor; }} ARM_SECTION(\"arm2d.asset.FONT.ARM_2D_FONT_{0}_A{5}\") const struct {{ implement(arm_2d_user_font_t); arm_2d_char_idx_t tUTF8Table; }} ARM_2D_FONT_{0}_A{5} = {{ .use_as__arm_2d_user_font_t = {{ .use_as__arm_2d_font_t = {{ .tileFont = impl_child_tile( c_tileUTF8{0}A{5}Mask, 0, /* x offset */ 0, /* y offset */ {1}, /* width */ {4} /* height */ ), .tCharSize = {{ .iWidth = {1}, .iHeight = {2}, }}, .nCount = {3}, //!< Character count .fnGetCharDescriptor = &__utf8_a{5}_font_get_char_descriptor, .fnDrawChar = &__arm_2d_lcd_text_default_a{5}_font_draw_char, }}, .hwCount = 1, .hwDefaultCharIndex = 1, /* tBlank */ }}, .tUTF8Table = {{ .hwCount = {3}, .hwOffset = 0, }}, }}; #undef __UTF8_FONT_SIZE_{5}__ """ c_body_string_ascii=""" ARM_SECTION(\"arm2d.tile.c_tileUTF8{0}A{5}Mask\") static const arm_2d_tile_t c_tileUTF8{0}A{5}Mask = {{ .tRegion = {{ .tSize = {{ .iWidth = {1}, .iHeight = {4}, }}, }}, .tInfo = {{ .bIsRoot = true, .bHasEnforcedColour = true, .tColourInfo = {{ .chScheme = ARM_2D_COLOUR_{5}BIT, }}, }}, .pchBuffer = (uint8_t *)c_bmpUTF8{0}A{5}Font, }}; #define __UTF8_FONT_SIZE_{5}__ static IMPL_FONT_GET_CHAR_DESCRIPTOR(__utf8_a{5}_font_get_char_descriptor) {{ assert(NULL != ptFont); assert(NULL != ptDescriptor); assert(NULL != pchCharCode); arm_2d_user_font_t *ptThis = (arm_2d_user_font_t *)ptFont; ARM_2D_UNUSED(ptThis); memset(ptDescriptor, 0, sizeof(arm_2d_char_descriptor_t)); ptDescriptor->tileChar.ptParent = (arm_2d_tile_t *)&ptFont->tileFont; ptDescriptor->tileChar.tInfo.bDerivedResource = true; /* use the white space as the default char */ __ttf_char_descriptor_t *ptUTF8Char = NULL; if (pchCharCode[0] > 0x20 && pchCharCode[0] <= 0x7e) {{ ptUTF8Char = (__ttf_char_descriptor_t *) &c_tUTF8{0}LookUpTableA{5}[ pchCharCode[0] - c_tUTF8{0}LookUpTableA{5}[0].chUTF8[0]]; }} else {{ /* use the white space as the default char */ ptUTF8Char = (__ttf_char_descriptor_t *) &c_tUTF8{0}LookUpTableA{5}[dimof(c_tUTF8{0}LookUpTableA{5})-1]; }} ptDescriptor->chCodeLength = ptUTF8Char->chCodeLength; ptDescriptor->tileChar.tRegion.tSize = ptUTF8Char->tCharSize; ptDescriptor->tileChar.tRegion.tLocation.iY = (int16_t)ptUTF8Char->hwIndex; ptDescriptor->iAdvance = ptUTF8Char->iAdvance; ptDescriptor->iBearingX= ptUTF8Char->iBearingX; ptDescriptor->iBearingY= ptUTF8Char->iBearingY; return ptDescriptor; }} ARM_SECTION(\"arm2d.asset.FONT.ARM_2D_FONT_{0}_A{5}\") const struct {{ implement(arm_2d_user_font_t); arm_2d_char_idx_t tUTF8Table; }} ARM_2D_FONT_{0}_A{5} = {{ .use_as__arm_2d_user_font_t = {{ .use_as__arm_2d_font_t = {{ .tileFont = impl_child_tile( c_tileUTF8{0}A{5}Mask, 0, /* x offset */ 0, /* y offset */ {1}, /* width */ {4} /* height */ ), .tCharSize = {{ .iWidth = {1}, .iHeight = {2}, }}, .nCount = {3}, //!< Character count .fnGetCharDescriptor = &__utf8_a{5}_font_get_char_descriptor, .fnDrawChar = &__arm_2d_lcd_text_default_a{5}_font_draw_char, }}, .hwCount = 1, .hwDefaultCharIndex = 1, /* tBlank */ }}, .tUTF8Table = {{ .hwCount = {3}, .hwOffset = 0, }}, }}; #undef __UTF8_FONT_SIZE_{5}__ """ c_tail_string=""" #if defined(__clang__) # pragma clang diagnostic pop #elif defined(__IS_COMPILER_ARM_COMPILER_5__) # pragma diag_warning=1296 #endif """ def generate_glyphs_data(input_file, text, pixel_size, font_bit_size, font_index): face = freetype.Face(input_file, index=font_index) face.set_pixel_sizes(0, pixel_size) glyphs_data = [] current_index = 0 width_max = 0 height_max = 0 for char in set(text): # Get the glyph bitmap face.load_char(char) bitmap = face.glyph.bitmap utf8_encoding = char.encode('utf-8') if len(bitmap.buffer) == 0: continue if list(utf8_encoding) == [0xef, 0xbb, 0xbf]: continue width_max = max(bitmap.width, width_max) height_max = max(bitmap.rows, height_max) width_max += 2 height_max += 2 for char in sorted(set(text)): face.load_char(char) bitmap = face.glyph.bitmap utf8_encoding = char.encode('utf-8') if len(bitmap.buffer) == 0: continue if list(utf8_encoding) == [0xef, 0xbb, 0xbf]: continue # update advance, bearing x and bearing y as we added the 1px boarder around each glyph. advance_width = math.ceil(face.glyph.advance.x / 64.0) + 1 bearing_x = face.glyph.bitmap_left + 1 bearing_y = face.glyph.bitmap_top + 1 width = bitmap.width height = bitmap.rows pitch = bitmap.pitch if bitmap.pixel_mode == freetype.FT_PIXEL_MODE_MONO: buffer = np.frombuffer(bytes(bitmap.buffer), dtype=np.uint8) bits_per_row = pitch * 8 bitmap_array = np.unpackbits(buffer).reshape((height, bits_per_row))[:, :width] bitmap_array = bitmap_array.astype(np.uint8) * 255 elif bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY2: buffer = np.frombuffer(bytes(bitmap.buffer), dtype=np.uint8) bits_per_row = pitch * 4 bitmap_array = np.unpackbits(buffer).reshape((height, bits_per_row))[:, :width * 2] bitmap_array = (bitmap_array.reshape((height, width * 2)) * 85).astype(np.uint8) elif bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY4: buffer = np.frombuffer(bytes(bitmap.buffer), dtype=np.uint8) bits_per_row = pitch * 2 bitmap_array = np.unpackbits(buffer).reshape((height, bits_per_row))[:, :width * 4] bitmap_array = (bitmap_array.reshape((height, width * 2)) * 17).astype(np.uint8) elif bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY: bitmap_array = np.array(bitmap.buffer, dtype=np.uint8).reshape((height, width)) else: raise ValueError(f"Unsupported pixel mode: {bitmap.pixel_mode}") continue if width < width_max: if (width_max - width) > 1: padding = ((0, 0), (1, width_max - width - 1)) else: padding = ((0, 0), (0, width_max - width)) bitmap_array = np.pad(bitmap_array, padding, 'constant') padding = ((1, 1), (0, 0)) bitmap_array = np.pad(bitmap_array, padding, 'constant') height += 2 char_index_advance = len(bitmap_array.flatten()); if font_bit_size == 4: def RevBitQuadPerByte(byteArr): return ((byteArr & 0x0f) << 4) | ((byteArr & 0xf0) >> 4) # temporary array with 2x reduced width & pad (r, c) = np.shape(bitmap_array) tmp = np.empty((0, int((c+1)/2)), dtype=np.uint8) for cur in bitmap_array: bitsArr = np.unpackbits(cur.astype(np.uint8)) # generate indexes for MSB bit quadruplet every byte idx = np.arange(0, np.size(bitsArr), 8) idx = np.reshape(np.column_stack( (np.column_stack((idx+0, idx+1)), np.column_stack((idx+2, idx+3)))), (1,-1)), # extraction + endianness conversion appended in temp array # packbits is taking care of padding tmp = np.vstack([tmp, RevBitQuadPerByte(np.packbits(bitsArr[idx]))]) bitmap_array = tmp elif font_bit_size == 2: def RevBitPairPerByte(byteArr): return ((byteArr & 0x03) << 6) | ((byteArr & 0xc0) >> 6) | ((byteArr & 0x30) >> 2 ) | ((byteArr & 0x0c) << 2) # temporary array with 4x reduced width & pad (r, c) = np.shape(bitmap_array) tmp = np.empty((0, int((c+3)/4)), dtype=np.uint8) for cur in bitmap_array: bitsArr = np.unpackbits(cur.astype(np.uint8)) # generate indexes for MSB bit pair every byte idx = np.arange(0, np.size(bitsArr), 8) idx = np.reshape(np.column_stack((idx+0, idx+1)), (1,-1)) # extraction + endianness conversion appended in temp array tmp = np.vstack([tmp, RevBitPairPerByte(np.packbits(bitsArr[idx]))]) bitmap_array = tmp elif font_bit_size == 1: def RevBitPerByte(byteArr): return ((byteArr & 0x01) << 7) | ((byteArr & 0x80) >> 7) | \ ((byteArr & 0x02) << 5) | ((byteArr & 0x40) >> 5) | \ ((byteArr & 0x04) << 3) | ((byteArr & 0x20) >> 3) | \ ((byteArr & 0x08) << 1) | ((byteArr & 0x10) >> 1) # temporary array with 8x reduced width & pad (r, c) = np.shape(bitmap_array) tmp = np.empty((0, int((c+7)/8)), dtype=np.uint8) for cur in bitmap_array: bitsArr = np.unpackbits(cur.astype(np.uint8)) # generate indexes for MSB bit every byte idx = np.arange(0, np.size(bitsArr), 8) # extraction + endianness conversion tmp = np.vstack([tmp, RevBitPerByte(np.packbits(bitsArr[idx]))]) bitmap_array = tmp char_mask_array = bitmap_array.flatten() glyphs_data.append((char, char_mask_array, width + 2, height, current_index, advance_width, bearing_x, bearing_y, utf8_encoding)) current_index += char_index_advance return glyphs_data, width_max, height_max def utf8_to_c_array(utf8_bytes): return '{' + ', '.join([f'0x{byte:02x}' for byte in utf8_bytes]) + '}' def write_c_code(glyphs_data, output_file, name, char_max_width, char_max_height, font_bit_size, ascii_mode): with open(output_file, "a") as f: print("ARM_SECTION(\"arm2d.asset.FONT.c_bmpUTF8{0}A{1}Font\")\nconst uint8_t c_bmpUTF8{0}A{1}Font[] = {{\n" .format(name, font_bit_size), file=f) for char, data, width, height, index, advance_width, bearing_x, bearing_y, utf8_encoding in glyphs_data: utf8_c_array = utf8_to_c_array(utf8_encoding) f.write(f"\n // Glyph for character {utf8_c_array}\n") hex_str = binascii.hexlify(data).decode() for i in range(0, len(hex_str), char_max_width*2): line = hex_str[i:i+char_max_width*2] spaced_line = ' '.join(f"0x{line[j:j+2]}," for j in range(0, len(line), 2)) f.write(" "); f.write(spaced_line) f.write("\n"); f.write(" // a white space\n "); f.write("0x00, " * (char_max_width * char_max_height)) f.write("\n};\n\n") print("ARM_SECTION(\"arm2d.asset.FONT.c_tUTF8{0}LookUpTableA{1}\")\n const __ttf_char_descriptor_t c_tUTF8{0}LookUpTableA{1}[{2}] = {{\n" .format(name, font_bit_size, len(glyphs_data) + 1), file=f) last_index = 0; last_advance = 0; last_height = 0; for char, data, width, height, index, advance_width, bearing_x, bearing_y, utf8_encoding in glyphs_data: utf8_c_array = utf8_to_c_array(utf8_encoding) last_index = index last_advance = advance_width last_height = height; f.write(f" {{ {round(index / char_max_width)}, {{ {width}, {height}, }}, {advance_width}, {bearing_x}, {bearing_y}, {len(utf8_encoding)}, {utf8_c_array} }},\n") last_index += char_max_width * last_height f.write(f" {{ {round(last_index / char_max_width)}, {{ {char_max_width}, {char_max_height}, }}, {round(char_max_width / 2)}, {0}, {char_max_height}, 1, {{0x20}} }},\n") last_index += char_max_width * char_max_height f.write("};\n") if ascii_mode == True: print(c_body_string_ascii.format( name, char_max_width, char_max_height, len(glyphs_data), round(last_index / char_max_width)+1, font_bit_size), file=f) else: print(c_body_string.format( name, char_max_width, char_max_height, len(glyphs_data), round(last_index / char_max_width)+1, font_bit_size), file=f) def main(): parser = argparse.ArgumentParser(description='TrueTypeFont to C array converter (v2.4.1)') parser.add_argument("-i", "--input", type=str, help="Path to the TTF file", required=True) parser.add_argument("--index", type=int, help="The Font Index in a TTC file", required=False, default=0) parser.add_argument("-t", "--text", type=str, help="Path to the text file", required=False) parser.add_argument("-n", "--name", type=str, help="The customized UTF8 font name", required=False, default="UTF8") parser.add_argument("-o", "--output", type=str, help="Path to the output C file", required=True) parser.add_argument("-p", "--pixelsize",type=int, help="Font size in pixels", required=False, default=32) parser.add_argument("-s", "--fontbitsize",type=int, help="font bit size (1,2,4,8)", required=False, default=0) if len(sys.argv)==1: parser.print_help(sys.stderr) sys.exit(1) args = parser.parse_args() if args.fontbitsize not in [1, 2, 4, 8, 0]: print(f'Invalid alpha size={args.fontbitsize}') sys.exit(1) ascii_mode = False if args.text == None or args.text == "" : ascii_mode = True fd, args.text = tempfile.mkstemp() with os.fdopen(fd, 'w') as temptextfile: print("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", file=temptextfile, end='') with open(args.output, "w") as outputfile: print(c_head_string, file=outputfile) if args.fontbitsize in [1, 2, 4, 8]: with open(args.text, 'r', encoding='utf-8') as f: text = f.read() glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, args.fontbitsize, args.index) write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, args.fontbitsize, ascii_mode) else: with open(args.text, 'r', encoding='utf-8') as f: text = f.read() glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 1, args.index) write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 1, ascii_mode) with open(args.text, 'r', encoding='utf-8') as f: text = f.read() glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 2, args.index) write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 2, ascii_mode) with open(args.text, 'r', encoding='utf-8') as f: text = f.read() glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 4, args.index) write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 4, ascii_mode) with open(args.text, 'r', encoding='utf-8') as f: text = f.read() glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 8, args.index) write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 8, ascii_mode) with open(args.output, "a") as outputfile: print(c_tail_string, file=outputfile) if __name__ == '__main__': main()