Skip to main content

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:

QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += ......

You will also likely need to select an appropriate driver in rules.mk, which is listed below.

caution

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.

Supported devices:

Display PanelPanel TypeSizeComms TransportDriver
GC9A01RGB LCD (circular)240x240SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += gc9a01_spi
ILI9163RGB LCD128x128SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += ili9163_spi
ILI9341RGB LCD240x320SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += ili9341_spi
ILI9488RGB LCD320x480SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += ili9488_spi
SSD1351RGB OLED128x128SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += ssd1351_spi
ST7735RGB LCD132x162, 80x160SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += st7735_spi
ST7789RGB LCD240x320, 240x240SPI + D/C + RSTQUANTUM_PAINTER_DRIVERS += st7789_spi
RGB565 SurfaceVirtualUser-definedNoneQUANTUM_PAINTER_DRIVERS += rgb565_surface

Quantum Painter Configuration

OptionDefaultPurpose
QUANTUM_PAINTER_NUM_IMAGES8The maximum number of images/animations that can be loaded at any one time.
QUANTUM_PAINTER_NUM_FONTS4The maximum number of fonts that can be loaded at any one time.
QUANTUM_PAINTER_CONCURRENT_ANIMATIONS4The maximum number of animations that can be executed at the same time.
QUANTUM_PAINTER_LOAD_FONTS_TO_RAMFALSEWhether 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_SIZE32The 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_PALETTEFALSEIf 256-color palettes are supported. Requires significantly more RAM on the MCU.
QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORSFALSEIf native color range is supported. Requires significantly more RAM on the MCU.
QUANTUM_PAINTER_DEBUGunsetPrints 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

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] [-w] [-d] [-r] -f FORMAT [-o OUTPUT] -i INPUT [-v]

options:
-h, --help show this help message and exit
-w, --raw Writes out the QGF file as raw data instead of c/h combo.
-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: rgb888, rgb565, 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:

FormatMeaning
rgb88816,777,216 colors in 8-8-8 RGB format (requires QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS)
rgb56565,536 colors in 5-6-5 RGB format (requires QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS)
pal256256-color palette (requires QUANTUM_PAINTER_SUPPORTS_256_PALETTE)
pal1616-color palette
pal44-color palette
pal22-color palette
mono256256-shade grayscale (requires QUANTUM_PAINTER_SUPPORTS_256_PALETTE)
mono1616-shade grayscale
mono44-shade grayscale
mono22-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] [-w] [-r] -f FORMAT [-u UNICODE_GLYPHS] [-n] [-o OUTPUT] [-i INPUT]

options:
-h, --help show this help message and exit
-w, --raw Writes out the QFF file as raw data instead of c/h combo.
-r, --no-rle Disable the use of RLE to minimise converted image size.
-f FORMAT, --format FORMAT
Output format, valid types: rgb565, 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 Display Drivers

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.

Enabling support for the GC9A01 in Quantum Painter is done by adding the following to rules.mk:

QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += gc9a01_spi

Creating a GC9A01 device in firmware can then be done with the following API:

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):

// 3 displays:
#define GC9A01_NUM_DEVICES 3

Native color format rgb565 is compatible with GC9A01

Enabling support for RGB565 surfaces in Quantum Painter is done by adding the following to rules.mk:

QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += rgb565_surface

Creating a RGB565 surface in firmware can then be done with the following API:

painter_device_t qp_rgb565_make_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);

The buffer is a user-supplied area of memory, and is assumed to be of the size sizeof(uint16_t) * panel_width * panel_height.

The device handle returned from the qp_rgb565_make_surface function can be used to perform all other drawing operations.

Example:

static painter_device_t my_surface;
static uint16_t my_framebuffer[320 * 240]; // Allocate a buffer for a 320x240 RGB565 display
void keyboard_post_init_kb(void) {
my_surface = qp_rgb565_make_surface(320, 240, my_framebuffer);
qp_init(my_surface, QP_ROTATION_0);
}

The maximum number of RGB565 surfaces can be configured by changing the following in your config.h (default is 1):

// 3 surfaces:
#define RGB565_SURFACE_NUM_DEVICES 3

To transfer the contents of the RGB565 surface to another display, the following API can be invoked:

bool qp_rgb565_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y);

The surface is the surface to copy out from. The display is the target display to draw into. x and y are the target location to draw the surface pixel data. Under normal circumstances, the location should be consistent, as the dirty region is calculated with respect to the x and y coordinates -- changing those will result in partial, overlapping draws.

tip

Calling qp_flush() on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.

Quantum Painter Drawing 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:

#include <qp.h>

General Notes

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.

tip

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.
tip

Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled.

Device Control

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.

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
}

Drawing Primitives

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.

tip

Using qp_setpixel for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs.

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);
}
}

Image Functions

Making an image available for use requires compiling it into your firmware. To do so, assuming you've created my_image.qgf.c and my_image.qgf.h as per the CLI examples above, you'd add the following to your rules.mk:

SRC += my_image.qgf.c

...and in your keymap.c, you'd add to the top of the file:

#include "my_image.qgf.h"
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 for instructions on how to convert images to QGF.

tip

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:

PropertyAccessor
Widthimage->width
Heightimage->height
Frame Countimage->frame_count

Font Functions

Making a font available for use requires compiling it into your firmware. To do so, assuming you've created my_font.qff.c and my_font.qff.h as per the CLI examples above, you'd add the following to your rules.mk:

SRC += noto11.qff.c

...and in your keymap.c, you'd add to the top of the file:

#include "noto11.qff.h"
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 for instructions on how to convert TTF fonts to QFF.

tip

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:

PropertyAccessor
Line Heightimage->line_height

Advanced Functions

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.