qp_draw_text.c (21432B)
1 // Copyright 2021 Nick Brassel (@tzarc) 2 // SPDX-License-Identifier: GPL-2.0-or-later 3 4 #include <quantum.h> 5 #include <utf8.h> 6 7 #include "qp_internal.h" 8 #include "qp_draw.h" 9 #include "qp_comms.h" 10 #include "qff.h" 11 12 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 13 // QFF font handles 14 15 typedef struct qff_font_handle_t { 16 painter_font_desc_t base; 17 bool validate_ok; 18 bool has_ascii_table; 19 uint16_t num_unicode_glyphs; 20 uint8_t bpp; 21 bool has_palette; 22 bool is_panel_native; 23 painter_compression_t compression_scheme; 24 union { 25 qp_stream_t stream; 26 qp_memory_stream_t mem_stream; 27 #ifdef QP_STREAM_HAS_FILE_IO 28 qp_file_stream_t file_stream; 29 #endif // QP_STREAM_HAS_FILE_IO 30 }; 31 #if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM 32 bool owns_buffer; 33 void *buffer; 34 #endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM 35 } qff_font_handle_t; 36 37 static qff_font_handle_t font_descriptors[QUANTUM_PAINTER_NUM_FONTS] = {0}; 38 39 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 40 // Helper: load font from stream 41 42 static painter_font_handle_t qp_load_font_internal(bool (*stream_factory)(qff_font_handle_t *font, void *arg), void *arg) { 43 qp_dprintf("qp_load_font: entry\n"); 44 qff_font_handle_t *font = NULL; 45 46 // Find a free slot 47 for (int i = 0; i < QUANTUM_PAINTER_NUM_FONTS; ++i) { 48 if (!font_descriptors[i].validate_ok) { 49 font = &font_descriptors[i]; 50 break; 51 } 52 } 53 54 // Drop out if not found 55 if (!font) { 56 qp_dprintf("qp_load_font: fail (no free slot)\n"); 57 return NULL; 58 } 59 60 if (!stream_factory(font, arg)) { 61 qp_dprintf("qp_load_font: fail (could not create stream)\n"); 62 return NULL; 63 } 64 65 // Now that we know the length, validate the input data 66 if (!qff_validate_stream(&font->stream)) { 67 qp_dprintf("qp_load_font: fail (failed validation)\n"); 68 return NULL; 69 } 70 71 #if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM 72 // Clear out any existing data 73 font->owns_buffer = false; 74 font->buffer = NULL; 75 76 void *ram_buffer = malloc(font->mem_stream.length); 77 if (ram_buffer == NULL) { 78 qp_dprintf("qp_load_font: could not allocate enough RAM for font, falling back to original\n"); 79 } else { 80 do { 81 // Copy the data into RAM 82 if (qp_stream_read(ram_buffer, 1, font->mem_stream.length, &font->mem_stream) != font->mem_stream.length) { 83 qp_dprintf("qp_load_font: could not copy from flash to RAM, falling back to original\n"); 84 break; 85 } 86 87 // Create the new stream with the new buffer 88 font->buffer = ram_buffer; 89 font->owns_buffer = true; 90 font->mem_stream = qp_make_memory_stream(font->buffer, font->mem_stream.length); 91 } while (0); 92 } 93 94 // Free the buffer if we were unable to recreate the RAM copy. 95 if (ram_buffer != NULL && !font->owns_buffer) { 96 free(ram_buffer); 97 } 98 #endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM 99 100 // Read the info (parsing already successful above, no need to check return value) 101 qff_read_font_descriptor(&font->stream, &font->base.line_height, &font->has_ascii_table, &font->num_unicode_glyphs, &font->bpp, &font->has_palette, &font->is_panel_native, &font->compression_scheme, NULL); 102 103 if (!qp_internal_bpp_capable(font->bpp)) { 104 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); 105 qp_close_font((painter_font_handle_t)font); 106 return NULL; 107 } 108 109 // Validation success, we can return the handle 110 font->validate_ok = true; 111 qp_dprintf("qp_load_font: ok\n"); 112 return (painter_font_handle_t)font; 113 } 114 115 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 116 // Quantum Painter External API: qp_load_font_mem 117 118 static inline bool font_mem_stream_factory(qff_font_handle_t *font, void *arg) { 119 void *buffer = arg; 120 121 // Assume we can read the graphics descriptor 122 font->mem_stream = qp_make_memory_stream(buffer, sizeof(qff_font_descriptor_v1_t)); 123 124 // Update the length of the stream to match, and rewind to the start 125 font->mem_stream.length = qff_get_total_size(&font->stream); 126 font->mem_stream.position = 0; 127 128 return true; 129 } 130 131 painter_font_handle_t qp_load_font_mem(const void *buffer) { 132 return qp_load_font_internal(font_mem_stream_factory, (void *)buffer); 133 } 134 135 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 136 // Quantum Painter External API: qp_close_font 137 138 bool qp_close_font(painter_font_handle_t font) { 139 qff_font_handle_t *qff_font = (qff_font_handle_t *)font; 140 if (!qff_font || !qff_font->validate_ok) { 141 qp_dprintf("qp_close_font: fail (invalid font)\n"); 142 return false; 143 } 144 145 #if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM 146 // Nuke the buffer, if required 147 if (qff_font->owns_buffer) { 148 free(qff_font->buffer); 149 qff_font->buffer = NULL; 150 qff_font->owns_buffer = false; 151 } 152 #endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM 153 154 // Free up this font for use elsewhere. 155 qp_stream_close(&qff_font->stream); 156 qff_font->validate_ok = false; 157 return true; 158 } 159 160 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 161 // Helpers 162 163 // Callback to be invoked for each codepoint detected in the UTF8 input string 164 typedef bool (*code_point_handler)(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg); 165 166 // Helper that sets up the palette (if required) and returns the offset in the stream that the data starts 167 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) { 168 painter_driver_t *driver = (painter_driver_t *)device; 169 170 // Drop out if we can't actually place the data we read out anywhere 171 if (!data_offset) { 172 qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n"); 173 return false; 174 } 175 176 // Work out where we're reading from 177 uint32_t offset = sizeof(qff_font_descriptor_v1_t); 178 if (qff_font->has_ascii_table) { 179 offset += sizeof(qff_ascii_glyph_table_v1_t); 180 } 181 if (qff_font->num_unicode_glyphs > 0) { 182 offset += sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * 6); 183 } 184 185 // Handle palette if needed 186 const uint16_t palette_entries = 1u << qff_font->bpp; 187 bool needs_pixconvert = false; 188 if (qff_font->has_palette) { 189 // If this font has a palette, we need to read it out and set up the pixel lookup table 190 qp_stream_setpos(&qff_font->stream, offset); 191 if (!qp_internal_load_qgf_palette(&qff_font->stream, qff_font->bpp)) { 192 return false; 193 } 194 195 // Skip this block, as far as offset calculations go 196 offset += sizeof(qgf_palette_v1_t) + (palette_entries * 3); 197 needs_pixconvert = true; 198 } else { 199 // Interpolate from fg/bg 200 int16_t palette_entries = 1 << qff_font->bpp; 201 needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries); 202 } 203 204 if (needs_pixconvert) { 205 // Convert the palette to native format 206 if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) { 207 qp_dprintf("qp_drawtext_recolor: fail (could not convert pixels to native)\n"); 208 qp_comms_stop(device); 209 return false; 210 } 211 } 212 213 *data_offset = offset; 214 return true; 215 } 216 217 static inline bool qp_drawtext_prepare_glyph_for_render(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t *width) { 218 if (code_point >= 0x20 && code_point < 0x7F && qff_font->has_ascii_table) { 219 // Do ascii table 220 qff_ascii_glyph_v1_t glyph_info; 221 uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor 222 + sizeof(qgf_block_header_v1_t) // Skip the ascii table header 223 + (code_point - 0x20) * sizeof(qff_ascii_glyph_v1_t); // Jump direct to the data offset based on the glyph index 224 if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) { 225 qp_dprintf("Failed to set stream position while reading ascii glyph info\n"); 226 return false; 227 } 228 229 if (qp_stream_read(&glyph_info, sizeof(qff_ascii_glyph_v1_t), 1, &qff_font->stream) != 1) { 230 qp_dprintf("Failed to read glyph info\n"); 231 return false; 232 } 233 234 uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); 235 uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS); 236 uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor 237 + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table 238 + (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 239 + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette 240 + sizeof(qgf_block_header_v1_t) // Skip the data block header 241 + glyph_offset; // Jump to the specified glyph offset 242 243 if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) { 244 qp_dprintf("Failed to set stream position while preparing ascii glyph data\n"); 245 return false; 246 } 247 248 *width = glyph_width; 249 return true; 250 } else { 251 // Do unicode table, which may include singular ascii glyphs if full ascii table isn't specified 252 uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor 253 + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table 254 + sizeof(qgf_block_header_v1_t); // Skip the unicode block header 255 256 if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) { 257 qp_dprintf("Failed to set stream position while preparing glyph data\n"); 258 return false; 259 } 260 261 qff_unicode_glyph_v1_t glyph_info; 262 for (uint16_t i = 0; i < qff_font->num_unicode_glyphs; ++i) { 263 if (qp_stream_read(&glyph_info, sizeof(qff_unicode_glyph_v1_t), 1, &qff_font->stream) != 1) { 264 qp_dprintf("Failed to set stream position while reading unicode glyph info\n"); 265 return false; 266 } 267 268 if (glyph_info.code_point == code_point) { 269 uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK); 270 uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS); 271 uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor 272 + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table 273 + (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 274 + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette 275 + sizeof(qgf_block_header_v1_t) // Skip the data block header 276 + glyph_offset; // Jump to the specified glyph offset 277 278 if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) { 279 qp_dprintf("Failed to set stream position while preparing unicode glyph data\n"); 280 return false; 281 } 282 283 *width = glyph_width; 284 return true; 285 } 286 } 287 288 // Not found 289 qp_dprintf("Failed to find unicode glyph info\n"); 290 return false; 291 } 292 return false; 293 } 294 295 // Function to iterate over each UTF8 codepoint, invoking the callback for each decoded glyph 296 static inline bool qp_iterate_code_points(qff_font_handle_t *qff_font, const char *str, code_point_handler handler, void *cb_arg) { 297 while (*str) { 298 int32_t code_point = 0; 299 str = decode_utf8(str, &code_point); 300 if (code_point < 0) { 301 qp_dprintf("Invalid unicode code point decoded. Cannot render.\n"); 302 return false; 303 } 304 305 uint8_t width; 306 if (!qp_drawtext_prepare_glyph_for_render(qff_font, code_point, &width)) { 307 qp_dprintf("Failed to prepare glyph for rendering.\n"); 308 return false; 309 } 310 311 if (!handler(qff_font, code_point, width, qff_font->base.line_height, cb_arg)) { 312 qp_dprintf("Failed to execute glyph handler.\n"); 313 return false; 314 } 315 } 316 return true; 317 } 318 319 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 320 // String width calculation 321 322 // Callback state 323 typedef struct code_point_iter_calcwidth_state_t { 324 int16_t width; 325 } code_point_iter_calcwidth_state_t; 326 327 // Codepoint handler callback: width calc 328 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) { 329 code_point_iter_calcwidth_state_t *state = (code_point_iter_calcwidth_state_t *)cb_arg; 330 331 // Increment the overall width by this glyph's width 332 state->width += width; 333 334 return true; 335 } 336 337 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 338 // String drawing implementation 339 340 // Callback state 341 typedef struct code_point_iter_drawglyph_state_t { 342 painter_device_t device; 343 int16_t xpos; 344 int16_t ypos; 345 qp_internal_byte_input_callback input_callback; 346 qp_internal_byte_input_state_t *input_state; 347 qp_internal_pixel_output_state_t *output_state; 348 } code_point_iter_drawglyph_state_t; 349 350 // Codepoint handler callback: drawing 351 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) { 352 code_point_iter_drawglyph_state_t *state = (code_point_iter_drawglyph_state_t *)cb_arg; 353 painter_driver_t *driver = (painter_driver_t *)state->device; 354 355 // Reset the input state's RLE mode -- the stream should already be correctly positioned by qp_iterate_code_points() 356 state->input_state->rle.mode = MARKER_BYTE; // ignored if not using RLE 357 358 // Reset the output state 359 state->output_state->pixel_write_pos = 0; 360 361 // Configure where we're going to be rendering to 362 driver->driver_vtable->viewport(state->device, state->xpos, state->ypos, state->xpos + width - 1, state->ypos + height - 1); 363 364 // Move the x-position for the next glyph 365 state->xpos += width; 366 367 // Decode the pixel data for the glyph, and stream it 368 uint32_t pixel_count = ((uint32_t)width) * height; 369 return qp_internal_appender(state->device, qff_font->bpp, pixel_count, state->input_callback, state->input_state); 370 } 371 372 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 373 // Quantum Painter External API: qp_textwidth 374 375 int16_t qp_textwidth(painter_font_handle_t font, const char *str) { 376 qff_font_handle_t *qff_font = (qff_font_handle_t *)font; 377 if (!qff_font || !qff_font->validate_ok) { 378 qp_dprintf("qp_textwidth: fail (invalid font)\n"); 379 return false; 380 } 381 382 // Create the codepoint iterator state 383 code_point_iter_calcwidth_state_t state = {.width = 0}; 384 // Iterate each codepoint, return the calculated width if successful. 385 return qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_calcwidth, &state) ? state.width : 0; 386 } 387 388 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 389 // Quantum Painter External API: qp_drawtext 390 391 int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str) { 392 // Offload to the recolor variant, substituting fg=white bg=black. 393 // Traditional LCDs with those colors will need to manually invoke qp_drawtext_recolor with the colors reversed. 394 return qp_drawtext_recolor(device, x, y, font, str, 0, 0, 255, 0, 0, 0); 395 } 396 397 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 398 // Quantum Painter External API: qp_drawtext_recolor 399 400 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) { 401 qp_dprintf("qp_drawtext_recolor: entry\n"); 402 painter_driver_t *driver = (painter_driver_t *)device; 403 if (!driver || !driver->validate_ok) { 404 qp_dprintf("qp_drawtext_recolor: fail (validation_ok == false)\n"); 405 return 0; 406 } 407 408 qff_font_handle_t *qff_font = (qff_font_handle_t *)font; 409 if (!qff_font || !qff_font->validate_ok) { 410 qp_dprintf("qp_drawtext_recolor: fail (invalid font)\n"); 411 return false; 412 } 413 414 if (!qp_comms_start(device)) { 415 qp_dprintf("qp_drawtext_recolor: fail (could not start comms)\n"); 416 return 0; 417 } 418 419 // Set up the byte input state and input callback 420 qp_internal_byte_input_state_t input_state = {.device = device, .src_stream = &qff_font->stream}; 421 qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, qff_font->compression_scheme); 422 if (input_callback == NULL) { 423 qp_dprintf("qp_drawtext_recolor: fail (invalid font compression scheme)\n"); 424 qp_comms_stop(device); 425 return false; 426 } 427 428 // Set up the pixel output state 429 qp_internal_pixel_output_state_t output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)}; 430 431 // Set up the codepoint iteration state 432 code_point_iter_drawglyph_state_t state = {// Common 433 .device = device, 434 .xpos = x, 435 .ypos = y, 436 // Input 437 .input_callback = input_callback, 438 .input_state = &input_state, 439 // Output 440 .output_state = &output_state}; 441 442 qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}}; 443 qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}}; 444 uint32_t data_offset; 445 if (!qp_drawtext_prepare_font_for_render(driver, qff_font, fg_hsv888, bg_hsv888, &data_offset)) { 446 qp_dprintf("qp_drawtext_recolor: fail (failed to prepare font for rendering)\n"); 447 qp_comms_stop(device); 448 return false; 449 } 450 451 // Iterate the codepoints with the drawglyph callback 452 bool ret = qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_drawglyph, &state); 453 454 qp_dprintf("qp_drawtext_recolor: %s\n", ret ? "ok" : "fail"); 455 qp_comms_stop(device); 456 return ret ? (state.xpos - x) : 0; 457 }