Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools/font2c.c #25

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nobuild.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions test.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "./assets/tsodinPog.c"
#include "./assets/tsodinCup.c"
#include "./assets/testFont.c"

#define PI 3.14159265359

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]))

Expand Down
Binary file added test/custom_font_expected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
250 changes: 250 additions & 0 deletions tools/font2c.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
#include <assert.h>
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include <ft2build.h>
#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] <input/file/path.ttf>\n", program_name);
fprintf(out, "Options:\n");
fprintf(out, " -o <output/file/path.h>\n");
fprintf(out, " -n <name>\n");
// TODO: fprintf(out, " -i <font face index>\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);
}