summaryrefslogtreecommitdiff
path: root/quantum/painter/qp_draw_text.c
diff options
context:
space:
mode:
authorNick Brassel <nick@tzarc.org>2022-04-13 18:00:18 +1000
committerGitHub <noreply@github.com>2022-04-13 18:00:18 +1000
commit1f2b1dedccdf21b629c45ece80b4ca32f6653296 (patch)
treea4283b928fe11c6662be10067314531f12774152 /quantum/painter/qp_draw_text.c
parent1dbbd2b6b068b9f921ebc0341c890df16a491007 (diff)
Quantum Painter (#10174)
* Install dependencies before executing unit tests. * Split out UTF-8 decoder. * Fixup python formatting rules. * Add documentation for QGF/QFF and the RLE format used. * Add CLI commands for converting images and fonts. * Add stub rules.mk for QP. * Add stream type. * Add base driver and comms interfaces. * Add support for SPI, SPI+D/C comms drivers. * Include <qp.h> when enabled. * Add base support for SPI+D/C+RST panels, as well as concrete implementation of ST7789. * Add support for GC9A01. * Add support for ILI9341. * Add support for ILI9163. * Add support for SSD1351. * Implement qp_setpixel, including pixdata buffer management. * Implement qp_line. * Implement qp_rect. * Implement qp_circle. * Implement qp_ellipse. * Implement palette interpolation. * Allow for streams to work with either flash or RAM. * Image loading. * Font loading. * QGF palette loading. * Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp monochrome and palette-based images. * Image drawing. * Animations. * Font rendering. * Check against 256 colours, dump out the loaded palette if debugging enabled. * Fix build. * AVR is not the intended audience. * `qmk format-c` * Generation fix. * First batch of docs. * More docs and examples. * Review comments. * Public API documentation.
Diffstat (limited to 'quantum/painter/qp_draw_text.c')
-rw-r--r--quantum/painter/qp_draw_text.c444
1 files changed, 444 insertions, 0 deletions
diff --git a/quantum/painter/qp_draw_text.c b/quantum/painter/qp_draw_text.c
new file mode 100644
index 0000000000..f99e082cad
--- /dev/null
+++ b/quantum/painter/qp_draw_text.c
@@ -0,0 +1,444 @@
+// 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};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_load_font_mem
+
+painter_font_handle_t qp_load_font_mem(const void *buffer) {
+ qp_dprintf("qp_load_font_mem: 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_mem: fail (no free slot)\n");
+ return NULL;
+ }
+
+ // Assume we can read the graphics descriptor
+ font->mem_stream = qp_make_memory_stream((void *)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;
+
+ // Now that we know the length, validate the input data
+ if (!qff_validate_stream(&font->stream)) {
+ qp_dprintf("qp_load_font_mem: 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_mem: 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_mem: 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_mem: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\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_mem: ok\n");
+ return (painter_font_handle_t)font;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// 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->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.
+ 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) {
+ struct painter_driver_t *driver = (struct 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
+ + sizeof(qff_ascii_glyph_table_v1_t) // 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
+ + sizeof(qff_ascii_glyph_table_v1_t) // 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
+struct code_point_iter_calcwidth_state {
+ int16_t width;
+};
+
+// 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) {
+ struct code_point_iter_calcwidth_state *state = (struct code_point_iter_calcwidth_state *)cb_arg;
+
+ // Increment the overall width by this glyph's width
+ state->width += width;
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// String drawing implementation
+
+// Callback state
+struct code_point_iter_drawglyph_state {
+ painter_device_t device;
+ int16_t xpos;
+ int16_t ypos;
+ qp_internal_byte_input_callback input_callback;
+ struct qp_internal_byte_input_state * input_state;
+ struct qp_internal_pixel_output_state *output_state;
+};
+
+// 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) {
+ struct code_point_iter_drawglyph_state *state = (struct code_point_iter_drawglyph_state *)cb_arg;
+ struct painter_driver_t * driver = (struct 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->validate_ok) {
+ qp_dprintf("qp_textwidth: fail (invalid font)\n");
+ return false;
+ }
+
+ // Create the codepoint iterator state
+ struct code_point_iter_calcwidth_state 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");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!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->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
+ struct qp_internal_byte_input_state 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
+ struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};
+
+ // Set up the codepoint iteration state
+ struct code_point_iter_drawglyph_state 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;
+}