Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quantum Painter #10174

Merged
merged 38 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
275a2bf
Install dependencies before executing unit tests.
tzarc Jan 10, 2022
bc7cafa
Split out UTF-8 decoder.
tzarc Jan 10, 2022
a65c8e7
Fixup python formatting rules.
tzarc Jan 10, 2022
95f7748
Add documentation for QGF/QFF and the RLE format used.
tzarc Jan 10, 2022
3f71372
Add CLI commands for converting images and fonts.
tzarc Jan 10, 2022
ef97ea8
Add stub rules.mk for QP.
tzarc Jan 10, 2022
cf6da0d
Add stream type.
tzarc Jan 10, 2022
8934860
Add base driver and comms interfaces.
tzarc Jan 10, 2022
619eb06
Add support for SPI, SPI+D/C comms drivers.
tzarc Jan 10, 2022
1ad330e
Include <qp.h> when enabled.
tzarc Jan 10, 2022
a37532c
Add base support for SPI+D/C+RST panels, as well as concrete implemen…
tzarc Jan 10, 2022
250eed4
Add support for GC9A01.
tzarc Jan 10, 2022
61381dd
Add support for ILI9341.
tzarc Jan 10, 2022
c2ddf8b
Add support for ILI9163.
tzarc Jan 10, 2022
9eb7e0c
Add support for SSD1351.
tzarc Jan 10, 2022
85e82b9
Implement qp_setpixel, including pixdata buffer management.
tzarc Jan 10, 2022
1ff5422
Implement qp_line.
tzarc Jan 10, 2022
acad413
Implement qp_rect.
tzarc Jan 10, 2022
98f4a8a
Implement qp_circle.
tzarc Jan 10, 2022
d06b657
Implement qp_ellipse.
tzarc Jan 10, 2022
c9bf5f4
Implement palette interpolation.
tzarc Jan 10, 2022
ff8815b
Allow for streams to work with either flash or RAM.
tzarc Jan 10, 2022
a26a988
Image loading.
tzarc Jan 10, 2022
14a12e2
Font loading.
tzarc Jan 10, 2022
2d5e32e
QGF palette loading.
tzarc Jan 10, 2022
19c6bee
Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp …
tzarc Jan 10, 2022
e7e4edc
Image drawing.
tzarc Jan 10, 2022
50996b4
Animations.
tzarc Jan 10, 2022
85b3738
Font rendering.
tzarc Jan 10, 2022
720003a
Check against 256 colours, dump out the loaded palette if debugging e…
tzarc Jan 20, 2022
49dd5f3
Fix build.
tzarc Jan 20, 2022
ee3b415
AVR is not the intended audience.
tzarc Feb 24, 2022
05a0acc
`qmk format-c`
tzarc Feb 25, 2022
bed9a60
Generation fix.
tzarc Mar 3, 2022
bedba63
First batch of docs.
tzarc Mar 26, 2022
7e598b2
More docs and examples.
tzarc Mar 26, 2022
6b233cc
Review comments.
tzarc Apr 12, 2022
af11f6b
Public API documentation.
tzarc Apr 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 7 additions & 1 deletion builddefs/common_features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)),)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
705 changes: 705 additions & 0 deletions docs/quantum_painter.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions docs/quantum_painter_qff.md
Original file line number Diff line number Diff line change
@@ -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.
178 changes: 178 additions & 0 deletions docs/quantum_painter_qgf.md
Original file line number Diff line number Diff line change
@@ -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;
```
29 changes: 29 additions & 0 deletions docs/quantum_painter_rle.md
Original file line number Diff line number Diff line change
@@ -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)

```
Loading