diff --git a/.gitmodules b/.gitmodules index 1a63268a..2653f226 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "stb"] path = stb url = https://github.com/nothings/stb.git +[submodule "libunibreak"] + path = libunibreak + url = https://github.com/adah1972/libunibreak diff --git a/CREDITS b/CREDITS index 2086b0bf..579d1b54 100644 --- a/CREDITS +++ b/CREDITS @@ -8,10 +8,13 @@ This project makes use of a number of third-party libraries, which we'll acknowl Released into the public domain. (https://www.cprogramming.com/tutorial/unicode.html) - * stb_image (Image loader), by Sean Barrett, + * stb_image (Image loader) and stb_truetype (font renderer), by Sean Barrett, Released into the public domain. (http://nothings.org/stb) + * libunibreak (Unicode line breaking implementation) by Wu Yongwei, et al + Licensed under the Zlib license + (https://github.com/adah1972/libunibreak) As well as a number of third-party resources (namely, font files): diff --git a/Makefile b/Makefile index 5a886f0a..febc2945 100644 --- a/Makefile +++ b/Makefile @@ -179,10 +179,16 @@ ifdef MATHS EXTRA_CPPFLAGS+=-DFBINK_WITH_MATHS LIBS+=-lm endif - +ifndef MINIMAL + LIBS+=-lm +endif ## # Now that we're done fiddling with flags, let's build stuff! -LIB_SRCS=fbink.c utf8/utf8.c +ifdef MINIMAL + LIB_SRCS=fbink.c utf8/utf8.c +else + LIB_SRCS=fbink.c utf8/utf8.c libunibreak/src/linebreak.c libunibreak/src/linebreakdata.c libunibreak/src/unibreakdef.c libunibreak/src/linebreakdef.c +endif CMD_SRCS=fbink_cmd.c BTN_SRCS=button_scan_cmd.c # Unless we're asking for a minimal build, include the Unscii fonts, too @@ -191,6 +197,7 @@ ifdef MINIMAL else EXTRA_CPPFLAGS+=-DFBINK_WITH_FONTS EXTRA_CPPFLAGS+=-DFBINK_WITH_IMAGE + EXTRA_CPPFLAGS+=-DFBINK_WITH_OPENTYPE # Connect button scanning is Kobo specific ifndef KINDLE ifndef CERVANTES @@ -235,7 +242,7 @@ $(OUT_DIR)/%.o: %.c $(CC) $(CPPFLAGS) $(EXTRA_CPPFLAGS) $(CFLAGS) $(EXTRA_CFLAGS) $(SHARED_CFLAGS) -o $@ -c $< outdir: - mkdir -p $(OUT_DIR)/shared/utf8 $(OUT_DIR)/static/utf8 + mkdir -p $(OUT_DIR)/shared/utf8 $(OUT_DIR)/static/utf8 $(OUT_DIR)/shared/libunibreak/src $(OUT_DIR)/static/libunibreak/src all: outdir static @@ -348,8 +355,10 @@ clean: rm -rf Release/*.so* rm -rf Release/shared/*.o rm -rf Release/shared/utf8/*.o + rm -rf Release/shared/libunibreak/src/*.o rm -rf Release/static/*.o rm -rf Release/static/utf8/*.o + rm -rf Release/static/libunibreak/src/*.o rm -rf Release/*.o rm -rf Release/fbink rm -rf Release/button_scan @@ -357,8 +366,10 @@ clean: rm -rf Debug/*.so* rm -rf Debug/shared/*.o rm -rf Debug/shared/utf8/*.o + rm -rf Debug/shared/libunibreak/src/*.o rm -rf Debug/static/*.o rm -rf Debug/static/utf8/*.o + rm -rf Debug/static/libunibreak/src/*.o rm -rf Debug/*.o rm -rf Debug/fbink rm -rf Debug/button_scan diff --git a/fbink.c b/fbink.c index c620dbf2..35fecbe5 100644 --- a/fbink.c +++ b/fbink.c @@ -61,6 +61,30 @@ # pragma GCC diagnostic pop #endif +#ifdef FBINK_WITH_OPENTYPE +# include +// stb_truetype needs maths, and so do we to round to the nearest pixel +# include +# define STB_TRUETYPE_IMPLEMENTATION +// Make it private, we don't need it anywhere else +# define STBTT_STATIC +// stb_truetype is.... noisy +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunknown-pragmas" +# pragma clang diagnostic ignored "-Wunknown-warning-option" +# pragma GCC diagnostic ignored "-Wcast-qual" +# pragma GCC diagnostic ignored "-Wcast-align" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wduplicated-branches" +# pragma GCC diagnostic ignored "-Wunused-parameter" +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wbad-function-cast" +# include "stb/stb_truetype.h" +# pragma GCC diagnostic pop +# include "libunibreak/src/linebreak.h" +#endif + // Return the library version as devised at library compile-time const char* fbink_version(void) @@ -2028,6 +2052,101 @@ int // Don't skip any ioctls on a first init ;) return initialize_fbink(fbfd, fbink_config, false); } +// Load font from given file path. Up to four font variants may be used by FBInk at any given time. +int + fbink_add_ot_font(const char* fp, FONT_VARIANT_T variant) +{ +#ifdef FBINK_WITH_OPENTYPE + otInit = true; + // Open font from given path, and load into buffer + FILE *f = fopen(fp, "rb"); + unsigned char *data = NULL; + if (f) { + int fd = fileno(f); + struct stat st; + fstat(fd, &st); + data = calloc(1, st.st_size); + if (!data) { + fclose(f); + otInit = false; + ELOG("[FBInk] Error allocating font data buffer."); + return(ERRCODE(EXIT_FAILURE)); + } + size_t read = fread(data, 1, st.st_size, f); + if (read != st.st_size) { + free(data); + fclose(f); + otInit = false; + ELOG("[FBInk] Error reading font file."); + return(ERRCODE(EXIT_FAILURE)); + } + fclose(f); + } + stbtt_fontinfo *font_info = calloc(1, sizeof(stbtt_fontinfo)); + if (!font_info) { + free(data); + ELOG("[FNInk] Error allocating stbtt_fontinfo struct"); + return ERRCODE(EXIT_FAILURE); + } + if (!stbtt_InitFont(font_info, data, 0)) { + ELOG("[FBInk] Error initialising font %s", fp); + return ERRCODE(EXIT_FAILURE); + } + // Assign the current font to it's appropriate otFonts struct member, depending + // on the variant specified by the caller. + // NOTE: We make sure we free any previous allocation first! + switch(variant) { + case FNT_REGULAR: + otFonts.otRegular = free_ot_font(otFonts.otRegular); + otFonts.otRegular = font_info; + break; + case FNT_ITALIC: + otFonts.otItalic = free_ot_font(otFonts.otItalic); + otFonts.otItalic = font_info; + break; + case FNT_BOLD: + otFonts.otBold = free_ot_font(otFonts.otBold); + otFonts.otBold = font_info; + break; + case FNT_BOLD_ITALIC: + otFonts.otBoldItalic = free_ot_font(otFonts.otBoldItalic); + otFonts.otBoldItalic = font_info; + break; + } + ELOG("[FBInk] Font %s loaded", fp); + return EXIT_SUCCESS; +#else + fprintf(stderr, "[FBInk] Opentype support is disabled in this FBInk build!\n"); + return ERRCODE(ENOSYS); +#endif // FBINK_WITH_OPENTYPE +} + +// Free an individual OpenType font structure +void* + free_ot_font(stbtt_fontinfo* font_info) +{ + if (font_info) { + free(font_info->data); // This is the font data we loaded + free(font_info); + } + return NULL; +} + +// Free all OpenType fonts +int + fbink_free_ot_fonts(void) +{ +#ifdef FBINK_WITH_OPENTYPE + free_ot_font(otFonts.otRegular); + free_ot_font(otFonts.otItalic); + free_ot_font(otFonts.otBold); + free_ot_font(otFonts.otBoldItalic); + return EXIT_SUCCESS; +#else + fprintf(stderr, "[FBInk] Opentype support is disabled in this FBInk build!\n"); + return ERRCODE(ENOSYS); +#endif // FBINK_WITH_OPENTYPE +} // Dump a few of our internal state variables to stdout, for shell script consumption void @@ -2595,6 +2714,826 @@ int return rv; } +// An extremely rudimentry "markdown" parser. It would probably be wise to cook up something better +// at some point... +// This is *italic* text. +// This is **bold** text. +// This is ***bold italic*** text. +// As well as their underscore equivalents +void + parse_simple_md(char* string, int size, unsigned char* result) +{ + int ci = 0; + char ch; + bool is_italic = false; + bool is_bold = false; + while (ci < size) { + //printf("ci: %d (< %d) is %c\n", ci, size, string[ci]); + switch (ch = string[ci]) { + case '*': + case '_': + if (ci + 1 < size && string[ci + 1] == ch) { + //printf("ci: %d && ci + 1 == %c\n", ci, ch); + if (ci + 2 < size && string[ci + 2] == ch) { + //printf("ci: %d && ci + 2 == %c\n", ci, ch); + is_bold = !is_bold; + is_italic = !is_italic; + result[ci] = CH_IGNORE; + result[ci + 1] = CH_IGNORE; + result[ci + 2] = CH_IGNORE; + ci += 3; + break; + } + is_bold = !is_bold; + result[ci] = CH_IGNORE; + result[ci + 1] = CH_IGNORE; + ci += 2; + break; + } + // Try to avoid flagging a single underscore in the middle of a word. + if (ch == '_' && ci > 0 && string[ci - 1] != ' ' && string[ci + 1] != ' ') { + result[ci] = CH_REGULAR; + ci++; + break; + } + is_italic = !is_italic; + result[ci] = CH_IGNORE; + ci++; + break; + default: + if (is_bold && is_italic) { + result[ci] = CH_BOLD_ITALIC; + } else if (is_bold) { + result[ci] = CH_BOLD; + } else if (is_italic) { + result[ci] = CH_ITALIC; + } else { + result[ci] = CH_REGULAR; + } + ci++; + break; + } + } +} + +int + fbink_print_ot(int fbfd, char* string, FBInkOTConfig* cfg, FBInkConfig* fbCfg) +{ +#ifdef FBINK_WITH_OPENTYPE + //Note, we do a lot of casting floats to ints, so silence those GCC warnings +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wfloat-conversion" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wbad-function-cast" + + // Abort if we were passed an empty string + if (! *string) { + ELOG("[FBInk] Cannot print empty string"); + return ERRCODE(EXIT_FAILURE); + } + + // Has fbink_init_ot() been called yet? + if (!otInit) { + ELOG("[FBInk] No fonts have been loaded"); + return ERRCODE(ENODATA); + } + + // Just in case we receive a NULL pointer to the cfg struct + if (!cfg) { + ELOG("[FBInk] FBInkOTConfig expected. Got NULL pointer instead"); + return ERRCODE(EXIT_FAILURE); + } + + // If we open a fd now, we'll only keep it open for this single print call! + // NOTE: We *expect* to be initialized at this point, though, but that's on the caller's hands! + bool keep_fd = true; + if (open_fb_fd(&fbfd, &keep_fd) != EXIT_SUCCESS) { + return ERRCODE(EXIT_FAILURE); + } + + // Assume success, until shit happens ;) + int rv = EXIT_SUCCESS; + + // Declare buffers early to make cleanup easier + FBInkOTLine* lines = NULL; + char * brk_buff = NULL; + unsigned char* fmt_buff = NULL; + unsigned char* line_buff = NULL; + unsigned char* glyph_buff = NULL; + + // map fb to user mem + // NOTE: If we're keeping the fb's fd open, keep this mmap around, too. + if (!isFbMapped) { + if (memmap_fb(fbfd) != EXIT_SUCCESS) { + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + } + LOG("Printing OpenType text."); + // Sanity check the provided margins and calculate the printable area. + // We'll cap the margin at 90% for each side. Margins for opposing edges + // should sum to less than 100% + if (cfg->margins.top > viewHeight || cfg->margins.bottom > viewHeight || cfg->margins.left > viewWidth || cfg->margins.right > viewWidth) { + ELOG("[FBInk] A margin was out of range (allowed ranges :: Vert < %u Horiz < %u)", (unsigned int)viewHeight, (unsigned int)viewWidth); + rv = ERRCODE(ERANGE); + goto cleanup; + } + if (cfg->margins.top + cfg->margins.bottom >= viewHeight || cfg->margins.left + cfg->margins.right >= viewWidth) { + ELOG("[FBInk] Opposing margins sum to greater than the viewport height or width"); + rv = ERRCODE(ERANGE); + goto cleanup; + } + struct { + FBInkCoordinates tl; + FBInkCoordinates br; + } area = { 0 }; + area.tl.x = cfg->margins.left; + area.tl.y = cfg->margins.top; + area.br.x = viewWidth - cfg->margins.right; + area.br.y = viewHeight - cfg->margins.bottom; + // Set default font size if required + uint8_t size_pt = cfg->size_pt; + if (!size_pt) { + size_pt = 12; + } + // TODO: handle ppi properly + unsigned int ppi = 265; + // Given the ppi, convert point height to pixels. Note, 1pt is 1/72th of an inch + unsigned int font_size_px = (unsigned int)(ppi / 72.0f * size_pt); + + // This is a pointer to whichever font is currently active. It gets updated for every + // character in the loop, as needed. + stbtt_fontinfo *curr_font = NULL; + + int max_row_height = 0; + // Calculate some metrics for every font we have loaded. + // Please forgive the repetition here. + + // Declaring these three variables early, so a default can be set + float sf = 0.0; + int max_baseline = 0; + int max_lg = 0; + int max_desc = 0; + float rgSF, itSF, bdSF, bditSF; + int asc, desc, lg; + int scaled_bl, scaled_desc, scaled_lg; + if (otFonts.otRegular) { + rgSF = stbtt_ScaleForPixelHeight(otFonts.otRegular, (float)font_size_px); + stbtt_GetFontVMetrics(otFonts.otRegular, &asc, &desc, &lg); + scaled_bl = (int)(ceilf(rgSF * asc)); + scaled_desc = (int)(ceilf(rgSF * desc)); + scaled_lg = (int)(ceilf(rgSF * lg)); + if (scaled_bl > max_baseline) { + max_baseline = scaled_bl; + } + if (scaled_desc < max_desc) { + max_desc = scaled_desc; + } + if (scaled_lg > max_lg) { + max_lg = scaled_lg; + } + // Set default font, for when markdown parsing is disabled + if (!curr_font) { + curr_font = otFonts.otRegular; + sf = rgSF; + ELOG("[FBInk] Unformatted text defaulting to regular font"); + } + } + if (otFonts.otItalic) { + itSF = stbtt_ScaleForPixelHeight(otFonts.otItalic, (float)font_size_px); + stbtt_GetFontVMetrics(otFonts.otItalic, &asc, &desc, &lg); + scaled_bl = (int)(ceilf(itSF * asc)); + scaled_desc = (int)(ceilf(itSF * desc)); + scaled_lg = (int)(ceilf(itSF * lg)); + if (scaled_bl > max_baseline) { + max_baseline = scaled_bl; + } + if (scaled_desc < max_desc) { + max_desc = scaled_desc; + } + if (scaled_lg > max_lg) { + max_lg = scaled_lg; + } + // Set default font, for when markdown parsing is disabled + if (!curr_font) { + curr_font = otFonts.otItalic; + sf = itSF; + ELOG("[FBInk] Unformatted text defaulting to italic font"); + } + } + if (otFonts.otBold) { + bdSF = stbtt_ScaleForPixelHeight(otFonts.otBold, (float)font_size_px); + stbtt_GetFontVMetrics(otFonts.otBold, &asc, &desc, &lg); + scaled_bl = (int)(ceilf(bdSF * asc)); + scaled_desc = (int)(ceilf(bdSF * desc)); + scaled_lg = (int)(ceilf(bdSF * lg)); + if (scaled_bl > max_baseline) { + max_baseline = scaled_bl; + } + if (scaled_desc < max_desc) { + max_desc = scaled_desc; + } + if (scaled_lg > max_lg) { + max_lg = scaled_lg; + } + // Set default font, for when markdown parsing is disabled + if (!curr_font) { + curr_font = otFonts.otBold; + sf = bdSF; + ELOG("[FBInk] Unformatted text defaulting to bold font"); + } + } + if (otFonts.otBoldItalic) { + bditSF = stbtt_ScaleForPixelHeight(otFonts.otBoldItalic, (float)font_size_px); + stbtt_GetFontVMetrics(otFonts.otBoldItalic, &asc, &desc, &lg); + scaled_bl = (int)(ceilf(bditSF * asc)); + scaled_desc = (int)(ceilf(bditSF * desc)); + scaled_lg = (int)(ceilf(bditSF * lg)); + if (scaled_bl > max_baseline) { + max_baseline = scaled_bl; + } + if (scaled_desc < max_desc) { + max_desc = scaled_desc; + } + if (scaled_lg > max_lg) { + max_lg = scaled_lg; + } + // Set default font, for when markdown parsing is disabled + if (!curr_font) { + curr_font = otFonts.otBoldItalic; + sf = bditSF; + ELOG("[FBInk] Unformatted text defaulting to bold italic font"); + } + } + // If no font was loaded, exit early. We checked earlier, but just in case... + if (!curr_font) { + ELOG("[FBInk] No font appears to be loaded"); + rv = ERRCODE(ENOENT); + goto cleanup; + } + printf("Max BL: %d Max Desc: %d Max LG: %d\n", max_baseline, max_desc, max_lg); + max_row_height = max_baseline + abs(max_desc) + max_lg; + // And if max_row_height was not changed from zero, this is also an unrecoverable error. + // Also guards against a potential divide-by-zero in the following calculation + if (max_row_height <= 0) { + ELOG("[FBInk] Max line height not set"); + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + + // Calculate the maximum number of lines we may have to deal with + unsigned int print_height, num_lines; + print_height = area.br.y - area.tl.y; + num_lines = print_height / (unsigned int)max_row_height; + + // And allocate the memory for it... + lines = calloc(num_lines, sizeof(FBInkOTLine)); + + // Now, lets use libunibreak to find the possible break opportunities in our string. + + // Note: we only care about the byte length here + size_t str_len_bytes = strlen(string); + brk_buff = calloc(str_len_bytes + 1, sizeof(*brk_buff)); + if (!brk_buff) { + ELOG("[FBInk] Linebreak buffer could not be allocated"); + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + + init_linebreak(); + set_linebreaks_utf8((utf8_t*)string, str_len_bytes + 1, "en", brk_buff); + LOG("Found linebreaks!"); + + // Parse our string for formatting, if requested + if (cfg->is_formatted) { + fmt_buff = calloc(str_len_bytes + 1, sizeof(*fmt_buff)); + if (!fmt_buff) { + ELOG("[FBInk] Formatted text buffer could not be allocated"); + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + parse_simple_md(string, str_len_bytes, fmt_buff); + } + // Lets find our lines! Nothing fancy, just a simple first fit algorithm, but we do + // our best not to break inside a word. + + unsigned int chars_in_str = u8_strlen(string); + unsigned int c_index = 0; + unsigned int tmp_c_index = c_index; + uint32_t c; + unsigned short max_lw = area.br.x - area.tl.x; + unsigned int line; + int max_line_height = max_row_height - max_lg; + // adv = advance: the horizontal distance along the baseline to the origin of + // the next glyph + // lsb = left side bearing: The horizontal distance from the origin point to + // left edge of the glyph + int adv, lsb, curr_x; + bool complete_str = false; + int x0, y0, x1, y1, gw, gh, cx; + unsigned int lw; + for (line = 0; line < num_lines; line++) { + // Every line has a start character index and an end char index. + curr_x = 0; + lw = 0; + lines[line].startCharIndex = c_index; + lines[line].line_used = true; + lines[line].line_gap = max_lg; + while (c_index < chars_in_str) { + if (cfg->is_formatted) { + // Check if we need to skip formatting characters + if (fmt_buff[c_index] == CH_IGNORE) { + u8_inc(string, &c_index); + continue; + } else { + switch (fmt_buff[c_index]) { + case CH_REGULAR: + curr_font = otFonts.otRegular; + sf = rgSF; + break; + case CH_ITALIC: + curr_font = otFonts.otItalic; + sf = itSF; + break; + case CH_BOLD: + curr_font = otFonts.otBold; + sf = bdSF; + break; + case CH_BOLD_ITALIC: + curr_font = otFonts.otBoldItalic; + sf = bditSF; + break; + } + } + } + if (!curr_font) { + ELOG("[FBInk] The specified font variant was not loaded"); + rv = ERRCODE(ENOENT); + goto cleanup; + } + // We check for a mandatory break + if (brk_buff[c_index] == LINEBREAK_MUSTBREAK) { + unsigned int last_index = c_index; + // We don't want to print the break character + u8_dec(string, &last_index); + lines[line].endCharIndex = last_index; + // We want our next line to start after this breakpoint + u8_inc(string, &c_index); + // And we're done processing this line + break; + } + c = u8_nextchar(string, &c_index); + // Note, these metrics are unscaled, we need to use our previously + // obtained scale factor (sf) to get the metrics as pixels + stbtt_GetCodepointHMetrics(curr_font, c, &adv, &lsb); + // But these are already scaled + stbtt_GetCodepointBitmapBox(curr_font, c, sf, sf, &x0, &y0, &x1, &y1); + gw = x1 - x0; + // Ensure that curr_x never goes negative + cx = curr_x; + if (cx + x0 < 0) { + curr_x += abs(cx + x0); + } + // Handle the situation where the metrics may lie, and the glyph descends + // below what the metrics say. + if (max_baseline + y1 > max_line_height) { + int height_diff = (max_baseline + y1) - max_line_height; + printf("Height Diff: %d, available LG: %d\n", height_diff, lines[line].line_gap); + if (height_diff > lines[line].line_gap) { + lines[line].line_gap = 0; + } else { + lines[line].line_gap -= height_diff; + } + max_line_height = max_baseline + y1; + } + // stb_truetype does not appear to create a bounding box for space characters, + // so we need to handle this situation. + if (!gw && adv) { + lw = (unsigned int)curr_x; + } else { + lw = (unsigned int)(curr_x + x0 + gw); + } + printf("Current Measured LW: %u Line# %u\n", lw, line); + // Oops, we appear to have advanced too far :) + // Better backtrack to see if we can find a suitable break opportunity + //unsigned short ot_meas_padding = 3; // Just so we don't use a magic number + if (lw > max_lw) { + // Is the glyph itself too wide for our printable area? If so, we abort + if ((unsigned int)gw >= max_lw) { + ELOG("[FBInk] Font size too big for current printable area. Try to reduce margins or font size"); + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + // Reset the index to our current character (c_index is ahead by one at this point) + u8_dec(string, &c_index); + // If the current glyph is a space, handle that now. + if (brk_buff[c_index] == LINEBREAK_ALLOWBREAK) { + tmp_c_index = c_index; + u8_dec(string, &tmp_c_index); + lines[line].endCharIndex = tmp_c_index; + u8_inc(string, &c_index); + break; + } else { + // Note, we need to do this a second time, to get the previous character, as + // u8_nextchar() 'consumes' a character. + u8_dec(string, &c_index); + lines[line].endCharIndex = c_index; + for (; c_index > lines[line].startCharIndex; u8_dec(string, &c_index)) { + if (brk_buff[c_index] == LINEBREAK_ALLOWBREAK) { + lines[line].endCharIndex = c_index; + break; + } + } + u8_inc(string, &c_index); + break; + } + } + curr_x += (int)lroundf(sf * adv); + // Adjust our x position for kerning, because we can :) + if (string[c_index + 1]) { + tmp_c_index = c_index; + uint32_t c2 = u8_nextchar(string, &tmp_c_index); + curr_x += (int)lroundf(sf * stbtt_GetCodepointKernAdvance(curr_font, c, c2)); + } + } + // We've run out of string! This is our last line. + if (c_index >= chars_in_str) { + u8_dec(string, &c_index); + lines[line].endCharIndex = c_index; + complete_str = true; + break; + } + } + ELOG("[FBInk] %u lines to be printed", line); + if (!complete_str) { + ELOG("[FBInk] String too long. Truncated to %u characters", (c_index + 1)); + } + // Let's determine our exact height, so we can determine vertical alignment later if required. + printf("Maximum printable height is %u\n", print_height); + unsigned int curr_print_height = 0; + for (line = 0; line < num_lines; line++) { + if (!lines[line].line_used) { + break; + } + if (curr_print_height + (unsigned int)max_line_height > print_height) { + // This line can't be printed, so set it to unused + lines[line].line_used = false; + break; + } + curr_print_height += (unsigned int)max_line_height; + if (line <= num_lines - 1) { + if (line == num_lines - 1 || !lines[line + 1].line_used) { + // Last line, we don't want to add a line gap + lines[line].line_gap = 0; + break; + } + } + // We only add a line gap if there's room for one + if (!(curr_print_height + (unsigned int)lines[line].line_gap > print_height)) { + curr_print_height += (unsigned int)lines[line].line_gap; + } + } + printf("Actual print height is %u\n", curr_print_height); + + // Let's get some rendering options from FBInkConfig + uint8_t valign = NONE, halign = NONE, fgcolor = FG_BLACK * 16U, bgcolor = BG_WHITE * 16U; + bool is_inverted = false, is_overlay = false, is_bgless = false, is_fgless = false, is_flashing = false, is_cleared = false; + if (fbCfg) { + valign = fbCfg->valign; + halign = fbCfg->halign; + fgcolor = fbCfg->fg_color * 16U; + bgcolor = fbCfg->bg_color * 16U; + is_inverted = fbCfg->is_inverted; + is_overlay = fbCfg->is_overlay; + is_bgless = fbCfg->is_bgless; + is_fgless = fbCfg->is_fgless; + is_flashing = fbCfg->is_flashing; + is_cleared = fbCfg->is_cleared; + } + fgcolor ^= 0xFF; + // Is the foreground color lighter than background? If so, we make things easier + // for ourselves by inverting the colors, and toggling the is_invert flag to reverse + // it back later. + if (fgcolor < bgcolor) { + fgcolor ^= 0xFF; + bgcolor ^= 0xFF; + is_inverted = !is_inverted; + } + // Hopefully, we have some lines to render! + + // Create a bitmap buffer to render a single line. We don't render the glyphs directly to the + // fb here, as we need to do some simple blending, and it makes it easier to calculate our + // centering if required. + line_buff = calloc(max_lw * (unsigned int)max_line_height, sizeof(*line_buff)); + // We also don't want to be creating a new buffer for every glyph + unsigned int glyph_buffer_dims = font_size_px * (unsigned int)max_line_height * 2U; + glyph_buff = calloc(glyph_buffer_dims, sizeof(*glyph_buff)); + if (!line_buff || !glyph_buff) { + ELOG("[FBInk] Line or glyph buffers could not be allocated"); + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + if (bgcolor > 0U) { + memset(line_buff, bgcolor, max_lw * (unsigned int)max_line_height * sizeof(unsigned char)); + } + unsigned int layer_diff = (unsigned int)fgcolor - (unsigned int)bgcolor; + // Setup the variables needed to render + FBInkCoordinates curr_point = { 0, 0 }; + FBInkCoordinates ins_point = {0, 0}; + FBInkCoordinates paint_point = {area.tl.x, area.tl.y}; + // Set the vertical positioning now + if (valign == CENTER) { + paint_point.y += (print_height - curr_print_height) / 2; + } else if (valign == EDGE) { + paint_point.y += print_height - curr_print_height; + } + // Setup our eink refresh region now. We will call refresh during cleanup. + struct mxcfb_rect region = { 0 }; + if (cfg->is_centered || halign == CENTER) { + region.left = area.tl.x + ((area.br.x - area.tl.x) / 2U); + //printf("Region LEFT = %u + ((%u - %u) / 2) = %u\n", area.tl.x, area.br.x, area.tl.x, (area.tl.x + ((area.br.x - area.tl.x) / 2U))); + } else if (halign == EDGE) { + region.left = area.br.x; + } else { + region.left = paint_point.x; + } + region.top = paint_point.y; + //printf("Region LEFT: %d\n", (int)region.left); + + // Do we need to clear the screen? + if (is_cleared) { + clear_screen(fbfd, !is_inverted ? fgcolor : bgcolor, is_flashing); + region.top = 0; + region.left = 0; + region.width = viewWidth; + region.height = viewHeight; + } + uint32_t tmp_c; + unsigned char *lnPtr, *glPtr = NULL; + unsigned short start_x; + // stb_truetype renders glyphs with color inverted to what our blitting functions expect + unsigned char invert = is_inverted ? 0x00 : 0xFF; + bool abort_line = false; + // Render! + for (line = 0; line < num_lines; line++) { + if (!lines[line].line_used) { + break; + } + // We have run out of (vertical) printable area, most likely due to incorrect font + // metrics in the font. + if (abort_line) { + break; + } + //printf("Line # %u\n", line); + lw = 0; + unsigned int ci; + for (ci = lines[line].startCharIndex; ci <= lines[line].endCharIndex;) { + if (cfg->is_formatted) { + if (fmt_buff[ci] == CH_IGNORE) { + u8_inc(string, &ci); + continue; + } else { + switch (fmt_buff[ci]) { + case CH_REGULAR: + curr_font = otFonts.otRegular; + sf = rgSF; + break; + case CH_ITALIC: + curr_font = otFonts.otItalic; + sf = itSF; + break; + case CH_BOLD: + curr_font = otFonts.otBold; + sf = bdSF; + break; + case CH_BOLD_ITALIC: + curr_font = otFonts.otBoldItalic; + sf = bditSF; + break; + } + } + } + curr_point.y = ins_point.y = (unsigned int)max_baseline; + c = u8_nextchar(string, &ci); + stbtt_GetCodepointHMetrics(curr_font, c, &adv, &lsb); + stbtt_GetCodepointBitmapBox(curr_font, c, sf, sf, &x0, &y0, &x1, &y1); + gw = x1 - x0; + gh = y1 - y0; + // Ensure that our glyph size does not exceed the buffer size. Resize the buffer if it does + if (gw * gh > (int)glyph_buffer_dims) { + unsigned int new_buff_size = (unsigned int)gw * (unsigned int)gh * 2U * sizeof(unsigned char); + unsigned char *tmp_g_buff = NULL; + tmp_g_buff = realloc(glyph_buff, new_buff_size); + if (!tmp_g_buff) { + ELOG("[FBInk] Failure resizing glyph buffer"); + ERRCODE(EXIT_FAILURE); + goto cleanup; + } + glyph_buff = tmp_g_buff; + glyph_buffer_dims = new_buff_size; + } + // Make sure we don't have an underflow/wrap around + cx = (int)curr_point.x; + if (cx + x0 < 0) { + curr_point.x += (unsigned short)abs(cx + x0); + } + ins_point.x = curr_point.x + (unsigned short)x0; + ins_point.y += y0; + //printf("gw: %d & gh: %d for c: U+%04X @ ins_point (%hu, %hu) & curr_point (%hu, %hu) / x0: %d y0: %d x1: %d y1: %d / lsb: %d\n", gw, gh, c, ins_point.x, ins_point.y, curr_point.x, curr_point.y, x0, y0, x1, y1, lsb); + // We only increase the lw if glyph not a space This hopefully prevent trailing + // spaces from being printed on a line. + if (gw > 0) { + lw = ins_point.x + (unsigned int)gw; + } else { + lw = ins_point.x; + } + + //printf("Current Rendered LW: %u Line# %u\n", lw, line); + // Just in case our arithmetic was off by a pixel or two... + // Note that we are deliberately using a slightly shorter line + // width during the measurement phase, so this should not happen. + // If it does occur, we will now exit instead of clipping the glyph + // bounding box, to avoid the possiblity of stb_truetype segfaulting. + if (lw > max_lw) { + ELOG("[FBInk] Max allowed line width exceeded"); + ELOG("[FBInk] Curr LW: %u Max Allowed: %hu", lw, max_lw); + rv = ERRCODE(EXIT_FAILURE); + goto cleanup; + } + if (gw > 0 && fgcolor != bgcolor) { + // Because the stbtt_MakeCodepointBitmap documentation is a bit vague on this + // point, the parameter 'out_stride' should be the width of the surface in our + // buffer. It's designed so that the glyph can be rendered directly to a screen buffer. + // For example, if we were rendering directly to a screen of 1080 x 1440m out_stride + // should be set to 1080. In this case however, we want to render to a 'box' of the + // dimensions of the glyph, so we set 'out_stride' to the glyph width. + stbtt_MakeCodepointBitmap(curr_font, glyph_buff, gw, gh, gw, sf, sf, c); + // paint our glyph into the line buffer + lnPtr = line_buff + ins_point.x + (max_lw * ins_point.y); + glPtr = glyph_buff; + // Note, two options here, because we REALLY want to avoid floating point + // math where at all possible. + if (layer_diff == 255U) { + for (int j = 0; j < gh; j++) { + for (int k = 0; k < gw; k++) { + // 0 value pixels are transparent + if (glPtr[k] > 0) { + lnPtr[k] = glPtr[k]; + } + } + // And advance one scanline. Quick! Hide! Pointer arithmetic + glPtr += gw; + lnPtr += max_lw; + } + } else { + for (int j = 0; j < gh; j++) { + for (int k = 0; k < gw; k++) { + // 0 value pixels are transparent + if (glPtr[k] == 255U) { + lnPtr[k] = fgcolor; + } else if (glPtr[k] > 0) { + lnPtr[k] = (unsigned char)(bgcolor + ((glPtr[k] / 255.0f) * layer_diff)); + } + } + // And advance one scanline. Quick! Hide! Pointer arithmetic + glPtr += gw; + lnPtr += max_lw; + } + } + + } + curr_point.x += (unsigned short int) lroundf(sf * adv); + if (ci < lines[line].endCharIndex) { + unsigned int tmp_i = ci; + tmp_c = u8_nextchar(string, &tmp_i); + curr_point.x += (unsigned short int) lroundf(sf * stbtt_GetCodepointKernAdvance(curr_font, c, tmp_c)); + } + ins_point.y = max_baseline; + } + curr_point.x = 0; + // Right, we've rendered a line to a bitmap, time to display it. + + if (cfg->is_centered || halign == CENTER) { + paint_point.x += (max_lw - lw) / 2U; + if (paint_point.x < region.left) { + region.left = paint_point.x; + region.width = lw; + } + } else if (halign == EDGE) { + paint_point.x += max_lw - lw; + if (paint_point.x < region.left) { + region.left = paint_point.x; + region.width = lw; + } + } else if (lw > region.width) { + region.width = lw; + } + //printf("Region LEFT: %d\n", (int)region.left); + FBInkColor color = { 0 }; + start_x = paint_point.x; + lnPtr = line_buff; + // Normal painting to framebuffer. Please forgive the code repetition. Performance... + if (!is_overlay && !is_fgless && !is_bgless) { + for (unsigned int j = 0; j < font_size_px; j++) { + for (unsigned int k = 0; k < lw; k++) { + color.r = color.b = color.g = lnPtr[k] ^ invert; + put_pixel(&paint_point, &color); + paint_point.x++; + } + lnPtr += max_lw; + paint_point.x = start_x; + paint_point.y++; + } + // Note, the current implementation of the following three branches don't properly account for + // anti-aliasing. Expect artifacting when using these options. + } else if (is_fgless) { + for (unsigned int j = 0; j < font_size_px; j++) { + for (unsigned int k = 0; k < lw; k++) { + if (lnPtr[k] == bgcolor) { + color.r = color.b = color.g = lnPtr[k] ^ invert; + put_pixel(&paint_point, &color); + } + paint_point.x++; + } + lnPtr += max_lw; + paint_point.x = start_x; + paint_point.y++; + } + } else if (is_bgless) { + for (unsigned int j = 0; j < font_size_px; j++) { + for (unsigned int k = 0; k < lw; k++) { + if (lnPtr[k] != bgcolor) { + color.r = color.b = color.g = lnPtr[k] ^ invert; + put_pixel(&paint_point, &color); + } + paint_point.x++; + } + lnPtr += max_lw; + paint_point.x = start_x; + paint_point.y++; + } + } else if (is_overlay) { + for (unsigned int j = 0; j < font_size_px; j++) { + for (unsigned int k = 0; k < lw; k++) { + if (lnPtr[k] != bgcolor) { + get_pixel(&paint_point, &color); + color.r ^= 0xFF; + color.b ^= 0xFF; + color.g ^= 0xFF; + put_pixel(&paint_point, &color); + } + paint_point.x++; + } + lnPtr += max_lw; + paint_point.x = start_x; + paint_point.y++; + } + } + paint_point.y += (unsigned short int)lines[line].line_gap; + paint_point.x = area.tl.x; + if (paint_point.y + max_line_height > area.br.y) { + abort_line = true; + } + region.height += (unsigned int)max_line_height; + if (region.top + region.height > viewHeight) { + region.height -= region.top + region.height - viewHeight; + } + LOG("Printed Line!"); + // And clear our line buffer for next use. The glyph buffer shouldn't + // need clearing, as stbtt_MakeCodepointBitmap() should overwrite it. + memset(line_buff, 0, (max_lw * (unsigned int)max_line_height * sizeof(unsigned char))); + } + if (paint_point.y + max_line_height > area.br.y) { + rv = 0; // Inform the caller there is no room left to print another row. + } else { + rv = paint_point.y; // inform the caller what their next top margin should be to follow on + } + cleanup: + // Rotate our eink refresh region before refreshing + printf("Refreshing region from LEFT: %d, TOP: %d, WIDTH: %d, HEIGHT: %d\n", (int)region.left, (int)region.top, (int)region.width, (int)region.height); + if (region.width > 0 && region.height > 0) { + (*fxpRotateRegion)(®ion); + refresh(fbfd, region, WAVEFORM_MODE_AUTO, is_flashing); + } + free(lines); + free(brk_buff); + free(fmt_buff); + free(line_buff); + free(glyph_buff); + if (isFbMapped && !keep_fd) { + unmap_fb(); + } + if (!keep_fd) { + close(fbfd); + } + return rv; +# pragma GCC diagnostic pop +#else + fprintf(stderr, "[FBInk] Opentype support is disabled in this FBInk build!\n"); + return ERRCODE(ENOSYS); +#endif // FBINK_WITH_OPENTYPE +} + // Small public wrapper around refresh(), without the caller having to depend on mxcfb headers int fbink_refresh(int fbfd, diff --git a/fbink.h b/fbink.h index f2f09f5d..2a7533ce 100644 --- a/fbink.h +++ b/fbink.h @@ -77,6 +77,14 @@ typedef enum SCIENTIFICAI // scientifica (italic) } FONT_INDEX_T; +typedef enum +{ + FNT_REGULAR = 0U, + FNT_ITALIC, + FNT_BOLD, + FNT_BOLD_ITALIC +} FONT_VARIANT_T; + // List of available halign/valign values typedef enum { @@ -180,6 +188,18 @@ typedef struct uint8_t valign; // Vertical alignment of images (NONE/TOP, CENTER, EDGE/BOTTOM; c.f., ALIGN_INDEX_T enum) } FBInkConfig; +typedef struct { + uint8_t size_pt; // Size of text in points. If not set (0), defaults to 12pt + struct { + unsigned short top; // Top margin in pixels + unsigned short bottom; // Bottom margin in pixels + unsigned short left; // Left margin in pixels + unsigned short right; // Right margin in pixels + } margins; + bool is_centered; // Horizontal text centering + bool is_formatted; // Is string "formatted"? Bold/Italic support only, markdown like syntax +} FBInkOTConfig; + // NOTE: Unless otherwise specified, // stuff returns a negative value (usually -(EXIT_FAILURE)) on failure & EXIT_SUCCESS otherwise ;). @@ -214,6 +234,17 @@ FBINK_API int fbink_close(int fbfd); // c.f., KFMon's handling of this via fbink_is_fb_quirky() to detect the initial 16bpp -> 32bpp switch. FBINK_API int fbink_init(int fbfd, const FBInkConfig* fbink_config); +// Add an OpenType font to FBInk. Note that at least one font must be added in order to use fbink_print_ot() +// Returns -(EXIT_FAILURE) on failure, or EXIT_SUCCESS otherwise +// fp: The font file path. This should be a valid *.otf or *.ttf font +// variant: What variant of font this is (FNT_REGULAR, FNT_ITALIC, FNT_BOLD, FNT_BOLD_ITALIC) +// NOTE: You MUST free the fonts loaded when you are fone by calling fbink_free_ot_fonts() +// NOTE: You may replace a font without first calling free +FBINK_API int fbink_add_ot_font(const char* fp, FONT_VARIANT_T variant); + +// Free all loaded OpenType fonts. You MUST call this when you have finished all OT printing. +FBINK_API int fbink_free_ot_fonts(void); + // Dump a few of our internal state variables to stdout, in a format easily consumable by a shell (i.e., eval) FBINK_API void fbink_state_dump(const FBInkConfig* fbink_config); @@ -239,6 +270,24 @@ FBINK_API int fbink_print(int fbfd, const char* string, const FBInkConfig* fbink FBINK_API int fbink_printf(int fbfd, const FBInkConfig* fbink_config, const char* fmt, ...) __attribute__((format(printf, 3, 4))); +// Print a string using an OpenType font. Note the caller MUST init with fbink_init_ot() FIRST. +// This function uses margins (as whole number percentages) instead of rows/columns for +// positioning and setting the printable area. +// Returns new top margin for use in subsequent calls, if the return value is positive. +// A zero return value indicates there is no room left to print another row of text at the current +// margins or font size. +// Returns -(ERANGE) if the provided margins are out of range, or sum to < view height or width +// Returns -(ENOSYS) if compiled with MINIMAL +// Returns -(ENODAT) if fbink_init_ot() hasn't yet been called. +// fbfd: open file descriptor to the framebuffer character device, +// if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call +// string: UTF-8 encoded string to print +// cfg: Pointer to a FBInkOTConfig struct. +// fbCfg: Optional pointer to a FBInkConfig struct. If set, the options +// is_inverted, is_overlay, is_fgless, is_bgless, fg_color, bg_color, valign, halign +// will be honored. +FBINK_API int fbink_print_ot(int fbfd, char* string, FBInkOTConfig* cfg, FBInkConfig* fbCfg); + // A simple wrapper around the internal screen refresh handling, without requiring you to include einkfb/mxcfb headers // fbfd: open file descriptor to the framebuffer character device, // if set to FBFD_AUTO, the fb is opened & mmap'ed for the duration of this call diff --git a/fbink_cmd.c b/fbink_cmd.c index fabf953e..9960194e 100644 --- a/fbink_cmd.c +++ b/fbink_cmd.c @@ -690,11 +690,29 @@ int fbink_config.fontname, fbink_config.fontmult); } + // TTF! + FBInkOTConfig cfg = { 0 }; + cfg.is_formatted = true; + cfg.margins.top = 5; + cfg.size_pt = 18; + // fbink_add_ot_font("/mnt/onboard/fonts/Bookerly-Regular.ttf", FNT_REGULAR); + // fbink_add_ot_font("/mnt/onboard/fonts/Bookerly-Italic.ttf", FNT_ITALIC); + // fbink_add_ot_font("/mnt/onboard/fonts/Bookerly-Bold.ttf", FNT_BOLD); + // fbink_add_ot_font("/mnt/onboard/fonts/Bookerly-BoldItalic.ttf", FNT_BOLD_ITALIC); + + fbink_add_ot_font("/mnt/onboard/fonts/Alegreya-Regular.ttf", FNT_REGULAR); + fbink_add_ot_font("/mnt/onboard/fonts/Alegreya-Italic.ttf", FNT_ITALIC); + fbink_add_ot_font("/mnt/onboard/fonts/Alegreya-Bold.ttf", FNT_BOLD); + fbink_add_ot_font("/mnt/onboard/fonts/Alegreya-BoldItalic.ttf", FNT_BOLD_ITALIC); + fbink_print_ot(fbfd, string, &cfg); + fbink_free_ot_fonts(); + /* if ((linecount = fbink_print(fbfd, string, &fbink_config)) < 0) { fprintf(stderr, "Failed to print that string!\n"); rv = ERRCODE(EXIT_FAILURE); goto cleanup; } + */ // NOTE: Don't clobber previous entries if multiple strings were passed... // We make sure to trust print's return value, // because it knows how much space it already took up ;). diff --git a/fbink_internal.h b/fbink_internal.h index 8b3ced6b..813c5b1b 100644 --- a/fbink_internal.h +++ b/fbink_internal.h @@ -36,6 +36,9 @@ # ifndef FBINK_WITH_IMAGE # define FBINK_WITH_IMAGE # endif +# ifndef FBINK_WITH_OPENTYPE +# define FBINK_WITH_OPENTYPE +# endif // Connect button scanning is Kobo specific # ifndef FBINK_FOR_KINDLE # ifndef FBINK_FOR_CERVANTES @@ -255,6 +258,17 @@ const uint32_t* (*fxpFont32xGetBitmap)(uint32_t) = NULL; // Where we track device/screen-specific quirks FBInkDeviceQuirks deviceQuirks = { 0 }; +// Information about the currently loaded OpenType font +bool otInit = false; +FBInkOTFonts otFonts = { NULL, NULL, NULL, NULL }; +typedef enum { + CH_IGNORE = 0U, + CH_REGULAR, + CH_ITALIC, + CH_BOLD, + CH_BOLD_ITALIC +} CHARACTER_FONT; + #ifndef FBINK_FOR_KINDLE static void rotate_coordinates_pickel(FBInkCoordinates*); static void rotate_coordinates_boot(FBInkCoordinates*); @@ -334,6 +348,11 @@ static int draw_image(int, unsigned char*, const int, const int, const int, const int, short int, short int, const FBInkConfig*); #endif +#ifdef FBINK_WITH_OPENTYPE +static void* free_ot_font(stbtt_fontinfo* font_info); +static void parse_simple_md(char* string, int size, unsigned char* result); +#endif + // For identify_device, which we need outside of fbink_device_id.c ;) #ifndef FBINK_FOR_LINUX # include "fbink_device_id.h" diff --git a/fbink_types.h b/fbink_types.h index 40378a99..42187d25 100644 --- a/fbink_types.h +++ b/fbink_types.h @@ -23,6 +23,10 @@ #include #include +// NOTE: We need to import stbtt early because we depend on stbtt_fontinfo here +// We'll want it as static/private, so do that here, because we're importing it earlier than fbink.c +#define STBTT_STATIC +#include "stb/stb_truetype.h" // List of flags for device or screen-specific quirks... typedef struct @@ -128,4 +132,19 @@ typedef union } color; } FBInkPixelBGR; +// Stores the information necessary to render a line of text +// using OpenType/TrueType fonts +typedef struct FBInkOTLine { + unsigned int startCharIndex; + unsigned int endCharIndex; + bool line_used; + int line_gap; +} FBInkOTLine; + +typedef struct FBInkOTFonts { + stbtt_fontinfo* otRegular; + stbtt_fontinfo* otItalic; + stbtt_fontinfo* otBold; + stbtt_fontinfo* otBoldItalic; +} FBInkOTFonts; #endif diff --git a/libunibreak b/libunibreak new file mode 160000 index 00000000..9c13f231 --- /dev/null +++ b/libunibreak @@ -0,0 +1 @@ +Subproject commit 9c13f23169670adf50309ce23ece70bb858bf7d5