From a751569bde3864ccb1d46c084c0a6796ec872ec6 Mon Sep 17 00:00:00 2001 From: fvanroie <15969459+fvanroie@users.noreply.github.com> Date: Sat, 26 Jun 2021 01:27:50 +0200 Subject: [PATCH] Add font_loader --- src/font/hasp_font_loader.cpp | 681 ++++++++++++++++++++++++++++++++++ src/font/hasp_font_loader.h | 44 +++ 2 files changed, 725 insertions(+) create mode 100644 src/font/hasp_font_loader.cpp create mode 100644 src/font/hasp_font_loader.h diff --git a/src/font/hasp_font_loader.cpp b/src/font/hasp_font_loader.cpp new file mode 100644 index 00000000..b1977894 --- /dev/null +++ b/src/font/hasp_font_loader.cpp @@ -0,0 +1,681 @@ +/** + * @file hasp_font_loader.c + * + */ + +/********************* + * INCLUDES + *********************/ + +#include +#include + +#include "lvgl.h" +#include "lv_misc/lv_fs.h" +#include "hasp_font_loader.h" + +#include "hasplib.h" + +#if LV_USE_FILESYSTEM + +/********************** + * TYPEDEFS + **********************/ +typedef struct +{ + lv_fs_file_t* fp; + int8_t bit_pos; + uint8_t byte_value; +} bit_iterator_t; + +typedef struct font_header_bin +{ + uint32_t version; + uint16_t tables_count; + uint16_t font_size; + uint16_t ascent; + int16_t descent; + uint16_t typo_ascent; + int16_t typo_descent; + uint16_t typo_line_gap; + int16_t min_y; + int16_t max_y; + uint16_t default_advance_width; + uint16_t kerning_scale; + uint8_t index_to_loc_format; + uint8_t glyph_id_format; + uint8_t advance_width_format; + uint8_t bits_per_pixel; + uint8_t xy_bits; + uint8_t wh_bits; + uint8_t advance_width_bits; + uint8_t compression_id; + uint8_t subpixels_mode; + uint8_t padding; + int16_t underline_position; + uint16_t underline_thickness; +} font_header_bin_t; + +typedef struct cmap_table_bin +{ + uint32_t data_offset; + uint32_t range_start; + uint16_t range_length; + uint16_t glyph_id_start; + uint16_t data_entries_count; + uint8_t format_type; + uint8_t padding; +} cmap_table_bin_t; + +/********************** + * STATIC PROTOTYPES + **********************/ +static bit_iterator_t init_bit_iterator(lv_fs_file_t* fp); +static bool lvgl_load_font(lv_fs_file_t* fp, lv_font_t* font); +int32_t load_kern(lv_fs_file_t* fp, lv_font_fmt_txt_dsc_t* font_dsc, uint8_t format, uint32_t start); + +static int read_bits_signed(bit_iterator_t* it, int n_bits, lv_fs_res_t* res); +static unsigned int read_bits(bit_iterator_t* it, int n_bits, lv_fs_res_t* res); + +/********************** + * MACROS + **********************/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +/** + * Loads a `lv_font_t` object from a binary font file + * @param font_name filename where the font file is located + * @return a pointer to the font or NULL in case of error + */ +lv_font_t* hasp_font_load(const char* font_name) +{ + bool success = false; + + lv_font_t* font = (lv_font_t*)malloc(sizeof(lv_font_t)); + memset(font, 0, sizeof(lv_font_t)); + + lv_fs_file_t file; + lv_fs_res_t res = lv_fs_open(&file, font_name, LV_FS_MODE_RD); + + if(res == LV_FS_RES_OK) { + success = lvgl_load_font(&file, font); + + if(!success) { + LOG_WARNING(TAG_FONT, "Error loading font file: %s\n", font_name); + /* + * When `lvgl_load_font` fails it can leak some pointers. + * All non-null pointers can be assumed as allocated and + * `lv_font_free` should free them correctly. + */ + hasp_font_free(font); + font = NULL; + } + + lv_fs_close(&file); + } + + return font; +} + +/** + * Frees the memory allocated by the `lv_font_load()` function + * @param font lv_font_t object created by the lv_font_load function + */ +void hasp_font_free(lv_font_t* font) +{ + if(NULL != font) { + lv_font_fmt_txt_dsc_t* dsc = (lv_font_fmt_txt_dsc_t*)font->dsc; + + if(NULL != dsc) { + + if(dsc->kern_classes == 0) { + lv_font_fmt_txt_kern_pair_t* kern_dsc = (lv_font_fmt_txt_kern_pair_t*)dsc->kern_dsc; + + if(NULL != kern_dsc) { + if(kern_dsc->glyph_ids) free((void*)kern_dsc->glyph_ids); + + if(kern_dsc->values) free((void*)kern_dsc->values); + + free((void*)kern_dsc); + } + } else { + lv_font_fmt_txt_kern_classes_t* kern_dsc = (lv_font_fmt_txt_kern_classes_t*)dsc->kern_dsc; + + if(NULL != kern_dsc) { + if(kern_dsc->class_pair_values) free((void*)kern_dsc->class_pair_values); + + if(kern_dsc->left_class_mapping) free((void*)kern_dsc->left_class_mapping); + + if(kern_dsc->right_class_mapping) free((void*)kern_dsc->right_class_mapping); + + free((void*)kern_dsc); + } + } + + lv_font_fmt_txt_cmap_t* cmaps = (lv_font_fmt_txt_cmap_t*)dsc->cmaps; + + if(NULL != cmaps) { + for(int i = 0; i < dsc->cmap_num; ++i) { + if(NULL != cmaps[i].glyph_id_ofs_list) free((void*)cmaps[i].glyph_id_ofs_list); + if(NULL != cmaps[i].unicode_list) free((void*)cmaps[i].unicode_list); + } + free(cmaps); + } + + if(NULL != dsc->glyph_bitmap) { + free((void*)dsc->glyph_bitmap); + } + if(NULL != dsc->glyph_dsc) { + free((void*)dsc->glyph_dsc); + } + free(dsc); + } + free(font); + } +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +static bit_iterator_t init_bit_iterator(lv_fs_file_t* fp) +{ + bit_iterator_t it; + it.fp = fp; + it.bit_pos = -1; + it.byte_value = 0; + return it; +} + +static unsigned int read_bits(bit_iterator_t* it, int n_bits, lv_fs_res_t* res) +{ + unsigned int value = 0; + while(n_bits--) { + it->byte_value = it->byte_value << 1; + it->bit_pos--; + + if(it->bit_pos < 0) { + it->bit_pos = 7; + *res = lv_fs_read(it->fp, &(it->byte_value), 1, NULL); + if(*res != LV_FS_RES_OK) { + return 0; + } + } + int8_t bit = (it->byte_value & 0x80) ? 1 : 0; + + value |= (bit << n_bits); + } + *res = LV_FS_RES_OK; + return value; +} + +static int read_bits_signed(bit_iterator_t* it, int n_bits, lv_fs_res_t* res) +{ + unsigned int value = read_bits(it, n_bits, res); + if(value & (1 << (n_bits - 1))) { + value |= ~0u << n_bits; + } + return value; +} + +static int read_label(lv_fs_file_t* fp, int start, const char* label) +{ + lv_fs_seek(fp, start); + + uint32_t length; + char buf[4]; + + if(lv_fs_read(fp, &length, 4, NULL) != LV_FS_RES_OK || lv_fs_read(fp, buf, 4, NULL) != LV_FS_RES_OK || + memcmp(label, buf, 4) != 0) { + LOG_WARNING(TAG_FONT, "Error reading '%s' label.", label); + return -1; + } + + return length; +} + +static bool load_cmaps_tables(lv_fs_file_t* fp, lv_font_fmt_txt_dsc_t* font_dsc, uint32_t cmaps_start, + cmap_table_bin_t* cmap_table) +{ + if(lv_fs_read(fp, cmap_table, font_dsc->cmap_num * sizeof(cmap_table_bin_t), NULL) != LV_FS_RES_OK) { + return false; + } + + for(unsigned int i = 0; i < font_dsc->cmap_num; ++i) { + lv_fs_res_t res = lv_fs_seek(fp, cmaps_start + cmap_table[i].data_offset); + if(res != LV_FS_RES_OK) { + return false; + } + + lv_font_fmt_txt_cmap_t* cmap = (lv_font_fmt_txt_cmap_t*)&(font_dsc->cmaps[i]); + + cmap->range_start = cmap_table[i].range_start; + cmap->range_length = cmap_table[i].range_length; + cmap->glyph_id_start = cmap_table[i].glyph_id_start; + cmap->type = cmap_table[i].format_type; + + switch(cmap_table[i].format_type) { + case LV_FONT_FMT_TXT_CMAP_FORMAT0_FULL: { + uint8_t ids_size = sizeof(uint8_t) * cmap_table[i].data_entries_count; + uint8_t* glyph_id_ofs_list = (uint8_t*)malloc(ids_size); + + cmap->glyph_id_ofs_list = glyph_id_ofs_list; + + if(lv_fs_read(fp, glyph_id_ofs_list, ids_size, NULL) != LV_FS_RES_OK) { + return false; + } + + cmap->list_length = cmap->range_length; + break; + } + case LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY: + break; + case LV_FONT_FMT_TXT_CMAP_SPARSE_FULL: + case LV_FONT_FMT_TXT_CMAP_SPARSE_TINY: { + uint32_t list_size = sizeof(uint16_t) * cmap_table[i].data_entries_count; + uint16_t* unicode_list = (uint16_t*)malloc(list_size); + + cmap->unicode_list = unicode_list; + cmap->list_length = cmap_table[i].data_entries_count; + + if(lv_fs_read(fp, unicode_list, list_size, NULL) != LV_FS_RES_OK) { + return false; + } + + if(cmap_table[i].format_type == LV_FONT_FMT_TXT_CMAP_SPARSE_FULL) { + uint16_t* buf = (uint16_t*)malloc(sizeof(uint16_t) * cmap->list_length); + + cmap->glyph_id_ofs_list = buf; + + if(lv_fs_read(fp, buf, sizeof(uint16_t) * cmap->list_length, NULL) != LV_FS_RES_OK) { + return false; + } + } + break; + } + default: + LOG_WARNING(TAG_FONT, "Unknown cmaps format type %d.", cmap_table[i].format_type); + return false; + } + } + return true; +} + +static int32_t load_cmaps(lv_fs_file_t* fp, lv_font_fmt_txt_dsc_t* font_dsc, uint32_t cmaps_start) +{ + int32_t cmaps_length = read_label(fp, cmaps_start, "cmap"); + if(cmaps_length < 0) { + return -1; + } + + uint32_t cmaps_subtables_count; + if(lv_fs_read(fp, &cmaps_subtables_count, sizeof(uint32_t), NULL) != LV_FS_RES_OK) { + return -1; + } + + lv_font_fmt_txt_cmap_t* cmaps = + (lv_font_fmt_txt_cmap_t*)malloc(cmaps_subtables_count * sizeof(lv_font_fmt_txt_cmap_t)); + + memset(cmaps, 0, cmaps_subtables_count * sizeof(lv_font_fmt_txt_cmap_t)); + + font_dsc->cmaps = cmaps; + font_dsc->cmap_num = cmaps_subtables_count; + + cmap_table_bin_t* cmaps_tables = (cmap_table_bin_t*)malloc(sizeof(cmap_table_bin_t) * font_dsc->cmap_num); + + bool success = load_cmaps_tables(fp, font_dsc, cmaps_start, cmaps_tables); + + free(cmaps_tables); + + return success ? cmaps_length : -1; +} + +static int32_t load_glyph(lv_fs_file_t* fp, lv_font_fmt_txt_dsc_t* font_dsc, uint32_t start, uint32_t* glyph_offset, + uint32_t loca_count, font_header_bin_t* header) +{ + int32_t glyph_length = read_label(fp, start, "glyf"); + if(glyph_length < 0) { + return -1; + } + + lv_font_fmt_txt_glyph_dsc_t* glyph_dsc = + (lv_font_fmt_txt_glyph_dsc_t*)malloc(loca_count * sizeof(lv_font_fmt_txt_glyph_dsc_t)); + + memset(glyph_dsc, 0, loca_count * sizeof(lv_font_fmt_txt_glyph_dsc_t)); + + font_dsc->glyph_dsc = glyph_dsc; + + int cur_bmp_size = 0; + + for(unsigned int i = 0; i < loca_count; ++i) { + lv_font_fmt_txt_glyph_dsc_t* gdsc = &glyph_dsc[i]; + + lv_fs_res_t res = lv_fs_seek(fp, start + glyph_offset[i]); + if(res != LV_FS_RES_OK) { + return -1; + } + + bit_iterator_t bit_it = init_bit_iterator(fp); + + if(header->advance_width_bits == 0) { + gdsc->adv_w = header->default_advance_width; + } else { + gdsc->adv_w = read_bits(&bit_it, header->advance_width_bits, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + } + + if(header->advance_width_format == 0) { + gdsc->adv_w *= 16; + } + + gdsc->ofs_x = read_bits_signed(&bit_it, header->xy_bits, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + + gdsc->ofs_y = read_bits_signed(&bit_it, header->xy_bits, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + + gdsc->box_w = read_bits(&bit_it, header->wh_bits, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + + gdsc->box_h = read_bits(&bit_it, header->wh_bits, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + + int nbits = header->advance_width_bits + 2 * header->xy_bits + 2 * header->wh_bits; + int next_offset = (i < loca_count - 1) ? glyph_offset[i + 1] : (uint32_t)glyph_length; + int bmp_size = next_offset - glyph_offset[i] - nbits / 8; + + if(i == 0) { + gdsc->adv_w = 0; + gdsc->box_w = 0; + gdsc->box_h = 0; + gdsc->ofs_x = 0; + gdsc->ofs_y = 0; + } + + gdsc->bitmap_index = cur_bmp_size; + if(gdsc->box_w * gdsc->box_h != 0) { + cur_bmp_size += bmp_size; + } + } + + uint8_t* glyph_bmp; +#ifdef ESP32 + if(psramFound()) + glyph_bmp = (uint8_t*)ps_malloc(sizeof(uint8_t) * cur_bmp_size); + else + glyph_bmp = (uint8_t*)malloc(sizeof(uint8_t) * cur_bmp_size); +#else + glyph_bmp = (uint8_t*)malloc(sizeof(uint8_t) * cur_bmp_size); +#endif + + font_dsc->glyph_bitmap = glyph_bmp; + + cur_bmp_size = 0; + + for(unsigned int i = 1; i < loca_count; ++i) { + lv_fs_res_t res = lv_fs_seek(fp, start + glyph_offset[i]); + if(res != LV_FS_RES_OK) { + return -1; + } + bit_iterator_t bit_it = init_bit_iterator(fp); + + int nbits = header->advance_width_bits + 2 * header->xy_bits + 2 * header->wh_bits; + + read_bits(&bit_it, nbits, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + + if(glyph_dsc[i].box_w * glyph_dsc[i].box_h == 0) { + continue; + } + + int next_offset = (i < loca_count - 1) ? glyph_offset[i + 1] : (uint32_t)glyph_length; + int bmp_size = next_offset - glyph_offset[i] - nbits / 8; + + if(nbits % 8 == 0) { /* Fast path */ + if(lv_fs_read(fp, &glyph_bmp[cur_bmp_size], bmp_size, NULL) != LV_FS_RES_OK) { + return -1; + } + } else { + for(int k = 0; k < bmp_size - 1; ++k) { + glyph_bmp[cur_bmp_size + k] = read_bits(&bit_it, 8, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + } + glyph_bmp[cur_bmp_size + bmp_size - 1] = read_bits(&bit_it, 8 - nbits % 8, &res); + if(res != LV_FS_RES_OK) { + return -1; + } + } + + cur_bmp_size += bmp_size; + } + return glyph_length; +} + +/* + * Loads a `lv_font_t` from a binary file, given a `lv_fs_file_t`. + * + * Memory allocations on `lvgl_load_font` should be immediately zeroed and + * the pointer should be set on the `lv_font_t` data before any possible return. + * + * When something fails, it returns `false` and the memory on the `lv_font_t` + * still needs to be freed using `lv_font_free`. + * + * `lv_font_free` will assume that all non-null pointers are allocated and + * should be freed. + */ +static bool lvgl_load_font(lv_fs_file_t* fp, lv_font_t* font) +{ + lv_font_fmt_txt_dsc_t* font_dsc = (lv_font_fmt_txt_dsc_t*)malloc(sizeof(lv_font_fmt_txt_dsc_t)); + + memset(font_dsc, 0, sizeof(lv_font_fmt_txt_dsc_t)); + + font->dsc = font_dsc; + + /* header */ + int32_t header_length = read_label(fp, 0, "head"); + if(header_length < 0) { + return false; + } + + font_header_bin_t font_header; + if(lv_fs_read(fp, &font_header, sizeof(font_header_bin_t), NULL) != LV_FS_RES_OK) { + return false; + } + + font->base_line = -font_header.descent; + font->line_height = font_header.ascent - font_header.descent; + font->get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt; + font->get_glyph_bitmap = lv_font_get_bitmap_fmt_txt; + font->subpx = font_header.subpixels_mode; + font->underline_position = font_header.underline_position; + font->underline_thickness = font_header.underline_thickness; + + font_dsc->bpp = font_header.bits_per_pixel; + font_dsc->kern_scale = font_header.kerning_scale; + font_dsc->bitmap_format = font_header.compression_id; + + /* cmaps */ + uint32_t cmaps_start = header_length; + int32_t cmaps_length = load_cmaps(fp, font_dsc, cmaps_start); + if(cmaps_length < 0) { + return false; + } + + /* loca */ + uint32_t loca_start = cmaps_start + cmaps_length; + int32_t loca_length = read_label(fp, loca_start, "loca"); + if(loca_length < 0) { + return false; + } + + uint32_t loca_count; + if(lv_fs_read(fp, &loca_count, sizeof(uint32_t), NULL) != LV_FS_RES_OK) { + return false; + } + + bool failed = false; + uint32_t* glyph_offset = (uint32_t*)malloc(sizeof(uint32_t) * (loca_count + 1)); + + /*TODO check NULL*/ + + if(font_header.index_to_loc_format == 0) { + for(unsigned int i = 0; i < loca_count; ++i) { + uint16_t offset; + if(lv_fs_read(fp, &offset, sizeof(uint16_t), NULL) != LV_FS_RES_OK) { + failed = true; + break; + } + glyph_offset[i] = offset; + } + } else if(font_header.index_to_loc_format == 1) { + if(lv_fs_read(fp, glyph_offset, loca_count * sizeof(uint32_t), NULL) != LV_FS_RES_OK) { + failed = true; + } + } else { + LOG_WARNING(TAG_FONT, "Unknown index_to_loc_format: %d.", font_header.index_to_loc_format); + failed = true; + } + + if(failed) { + free(glyph_offset); + return false; + } + + /* glyph */ + uint32_t glyph_start = loca_start + loca_length; + int32_t glyph_length = load_glyph(fp, font_dsc, glyph_start, glyph_offset, loca_count, &font_header); + + free(glyph_offset); + + if(glyph_length < 0) { + return false; + } + + // if(font_header.tables_count < 4) { + font_dsc->kern_dsc = NULL; + font_dsc->kern_classes = 0; + font_dsc->kern_scale = 0; + return true; + // } + + // uint32_t kern_start = glyph_start + glyph_length; + + // int32_t kern_length = load_kern(fp, font_dsc, font_header.glyph_id_format, kern_start); + + // return kern_length >= 0; +} + +// int32_t load_kern(lv_fs_file_t * fp, lv_font_fmt_txt_dsc_t * font_dsc, uint8_t format, uint32_t start) +// { +// int32_t kern_length = read_label(fp, start, "kern"); +// if(kern_length < 0) { +// return -1; +// } + +// uint8_t kern_format_type; +// int32_t padding; +// if(lv_fs_read(fp, &kern_format_type, sizeof(uint8_t), NULL) != LV_FS_RES_OK || +// lv_fs_read(fp, &padding, 3 * sizeof(uint8_t), NULL) != LV_FS_RES_OK) { +// return -1; +// } + +// if(0 == kern_format_type) { /* sorted pairs */ +// lv_font_fmt_txt_kern_pair_t * kern_pair = malloc(sizeof(lv_font_fmt_txt_kern_pair_t)); + +// memset(kern_pair, 0, sizeof(lv_font_fmt_txt_kern_pair_t)); + +// font_dsc->kern_dsc = kern_pair; +// font_dsc->kern_classes = 0; + +// uint32_t glyph_entries; +// if(lv_fs_read(fp, &glyph_entries, sizeof(uint32_t), NULL) != LV_FS_RES_OK) { +// return -1; +// } + +// int ids_size; +// if(format == 0) { +// ids_size = sizeof(int8_t) * 2 * glyph_entries; +// } +// else { +// ids_size = sizeof(int16_t) * 2 * glyph_entries; +// } + +// uint8_t * glyph_ids = malloc(ids_size); +// int8_t * values = malloc(glyph_entries); + +// kern_pair->glyph_ids_size = format; +// kern_pair->pair_cnt = glyph_entries; +// kern_pair->glyph_ids = glyph_ids; +// kern_pair->values = values; + +// if(lv_fs_read(fp, glyph_ids, ids_size, NULL) != LV_FS_RES_OK) { +// return -1; +// } + +// if(lv_fs_read(fp, values, glyph_entries, NULL) != LV_FS_RES_OK) { +// return -1; +// } +// } +// else if(3 == kern_format_type) { /* array M*N of classes */ + +// lv_font_fmt_txt_kern_classes_t * kern_classes = malloc(sizeof(lv_font_fmt_txt_kern_classes_t)); + +// memset(kern_classes, 0, sizeof(lv_font_fmt_txt_kern_classes_t)); + +// font_dsc->kern_dsc = kern_classes; +// font_dsc->kern_classes = 1; + +// uint16_t kern_class_mapping_length; +// uint8_t kern_table_rows; +// uint8_t kern_table_cols; + +// if(lv_fs_read(fp, &kern_class_mapping_length, sizeof(uint16_t), NULL) != LV_FS_RES_OK || +// lv_fs_read(fp, &kern_table_rows, sizeof(uint8_t), NULL) != LV_FS_RES_OK || +// lv_fs_read(fp, &kern_table_cols, sizeof(uint8_t), NULL) != LV_FS_RES_OK) { +// return -1; +// } + +// int kern_values_length = sizeof(int8_t) * kern_table_rows * kern_table_cols; + +// uint8_t * kern_left = malloc(kern_class_mapping_length); +// uint8_t * kern_right = malloc(kern_class_mapping_length); +// int8_t * kern_values = malloc(kern_values_length); + +// kern_classes->left_class_mapping = kern_left; +// kern_classes->right_class_mapping = kern_right; +// kern_classes->left_class_cnt = kern_table_rows; +// kern_classes->right_class_cnt = kern_table_cols; +// kern_classes->class_pair_values = kern_values; + +// if(lv_fs_read(fp, kern_left, kern_class_mapping_length, NULL) != LV_FS_RES_OK || +// lv_fs_read(fp, kern_right, kern_class_mapping_length, NULL) != LV_FS_RES_OK || +// lv_fs_read(fp, kern_values, kern_values_length, NULL) != LV_FS_RES_OK) { +// return -1; +// } +// } +// else { +// LOG_WARNING(TAG_FONT,"Unknown kern_format_type: %d", kern_format_type); +// return -1; +// } + +// return kern_length; +// } + +#endif /*LV_USE_FILESYSTEM*/ diff --git a/src/font/hasp_font_loader.h b/src/font/hasp_font_loader.h new file mode 100644 index 00000000..f0e80085 --- /dev/null +++ b/src/font/hasp_font_loader.h @@ -0,0 +1,44 @@ +/** + * @file hasp_font_loader.h + * + */ + +#ifndef HASP_FONT_LOADER_H +#define HASP_FONT_LOADER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +#if LV_USE_FILESYSTEM + +lv_font_t * hasp_font_load(const char * fontName); +void hasp_font_free(lv_font_t * font); + +#endif + +/********************** + * MACROS + **********************/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /*LV_FONT_LOADER_H*/