summaryrefslogtreecommitdiffstats
path: root/quantum/painter
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
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')
-rw-r--r--quantum/painter/qff.c137
-rw-r--r--quantum/painter/qff.h88
-rw-r--r--quantum/painter/qgf.c292
-rw-r--r--quantum/painter/qgf.h136
-rw-r--r--quantum/painter/qp.c228
-rw-r--r--quantum/painter/qp.h453
-rw-r--r--quantum/painter/qp_comms.c72
-rw-r--r--quantum/painter/qp_comms.h25
-rw-r--r--quantum/painter/qp_draw.h85
-rw-r--r--quantum/painter/qp_draw_circle.c172
-rw-r--r--quantum/painter/qp_draw_codec.c142
-rw-r--r--quantum/painter/qp_draw_core.c294
-rw-r--r--quantum/painter/qp_draw_ellipse.c116
-rw-r--r--quantum/painter/qp_draw_image.c382
-rw-r--r--quantum/painter/qp_draw_text.c444
-rw-r--r--quantum/painter/qp_internal.h33
-rw-r--r--quantum/painter/qp_internal_driver.h82
-rw-r--r--quantum/painter/qp_internal_formats.h49
-rw-r--r--quantum/painter/qp_stream.c171
-rw-r--r--quantum/painter/qp_stream.h82
-rw-r--r--quantum/painter/rules.mk116
21 files changed, 3599 insertions, 0 deletions
diff --git a/quantum/painter/qff.c b/quantum/painter/qff.c
new file mode 100644
index 0000000000..cd6af788f9
--- /dev/null
+++ b/quantum/painter/qff.c
@@ -0,0 +1,137 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Quantum Font File "QFF" File Format.
+// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
+
+#include "qff.h"
+#include "qp_draw.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QFF API
+
+bool qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *has_ascii_table, uint16_t *num_unicode_glyphs, uint8_t *bpp, bool *has_palette, painter_compression_t *compression_scheme, uint32_t *total_bytes) {
+ // Seek to the start
+ qp_stream_setpos(stream, 0);
+
+ // Read and validate the font descriptor
+ qff_font_descriptor_v1_t font_descriptor;
+ if (qp_stream_read(&font_descriptor, sizeof(qff_font_descriptor_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read font_descriptor, expected length was not %d\n", (int)sizeof(qff_font_descriptor_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&font_descriptor.header, QFF_FONT_DESCRIPTOR_TYPEID, (sizeof(qff_font_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
+ return false;
+ }
+
+ // Make sure the magic and version are correct
+ if (font_descriptor.magic != QFF_MAGIC || font_descriptor.qff_version != 0x01) {
+ qp_dprintf("Failed to validate font_descriptor, expected magic 0x%06X was 0x%06X, expected version = 0x%02X was 0x%02X\n", (int)QFF_MAGIC, (int)font_descriptor.magic, (int)0x01, (int)font_descriptor.qff_version);
+ return false;
+ }
+
+ // Make sure the file length is valid
+ if (font_descriptor.neg_total_file_size != ~font_descriptor.total_file_size) {
+ qp_dprintf("Failed to validate font_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~font_descriptor.total_file_size), (int)font_descriptor.neg_total_file_size);
+ return false;
+ }
+
+ // Copy out the required info
+ if (line_height) {
+ *line_height = font_descriptor.line_height;
+ }
+ if (has_ascii_table) {
+ *has_ascii_table = font_descriptor.has_ascii_table;
+ }
+ if (num_unicode_glyphs) {
+ *num_unicode_glyphs = font_descriptor.num_unicode_glyphs;
+ }
+ if (bpp || has_palette) {
+ if (!qgf_parse_format(font_descriptor.format, bpp, has_palette)) {
+ return false;
+ }
+ }
+ if (compression_scheme) {
+ *compression_scheme = font_descriptor.compression_scheme;
+ }
+ if (total_bytes) {
+ *total_bytes = font_descriptor.total_file_size;
+ }
+
+ return true;
+}
+
+static bool qff_validate_ascii_descriptor(qp_stream_t *stream) {
+ // Read the raw descriptor
+ qff_ascii_glyph_table_v1_t ascii_descriptor;
+ if (qp_stream_read(&ascii_descriptor, sizeof(qff_ascii_glyph_table_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read ascii_descriptor, expected length was not %d\n", (int)sizeof(qff_ascii_glyph_table_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&ascii_descriptor.header, QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID, (sizeof(qff_ascii_glyph_table_v1_t) - sizeof(qgf_block_header_v1_t)))) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool qff_validate_unicode_descriptor(qp_stream_t *stream, uint16_t num_unicode_glyphs) {
+ // Read the raw descriptor
+ qff_unicode_glyph_table_v1_t unicode_descriptor;
+ if (qp_stream_read(&unicode_descriptor, sizeof(qff_unicode_glyph_table_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read unicode_descriptor, expected length was not %d\n", (int)sizeof(qff_unicode_glyph_table_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&unicode_descriptor.header, QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID, num_unicode_glyphs * 6)) {
+ return false;
+ }
+
+ // Skip the necessary amount of data to get to the next block
+ qp_stream_seek(stream, num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t), SEEK_CUR);
+
+ return true;
+}
+
+bool qff_validate_stream(qp_stream_t *stream) {
+ bool has_ascii_table;
+ uint16_t num_unicode_glyphs;
+
+ if (!qff_read_font_descriptor(stream, NULL, &has_ascii_table, &num_unicode_glyphs, NULL, NULL, NULL, NULL)) {
+ return false;
+ }
+
+ if (has_ascii_table) {
+ if (!qff_validate_ascii_descriptor(stream)) {
+ return false;
+ }
+ }
+
+ if (num_unicode_glyphs > 0) {
+ if (!qff_validate_unicode_descriptor(stream, num_unicode_glyphs)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+uint32_t qff_get_total_size(qp_stream_t *stream) {
+ // Get the original location
+ uint32_t oldpos = qp_stream_tell(stream);
+
+ // Read the font descriptor, grabbing the size
+ uint32_t total_size;
+ if (!qff_read_font_descriptor(stream, NULL, NULL, NULL, NULL, NULL, NULL, &total_size)) {
+ return false;
+ }
+
+ // Restore the original location
+ qp_stream_setpos(stream, oldpos);
+ return total_size;
+}
diff --git a/quantum/painter/qff.h b/quantum/painter/qff.h
new file mode 100644
index 0000000000..6f1a1fd815
--- /dev/null
+++ b/quantum/painter/qff.h
@@ -0,0 +1,88 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+// Quantum Font File "QFF" File Format.
+// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "qp_stream.h"
+#include "qp_internal.h"
+#include "qgf.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QFF structures
+
+/////////////////////////////////////////
+// Font descriptor
+
+#define QFF_FONT_DESCRIPTOR_TYPEID 0x00
+
+typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
+ uint32_t magic : 24; // constant, equal to 0x464651 ("QFF")
+ uint8_t qff_version; // constant, equal to 0x01
+ uint32_t total_file_size; // total size of the entire file, starting at offset zero
+ uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
+ uint8_t line_height; // glyph height in pixels
+ bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E)
+ uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero
+ qp_image_format_t format : 8; // Frame format, see qp.h.
+ uint8_t flags; // frame flags, see below.
+ uint8_t compression_scheme; // compression scheme, see below.
+ uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
+} qff_font_descriptor_v1_t;
+
+_Static_assert(sizeof(qff_font_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 20), "qff_font_descriptor_v1_t must be 25 bytes in v1 of QFF");
+
+#define QFF_MAGIC 0x464651
+
+/////////////////////////////////////////
+// ASCII glyph table descriptor
+
+#define QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID 0x01
+
+#define QFF_GLYPH_WIDTH_BITS 6
+#define QFF_GLYPH_WIDTH_MASK ((1 << QFF_GLYPH_WIDTH_BITS) - 1)
+#define QFF_GLYPH_OFFSET_BITS 18
+#define QFF_GLYPH_OFFSET_MASK (((1 << QFF_GLYPH_OFFSET_BITS) - 1) << QFF_GLYPH_WIDTH_BITS)
+
+typedef struct __attribute__((packed)) qff_ascii_glyph_v1_t {
+ uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
+} qff_ascii_glyph_v1_t;
+
+_Static_assert(sizeof(qff_ascii_glyph_v1_t) == 3, "qff_ascii_glyph_v1_t must be 3 bytes in v1 of QFF");
+
+typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
+ qff_ascii_glyph_v1_t glyph[95]; // 95 glyphs, 0x20..0x7E
+} qff_ascii_glyph_table_v1_t;
+
+_Static_assert(sizeof(qff_ascii_glyph_table_v1_t) == (sizeof(qgf_block_header_v1_t) + (95 * sizeof(qff_ascii_glyph_v1_t))), "qff_ascii_glyph_table_v1_t must be 290 bytes in v1 of QFF");
+
+/////////////////////////////////////////
+// Unicode glyph table descriptor
+
+#define QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID 0x02
+
+typedef struct __attribute__((packed)) qff_unicode_glyph_v1_t {
+ uint32_t code_point : 24;
+ uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
+} qff_unicode_glyph_v1_t;
+
+_Static_assert(sizeof(qff_unicode_glyph_v1_t) == 6, "qff_unicode_glyph_v1_t must be 6 bytes in v1 of QFF");
+
+typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) }
+ qff_unicode_glyph_v1_t glyph[0]; // Extent of '0' signifies that this struct is immediately followed by the glyph data
+} qff_unicode_glyph_table_v1_t;
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QFF API
+
+bool qff_validate_stream(qp_stream_t *stream);
+uint32_t qff_get_total_size(qp_stream_t *stream);
+bool qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *has_ascii_table, uint16_t *num_unicode_glyphs, uint8_t *bpp, bool *has_palette, painter_compression_t *compression_scheme, uint32_t *total_bytes);
diff --git a/quantum/painter/qgf.c b/quantum/painter/qgf.c
new file mode 100644
index 0000000000..834837105b
--- /dev/null
+++ b/quantum/painter/qgf.c
@@ -0,0 +1,292 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Quantum Graphics File "QGF" File Format.
+// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
+
+#include "qgf.h"
+#include "qp_draw.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QGF API
+
+bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length) {
+ if (desc->type_id != expected_typeid || desc->neg_type_id != ((~expected_typeid) & 0xFF)) {
+ qp_dprintf("Failed to validate header, expected typeid 0x%02X, was 0x%02X, expected negated typeid 0x%02X, was 0x%02X\n", (int)expected_typeid, (int)desc->type_id, (int)((~desc->type_id) & 0xFF), (int)desc->neg_type_id);
+ return false;
+ }
+
+ if (expected_length >= 0 && desc->length != expected_length) {
+ qp_dprintf("Failed to validate header (typeid 0x%02X), expected length %d, was %d\n", (int)desc->type_id, (int)expected_length, (int)desc->length);
+ return false;
+ }
+
+ return true;
+}
+
+bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette) {
+ // clang-format off
+ static const struct QP_PACKED {
+ uint8_t bpp;
+ bool has_palette;
+ } formats[] = {
+ [GRAYSCALE_1BPP] = {.bpp = 1, .has_palette = false},
+ [GRAYSCALE_2BPP] = {.bpp = 2, .has_palette = false},
+ [GRAYSCALE_4BPP] = {.bpp = 4, .has_palette = false},
+ [GRAYSCALE_8BPP] = {.bpp = 8, .has_palette = false},
+ [PALETTE_1BPP] = {.bpp = 1, .has_palette = true},
+ [PALETTE_2BPP] = {.bpp = 2, .has_palette = true},
+ [PALETTE_4BPP] = {.bpp = 4, .has_palette = true},
+ [PALETTE_8BPP] = {.bpp = 8, .has_palette = true},
+ };
+ // clang-format on
+
+ // Copy out the required info
+ if (format > PALETTE_8BPP) {
+ qp_dprintf("Failed to parse frame_descriptor, invalid format 0x%02X\n", (int)format);
+ return false;
+ }
+
+ // Copy out the required info
+ if (bpp) {
+ *bpp = formats[format].bpp;
+ }
+ if (has_palette) {
+ *has_palette = formats[format].has_palette;
+ }
+
+ return true;
+}
+
+bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay) {
+ // Decode the format
+ qgf_parse_format(frame_descriptor->format, bpp, has_palette);
+
+ // Copy out the required info
+ if (is_delta) {
+ *is_delta = (frame_descriptor->flags & QGF_FRAME_FLAG_DELTA) == QGF_FRAME_FLAG_DELTA;
+ }
+ if (compression_scheme) {
+ *compression_scheme = frame_descriptor->compression_scheme;
+ }
+ if (delay) {
+ *delay = frame_descriptor->delay;
+ }
+
+ return true;
+}
+
+bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes) {
+ // Seek to the start
+ qp_stream_setpos(stream, 0);
+
+ // Read and validate the graphics descriptor
+ qgf_graphics_descriptor_v1_t graphics_descriptor;
+ if (qp_stream_read(&graphics_descriptor, sizeof(qgf_graphics_descriptor_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read graphics_descriptor, expected length was not %d\n", (int)sizeof(qgf_graphics_descriptor_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&graphics_descriptor.header, QGF_GRAPHICS_DESCRIPTOR_TYPEID, (sizeof(qgf_graphics_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
+ return false;
+ }
+
+ // Make sure the magic and version are correct
+ if (graphics_descriptor.magic != QGF_MAGIC || graphics_descriptor.qgf_version != 0x01) {
+ qp_dprintf("Failed to validate graphics_descriptor, expected magic 0x%06X was 0x%06X, expected version = 0x%02X was 0x%02X\n", (int)QGF_MAGIC, (int)graphics_descriptor.magic, (int)0x01, (int)graphics_descriptor.qgf_version);
+ return false;
+ }
+
+ // Make sure the file length is valid
+ if (graphics_descriptor.neg_total_file_size != ~graphics_descriptor.total_file_size) {
+ qp_dprintf("Failed to validate graphics_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~graphics_descriptor.total_file_size), (int)graphics_descriptor.neg_total_file_size);
+ return false;
+ }
+
+ // Copy out the required info
+ if (image_width) {
+ *image_width = graphics_descriptor.image_width;
+ }
+ if (image_height) {
+ *image_height = graphics_descriptor.image_height;
+ }
+ if (frame_count) {
+ *frame_count = graphics_descriptor.frame_count;
+ }
+ if (total_bytes) {
+ *total_bytes = graphics_descriptor.total_file_size;
+ }
+
+ return true;
+}
+
+static bool qgf_read_frame_offset(qp_stream_t *stream, uint16_t frame_number, uint32_t *frame_offset) {
+ uint16_t frame_count;
+ if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) {
+ return false;
+ }
+
+ // Read the frame offsets descriptor
+ qgf_frame_offsets_v1_t frame_offsets;
+ if (qp_stream_read(&frame_offsets, sizeof(qgf_frame_offsets_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read frame_offsets, expected length was not %d\n", (int)sizeof(qgf_frame_offsets_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&frame_offsets.header, QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID, (frame_count * sizeof(uint32_t)))) {
+ return false;
+ }
+
+ if (frame_number >= frame_count) {
+ qp_dprintf("Invalid frame number, was %d but only %d frames in image\n", (int)frame_number, (int)frame_count);
+ return false;
+ }
+
+ // Skip the necessary amount of data to get to the requested frame offset
+ qp_stream_seek(stream, frame_number * sizeof(uint32_t), SEEK_CUR);
+
+ // Read the frame offset
+ uint32_t offset = 0;
+ if (qp_stream_read(&offset, sizeof(uint32_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read frame offset, expected length was not %d\n", (int)sizeof(uint32_t));
+ return false;
+ }
+
+ // Copy out the required info
+ if (frame_offset) {
+ *frame_offset = offset;
+ }
+
+ return true;
+}
+
+void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number) {
+ // Read the offset
+ uint32_t offset = 0;
+ qgf_read_frame_offset(stream, frame_number, &offset);
+
+ // Move to the offset
+ qp_stream_setpos(stream, offset);
+}
+
+bool qgf_validate_frame_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t *bpp, bool *has_palette, bool *is_delta) {
+ // Seek to the correct location
+ qgf_seek_to_frame_descriptor(stream, frame_number);
+
+ // Read the raw descriptor
+ qgf_frame_v1_t frame_descriptor;
+ if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&frame_descriptor.header, QGF_FRAME_DESCRIPTOR_TYPEID, (sizeof(qgf_frame_v1_t) - sizeof(qgf_block_header_v1_t)))) {
+ return false;
+ }
+
+ return qgf_parse_frame_descriptor(&frame_descriptor, bpp, has_palette, is_delta, NULL, NULL);
+}
+
+bool qgf_validate_palette_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t bpp) {
+ // Read the palette descriptor
+ qgf_palette_v1_t palette_descriptor;
+ if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ uint32_t expected_length = (1 << bpp) * 3 * sizeof(uint8_t);
+ if (!qgf_validate_block_header(&palette_descriptor.header, QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID, expected_length)) {
+ return false;
+ }
+
+ // Move forward in the stream to the next block
+ qp_stream_seek(stream, expected_length, SEEK_CUR);
+ return true;
+}
+
+bool qgf_validate_delta_descriptor(qp_stream_t *stream, uint16_t frame_number) {
+ // Read the delta descriptor
+ qgf_delta_v1_t delta_descriptor;
+ if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
+ return false;
+ }
+
+ // Make sure this block is valid
+ if (!qgf_validate_block_header(&delta_descriptor.header, QGF_FRAME_DELTA_DESCRIPTOR_TYPEID, (sizeof(qgf_delta_v1_t) - sizeof(qgf_block_header_v1_t)))) {
+ return false;
+ }
+
+ return true;
+}
+
+bool qgf_validate_frame_data_descriptor(qp_stream_t *stream, uint16_t frame_number) {
+ // Read and validate the data block
+ qgf_data_v1_t data_descriptor;
+ if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, stream) != 1) {
+ qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
+ return false;
+ }
+
+ if (!qgf_validate_block_header(&data_descriptor.header, QGF_FRAME_DATA_DESCRIPTOR_TYPEID, -1)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool qgf_validate_stream(qp_stream_t *stream) {
+ uint16_t frame_count;
+ if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) {
+ return false;
+ }
+
+ // Read and validate all the frames (automatically validates the frame offset descriptor in the process)
+ for (uint16_t i = 0; i < frame_count; ++i) {
+ // Validate the frame descriptor block
+ uint8_t bpp;
+ bool has_palette;
+ bool has_delta;
+ if (!qgf_validate_frame_descriptor(stream, i, &bpp, &has_palette, &has_delta)) {
+ return false;
+ }
+
+ // If we've got a palette block, check it
+ if (has_palette && !qgf_validate_palette_descriptor(stream, i, bpp)) {
+ return false;
+ }
+
+ // If we've got a delta block, check it
+ if (has_delta && !qgf_validate_delta_descriptor(stream, i)) {
+ return false;
+ }
+
+ // Check the data block
+ if (!qgf_validate_frame_data_descriptor(stream, i)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Work out the total size of an image definition, assuming we can read far enough into the file
+uint32_t qgf_get_total_size(qp_stream_t *stream) {
+ // Get the original location
+ uint32_t oldpos = qp_stream_tell(stream);
+
+ // Read the graphics descriptor, grabbing the size
+ uint32_t total_size;
+ if (!qgf_read_graphics_descriptor(stream, NULL, NULL, NULL, &total_size)) {
+ return false;
+ }
+
+ // Restore the original location
+ qp_stream_setpos(stream, oldpos);
+ return total_size;
+}
diff --git a/quantum/painter/qgf.h b/quantum/painter/qgf.h
new file mode 100644
index 0000000000..54585edd04
--- /dev/null
+++ b/quantum/painter/qgf.h
@@ -0,0 +1,136 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+// Quantum Graphics File "QGF" File Format.
+// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "qp_stream.h"
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QGF structures
+
+/////////////////////////////////////////
+// Common block header
+
+typedef struct QP_PACKED qgf_block_header_v1_t {
+ uint8_t type_id; // See each respective block type below.
+ uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors.
+ uint32_t length : 24; // 24-bit blob length, allowing for block sizes of a maximum of 16MB.
+} qgf_block_header_v1_t;
+
+_Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");
+
+/////////////////////////////////////////
+// Graphics descriptor
+
+#define QGF_GRAPHICS_DESCRIPTOR_TYPEID 0x00
+
+typedef struct QP_PACKED qgf_graphics_descriptor_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
+ uint32_t magic : 24; // constant, equal to 0x464751 ("QGF")
+ uint8_t qgf_version; // constant, equal to 0x01
+ uint32_t total_file_size; // total size of the entire file, starting at offset zero
+ uint32_t neg_total_file_size; // negated value of total_file_size
+ uint16_t image_width; // in pixels
+ uint16_t image_height; // in pixels
+ uint16_t frame_count; // minimum of 1
+} qgf_graphics_descriptor_v1_t;
+
+_Static_assert(sizeof(qgf_graphics_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 18), "qgf_graphics_descriptor_v1_t must be 23 bytes in v1 of QGF");
+
+#define QGF_MAGIC 0x464751
+
+/////////////////////////////////////////
+// Frame offset descriptor
+
+#define QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID 0x01
+
+typedef struct QP_PACKED qgf_frame_offsets_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) }
+ uint32_t offset[0]; // '0' signifies that this struct is immediately followed by the frame offsets
+} qgf_frame_offsets_v1_t;
+
+_Static_assert(sizeof(qgf_frame_offsets_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_frame_offsets_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
+
+/////////////////////////////////////////
+// Frame descriptor
+
+#define QGF_FRAME_DESCRIPTOR_TYPEID 0x02
+
+typedef struct QP_PACKED qgf_frame_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 6 }
+ qp_image_format_t format : 8; // Frame format, see qp.h.
+ uint8_t flags; // Frame flags, see below.
+ painter_compression_t compression_scheme : 8; // Compression scheme, see qp.h.
+ uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
+ uint16_t delay; // frame delay time for animations (in units of milliseconds)
+} qgf_frame_v1_t;
+
+_Static_assert(sizeof(qgf_frame_v1_t) == (sizeof(qgf_block_header_v1_t) + 6), "qgf_frame_v1_t must be 11 bytes in v1 of QGF");
+
+#define QGF_FRAME_FLAG_DELTA 0x02
+#define QGF_FRAME_FLAG_TRANSPARENT 0x01
+
+/////////////////////////////////////////
+// Frame palette descriptor
+
+#define QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID 0x03
+
+typedef struct QP_PACKED qgf_palette_entry_v1_t {
+ uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t.
+ uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t.
+ uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t.
+} qgf_palette_entry_v1_t;
+
+_Static_assert(sizeof(qgf_palette_entry_v1_t) == 3, "Palette entry is not 3 bytes in size");
+
+typedef struct QP_PACKED qgf_palette_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
+ qgf_palette_entry_v1_t hsv[0]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor
+} qgf_palette_v1_t;
+
+_Static_assert(sizeof(qgf_palette_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_palette_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
+
+/////////////////////////////////////////
+// Frame delta descriptor
+
+#define QGF_FRAME_DELTA_DESCRIPTOR_TYPEID 0x04
+
+typedef struct QP_PACKED qgf_delta_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 }
+ uint16_t left; // The left pixel location to draw the delta image
+ uint16_t top; // The top pixel location to draw the delta image
+ uint16_t right; // The right pixel location to to draw the delta image
+ uint16_t bottom; // The bottom pixel location to to draw the delta image
+} qgf_delta_v1_t;
+
+_Static_assert(sizeof(qgf_delta_v1_t) == (sizeof(qgf_block_header_v1_t) + 8), "qgf_delta_v1_t must be 13 bytes in v1 of QGF");
+
+/////////////////////////////////////////
+// Frame data descriptor
+
+#define QGF_FRAME_DATA_DESCRIPTOR_TYPEID 0x05
+
+typedef struct QP_PACKED qgf_data_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N }
+ uint8_t data[0]; // 0 signifies that this struct is immediately followed by the length of data specified in the header
+} qgf_data_v1_t;
+
+_Static_assert(sizeof(qgf_data_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_data_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QGF API
+
+uint32_t qgf_get_total_size(qp_stream_t *stream);
+bool qgf_validate_stream(qp_stream_t *stream);
+bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length);
+bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes);
+bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette);
+void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number);
+bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay);
diff --git a/quantum/painter/qp.c b/quantum/painter/qp.c
new file mode 100644
index 0000000000..e292ff6497
--- /dev/null
+++ b/quantum/painter/qp.c
@@ -0,0 +1,228 @@
+// 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_comms.h"
+#include "qp_draw.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Internal driver validation
+
+static bool validate_driver_vtable(struct painter_driver_t *driver) {
+ return (driver->driver_vtable && driver->driver_vtable->init && driver->driver_vtable->power && driver->driver_vtable->clear && driver->driver_vtable->viewport && driver->driver_vtable->pixdata && driver->driver_vtable->palette_convert && driver->driver_vtable->append_pixels) ? true : false;
+}
+
+static bool validate_comms_vtable(struct painter_driver_t *driver) {
+ return (driver->comms_vtable && driver->comms_vtable->comms_init && driver->comms_vtable->comms_start && driver->comms_vtable->comms_stop && driver->comms_vtable->comms_send) ? true : false;
+}
+
+static bool validate_driver_integrity(struct painter_driver_t *driver) {
+ return validate_driver_vtable(driver) && validate_comms_vtable(driver);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_init
+
+bool qp_init(painter_device_t device, painter_rotation_t rotation) {
+ qp_dprintf("qp_init: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+
+ driver->validate_ok = false;
+ if (!validate_driver_integrity(driver)) {
+ qp_dprintf("Failed to validate driver integrity in qp_init\n");
+ return false;
+ }
+
+ driver->validate_ok = true;
+
+ if (!qp_comms_init(device)) {
+ driver->validate_ok = false;
+ qp_dprintf("qp_init: fail (could not init comms)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_init: fail (could not start comms)\n");
+ return false;
+ }
+
+ // Set the rotation before init
+ driver->rotation = rotation;
+
+ // Invoke init
+ bool ret = driver->driver_vtable->init(device, rotation);
+ qp_comms_stop(device);
+ qp_dprintf("qp_init: %s\n", ret ? "ok" : "fail");
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_power
+
+bool qp_power(painter_device_t device, bool power_on) {
+ qp_dprintf("qp_power: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_power: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_power: fail (could not start comms)\n");
+ return false;
+ }
+
+ bool ret = driver->driver_vtable->power(device, power_on);
+ qp_comms_stop(device);
+ qp_dprintf("qp_power: %s\n", ret ? "ok" : "fail");
+ return ret;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////