summaryrefslogtreecommitdiff
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
parent1dbbd2b6b068b9f921ebc0341c890df16a491007 (diff)
downloadqmk_firmware-1f2b1dedccdf21b629c45ece80b4ca32f6653296.tar.gz
qmk_firmware-1f2b1dedccdf21b629c45ece80b4ca32f6653296.zip
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.
-rw-r--r--.github/workflows/unit_test.yml2
-rw-r--r--builddefs/common_features.mk8
-rw-r--r--docs/_summary.md1
-rw-r--r--docs/cli_commands.md12
-rw-r--r--docs/quantum_painter.md705
-rw-r--r--docs/quantum_painter_qff.md103
-rw-r--r--docs/quantum_painter_qgf.md178
-rw-r--r--docs/quantum_painter_rle.md29
-rw-r--r--drivers/painter/comms/qp_comms_spi.c137
-rw-r--r--drivers/painter/comms/qp_comms_spi.h51
-rw-r--r--drivers/painter/gc9a01/qp_gc9a01.c150
-rw-r--r--drivers/painter/gc9a01/qp_gc9a01.h37
-rw-r--r--drivers/painter/gc9a01/qp_gc9a01_opcodes.h78
-rw-r--r--drivers/painter/ili9xxx/qp_ili9163.c121
-rw-r--r--drivers/painter/ili9xxx/qp_ili9163.h37
-rw-r--r--drivers/painter/ili9xxx/qp_ili9341.c128
-rw-r--r--drivers/painter/ili9xxx/qp_ili9341.h37
-rw-r--r--drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h100
-rw-r--r--drivers/painter/ssd1351/qp_ssd1351.c125
-rw-r--r--drivers/painter/ssd1351/qp_ssd1351.h37
-rw-r--r--drivers/painter/ssd1351/qp_ssd1351_opcodes.h48
-rw-r--r--drivers/painter/st77xx/qp_st7789.c144
-rw-r--r--drivers/painter/st77xx/qp_st7789.h44
-rw-r--r--drivers/painter/st77xx/qp_st7789_opcodes.h64
-rw-r--r--drivers/painter/st77xx/qp_st77xx_opcodes.h51
-rw-r--r--drivers/painter/tft_panel/qp_tft_panel.c130
-rw-r--r--drivers/painter/tft_panel/qp_tft_panel.h67
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rw-r--r--lib/python/qmk/cli/painter/__init__.py2
-rw-r--r--lib/python/qmk/cli/painter/convert_graphics.py86
-rw-r--r--lib/python/qmk/cli/painter/make_font.py87
-rw-r--r--lib/python/qmk/painter.py268
-rw-r--r--lib/python/qmk/painter_qff.py401
-rw-r--r--lib/python/qmk/painter_qgf.py408
-rw-r--r--quantum/main.c11
-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
-rw-r--r--quantum/process_keycode/process_unicode_common.c30
-rw-r--r--quantum/quantum.h4
-rw-r--r--quantum/utf8.c46
-rw-r--r--quantum/utf8.h21
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg4
62 files changed, 7561 insertions, 35 deletions
diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml
index 26bcb2f511..726ce19f0c 100644
--- a/.github/workflows/unit_test.yml
+++ b/.github/workflows/unit_test.yml
@@ -26,5 +26,7 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: recursive
+ - name: Install dependencies
+ run: pip3 install -r requirements-dev.txt
- name: Run tests
run: make test:all
diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
index a1793f91a5..c976b8296d 100644
--- a/builddefs/common_features.mk
+++ b/builddefs/common_features.mk
@@ -149,6 +149,11 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
endif
endif
+QUANTUM_PAINTER_ENABLE ?= no
+ifeq ($(strip $(QUANTUM_PAINTER_ENABLE)), yes)
+ include $(QUANTUM_DIR)/painter/rules.mk
+endif
+
VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi
EEPROM_DRIVER ?= vendor
ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),)
@@ -696,7 +701,8 @@ endif
ifeq ($(strip $(UNICODE_COMMON)), yes)
OPT_DEFS += -DUNICODE_COMMON_ENABLE
- SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c
+ SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c \
+ $(QUANTUM_DIR)/utf8.c
endif
MAGIC_ENABLE ?= yes
diff --git a/docs/_summary.md b/docs/_summary.md
index 249bfcd9ed..786685eba4 100644
--- a/docs/_summary.md
+++ b/docs/_summary.md
@@ -94,6 +94,7 @@
* Hardware Features
* Displays
+ * [Quantum Painter](quantum_painter.md)
* [HD44780 LCD Driver](feature_hd44780.md)
* [ST7565 LCD Driver](feature_st7565.md)
* [OLED Driver](feature_oled_driver.md)
diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index 463abcef12..a380d3eb2f 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -515,3 +515,15 @@ Run single test:
qmk pytest -t qmk.tests.test_cli_commands.test_c2json
qmk pytest -t qmk.tests.test_qmk_path
+
+## `qmk painter-convert-graphics`
+
+This command converts images to a format usable by QMK, i.e. the QGF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
+
+## `qmk painter-make-font-image`
+
+This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
+
+## `qmk painter-convert-font-image`
+
+This command converts an intermediate font image to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
diff --git a/docs/quantum_painter.md b/docs/quantum_painter.md
new file mode 100644
index 0000000000..a3705b62ce
--- /dev/null
+++ b/docs/quantum_painter.md
@@ -0,0 +1,705 @@
+# Quantum Painter :id=quantum-painter
+
+Quantum Painter is the standardised API for graphical displays. It currently includes support for basic drawing primitives, as well as custom images, animations, and fonts.
+
+Due to the complexity, there is no support for Quantum Painter on AVR-based boards.
+
+To enable overall Quantum Painter to be built into your firmware, add the following to `rules.mk`:
+
+```make
+QUANTUM_PAINTER_ENABLE = yes
+QUANTUM_PAINTER_DRIVERS = ......
+```
+
+You will also likely need to select an appropriate driver in `rules.mk`, which is listed below.
+
+!> Quantum Painter is not currently integrated with system-level operations such as disabling displays after a configurable timeout, or when the keyboard goes into suspend. Users will need to handle this manually at the current time.
+
+The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files.
+
+Hardware supported:
+
+| Display Panel | Panel Type | Size | Comms Transport | Driver |
+|---------------|--------------------|------------------|-----------------|-----------------------------------------|
+| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = gc9a01_spi` |
+| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9163_spi` |
+| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9341_spi` |
+| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ssd1351_spi` |
+| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = st7789_spi` |
+
+## Quantum Painter Configuration :id=quantum-painter-config
+
+| Option | Default | Purpose |
+|-----------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
+| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. |
+| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. |
+| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. |
+| `QUANTUM_PAINTER_LOAD_FONTS_TO_RAM` | `FALSE` | Whether or not fonts should be loaded to RAM. Relevant for fonts stored in off-chip persistent storage, such as external flash. |
+| `QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE` | `32` | The limit of the amount of pixel data that can be transmitted in one transaction to the display. Higher values require more RAM on the MCU. |
+| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. |
+| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. |
+
+Drivers have their own set of configurable options, and are described in their respective sections.
+
+## Quantum Painter CLI Commands :id=quantum-painter-cli
+
+### `qmk painter-convert-graphics`
+
+This command converts images to a format usable by QMK, i.e. the QGF File Format.
+
+**Usage**:
+
+```
+usage: qmk painter-convert-graphics [-h] [-d] [-r] -f FORMAT [-o OUTPUT] -i INPUT [-v]
+
+optional arguments:
+ -h, --help show this help message and exit
+ -d, --no-deltas Disables the use of delta frames when encoding animations.
+ -r, --no-rle Disables the use of RLE when encoding images.
+ -f FORMAT, --format FORMAT
+ Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
+ -o OUTPUT, --output OUTPUT
+ Specify output directory. Defaults to same directory as input.
+ -i INPUT, --input INPUT
+ Specify input graphic file.
+ -v, --verbose Turns on verbose output.
+```
+
+The `INPUT` argument can be any image file loadable by Python's Pillow module. Common formats include PNG, or Animated GIF.
+
+The `OUTPUT` argument needs to be a directory, and will default to the same directory as the input argument.
+
+The `FORMAT` argument can be any of the following:
+
+| Format | Meaning |
+|-----------|-----------------------------------------------------------------------|
+| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
+| `pal16` | 16-color palette |
+| `pal4` | 4-color palette |
+| `pal2` | 2-color palette |
+| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
+| `mono16` | 16-shade grayscale |
+| `mono4` | 4-shade grayscale |
+| `mono2` | 2-shade grayscale |
+
+**Examples**:
+
+```
+$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
+$ qmk painter-convert-graphics -f mono16 -i my_image.gif -o ./generated/
+Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.h...
+Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.c...
+```
+
+### `qmk painter-make-font-image`
+
+This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format.
+
+**Usage**:
+
+```
+usage: qmk painter-make-font-image [-h] [-a] [-u UNICODE_GLYPHS] [-n] [-s SIZE] -o OUTPUT -f FONT
+
+optional arguments:
+ -h, --help show this help message and exit
+ -a, --no-aa Disable anti-aliasing on fonts.
+ -u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
+ Also generate the specified unicode glyphs.
+ -n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
+ -s SIZE, --size SIZE Specify font size. Default 12.
+ -o OUTPUT, --output OUTPUT
+ Specify output image path.
+ -f FONT, --font FONT Specify input font file.
+```
+
+The `FONT` argument is generally a TrueType Font file (TTF).
+
+The `OUTPUT` argument is the output image to generate, generally something like `my_font.png`.
+
+The `UNICODE_GLYPHS` argument allows for specifying extra unicode glyphs to generate, and accepts a string.
+
+**Examples**:
+
+```
+$ qmk painter-make-font-image --font NotoSans-ExtraCondensedBold.ttf --size 11 -o noto11.png --unicode-glyphs "ĄȽɂɻɣɈʣ"
+```
+
+### `qmk painter-convert-font-image`
+
+This command converts an intermediate font image to the QFF File Format.
+
+This command expects an image that conforms to the following format:
+
+* Top-left pixel (at `0,0`) is the "delimiter" color:
+ * Each glyph in the font starts when a pixel of this color is found on the first row
+ * The first row is discarded when converting to the QFF format
+* The number of delimited glyphs must match the supplied arguments to the command:
+ * The full ASCII set `0x20..0x7E` (if `--no-ascii` was not specified)
+ * The corresponding number of unicode glyphs if any were specified with `--unicode-glyphs`
+* The order of the glyphs matches the ASCII set, if any, followed by the Unicode glyph set, if any.
+
+**Usage**:
+
+```
+usage: qmk painter-convert-font-image [-h] [-r] -f FORMAT [-u UNICODE_GLYPHS] [-n] [-o OUTPUT] [-i INPUT]
+
+optional arguments:
+ -h, --help show this help message and exit
+ -r, --no-rle Disable the use of RLE to minimise converted image size.
+ -f FORMAT, --format FORMAT
+ Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
+ -u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
+ Also generate the specified unicode glyphs.
+ -n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
+ -o OUTPUT, --output OUTPUT
+ Specify output directory. Defaults to same directory as input.
+ -i INPUT, --input INPUT
+ Specify input graphic file.
+```
+
+The same arguments for `--no-ascii` and `--unicode-glyphs` need to be specified, as per `qmk painter-make-font-image`.
+
+**Examples**:
+
+```
+$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
+$ qmk painter-convert-font-image --input noto11.png -f mono4 --unicode-glyphs "ĄȽɂɻɣɈʣ"
+Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.h...
+Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c...
+```
+
+## Quantum Painter Drawing API :id=quantum-painter-api
+
+All APIs require a `painter_device_t` object as their first parameter -- this object comes from the specific device initialisation, and instructions on creating it can be found in each driver's respective section.
+
+To use any of the APIs, you need to include `qp.h`:
+```c
+#include <qp.h>
+```
+
+### General Notes :id=quantum-painter-api-general
+
+The coordinate system used in Quantum Painter generally accepts `left`, `top`, `right`, and `bottom` instead of x/y/width/height, and each coordinate is inclusive of where pixels should be drawn. This is required as some datatypes used by display panels have a maximum value of `255` -- for any value or geometry extent that matches `256`, this would be represented as a `0`, instead.
+
+?> Drawing a horizontal line 8 pixels long, starting from 4 pixels inside the left side of the display, will need `left=4`, `right=11`.
+
+All color data matches the standard QMK HSV triplet definitions:
+
+* Hue is of the range `0...255` and is internally mapped to 0...360 degrees.
+* Saturation is of the range `0...255` and is internally mapped to 0...100% saturation.
+* Value is of the range `0...255` and is internally mapped to 0...100% brightness.
+
+?> Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled.
+
+### Device Control :id=quantum-painter-api-device-control
+
+#### Display Initialisation :id=quantum-painter-api-init
+
+```c
+bool qp_init(painter_device_t device, painter_rotation_t rotation);
+```
+
+The `qp_init` function is used to initialise a display device after it has been created. This accepts a rotation parameter (`QP_ROTATION_0`, `QP_ROTATION_90`, `QP_ROTATION_180`, `QP_ROTATION_270`), which makes sure that the orientation of what's drawn on the display is correct.
+
+```c
+static painter_device_t display;
+void keyboard_post_init_kb(void) {
+ display = qp_make_.......; // Create the display
+ qp_init(display, QP_ROTATION_0); // Initialise the display
+}
+```
+
+#### Display Power :id=quantum-painter-api-power
+
+```c
+bool qp_power(painter_device_t device, bool power_on);
+```
+
+The `qp_power` function instructs the display whether or not the display panel should be on or off.
+
+!> If there is a separate backlight controlled through the normal QMK backlight API, this is not controlled by the `qp_power` function and needs to be manually handled elsewhere.
+
+```c
+static uint8_t last_backlight = 255;
+void suspend_power_down_user(void) {
+ if (last_backlight == 255) {
+ last_backlight = get_backlight_level();
+ }
+ backlight_set(0);
+ rgb_matrix_set_suspend_state(true);
+ qp_power(display, false);
+}
+
+void suspend_wakeup_init_user(void) {
+ qp_power(display, true);
+ rgb_matrix_set_suspend_state(false);
+ if (last_backlight != 255) {
+ backlight_set(last_backlight);
+ }
+ last_backlight = 255;
+}
+```
+
+#### Display Clear :id=quantum-painter-api-clear
+
+```c
+bool qp_clear(painter_device_t device);
+```
+
+The `qp_clear` function clears the display's screen.
+
+#### Display Flush :id=quantum-painter-api-flush
+
+```c
+bool qp_flush(painter_device_t device);
+```
+
+The `qp_flush` function ensures that all drawing operations are "pushed" to the display. This should be done as the last operation whenever a sequence of draws occur, and guarantees that any changes are applied.
+
+!> Some display panels may seem to work even without a call to `qp_flush` -- this may be because the driver cannot queue drawing operations and needs to display them immediately when invoked. In general, calling `qp_flush` at the end is still considered "best practice".
+
+```c
+void housekeeping_task_user(void) {
+ static uint32_t last_draw = 0;
+ if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
+ last_draw = timer_read32();
+ // Draw a rect based off the current RGB color
+ qp_rect(display, 0, 7, 0, 239, rgb_matrix_get_hue(), 255, 255);
+ qp_flush(display);
+ }
+}
+```
+
+### Drawing Primitives :id=quantum-painter-api-primitives
+
+#### Set Pixel :id=quantum-painter-api-setpixel
+
+```c
+bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
+```
+
+The `qp_setpixel` can be used to set a specific pixel on the screen to the supplied color.
+
+?> Using `qp_setpixel` for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs.
+
+```c
+void housekeeping_task_user(void) {
+ static uint32_t last_draw = 0;
+ if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
+ last_draw = timer_read32();
+ // Draw a 240px high vertical rainbow line on X=0:
+ for (int i = 0; i < 239; ++i) {
+ qp_setpixel(display, 0, i, i, 255, 255);
+ }
+ qp_flush(display);
+ }
+}
+```
+
+#### Draw Line :id=quantum-painter-api-line
+
+```c
+bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
+```
+
+The `qp_line` can be used to draw lines on the screen with the supplied color.
+
+```c
+void housekeeping_task_user(void) {
+ static uint32_t last_draw = 0;
+ if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
+ last_draw = timer_read32();
+ // Draw 8px-wide rainbow down the left side of the display
+ for (int i = 0; i < 239; ++i) {
+ qp_line(display, 0, i, 7, i, i, 255, 255);
+ }
+ qp_flush(display);
+ }
+}
+```
+
+#### Draw Rect :id=quantum-painter-api-rect
+
+```c
+bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
+```
+
+The `qp_rect` can be used to draw rectangles on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the rectangle will be left as-is.
+
+```c
+void housekeeping_task_user(void) {
+ static uint32_t last_draw = 0;
+ if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
+ last_draw = timer_read32();
+ // Draw 8px-wide rainbow filled rectangles down the left side of the display
+ for (int i = 0; i < 239; i+=8) {
+ qp_rect(display, 0, i, 7, i+7, i, 255, 255, true);
+ }
+ qp_flush(display);
+ }
+}
+```
+
+#### Draw Circle :id=quantum-painter-api-circle
+
+```c
+bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
+```
+
+The `qp_circle` can be used to draw circles on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the circle will be left as-is.
+
+```c
+void housekeeping_task_user(void) {
+ static uint32_t last_draw = 0;
+ if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
+ last_draw = timer_read32();
+ // Draw r=4 filled circles down the left side of the display
+ for (int i = 0; i < 239; i+=8) {
+ qp_circle(display, 4, 4+i, 4, i, 255, 255, true);
+ }
+ qp_flush(display);
+ }
+}
+```
+
+#### Draw Ellipse :id=quantum-painter-api-ellipse
+
+```c
+bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
+```
+
+The `qp_ellipse` can be used to draw ellipses on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the ellipses will be left as-is.
+
+```c
+void housekeeping_task_user(void) {
+ static uint32_t last_draw = 0;
+ if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
+ last_draw = timer_read32();
+ // Draw 16x8 filled ellipses down the left side of the display
+ for (int i = 0; i < 239; i+=8) {
+ qp_ellipse(display, 8, 4+i, 16, 8, i, 255, 255, true);
+ }
+ qp_flush(display);
+ }
+}
+```
+
+### Image Functions :id=quantum-painter-api-images
+
+#### Load Image :id=quantum-painter-api-load-image
+
+```c
+painter_image_handle_t qp_load_image_mem(const void *buffer);
+```
+
+The `qp_load_image_mem` function loads a QGF image from memory or flash.
+
+`qp_load_image_mem` returns a handle to the loaded image, which can then be used to draw to the screen using `qp_drawimage`, `qp_drawimage_recolor`, `qp_animate`, or `qp_animate_recolor`. If an image is no longer required, it can be unloaded by calling `qp_close_image` below.
+
+See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert images to [QGF](quantum_painter_qgf.md).
+
+?> The total number of images available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_IMAGES` in the table above. If more images are required, the number should be increased in `config.h`.
+
+Image information is available through accessing the handle:
+
+| Property | Accessor |
+|-------------|----------------------|
+| Width | `image->width` |
+| Height | `image->height` |
+| Frame Count | `image->frame_count` |
+
+#### Unload Image :id=quantum-painter-api-close-image
+
+```c
+bool qp_close_image(painter_image_handle_t image);
+```
+
+The `qp_close_image` function releases resources related to the loading of the supplied image.
+
+#### Draw image :id=quantum-painter-api-draw-image
+
+```c
+bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
+bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
+```
+
+The `qp_drawimage` and `qp_drawimage_recolor` functions draw the supplied image to the screen at the supplied location, with the latter function allowing for monochrome-based images to be recolored.
+
+```c
+// Draw an image on the bottom-right of the 240x320 display on initialisation
+static painter_image_handle_t my_image;
+void keyboard_post_init_kb(void) {
+ my_image = qp_load_image_mem(gfx_my_image);
+ if (my_image != NULL) {
+ qp_drawimage(display, (239 - my_image->width), (319 - my_image->height), my_image);
+ }
+}
+```
+
+#### Animate Image :id=quantum-painter-api-animate-image
+
+```c
+deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
+deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
+```
+
+The `qp_animate` and `qp_animate_recolor` functions draw the supplied image to the screen at the supplied location, with the latter function allowing for monochrome-based animations to be recolored. They also set up internal timing such that each frame is rendered at the correct time as per the animated image.
+
+Once an image has been set to animate, it will loop indefinitely until stopped, with no user intervention required.
+
+Both functions return a `deferred_token`, which can then be used to stop the animation, using `qp_stop_animation` below.
+
+```c
+// Animate an image on the bottom-right of the 240x320 display on initialisation
+static painter_image_handle_t my_image;
+static deferred_token my_anim;
+void keyboard_post_init_kb(void) {
+ my_image = qp_load_image_mem(gfx_my_image);
+ if (my_image != NULL) {
+ my_anim = qp_animate(display, (239 - my_image->width), (319 - my_image->height), my_image);
+ }
+}
+```
+
+#### Stop Animation :id=quantum-painter-api-stop-animation
+
+```c
+void qp_stop_animation(deferred_token anim_token);
+```
+
+The `qp_stop_animation` function stops the previously-started animation.
+```c
+void housekeeping_task_user(void) {
+ if (some_random_stop_reason) {
+ qp_stop_animation(my_anim);
+ }
+}
+```
+
+### Font Functions :id=quantum-painter-api-fonts
+
+#### Load Font :id=quantum-painter-api-load-font
+
+```c
+painter_font_handle_t qp_load_font_mem(const void *buffer);
+```
+
+The `qp_load_font_mem` function loads a QFF font from memory or flash.
+
+`qp_load_font_mem` returns a handle to the loaded font, which can then be measured using `qp_textwidth`, or drawn to the screen using `qp_drawtext`, or `qp_drawtext_recolor`. If a font is no longer required, it can be unloaded by calling `qp_close_font` below.
+
+See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert TTF fonts to [QFF](quantum_painter_qff.md).
+
+?> The total number of fonts available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_FONTS` in the table above. If more fonts are required, the number should be increased in `config.h`.
+
+Font information is available through accessing the handle:
+
+| Property | Accessor |
+|-------------|----------------------|
+| Line Height | `image->line_height` |
+
+#### Unload Font :id=quantum-painter-api-close-font
+
+```c
+bool qp_close_font(painter_font_handle_t font);
+```
+
+The `qp_close_font` function releases resources related to the loading of the supplied font.
+
+#### Measure Text :id=quantum-painter-api-textwidth
+
+```c
+int16_t qp_textwidth(painter_font_handle_t font, const char *str);
+```
+
+The `qp_textwidth` function allows measurement of how many pixels wide the supplied string would result in, for the given font.
+
+#### Draw Text :id=quantum-painter-api-drawtext
+
+```c
+int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
+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);
+```
+
+The `qp_drawtext` and `qp_drawtext_recolor` functions draw the supplied string to the screen at the given location using the font supplied, with the latter function allowing for monochrome-based fonts to be recolored.
+
+```c
+// Draw a text message on the bottom-right of the 240x320 display on initialisation
+static painter_font_handle_t my_font;
+void keyboard_post_init_kb(void) {
+ my_font = qp_load_font_mem(font_opensans);
+ if (my_font != NULL) {
+ static const char *text = "Hello from QMK!";
+ int16_t width = qp_textwidth(my_font, text);
+ qp_drawtext(display, (239 - width), (319 - my_font->line_height), my_font, text);
+ }
+}
+```
+
+### Advanced Functions :id=quantum-painter-api-advanced
+
+#### Get Geometry :id=quantum-painter-api-get-geometry
+
+```c
+void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
+```
+
+The `qp_get_geometry` function allows external code to retrieve the current width, height, rotation, and drawing offsets.
+
+#### Set Viewport Offsets :id=quantum-painter-api-set-viewport
+
+```c
+void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
+```
+
+The `qp_set_viewport_offsets` function can be used to offset all subsequent drawing operations. For example, if a display controller is internally 240x320, but the display panel is 240x240 and has a Y offset of 80 pixels, you could invoke `qp_set_viewport_offsets(display, 0, 80);` and the drawing positioning would be corrected.
+
+#### Set Viewport :id=quantum-painter-api-viewport
+
+```c
+bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
+```
+
+The `qp_viewport` function controls where raw pixel data is written to.
+
+#### Stream Pixel Data :id=quantum-painter-api-pixdata
+
+```c
+bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
+```
+
+The `qp_pixdata` function allows raw pixel data to be streamed to the display. It requires a native pixel count rather than the number of bytes to transfer, to ensure display panel data alignment is respected. E.g. for display panels using RGB565 internal format, sending 10 pixels will result in 20 bytes of transfer.
+
+!> Under normal circumstances, users will not need to manually call either `qp_viewport` or `qp_pixdata`. These allow for writing of raw pixel information, in the display panel's native format, to the area defined by the viewport.
+
+## Quantum Painter Display Drivers :id=quantum-painter-drivers
+
+### Common: Standard TFT (SPI + D/C + RST)
+
+Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins.
+
+For these displays, QMK's `spi_master` must already be correctly configured for the platform you're building for.
+
+The pin assignments for SPI CS, D/C, and RST are specified during device construction.
+
+### GC9A01 :id=qp-driver-gc9a01
+
+Enabling support for the GC9A01 in Quantum Painter is done by adding the following to `rules.mk`:
+
+```make
+QUANTUM_PAINTER_ENABLE = yes
+QUANTUM_PAINTER_DRIVERS = gc9a01_spi
+```
+
+Creating a GC9A01 device in firmware can then be done with the following API:
+
+```c
+painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+```
+
+The device handle returned from the `qp_gc9a01_make_spi_device` function can be used to perform all other drawing operations.
+
+The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
+
+```c
+// 3 displays:
+#define GC9A01_NUM_DEVICES 3
+```
+
+### ILI9163 :id=qp-driver-ili9163
+
+Enabling support for the ILI9163 in Quantum Painter is done by adding the following to `rules.mk`:
+
+```make
+QUANTUM_PAINTER_ENABLE = yes
+QUANTUM_PAINTER_DRIVERS = ili9163_spi
+```
+
+Creating a ILI9163 device in firmware can then be done with the following API:
+
+```c
+painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+```
+
+The device handle returned from the `qp_ili9163_make_spi_device` function can be used to perform all other drawing operations.
+
+The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
+
+```c
+// 3 displays:
+#define ILI9163_NUM_DEVICES 3
+```
+
+### ILI9341 :id=qp-driver-ili9341
+
+Enabling support for the ILI9341 in Quantum Painter is done by adding the following to `rules.mk`:
+
+```make
+QUANTUM_PAINTER_ENABLE = yes
+QUANTUM_PAINTER_DRIVERS = ili9341_spi
+```
+
+Creating a ILI9341 device in firmware can then be done with the following API:
+
+```c
+painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+```
+
+The device handle returned from the `qp_ili9341_make_spi_device` function can be used to perform all other drawing operations.
+
+The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
+
+```c
+// 3 displays:
+#define ILI9341_NUM_DEVICES 3
+```
+
+### SSD1351 :id=qp-driver-ssd1351
+
+Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:
+
+```make
+QUANTUM_PAINTER_ENABLE = yes
+QUANTUM_PAINTER_DRIVERS = ssd1351_spi
+```
+
+Creating a SSD1351 device in firmware can then be done with the following API:
+
+```c
+painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+```
+
+The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations.
+
+The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
+
+```c
+// 3 displays:
+#define SSD1351_NUM_DEVICES 3
+```
+
+### ST7789 :id=qp-driver-st7789
+
+Enabling support for the ST7789 in Quantum Painter is done by adding the following to `rules.mk`:
+
+```make
+QUANTUM_PAINTER_ENABLE = yes
+QUANTUM_PAINTER_DRIVERS = st7789_spi
+```
+
+Creating a ST7789 device in firmware can then be done with the following API:
+
+```c
+painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+```
+
+The device handle returned from the `qp_st7789_make_spi_device` function can be used to perform all other drawing operations.
+
+The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
+
+```c
+// 3 displays:
+#define ST7789_NUM_DEVICES 3
+```
+
+!> Some ST7789 devices are known to have different drawing offsets -- despite being a 240x320 pixel display controller internally, some display panels are only 240x240, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered. \ No newline at end of file
diff --git a/docs/quantum_painter_qff.md b/docs/quantum_painter_qff.md
new file mode 100644
index 0000000000..f62d59bdcb
--- /dev/null
+++ b/docs/quantum_painter_qff.md
@@ -0,0 +1,103 @@
+# QMK Font Format :id=qmk-font-format
+
+QMK uses a font format _("Quantum Font Format" - QFF)_ specifically for resource-constrained systems.
+
+This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images into a font. It also includes RLE for pixel data for some basic compression.
+
+All integer values are in little-endian format.
+
+The QFF is defined in terms of _blocks_ -- each _block_ contains a _header_ and an optional _blob_ of data. The _header_ contains the block's _typeid_, and the length of the _blob_ that follows. Each block type is denoted by a different _typeid_ has its own block definition below. All blocks are defined as packed structs, containing zero padding between fields.
+
+The general structure of the file is:
+
+* _Font descriptor block_
+* _ASCII glyph block_ (optional, only if ASCII glyphs are included)
+* _Unicode glyph block_ (optional, only if Unicode glyphs are included)
+* _Font palette block_ (optional, depending on frame format)
+* _Font data block_
+
+## Block Header :id=qff-block-header
+
+The block header is identical to [QGF's block header](quantum_painter_qgf.md#qgf-block-header), and is present for all blocks, including the font descriptor.
+
+## Font descriptor block :id=qff-font-descriptor
+
+* _typeid_ = 0x00
+* _length_ = 20
+
+This block must be located at the start of the file contents, and can exist a maximum of once in an entire QGF file. It is always followed by either the _ASCII glyph table_ or the _Unicode glyph table_, depending on which glyphs are included in the font.
+
+_Block_ format:
+
+```c
+typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
+ uint24_t magic; // 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
+ uint8_t format; // frame format, see below.
+ 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");
+```
+
+The values for `format`, `flags`, `compression_scheme`, and `transparency_index` match [QGF's frame descriptor block](quantum_painter_qgf.md#qgf-frame-descriptor), with the exception that the `delta` flag is ignored by QFF.
+
+## ASCII glyph table :id=qff-ascii-table
+
+* _typeid_ = 0x01
+* _length_ = 290
+
+If the font contains ascii characters, the _ASCII glyph block_ must be located directly after the _font descriptor block_.
+
+```c
+#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_table_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
+ uint24_t glyph[95]; // 95 glyphs, 0x20..0x7E, see bits/masks above for values
+} qff_ascii_glyph_table_v1_t;
+// _Static_assert(sizeof(qff_ascii_glyph_table_v1_t) == (sizeof(qgf_block_header_v1_t) + 285), "qff_ascii_glyph_table_v1_t must be 290 bytes in v1 of QFF");
+```
+
+## Unicode glyph table :id=qff-unicode-table
+
+* _typeid_ = 0x02
+* _length_ = variable
+
+If this font contains unicode characters, the _unicode glyph block_ must be located directly after the _ASCII glyph table block_, or the _font descriptor block_ if the font does not contain ASCII characters.
+
+```c
+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) }
+ struct __attribute__((packed)) { // container for a single unicode glyph
+ uint24_t code_point; // the unicode code point
+ uint24_t glyph; // the glyph information, as per ASCII glyphs above
+ } glyph[N]; // N glyphs worth of data
+} qff_unicode_glyph_table_v1_t;
+```
+
+## Font palette block :id=qff-palette-descriptor
+
+* _typeid_ = 0x03
+* _length_ = variable
+
+The _font palette block_ is identical to [QGF's frame palette block](quantum_painter_qgf.md#qgf-frame-palette-descriptor), retaining the same _typeid_ of 0x03.
+
+It is only specified in the QFF if the font is palette-based, and follows the _unicode glyph block_ if the font contains any Unicode glyphs, or the _ASCII glyph block_ if the font contains only ASCII glyphs.
+
+## Font data block :id=qff-data-descriptor
+
+* _typeid_ = 0x04
+* _length_ = variable
+
+The _font data block_ is the last block in the file and is identical to [QGF's frame data block](quantum_painter_qgf.md#qgf-frame-data-descriptor), however has a different _typeid_ of 0x04 in QFF.
diff --git a/docs/quantum_painter_qgf.md b/docs/quantum_painter_qgf.md
new file mode 100644
index 0000000000..caf6731e65
--- /dev/null
+++ b/docs/quantum_painter_qgf.md
@@ -0,0 +1,178 @@
+# QMK Graphics Format :id=qmk-graphics-format
+
+QMK uses a graphics format _("Quantum Graphics Format" - QGF)_ specifically for resource-constrained systems.
+
+This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images. It also includes RLE for pixel data for some basic compression.
+
+All integer values are in little-endian format.
+
+The QGF is defined in terms of _blocks_ -- each _block_ contains a _header_ and an optional _blob_ of data. The _header_ contains the block's _typeid_, and the length of the _blob_ that follows. Each block type is denoted by a different _typeid_ has its own block definition below. All blocks are defined as packed structs, containing zero padding between fields.
+
+The general structure of the file is:
+
+* _Graphics descriptor block_
+* _Frame offset block_
+* Repeating list of frames:
+ * _Frame descriptor block_
+ * _Frame palette block_ (optional, depending on frame format)
+ * _Frame delta block_ (optional, depending on delta flag)
+ * _Frame data block_
+
+Different frames within the file should be considered "isolated" and may have their own image format and/or palette.
+
+## Block Header :id=qgf-block-header
+
+This block header is present for all blocks, including the graphics descriptor.
+
+_Block header_ format:
+
+```c
+typedef struct __attribute__((packed)) qgf_block_header_v1_t {
+ uint8_t type_id; // See each respective block type
+ uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors
+ uint24_t length; // 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");
+```
+The _length_ describes the number of octets in the data following the block header -- a block header may specify a _length_ of `0` if no blob is specified.
+
+## Graphics descriptor block :id=qgf-graphics-descriptor
+
+* _typeid_ = 0x00
+* _length_ = 18
+
+This block must be located at the start of the file contents, and can exist a maximum of once in an entire QGF file. It is always followed by the _frame offset block_.
+
+_Block_ format:
+
+```c
+typedef struct __attribute__((packed)) qgf_graphics_descriptor_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
+ uint24_t magic; // 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, used for detecting parsing errors
+ 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");
+```
+
+## Frame offset block :id=qgf-frame-offset-descriptor
+
+* _typeid_ = 0x01
+* _length_ = variable
+
+This block denotes the offsets within the file to each frame's _frame descriptor block_, relative to the start of the file. The _frame offset block_ always immediately follows the _graphics descriptor block_. The contents of this block are an array of U32's, with one entry for each frame.
+
+Duplicate frame offsets in this block are allowed, if a certain frame is to be shown multiple times during animation.
+
+_Block_ format:
+
+```c
+typedef struct __attribute__((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[N]; // where 'N' is the number of frames in the file
+} qgf_frame_offsets_v1_t;
+```
+
+## Frame descriptor block :id=qgf-frame-descriptor
+
+* _typeid_ = 0x02
+* _length_ = 5
+
+This block denotes the start of a frame.
+
+_Block_ format:
+
+```c
+typedef struct __attribute__((packed)) qgf_frame_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 5 }
+ uint8_t format; // Frame format, see below.
+ 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)
+ 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");
+```
+
+If this frame is grayscale, the _frame descriptor block_ (or _frame delta block_ if flags denote a delta frame) is immediately followed by this frame's corresponding _frame data block_.
+
+If the frame uses an indexed palette, the _frame descriptor block_ (or _frame delta block_ if flags denote a delta frame) is immediately followed by this frame's corresponding _frame palette block_.
+
+Frame format possible values:
+
+* `0x00`: 1bpp grayscale, no palette, `0` = black, `1` = white, LSb first pixel
+* `0x01`: 2bpp grayscale, no palette, `0` = black, `3` = white, linear interpolation of brightness, LSb first pixel
+* `0x02`: 4bpp grayscale, no palette, `0` = black, `15` = white, linear interpolation of brightness, LSb first pixel
+* `0x03`: 8bpp grayscale, no palette, `0` = black, `255` = white, linear interpolation of brightness, LSb first pixel
+* `0x04`: 1bpp indexed palette, 2 colors, LSb first pixel
+* `0x05`: 2bpp indexed palette, 4 colors, LSb first pixel
+* `0x06`: 4bpp indexed palette, 16 colors, LSb first pixel
+* `0x07`: 8bpp indexed palette, 256 colors, LSb first pixel
+
+Frame flags is a bitmask with the following format:
+
+| `bit 7` | `bit 6` | `bit 5` | `bit 4` | `bit 3` | `bit 2` | `bit 1` | `bit 0` |
+|---------|---------|---------|---------|---------|---------|---------|--------------|
+| - | - | - | - | - | - | Delta | Transparency |
+
+* `[1]` -- Delta: Signifies that the current frame is a delta frame, which specifies only a sub-image. The _frame delta block_ follows the _frame palette block_ if the image format specifies a palette, otherwise it directly follows the _frame descriptor block_.
+* `[0]` -- Transparency: The transparent palette index in the _blob_ is considered valid and should be used when considering which pixels should be transparent during rendering this frame, if possible.
+
+Compression scheme possible values:
+
+* `0x00`: No compression
+* `0x01`: [QMK RLE](quantum_painter_rle.md)
+
+## Frame palette block :id=qgf-frame-palette-descriptor
+
+* _typeid_ = 0x03
+* _length_ = variable
+
+This block describes the palette used for the frame. The _blob_ contains an array of palette entries -- one palette entry is present for each color used -- each palette entry is in QMK HSV888 format:
+
+```c
+typedef struct __attribute__((packed)) qgf_palette_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
+ struct { // container for a single HSV palette entry
+ 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.
+ } hsv[N]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor
+} qgf_palette_v1_t;
+```
+
+## Frame delta block :id=qgf-frame-delta-descriptor
+
+* _typeid_ = 0x04
+* _length_ = 8
+
+This block describes where the delta frame should be drawn, with respect to the top left location of the image.
+
+```c
+typedef struct __attribute__((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) == 13, "qgf_delta_v1_t must be 13 bytes in v1 of QGF");
+```
+
+## Frame data block :id=qgf-frame-data-descriptor
+
+* _typeid_ = 0x05
+* _length_ = variable
+
+This block describes the data associated with the frame. The _blob_ contains an array of bytes containing the data corresponding to the frame's image format:
+
+```c
+typedef struct __attribute__((packed)) qgf_data_v1_t {
+ qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N }
+ uint8_t data[N]; // N data octets
+} qgf_data_v1_t;
+```
diff --git a/docs/quantum_painter_rle.md b/docs/quantum_painter_rle.md
new file mode 100644
index 0000000000..dcb9a1e1a7
--- /dev/null
+++ b/docs/quantum_painter_rle.md
@@ -0,0 +1,29 @@
+# QMK QGF/QFF RLE data schema :id=qmk-qp-rle-schema
+
+There are two "modes" to the RLE algorithm used in both [QGF](quantum_painter_qgf.md)/[QFF](quantum_painter_qff.md):
+
+* Non-repeating sections of octets, with associated length of up to `128` octets
+ * `length` = `marker - 128`
+ * A corresponding `length` number of octets follow directly after the marker octet
+* Repeated octet with associated length, with associated length of up to `128`
+ * `length` = `marker`
+ * A single octet follows the marker that should be repeated `length` times.
+
+Decoder pseudocode:
+```
+while !EOF
+ marker = READ_OCTET()
+
+ if marker >= 128
+ length = marker - 128
+ for i = 0 ... length-1
+ c = READ_OCTET()
+ WRITE_OCTET(c)
+
+ else
+ length = marker
+ c = READ_OCTET()
+ for i = 0 ... length-1
+ WRITE_OCTET(c)
+
+```
diff --git a/drivers/painter/comms/qp_comms_spi.c b/drivers/painter/comms/qp_comms_spi.c
new file mode 100644
index 0000000000..e644ba9f84
--- /dev/null
+++ b/drivers/painter/comms/qp_comms_spi.c
@@ -0,0 +1,137 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifdef QUANTUM_PAINTER_SPI_ENABLE
+
+# include "spi_master.h"
+# include "qp_comms_spi.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Base SPI support
+
+bool qp_comms_spi_init(painter_device_t device) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
+
+ // Initialize the SPI peripheral
+ spi_init();
+
+ // Set up CS as output high
+ setPinOutput(comms_config->chip_select_pin);
+ writePinHigh(comms_config->chip_select_pin);
+
+ return true;
+}
+
+bool qp_comms_spi_start(painter_device_t device) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
+
+ return spi_start(comms_config->chip_select_pin, comms_config->lsb_first, comms_config->mode, comms_config->divisor);
+}
+
+uint32_t qp_comms_spi_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
+ uint32_t bytes_remaining = byte_count;
+ const uint8_t *p = (const uint8_t *)data;
+ while (bytes_remaining > 0) {
+ uint32_t bytes_this_loop = bytes_remaining < 1024 ? bytes_remaining : 1024;
+ spi_transmit(p, bytes_this_loop);
+ p += bytes_this_loop;
+ bytes_remaining -= bytes_this_loop;
+ }
+
+ return byte_count - bytes_remaining;
+}
+
+void qp_comms_spi_stop(painter_device_t device) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
+ spi_stop();
+ writePinHigh(comms_config->chip_select_pin);
+}
+
+const struct painter_comms_vtable_t spi_comms_vtable = {
+ .comms_init = qp_comms_spi_init,
+ .comms_start = qp_comms_spi_start,
+ .comms_send = qp_comms_spi_send_data,
+ .comms_stop = qp_comms_spi_stop,
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SPI with D/C and RST pins
+
+# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
+
+bool qp_comms_spi_dc_reset_init(painter_device_t device) {
+ if (!qp_comms_spi_init(device)) {
+ return false;
+ }
+
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
+
+ // Set up D/C as output low, if specified
+ if (comms_config->dc_pin != NO_PIN) {
+ setPinOutput(comms_config->dc_pin);
+ writePinLow(comms_config->dc_pin);
+ }
+
+ // Set up RST as output, if specified, performing a reset in the process
+ if (comms_config->reset_pin != NO_PIN) {
+ setPinOutput(comms_config->reset_pin);
+ writePinLow(comms_config->reset_pin);
+ wait_ms(20);
+ writePinHigh(comms_config->reset_pin);
+ wait_ms(20);
+ }
+
+ return true;
+}
+
+uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
+ writePinHigh(comms_config->dc_pin);
+ return qp_comms_spi_send_data(device, data, byte_count);
+}
+
+void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
+ writePinLow(comms_config->dc_pin);
+ spi_write(cmd);
+}
+
+void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
+ for (size_t i = 0; i < sequence_len;) {
+ uint8_t command = sequence[i];
+ uint8_t delay = sequence[i + 1];
+ uint8_t num_bytes = sequence[i + 2];
+ qp_comms_spi_dc_reset_send_command(device, command);
+ if (num_bytes > 0) {
+ qp_comms_spi_dc_reset_send_data(device, &sequence[i + 3], num_bytes);
+ }
+ if (delay > 0) {
+ wait_ms(delay);
+ }
+ i += (3 + num_bytes);
+ }
+}
+
+const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable = {
+ .base =
+ {
+ .comms_init = qp_comms_spi_dc_reset_init,
+ .comms_start = qp_comms_spi_start,
+ .comms_send = qp_comms_spi_dc_reset_send_data,
+ .comms_stop = qp_comms_spi_stop,
+ },
+ .send_command = qp_comms_spi_dc_reset_send_command,
+ .bulk_command_sequence = qp_comms_spi_dc_reset_bulk_command_sequence,
+};
+
+# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#endif // QUANTUM_PAINTER_SPI_ENABLE
diff --git a/drivers/painter/comms/qp_comms_spi.h b/drivers/painter/comms/qp_comms_spi.h
new file mode 100644
index 0000000000..9989987327
--- /dev/null
+++ b/drivers/painter/comms/qp_comms_spi.h
@@ -0,0 +1,51 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#ifdef QUANTUM_PAINTER_SPI_ENABLE
+
+# include <stdint.h>
+
+# include "gpio.h"
+# include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Base SPI support
+
+struct qp_comms_spi_config_t {
+ pin_t chip_select_pin;
+ uint16_t divisor;
+ bool lsb_first;
+ int8_t mode;
+};
+
+bool qp_comms_spi_init(painter_device_t device);
+bool qp_comms_spi_start(painter_device_t device);
+uint32_t qp_comms_spi_send_data(painter_device_t device, const void* data, uint32_t byte_count);
+void qp_comms_spi_stop(painter_device_t device);
+
+extern const struct painter_comms_vtable_t spi_comms_vtable;
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SPI with D/C and RST pins
+
+# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
+
+struct qp_comms_spi_dc_reset_config_t {
+ struct qp_comms_spi_config_t spi_config;
+ pin_t dc_pin;
+ pin_t reset_pin;
+};
+
+void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd);
+uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void* data, uint32_t byte_count);
+void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
+
+extern const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable;
+
+# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#endif // QUANTUM_PAINTER_SPI_ENABLE
diff --git a/drivers/painter/gc9a01/qp_gc9a01.c b/drivers/painter/gc9a01/qp_gc9a01.c
new file mode 100644
index 0000000000..ad76d58b07
--- /dev/null
+++ b/drivers/painter/gc9a01/qp_gc9a01.c
@@ -0,0 +1,150 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <wait.h>
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_gc9a01.h"
+#include "qp_gc9a01_opcodes.h"
+#include "qp_tft_panel.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver storage
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+tft_panel_dc_reset_painter_device_t gc9a01_drivers[GC9A01_NUM_DEVICES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Initialization
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+bool qp_gc9a01_init(painter_device_t device, painter_rotation_t rotation) {
+ // A lot of these "unknown" opcodes are sourced from other OSS projects and are seemingly required for this display to function.
+ // clang-format off
+ const uint8_t gc9a01_init_sequence[] = {
+ // Command, Delay, N, Data[N]
+ GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
+ 0xEB, 0, 1, 0x14,
+ GC9A01_SET_INTER_REG_ENABLE1, 0, 0,
+ GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
+ 0xEB, 0, 1, 0x14,
+ 0x84, 0, 1, 0x40,
+ 0x85, 0, 1, 0xFF,
+ 0x86, 0, 1, 0xFF,
+ 0x87, 0, 1, 0xFF,
+ 0x88, 0, 1, 0x0A,
+ 0x89, 0, 1, 0x21,
+ 0x8a, 0, 1, 0x00,
+ 0x8b, 0, 1, 0x80,
+ 0x8c, 0, 1, 0x01,
+ 0x8d, 0, 1, 0x01,
+ 0x8e, 0, 1, 0xFF,
+ 0x8f, 0, 1, 0xFF,
+ GC9A01_SET_FUNCTION_CTL, 0, 2, 0x00, 0x20,
+ GC9A01_SET_PIX_FMT, 0, 1, 0x55,
+ 0x90, 0, 4, 0x08, 0x08, 0x08, 0x08,
+ 0xBD, 0, 1, 0x06,
+ 0xBC, 0, 1, 0x00,
+ 0xFF, 0, 3, 0x60, 0x01, 0x04,
+ GC9A01_SET_POWER_CTL_2, 0, 1, 0x13,
+ GC9A01_SET_POWER_CTL_3, 0, 1, 0x13,
+ GC9A01_SET_POWER_CTL_4, 0, 1, 0x22,
+ 0xBE, 0, 1, 0x11,
+ 0xE1, 0, 2, 0x10, 0x0E,
+ 0xDF, 0, 3, 0x21, 0x0C, 0x02,
+ GC9A01_SET_GAMMA1, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
+ GC9A01_SET_GAMMA2, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
+ GC9A01_SET_GAMMA3, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
+ GC9A01_SET_GAMMA4, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
+ 0xED, 0, 2, 0x1B, 0x0B,
+ 0xAE, 0, 1, 0x77,
+ 0xCD, 0, 1, 0x63,
+ 0x70, 0, 9, 0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03,
+ GC9A01_SET_FRAME_RATE, 0, 1, 0x34,
+ 0x62, 0, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70,
+ 0x63, 0, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70,
+ 0x64, 0, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07,
+ 0x66, 0, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00,
+ 0x67, 0, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98,
+ 0x74, 0, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00,
+ 0x98, 0, 2, 0x3E, 0x07,
+ GC9A01_CMD_TEARING_OFF, 0, 0,
+ GC9A01_CMD_INVERT_OFF, 0, 0,
+ GC9A01_CMD_SLEEP_OFF, 120, 0,
+ GC9A01_CMD_DISPLAY_ON, 20, 0
+ };
+ // clang-format on
+
+ // clang-format on
+ qp_comms_bulk_command_sequence(device, gc9a01_init_sequence, sizeof(gc9a01_init_sequence));
+
+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
+ const uint8_t madctl[] = {
+ [QP_ROTATION_0] = GC9A01_MADCTL_BGR,
+ [QP_ROTATION_90] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MV,
+ [QP_ROTATION_180] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MY,
+ [QP_ROTATION_270] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MV | GC9A01_MADCTL_MY,
+ };
+ qp_comms_command_databyte(device, GC9A01_SET_MEM_ACS_CTL, madctl[rotation]);
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver vtable
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+const struct tft_panel_dc_reset_painter_driver_vtable_t gc9a01_driver_vtable = {
+ .base =
+ {
+ .init = qp_gc9a01_init,
+ .power = qp_tft_panel_power,
+ .clear = qp_tft_panel_clear,
+ .flush = qp_tft_panel_flush,
+ .pixdata = qp_tft_panel_pixdata,
+ .viewport = qp_tft_panel_viewport,
+ .palette_convert = qp_tft_panel_palette_convert,
+ .append_pixels = qp_tft_panel_append_pixels,
+ },
+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
+ .num_window_bytes = 2,
+ .swap_window_coords = false,
+ .opcodes =
+ {
+ .display_on = GC9A01_CMD_DISPLAY_ON,
+ .display_off = GC9A01_CMD_DISPLAY_OFF,
+ .set_column_address = GC9A01_SET_COL_ADDR,
+ .set_row_address = GC9A01_SET_PAGE_ADDR,
+ .enable_writes = GC9A01_SET_MEM,
+ },
+};
+
+#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
+// Factory function for creating a handle to the ILI9341 device
+painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
+ for (uint32_t i = 0; i < GC9A01_NUM_DEVICES; ++i) {
+ tft_panel_dc_reset_painter_device_t *driver = &gc9a01_drivers[i];
+ if (!driver->base.driver_vtable) {
+ driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&gc9a01_driver_vtable;
+ driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
+ driver->base.native_bits_per_pixel = 16; // RGB565
+ driver->base.panel_width = panel_width;
+ driver->base.panel_height = panel_height;
+ driver->base.rotation = QP_ROTATION_0;
+ driver->base.offset_x = 0;
+ driver->base.offset_y = 0;
+
+ // SPI and other pin configuration
+ driver->base.comms_config = &driver->spi_dc_reset_config;
+ driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
+ driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
+ driver->spi_dc_reset_config.spi_config.lsb_first = false;
+ driver->spi_dc_reset_config.spi_config.mode = spi_mode;
+ driver->spi_dc_reset_config.dc_pin = dc_pin;
+ driver->spi_dc_reset_config.reset_pin = reset_pin;
+ return (painter_device_t)driver;
+ }
+ }
+ return NULL;
+}
+
+#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE
diff --git a/drivers/painter/gc9a01/qp_gc9a01.h b/drivers/painter/gc9a01/qp_gc9a01.h
new file mode 100644
index 0000000000..e2b1939564
--- /dev/null
+++ b/drivers/painter/gc9a01/qp_gc9a01.h
@@ -0,0 +1,37 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "gpio.h"
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter GC9A01 configurables (add to your keyboard's config.h)
+
+#ifndef GC9A01_NUM_DEVICES
+/**
+ * @def This controls the maximum number of GC9A01 devices that Quantum Painter can communicate with at any one time.
+ * Increasing this number allows for multiple displays to be used.
+ */
+# define GC9A01_NUM_DEVICES 1
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter GC9A01 device factories
+
+#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
+/**
+ * Factory method for an GC9A01 SPI LCD device.
+ *
+ * @param panel_width[in] the width of the display panel
+ * @param panel_height[in] the height of the display panel
+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
+ * @param dc_pin[in] the GPIO pin used for D/C control
+ * @param reset_pin[in] the GPIO pin used for RST
+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
+ * @param spi_mode[in] the SPI mode to use when communicating with the display
+ * @return the device handle used with all drawing routines in Quantum Painter
+ */
+painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE
diff --git a/drivers/painter/gc9a01/qp_gc9a01_opcodes.h b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h
new file mode 100644
index 0000000000..6ff4efe7a8
--- /dev/null
+++ b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h
@@ -0,0 +1,78 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter GC9A01 command opcodes
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Level 1 command opcodes
+
+#define GC9A01_GET_ID_INFO 0x04 // Get ID information
+#define GC9A01_GET_STATUS 0x09 // Get status
+#define GC9A01_CMD_SLEEP_ON 0x10 // Enter sleep mode
+#define GC9A01_CMD_SLEEP_OFF 0x11 // Exit sleep mode
+#define GC9A01_CMD_PARTIAL_ON 0x12 // Enter partial mode
+#define GC9A01_CMD_PARTIAL_OFF 0x13 // Exit partial mode
+#define GC9A01_CMD_INVERT_ON 0x20 // Enter inverted mode
+#define GC9A01_CMD_INVERT_OFF 0x21 // Exit inverted mode
+#define GC9A01_CMD_DISPLAY_OFF 0x28 // Disable display
+#define GC9A01_CMD_DISPLAY_ON 0x29 // Enable display
+#define GC9A01_SET_COL_ADDR 0x2A // Set column address
+#define GC9A01_SET_PAGE_ADDR 0x2B // Set page address
+#define GC9A01_SET_MEM 0x2C // Set memory
+#define GC9A01_SET_PARTIAL_AREA 0x30 // Set partial area
+#define GC9A01_SET_VSCROLL 0x33 // Set vertical scroll def
+#define GC9A01_CMD_TEARING_ON 0x34 // Tearing line enabled
+#define GC9A01_CMD_TEARING_OFF 0x35 // Tearing line disabled
+#define GC9A01_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
+#define GC9A01_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
+#define GC9A01_CMD_IDLE_OFF 0x38 // Exit idle mode
+#define GC9A01_CMD_IDLE_ON 0x39 // Enter idle mode
+#define GC9A01_SET_PIX_FMT 0x3A // Set pixel format
+#define GC9A01_SET_MEM_CONT 0x3C // Set memory continue
+#define GC9A01_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
+#define GC9A01_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
+#define GC9A01_SET_BRIGHTNESS 0x51 // Set brightness
+#define GC9A01_SET_DISPLAY_CTL 0x53 // Set display ctl
+#define GC9A01_GET_ID1 0xDA // Get ID1
+#define GC9A01_GET_ID2 0xDB // Get ID2
+#define GC9A01_GET_ID3 0xDC // Get ID3
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Level 2 command opcodes
+
+#define GC9A01_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
+#define GC9A01_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
+#define GC9A01_SET_FUNCTION_CTL 0xB6 // Set function ctl
+#define GC9A01_SET_TEARING_EFFECT 0xBA // Set backlight ctl 3
+#define GC9A01_SET_IF_CTL 0xF6 // Set interface control
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Level 3 command opcodes
+
+#define GC9A01_SET_FRAME_RATE 0xE8 // Set frame rate
+#define GC9A01_SET_SPI_2DATA 0xE9 // Set frame rate
+#define GC9A01_SET_POWER_CTL_1 0xC1 // Set power ctl 1
+#define GC9A01_SET_POWER_CTL_2 0xC3 // Set power ctl 2
+#define GC9A01_SET_POWER_CTL_3 0xC4 // Set power ctl 3
+#define GC9A01_SET_POWER_CTL_4 0xC9 // Set power ctl 4
+#define GC9A01_SET_POWER_CTL_7 0xA7 // Set power ctl 7
+#define GC9A01_SET_INTER_REG_ENABLE1 0xFE // Enable Inter Register 1
+#define GC9A01_SET_INTER_REG_ENABLE2 0xEF // Enable Inter Register 2
+#define GC9A01_SET_GAMMA1 0xF0 //
+#define GC9A01_SET_GAMMA2 0xF1
+#define GC9A01_SET_GAMMA3 0xF2
+#define GC9A01_SET_GAMMA4 0xF3
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// MADCTL Flags
+#define GC9A01_MADCTL_MY 0b10000000
+#define GC9A01_MADCTL_MX 0b01000000
+#define GC9A01_MADCTL_MV 0b00100000
+#define GC9A01_MADCTL_ML 0b00010000
+#define GC9A01_MADCTL_RGB 0b00000000
+#define GC9A01_MADCTL_BGR 0b00001000
+#define GC9A01_MADCTL_MH 0b00000100
diff --git a/drivers/painter/ili9xxx/qp_ili9163.c b/drivers/painter/ili9xxx/qp_ili9163.c
new file mode 100644
index 0000000000..beaac0fbb5
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9163.c
@@ -0,0 +1,121 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_ili9163.h"
+#include "qp_ili9xxx_opcodes.h"
+#include "qp_tft_panel.h"
+
+#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
+# include "qp_comms_spi.h"
+#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Common
+
+// Driver storage
+tft_panel_dc_reset_painter_device_t ili9163_drivers[ILI9163_NUM_DEVICES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Initialization
+
+bool qp_ili9163_init(painter_device_t device, painter_rotation_t rotation) {
+ // clang-format off
+ const uint8_t ili9163_init_sequence[] = {
+ // Command, Delay, N, Data[N]
+ ILI9XXX_CMD_RESET, 120, 0,
+ ILI9XXX_CMD_SLEEP_OFF, 5, 0,
+ ILI9XXX_SET_PIX_FMT, 0, 1, 0x55,
+ ILI9XXX_SET_GAMMA, 0, 1, 0x04,
+ ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x01,
+ ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0xFF, 0x06,
+ ILI9XXX_SET_PGAMMA, 0, 15, 0x36, 0x29, 0x12, 0x22, 0x1C, 0x15, 0x42, 0xB7, 0x2F, 0x13, 0x12, 0x0A, 0x11, 0x0B, 0x06,
+ ILI9XXX_SET_NGAMMA, 0, 15, 0x09, 0x16, 0x2D, 0x0D, 0x13, 0x15, 0x40, 0x48, 0x53, 0x0C, 0x1D, 0x25, 0x2E, 0x34, 0x39,
+ ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x08, 0x02,
+ ILI9XXX_SET_POWER_CTL_1, 0, 2, 0x0A, 0x02,
+ ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x02,
+ ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x50, 0x63,
+ ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0x00,
+ ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
+ ILI9XXX_CMD_DISPLAY_ON, 20, 0
+ };
+ // clang-format on
+ qp_comms_bulk_command_sequence(device, ili9163_init_sequence, sizeof(ili9163_init_sequence));
+
+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
+ const uint8_t madctl[] = {
+ [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
+ [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
+ [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
+ [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
+ };
+ qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver vtable
+
+const struct tft_panel_dc_reset_painter_driver_vtable_t ili9163_driver_vtable = {
+ .base =
+ {
+ .init = qp_ili9163_init,
+ .power = qp_tft_panel_power,
+ .clear = qp_tft_panel_clear,
+ .flush = qp_tft_panel_flush,
+ .pixdata = qp_tft_panel_pixdata,
+ .viewport = qp_tft_panel_viewport,
+ .palette_convert = qp_tft_panel_palette_convert,
+ .append_pixels = qp_tft_panel_append_pixels,
+ },
+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
+ .num_window_bytes = 2,
+ .swap_window_coords = false,
+ .opcodes =
+ {
+ .display_on = ILI9XXX_CMD_DISPLAY_ON,
+ .display_off = ILI9XXX_CMD_DISPLAY_OFF,
+ .set_column_address = ILI9XXX_SET_COL_ADDR,
+ .set_row_address = ILI9XXX_SET_PAGE_ADDR,
+ .enable_writes = ILI9XXX_SET_MEM,
+ },
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SPI
+
+#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
+
+// Factory function for creating a handle to the ILI9163 device
+painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
+ for (uint32_t i = 0; i < ILI9163_NUM_DEVICES; ++i) {
+ tft_panel_dc_reset_painter_device_t *driver = &ili9163_drivers[i];
+ if (!driver->base.driver_vtable) {
+ driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9163_driver_vtable;
+ driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
+ driver->base.panel_width = panel_width;
+ driver->base.panel_height = panel_height;
+ driver->base.rotation = QP_ROTATION_0;
+ driver->base.offset_x = 0;
+ driver->base.offset_y = 0;
+ driver->base.native_bits_per_pixel = 16; // RGB565
+
+ // SPI and other pin configuration
+ driver->base.comms_config = &driver->spi_dc_reset_config;
+ driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
+ driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
+ driver->spi_dc_reset_config.spi_config.lsb_first = false;
+ driver->spi_dc_reset_config.spi_config.mode = spi_mode;
+ driver->spi_dc_reset_config.dc_pin = dc_pin;
+ driver->spi_dc_reset_config.reset_pin = reset_pin;
+ return (painter_device_t)driver;
+ }
+ }
+ return NULL;
+}
+
+#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/ili9xxx/qp_ili9163.h b/drivers/painter/ili9xxx/qp_ili9163.h
new file mode 100644
index 0000000000..88d23629a9
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9163.h
@@ -0,0 +1,37 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "gpio.h"
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ILI9163 configurables (add to your keyboard's config.h)
+
+#ifndef ILI9163_NUM_DEVICES
+/**
+ * @def This controls the maximum number of ILI9163 devices that Quantum Painter can communicate with at any one time.
+ * Increasing this number allows for multiple displays to be used.
+ */
+# define ILI9163_NUM_DEVICES 1
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ILI9163 device factories
+
+#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
+/**
+ * Factory method for an ILI9163 SPI LCD device.
+ *
+ * @param panel_width[in] the width of the display panel
+ * @param panel_height[in] the height of the display panel
+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
+ * @param dc_pin[in] the GPIO pin used for D/C control
+ * @param reset_pin[in] the GPIO pin used for RST
+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
+ * @param spi_mode[in] the SPI mode to use when communicating with the display
+ * @return the device handle used with all drawing routines in Quantum Painter
+ */
+painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
diff --git a/drivers/painter/ili9xxx/qp_ili9341.c b/drivers/painter/ili9xxx/qp_ili9341.c
new file mode 100644
index 0000000000..1f41dcfc0b
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9341.c
@@ -0,0 +1,128 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_ili9341.h"
+#include "qp_ili9xxx_opcodes.h"
+#include "qp_tft_panel.h"
+
+#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
+# include <qp_comms_spi.h>
+#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Common
+
+// Driver storage
+tft_panel_dc_reset_painter_device_t ili9341_drivers[ILI9341_NUM_DEVICES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Initialization
+
+bool qp_ili9341_init(painter_device_t device, painter_rotation_t rotation) {
+ // clang-format off
+ const uint8_t ili9341_init_sequence[] = {
+ // Command, Delay, N, Data[N]
+ ILI9XXX_CMD_RESET, 120, 0,
+ ILI9XXX_CMD_SLEEP_OFF, 5, 0,
+ ILI9XXX_POWER_CTL_A, 0, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
+ ILI9XXX_POWER_CTL_B, 0, 3, 0x00, 0xD9, 0x30,
+ ILI9XXX_POWER_ON_SEQ_CTL, 0, 4, 0x64, 0x03, 0x12, 0x81,
+ ILI9XXX_SET_PUMP_RATIO_CTL, 0, 1, 0x20,
+ ILI9XXX_SET_POWER_CTL_1, 0, 1, 0x26,
+ ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x11,
+ ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x35, 0x3E,
+ ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0xBE,
+ ILI9XXX_DRV_TIMING_CTL_A, 0, 3, 0x85, 0x10, 0x7A,
+ ILI9XXX_DRV_TIMING_CTL_B, 0, 2, 0x00, 0x00,
+ ILI9XXX_SET_BRIGHTNESS, 0, 1, 0xFF,
+ ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x00,
+ ILI9XXX_SET_GAMMA, 0, 1, 0x01,
+ ILI9XXX_SET_PGAMMA, 0, 15, 0x0F, 0x29, 0x24, 0x0C, 0x0E, 0x09, 0x4E, 0x78, 0x3C, 0x09, 0x13, 0x05, 0x17, 0x11, 0x00,
+ ILI9XXX_SET_NGAMMA, 0, 15, 0x00, 0x16, 0x1B, 0x04, 0x11, 0x07, 0x31, 0x33, 0x42, 0x05, 0x0C, 0x0A, 0x28, 0x2F, 0x0F,
+ ILI9XXX_SET_PIX_FMT, 0, 1, 0x05,
+ ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x00, 0x1B,
+ ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0x0A, 0xA2,
+ ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
+ ILI9XXX_CMD_DISPLAY_ON, 20, 0
+ };
+ // clang-format on
+ qp_comms_bulk_command_sequence(device, ili9341_init_sequence, sizeof(ili9341_init_sequence));
+
+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
+ const uint8_t madctl[] = {
+ [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
+ [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
+ [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
+ [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
+ };
+ qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver vtable
+
+const struct tft_panel_dc_reset_painter_driver_vtable_t ili9341_driver_vtable = {
+ .base =
+ {
+ .init = qp_ili9341_init,
+ .power = qp_tft_panel_power,
+ .clear = qp_tft_panel_clear,
+ .flush = qp_tft_panel_flush,
+ .pixdata = qp_tft_panel_pixdata,
+ .viewport = qp_tft_panel_viewport,
+ .palette_convert = qp_tft_panel_palette_convert,
+ .append_pixels = qp_tft_panel_append_pixels,
+ },
+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
+ .num_window_bytes = 2,
+ .swap_window_coords = false,
+ .opcodes =
+ {
+ .display_on = ILI9XXX_CMD_DISPLAY_ON,
+ .display_off = ILI9XXX_CMD_DISPLAY_OFF,
+ .set_column_address = ILI9XXX_SET_COL_ADDR,
+ .set_row_address = ILI9XXX_SET_PAGE_ADDR,
+ .enable_writes = ILI9XXX_SET_MEM,
+ },
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SPI
+
+#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
+
+// Factory function for creating a handle to the ILI9341 device
+painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
+ for (uint32_t i = 0; i < ILI9341_NUM_DEVICES; ++i) {
+ tft_panel_dc_reset_painter_device_t *driver = &ili9341_drivers[i];
+ if (!driver->base.driver_vtable) {
+ driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9341_driver_vtable;
+ driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
+ driver->base.native_bits_per_pixel = 16; // RGB565
+ driver->base.panel_width = panel_width;
+ driver->base.panel_height = panel_height;
+ driver->base.rotation = QP_ROTATION_0;
+ driver->base.offset_x = 0;
+ driver->base.offset_y = 0;
+
+ // SPI and other pin configuration
+ driver->base.comms_config = &driver->spi_dc_reset_config;
+ driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
+ driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
+ driver->spi_dc_reset_config.spi_config.lsb_first = false;
+ driver->spi_dc_reset_config.spi_config.mode = spi_mode;
+ driver->spi_dc_reset_config.dc_pin = dc_pin;
+ driver->spi_dc_reset_config.reset_pin = reset_pin;
+ return (painter_device_t)driver;
+ }
+ }
+ return NULL;
+}
+
+#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/ili9xxx/qp_ili9341.h b/drivers/painter/ili9xxx/qp_ili9341.h
new file mode 100644
index 0000000000..28b0152a84
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9341.h
@@ -0,0 +1,37 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "gpio.h"
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ILI9341 configurables (add to your keyboard's config.h)
+
+#ifndef ILI9341_NUM_DEVICES
+/**
+ * @def This controls the maximum number of ILI9341 devices that Quantum Painter can communicate with at any one time.
+ * Increasing this number allows for multiple displays to be used.
+ */
+# define ILI9341_NUM_DEVICES 1
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ILI9341 device factories
+
+#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
+/**
+ * Factory method for an ILI9341 SPI LCD device.
+ *
+ * @param panel_width[in] the width of the display panel
+ * @param panel_height[in] the height of the display panel
+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
+ * @param dc_pin[in] the GPIO pin used for D/C control
+ * @param reset_pin[in] the GPIO pin used for RST
+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
+ * @param spi_mode[in] the SPI mode to use when communicating with the display
+ * @return the device handle used with all drawing routines in Quantum Painter
+ */
+painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
diff --git a/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
new file mode 100644
index 0000000000..1fa395cb89
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
@@ -0,0 +1,100 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ILI9xxx command opcodes
+#define ILI9XXX_CMD_NOP 0x00 // No operation
+#define ILI9XXX_CMD_RESET 0x01 // Software reset
+#define ILI9XXX_GET_ID_INFO 0x04 // Get ID information
+#define ILI9XXX_GET_STATUS 0x09 // Get status
+#define ILI9XXX_GET_PWR_MODE 0x0A // Get power mode
+#define ILI9XXX_GET_MADCTL 0x0B // Get MADCTL
+#define ILI9XXX_GET_PIX_FMT 0x0C // Get pixel format
+#define ILI9XXX_GET_IMG_FMT 0x0D // Get image format
+#define ILI9XXX_GET_SIG_MODE 0x0E // Get signal mode
+#define ILI9XXX_GET_SELF_DIAG 0x0F // Get self-diagnostics
+#define ILI9XXX_CMD_SLEEP_ON 0x10 // Enter sleep mode
+#define ILI9XXX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
+#define ILI9XXX_CMD_PARTIAL_ON 0x12 // Enter partial mode
+#define ILI9XXX_CMD_PARTIAL_OFF 0x13 // Exit partial mode
+#define ILI9XXX_CMD_INVERT_ON 0x20 // Enter inverted mode
+#define ILI9XXX_CMD_INVERT_OFF 0x21 // Exit inverted mode
+#define ILI9XXX_SET_GAMMA 0x26 // Set gamma params
+#define ILI9XXX_CMD_DISPLAY_OFF 0x28 // Disable display
+#define ILI9XXX_CMD_DISPLAY_ON 0x29 // Enable display
+#define ILI9XXX_SET_COL_ADDR 0x2A // Set column address
+#define ILI9XXX_SET_PAGE_ADDR 0x2B // Set page address
+#define ILI9XXX_SET_MEM 0x2C // Set memory
+#define ILI9XXX_SET_COLOR 0x2D // Set color
+#define ILI9XXX_GET_MEM 0x2E // Get memory
+#define ILI9XXX_SET_PARTIAL_AREA 0x30 // Set partial area
+#define ILI9XXX_SET_VSCROLL 0x33 // Set vertical scroll def
+#define ILI9XXX_CMD_TEARING_ON 0x34 // Tearing line enabled
+#define ILI9XXX_CMD_TEARING_OFF 0x35 // Tearing line disabled
+#define ILI9XXX_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
+#define ILI9XXX_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
+#define ILI9XXX_CMD_IDLE_OFF 0x38 // Exit idle mode
+#define ILI9XXX_CMD_IDLE_ON 0x39 // Enter idle mode
+#define ILI9XXX_SET_PIX_FMT 0x3A // Set pixel format
+#define ILI9XXX_SET_MEM_CONT 0x3C // Set memory continue
+#define ILI9XXX_GET_MEM_CONT 0x3E // Get memory continue
+#define ILI9XXX_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
+#define ILI9XXX_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
+#define ILI9XXX_SET_BRIGHTNESS 0x51 // Set brightness
+#define ILI9XXX_GET_BRIGHTNESS 0x52 // Get brightness
+#define ILI9XXX_SET_DISPLAY_CTL 0x53 // Set display ctl
+#define ILI9XXX_GET_DISPLAY_CTL 0x54 // Get display ctl
+#define ILI9XXX_SET_CABC 0x55 // Set CABC
+#define ILI9XXX_GET_CABC 0x56 // Get CABC
+#define ILI9XXX_SET_CABC_MIN 0x5E // Set CABC min
+#define ILI9XXX_GET_CABC_MIN 0x5F // Set CABC max
+#define ILI9XXX_GET_ID1 0xDA // Get ID1
+#define ILI9XXX_GET_ID2 0xDB // Get ID2
+#define ILI9XXX_GET_ID3 0xDC // Get ID3
+#define ILI9XXX_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
+#define ILI9XXX_SET_FRAME_CTL_NORMAL 0xB1 // Set frame ctl (normal)
+#define ILI9XXX_SET_FRAME_CTL_IDLE 0xB2 // Set frame ctl (idle)
+#define ILI9XXX_SET_FRAME_CTL_PARTIAL 0xB3 // Set frame ctl (partial)
+#define ILI9XXX_SET_INVERSION_CTL 0xB4 // Set inversion ctl
+#define ILI9XXX_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
+#define ILI9XXX_SET_FUNCTION_CTL 0xB6 // Set function ctl
+#define ILI9XXX_SET_ENTRY_MODE 0xB7 // Set entry mode
+#define ILI9XXX_SET_LIGHT_CTL_1 0xB8 // Set backlight ctl 1
+#define ILI9XXX_SET_LIGHT_CTL_2 0xB9 // Set backlight ctl 2
+#define ILI9XXX_SET_LIGHT_CTL_3 0xBA // Set backlight ctl 3
+#define ILI9XXX_SET_LIGHT_CTL_4 0xBB // Set backlight ctl 4
+#define ILI9XXX_SET_LIGHT_CTL_5 0xBC // Set backlight ctl 5
+#define ILI9XXX_SET_LIGHT_CTL_7 0xBE // Set backlight ctl 7
+#define ILI9XXX_SET_LIGHT_CTL_8 0xBF // Set backlight ctl 8
+#define ILI9XXX_SET_POWER_CTL_1 0xC0 // Set power ctl 1
+#define ILI9XXX_SET_POWER_CTL_2 0xC1 // Set power ctl 2
+#define ILI9XXX_SET_VCOM_CTL_1 0xC5 // Set VCOM ctl 1
+#define ILI9XXX_SET_VCOM_CTL_2 0xC7 // Set VCOM ctl 2
+#define ILI9XXX_POWER_CTL_A 0xCB // Set power control A
+#define ILI9XXX_POWER_CTL_B 0xCF // Set power control B
+#define ILI9XXX_DRV_TIMING_CTL_A 0xE8 // Set driver timing control A
+#define ILI9XXX_DRV_TIMING_CTL_B 0xEA // Set driver timing control B
+#define ILI9XXX_POWER_ON_SEQ_CTL 0xED // Set Power on sequence control
+#define ILI9XXX_SET_NVMEM 0xD0 // Set NVMEM data
+#define ILI9XXX_GET_NVMEM_KEY 0xD1 // Get NVMEM protect key
+#define ILI9XXX_GET_NVMEM_STATUS 0xD2 // Get NVMEM status
+#define ILI9XXX_GET_ID4 0xD3 // Get ID4
+#define ILI9XXX_SET_PGAMMA 0xE0 // Set positive gamma
+#define ILI9XXX_SET_NGAMMA 0xE1 // Set negative gamma
+#define ILI9XXX_SET_DGAMMA_CTL_1 0xE2 // Set digital gamma ctl 1
+#define ILI9XXX_SET_DGAMMA_CTL_2 0xE3 // Set digital gamma ctl 2
+#define ILI9XXX_ENABLE_3_GAMMA 0xF2 // Enable 3 gamma
+#define ILI9XXX_SET_IF_CTL 0xF6 // Set interface control
+#define ILI9XXX_SET_PUMP_RATIO_CTL 0xF7 // Set pump ratio control
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// MADCTL Flags
+#define ILI9XXX_MADCTL_MY 0b10000000
+#define ILI9XXX_MADCTL_MX 0b01000000
+#define ILI9XXX_MADCTL_MV 0b00100000
+#define ILI9XXX_MADCTL_ML 0b00010000
+#define ILI9XXX_MADCTL_RGB 0b00000000
+#define ILI9XXX_MADCTL_BGR 0b00001000
+#define ILI9XXX_MADCTL_MH 0b00000100
diff --git a/drivers/painter/ssd1351/qp_ssd1351.c b/drivers/painter/ssd1351/qp_ssd1351.c
new file mode 100644
index 0000000000..970e7e67f3
--- /dev/null
+++ b/drivers/painter/ssd1351/qp_ssd1351.c
@@ -0,0 +1,125 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_ssd1351.h"
+#include "qp_ssd1351_opcodes.h"
+#include "qp_tft_panel.h"
+
+#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
+# include "qp_comms_spi.h"
+#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Common
+
+// Driver storage
+tft_panel_dc_reset_painter_device_t ssd1351_drivers[SSD1351_NUM_DEVICES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Initialization
+
+bool qp_ssd1351_init(painter_device_t device, painter_rotation_t rotation) {
+ tft_panel_dc_reset_painter_device_t *driver = (tft_panel_dc_reset_painter_device_t *)device;
+
+ // clang-format off
+ const uint8_t ssd1351_init_sequence[] = {
+ // Command, Delay, N, Data[N]
+ SSD1351_COMMANDLOCK, 5, 1, 0x12,
+ SSD1351_COMMANDLOCK, 5, 1, 0xB1,
+ SSD1351_DISPLAYOFF, 5, 0,
+ SSD1351_CLOCKDIV, 5, 1, 0xF1,
+ SSD1351_MUXRATIO, 5, 1, 0x7F,
+ SSD1351_DISPLAYOFFSET, 5, 1, 0x00,
+ SSD1351_SETGPIO, 5, 1, 0x00,
+ SSD1351_FUNCTIONSELECT, 5, 1, 0x01,
+ SSD1351_PRECHARGE, 5, 1, 0x32,
+ SSD1351_VCOMH, 5, 1, 0x05,
+ SSD1351_NORMALDISPLAY, 5, 0,
+ SSD1351_CONTRASTABC, 5, 3, 0xC8, 0x80, 0xC8,
+ SSD1351_CONTRASTMASTER, 5, 1, 0x0F,
+ SSD1351_SETVSL, 5, 3, 0xA0, 0xB5, 0x55,
+ SSD1351_PRECHARGE2, 5, 1, 0x01,
+ SSD1351_DISPLAYON, 5, 0,
+ };
+ // clang-format on
+ qp_comms_bulk_command_sequence(device, ssd1351_init_sequence, sizeof(ssd1351_init_sequence));
+
+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
+ const uint8_t madctl[] = {
+ [QP_ROTATION_0] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MY,
+ [QP_ROTATION_90] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX | SSD1351_MADCTL_MY | SSD1351_MADCTL_MV,
+ [QP_ROTATION_180] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX,
+ [QP_ROTATION_270] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MV,
+ };
+ qp_comms_command_databyte(device, SSD1351_SETREMAP, madctl[rotation]);
+ qp_comms_command_databyte(device, SSD1351_STARTLINE, (rotation == QP_ROTATION_0 || rotation == QP_ROTATION_90) ? driver->base.panel_height : 0);
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver vtable
+
+const struct tft_panel_dc_reset_painter_driver_vtable_t ssd1351_driver_vtable = {
+ .base =
+ {
+ .init = qp_ssd1351_init,
+ .power = qp_tft_panel_power,
+ .clear = qp_tft_panel_clear,
+ .flush = qp_tft_panel_flush,
+ .pixdata = qp_tft_panel_pixdata,
+ .viewport = qp_tft_panel_viewport,
+ .palette_convert = qp_tft_panel_palette_convert,
+ .append_pixels = qp_tft_panel_append_pixels,
+ },
+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
+ .num_window_bytes = 1,
+ .swap_window_coords = true,
+ .opcodes =
+ {
+ .display_on = SSD1351_DISPLAYON,
+ .display_off = SSD1351_DISPLAYOFF,
+ .set_column_address = SSD1351_SETCOLUMN,
+ .set_row_address = SSD1351_SETROW,
+ .enable_writes = SSD1351_WRITERAM,
+ },
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SPI
+
+#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
+
+// Factory function for creating a handle to the SSD1351 device
+painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
+ for (uint32_t i = 0; i < SSD1351_NUM_DEVICES; ++i) {
+ tft_panel_dc_reset_painter_device_t *driver = &ssd1351_drivers[i];
+ if (!driver->base.driver_vtable) {
+ driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ssd1351_driver_vtable;
+ driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
+ driver->base.panel_width = panel_width;
+ driver->base.panel_height = panel_height;
+ driver->base.rotation = QP_ROTATION_0;
+ driver->base.offset_x = 0;
+ driver->base.offset_y = 0;
+ driver->base.native_bits_per_pixel = 16; // RGB565
+
+ // SPI and other pin configuration
+ driver->base.comms_config = &driver->spi_dc_reset_config;
+ driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
+ driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
+ driver->spi_dc_reset_config.spi_config.lsb_first = false;
+ driver->spi_dc_reset_config.spi_config.mode = spi_mode;
+ driver->spi_dc_reset_config.dc_pin = dc_pin;
+ driver->spi_dc_reset_config.reset_pin = reset_pin;
+ return (painter_device_t)driver;
+ }
+ }
+ return NULL;
+}
+
+#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/ssd1351/qp_ssd1351.h b/drivers/painter/ssd1351/qp_ssd1351.h
new file mode 100644
index 0000000000..0df34f204d
--- /dev/null
+++ b/drivers/painter/ssd1351/qp_ssd1351.h
@@ -0,0 +1,37 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "gpio.h"
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter SSD1351 configurables (add to your keyboard's config.h)
+
+#ifndef SSD1351_NUM_DEVICES
+/**
+ * @def This controls the maximum number of SSD1351 devices that Quantum Painter can communicate with at any one time.
+ * Increasing this number allows for multiple displays to be used.
+ */
+# define SSD1351_NUM_DEVICES 1
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter SSD1351 device factories
+
+#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
+/**
+ * Factory method for an SSD1351 SPI OLED device.
+ *
+ * @param panel_width[in] the width of the display panel
+ * @param panel_height[in] the height of the display panel
+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
+ * @param dc_pin[in] the GPIO pin used for D/C control
+ * @param reset_pin[in] the GPIO pin used for RST
+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
+ * @param spi_mode[in] the SPI mode to use when communicating with the display
+ * @return the device handle used with all drawing routines in Quantum Painter
+ */
+painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
diff --git a/drivers/painter/ssd1351/qp_ssd1351_opcodes.h b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h
new file mode 100644
index 0000000000..48ed2a3a7c
--- /dev/null
+++ b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h
@@ -0,0 +1,48 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter SSD1351 command opcodes
+
+// System function commands
+#define SSD1351_SETCOLUMN 0x15
+#define SSD1351_SETROW 0x75
+#define SSD1351_WRITERAM 0x5C
+#define SSD1351_READRAM 0x5D
+#define SSD1351_SETREMAP 0xA0
+#define SSD1351_STARTLINE 0xA1
+#define SSD1351_DISPLAYOFFSET 0xA2
+#define SSD1351_DISPLAYALLOFF 0xA4
+#define SSD1351_DISPLAYALLON 0xA5
+#define SSD1351_NORMALDISPLAY 0xA6
+#define SSD1351_INVERTDISPLAY 0xA7
+#define SSD1351_FUNCTIONSELECT 0xAB
+#define SSD1351_DISPLAYOFF 0xAE
+#define SSD1351_DISPLAYON 0xAF
+#define SSD1351_PRECHARGE 0xB1
+#define SSD1351_DISPLAYENHANCE 0xB2
+#define SSD1351_CLOCKDIV 0xB3
+#define SSD1351_SETVSL 0xB4
+#define SSD1351_SETGPIO 0xB5
+#define SSD1351_PRECHARGE2 0xB6
+#define SSD1351_SETGRAY 0xB8
+#define SSD1351_USELUT 0xB9
+#define SSD1351_PRECHARGELEVEL 0xBB
+#define SSD1351_VCOMH 0xBE
+#define SSD1351_CONTRASTABC 0xC1
+#define SSD1351_CONTRASTMASTER 0xC7
+#define SSD1351_MUXRATIO 0xCA
+#define SSD1351_COMMANDLOCK 0xFD
+#define SSD1351_HORIZSCROLL 0x96
+#define SSD1351_STOPSCROLL 0x9E
+#define SSD1351_STARTSCROLL 0x9F
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SETREMAP (MADCTL) Flags
+#define SSD1351_MADCTL_MY 0b00010000
+#define SSD1351_MADCTL_MX 0b00000010
+#define SSD1351_MADCTL_MV 0b00000001
+#define SSD1351_MADCTL_RGB 0b01100000
+#define SSD1351_MADCTL_BGR 0b01100100
diff --git a/drivers/painter/st77xx/qp_st7789.c b/drivers/painter/st77xx/qp_st7789.c
new file mode 100644
index 0000000000..d005ece050
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st7789.c
@@ -0,0 +1,144 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_st7789.h"
+#include "qp_st77xx_opcodes.h"
+#include "qp_st7789_opcodes.h"
+#include "qp_tft_panel.h"
+
+#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
+# include "qp_comms_spi.h"
+#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Common
+
+// Driver storage
+tft_panel_dc_reset_painter_device_t st7789_drivers[ST7789_NUM_DEVICES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Automatic viewport offsets
+
+#ifndef ST7789_NO_AUTOMATIC_OFFSETS
+static inline void st7789_automatic_viewport_offsets(painter_device_t device, painter_rotation_t rotation) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+
+ // clang-format off
+ const struct {
+ uint16_t offset_x;
+ uint16_t offset_y;
+ } rotation_offsets_240x240[] = {
+ [QP_ROTATION_0] = { .offset_x = 0, .offset_y = 0 },
+ [QP_ROTATION_90] = { .offset_x = 0, .offset_y = 0 },
+ [QP_ROTATION_180] = { .offset_x = 0, .offset_y = 80 },
+ [QP_ROTATION_270] = { .offset_x = 80, .offset_y = 0 },
+ };
+ // clang-format on
+
+ if (driver->panel_width == 240 && driver->panel_height == 240) {
+ driver->offset_x = rotation_offsets_240x240[rotation].offset_x;
+ driver->offset_y = rotation_offsets_240x240[rotation].offset_y;
+ }
+}
+#endif // ST7789_NO_AUTOMATIC_OFFSETS
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Initialization
+
+bool qp_st7789_init(painter_device_t device, painter_rotation_t rotation) {
+ // clang-format off
+ const uint8_t st7789_init_sequence[] = {
+ // Command, Delay, N, Data[N]
+ ST77XX_CMD_RESET, 120, 0,
+ ST77XX_CMD_SLEEP_OFF, 5, 0,
+ ST77XX_SET_PIX_FMT, 0, 1, 0x55,
+ ST77XX_CMD_INVERT_ON, 0, 0,
+ ST77XX_CMD_NORMAL_ON, 0, 0,
+ ST77XX_CMD_DISPLAY_ON, 20, 0
+ };
+ // clang-format on
+ qp_comms_bulk_command_sequence(device, st7789_init_sequence, sizeof(st7789_init_sequence));
+
+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
+ const uint8_t madctl[] = {
+ [QP_ROTATION_0] = ST77XX_MADCTL_RGB,
+ [QP_ROTATION_90] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MV,
+ [QP_ROTATION_180] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MY,
+ [QP_ROTATION_270] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MY,
+ };
+ qp_comms_command_databyte(device, ST77XX_SET_MADCTL, madctl[rotation]);
+
+#ifndef ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
+ st7789_automatic_viewport_offsets(device, rotation);
+#endif // ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver vtable
+
+const struct tft_panel_dc_reset_painter_driver_vtable_t st7789_driver_vtable = {
+ .base =
+ {
+ .init = qp_st7789_init,
+ .power = qp_tft_panel_power,
+ .clear = qp_tft_panel_clear,
+ .flush = qp_tft_panel_flush,
+ .pixdata = qp_tft_panel_pixdata,
+ .viewport = qp_tft_panel_viewport,
+ .palette_convert = qp_tft_panel_palette_convert,
+ .append_pixels = qp_tft_panel_append_pixels,
+ },
+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
+ .num_window_bytes = 2,
+ .swap_window_coords = false,
+ .opcodes =
+ {
+ .display_on = ST77XX_CMD_DISPLAY_ON,
+ .display_off = ST77XX_CMD_DISPLAY_OFF,
+ .set_column_address = ST77XX_SET_COL_ADDR,
+ .set_row_address = ST77XX_SET_ROW_ADDR,
+ .enable_writes = ST77XX_SET_MEM,
+ },
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// SPI
+
+#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
+
+// Factory function for creating a handle to the ST7789 device
+painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
+ for (uint32_t i = 0; i < ST7789_NUM_DEVICES; ++i) {
+ tft_panel_dc_reset_painter_device_t *driver = &st7789_drivers[i];
+ if (!driver->base.driver_vtable) {
+ driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&st7789_driver_vtable;
+ driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
+ driver->base.panel_width = panel_width;
+ driver->base.panel_height = panel_height;
+ driver->base.rotation = QP_ROTATION_0;
+ driver->base.offset_x = 0;
+ driver->base.offset_y = 0;
+ driver->base.native_bits_per_pixel = 16; // RGB565
+
+ // SPI and other pin configuration
+ driver->base.comms_config = &driver->spi_dc_reset_config;
+ driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
+ driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
+ driver->spi_dc_reset_config.spi_config.lsb_first = false;
+ driver->spi_dc_reset_config.spi_config.mode = spi_mode;
+ driver->spi_dc_reset_config.dc_pin = dc_pin;
+ driver->spi_dc_reset_config.reset_pin = reset_pin;
+ return (painter_device_t)driver;
+ }
+ }
+ return NULL;
+}
+
+#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/st77xx/qp_st7789.h b/drivers/painter/st77xx/qp_st7789.h
new file mode 100644
index 0000000000..ec61f5d70b
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st7789.h
@@ -0,0 +1,44 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "gpio.h"
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ST7789 configurables (add to your keyboard's config.h)
+
+#ifndef ST7789_NUM_DEVICES
+/**
+ * @def This controls the maximum number of ST7789 devices that Quantum Painter can communicate with at any one time.
+ * Increasing this number allows for multiple displays to be used.
+ */
+# define ST7789_NUM_DEVICES 1
+#endif
+
+// Additional configuration options to be copied to your keyboard's config.h (don't change here):
+
+// If you know exactly which offsets should be used on your panel with respect to selected rotation, then this config
+// option allows you to save some flash space -- you'll need to invoke qp_set_viewport_offsets() instead from your keyboard.
+// #define ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ST7789 device factories
+
+#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
+/**
+ * Factory method for an ST7789 SPI LCD device.
+ *
+ * @param panel_width[in] the width of the display panel
+ * @param panel_height[in] the height of the display panel
+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
+ * @param dc_pin[in] the GPIO pin used for D/C control
+ * @param reset_pin[in] the GPIO pin used for RST
+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
+ * @param spi_mode[in] the SPI mode to use when communicating with the display
+ * @return the device handle used with all drawing routines in Quantum Painter
+ */
+painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
+#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
diff --git a/drivers/painter/st77xx/qp_st7789_opcodes.h b/drivers/painter/st77xx/qp_st7789_opcodes.h
new file mode 100644
index 0000000000..b5baba7184
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st7789_opcodes.h
@@ -0,0 +1,64 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ST7789 additional command opcodes
+
+// System function commands
+#define ST7789_GET_SELF_DIAG 0x0F // Get self-diagnostic result
+#define ST7789_SET_VERT_SCRL 0x33 // Set vertical scroll definition
+#define ST7789_SET_VERT_SCRL_ADDR 0x37 // SEt Vertical scroll start address
+#define ST7789_SET_MEM_CONT 0x3C // Memory Write continue
+#define ST7789_GET_MEM_CONT 0x3E // Memory Read continue
+#define ST7789_SET_TEAR_LINE 0x44 // Set tear scanline
+#define ST7789_GET_TEAR_LINE 0x45 // Get tear scanline
+#define ST7789_SET_BRIGHTNESS 0x51 // Set display brightness
+#define ST7789_GET_BRIGHTNESS 0x52 // Get display brightness
+#define ST7789_SET_CTRL 0x53 // Set CTRL display
+#define ST7789_GET_CTRL 0x54 // Get CTRL display value
+#define ST7789_SET_CAB_COLOR 0x55 // Set content adaptive brightness control and color enhancement
+#define ST7789_GET_CAB_COLOR 0x56 // Get content adaptive brightness control and color enhancement
+#define ST7789_SET_CAB_BRIGHTNESS 0x5E // Set content adaptive minimum brightness
+#define ST7789_GET_CAB_BRIGHTNESS 0x5F // Get content adaptive minimum brightness
+#define ST7789_GET_ABC_SELF_DIAG 0x68 // Get Auto brightness control self diagnostics
+
+// Panel Function Commands
+#define ST7789_SET_RAM_CTL 0xB0 // Set RAM control
+#define ST7789_SET_RGB_CTL 0xB1 // Set RGB control
+#define ST7789_SET_PORCH_CTL 0xB2 // Set Porch control
+#define ST7789_SET_FRAME_RATE_CTL_1 0xB3 // Set frame rate control 1
+#define ST7789_SET_PARTIAL_CTL 0xB5 // Set Partial control
+#define ST7789_SET_GATE_CTL 0xB7 // Set gate control
+#define ST7789_SET_GATE_ON_TIMING 0xB8 // Set gate on timing adjustment
+#define ST7789_SET_DIGITAL_GAMMA_ON 0xBA // Enable digital gamma
+#define ST7789_SET_VCOM 0xBB // Set VCOM
+#define ST7789_SET_POWER_SAVE 0xBC // Set power saving mode
+#define ST7789_SET_DISP_OFF_POWER 0xBD // Set display off power saving
+#define ST7789_SET_LCM_CTL 0xC0 // Set LCM control
+#define ST7789_SET_IDS 0xC1 // Set IDs
+#define ST7789_SET_VDV_VRH_ON 0xC2 // Set VDV and VRH command enable
+#define ST7789_SET_VRH 0xC3 // Set VRH
+#define ST7789_SET_VDV 0xC4 // Set VDV
+#define ST7789_SET_VCOM_OFFSET 0xC5 // Set VCOM offset ctl
+#define ST7789_SET_FRAME_RATE_CTL_2 0xC6 // Set frame rate control 2
+#define ST7789_SET_CABC_CTL 0xC7 // Set CABC Control
+#define ST7789_GET_REG_1 0xC8 // Get register value selection1
+#define ST7789_GET_REG_2 0xCA // Get register value selection2
+#define ST7789_SET_PWM_FREQ 0xCC // Set PWM frequency
+#define ST7789_SET_POWER_CTL_1 0xD0 // Set power ctl 1
+#define ST7789_SET_VAP_VAN_ON 0xD2 // Enable VAP/VAN signal output
+#define ST7789_SET_CMD2_ENABLE 0xDF // Enable command 2
+#define ST7789_SET_PGAMMA 0xE0 // Set positive gamma
+#define ST7789_SET_NGAMMA 0xE1 // Set negative gamma
+#define ST7789_SET_DIGITAL_GAMMA_RED 0xE2 // Set digital gamma lookup table for red
+#define ST7789_SET_DIGITAL_GAMMA_BLUE 0xE3 // Get digital gamma lookup table for blue
+#define ST7789_SET_GATE_CTL_2 0xE4 // Set gate control 2
+#define ST7789_SET_SPI2_ENABLE 0xE7 // Enable SPI2
+#define ST7789_SET_POWER_CTL_2 0xE8 // Set power ctl 2
+#define ST7789_SET_EQ_TIME_CTL 0xE9 // Set equalize time control
+#define ST7789_SET_PROG_CTL 0xEC // Set program control
+#define ST7789_SET_PROG_MODE_ENABLE 0xFA // Set program mode enable
+#define ST7789_SET_NVMEM 0xFC // Set NVMEM data
+#define ST7789_SET_PROG_ACTION 0xFE // Set program action
diff --git a/drivers/painter/st77xx/qp_st77xx_opcodes.h b/drivers/painter/st77xx/qp_st77xx_opcodes.h
new file mode 100644
index 0000000000..131378d832
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st77xx_opcodes.h
@@ -0,0 +1,51 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter ST77XX command opcodes
+
+// System function commands
+#define ST77XX_CMD_NOP 0x00 // No operation
+#define ST77XX_CMD_RESET 0x01 // Software reset
+#define ST77XX_GET_ID_INFO 0x04 // Get ID information
+#define ST77XX_GET_STATUS 0x09 // Get status
+#define ST77XX_GET_PWR_MODE 0x0A // Get power mode
+#define ST77XX_GET_MADCTL 0x0B // Get mem access ctl
+#define ST77XX_GET_PIX_FMT 0x0C // Get pixel format
+#define ST77XX_GET_IMG_FMT 0x0D // Get image format
+#define ST77XX_GET_SIG_MODE 0x0E // Get signal mode
+#define ST77XX_CMD_SLEEP_ON 0x10 // Enter sleep mode
+#define ST77XX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
+#define ST77XX_CMD_PARTIAL_ON 0x12 // Enter partial mode
+#define ST77XX_CMD_NORMAL_ON 0x13 // Exit partial mode
+#define ST77XX_CMD_INVERT_OFF 0x20 // Exit inverted mode
+#define ST77XX_CMD_INVERT_ON 0x21 // Enter inverted mode
+#define ST77XX_SET_GAMMA 0x26 // Set gamma params
+#define ST77XX_CMD_DISPLAY_OFF 0x28 // Disable display
+#define ST77XX_CMD_DISPLAY_ON 0x29 // Enable display
+#define ST77XX_SET_COL_ADDR 0x2A // Set column address
+#define ST77XX_SET_ROW_ADDR 0x2B // Set page (row) address
+#define ST77XX_SET_MEM 0x2C // Set memory
+#define ST77XX_GET_MEM 0x2E // Get memory
+#define ST77XX_SET_PARTIAL_AREA 0x30 // Set partial area
+#define ST77XX_CMD_TEARING_OFF 0x34 // Tearing line disabled
+#define ST77XX_CMD_TEARING_ON 0x35 // Tearing line enabled
+#define ST77XX_SET_MADCTL 0x36 // Set mem access ctl
+#define ST77XX_CMD_IDLE_OFF 0x38 // Exit idle mode
+#define ST77XX_CMD_IDLE_ON 0x39 // Enter idle mode
+#define ST77XX_SET_PIX_FMT 0x3A // Set pixel format
+#define ST77XX_GET_ID1 0xDA // Get ID1
+#define ST77XX_GET_ID2 0xDB // Get ID2
+#define ST77XX_GET_ID3 0xDC // Get ID3
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// MADCTL Flags
+#define ST77XX_MADCTL_MY 0b10000000
+#define ST77XX_MADCTL_MX 0b01000000
+#define ST77XX_MADCTL_MV 0b00100000
+#define ST77XX_MADCTL_ML 0b00010000
+#define ST77XX_MADCTL_RGB 0b00000000
+#define ST77XX_MADCTL_BGR 0b00001000
+#define ST77XX_MADCTL_MH 0b00000100
diff --git a/drivers/painter/tft_panel/qp_tft_panel.c b/drivers/painter/tft_panel/qp_tft_panel.c
new file mode 100644
index 0000000000..4d636c9509
--- /dev/null
+++ b/drivers/painter/tft_panel/qp_tft_panel.c
@@ -0,0 +1,130 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "color.h"
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_draw.h"
+#include "qp_tft_panel.h"
+
+#define BYTE_SWAP(x) (((((uint16_t)(x)) >> 8) & 0x00FF) | ((((uint16_t)(x)) << 8) & 0xFF00))
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Native pixel format conversion
+
+uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
+ uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
+ return rgb565;
+}
+
+uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b) {
+ uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
+ return BYTE_SWAP(rgb565);
+}
+
+uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b) {
+ uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
+ return bgr565;
+}
+
+uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b) {
+ uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
+ return BYTE_SWAP(bgr565);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter API implementations
+
+// Power control
+bool qp_tft_panel_power(painter_device_t device, bool power_on) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
+ qp_comms_command(device, power_on ? vtable->opcodes.display_on : vtable->opcodes.display_off);
+ return true;
+}
+
+// Screen clear
+bool qp_tft_panel_clear(painter_device_t device) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ driver->driver_vtable->init(device, driver->rotation); // Re-init the LCD
+ return true;
+}
+
+// Screen flush
+bool qp_tft_panel_flush(painter_device_t device) {
+ // No-op, as there's no framebuffer in RAM for this device.
+ return true;
+}
+
+// Viewport to draw to
+bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
+
+ // Fix up the drawing location if required
+ left += driver->offset_x;
+ right += driver->offset_x;
+ top += driver->offset_y;
+ bottom += driver->offset_y;
+
+ // Check if we need to manually swap the window coordinates based on whether or not we're in a sideways rotation
+ if (vtable->swap_window_coords && (driver->rotation == QP_ROTATION_90 || driver->rotation == QP_ROTATION_270)) {
+ uint16_t temp;
+
+ temp = left;
+ left = top;
+ top = temp;
+
+ temp = right;
+ right = bottom;
+ bottom = temp;
+ }
+
+ if (vtable->num_window_bytes == 1) {
+ // Set up the x-window
+ uint8_t xbuf[2] = {left & 0xFF, right & 0xFF};
+ qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
+
+ // Set up the y-window
+ uint8_t ybuf[2] = {top & 0xFF, bottom & 0xFF};
+ qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
+ } else if (vtable->num_window_bytes == 2) {
+ // Set up the x-window
+ uint8_t xbuf[4] = {left >> 8, left & 0xFF, right >> 8, right & 0xFF};
+ qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
+
+ // Set up the y-window
+ uint8_t ybuf[4] = {top >> 8, top & 0xFF, bottom >> 8, bottom & 0xFF};
+ qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
+ }
+
+ // Lock in the window
+ qp_comms_command(device, vtable->opcodes.enable_writes);
+ return true;
+}
+
+// Stream pixel data to the current write position in GRAM
+bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
+ qp_comms_send(device, pixel_data, native_pixel_count * sizeof(uint16_t));
+ return true;
+}
+
+// Convert supplied palette entries into their native equivalents
+bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
+ for (int16_t i = 0; i < palette_size; ++i) {
+ RGB rgb = hsv_to_rgb_nocie((HSV){palette[i].hsv888.h, palette[i].hsv888.s, palette[i].hsv888.v});
+ palette[i].rgb565 = vtable->rgb888_to_native16bit(rgb.r, rgb.g, rgb.b);
+ }
+ return true;
+}
+
+// Append pixels to the target location, keyed by the pixel index
+bool qp_tft_panel_append_pixels(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices) {
+ uint16_t *buf = (uint16_t *)target_buffer;
+ for (uint32_t i = 0; i < pixel_count; ++i) {
+ buf[pixel_offset + i] = palette[palette_indices[i]].rgb565;
+ }
+ return true;
+}
diff --git a/drivers/painter/tft_panel/qp_tft_panel.h b/drivers/painter/tft_panel/qp_tft_panel.h
new file mode 100644
index 0000000000..6eddfc503d
--- /dev/null
+++ b/drivers/painter/tft_panel/qp_tft_panel.h
@@ -0,0 +1,67 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "color.h"
+#include "qp_internal.h"
+
+#ifdef QUANTUM_PAINTER_SPI_ENABLE
+# include "qp_comms_spi.h"
+#endif // QUANTUM_PAINTER_SPI_ENABLE
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Common TFT panel implementation using D/C, and RST pins.
+
+typedef uint16_t (*rgb888_to_native_uint16_t)(uint8_t r, uint8_t g, uint8_t b);
+
+// Driver vtable with extras
+struct tft_panel_dc_reset_painter_driver_vtable_t {
+ struct painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type
+
+ // Conversion function for palette entries
+ rgb888_to_native_uint16_t rgb888_to_native16bit;
+
+ // Number of bytes for transmitting x/y coordinates
+ uint8_t num_window_bytes;
+
+ // Whether or not the x/y coords should be swapped on 90/270 rotation
+ bool swap_window_coords;
+
+ // Opcodes for normal display operation
+ struct {
+ uint8_t display_on;
+ uint8_t display_off;
+ uint8_t set_column_address;
+ uint8_t set_row_address;
+ uint8_t enable_writes;
+ } opcodes;
+};
+
+// Device definition
+typedef struct tft_panel_dc_reset_painter_device_t {
+ struct painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type
+
+ union {
+#ifdef QUANTUM_PAINTER_SPI_ENABLE
+ // SPI-based configurables
+ struct qp_comms_spi_dc_reset_config_t spi_dc_reset_config;
+#endif // QUANTUM_PAINTER_SPI_ENABLE
+
+ // TODO: I2C/parallel etc.
+ };
+} tft_panel_dc_reset_painter_device_t;
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Forward declarations for injecting into concrete driver vtables
+
+bool qp_tft_panel_power(painter_device_t device, bool power_on);
+bool qp_tft_panel_clear(painter_device_t device);
+bool qp_tft_panel_flush(painter_device_t device);
+bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
+bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
+bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
+bool qp_tft_panel_append_pixels(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);
+
+uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b);
+uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b);
+uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b);
+uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b);
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 5f65e677e5..85baa238a8 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -16,7 +16,8 @@ import_names = {
# A mapping of package name to importable name
'pep8-naming': 'pep8ext_naming',
'pyusb': 'usb.core',
- 'qmk-dotty-dict': 'dotty_dict'
+ 'qmk-dotty-dict': 'dotty_dict',
+ 'pillow': 'PIL'
}
safe_commands = [
@@ -67,6 +68,7 @@ subcommands = [
'qmk.cli.multibuild',
'qmk.cli.new.keyboard',
'qmk.cli.new.keymap',
+ 'qmk.cli.painter',
'qmk.cli.pyformat',
'qmk.cli.pytest',
'qmk.cli.via2json',
diff --git a/lib/python/qmk/cli/painter/__init__.py b/lib/python/qmk/cli/painter/__init__.py
new file mode 100644
index 0000000000..d1a225346c
--- /dev/null
+++ b/lib/python/qmk/cli/painter/__init__.py
@@ -0,0 +1,2 @@
+from . import convert_graphics
+from . import make_font
diff --git a/lib/python/qmk/cli/painter/convert_graphics.py b/lib/python/qmk/cli/painter/convert_graphics.py
new file mode 100644
index 0000000000..bbc30d26ff
--- /dev/null
+++ b/lib/python/qmk/cli/painter/convert_graphics.py
@@ -0,0 +1,86 @@
+"""This script tests QGF functionality.
+"""
+import re
+import datetime
+from io import BytesIO
+from qmk.path import normpath
+from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
+from milc import cli
+from PIL import Image
+
+
+@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.')
+@cli.argument('-i', '--input', required=True, help='Specify input graphic file.')
+@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
+@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
+@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.')
+@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.')
+@cli.subcommand('Converts an input image to something QMK understands')
+def painter_convert_graphics(cli):
+ """Converts an image file to a format that Quantum Painter understands.
+
+ This command uses the `qmk.painter` module to generate a Quantum Painter image defintion from an image. The generated definitions are written to a files next to the input -- `INPUT.c` and `INPUT.h`.
+ """
+ # Work out the input file
+ if cli.args.input != '-':
+ cli.args.input = normpath(cli.args.input)
+
+ # Error checking
+ if not cli.args.input.exists():
+ cli.log.error('Input image file does not exist!')
+ cli.print_usage()
+ return False
+
+ # Work out the output directory
+ if len(cli.args.output) == 0:
+ cli.args.output = cli.args.input.parent
+ cli.args.output = normpath(cli.args.output)
+
+ # Ensure we have a valid format
+ if cli.args.format not in valid_formats.keys():
+ cli.log.error('Output format %s is invalid. Allowed values: %s' % (cli.args.format, ', '.join(valid_formats.keys())))
+ cli.print_usage()
+ return False
+
+ # Work out the encoding parameters
+ format = valid_formats[cli.args.format]
+
+ # Load the input image
+ input_img = Image.open(cli.args.input)
+
+ # Convert the image to QGF using PIL
+ out_data = BytesIO()
+ input_img.save(out_data, "QGF", use_deltas=(not cli.args.no_deltas), use_rle=(not cli.args.no_rle), qmk_format=format, verbose=cli.args.verbose)
+ out_bytes = out_data.getvalue()
+
+ # Work out the text substitutions for rendering the output data
+ subs = {
+ 'generated_type': 'image',
+ 'var_prefix': 'gfx',
+ 'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}',
+ 'year': datetime.date.today().strftime("%Y"),
+ 'input_file': cli.args.input.name,
+ 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
+ 'byte_count': len(out_bytes),
+ 'bytes_lines': render_bytes(out_bytes),
+ 'format': cli.args.format,
+ }
+
+ # Render the license
+ subs.update({'license': render_license(subs)})
+
+ # Render and write the header file
+ header_text = render_header(subs)
+ header_file = cli.args.output / (cli.args.input.stem + ".qgf.h")
+ with open(header_file, 'w') as header:
+ print(f"Writing {header_file}...")
+ header.write(header_text)
+ header.close()
+
+ # Render and write the source file
+ source_text = render_source(subs)
+ source_file = cli.args.output / (cli.args.input.stem + ".qgf.c")
+ with open(source_file, 'w') as source:
+ print(f"Writing {source_file}...")
+ source.write(source_text)
+ source.close()
diff --git a/lib/python/qmk/cli/painter/make_font.py b/lib/python/qmk/cli/painter/make_font.py
new file mode 100644
index 0000000000..0762843fd3
--- /dev/null
+++ b/lib/python/qmk/cli/painter/make_font.py
@@ -0,0 +1,87 @@
+"""This script automates the conversion of font files into a format QMK firmware understands.
+"""
+
+import re
+import datetime
+from io import BytesIO
+from qmk.path import normpath
+from qmk.painter_qff import QFFFont
+from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
+from milc import cli
+
+
+@cli.argument('-f', '--font', required=True, help='Specify input font file.')
+@cli.argument('-o', '--output', required=True, help='Specify output image path.')
+@cli.argument('-s', '--size', default=12, help='Specify font size. Default 12.')
+@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.')
+@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
+@cli.argument('-a', '--no-aa', arg_only=True, action='store_true', help='Disable anti-aliasing on fonts.')
+@cli.subcommand('Converts an input font to something QMK understands')
+def painter_make_font_image(cli):
+ # Create the font object
+ font = QFFFont(cli)
+ # Read from the input file
+ cli.args.font = normpath(cli.args.font)
+ font.generate_image(cli.args.font, cli.args.size, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs, use_aa=(False if cli.args.no_aa else True))
+ # Render out the data
+ font.save_to_image(normpath(cli.args.output))
+
+
+@cli.argument('-i', '--input', help='Specify input graphic file.')
+@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
+@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.')
+@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
+@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
+@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.')
+@cli.subcommand('Converts an input font image to something QMK firmware understands')
+def painter_convert_font_image(cli):
+ # Work out the format
+ format = valid_formats[cli.args.format]
+
+ # Create the font object
+ font = QFFFont(cli.log)
+
+ # Read from the input file
+ cli.args.input = normpath(cli.args.input)
+ font.read_from_image(cli.args.input, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs)
+
+ # Work out the output directory
+ if len(cli.args.output) == 0:
+ cli.args.output = cli.args.input.parent
+ cli.args.output = normpath(cli.args.output)
+
+ # Render out the data
+ out_data = BytesIO()
+ font.save_to_qff(format, (False if cli.args.no_rle else True), out_data)
+
+ # Work out the text substitutions for rendering the output data
+ subs = {
+ 'generated_type': 'font',
+ 'var_prefix': 'font',
+ 'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}',
+ 'year': datetime.date.today().strftime("%Y"),
+ 'input_file': cli.args.input.name,
+ 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
+ 'byte_count': out_data.getbuffer().nbytes,
+ 'bytes_lines': render_bytes(out_data.getbuffer().tobytes()),
+ 'format': cli.args.format,
+ }
+
+ # Render the license
+ subs.update({'license': render_license(subs)})
+
+ # Render and write the header file
+ header_text = render_header(subs)
+ header_file = cli.args.output / (cli.args.input.stem + ".qff.h")
+ with open(header_file, 'w') as header:
+ print(f"Writing {header_file}...")
+ header.write(header_text)
+ header.close()
+
+ # Render and write the source file
+ source_text = render_source(subs)
+ source_file = cli.args.output / (cli.args.input.stem + ".qff.c")
+ with open(source_file, 'w') as source:
+ print(f"Writing {source_file}...")
+ source.write(source_text)
+ source.close()
diff --git a/lib/python/qmk/painter.py b/lib/python/qmk/painter.py
new file mode 100644
index 0000000000..d0cc1dddec
--- /dev/null
+++ b/lib/python/qmk/painter.py
@@ -0,0 +1,268 @@
+"""Functions that help us work with Quantum Painter's file formats.
+"""
+import math
+import re
+from string import Template
+from PIL import Image, ImageOps
+
+# The list of valid formats Quantum Painter supports
+valid_formats = {
+ 'pal256': {
+ 'image_format': 'IMAGE_FORMAT_PALETTE',
+ 'bpp': 8,
+ 'has_palette': True,
+ 'num_colors': 256,
+ 'image_format_byte': 0x07, # see qp_internal_formats.h
+ },
+ 'pal16': {
+ 'image_format': 'IMAGE_FORMAT_PALETTE',
+ 'bpp': 4,
+ 'has_palette': True,
+ 'num_colors': 16,
+ 'image_format_byte': 0x06, # see qp_internal_formats.h
+ },
+ 'pal4': {
+ 'image_format': 'IMAGE_FORMAT_PALETTE',
+ 'bpp': 2,
+ 'has_palette': True,
+ 'num_colors': 4,
+ 'image_format_byte': 0x05, # see qp_internal_formats.h
+ },
+ 'pal2': {
+ 'image_format': 'IMAGE_FORMAT_PALETTE',
+ 'bpp': 1,
+ 'has_palette': True,
+ 'num_colors': 2,
+ 'image_format_byte': 0x04, # see qp_internal_formats.h
+ },
+ 'mono256': {
+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
+ 'bpp': 8,
+ 'has_palette': False,
+ 'num_colors': 256,
+ 'image_format_byte': 0x03, # see qp_internal_formats.h
+ },
+ 'mono16': {
+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
+ 'bpp': 4,
+ 'has_palette': False,
+ 'num_colors': 16,
+ 'image_format_byte': 0x02, # see qp_internal_formats.h
+ },
+ 'mono4': {
+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
+ 'bpp': 2,
+ 'has_palette': False,
+ 'num_colors': 4,
+ 'image_format_byte': 0x01, # see qp_internal_formats.h
+ },
+ 'mono2': {
+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
+ 'bpp': 1,
+ 'has_palette': False,
+ 'num_colors': 2,
+ 'image_format_byte': 0x00, # see qp_internal_formats.h
+ }
+}
+
+license_template = """\
+// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// This file was auto-generated by `${generator_command}`
+"""
+
+
+def render_license(subs):
+ license_txt = Template(license_template)
+ return license_txt.substitute(subs)
+
+
+header_file_template = """\
+${license}
+#pragma once
+
+#include <qp.h>
+
+extern const uint32_t ${var_prefix}_${sane_name}_length;
+extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}];
+"""
+
+
+def render_header(subs):
+ header_txt = Template(header_file_template)
+ return header_txt.substitute(subs)
+
+
+source_file_template = """\
+${license}
+#include <qp.h>
+
+const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};
+
+// clang-format off
+const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = {
+${bytes_lines}
+};
+// clang-format on
+"""
+
+
+def render_source(subs):
+ source_txt = Template(source_file_template)
+ return source_txt.substitute(subs)
+
+
+def render_bytes(bytes, newline_after=16):
+ lines = ''
+ for n in range(len(bytes)):
+ if n % newline_after == 0 and n > 0 and n != len(bytes):
+ lines = lines + "\n "
+ elif n == 0:
+ lines = lines + " "
+ lines = lines + " 0x{0:02X},".format(bytes[n])
+ return lines.rstrip()
+
+
+def clean_output(str):
+ str = re.sub(r'\r', '', str)
+ str = re.sub(r'[\n]{3,}', r'\n\n', str)
+ return str
+
+
+def rescale_byte(val, maxval):
+ """Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval].
+ """
+ return int(round(val * maxval / 255.0))
+
+
+def convert_requested_format(im, format):
+ """Convert an image to the requested format.
+ """
+
+ # Work out the requested format
+ ncolors = format["num_colors"]
+ image_format = format["image_format"]
+
+ # Ensure we have a valid number of colors for the palette
+ if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0):
+ raise ValueError("Number of colors must be 2, 4, 16, or 256.")
+
+ # Work out where we're getting the bytes from
+ if image_format == 'IMAGE_FORMAT_GRAYSCALE':
+ # If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel
+ im = ImageOps.grayscale(im)
+ im = im.convert("RGB")
+ elif image_format == 'IMAGE_FORMAT_PALETTE':
+ # If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes
+ im = im.convert("RGB")
+ im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors)
+
+ return im
+
+
+def convert_image_bytes(im, format):
+ """Convert the supplied image to the equivalent bytes required by the QMK firmware.
+ """
+
+ # Work out the requested format
+ ncolors = format["num_colors"]
+ image_format = format["image_format"]
+ shifter = int(math.log2(ncolors))
+ pixels_per_byte = int(8 / math.log2(ncolors))
+ (width, height) = im.size
+ expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
+
+ if image_format == 'IMAGE_FORMAT_GRAYSCALE':
+ # Take the red channel
+ image_bytes = im.tobytes("raw", "R")
+ image_bytes_len = len(image_bytes)
+
+ # No palette
+ palette = None
+
+ bytearray = []
+ for x in range(expected_byte_count):
+ byte = 0
+ for n in range(pixels_per_byte):
+ byte_offset = x * pixels_per_byte + n
+ if byte_offset < image_bytes_len:
+ # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together
+ byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter))
+ bytearray.append(byte)
+
+ elif image_format == 'IMAGE_FORMAT_PALETTE':
+ # Convert each pixel to the palette bytes
+ image_bytes = im.tobytes("raw", "P")
+ image_bytes_len = len(image_bytes)
+
+ # Export the palette
+ palette = []
+ pal = im.getpalette()
+ for n in range(0, ncolors * 3, 3):
+ palette.append((pal[n + 0], pal[n + 1], pal[n + 2]))
+
+ bytearray = []
+ for x in range(expected_byte_count):
+ byte = 0
+ for n in range(pixels_per_byte):
+ byte_offset = x * pixels_per_byte + n
+ if byte_offset < image_bytes_len:
+ # If color, each input byte is the index into the color palette -- pack them together
+ byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
+ bytearray.append(byte)
+
+ if len(bytearray) != expected_byte_count:
+ raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")
+
+ return (palette, bytearray)
+
+
+def compress_bytes_qmk_rle(bytearray):
+ debug_dump = False
+ output = []
+ temp = []
+ repeat = False
+
+ def append_byte(c):
+ if debug_dump:
+ print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c)
+ output.append(c)
+
+ def append_range(r):
+ append_byte(127 + len(r))
+ if debug_dump:
+ print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']')
+ output.extend(r)
+
+ for n in range(0, len(bytearray) + 1):
+ end = True if n == len(bytearray) else False
+ if not end:
+ c = bytearray[n]
+ temp.append(c)
+ if len(temp) <= 1:
+ continue
+
+ if debug_dump:
+ print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']')
+
+ if repeat:
+ if temp[-1] != temp[-2]:
+ repeat = False
+ if not repeat or len(temp) == 128 or end:
+ append_byte(len(temp) if end else len(temp) - 1)
+ append_byte(temp[0])
+ temp = [temp[-1]]
+ repeat = False
+ else:
+ if len(temp) >= 2 and temp[-1] == temp[-2]:
+ repeat = True
+ if len(temp) > 2:
+ append_range(temp[0:(len(temp) - 2)])
+ temp = [temp[-1], temp[-1]]
+ continue
+ if len(temp) == 128 or end:
+ append_range(temp)
+ temp = []
+ repeat = False
+ return output
diff --git a/lib/python/qmk/painter_qff.py b/lib/python/qmk/painter_qff.py
new file mode 100644
index 0000000000..746bb166e5
--- /dev/null
+++ b/lib/python/qmk/painter_qff.py
@@ -0,0 +1,401 @@
+# Copyright 2021 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Quantum Font File "QFF" Font File Format.
+# See https://docs.qmk.fm/#/quantum_painter_qff for more information.
+
+from pathlib import Path
+from typing import Dict, Any
+from colorsys import rgb_to_hsv
+from PIL import Image, ImageDraw, ImageFont, ImageChops
+from PIL._binary import o8, o16le as o16, o32le as o32
+from qmk.painter_qgf import QGFBlockHeader, QGFFramePaletteDescriptorV1
+from milc.attrdict import AttrDict
+import qmk.painter
+
+
+def o24(i):
+ return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
+
+
+########################################################################################################################
+
+
+class QFFGlyphInfo(AttrDict):
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+
+ for n, value in enumerate(args):
+ self[f'arg:{n}'] = value
+
+ for key, value in kwargs.items():
+ self[key] = value
+
+ def write(self, fp, include_code_point):
+ if include_code_point is True:
+ fp.write(o24(ord(self.code_point)))
+
+ value = ((self.data_offset << 6) & 0xFFFFC0) | (self.w & 0x3F)
+ fp.write(o24(value))
+
+
+########################################################################################################################
+
+
+class QFFFontDescriptor:
+ type_id = 0x00
+ length = 20
+ magic = 0x464651
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QFFFontDescriptor.type_id
+ self.header.length = QFFFontDescriptor.length
+ self.version = 1
+ self.total_file_size = 0
+ self.line_height = 0
+ self.has_ascii_table = False
+ self.unicode_glyph_count = 0
+ self.format = 0xFF
+ self.flags = 0
+ self.compression = 0xFF
+ self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
+
+ def write(self, fp):
+ self.header.write(fp)
+ fp.write(
+ b'' # start off with empty bytes...
+ + o24(QFFFontDescriptor.magic) # magic
+ + o8(self.version) # version
+ + o32(self.total_file_size) # file size
+ + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
+ + o8(self.line_height) # line height
+ + o8(1 if self.has_ascii_table is True else 0) # whether or not we have an ascii table present
+ + o16(self.unicode_glyph_count & 0xFFFF) # number of unicode glyphs present
+ + o8(self.format) # format
+ + o8(self.flags) # flags
+ + o8(self.compression) # compression
+ + o8(self.transparency_index) # transparency index
+ )
+
+ @property
+ def is_transparent(self):
+ return (self.flags & 0x01) == 0x01
+
+ @is_transparent.setter
+ def is_transparent(self, val):
+ if val:
+ self.flags |= 0x01
+ else:
+ self.flags &= ~0x01
+
+
+########################################################################################################################
+
+
+class QFFAsciiGlyphTableV1:
+ type_id = 0x01
+ length = 95 * 3 # We have 95 glyphs: [0x20...0x7E]
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QFFAsciiGlyphTableV1.type_id
+ self.header.length = QFFAsciiGlyphTableV1.length
+
+ # Each glyph is key=code_point, value=QFFGlyphInfo
+ self.glyphs = {}
+
+ def add_glyph(self, glyph: QFFGlyphInfo):
+ self.glyphs[ord(glyph.code_point)] = glyph
+
+ def write(self, fp):
+ self.header.write(fp)
+
+ for n in range(0x20, 0x7F):
+ self.glyphs[n].write(fp, False)
+
+
+########################################################################################################################
+
+
+class QFFUnicodeGlyphTableV1:
+ type_id = 0x02
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QFFUnicodeGlyphTableV1.type_id
+ self.header.length = 0
+
+ # Each glyph is key=code_point, value=QFFGlyphInfo
+ self.glyphs = {}
+
+ def add_glyph(self, glyph: QFFGlyphInfo):
+ self.glyphs[ord(glyph.code_point)] = glyph
+
+ def write(self, fp):
+ self.header.length = len(self.glyphs.keys()) * 6
+ self.header.write(fp)
+
+ for n in sorted(self.glyphs.keys()):
+ self.glyphs[n].write(fp, True)
+
+
+########################################################################################################################
+
+
+class QFFFontDataDescriptorV1:
+ type_id = 0x04
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QFFFontDataDescriptorV1.type_id
+ self.data = []
+
+ def write(self, fp):
+ self.header.length = len(self.data)
+ self.header.write(fp)
+ fp.write(bytes(self.data))
+
+
+########################################################################################################################
+
+
+def _generate_font_glyphs_list(use_ascii, unicode_glyphs):
+ # The set of glyphs that we want to generate images for
+ glyphs = {}
+
+ # Add ascii charset if requested
+ if use_ascii is True:
+ for c in range(0x20, 0x7F): # does not include 0x7F!
+ glyphs[chr(c)] = True
+
+ # Append any extra unicode glyphs
+ unicode_glyphs = list(unicode_glyphs)
+ for c in unicode_glyphs:
+ glyphs[c] = True
+
+ return sorted(glyphs.keys())
+
+
+class QFFFont:
+ def __init__(self, logger):
+ self.logger = logger
+ self.image = None
+ self.glyph_data = {}
+ self.glyph_height = 0
+ return
+
+ def _extract_glyphs(self, format):
+ total_data_size = 0
+ total_rle_data_size = 0
+
+ converted_img = qmk.painter.convert_requested_format(self.image, format)
+ (self.palette, _) = qmk.painter.convert_image_bytes(converted_img, format)
+
+ # Work out how many bytes used for RLE vs. non-RLE
+ for _, glyph_entry in self.glyph_data.items():
+ glyph_img = converted_img.crop((glyph_entry.x, 1, glyph_entry.x + glyph_entry.w, 1 + self.glyph_height))
+ (_, this_glyph_image_bytes) = qmk.painter.convert_image_bytes(glyph_img, format)
+ this_glyph_rle_bytes = qmk.painter.compress_bytes_qmk_rle(this_glyph_image_bytes)
+ total_data_size += len(this_glyph_image_bytes)
+ total_rle_data_size += len(this_glyph_rle_bytes)
+ glyph_entry['image_uncompressed_bytes'] = this_glyph_image_bytes
+ glyph_entry['image_compressed_bytes'] = this_glyph_rle_bytes
+
+ return (total_data_size, total_rle_data_size)
+
+ def _parse_image(self, img, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
+ # Clear out any existing font metadata
+ self.image = None
+ # Each glyph is key=code_point, value={ x: ?, w: ? }
+ self.glyph_data = {}
+ self.glyph_height = 0
+
+ # Work out the list of glyphs required
+ glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
+
+ # Work out the geometry
+ (width, height) = img.size
+
+ # Work out the glyph offsets/widths
+ glyph_pixel_offsets = []
+ glyph_pixel_widths = []
+ pixels = img.load()
+
+ # Run through the markers and work out where each glyph starts/stops
+ glyph_split_color = pixels[0, 0] # top left pixel is the marker color we're going to use to split each glyph
+ glyph_pixel_offsets.append(0)
+ last_offset = 0
+ for x in range(1, width):
+ if pixels[x, 0] == glyph_split_color:
+ glyph_pixel_offsets.append(x)
+ glyph_pixel_widths.append(x - last_offset)
+ last_offset = x
+ glyph_pixel_widths.append(width - last_offset)
+
+ # Make sure the number of glyphs we're attempting to generate matches the input image
+ if len(glyph_pixel_offsets) != len(glyphs):
+ self.logger.error('The number of glyphs to generate doesn\'t match the number of detected glyphs in the input image.')
+ return
+
+ # Set up the required metadata for each glyph
+ for n in range(0, len(glyph_pixel_offsets)):
+ self.glyph_data[glyphs[n]] = QFFGlyphInfo(code_point=glyphs[n], x=glyph_pixel_offsets[n], w=glyph_pixel_widths[n])
+
+ # Parsing was successful, keep the image in this instance
+ self.image = img
+ self.glyph_height = height - 1 # subtract the line with the markers
+
+ def generate_image(self, ttf_file: Path, font_size: int, include_ascii_glyphs: bool = True, unicode_glyphs: str = '', include_before_left: bool = False, use_aa: bool = True):
+ # Load the font
+ font = ImageFont.truetype(str(ttf_file), int(font_size))
+ # Work out the max font size
+ max_font_size = font.font.ascent + abs(font.font.descent)
+ # Work out the list of glyphs required
+ glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
+
+ baseline_offset = 9999999
+ total_glyph_width = 0
+ max_glyph_height = -1
+
+ # Measure each glyph to determine the overall baseline offset required
+ for glyph in glyphs:
+ (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
+ glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
+ glyph_height = font.getbbox(glyph, anchor='la')[3]
+ if max_glyph_height < glyph_height:
+ max_glyph_height = glyph_height
+ total_glyph_width += glyph_width
+ if baseline_offset > ls_t:
+ baseline_offset = ls_t
+
+ # Create the output image
+ img = Image.new("RGB", (total_glyph_width + 1, max_font_size * 2 + 1), (0, 0, 0, 255))
+ cur_x_pos = 0
+
+ # Loop through each glyph...
+ for glyph in glyphs:
+ # Work out this glyph's bounding box
+ (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
+ glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
+ glyph_height = ls_b - ls_t
+ x_offset = -ls_l
+ y_offset = ls_t - baseline_offset
+
+ # Draw each glyph to its own image so we don't get anti-aliasing applied to the final image when straddling edges
+ glyph_img = Image.new("RGB", (glyph_width, max_font_size), (0, 0, 0, 255))
+ glyph_draw = ImageDraw.Draw(glyph_img)
+ if not use_aa:
+ glyph_draw.fontmode = "1"
+ glyph_draw.text((x_offset, y_offset), glyph, font=font, anchor='lt')
+
+ # Place the glyph-specific image in the correct location overall
+ img.paste(glyph_img, (cur_x_pos, 1))
+
+ # Set up the marker for start of each glyph
+ pixels = img.load()
+ pixels[cur_x_pos, 0] = (255, 0, 255)
+
+ # Increment for the next glyph's position
+ cur_x_pos += glyph_width
+
+ # Add the ending marker so that the difference/crop works
+ pixels = img.load()
+ pixels[cur_x_pos, 0] = (255, 0, 255)
+
+ # Determine the usable font area
+ dummy_img = Image.new("RGB", (total_glyph_width + 1, max_font_size + 1), (0, 0, 0, 255))
+ bbox = ImageChops.difference(img, dummy_img).getbbox()
+ bbox = (bbox[0], bbox[1], bbox[2] - 1, bbox[3]) # remove the unused end-marker
+
+ # Crop and re-parse the resulting image to ensure we're generating the correct format
+ self._parse_image(img.crop(bbox), include_ascii_glyphs, unicode_glyphs)
+
+ def save_to_image(self, img_file: Path):
+ # Drop out if there's no image loaded
+ if self.image is None:
+ self.logger.error('No image is loaded.')
+ return
+
+ # Save the image to the supplied file
+ self.image.save(str(img_file))
+
+ def read_from_image(self, img_file: Path, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
+ # Load and parse the supplied image file
+ self._parse_image(Image.open(str(img_file)), include_ascii_glyphs, unicode_glyphs)
+ return
+
+ def save_to_qff(self, format: Dict[str, Any], use_rle: bool, fp):
+ # Drop out if there's no image loaded
+ if self.image is None:
+ self.logger.error('No image is loaded.')
+ return
+
+ # Work out if we want to use RLE at all, skipping it if it's not any smaller (it's applied per-glyph)
+ (total_data_size, total_rle_data_size) = self._extract_glyphs(format)
+ if use_rle:
+ use_rle = (total_rle_data_size < total_data_size)
+
+ # For each glyph, work out which image data we want to use and append it to the image buffer, recording the byte-wise offset
+ img_buffer = bytes()
+ for _, glyph_entry in self.glyph_data.items():
+ glyph_entry['data_offset'] = len(img_buffer)
+ glyph_img_bytes = glyph_entry.image_compressed_bytes if use_rle else glyph_entry.image_uncompressed_bytes
+ img_buffer += bytes(glyph_img_bytes)
+
+ font_descriptor = QFFFontDescriptor()
+ ascii_table = QFFAsciiGlyphTableV1()
+ unicode_table = QFFUnicodeGlyphTableV1()
+ data_descriptor = QFFFontDataDescriptorV1()
+ data_descriptor.data = img_buffer
+
+ # Check if we have all the ASCII glyphs present
+ include_ascii_glyphs = all([chr(n) in self.glyph_data for n in range(0x20, 0x7F)])
+
+ # Helper for populating the blocks
+ for code_point, glyph_entry in self.glyph_data.items():
+ if ord(code_point) >= 0x20 and ord(code_point) <= 0x7E and include_ascii_glyphs:
+ ascii_table.add_glyph(glyph_entry)
+ else:
+ unicode_table.add_glyph(glyph_entry)
+
+ # Configure the font descriptor
+ font_descriptor.line_height = self.glyph_height
+ font_descriptor.has_ascii_table = include_ascii_glyphs
+ font_descriptor.unicode_glyph_count = len(unicode_table.glyphs.keys())
+ font_descriptor.is_transparent = False
+ font_descriptor.format = format['image_format_byte']
+ font_descriptor.compression = 0x01 if use_rle else 0x00
+
+ # Write a dummy font descriptor -- we'll have to come back and write it properly once we've rendered out everything else
+ font_descriptor_location = fp.tell()
+ font_descriptor.write(fp)
+
+ # Write out the ASCII table if required
+ if font_descriptor.has_ascii_table:
+ ascii_table.write(fp)
+
+ # Write out the unicode table if required
+ if font_descriptor.unicode_glyph_count > 0:
+ unicode_table.write(fp)
+
+ # Write out the palette if required
+ if format['has_palette']:
+ palette_descriptor = QGFFramePaletteDescriptorV1()
+
+ # Helper to convert from RGB888 to the QMK "dialect" of HSV888
+ def rgb888_to_qmk_hsv888(e):
+ hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
+ return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
+
+ # Convert all palette entries to HSV888 and write to the output
+ palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, self.palette))
+ palette_descriptor.write(fp)
+
+ # Write out the image data
+ data_descriptor.write(fp)
+
+ # Now fix up the overall font descriptor, then write it in the correct location
+ font_descriptor.total_file_size = fp.tell()
+ fp.seek(font_descriptor_location, 0)
+ font_descriptor.write(fp)
diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py
new file mode 100644
index 0000000000..71ce1f5a02
--- /dev/null
+++ b/lib/python/qmk/painter_qgf.py
@@ -0,0 +1,408 @@
+# Copyright 2021 Nick Brassel (@tzarc)
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Quantum Graphics File "QGF" Image File Format.
+# See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
+
+from colorsys import rgb_to_hsv
+from types import FunctionType
+from PIL import Image, ImageFile, ImageChops
+from PIL._binary import o8, o16le as o16, o32le as o32
+import qmk.painter
+
+
+def o24(i):
+ return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
+
+
+########################################################################################################################
+
+
+class QGFBlockHeader:
+ block_size = 5
+
+ def write(self, fp):
+ fp.write(b'' # start off with empty bytes...
+ + o8(self.type_id) # block type id
+ + o8((~self.type_id) & 0xFF) # negated block type id
+ + o24(self.length) # blob length
+ )
+
+
+########################################################################################################################
+
+
+class QGFGraphicsDescriptor:
+ type_id = 0x00
+ length = 18
+ magic = 0x464751
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QGFGraphicsDescriptor.type_id
+ self.header.length = QGFGraphicsDescriptor.length
+ self.version = 1
+ self.total_file_size = 0
+ self.image_width = 0
+ self.image_height = 0
+ self.frame_count = 0
+
+ def write(self, fp):
+ self.header.write(fp)
+ fp.write(
+ b'' # start off with empty bytes...
+ + o24(QGFGraphicsDescriptor.magic) # magic
+ + o8(self.version) # version
+ + o32(self.total_file_size) # file size
+ + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
+ + o16(self.image_width) # width
+ + o16(self.image_height) # height
+ + o16(self.frame_count) # frame count
+ )
+
+
+########################################################################################################################
+
+
+class QGFFrameOffsetDescriptorV1:
+ type_id = 0x01
+
+ def __init__(self, frame_count):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QGFFrameOffsetDescriptorV1.type_id
+ self.frame_offsets = [0xFFFFFFFF] * frame_count
+ self.frame_count = frame_count
+
+ def write(self, fp):
+ self.header.length = len(self.frame_offsets) * 4
+ self.header.write(fp)
+ for offset in self.frame_offsets:
+ fp.write(b'' # start off with empty bytes...
+ + o32(offset) # offset
+ )
+
+
+########################################################################################################################
+
+
+class QGFFrameDescriptorV1:
+ type_id = 0x02
+ length = 6
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QGFFrameDescriptorV1.type_id
+ self.header.length = QGFFrameDescriptorV1.length
+ self.format = 0xFF
+ self.flags = 0
+ self.compression = 0xFF
+ self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
+ self.delay = 1000 # Placeholder until it gets read from the animation
+
+ def write(self, fp):
+ self.header.write(fp)
+ fp.write(b'' # start off with empty bytes...
+ + o8(self.format) # format
+ + o8(self.flags) # flags
+ + o8(self.compression) # compression
+ + o8(self.transparency_index) # transparency index
+ + o16(self.delay) # delay
+ )
+
+ @property
+ def is_transparent(self):
+ return (self.flags & 0x01) == 0x01
+
+ @is_transparent.setter
+ def is_transparent(self, val):
+ if val:
+ self.flags |= 0x01
+ else:
+ self.flags &= ~0x01
+
+ @property
+ def is_delta(self):
+ return (self.flags & 0x02) == 0x02
+
+ @is_delta.setter
+ def is_delta(self, val):
+ if val:
+ self.flags |= 0x02
+ else:
+ self.flags &= ~0x02
+
+
+########################################################################################################################
+
+
+class QGFFramePaletteDescriptorV1:
+ type_id = 0x03
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QGFFramePaletteDescriptorV1.type_id
+ self.header.length = 0
+ self.palette_entries = [(0xFF, 0xFF, 0xFF)] * 4
+
+ def write(self, fp):
+ self.header.length = len(self.palette_entries) * 3
+ self.header.write(fp)
+ for entry in self.palette_entries:
+ fp.write(b'' # start off with empty bytes...
+ + o8(entry[0]) # h
+ + o8(entry[1]) # s
+ + o8(entry[2]) # v
+ )
+
+
+########################################################################################################################
+
+
+class QGFFrameDeltaDescriptorV1:
+ type_id = 0x04
+ length = 8
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QGFFrameDeltaDescriptorV1.type_id
+ self.header.length = QGFFrameDeltaDescriptorV1.length
+ self.left = 0
+ self.top = 0
+ self.right = 0
+ self.bottom = 0
+
+ def write(self, fp):
+ self.header.write(fp)
+ fp.write(b'' # start off with empty bytes...
+ + o16(self.left) # left
+ + o16(self.top) # top
+ + o16(self.right) # right
+ + o16(self.bottom) # bottom
+ )
+
+
+########################################################################################################################
+
+
+class QGFFrameDataDescriptorV1:
+ type_id = 0x05
+
+ def __init__(self):
+ self.header = QGFBlockHeader()
+ self.header.type_id = QGFFrameDataDescriptorV1.type_id
+ self.data = []
+
+ def write(self, fp):
+ self.header.length = len(self.data)
+ self.header.write(fp)
+ fp.write(bytes(self.data))
+
+
+########################################################################################################################
+
+
+class QGFImageFile(ImageFile.ImageFile):
+
+ format = "QGF"
+ format_description = "Quantum Graphics File Format"
+
+ def _open(self):
+ raise NotImplementedError("Reading QGF files is not supported")
+
+
+########################################################################################################################
+
+
+def _accept(prefix):
+ """Helper method used by PIL to work out if it can parse an input file.
+
+ Currently unimplemented.
+ """
+ return False
+
+
+def _save(im, fp, filename):
+ """Helper method used by PIL to write to an output file.
+ """
+ # Work out from the parameters if we need to do anything special
+ encoderinfo = im.encoderinfo.copy()
+ append_images = list(encoderinfo.get("append_images", []))
+ verbose = encoderinfo.get("verbose", False)
+ use_deltas = encoderinfo.get("use_deltas", True)
+ use_rle = encoderinfo.get("use_rle", True)
+
+ # Helper for inline verbose prints
+ def vprint(s):
+ if verbose:
+ print(s)
+
+ # Helper to iterate through all frames in the input image
+ def _for_all_frames(x: FunctionType):
+ frame_num = 0
+ last_frame = None
+ for frame in [im] + append_images:
+ # Get number of of frames in this image
+ nfr = getattr(frame, "n_frames", 1)
+ for idx in range(nfr):
+ frame.seek(idx)
+ frame.load()
+ copy = frame.copy().convert("RGB")
+ x(frame_num, copy, last_frame)
+ last_frame = copy
+ frame_num += 1
+
+ # Collect all the frame sizes
+ frame_sizes = []
+ _for_all_frames(lambda idx, frame, last_frame: frame_sizes.append(frame.size))
+
+ # Make sure all frames are the same size
+ if len(list(set(frame_sizes))) != 1:
+ raise ValueError("Mismatching sizes on frames")
+
+ # Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the
+ # correct values once we've written all the frames to the output
+ graphics_descriptor_location = fp.tell()
+ graphics_descriptor = QGFGraphicsDescriptor()
+ graphics_descriptor.frame_count = len(frame_sizes)
+ graphics_descriptor.image_width = frame_sizes[0][0]
+ graphics_descriptor.image_height = frame_sizes[0][1]
+ vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ graphics_descriptor.write(fp)
+
+ # Work out the frame offset descriptor location (and write a dummy value), so that we can come back and fill in the
+ # correct offsets once we've written all the frames to the output
+ frame_offset_location = fp.tell()
+ frame_offsets = QGFFrameOffsetDescriptorV1(graphics_descriptor.frame_count)
+ vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ frame_offsets.write(fp)
+
+ # Helper function to save each frame to the output file
+ def _write_frame(idx, frame, last_frame):
+ # If we replace the frame we're going to output with a delta, we can override it here
+ this_frame = frame
+ location = (0, 0)
+ size = frame.size
+
+ # Work out the format we're going to use
+ format = encoderinfo["qmk_format"]
+
+ # Convert the original frame so we can do comparisons
+ converted = qmk.painter.convert_requested_format(this_frame, format)
+ graphic_data = qmk.painter.convert_image_bytes(converted, format)
+
+ # Convert the raw data to RLE-encoded if requested
+ raw_data = graphic_data[1]
+ if use_rle:
+ rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1])
+ use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data)
+ image_data = raw_data if use_raw_this_frame else rle_data
+
+ # Work out if a delta frame is smaller than injecting it directly
+ use_delta_this_frame = False
+ if use_deltas and last_frame is not None:
+ # If we want to use deltas, then find the difference
+ diff = ImageChops.difference(frame, last_frame)
+
+ # Get the bounding box of those differences
+ bbox = diff.getbbox()
+
+ # If we have a valid bounding box...
+ if bbox:
+ # ...create the delta frame by cropping the original.
+ delta_frame = frame.crop(bbox)
+ delta_location = (bbox[0], bbox[1])
+ delta_size = (bbox[2] - bbox[0], bbox[3] - bbox[1])
+
+ # Convert the delta frame to the requested format
+ delta_converted = qmk.painter.convert_requested_format(delta_frame, format)
+ delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format)
+
+ # Work out how large the delta frame is going to be with compression etc.
+ delta_raw_data = delta_graphic_data[1]
+ if use_rle:
+ delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1])
+ delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data)
+ delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data
+
+ # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead
+ # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash
+ # sizing constraints.
+ if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data):
+ # Copy across all the delta equivalents so that the rest of the processing acts on those
+ this_frame = delta_frame
+ location = delta_location
+ size = delta_size
+ converted = delta_converted
+ graphic_data = delta_graphic_data
+ raw_data = delta_raw_data
+ rle_data = delta_rle_data
+ use_raw_this_frame = delta_use_raw_this_frame
+ image_data = delta_image_data
+ use_delta_this_frame = True
+
+ # Write out the frame descriptor
+ frame_offsets.frame_offsets[idx] = fp.tell()
+ vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ frame_descriptor = QGFFrameDescriptorV1()
+ frame_descriptor.is_delta = use_delta_this_frame
+ frame_descriptor.is_transparent = False
+ frame_descriptor.format = format['image_format_byte']
+ frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t
+ frame_descriptor.delay = frame.info['duration'] if 'duration' in frame.info else 1000 # If we're not an animation, just pretend we're delaying for 1000ms
+ frame_descriptor.write(fp)
+
+ # Write out the palette if required
+ if format['has_palette']:
+ palette = graphic_data[0]
+ palette_descriptor = QGFFramePaletteDescriptorV1()
+
+ # Helper to convert from RGB888 to the QMK "dialect" of HSV888
+ def rgb888_to_qmk_hsv888(e):
+ hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
+ return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
+
+ # Convert all palette entries to HSV888 and write to the output
+ palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette))
+ vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ palette_descriptor.write(fp)
+
+ # Write out the delta info if required
+ if use_delta_this_frame:
+ # Set up the rendering location of where the delta frame should be situated
+ delta_descriptor = QGFFrameDeltaDescriptorV1()
+ delta_descriptor.left = location[0]
+ delta_descriptor.top = location[1]
+ delta_descriptor.right = location[0] + size[0]
+ delta_descriptor.bottom = location[1] + size[1]
+
+ # Write the delta frame to the output
+ vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ delta_descriptor.write(fp)
+
+ # Write out the data for this frame to the output
+ data_descriptor = QGFFrameDataDescriptorV1()
+ data_descriptor.data = image_data
+ vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ data_descriptor.write(fp)
+
+ # Iterate over each if the input frames, writing it to the output in the process
+ _for_all_frames(_write_frame)
+
+ # Go back and update the graphics descriptor now that we can determine the final file size
+ graphics_descriptor.total_file_size = fp.tell()
+ fp.seek(graphics_descriptor_location, 0)
+ graphics_descriptor.write(fp)
+
+ # Go back and update the frame offsets now that they're written to the file
+ fp.seek(frame_offset_location, 0)
+ frame_offsets.write(fp)
+
+
+########################################################################################################################
+
+# Register with PIL so that it knows about the QGF format
+Image.register_open(QGFImageFile.format, QGFImageFile, _accept)
+Image.register_save(QGFImageFile.format, _save)
+Image.register_save_all(QGFImageFile.format, _save)
+Image.register_extension(QGFImageFile.format, f".{QGFImageFile.format.lower()}")
+Image.register_mime(QGFImageFile.format, f"image/{QGFImageFile.format.lower()}")
diff --git a/quantum/main.c b/quantum/main.c
index faba668056..2d5911b708 100644
--- a/quantum/main.c
+++ b/quantum/main.c
@@ -43,10 +43,6 @@ void protocol_task(void) {
protocol_post_task();
}
-#ifdef DEFERRED_EXEC_ENABLE
-void deferred_exec_task(void);
-#endif // DEFERRED_EXEC_ENABLE
-
/** \brief Main
*
* FIXME: Needs doc
@@ -63,8 +59,15 @@ int main(void) {
while (true) {
protocol_task();
+#ifdef QUANTUM_PAINTER_ENABLE
+ // Run Quantum Painter animations
+ void qp_internal_animation_tick(void);
+ qp_internal_animation_tick();
+#endif
+
#ifdef DEFERRED_EXEC_ENABLE
// Run deferred executions
+ void deferred_exec_task(void);
deferred_exec_task();
#endif // DEFERRED_EXEC_ENABLE
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;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_clear
+
+bool qp_clear(painter_device_t device) {
+ qp_dprintf("qp_clear: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_clear: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_clear: fail (could not start comms)\n");
+ return false;
+ }
+
+ bool ret = driver->driver_vtable->clear(device);
+ qp_comms_stop(device);
+ qp_dprintf("qp_clear: %s\n", ret ? "ok" : "fail");
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_flush
+
+bool qp_flush(painter_device_t device) {
+ qp_dprintf("qp_flush: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_flush: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_flush: fail (could not start comms)\n");
+ return false;
+ }
+
+ bool ret = driver->driver_vtable->flush(device);
+ qp_comms_stop(device);
+ qp_dprintf("qp_flush: %s\n", ret ? "ok" : "fail");
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_get_geometry
+
+void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y) {
+ qp_dprintf("qp_geometry: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+
+ switch (driver->rotation) {
+ default:
+ case QP_ROTATION_0:
+ case QP_ROTATION_180:
+ if (width) {
+ *width = driver->panel_width;
+ }
+ if (height) {
+ *height = driver->panel_height;
+ }
+ break;
+ case QP_ROTATION_90:
+ case QP_ROTATION_270:
+ if (width) {
+ *width = driver->panel_height;
+ }
+ if (height) {
+ *height = driver->panel_width;
+ }
+ break;
+ }
+
+ if (rotation) {
+ *rotation = driver->rotation;
+ }
+
+ if (offset_x) {
+ *offset_x = driver->offset_x;
+ }
+
+ if (offset_y) {
+ *offset_y = driver->offset_y;
+ }
+
+ qp_dprintf("qp_geometry: ok\n");
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_set_viewport_offsets
+
+void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y) {
+ qp_dprintf("qp_set_viewport_offsets: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+
+ driver->offset_x = offset_x;
+ driver->offset_y = offset_y;
+
+ qp_dprintf("qp_set_viewport_offsets: ok\n");
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_viewport
+
+bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
+ qp_dprintf("qp_viewport: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_viewport: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_viewport: fail (could not start comms)\n");
+ return false;
+ }
+
+ // Set the viewport
+ bool ret = driver->driver_vtable->viewport(device, left, top, right, bottom);
+ qp_dprintf("qp_viewport: %s\n", ret ? "ok" : "fail");
+ qp_comms_stop(device);
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_pixdata
+
+bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
+ qp_dprintf("qp_pixdata: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_pixdata: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_pixdata: fail (could not start comms)\n");
+ return false;
+ }
+
+ bool ret = driver->driver_vtable->pixdata(device, pixel_data, native_pixel_count);
+ qp_dprintf("qp_pixdata: %s\n", ret ? "ok" : "fail");
+ qp_comms_stop(device);
+ return ret;
+}
diff --git a/quantum/painter/qp.h b/quantum/painter/qp.h
new file mode 100644
index 0000000000..e1c14d156c
--- /dev/null
+++ b/quantum/painter/qp.h
@@ -0,0 +1,453 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "deferred_exec.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter global configurables (add to your keyboard's config.h)
+
+#ifndef QUANTUM_PAINTER_NUM_IMAGES
+/**
+ * @def This controls the maximum number of images that Quantum Painter can load at any one time. Images can be loaded
+ * using \ref qp_load_image_mem, and can be unloaded by calling \ref qp_close_image. Increasing this number in
+ * order to load more images increases the amount of RAM required. Image data is not held in RAM, just metadata.
+ */
+# define QUANTUM_PAINTER_NUM_IMAGES 8
+#endif // QUANTUM_PAINTER_NUM_IMAGES
+
+#ifndef QUANTUM_PAINTER_NUM_FONTS
+/**
+ * @def This controls the maximum number of fonts that Quantum Painter can load. Fonts can be loaded using
+ * \ref qp_load_font_mem, and can be unloaded by calling \ref qp_close_font. Increasing this number in order to
+ * load more fonts increases the amount of RAM required. Font data is not held in RAM, unless
+ * \ref QUANTUM_PAINTER_LOAD_FONTS_TO_RAM is set to TRUE.
+ */
+# define QUANTUM_PAINTER_NUM_FONTS 4
+#endif // QUANTUM_PAINTER_NUM_FONTS
+
+#ifndef QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
+/**
+ * @def This controls whether or not fonts should be cached in RAM. Under normal circumstances, fonts can have quite
+ * random access patterns, and due to timing of flash memory or external storage, it may be a significant speedup
+ * moving the font into RAM before use. Defaults to "off", but if it's enabled it will fallback to reading from the
+ * original location if corresponding RAM could not be allocated (such as being too large).
+ */
+# define QUANTUM_PAINTER_LOAD_FONTS_TO_RAM FALSE
+#endif
+
+#ifndef QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
+/**
+ * @def This controls the maximum number of animations that Quantum Painter can play simultaneously. Increasing this
+ * number in order to play more animations at the same time increases the amount of RAM required.
+ */
+# define QUANTUM_PAINTER_CONCURRENT_ANIMATIONS 4
+#endif // QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
+
+#ifndef QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE
+/**
+ * @def This controls the maximum size of the pixel data buffer used for single blocks of transmission. Larger buffers
+ * means more data is processed at one time, with less frequent transmissions, at the cost of RAM.
+ */
+# define QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE 32
+#endif
+
+#ifndef QUANTUM_PAINTER_SUPPORTS_256_PALETTE
+/**
+ * @def This controls whether 256-color palettes are supported. This has relatively hefty requirements on RAM -- at
+ * least 1kB extra is required just to store the palette information, with more required for other metadata.
+ */
+# define QUANTUM_PAINTER_SUPPORTS_256_PALETTE FALSE
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter types
+
+/**
+ * @typedef A handle to a Quantum Painter device, such as an LCD or OLED. Most Quantum Painter APIs require this
+ * argument in order to perform operations on the display.
+ */
+typedef const void *painter_device_t;
+
+/**
+ * @typedef The desired rotation of a panel. Used as a parameter to \ref qp_init, and can be queried by
+ * \ref qp_get_geometry.
+ */
+typedef enum { QP_ROTATION_0, QP_ROTATION_90, QP_ROTATION_180, QP_ROTATION_270 } painter_rotation_t;
+
+/**
+ * @typedef A descriptor for a Quantum Painter image.
+ */
+typedef struct painter_image_desc_t {
+ uint16_t width; ///< Image width
+ uint16_t height; ///< Image height
+ uint16_t frame_count; ///< Number of frames in this image
+} painter_image_desc_t;
+
+/**
+ * @typedef A handle to a Quantum Painter image.
+ */
+typedef const painter_image_desc_t *painter_image_handle_t;
+
+/**
+ * @typedef A descriptor for a Quantum Painter font.
+ */
+typedef struct painter_font_desc_t {
+ uint8_t line_height; ///< The number of pixels in height for each line
+} painter_font_desc_t;
+
+/**
+ * @typedef A handle to a Quantum Painter font.
+ */
+typedef const painter_font_desc_t *painter_font_handle_t;
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API
+
+/**
+ * Initialize a device and set its rotation.
+ *
+ * @param device[in] the handle of the device to initialize
+ * @param rotation[in] the rotation to use
+ * @return true if initialization succeeded
+ * @return false if initialization failed
+ */
+bool qp_init(painter_device_t device, painter_rotation_t rotation);
+
+/**
+ * Controls whether a display is on or off.
+ *
+ * @note If backlighting is used to control brightness (such as for an LCD), it will need to be handled external to
+ * Quantum Painter.
+ *
+ * @param device[in] the handle of the device to control
+ * @param power_on[in] whether or not the device should be on
+ * @return true if controlling the power state succeeded
+ * @return false if controlling the power state failed
+ */
+bool qp_power(painter_device_t device, bool power_on);
+
+/**
+ * Clears a device's screen.
+ *
+ * @param device[in] the handle of the device to control
+ * @return true if clearing the screen succeeded
+ * @return false if clearing the screen failed
+ */
+bool qp_clear(painter_device_t device);
+
+/**
+ * Transmits any outstanding data to the screen in order to persist all changes to the display.
+ *
+ * @note Drivers without internal framebuffers will likely ignore this API.
+ *
+ * @param device[in] the handle of the device to control
+ * @return true if flushing changes to the screen succeeded
+ * @return false if flushing changes to the screen failed
+ */
+bool qp_flush(painter_device_t device);
+
+/**
+ * Retrieves the size, rotation, and offsets for the display.
+ *
+ * @note Any arguments of NULL will be ignored.
+ *
+ * @param device[in] the handle of the device to control
+ * @param width[out] the device's width
+ * @param height[out] the device's height
+ * @param rotation[out] the device's rotation
+ * @param offset_x[out] the device's x-offset applied while drawing
+ * @param offset_y[out] the device's y-offset applied while drawing
+ */
+void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
+
+/**
+ * Allows repositioning of the viewport if the panel geometry offsets are non-zero.
+ *
+ * @param device[in] the handle of the device to control
+ * @param offset_x[in] the device's x-offset applied while drawing
+ * @param offset_y[in] the device's y-offset applied while drawing
+ */
+void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
+
+/**
+ * Sets a pixel to the specified color.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position to draw onto the device
+ * @param y[in] the y-position to draw onto the device
+ * @param hue[in] the hue to use, with 0-360 mapped to 0-255
+ * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
+ * @param val[in] the value to use, with 0-100% mapped to 0-255
+ * @return true if setting the pixel succeeded
+ * @return false if setting the pixel failed
+ */
+bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
+
+/**
+ * Draws a line using the specified color.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x0[in] the device's x-position to start
+ * @param y0[in] the device's y-position to start
+ * @param x1[in] the device's x-position to finish
+ * @param y1[in] the device's y-position to finish
+ * @param hue[in] the hue to use, with 0-360 mapped to 0-255
+ * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
+ * @param val[in] the value to use, with 0-100% mapped to 0-255
+ * @return true if drawing the line succeeded
+ * @return false if drawing the line failed
+ */
+bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
+
+/**
+ * Draws a rectangle using the specified color, optionally filled.
+ *
+ * @param device[in] the handle of the device to control
+ * @param left[in] the device's x-position to start
+ * @param top[in] the device's y-position to start
+ * @param right[in] the device's x-position to finish
+ * @param bottom[in] the device's y-position to finish
+ * @param hue[in] the hue to use, with 0-360 mapped to 0-255
+ * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
+ * @param val[in] the value to use, with 0-100% mapped to 0-255
+ * @param filled[in] whether the rectangle should be filled
+ * @return true if drawing the rectangle succeeded
+ * @return false if drawing the rectangle failed
+ */
+bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
+
+/**
+ * Draws a circle using the specified color, optionally filled.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position of the centre of the circle to draw onto the device
+ * @param y[in] the y-position of the centre of the circle to draw onto the device
+ * @param radius[in] the radius of the circle to draw
+ * @param hue[in] the hue to use, with 0-360 mapped to 0-255
+ * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
+ * @param val[in] the value to use, with 0-100% mapped to 0-255
+ * @param filled[in] whether the circle should be filled
+ * @return true if drawing the circle succeeded
+ * @return false if drawing the circle failed
+ */
+bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
+
+/**
+ * Draws a ellipse using the specified color, optionally filled.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position of the centre of the ellipse to draw onto the device
+ * @param y[in] the y-position of the centre of the ellipse to draw onto the device
+ * @param sizex[in] the horizontal size of the ellipse
+ * @param sizey[in] the vertical size of the ellipse
+ * @param hue[in] the hue to use, with 0-360 mapped to 0-255
+ * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
+ * @param val[in] the value to use, with 0-100% mapped to 0-255
+ * @param filled[in] whether the ellipse should be filled
+ * @return true if drawing the ellipse succeeded
+ * @return false if drawing the ellipse failed
+ */
+bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
+
+/**
+ * Sets up the location on the display to stream raw pixel data to the display, using \ref qp_pixdata.
+ *
+ * @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality.
+ *
+ * @param device[in] the handle of the device to control
+ * @param left[in] the device's x-position to start
+ * @param top[in] the device's y-position to start
+ * @param right[in] the device's x-position to finish
+ * @param bottom[in] the device's y-position to finish
+ * @return true if setting the viewport succeeded
+ * @return false if setting the viewport failed
+ */
+bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
+
+/**
+ * Streams raw pixel data (in the native panel format) to the area previously set by \ref qp_viewport.
+ *
+ * @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality.
+ *
+ * @param device[in] the handle of the device to control
+ * @param pixel_data[in] pointer to buffer data
+ * @param native_pixel_count[in] the number of pixels to transmit
+ * @return true if streaming of data succeeded
+ * @return false if streaming of data failed
+ */
+bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
+
+/**
+ * Loads an image into memory.
+ *
+ * @note Images can be unloaded by calling \ref qp_close_image.
+ *
+ * @param buffer[in] the image data to load
+ * @return an image handle usable with \ref qp_drawimage, \ref qp_drawimage_recolor, \ref qp_animate, and
+ * \ref qp_animate_recolor.
+ * @return NULL if loading the image failed
+ */
+painter_image_handle_t qp_load_image_mem(const void *buffer);
+
+/**
+ * Closes an image handle when no longer in use.
+ *
+ * @param image[in] the handle of the image to unload
+ * @return true if unloading the image succeeded
+ * @return false if unloading the image failed
+ */
+bool qp_close_image(painter_image_handle_t image);
+
+/**
+ * Draws an image to the display.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position where the image should be drawn onto the device
+ * @param y[in] the y-position where the image should be drawn onto the device
+ * @param image[in] the handle of the image to draw
+ * @return true if drawing the image succeeded
+ * @return false if drawing the image failed
+ */
+bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
+
+/**
+ * Draws an image to the display, recoloring monochrome images to the desired foreground/background.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position where the image should be drawn onto the device
+ * @param y[in] the y-position where the image should be drawn onto the device
+ * @param image[in] the handle of the image to draw
+ * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
+ * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
+ * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
+ * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
+ * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
+ * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
+ * @return true if drawing the image succeeded
+ * @return false if drawing the image failed
+ */
+bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
+
+/**
+ * Draws an animation to the display.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position where the image should be drawn onto the device
+ * @param y[in] the y-position where the image should be drawn onto the device
+ * @param image[in] the handle of the image to draw
+ * @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating
+ * @return INVALID_DEFERRED_TOKEN if animating the image failed
+ */
+deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
+
+/**
+ * Draws an animation to the display, recoloring monochrome images to the desired foreground/background.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position where the image should be drawn onto the device
+ * @param y[in] the y-position where the image should be drawn onto the device
+ * @param image[in] the handle of the image to draw
+ * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
+ * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
+ * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
+ * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
+ * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
+ * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
+ * @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating
+ * @return INVALID_DEFERRED_TOKEN if animating the image failed
+ */
+deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
+
+/**
+ * Cancels a running animation.
+ *
+ * @param anim_token[in] the animation token returned by \ref qp_animate, or \ref qp_animate_recolor.
+ */
+void qp_stop_animation(deferred_token anim_token);
+
+/**
+ * Loads a font into memory.
+ *
+ * @note Fonts can be unloaded by calling \ref qp_close_font.
+ *
+ * @param buffer[in] the font data to load
+ * @return an image handle usable with \ref qp_textwidth, \ref qp_drawtext, and \ref qp_drawtext_recolor.
+ * @return NULL if loading the font failed
+ */
+painter_font_handle_t qp_load_font_mem(const void *buffer);
+
+/**
+ * Closes a font handle when no longer in use.
+ *
+ * @param font[in] the handle of the font to unload
+ * @return true if unloading the font succeeded
+ * @return false if unloading the font failed
+ */
+bool qp_close_font(painter_font_handle_t font);
+
+/**
+ * Measures the width (in pixels) of the supplied string, given the specified font.
+ *
+ * @param font[in] the handle of the font
+ * @param str[in] the string to measure
+ * @return the width (in pixels) needed to draw the specified string
+ */
+int16_t qp_textwidth(painter_font_handle_t font, const char *str);
+
+/**
+ * Draws text to the display.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position where the text should be drawn onto the device
+ * @param y[in] the y-position where the text should be drawn onto the device
+ * @param font[in] the handle of the font
+ * @param str[in] the string to draw
+ * @return the width (in pixels) used when drawing the specified string
+ */
+int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
+
+/**
+ * Draws text to the display, recoloring monochrome fonts to the desired foreground/background.
+ *
+ * @param device[in] the handle of the device to control
+ * @param x[in] the x-position where the text should be drawn onto the device
+ * @param y[in] the y-position where the text should be drawn onto the device
+ * @param font[in] the handle of the font
+ * @param str[in] the string to draw
+ * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
+ * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
+ * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
+ * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
+ * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
+ * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
+ * @return the width (in pixels) used when drawing the specified string
+ */
+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);
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter Drivers
+
+#ifdef QUANTUM_PAINTER_ILI9163_ENABLE
+# include "qp_ili9163.h"
+#endif // QUANTUM_PAINTER_ILI9163_ENABLE
+
+#ifdef QUANTUM_PAINTER_ILI9341_ENABLE
+# include "qp_ili9341.h"
+#endif // QUANTUM_PAINTER_ILI9341_ENABLE
+
+#ifdef QUANTUM_PAINTER_ST7789_ENABLE
+# include "qp_st7789.h"
+#endif // QUANTUM_PAINTER_ST7789_ENABLE
+
+#ifdef QUANTUM_PAINTER_GC9A01_ENABLE
+# include "qp_gc9a01.h"
+#endif // QUANTUM_PAINTER_GC9A01_ENABLE
+
+#ifdef QUANTUM_PAINTER_SSD1351_ENABLE
+# include "qp_ssd1351.h"
+#endif // QUANTUM_PAINTER_SSD1351_ENABLE
diff --git a/quantum/painter/qp_comms.c b/quantum/painter/qp_comms.c
new file mode 100644
index 0000000000..dc17b49460
--- /dev/null
+++ b/quantum/painter/qp_comms.c
@@ -0,0 +1,72 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_comms.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Base comms APIs
+
+bool qp_comms_init(painter_device_t device) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_comms_init: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ return driver->comms_vtable->comms_init(device);
+}
+
+bool qp_comms_start(painter_device_t device) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_comms_start: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ return driver->comms_vtable->comms_start(device);
+}
+
+void qp_comms_stop(painter_device_t device) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_comms_stop: fail (validation_ok == false)\n");
+ return;
+ }
+
+ driver->comms_vtable->comms_stop(device);
+}
+
+uint32_t qp_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_comms_send: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ return driver->comms_vtable->comms_send(device, data, byte_count);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Comms APIs that use a D/C pin
+
+void qp_comms_command(painter_device_t device, uint8_t cmd) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
+ comms_vtable->send_command(device, cmd);
+}
+
+void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data) {
+ qp_comms_command(device, cmd);
+ qp_comms_send(device, &data, sizeof(data));
+}
+
+uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void *data, uint32_t byte_count) {
+ qp_comms_command(device, cmd);
+ return qp_comms_send(device, data, byte_count);
+}
+
+void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
+ struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
+ comms_vtable->bulk_command_sequence(device, sequence, sequence_len);
+}
diff --git a/quantum/painter/qp_comms.h b/quantum/painter/qp_comms.h
new file mode 100644
index 0000000000..8fbf25c201
--- /dev/null
+++ b/quantum/painter/qp_comms.h
@@ -0,0 +1,25 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdlib.h>
+
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Base comms APIs
+
+bool qp_comms_init(painter_device_t device);
+bool qp_comms_start(painter_device_t device);
+void qp_comms_stop(painter_device_t device);
+uint32_t qp_comms_send(painter_device_t device, const void* data, uint32_t byte_count);
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Comms APIs that use a D/C pin
+
+void qp_comms_command(painter_device_t device, uint8_t cmd);
+void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data);
+uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void* data, uint32_t byte_count);
+void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
diff --git a/quantum/painter/qp_draw.h b/quantum/painter/qp_draw.h
new file mode 100644
index 0000000000..7094d80eaa
--- /dev/null
+++ b/quantum/painter/qp_draw.h
@@ -0,0 +1,85 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "qp_internal.h"
+#include "qp_stream.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter utility functions
+
+// Global variable used for native pixel data streaming.
+extern uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
+
+// Check if the supplied bpp is capable of being rendered
+bool qp_internal_bpp_capable(uint8_t bits_per_pixel);
+
+// Returns the number of pixels that can fit in the pixdata buffer
+uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device);
+
+// Fills the supplied buffer with equivalent native pixels matching the supplied HSV
+void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val);
+
+// qp_setpixel internal implementation, but uses the global pixdata buffer with pre-converted native pixel. Only the first pixel is used.
+bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y);
+
+// qp_rect internal implementation, but uses the global pixdata buffer with pre-converted native pixels.
+bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t l, uint16_t t, uint16_t r, uint16_t b);
+
+// Convert from input pixel data + palette to equivalent pixels
+typedef int16_t (*qp_internal_byte_input_callback)(void* cb_arg);
+typedef bool (*qp_internal_pixel_output_callback)(qp_pixel_t* palette, uint8_t index, void* cb_arg);
+bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg);
+bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg);
+bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg);
+
+// Global variable used for interpolated pixel lookup table.
+#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
+extern qp_pixel_t qp_internal_global_pixel_lookup_table[256];
+#else
+extern qp_pixel_t qp_internal_global_pixel_lookup_table[16];
+#endif
+
+// Generates a color-interpolated lookup table based off the number of items, from foreground to background, for use with monochrome image rendering.
+// Returns true if a palette was created, false if the palette is reused.
+// As this uses a global, this may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() below to reset.
+bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps);
+
+// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format.
+void qp_internal_invalidate_palette(void);
+
+// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
+bool qp_internal_load_qgf_palette(qp_stream_t* stream, uint8_t bpp);
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter codec functions
+
+enum qp_internal_rle_mode_t {
+ MARKER_BYTE,
+ REPEATING_RUN,
+ NON_REPEATING_RUN,
+};
+
+struct qp_internal_byte_input_state {
+ painter_device_t device;
+ qp_stream_t* src_stream;
+ int16_t curr;
+ union {
+ // RLE-specific
+ struct {
+ enum qp_internal_rle_mode_t mode;
+ uint8_t remain; // number of bytes remaining in the current mode
+ } rle;
+ };
+};
+
+struct qp_internal_pixel_output_state {
+ painter_device_t device;
+ uint32_t pixel_write_pos;
+ uint32_t max_pixels;
+};
+
+bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg);
+
+qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression);
diff --git a/quantum/painter/qp_draw_circle.c b/quantum/painter/qp_draw_circle.c
new file mode 100644
index 0000000000..edaae35835
--- /dev/null
+++ b/quantum/painter/qp_draw_circle.c
@@ -0,0 +1,172 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp.h"
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_draw.h"
+
+// Utilize 8-way symmetry to draw circles
+static bool qp_circle_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
+ /*
+ Circles have the property of 8-way symmetry, so eight pixels can be drawn
+ for each computed [offsetx,offsety] given the center coordinates
+ represented by [centerx,centery].
+
+ For filled circles, we can draw horizontal lines between each pair of
+ pixels with the same final value of y.
+
+ Two special cases exist and have been optimized:
+ 1) offsetx == offsety (the final point), makes half the coordinates
+ equivalent, so we can omit them (and the corresponding fill lines)
+ 2) offsetx == 0 (the starting point) means that some horizontal lines
+ would be a single pixel in length, so we write individual pixels instead.
+ This also makes half the symmetrical points identical to their twins,
+ so we only need four points or two points and one line
+ */
+
+ int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
+ int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
+ int16_t xpy = ((int16_t)centerx) + ((int16_t)offsety);
+ int16_t xmy = ((int16_t)centerx) - ((int16_t)offsety);
+ int16_t ypx = ((int16_t)centery) + ((int16_t)offsetx);
+ int16_t ymx = ((int16_t)centery) - ((int16_t)offsetx);
+ int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
+ int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
+
+ if (offsetx == 0) {
+ if (!qp_internal_setpixel_impl(device, centerx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, centerx, ymy)) {
+ return false;
+ }
+ if (filled) {
+ if (!qp_internal_fillrect_helper_impl(device, xpy, centery, xmy, centery)) {
+ return false;
+ }
+ } else {
+ if (!qp_internal_setpixel_impl(device, xpy, centery)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmy, centery)) {
+ return false;
+ }
+ }
+ } else if (offsetx == offsety) {
+ if (filled) {
+ if (!qp_internal_fillrect_helper_impl(device, xpy, ypy, xmy, ypy)) {
+ return false;
+ }
+ if (!qp_internal_fillrect_helper_impl(device, xpy, ymy, xmy, ymy)) {
+ return false;
+ }
+ } else {
+ if (!qp_internal_setpixel_impl(device, xpy, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmy, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xpy, ymy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmy, ymy)) {
+ return false;
+ }
+ }
+
+ } else {
+ if (filled) {
+ if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
+ return false;
+ }
+ if (!qp_internal_fillrect_helper_impl(device, xpy, ypx, xmy, ypx)) {
+ return false;
+ }
+ if (!qp_internal_fillrect_helper_impl(device, xpy, ymx, xmy, ymx)) {
+ return false;
+ }
+ } else {
+ if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xpy, ypx)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmy, ypx)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xpy, ymx)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmy, ymx)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_circle
+
+bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
+ qp_dprintf("qp_circle: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_circle: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ // plot the initial set of points for x, y and r
+ int16_t xcalc = 0;
+ int16_t ycalc = (int16_t)radius;
+ int16_t err = ((5 - (radius >> 2)) >> 2);
+
+ qp_internal_fill_pixdata(device, (radius * 2) + 1, hue, sat, val);
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_circle: fail (could not start comms)\n");
+ return false;
+ }
+
+ bool ret = true;
+ if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) {
+ ret = false;
+ }
+
+ if (ret) {
+ while (xcalc < ycalc) {
+ xcalc++;
+ if (err < 0) {
+ err += (xcalc << 1) + 1;
+ } else {
+ ycalc--;
+ err += ((xcalc - ycalc) << 1) + 1;
+ }
+ if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) {
+ ret = false;
+ break;
+ }
+ }
+ }
+
+ qp_dprintf("qp_circle: %s\n", ret ? "ok" : "fail");
+ qp_comms_stop(device);
+ return ret;
+}
diff --git a/quantum/painter/qp_draw_codec.c b/quantum/painter/qp_draw_codec.c
new file mode 100644
index 0000000000..438dce3994
--- /dev/null
+++ b/quantum/painter/qp_draw_codec.c
@@ -0,0 +1,142 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_draw.h"
+#include "qp_comms.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Palette / Monochrome-format decoder
+
+static const qp_pixel_t qp_pixel_white = {.hsv888 = {.h = 0, .s = 0, .v = 255}};
+static const qp_pixel_t qp_pixel_black = {.hsv888 = {.h = 0, .s = 0, .v = 0}};
+
+bool qp_internal_bpp_capable(uint8_t bits_per_pixel) {
+#if !(QUANTUM_PAINTER_SUPPORTS_256_PALETTE)
+ if (bits_per_pixel > 4) {
+ qp_dprintf("qp_internal_decode_palette: image bpp greater than 4\n");
+ return false;
+ }
+#endif
+
+ if (bits_per_pixel > 8) {
+ qp_dprintf("qp_internal_decode_palette: image bpp greater than 8\n");
+ return false;
+ }
+
+ return true;
+}
+
+bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg) {
+ const uint8_t pixel_bitmask = (1 << bits_per_pixel) - 1;
+ const uint8_t pixels_per_byte = 8 / bits_per_pixel;
+ uint32_t remaining_pixels = pixel_count; // don't try to derive from byte_count, we may not use an entire byte
+ while (remaining_pixels > 0) {
+ uint8_t byteval = input_callback(input_arg);
+ if (byteval < 0) {
+ return false;
+ }
+ uint8_t loop_pixels = remaining_pixels < pixels_per_byte ? remaining_pixels : pixels_per_byte;
+ for (uint8_t q = 0; q < loop_pixels; ++q) {
+ if (!output_callback(palette, byteval & pixel_bitmask, output_arg)) {
+ return false;
+ }
+ byteval >>= bits_per_pixel;
+ }
+ remaining_pixels -= loop_pixels;
+ }
+ return true;
+}
+
+bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg) {
+ return qp_internal_decode_recolor(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_pixel_white, qp_pixel_black, output_callback, output_arg);
+}
+
+bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg) {
+ struct painter_driver_t* driver = (struct painter_driver_t*)device;
+ int16_t steps = 1 << bits_per_pixel; // number of items we need to interpolate
+ if (qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, steps)) {
+ if (!driver->driver_vtable->palette_convert(device, steps, qp_internal_global_pixel_lookup_table)) {
+ return false;
+ }
+ }
+
+ return qp_internal_decode_palette(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_internal_global_pixel_lookup_table, output_callback, output_arg);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Progressive pull of bytes, push of pixels
+
+static inline int16_t qp_drawimage_byte_uncompressed_decoder(void* cb_arg) {
+ struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg;
+ state->curr = qp_stream_get(state->src_stream);
+ return state->curr;
+}
+
+static inline int16_t qp_drawimage_byte_rle_decoder(void* cb_arg) {
+ struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg;
+
+ // Work out if we're parsing the initial marker byte
+ if (state->rle.mode == MARKER_BYTE) {
+ uint8_t c = qp_stream_get(state->src_stream);
+ if (c >= 128) {
+ state->rle.mode = NON_REPEATING_RUN; // non-repeated run
+ state->rle.remain = c - 127;
+ } else {
+ state->rle.mode = REPEATING_RUN; // repeated run
+ state->rle.remain = c;
+ }
+
+ state->curr = qp_stream_get(state->src_stream);
+ }
+
+ // Work out which byte we're returning
+ uint8_t c = state->curr;
+
+ // Decrement the counter of the bytes remaining
+ state->rle.remain--;
+
+ if (state->rle.remain > 0) {
+ // If we're in a non-repeating run, queue up the next byte
+ if (state->rle.mode == NON_REPEATING_RUN) {
+ state->curr = qp_stream_get(state->src_stream);
+ }
+ } else {
+ // Swap back to querying the marker byte mode
+ state->rle.mode = MARKER_BYTE;
+ }
+
+ return c;
+}
+
+bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg) {
+ struct qp_internal_pixel_output_state* state = (struct qp_internal_pixel_output_state*)cb_arg;
+ struct painter_driver_t* driver = (struct painter_driver_t*)state->device;
+
+ if (!driver->driver_vtable->append_pixels(state->device, qp_internal_global_pixdata_buffer, palette, state->pixel_write_pos++, 1, &index)) {
+ return false;
+ }
+
+ // If we've hit the transmit limit, send out the entire buffer and reset the write position
+ if (state->pixel_write_pos == state->max_pixels) {
+ if (!driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->pixel_write_pos)) {
+ return false;
+ }
+ state->pixel_write_pos = 0;
+ }
+
+ return true;
+}
+
+qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression) {
+ switch (compression) {
+ case IMAGE_UNCOMPRESSED:
+ return qp_drawimage_byte_uncompressed_decoder;
+ case IMAGE_COMPRESSED_RLE:
+ input_state->rle.mode = MARKER_BYTE;
+ input_state->rle.remain = 0;
+ return qp_drawimage_byte_rle_decoder;
+ default:
+ return NULL;
+ }
+}
diff --git a/quantum/painter/qp_draw_core.c b/quantum/painter/qp_draw_core.c
new file mode 100644
index 0000000000..c31c734132
--- /dev/null
+++ b/quantum/painter/qp_draw_core.c
@@ -0,0 +1,294 @@
+// Copyright 2021-2022 Nick Brassel (@tzarc)
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_draw.h"
+#include "qgf.h"
+
+_Static_assert((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE > 0) && (QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE % 16) == 0, "QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE needs to be a non-zero multiple of 16");
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Global variables
+//
+// NOTE: The variables in this section are intentionally outside a stack frame. They are able to be defined with larger
+// sizes than the normal stack frames would allow, and as such need to be external.
+//
+// **** DO NOT refactor this and decide to place the variables inside the function calling them -- you will ****
+// **** very likely get artifacts rendered to the screen as a result. ****
+//
+
+// Buffer used for transmitting native pixel data to the downstream device.
+uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
+
+// Static buffer to contain a generated color palette
+static bool generated_palette = false;
+static int16_t generated_steps = -1;
+static qp_pixel_t interpolated_fg_hsv888;
+static qp_pixel_t interpolated_bg_hsv888;
+#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
+qp_pixel_t qp_internal_global_pixel_lookup_table[256];
+#else
+qp_pixel_t qp_internal_global_pixel_lookup_table[16];
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Helpers
+
+uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ return ((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE * 8) / driver->native_bits_per_pixel);
+}
+
+// qp_setpixel internal implementation, but accepts a buffer with pre-converted native pixel. Only the first pixel is used.
+bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ return driver->driver_vtable->viewport(device, x, y, x, y) && driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, 1);
+}
+
+// Fills the global native pixel buffer with equivalent pixels matching the supplied HSV
+void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
+ num_pixels = QP_MIN(pixels_in_pixdata, num_pixels);
+
+ // Convert the color to native pixel format
+ qp_pixel_t color = {.hsv888 = {.h = hue, .s = sat, .v = val}};
+ driver->driver_vtable->palette_convert(device, 1, &color);
+
+ // Append the required number of pixels
+ uint8_t palette_idx = 0;
+ for (uint32_t i = 0; i < num_pixels; ++i) {
+ driver->driver_vtable->append_pixels(device, qp_internal_global_pixdata_buffer, &color, i, 1, &palette_idx);
+ }
+}
+
+// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format.
+void qp_internal_invalidate_palette(void) {
+ generated_palette = false;
+ generated_steps = -1;
+}
+
+// Interpolates between two colors to generate a palette
+bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps) {
+ // Check if we need to generate a new palette -- if the input parameters match then assume the palette can stay unchanged.
+ // This may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() to reset.
+ if (generated_palette == true && generated_steps == steps && memcmp(&interpolated_fg_hsv888, &fg_hsv888, sizeof(fg_hsv888)) == 0 && memcmp(&interpolated_bg_hsv888, &bg_hsv888, sizeof(bg_hsv888)) == 0) {
+ // We already have the correct palette, no point regenerating it.
+ return false;
+ }
+
+ // Save the parameters so we know whether we can skip generation
+ generated_palette = true;
+ generated_steps = steps;
+ interpolated_fg_hsv888 = fg_hsv888;
+ interpolated_bg_hsv888 = bg_hsv888;
+
+ int16_t hue_fg = fg_hsv888.hsv888.h;
+ int16_t hue_bg = bg_hsv888.hsv888.h;
+
+ // Make sure we take the "shortest" route from one hue to the other
+ if ((hue_fg - hue_bg) >= 128) {
+ hue_bg += 256;
+ } else if ((hue_fg - hue_bg) <= -128) {
+ hue_bg -= 256;
+ }
+
+ // Interpolate each of the lookup table entries
+ for (int16_t i = 0; i < steps; ++i) {
+ qp_internal_global_pixel_lookup_table[i].hsv888.h = (uint8_t)((hue_fg - hue_bg) * i / (steps - 1) + hue_bg);
+ qp_internal_global_pixel_lookup_table[i].hsv888.s = (uint8_t)((fg_hsv888.hsv888.s - bg_hsv888.hsv888.s) * i / (steps - 1) + bg_hsv888.hsv888.s);
+ qp_internal_global_pixel_lookup_table[i].hsv888.v = (uint8_t)((fg_hsv888.hsv888.v - bg_hsv888.hsv888.v) * i / (steps - 1) + bg_hsv888.hsv888.v);
+
+ qp_dprintf("qp_internal_interpolate_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)steps, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
+ }
+
+ return true;
+}
+
+// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
+bool qp_internal_load_qgf_palette(qp_stream_t *stream, uint8_t bpp) {
+ 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;
+ }
+
+ // BPP determines the number of palette entries, each entry is a HSV888 triplet.
+ const uint16_t palette_entries = 1u << bpp;
+
+ // Ensure we aren't reusing any palette
+ qp_internal_invalidate_palette();
+
+ // Read the palette entries
+ for (uint16_t i = 0; i < palette_entries; ++i) {
+ // Read the palette entry
+ qgf_palette_entry_v1_t entry;
+ if (qp_stream_read(&entry, sizeof(qgf_palette_entry_v1_t), 1, stream) != 1) {
+ return false;
+ }
+
+ // Update the lookup table
+ qp_internal_global_pixel_lookup_table[i].hsv888.h = entry.h;
+ qp_internal_global_pixel_lookup_table[i].hsv888.s = entry.s;
+ qp_internal_global_pixel_lookup_table[i].hsv888.v = entry.v;
+
+ qp_dprintf("qp_internal_load_qgf_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)palette_entries, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
+ }
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_setpixel
+
+bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_setpixel: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("Failed to start comms in qp_setpixel\n");
+ return false;
+ }
+
+ qp_internal_fill_pixdata(device, 1, hue, sat, val);
+ bool ret = qp_internal_setpixel_impl(device, x, y);
+ qp_comms_stop(device);
+ qp_dprintf("qp_setpixel: %s\n", ret ? "ok" : "fail");
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_line
+
+bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val) {
+ if (x0 == x1 || y0 == y1) {
+ qp_dprintf("qp_line(%d, %d, %d, %d): entry (deferring to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1);
+ bool ret = qp_rect(device, x0, y0, x1, y1, hue, sat, val, true);
+ qp_dprintf("qp_line(%d, %d, %d, %d): %s (deferred to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
+ return ret;
+ }
+
+ qp_dprintf("qp_line(%d, %d, %d, %d): entry\n", (int)x0, (int)y0, (int)x1, (int)y1);
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_line: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("Failed to start comms in qp_line\n");
+ return false;
+ }
+
+ qp_internal_fill_pixdata(device, 1, hue, sat, val);
+
+ // draw angled line using Bresenham's algo
+ int16_t x = ((int16_t)x0);
+ int16_t y = ((int16_t)y0);
+ int16_t slopex = ((int16_t)x0) < ((int16_t)x1) ? 1 : -1;
+ int16_t slopey = ((int16_t)y0) < ((int16_t)y1) ? 1 : -1;
+ int16_t dx = abs(((int16_t)x1) - ((int16_t)x0));
+ int16_t dy = -abs(((int16_t)y1) - ((int16_t)y0));
+
+ int16_t e = dx + dy;
+ int16_t e2 = 2 * e;
+
+ bool ret = true;
+ while (x != x1 || y != y1) {
+ if (!qp_internal_setpixel_impl(device, x, y)) {
+ ret = false;
+ break;
+ }
+ e2 = 2 * e;
+ if (e2 >= dy) {
+ e += dy;
+ x += slopex;
+ }
+ if (e2 <= dx) {
+ e += dx;
+ y += slopey;
+ }
+ }
+ // draw the last pixel
+ if (!qp_internal_setpixel_impl(device, x, y)) {
+ ret = false;
+ }
+
+ qp_comms_stop(device);
+ qp_dprintf("qp_line(%d, %d, %d, %d): %s\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_rect
+
+bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
+ uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+
+ uint16_t l = QP_MIN(left, right);
+ uint16_t r = QP_MAX(left, right);
+ uint16_t t = QP_MIN(top, bottom);
+ uint16_t b = QP_MAX(top, bottom);
+ uint16_t w = r - l + 1;
+ uint16_t h = b - t + 1;
+
+ uint32_t remaining = w * h;
+ driver->driver_vtable->viewport(device, l, t, r, b);
+ while (remaining > 0) {
+ uint32_t transmit = QP_MIN(remaining, pixels_in_pixdata);
+ if (!driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, transmit)) {
+ return false;
+ }
+ remaining -= transmit;
+ }
+ return true;
+}
+
+bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
+ qp_dprintf("qp_rect(%d, %d, %d, %d): entry\n", (int)left, (int)top, (int)right, (int)bottom);
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_rect: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ // Cater for cases where people have submitted the coordinates backwards
+ uint16_t l = QP_MIN(left, right);
+ uint16_t r = QP_MAX(left, right);
+ uint16_t t = QP_MIN(top, bottom);
+ uint16_t b = QP_MAX(top, bottom);
+ uint16_t w = r - l + 1;
+ uint16_t h = b - t + 1;
+
+ bool ret = true;
+ if (!qp_comms_start(device)) {
+ qp_dprintf("Failed to start comms in qp_rect\n");
+ return false;
+ }
+
+ if (filled) {
+ // Fill up the pixdata buffer with the required number of native pixels
+ qp_internal_fill_pixdata(device, w * h, hue, sat, val);
+
+ // Perform the draw
+ ret = qp_internal_fillrect_helper_impl(device, l, t, r, b);
+ } else {
+ // Fill up the pixdata buffer with the required number of native pixels
+ qp_internal_fill_pixdata(device, QP_MAX(w, h), hue, sat, val);
+
+ // Draw 4x filled single-width rects to create an outline
+ if (!qp_internal_fillrect_helper_impl(device, l, t, r, t) || !qp_internal_fillrect_helper_impl(device, l, b, r, b) || !qp_internal_fillrect_helper_impl(device, l, t + 1, l, b - 1) || !qp_internal_fillrect_helper_impl(device, r, t + 1, r, b - 1)) {
+ ret = false;
+ }
+ }
+
+ qp_comms_stop(device);
+ qp_dprintf("qp_rect(%d, %d, %d, %d): %s\n", (int)l, (int)t, (int)r, (int)b, ret ? "ok" : "fail");
+ return ret;
+}
diff --git a/quantum/painter/qp_draw_ellipse.c b/quantum/painter/qp_draw_ellipse.c
new file mode 100644
index 0000000000..7f2f4abcfd
--- /dev/null
+++ b/quantum/painter/qp_draw_ellipse.c
@@ -0,0 +1,116 @@
+// Copyright 2021 Paul Cotter (@gr1mr3aver)
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_comms.h"
+#include "qp_draw.h"
+
+// Utilize 4-way symmetry to draw an ellipse
+static bool qp_ellipse_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
+ /*
+ Ellipses have the property of 4-way symmetry, so four pixels can be drawn
+ for each computed [offsetx,offsety] given the center coordinates
+ represented by [centerx,centery].
+
+ For filled ellipses, we can draw horizontal lines between each pair of
+ pixels with the same final value of y.
+
+ When offsetx == 0 only two pixels can be drawn for filled or unfilled ellipses
+ */
+
+ int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
+ int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
+ int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
+ int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
+
+ if (offsetx == 0) {
+ if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
+ return false;
+ }
+ } else if (filled) {
+ if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
+ return false;
+ }
+ if (offsety > 0 && !qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
+ return false;
+ }
+ } else {
+ if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
+ return false;
+ }
+ if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_ellipse
+
+bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
+ qp_dprintf("qp_ellipse: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_ellipse: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ int16_t aa = ((int16_t)sizex) * ((int16_t)sizex);
+ int16_t bb = ((int16_t)sizey) * ((int16_t)sizey);
+ int16_t fa = 4 * ((int16_t)aa);
+ int16_t fb = 4 * ((int16_t)bb);
+
+ int16_t dx = 0;
+ int16_t dy = ((int16_t)sizey);
+
+ qp_internal_fill_pixdata(device, QP_MAX(sizex, sizey), hue, sat, val);
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_ellipse: fail (could not start comms)\n");
+ return false;
+ }
+
+ bool ret = true;
+ for (int16_t delta = (2 * bb) + (aa * (1 - (2 * sizey))); bb * dx <= aa * dy; dx++) {
+ if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
+ ret = false;
+ break;
+ }
+ if (delta >= 0) {
+ delta += fa * (1 - dy);
+ dy--;
+ }
+ delta += bb * (4 * dx + 6);
+ }
+
+ dx = sizex;
+ dy = 0;
+
+ for (int16_t delta = (2 * aa) + (bb * (1 - (2 * sizex))); aa * dy <= bb * dx; dy++) {
+ if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
+ ret = false;
+ break;
+ }
+ if (delta >= 0) {
+ delta += fb * (1 - dx);
+ dx--;
+ }
+ delta += aa * (4 * dy + 6);
+ }
+
+ qp_dprintf("qp_ellipse: %s\n", ret ? "ok" : "fail");
+ qp_comms_stop(device);
+ return ret;
+}
diff --git a/quantum/painter/qp_draw_image.c b/quantum/painter/qp_draw_image.c
new file mode 100644
index 0000000000..5134ae7e99
--- /dev/null
+++ b/quantum/painter/qp_draw_image.c
@@ -0,0 +1,382 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_draw.h"
+#include "qp_comms.h"
+#include "qgf.h"
+#include "deferred_exec.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QGF image handles
+
+typedef struct qgf_image_handle_t {
+ painter_image_desc_t base;
+ bool validate_ok;
+ 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
+ };
+} qgf_image_handle_t;
+
+static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_load_image_mem
+
+painter_image_handle_t qp_load_image_mem(const void *buffer) {
+ qp_dprintf("qp_load_image_mem: entry\n");
+ qgf_image_handle_t *image = NULL;
+
+ // Find a free slot
+ for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) {
+ if (!image_descriptors[i].validate_ok) {
+ image = &image_descriptors[i];
+ break;
+ }
+ }
+
+ // Drop out if not found
+ if (!image) {
+ qp_dprintf("qp_load_image_mem: fail (no free slot)\n");
+ return NULL;
+ }
+
+ // Assume we can read the graphics descriptor
+ image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t));
+
+ // Update the length of the stream to match, and rewind to the start
+ image->mem_stream.length = qgf_get_total_size(&image->stream);
+ image->mem_stream.position = 0;
+
+ // Now that we know the length, validate the input data
+ if (!qgf_validate_stream(&image->stream)) {
+ qp_dprintf("qp_load_image_mem: fail (failed validation)\n");
+ return NULL;
+ }
+
+ // Fill out the QP image descriptor
+ qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL);
+
+ // Validation success, we can return the handle
+ image->validate_ok = true;
+ qp_dprintf("qp_load_image_mem: ok\n");
+ return (painter_image_handle_t)image;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_close_image
+
+bool qp_close_image(painter_image_handle_t image) {
+ qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
+ if (!qgf_image->validate_ok) {
+ qp_dprintf("qp_close_image: fail (invalid image)\n");
+ return false;
+ }
+
+ // Free up this image for use elsewhere.
+ qgf_image->validate_ok = false;
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_drawimage
+
+bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
+ return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_drawimage_recolor
+
+typedef struct qgf_frame_info_t {
+ painter_compression_t compression_scheme;
+ uint8_t bpp;
+ bool has_palette;
+ bool is_delta;
+ uint16_t left;
+ uint16_t top;
+ uint16_t right;
+ uint16_t bottom;
+ uint16_t delay;
+} qgf_frame_info_t;
+
+static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device, qgf_image_handle_t *qgf_image, uint16_t frame_number, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qgf_frame_info_t *info) {
+ 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 (!info) {
+ qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
+ return false;
+ }
+
+ // Seek to the frame
+ qgf_seek_to_frame_descriptor(&qgf_image->stream, frame_number);
+
+ // Read the frame descriptor
+ qgf_frame_v1_t frame_descriptor;
+ if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, &qgf_image->stream) != 1) {
+ qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
+ return false;
+ }
+
+ // Parse out the frame info
+ if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_delta, &info->compression_scheme, &info->delay)) {
+ return false;
+ }
+
+ // Ensure we aren't reusing any palette
+ qp_internal_invalidate_palette();
+
+ // Handle palette if needed
+ const uint16_t palette_entries = 1u << info->bpp;
+ bool needs_pixconvert = false;
+ if (info->has_palette) {
+ // Load the palette from the stream
+ if (!qp_internal_load_qgf_palette((qp_stream_t *)&qgf_image->stream, info->bpp)) {
+ return false;
+ }
+
+ needs_pixconvert = true;
+ } else {
+ // Interpolate from fg/bg
+ needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries);
+ }
+
+ if (!qp_internal_bpp_capable(info->bpp)) {
+ qp_dprintf("qp_drawimage_recolor: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)info->bpp);
+ qp_comms_stop(device);
+ return false;
+ }
+
+ 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_drawimage_recolor: fail (could not convert pixels to native)\n");
+ qp_comms_stop(device);
+ return false;
+ }
+ }
+
+ // Handle delta if needed
+ if (info->is_delta) {
+ qgf_delta_v1_t delta_descriptor;
+ if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, &qgf_image->stream) != 1) {
+ qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
+ return false;
+ }
+
+ info->left = delta_descriptor.left;
+ info->top = delta_descriptor.top;
+ info->right = delta_descriptor.right;
+ info->bottom = delta_descriptor.bottom;
+ }
+
+ // Read the data block
+ qgf_data_v1_t data_descriptor;
+ if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, &qgf_image->stream) != 1) {
+ qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
+ return false;
+ }
+
+ // Stream is now at the point of being able to read pixdata
+ return true;
+}
+
+static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, int frame_number, qgf_frame_info_t *frame_info, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888) {
+ qp_dprintf("qp_drawimage_recolor: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_drawimage_recolor: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
+ if (!qgf_image->validate_ok) {
+ qp_dprintf("qp_drawimage_recolor: fail (invalid image)\n");
+ return false;
+ }
+
+ // Read the frame info
+ if (!qp_drawimage_prepare_frame_for_stream_read(device, qgf_image, frame_number, fg_hsv888, bg_hsv888, frame_info)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not read frame %d)\n", frame_number);
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not start comms)\n");
+ return false;
+ }
+
+ uint16_t l, t, r, b;
+ if (frame_info->is_delta) {
+ l = x + frame_info->left;
+ t = y + frame_info->top;
+ r = x + frame_info->right - 1;
+ b = y + frame_info->bottom - 1;
+ } else {
+ l = x;
+ t = y;
+ r = x + image->width - 1;
+ b = y + image->height - 1;
+ }
+ uint32_t pixel_count = ((uint32_t)(r - l + 1)) * (b - t + 1);
+
+ // Configure where we're going to be rendering to
+ if (!driver->driver_vtable->viewport(device, l, t, r, b)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not set viewport)\n");
+ qp_comms_stop(device);
+ return false;
+ }
+
+ // Set up the input state
+ struct qp_internal_byte_input_state input_state = {.device = device, .src_stream = &qgf_image->stream};
+ qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, frame_info->compression_scheme);
+ if (input_callback == NULL) {
+ qp_dprintf("qp_drawimage_recolor: fail (invalid image compression scheme)\n");
+ qp_comms_stop(device);
+ return false;
+ }
+
+ // Set up the 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)};
+
+ // Decode the pixel data and stream to the display
+ bool ret = qp_internal_decode_palette(device, pixel_count, frame_info->bpp, input_callback, &input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, &output_state);
+
+ // Any leftovers need transmission as well.
+ if (ret && output_state.pixel_write_pos > 0) {
+ ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.pixel_write_pos);
+ }
+
+ qp_dprintf("qp_drawimage_recolor: %s\n", ret ? "ok" : "fail");
+ qp_comms_stop(device);
+ return ret;
+}
+
+bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
+ qgf_frame_info_t frame_info = {0};
+ 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}};
+ return qp_drawimage_recolor_impl(device, x, y, image, 0, &frame_info, fg_hsv888, bg_hsv888);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_animate
+
+deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
+ return qp_animate_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_animate_recolor
+
+typedef struct animation_state_t {
+ painter_device_t device;
+ uint16_t x;
+ uint16_t y;
+ painter_image_handle_t image;
+ qp_pixel_t fg_hsv888;
+ qp_pixel_t bg_hsv888;
+ uint16_t frame_number;
+ deferred_token defer_token;
+} animation_state_t;
+
+static deferred_executor_t animation_executors[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
+static animation_state_t animation_states[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
+
+static deferred_token qp_render_animation_state(animation_state_t *state, uint16_t *delay_ms) {
+ qgf_frame_info_t frame_info = {0};
+ qp_dprintf("qp_render_animation_state: entry (frame #%d)\n", (int)state->frame_number);
+ bool ret = qp_drawimage_recolor_impl(state->device, state->x, state->y, state->image, state->frame_number, &frame_info, state->fg_hsv888, state->bg_hsv888);
+ if (ret) {
+ ++state->frame_number;
+ if (state->frame_number >= state->image->frame_count) {
+ state->frame_number = 0;
+ }
+ *delay_ms = frame_info.delay;
+ }
+ qp_dprintf("qp_render_animation_state: %s (delay %dms)\n", ret ? "ok" : "fail", (int)(*delay_ms));
+ return ret;
+}
+
+static uint32_t animation_callback(uint32_t trigger_time, void *cb_arg) {
+ animation_state_t *state = (animation_state_t *)cb_arg;
+ uint16_t delay_ms;
+ bool ret = qp_render_animation_state(state, &delay_ms);
+ if (!ret) {
+ // Setting the device to NULL clears the animation slot
+ state->device = NULL;
+ }
+ // If we're successful, keep animating -- returning 0 cancels the deferred execution
+ return ret ? delay_ms : 0;
+}
+
+deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, 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_animate_recolor: entry\n");
+
+ animation_state_t *anim_state = NULL;
+ for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
+ if (animation_states[i].device == NULL) {
+ anim_state = &animation_states[i];
+ break;
+ }
+ }
+
+ if (!anim_state) {
+ qp_dprintf("qp_animate_recolor: fail (could not find free animation slot)\n");
+ return INVALID_DEFERRED_TOKEN;
+ }
+
+ // Prepare the animation state
+ anim_state->device = device;
+ anim_state->x = x;
+ anim_state->y = y;
+ anim_state->image = image;
+ anim_state->fg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
+ anim_state->bg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
+ anim_state->frame_number = 0;
+
+ // Draw the first frame
+ uint16_t delay_ms;
+ if (!qp_render_animation_state(anim_state, &delay_ms)) {
+ anim_state->device = NULL; // disregard the allocated animation slot
+ qp_dprintf("qp_animate_recolor: fail (could not render first frame)\n");
+ return INVALID_DEFERRED_TOKEN;
+ }
+
+ // Set up the timer
+ anim_state->defer_token = defer_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, delay_ms, animation_callback, anim_state);
+ if (anim_state->defer_token == INVALID_DEFERRED_TOKEN) {
+ anim_state->device = NULL; // disregard the allocated animation slot
+ qp_dprintf("qp_animate_recolor: fail (could not set up animation executor)\n");
+ return INVALID_DEFERRED_TOKEN;
+ }
+
+ qp_dprintf("qp_animate_recolor: ok (deferred token = %d)\n", (int)anim_state->defer_token);
+ return anim_state->defer_token;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_stop_animation
+
+void qp_stop_animation(deferred_token anim_token) {
+ for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
+ if (animation_states[i].defer_token == anim_token) {
+ cancel_deferred_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, anim_token);
+ animation_states[i].device = NULL;
+ return;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter Core API: qp_internal_animation_tick
+
+void qp_internal_animation_tick(void) {
+ static uint32_t last_anim_exec = 0;
+ deferred_exec_advanced_task(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, &last_anim_exec);
+}
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;
+}
diff --git a/quantum/painter/qp_internal.h b/quantum/painter/qp_internal.h
new file mode 100644
index 0000000000..e7a6d113c5
--- /dev/null
+++ b/quantum/painter/qp_internal.h
@@ -0,0 +1,33 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "quantum.h"
+#include "qp.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Helpers
+
+// Mark certain types that there should be no padding bytes between members.
+#define QP_PACKED __attribute__((packed))
+
+// Min/max defines
+#define QP_MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
+#define QP_MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
+
+#ifdef QUANTUM_PAINTER_DEBUG
+# include <debug.h>
+# include <print.h>
+# define qp_dprintf(...) dprintf(__VA_ARGS__)
+#else
+# define qp_dprintf(...) \
+ do { \
+ } while (0)
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Specific internal definitions
+
+#include <qp_internal_formats.h>
+#include <qp_internal_driver.h>
diff --git a/quantum/painter/qp_internal_driver.h b/quantum/painter/qp_internal_driver.h
new file mode 100644
index 0000000000..9e9d6bc848
--- /dev/null
+++ b/quantum/painter/qp_internal_driver.h
@@ -0,0 +1,82 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver callbacks
+
+typedef bool (*painter_driver_init_func)(painter_device_t device, painter_rotation_t rotation);
+typedef bool (*painter_driver_power_func)(painter_device_t device, bool power_on);
+typedef bool (*painter_driver_clear_func)(painter_device_t device);
+typedef bool (*painter_driver_flush_func)(painter_device_t device);
+typedef bool (*painter_driver_viewport_func)(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
+typedef bool (*painter_driver_pixdata_func)(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
+typedef bool (*painter_driver_convert_palette_func)(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
+typedef bool (*painter_driver_append_pixels)(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);
+
+// Driver vtable definition
+struct painter_driver_vtable_t {
+ painter_driver_init_func init;
+ painter_driver_power_func power;
+ painter_driver_clear_func clear;
+ painter_driver_flush_func flush;
+ painter_driver_viewport_func viewport;
+ painter_driver_pixdata_func pixdata;
+ painter_driver_convert_palette_func palette_convert;
+ painter_driver_append_pixels append_pixels;
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Comms callbacks
+
+typedef bool (*painter_driver_comms_init_func)(painter_device_t device);
+typedef bool (*painter_driver_comms_start_func)(painter_device_t device);
+typedef void (*painter_driver_comms_stop_func)(painter_device_t device);
+typedef uint32_t (*painter_driver_comms_send_func)(painter_device_t device, const void *data, uint32_t byte_count);
+
+struct painter_comms_vtable_t {
+ painter_driver_comms_init_func comms_init;
+ painter_driver_comms_start_func comms_start;
+ painter_driver_comms_stop_func comms_stop;
+ painter_driver_comms_send_func comms_send;
+};
+
+typedef void (*painter_driver_comms_send_command_func)(painter_device_t device, uint8_t cmd);
+typedef void (*painter_driver_comms_bulk_command_sequence)(painter_device_t device, const uint8_t *sequence, size_t sequence_len);
+
+struct painter_comms_with_command_vtable_t {
+ struct painter_comms_vtable_t base; // must be first, so this object can be cast from the painter_comms_vtable_t* type
+ painter_driver_comms_send_command_func send_command;
+ painter_driver_comms_bulk_command_sequence bulk_command_sequence;
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Driver base definition
+
+struct painter_driver_t {
+ const struct painter_driver_vtable_t *driver_vtable;
+ const struct painter_comms_vtable_t * comms_vtable;
+
+ // Flag signifying if validation was successful
+ bool validate_ok;
+
+ // Panel geometry
+ uint16_t panel_width;
+ uint16_t panel_height;
+
+ // Target drawing rotation
+ painter_rotation_t rotation;
+
+ // Automated offsets for setting viewport
+ uint16_t offset_x;
+ uint16_t offset_y;
+
+ // Number of bits per pixel, used for determining how many pixels can be sent during a transmission of the pixdata buffer
+ uint8_t native_bits_per_pixel;
+
+ // Comms config pointer -- needs to point to an appropriate comms config if the comms driver requires it.
+ void *comms_config;
+};
diff --git a/quantum/painter/qp_internal_formats.h b/quantum/painter/qp_internal_formats.h
new file mode 100644
index 0000000000..a4a86f0345
--- /dev/null
+++ b/quantum/painter/qp_internal_formats.h
@@ -0,0 +1,49 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter pixel formats
+
+// Datatype containing a pixel's color. The internal member used is dependent on the external context.
+typedef union QP_PACKED qp_pixel_t {
+ uint8_t mono;
+ uint8_t palette_idx;
+
+ struct QP_PACKED {
+ uint8_t h;
+ uint8_t s;
+ uint8_t v;
+ } hsv888;
+
+ struct QP_PACKED {
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+ } rgb888;
+
+ uint16_t rgb565;
+
+ uint32_t dummy;
+} qp_pixel_t;
+_Static_assert(sizeof(qp_pixel_t) == 4, "Invalid size for qp_pixel_t");
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter image format
+
+typedef enum qp_image_format_t {
+ // Pixel formats available in the QGF frame format
+ GRAYSCALE_1BPP = 0x00,
+ GRAYSCALE_2BPP = 0x01,
+ GRAYSCALE_4BPP = 0x02,
+ GRAYSCALE_8BPP = 0x03,
+ PALETTE_1BPP = 0x04,
+ PALETTE_2BPP = 0x05,
+ PALETTE_4BPP = 0x06,
+ PALETTE_8BPP = 0x07,
+} qp_image_format_t;
+
+typedef enum painter_compression_t { IMAGE_UNCOMPRESSED, IMAGE_COMPRESSED_RLE } painter_compression_t;
diff --git a/quantum/painter/qp_stream.c b/quantum/painter/qp_stream.c
new file mode 100644
index 0000000000..f00ae5ed38
--- /dev/null
+++ b/quantum/painter/qp_stream.c
@@ -0,0 +1,171 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_stream.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Stream API
+
+uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) {
+ uint8_t *output_ptr = (uint8_t *)output_buf;
+
+ uint32_t i;
+ for (i = 0; i < (num_members * member_size); ++i) {
+ int16_t c = qp_stream_get(stream);
+ if (c < 0) {
+ break;
+ }
+
+ output_ptr[i] = (uint8_t)(c & 0xFF);
+ }
+
+ return i / member_size;
+}
+
+uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) {
+ uint8_t *input_ptr = (uint8_t *)input_buf;
+
+ uint32_t i;
+ for (i = 0; i < (num_members * member_size); ++i) {
+ if (!qp_stream_put(stream, input_ptr[i])) {
+ break;
+ }
+ }
+
+ return i / member_size;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Memory streams
+
+int16_t mem_get(qp_stream_t *stream) {
+ qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
+ if (s->position >= s->length) {
+ s->is_eof = true;
+ return STREAM_EOF;
+ }
+ return s->buffer[s->position++];
+}
+
+bool mem_put(qp_stream_t *stream, uint8_t c) {
+ qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
+ if (s->position >= s->length) {
+ s->is_eof = true;
+ return false;
+ }
+ s->buffer[s->position++] = c;
+ return true;
+}
+
+int mem_seek(qp_stream_t *stream, int32_t offset, int origin) {
+ qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
+
+ // Handle as per fseek
+ int32_t position = s->position;
+ switch (origin) {
+ case SEEK_SET:
+ position = offset;
+ break;
+ case SEEK_CUR:
+ position += offset;
+ break;
+ case SEEK_END:
+ position = s->length + offset;
+ break;
+ default:
+ return -1;
+ }
+
+ // If we're before the start, ignore it.
+ if (position < 0) {
+ return -1;
+ }
+
+ // If we're at the end it's okay, we only care if we're after the end for failure purposes -- as per lseek()
+ if (position > s->length) {
+ return -1;
+ }
+
+ // Update the offset
+ s->position = position;
+
+ // Successful invocation of fseek() results in clearing of the EOF flag by default, mirror the same functionality
+ s->is_eof = false;
+
+ return 0;
+}
+
+int32_t mem_tell(qp_stream_t *stream) {
+ qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
+ return s->position;
+}
+
+bool mem_is_eof(qp_stream_t *stream) {
+ qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
+ return s->is_eof;
+}
+
+qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length) {
+ qp_memory_stream_t stream = {
+ .base =
+ {
+ .get = mem_get,
+ .put = mem_put,
+ .seek = mem_seek,
+ .tell = mem_tell,
+ .is_eof = mem_is_eof,
+ },
+ .buffer = (uint8_t *)buffer,
+ .length = length,
+ .position = 0,
+ };
+ return stream;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// FILE streams
+
+#ifdef QP_STREAM_HAS_FILE_IO
+
+int16_t file_get(qp_stream_t *stream) {
+ qp_file_stream_t *s = (qp_file_stream_t *)stream;
+ int c = fgetc(s->file);
+ if (c < 0 || feof(s->file)) return STREAM_EOF;
+ return (uint16_t)c;
+}
+
+bool file_put(qp_stream_t *stream, uint8_t c) {
+ qp_file_stream_t *s = (qp_file_stream_t *)stream;
+ return fputc(c, s->file) == c;
+}
+
+int file_seek(qp_stream_t *stream, int32_t offset, int origin) {
+ qp_file_stream_t *s = (qp_file_stream_t *)stream;
+ return fseek(s->file, offset, origin);
+}
+
+int32_t file_tell(qp_stream_t *stream) {
+ qp_file_stream_t *s = (qp_file_stream_t *)stream;
+ return (int32_t)ftell(s->file);
+}
+
+bool file_is_eof(qp_stream_t *stream) {
+ qp_file_stream_t *s = (qp_file_stream_t *)stream;
+ return (bool)feof(s->file);
+}
+
+qp_file_stream_t qp_make_file_stream(FILE *f) {
+ qp_file_stream_t stream = {
+ .base =
+ {
+ .get = file_get,
+ .put = file_put,
+ .seek = file_seek,
+ .tell = file_tell,
+ .is_eof = file_is_eof,
+ },
+ .file = f,
+ };
+ return stream;
+}
+#endif // QP_STREAM_HAS_FILE_IO
diff --git a/quantum/painter/qp_stream.h b/quantum/painter/qp_stream.h
new file mode 100644
index 0000000000..878b9bf530
--- /dev/null
+++ b/quantum/painter/qp_stream.h
@@ -0,0 +1,82 @@
+/* Copyright 2021 Nick Brassel (@tzarc)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "qp_internal.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Stream API
+
+typedef struct qp_stream_t qp_stream_t;
+
+#define qp_stream_get(stream_ptr) (((qp_stream_t *)(stream_ptr))->get((qp_stream_t *)(stream_ptr)))
+#define qp_stream_put(stream_ptr, c) (((qp_stream_t *)(stream_ptr))->put((qp_stream_t *)(stream_ptr), (c)))
+#define qp_stream_seek(stream_ptr, offset, origin) (((qp_stream_t *)(stream_ptr))->seek((qp_stream_t *)(stream_ptr), (offset), (origin)))
+#define qp_stream_tell(stream_ptr) (((qp_stream_t *)(stream_ptr))->tell((qp_stream_t *)(stream_ptr)))
+#define qp_stream_eof(stream_ptr) (((qp_stream_t *)(stream_ptr))->is_eof((qp_stream_t *)(stream_ptr)))
+#define qp_stream_setpos(stream_ptr, offset) qp_stream_seek((stream_ptr), (offset), SEEK_SET)
+#define qp_stream_getpos(stream_ptr) qp_stream_tell((stream_ptr))
+#define qp_stream_read(output_buf, member_size, num_members, stream_ptr) qp_stream_read_impl((output_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr))
+#define qp_stream_write(input_buf, member_size, num_members, stream_ptr) qp_stream_write_impl((input_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr))
+
+uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream);
+uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream);
+
+#define STREAM_EOF ((int16_t)(-1))
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Stream definition
+
+struct qp_stream_t {
+ int16_t (*get)(qp_stream_t *stream);
+ bool (*put)(qp_stream_t *stream, uint8_t c);
+ int (*seek)(qp_stream_t *stream, int32_t offset, int origin);
+ int32_t (*tell)(qp_stream_t *stream);
+ bool (*is_eof)(qp_stream_t *stream);
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Memory streams
+
+typedef struct qp_memory_stream_t {
+ qp_stream_t base;
+ uint8_t * buffer;
+ int32_t length;
+ int32_t position;
+ bool is_eof;
+} qp_memory_stream_t;
+
+qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length);
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// FILE streams
+
+#ifdef QP_STREAM_HAS_FILE_IO
+
+typedef struct qp_file_stream_t {
+ qp_stream_t base;
+ FILE * file;
+} qp_file_stream_t;
+
+qp_file_stream_t qo_make_file_stream(FILE *f);
+
+#endif // QP_STREAM_HAS_FILE_IO
diff --git a/quantum/painter/rules.mk b/quantum/painter/rules.mk
new file mode 100644
index 0000000000..9115d3d406
--- /dev/null
+++ b/quantum/painter/rules.mk
@@ -0,0 +1,116 @@
+# Quantum Painter Configurables
+QUANTUM_PAINTER_DRIVERS ?=
+QUANTUM_PAINTER_ANIMATIONS_ENABLE ?= yes
+
+# The list of permissible drivers that can be listed in QUANTUM_PAINTER_DRIVERS
+VALID_QUANTUM_PAINTER_DRIVERS := ili9163_spi ili9341_spi st7789_spi gc9a01_spi ssd1351_spi
+
+#-------------------------------------------------------------------------------
+
+OPT_DEFS += -DQUANTUM_PAINTER_ENABLE
+COMMON_VPATH += $(QUANTUM_DIR)/painter
+SRC += \
+ $(QUANTUM_DIR)/utf8.c \
+ $(QUANTUM_DIR)/color.c \
+ $(QUANTUM_DIR)/painter/qp.c \
+ $(QUANTUM_DIR)/painter/qp_stream.c \
+ $(QUANTUM_DIR)/painter/qgf.c \
+ $(QUANTUM_DIR)/painter/qff.c \
+ $(QUANTUM_DIR)/painter/qp_draw_core.c \
+ $(QUANTUM_DIR)/painter/qp_draw_codec.c \
+ $(QUANTUM_DIR)/painter/qp_draw_circle.c \
+ $(QUANTUM_DIR)/painter/qp_draw_ellipse.c \
+ $(QUANTUM_DIR)/painter/qp_draw_image.c \
+ $(QUANTUM_DIR)/painter/qp_draw_text.c
+
+# Check if people want animations... enable the defered exec if so.
+ifeq ($(strip $(QUANTUM_PAINTER_ANIMATIONS_ENABLE)), yes)
+ DEFERRED_EXEC_ENABLE := yes
+ OPT_DEFS += -DQUANTUM_PAINTER_ANIMATIONS_ENABLE
+endif
+
+# Comms flags
+QUANTUM_PAINTER_NEEDS_COMMS_SPI ?= no
+
+# Handler for each driver
+define handle_quantum_painter_driver
+ CURRENT_PAINTER_DRIVER := $1
+
+ ifeq ($$(filter $$(strip $$(CURRENT_PAINTER_DRIVER)),$$(VALID_QUANTUM_PAINTER_DRIVERS)),)
+ $$(error "$$(CURRENT_PAINTER_DRIVER)" is not a valid Quantum Painter driver)
+
+ else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9163_spi)
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
+ OPT_DEFS += -DQUANTUM_PAINTER_ILI9163_ENABLE -DQUANTUM_PAINTER_ILI9163_SPI_ENABLE
+ COMMON_VPATH += \
+ $(DRIVER_PATH)/painter/tft_panel \
+ $(DRIVER_PATH)/painter/ili9xxx
+ SRC += \
+ $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
+ $(DRIVER_PATH)/painter/ili9xxx/qp_ili9163.c \
+
+ else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9341_spi)
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
+ OPT_DEFS += -DQUANTUM_PAINTER_ILI9341_ENABLE -DQUANTUM_PAINTER_ILI9341_SPI_ENABLE
+ COMMON_VPATH += \
+ $(DRIVER_PATH)/painter/tft_panel \
+ $(DRIVER_PATH)/painter/ili9xxx
+ SRC += \
+ $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
+ $(DRIVER_PATH)/painter/ili9xxx/qp_ili9341.c \
+
+ else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),st7789_spi)
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
+ OPT_DEFS += -DQUANTUM_PAINTER_ST7789_ENABLE -DQUANTUM_PAINTER_ST7789_SPI_ENABLE
+ COMMON_VPATH += \
+ $(DRIVER_PATH)/painter/tft_panel \
+ $(DRIVER_PATH)/painter/st77xx
+ SRC += \
+ $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
+ $(DRIVER_PATH)/painter/st77xx/qp_st7789.c
+
+ else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),gc9a01_spi)
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
+ OPT_DEFS += -DQUANTUM_PAINTER_GC9A01_ENABLE -DQUANTUM_PAINTER_GC9A01_SPI_ENABLE
+ COMMON_VPATH += \
+ $(DRIVER_PATH)/painter/tft_panel \
+ $(DRIVER_PATH)/painter/gc9a01
+ SRC += \
+ $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
+ $(DRIVER_PATH)/painter/gc9a01/qp_gc9a01.c
+
+ else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ssd1351_spi)
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
+ QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
+ OPT_DEFS += -DQUANTUM_PAINTER_SSD1351_ENABLE -DQUANTUM_PAINTER_SSD1351_SPI_ENABLE
+ COMMON_VPATH += \
+ $(DRIVER_PATH)/painter/tft_panel \
+ $(DRIVER_PATH)/painter/ssd1351
+ SRC += \
+ $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
+ $(DRIVER_PATH)/painter/ssd1351/qp_ssd1351.c
+
+ endif
+endef
+
+# Iterate through the listed drivers for the build, including what's necessary
+$(foreach qp_driver,$(QUANTUM_PAINTER_DRIVERS),$(eval $(call handle_quantum_painter_driver,$(qp_driver))))
+
+# If SPI comms is needed, set up the required files
+ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI)), yes)
+ OPT_DEFS += -DQUANTUM_PAINTER_SPI_ENABLE
+ QUANTUM_LIB_SRC += spi_master.c
+ VPATH += $(DRIVER_PATH)/painter/comms
+ SRC += \
+ $(QUANTUM_DIR)/painter/qp_comms.c \
+ $(DRIVER_PATH)/painter/comms/qp_comms_spi.c
+
+ ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET)), yes)
+ OPT_DEFS += -DQUANTUM_PAINTER_SPI_DC_RESET_ENABLE
+ endif
+endif
+
diff --git a/quantum/process_keycode/process_unicode_common.c b/quantum/process_keycode/process_unicode_common.c
index 2606ea1f37..652becbc9a 100644
--- a/quantum/process_keycode/process_unicode_common.c
+++ b/quantum/process_keycode/process_unicode_common.c
@@ -16,6 +16,7 @@
#include "process_unicode_common.h"
#include "eeprom.h"
+#include "utf8.h"
unicode_config_t unicode_config;
uint8_t unicode_saved_mods;
@@ -229,35 +230,6 @@ void register_unicode(uint32_t code_point) {
unicode_input_finish();
}
-// Borrowed from https://nullprogram.com/blog/2017/10/06/
-static const char *decode_utf8(const char *str, int32_t *code_point) {
- const char *next;
-
- if (str[0] < 0x80) { // U+0000-007F
- *code_point = str[0];
- next = str + 1;
- } else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF
- *code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0);
- next = str + 2;
- } else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF
- *code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0);
- next = str + 3;
- } else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF
- *code_point = ((int32_t)(str[0] & 0x07) << 18) | ((int32_t)(str[1] & 0x3F) << 12) | ((int32_t)(str[2] & 0x3F) << 6) | ((int32_t)(str[3] & 0x3F) << 0);
- next = str + 4;
- } else {
- *code_point = -1;
- next = str + 1;
- }
-
- // part of a UTF-16 surrogate pair - invalid
- if (*code_point >= 0xD800 && *code_point <= 0xDFFF) {
- *code_point = -1;
- }
-
- return next;
-}
-
void send_unicode_string(const char *str) {
if (!str) {
return;
diff --git a/quantum/quantum.h b/quantum/quantum.h
index f87e5f1916..9ce3c1f5d6 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -188,6 +188,10 @@ extern layer_state_t layer_state;
# include "st7565.h"
#endif
+#ifdef QUANTUM_PAINTER_ENABLE
+# include "qp.h"
+#endif
+
#ifdef DIP_SWITCH_ENABLE
# include "dip_switch.h"
#endif
diff --git a/quantum/utf8.c b/quantum/utf8.c
new file mode 100644
index 0000000000..4b2cd4d8d4
--- /dev/null
+++ b/quantum/utf8.c
@@ -0,0 +1,46 @@
+/* Copyright 2021 QMK
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "utf8.h"
+
+// Borrowed from https://nullprogram.com/blog/2017/10/06/
+const char *decode_utf8(const char *str, int32_t *code_point) {
+ const char *next;
+
+ if (str[0] < 0x80) { // U+0000-007F
+ *code_point = str[0];
+ next = str + 1;
+ } else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF
+ *code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0);
+ next = str + 2;
+ } else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF
+ *code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0);
+ next = str + 3;
+ } else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF
+ *code_point = ((int32_t)(str[0] & 0x07) << 18) | ((int32_t)(str[1] & 0x3F) << 12) | ((int32_t)(str[2] & 0x3F) << 6) | ((int32_t)(str[3] & 0x3F) << 0);
+ next = str + 4;
+ } else {
+ *code_point = -1;
+ next = str + 1;
+ }
+
+ // part of a UTF-16 surrogate pair - invalid
+ if (*code_point >= 0xD800 && *code_point <= 0xDFFF) {
+ *code_point = -1;
+ }
+
+ return next;
+}
diff --git a/quantum/utf8.h b/quantum/utf8.h
new file mode 100644
index 0000000000..fb10910944
--- /dev/null
+++ b/quantum/utf8.h
@@ -0,0 +1,21 @@
+/* Copyright 2021 QMK
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+const char *decode_utf8(const char *str, int32_t *code_point); \ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 6d338ae1cb..e09d58d829 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,4 @@ milc>=1.4.2
pygments
pyusb
qmk-dotty-dict
+pillow
diff --git a/setup.cfg b/setup.cfg
index c7d7952098..6cbe1a616d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,11 @@ ignore =
# Conflicts with our yapf config
E231
per_file_ignores =
+ # Module imported but unused
**/__init__.py:F401
+ # Quantum Painter also outputs append data using bytes object arithmetic on multiple lines
+ **/painter_qgf.py:W503
+ **/painter_qff.py:W503
# Let's slowly crank this down
max_complexity=16