summaryrefslogtreecommitdiff
path: root/docs
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 /docs
parent1dbbd2b6b068b9f921ebc0341c890df16a491007 (diff)
Quantum Painter (#10174)
* Install dependencies before executing unit tests. * Split out UTF-8 decoder. * Fixup python formatting rules. * Add documentation for QGF/QFF and the RLE format used. * Add CLI commands for converting images and fonts. * Add stub rules.mk for QP. * Add stream type. * Add base driver and comms interfaces. * Add support for SPI, SPI+D/C comms drivers. * Include <qp.h> when enabled. * Add base support for SPI+D/C+RST panels, as well as concrete implementation of ST7789. * Add support for GC9A01. * Add support for ILI9341. * Add support for ILI9163. * Add support for SSD1351. * Implement qp_setpixel, including pixdata buffer management. * Implement qp_line. * Implement qp_rect. * Implement qp_circle. * Implement qp_ellipse. * Implement palette interpolation. * Allow for streams to work with either flash or RAM. * Image loading. * Font loading. * QGF palette loading. * Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp monochrome and palette-based images. * Image drawing. * Animations. * Font rendering. * Check against 256 colours, dump out the loaded palette if debugging enabled. * Fix build. * AVR is not the intended audience. * `qmk format-c` * Generation fix. * First batch of docs. * More docs and examples. * Review comments. * Public API documentation.
Diffstat (limited to 'docs')
-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
6 files changed, 1028 insertions, 0 deletions
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)
+
+```