From 4c7f440b49be4913d1bdccbe9f01b895f9bcc717 Mon Sep 17 00:00:00 2001 From: Frederik Sicking Date: Tue, 21 Feb 2023 18:22:38 +0100 Subject: [PATCH] added simple freetype2 font2c tool for converting ttf files to Olivec_Font struct format --- nobuild.c | 2 + test.c | 23 ++++ test/custom_font_expected.png | Bin 0 -> 15947 bytes tools/font2c.c | 250 ++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+) create mode 100644 test/custom_font_expected.png create mode 100644 tools/font2c.c diff --git a/nobuild.c b/nobuild.c index a6bef96..15117b9 100644 --- a/nobuild.c +++ b/nobuild.c @@ -8,6 +8,7 @@ void build_tools(void) MKDIRS("build", "tools"); CMD("clang", COMMON_CFLAGS, "-o", "./build/tools/png2c", "./tools/png2c.c", "-lm"); CMD("clang", COMMON_CFLAGS, "-o", "./build/tools/obj2c", "./tools/obj2c.c", "-lm"); + CMD("clang", COMMON_CFLAGS, "-I/usr/include/freetype2", "-I/usr/include/libpng16", "-L/usr/local/lib", "-lfreetype", "-o", "./build/tools/font2c", "./tools/font2c.c"); } void build_assets(void) @@ -19,6 +20,7 @@ void build_assets(void) CMD("./build/tools/png2c", "-n", "lavastone", "-o", "./build/assets/lavastone.c", "./assets/lavastone.png"); CMD("./build/tools/obj2c", "-o", "./build/assets/tsodinCupLowPoly.c", "./assets/tsodinCupLowPoly.obj"); CMD("./build/tools/obj2c", "-s", "0.40", "-o", "./build/assets/utahTeapot.c", "./assets/utahTeapot.obj"); + CMD("./build/tools/font2c", "-o", "./build/assets/testFont.c", "-n", "test_font", "./fonts/LibreBaskerville-Regular.ttf"); } void build_tests(void) diff --git a/test.c b/test.c index 2660d8f..f66ff36 100644 --- a/test.c +++ b/test.c @@ -7,6 +7,7 @@ #include "./assets/tsodinPog.c" #include "./assets/tsodinCup.c" +#include "./assets/testFont.c" #define PI 3.14159265359 @@ -396,6 +397,27 @@ Olivec_Canvas test_hello_world_text_rendering(void) return oc; } +Olivec_Canvas test_custom_font(void) +{ + size_t size = 1; + + char *text = "the quick brown fox jumped over the lazy dog"; + size_t text_len = strlen(text); + Olivec_Canvas oc = canvas_alloc(1000, 300); + olivec_fill(oc, BACKGROUND_COLOR); + olivec_text(oc, text, oc.width/2 - test_font.width*size*text_len/2, oc.height/4 - test_font.height*size/2, test_font, size, FOREGROUND_COLOR); + + text = "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG"; + text_len = strlen(text); + olivec_text(oc, text, oc.width/2 - test_font.width*size*text_len/2, oc.height/2 - test_font.height*size/2, test_font, size, FOREGROUND_COLOR); + + text = "1234567890 !@#$%%&*(){}[]<>=\\|/,.;:?+-_"; + text_len = strlen(text); + olivec_text(oc, text, oc.width/2 - test_font.width*size*text_len/2, 3 * oc.height/4 - test_font.height*size/2, test_font, size, FOREGROUND_COLOR); + + return oc; +} + Olivec_Canvas test_line_edge_cases(void) { size_t width = 10; @@ -637,6 +659,7 @@ Test_Case test_cases[] = { DEFINE_TEST_CASE(barycentric_overflow), DEFINE_TEST_CASE(bilinear_interpolation), DEFINE_TEST_CASE(fill_ellipse), + DEFINE_TEST_CASE(custom_font), }; #define TEST_CASES_COUNT (sizeof(test_cases)/sizeof(test_cases[0])) diff --git a/test/custom_font_expected.png b/test/custom_font_expected.png new file mode 100644 index 0000000000000000000000000000000000000000..c9df16f9316619e00c18ad2a171735499f54b50c GIT binary patch literal 15947 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 VXA#$K<-8;gur%LgR%VJn`F{r-->Coq literal 0 HcmV?d00001 diff --git a/tools/font2c.c b/tools/font2c.c new file mode 100644 index 0000000..77022d0 --- /dev/null +++ b/tools/font2c.c @@ -0,0 +1,250 @@ +#include +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H + +#define FROM_CHAR 32 +#define TO_CHAR 127 + +void determine_bitmap_sizes(FT_Face face, size_t *width, size_t *height, size_t *baseline) +{ + // Iterate over all desired glyphs + // find the maximum ascender height, by checking the glyphs bitmap_top value + // find the maximum descender depth, by checking bitmap_top - height + // height of all bitmaps has to be max ascender + max descender (+1? Is baseline included in asc or desc? or none?) + + // width of all bitmaps has to be the max width of any glyph + // TODO: align glyphs using bitmap_left + + FT_Int max_ascender = 0; + FT_Int max_descender = 0; + FT_UInt max_width = 0; + + for (unsigned char c = FROM_CHAR; c < TO_CHAR; ++c) { + int glyph_index = FT_Get_Char_Index(face, c); + + if (FT_Load_Glyph(face, glyph_index, FT_LOAD_RENDER)) { + fprintf(stderr, "There was a problem loading the glyph for A\n"); + continue; + } + + if (face->glyph->bitmap_top > max_ascender) { + max_ascender = face->glyph->bitmap_top; + } + if (((FT_Int)face->glyph->bitmap.rows - face->glyph->bitmap_top) > max_descender) { + max_descender = (face->glyph->bitmap.rows - face->glyph->bitmap_top); + } + if (face->glyph->bitmap.width > max_width) { + max_width = face->glyph->bitmap.width; + } + } + + *height = (size_t) (max_ascender + max_descender); + *width = (size_t) max_width; + *baseline = (size_t) max_ascender; +} + +void write_byte_array_code_for_char_to_file(FILE *out, FT_Face face, const unsigned char c, const size_t width, const size_t height, const size_t baseline) +{ + int glyph_index = FT_Get_Char_Index(face, c); + + if(FT_Load_Glyph(face, glyph_index, FT_LOAD_RENDER)){ + fprintf(stderr, "There was a problem loading the glyph for %c\n", c); + // TODO: return a fallback, like missing character box + return; + } + + // work out how many lines to skip at start + int skip_lines = (int) baseline - face->glyph->bitmap_top; + if(skip_lines < 0) skip_lines = 0; + + fprintf(out, " [%d] = { // \'%c\'\n", c, c); + FT_Bitmap *bitmap = &face->glyph->bitmap; + for (size_t y = 0; y < height; ++y) { + fprintf(out, " {"); + if (y < (unsigned int) skip_lines || y >= (unsigned int) (skip_lines + bitmap->rows)) { + for (size_t x = 0; x < width; ++x) { + fprintf(out, "0, "); + } + } else { + for (size_t x = 0; x < width; ++x) { + if (x >= bitmap->width) { + fprintf(out, "0, "); + continue; + } + fprintf(out, "%c, ", bitmap->buffer[bitmap->width * (y - skip_lines) + x] == 0 ? '0' : '1'); + } + } + fprintf(out, "},\n"); + } + fprintf(out, " },\n"); +} + +int generate_c_file_from_ttf(const char *input_file_path, const char *output_file_path, const char *name) +{ + FT_Library library; + FT_Face face; + + int error = 0; + + error = FT_Init_FreeType(&library); + if (error) { + fprintf(stderr, "ERROR: there was a problem initializing the FreeType library\n"); + return 1; + } + + // TODO: allow user to choose a face_index other than 0 + // TODO: check for available face_indices by setting face_index to -1 and checking face->num_faces + error = FT_New_Face(library, input_file_path, 0, &face); + if (error == FT_Err_Unknown_File_Format) { + fprintf(stderr, "ERROR: font file %s: format is not supported\n", input_file_path); + return 1; + } else if(error || face == NULL){ + fprintf(stderr, "ERROR: font file %s could not be read\n", input_file_path); + return 1; + } + + // TODO: let user specify a resolution and font size + error = FT_Set_Char_Size(face, 0, 16*64, 72, 72); + + size_t width, height, baseline; + determine_bitmap_sizes(face, &width, &height, &baseline); + + FILE *out = NULL; + + if (output_file_path) { + out = fopen(output_file_path, "wb"); + if (out == NULL) { + fprintf(stderr, "ERROR: could not write to file `%s`: %s\n", output_file_path, strerror(errno)); + return -1; + } + } else { + out = stdout; + } + + fprintf(out, "#include \"olive.c\"\n\n"); + fprintf(out, "static char %s_glyphs[%zu][%zu][%zu] = {\n", name, TO_CHAR, height, width); + + for (unsigned char c = FROM_CHAR; c < TO_CHAR; ++c) { + write_byte_array_code_for_char_to_file(out, face, c, width, height, baseline); + } + + fprintf(out, "};\n"); + + // static Olivec_Font olivec_default_font = { + // .glyphs = &olivec_default_glyphs[0][0][0], + // .width = OLIVEC_DEFAULT_FONT_WIDTH, + // .height = OLIVEC_DEFAULT_FONT_HEIGHT, + // }; + + fprintf(out, "\nstatic Olivec_Font %s = {\n", name); + fprintf(out, " .glyphs = &%s_glyphs[0][0][0],\n", name); + fprintf(out, " .width = %zu,\n", width); + fprintf(out, " .height = %zu,\n", height); + fprintf(out, "};\n"); + + return 0; +} + +const char *shift(int *argc, char ***argv) +{ + assert(*argc > 0); + const char *result = *argv[0]; + *argc -= 1; + *argv += 1; + return result; +} + +void usage(FILE *out, const char *program_name) +{ + fprintf(out, "Usage: %s [OPTIONS] \n", program_name); + fprintf(out, "Options:\n"); + fprintf(out, " -o \n"); + fprintf(out, " -n \n"); + // TODO: fprintf(out, " -i \n"); +} + +int main(int argc, char *argv[]) +{ + assert(argc > 0); + const char *program_name = shift(&argc, &argv); + const char *output_file_path = NULL; + const char *input_file_path = NULL; + const char *name = NULL; + + while (argc > 0) { + const char *flag = shift(&argc, &argv); + if (strcmp(flag, "-o") == 0) { + if (argc <= 0) { + usage(stderr, program_name); + fprintf(stderr, "ERROR: no value is provided for flag %s\n", flag); + return 1; + } + + if (output_file_path != NULL) { + usage(stderr, program_name); + fprintf(stderr, "ERROR: %s was already provided\n", flag); + return 1; + } + + output_file_path = shift(&argc, &argv); + } else if (strcmp(flag, "-n") == 0) { + if (argc <= 0) { + usage(stderr, program_name); + fprintf(stderr, "ERROR: no value is provided for flag %s\n", flag); + return 1; + } + + if (name != NULL) { + usage(stderr, program_name); + fprintf(stderr, "ERROR: %s was already provided\n", flag); + return 1; + } + + name = shift(&argc, &argv); + } else { + if (input_file_path != NULL) { + usage(stderr, program_name); + fprintf(stderr, "ERROR: input file path was already provided\n"); + return 1; + } + input_file_path = flag; + } + } + + if (input_file_path == NULL) { + usage(stderr, program_name); + fprintf(stderr, "ERROR: expected input file path\n"); + return(1); + } + + if (name == NULL) { + // TODO: infer a fitting name from input path + name = "font"; + } else { + size_t n = strlen(name); + if (n == 0) { + fprintf(stderr, "ERROR: name cannot be empty\n"); + return 1; + } + + if (isdigit(name[0])) { + fprintf(stderr, "ERROR: name cannot start from a digit\n"); + return 1; + } + + for (size_t i = 0; i < n; ++i) { + if (!isalnum(name[i]) && name[i] != '_') { + fprintf(stderr, "ERROR: name can only contains alphanumeric characters and underscores\n"); + return 1; + } + } + } + + return generate_c_file_from_ttf(input_file_path, output_file_path, name); +}