// Copyright 2021 Nick Brassel (@tzarc) // SPDX-License-Identifier: GPL-2.0-or-later #include <quantum.h> #include <utf8.h> #include "qp_internal.h" #include "qp_draw.h" #include "qp_comms.h" #include "qff.h" //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // QFF font handles typedef struct qff_font_handle_t { painter_font_desc_t base; bool validate_ok; bool has_ascii_table; uint16_t num_unicode_glyphs; uint8_t bpp; bool has_palette; painter_compression_t compression_scheme; union { qp_stream_t stream; qp_memory_stream_t mem_stream; #ifdef QP_STREAM_HAS_FILE_IO qp_file_stream_t file_stream; #endif // QP_STREAM_HAS_FILE_IO }; #if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM bool owns_buffer; void *buffer; #endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM } qff_font_handle_t; static qff_font_handle_t font_descriptors[QUANTUM_PAINTER_NUM_FONTS] = {0}; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Helper: load font from stream static painter_font_handle_t qp_load_font_internal(bool (*stream_factory)(qff_font_handle_t *font, void *arg), void *arg) { qp_dprintf("qp_load_font: entry\n"); qff_font_handle_t *font = NULL; // Find a free slot for (int i = 0; i < QUANTUM_PAINTER_NUM_FONTS; ++i) { if (!font_descriptors[i].validate_ok) { font = &font_descriptors[i]; break; } } // Drop out if not found if (!font) { qp_dprintf("qp_load_font: fail (no free slot)\n"); return NULL; } if (!stream_factory(font, arg)) { qp_dprintf("qp_load_font: fail (could not create stream)\n"); return NULL; } // Now that we know the length, validate the input data if (!qff_validate_stream(&font->stream)) { qp_dprintf("qp_load_font: fail (failed validation)\n"); return NULL; } #if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM // Clear out any existing data font->owns_buffer = false; font->buffer = NULL; void *ram_buffer = malloc(font->mem_stream.length); if (ram_buffer == NULL) { qp_dprintf("qp_load_font: could not allocate enough RAM for font, falling back to original\n"); } else { do { // Copy the data into RAM if (qp_stream_read(ram_buffer, 1, font->mem_stream.length, &font->mem_stream) != font->mem_stream.length) { qp_dprintf("qp_load_font: could not copy from flash to RAM, falling back to original\n"); break; } // Create the new stream with the new buffer font->buffer = ram_buffer; font->owns_buffer = true; font->mem_stream = qp_make_memory_stream(font->buffer, font->mem_stream.length); } while (0); } // Free the buffer if we were unable to recreate the RAM copy. if (ram_buffer != NULL && !font->owns_buffer) { free(ram_buffer); } #endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM // Read the info (parsing already successful above, no need to check return value) qff_read_font_descriptor(&font->stream, &font->base.line_height, &font->has_ascii_table, &font->num_unicode_glyphs, &font->bpp, &font->has_palette, &font->compression_scheme, NULL); if (!qp_internal_bpp_capable(font->bpp)) { qp_dprintf("qp_load_font: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE or QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS)\n", (int)font->bpp); qp_close_font((painter_font_handle_t)font); return NULL; } // Validation success, we can return the handle font->validate_ok = true; qp_dprintf("qp_load_font: ok\n"); return (painter_font_handle_t)font; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Quantum Painter External API: qp_load_font_mem static inline bool font_mem_stream_factory(qff_font_handle_t *font, void *arg) { void *buffer = arg; // Assume we can read the graphics descriptor font->mem_stream = qp_make_memory_stream(buffer, sizeof(qff_font_descriptor_v1_t)); // Update the length of the stream to match, and rewind to the start font->mem_stream.length = qff_get_total_size(&font->stream); font->mem_stream.position = 0; return true; } painter_font_handle_t qp_load_font_mem(const void *buffer) { return qp_load_font_internal(font_mem_stream_factory, (void *)buffer); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Quantum Painter External API: qp_close_font bool qp_close_font(painter_font_handle_t font) { qff_font_handle_t *qff_font = (qff_font_handle_t *)font; if (!qff_font || !qff_font->validate_ok) { qp_dprintf("qp_close_font: fail (invalid font)\n"); return false; } #if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM // Nuke the buffer, if required if (qff_font->owns_buffer) { free(qff_font->buffer); qff_font->buffer = NULL; qff_font->owns_buffer = false; } #endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM // Free up this font for use elsewhere. qp_stream_close(&qff_font->stream); qff_font->validate_ok = false; return true; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Helpers // Callback to be invoked for each codepoint detected in the UTF8 input string typedef bool (*code_point_handler)(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg); // Helper that sets up the palette (if required) and returns the offset in the stream that the data starts static inline bool qp_drawtext_prepare_font_for_render(painter_device_t device, qff_font_handle_t *qff_font, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, uint32_t *data_offset) { painter_driver_t *driver = (painter_driver_t *)device; // Drop out if we can't actually place the data we read out anywhere if (!data_offset) { qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n"); return false; } // Work out where we're reading from uint32_t offset = sizeof(qff_font_descriptor_v1_t); if (qff_font->has_ascii_table) { offset += sizeof(qff_ascii_glyph_table_v1_t); } if (qff_font->num_unicode_glyphs > 0) { offset += sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * 6); } // Handle palette if needed const uint16_t palette_entries = 1u << qff_font->bpp; bool needs_pixconvert = false; if (qff_font->has_palette) { // If this font has a palette, we need to read it out and set up the pixel lookup table qp_stream_setpos(&qff_font->stream, offset); if (!qp_internal_load_qgf_palette(&qff_font->stream, qff_font->bpp)) { return false; } // Skip this block, as far as offset calculations go offset += sizeof(qgf_palette_v1_t) + (palette_entries * 3); needs_pixconvert = true; } else { // Interpolate from fg/bg int16_t palette_entries = 1 << qff_font->bpp; needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries); } if (needs_pixconvert) { // Convert the palette to native format if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) { qp_dprintf("qp_drawtext_recolor: fail (could not convert pixels to native)\n"); qp_comms_stop(device); return false; } } *data_offset = offset; return true; } static inline bool qp_drawtext_prepare_glyph_for_render(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t *width) { if (code_point >= 0x20 && code_point < 0x7F && qff_font->has_ascii_table) { // Do ascii table qff_ascii_glyph_v1_t glyph_info; uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor + sizeof(qgf_block_header_v1_t) // Skip the ascii table header + (code_point - 0x20) * sizeof(qff_ascii_glyph_v1_t); // Jump direct to the data offset based on the glyph index if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) { qp_dprintf("Failed to set stream position while reading ascii glyph info\n"); return false; } if (qp_stream_read(&glyph_info, sizeof(qff_ascii_glyph_v1_t), 1, &qff_font->stream) != 1) { qp_dprintf("Failed to read glyph info\n"); return false; } uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS); uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table + (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette + sizeof(qgf_block_header_v1_t) // Skip the data block header + glyph_offset; // Jump to the specified glyph offset if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) { qp_dprintf("Failed to set stream position while preparing ascii glyph data\n"); return false; } *width = glyph_width; return true; } else { // Do unicode table, which may include singular ascii glyphs if full ascii table isn't specified uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table + sizeof(qgf_block_header_v1_t); // Skip the unicode block header if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) { qp_dprintf("Failed to set stream position while preparing glyph data\n"); return false; } qff_unicode_glyph_v1_t glyph_info; for (uint16_t i = 0; i < qff_font->num_unicode_glyphs; ++i) { if (qp_stream_read(&glyph_info, sizeof(qff_unicode_glyph_v1_t), 1, &qff_font->stream) != 1) { qp_dprintf("Failed to set stream position while reading unicode glyph info\n"); return false; } if (glyph_info.code_point == code_point) { uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS); uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table + (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette + sizeof(qgf_block_header_v1_t) // Skip the data block header + glyph_offset; // Jump to the specified glyph offset if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) { qp_dprintf("Failed to set stream position while preparing unicode glyph data\n"); return false; } *width = glyph_width; return true; } } // Not found qp_dprintf("Failed to find unicode glyph info\n"); return false; } return false; } // Function to iterate over each UTF8 codepoint, invoking the callback for each decoded glyph static inline bool qp_iterate_code_points(qff_font_handle_t *qff_font, const char *str, code_point_handler handler, void *cb_arg) { while (*str) { int32_t code_point = 0; str = decode_utf8(str, &code_point); if (code_point < 0) { qp_dprintf("Invalid unicode code point decoded. Cannot render.\n"); return false; } uint8_t width; if (!qp_drawtext_prepare_glyph_for_render(qff_font, code_point, &width)) { qp_dprintf("Failed to prepare glyph for rendering.\n"); return false; } if (!handler(qff_font, code_point, width, qff_font->base.line_height, cb_arg)) { qp_dprintf("Failed to execute glyph handler.\n"); return false; } } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // String width calculation // Callback state typedef struct code_point_iter_calcwidth_state_t { int16_t width; } code_point_iter_calcwidth_state_t; // Codepoint handler callback: width calc static inline bool qp_font_code_point_handler_calcwidth(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) { code_point_iter_calcwidth_state_t *state = (code_point_iter_calcwidth_state_t *)cb_arg; // Increment the overall width by this glyph's width state->width += width; return true; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // String drawing implementation // Callback state typedef struct code_point_iter_drawglyph_state_t { painter_device_t device; int16_t xpos; int16_t ypos; qp_internal_byte_input_callback input_callback; qp_internal_byte_input_state_t * input_state; qp_internal_pixel_output_state_t *output_state; } code_point_iter_drawglyph_state_t; // Codepoint handler callback: drawing static inline bool qp_font_code_point_handler_drawglyph(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) { code_point_iter_drawglyph_state_t *state = (code_point_iter_drawglyph_state_t *)cb_arg; painter_driver_t * driver = (painter_driver_t *)state->device; // Reset the input state's RLE mode -- the stream should already be correctly positioned by qp_iterate_code_points() state->input_state->rle.mode = MARKER_BYTE; // ignored if not using RLE // Reset the output state state->output_state->pixel_write_pos = 0; // Configure where we're going to be rendering to driver->driver_vtable->viewport(state->device, state->xpos, state->ypos, state->xpos + width - 1, state->ypos + height - 1); // Move the x-position for the next glyph state->xpos += width; // Decode the pixel data for the glyph uint32_t pixel_count = ((uint32_t)width) * height; bool ret = qp_internal_decode_palette(state->device, pixel_count, qff_font->bpp, state->input_callback, state->input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, state->output_state); // Any leftovers need transmission as well. if (ret && state->output_state->pixel_write_pos > 0) { ret &= driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->output_state->pixel_write_pos); } return ret; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Quantum Painter External API: qp_textwidth int16_t qp_textwidth(painter_font_handle_t font, const char *str) { qff_font_handle_t *qff_font = (qff_font_handle_t *)font; if (!qff_font || !qff_font->validate_ok) { qp_dprintf("qp_textwidth: fail (invalid font)\n"); return false; } // Create the codepoint iterator state code_point_iter_calcwidth_state_t state = {.width = 0}; // Iterate each codepoint, return the calculated width if successful. return qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_calcwidth, &state) ? state.width : 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Quantum Painter External API: qp_drawtext int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str) { // Offload to the recolor variant, substituting fg=white bg=black. // Traditional LCDs with those colors will need to manually invoke qp_drawtext_recolor with the colors reversed. return qp_drawtext_recolor(device, x, y, font, str, 0, 0, 255, 0, 0, 0); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Quantum Painter External API: qp_drawtext_recolor int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) { qp_dprintf("qp_drawtext_recolor: entry\n"); painter_driver_t *driver = (painter_driver_t *)device; if (!driver || !driver->validate_ok) { qp_dprintf("qp_drawtext_recolor: fail (validation_ok == false)\n"); return 0; } qff_font_handle_t *qff_font = (qff_font_handle_t *)font; if (!qff_font || !qff_font->validate_ok) { qp_dprintf("qp_drawtext_recolor: fail (invalid font)\n"); return false; } if (!qp_comms_start(device)) { qp_dprintf("qp_drawtext_recolor: fail (could not start comms)\n"); return 0; } // Set up the byte input state and input callback qp_internal_byte_input_state_t input_state = {.device = device, .src_stream = &qff_font->stream}; qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, qff_font->compression_scheme); if (input_callback == NULL) { qp_dprintf("qp_drawtext_recolor: fail (invalid font compression scheme)\n"); qp_comms_stop(device); return false; } // Set up the pixel output state qp_internal_pixel_output_state_t output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)}; // Set up the codepoint iteration state code_point_iter_drawglyph_state_t state = {// Common .device = device, .xpos = x, .ypos = y, // Input .input_callback = input_callback, .input_state = &input_state, // Output .output_state = &output_state}; qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}}; qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}}; uint32_t data_offset; if (!qp_drawtext_prepare_font_for_render(driver, qff_font, fg_hsv888, bg_hsv888, &data_offset)) { qp_dprintf("qp_drawtext_recolor: fail (failed to prepare font for rendering)\n"); qp_comms_stop(device); return false; } // Iterate the codepoints with the drawglyph callback bool ret = qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_drawglyph, &state); qp_dprintf("qp_drawtext_recolor: %s\n", ret ? "ok" : "fail"); qp_comms_stop(device); return ret ? (state.xpos - x) : 0; }