summaryrefslogtreecommitdiffstats
path: root/quantum/painter/qp_draw_text.c
blob: ed3d373867c786bc0b427e641adb279dcb1dcfe5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
// 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->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
                               + 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
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->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->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
    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;
}