|
| 1 | +# QMK Graphics Format :id=qmk-graphics-format |
| 2 | + |
| 3 | +QMK uses a graphics format _("Quantum Graphics Format" - QGF)_ specifically for resource-constrained systems. |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +All integer values are in little-endian format. |
| 8 | + |
| 9 | +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. |
| 10 | + |
| 11 | +The general structure of the file is: |
| 12 | + |
| 13 | +* _Graphics descriptor block_ |
| 14 | +* _Frame offset block_ |
| 15 | +* Repeating list of frames: |
| 16 | + * _Frame descriptor block_ |
| 17 | + * _Frame palette block_ (optional, depending on frame format) |
| 18 | + * _Frame delta block_ (optional, depending on delta flag) |
| 19 | + * _Frame data block_ |
| 20 | + |
| 21 | +Different frames within the file should be considered "isolated" and may have their own image format and/or palette. |
| 22 | + |
| 23 | +## Block Header :id=qgf-block-header |
| 24 | + |
| 25 | +This block header is present for all blocks, including the graphics descriptor. |
| 26 | + |
| 27 | +_Block header_ format: |
| 28 | + |
| 29 | +```c |
| 30 | +typedef struct __attribute__((packed)) qgf_block_header_v1_t { |
| 31 | + uint8_t type_id; // See each respective block type |
| 32 | + uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors |
| 33 | + uint24_t length; // 24-bit blob length, allowing for block sizes of a maximum of 16MB |
| 34 | +} qgf_block_header_v1_t; |
| 35 | +// _Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF"); |
| 36 | +``` |
| 37 | +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. |
| 38 | +
|
| 39 | +## Graphics descriptor block :id=qgf-graphics-descriptor |
| 40 | +
|
| 41 | +* _typeid_ = 0x00 |
| 42 | +* _length_ = 18 |
| 43 | +
|
| 44 | +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_. |
| 45 | +
|
| 46 | +_Block_ format: |
| 47 | +
|
| 48 | +```c |
| 49 | +typedef struct __attribute__((packed)) qgf_graphics_descriptor_v1_t { |
| 50 | + qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 } |
| 51 | + uint24_t magic; // constant, equal to 0x464751 ("QGF") |
| 52 | + uint8_t qgf_version; // constant, equal to 0x01 |
| 53 | + uint32_t total_file_size; // total size of the entire file, starting at offset zero |
| 54 | + uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors |
| 55 | + uint16_t image_width; // in pixels |
| 56 | + uint16_t image_height; // in pixels |
| 57 | + uint16_t frame_count; // minimum of 1 |
| 58 | +} qgf_graphics_descriptor_v1_t; |
| 59 | +// _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"); |
| 60 | +``` |
| 61 | + |
| 62 | +## Frame offset block :id=qgf-frame-offset-descriptor |
| 63 | + |
| 64 | +* _typeid_ = 0x01 |
| 65 | +* _length_ = variable |
| 66 | + |
| 67 | +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. |
| 68 | + |
| 69 | +Duplicate frame offsets in this block are allowed, if a certain frame is to be shown multiple times during animation. |
| 70 | + |
| 71 | +_Block_ format: |
| 72 | + |
| 73 | +```c |
| 74 | +typedef struct __attribute__((packed)) qgf_frame_offsets_v1_t { |
| 75 | + qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) } |
| 76 | + uint32_t offset[N]; // where 'N' is the number of frames in the file |
| 77 | +} qgf_frame_offsets_v1_t; |
| 78 | +``` |
| 79 | +
|
| 80 | +## Frame descriptor block :id=qgf-frame-descriptor |
| 81 | +
|
| 82 | +* _typeid_ = 0x02 |
| 83 | +* _length_ = 5 |
| 84 | +
|
| 85 | +This block denotes the start of a frame. |
| 86 | +
|
| 87 | +_Block_ format: |
| 88 | +
|
| 89 | +```c |
| 90 | +typedef struct __attribute__((packed)) qgf_frame_v1_t { |
| 91 | + qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 5 } |
| 92 | + uint8_t format; // Frame format, see below. |
| 93 | + uint8_t flags; // Frame flags, see below. |
| 94 | + uint8_t compression_scheme; // Compression scheme, see below. |
| 95 | + uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented) |
| 96 | + uint16_t delay; // frame delay time for animations (in units of milliseconds) |
| 97 | +} qgf_frame_v1_t; |
| 98 | +// _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"); |
| 99 | +``` |
| 100 | + |
| 101 | +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_. |
| 102 | + |
| 103 | +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_. |
| 104 | + |
| 105 | +Frame format possible values: |
| 106 | + |
| 107 | +* `0x00`: 1bpp grayscale, no palette, `0` = black, `1` = white, LSb first pixel |
| 108 | +* `0x01`: 2bpp grayscale, no palette, `0` = black, `3` = white, linear interpolation of brightness, LSb first pixel |
| 109 | +* `0x02`: 4bpp grayscale, no palette, `0` = black, `15` = white, linear interpolation of brightness, LSb first pixel |
| 110 | +* `0x03`: 8bpp grayscale, no palette, `0` = black, `255` = white, linear interpolation of brightness, LSb first pixel |
| 111 | +* `0x04`: 1bpp indexed palette, 2 colors, LSb first pixel |
| 112 | +* `0x05`: 2bpp indexed palette, 4 colors, LSb first pixel |
| 113 | +* `0x06`: 4bpp indexed palette, 16 colors, LSb first pixel |
| 114 | +* `0x07`: 8bpp indexed palette, 256 colors, LSb first pixel |
| 115 | + |
| 116 | +Frame flags is a bitmask with the following format: |
| 117 | + |
| 118 | +| `bit 7` | `bit 6` | `bit 5` | `bit 4` | `bit 3` | `bit 2` | `bit 1` | `bit 0` | |
| 119 | +|---------|---------|---------|---------|---------|---------|---------|--------------| |
| 120 | +| - | - | - | - | - | - | Delta | Transparency | |
| 121 | + |
| 122 | +* `[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_. |
| 123 | +* `[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. |
| 124 | + |
| 125 | +Compression scheme possible values: |
| 126 | + |
| 127 | +* `0x00`: No compression |
| 128 | +* `0x01`: [QMK RLE](quantum_painter_rle.md) |
| 129 | + |
| 130 | +## Frame palette block :id=qgf-frame-palette-descriptor |
| 131 | + |
| 132 | +* _typeid_ = 0x03 |
| 133 | +* _length_ = variable |
| 134 | + |
| 135 | +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: |
| 136 | + |
| 137 | +```c |
| 138 | +typedef struct __attribute__((packed)) qgf_palette_v1_t { |
| 139 | + qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) } |
| 140 | + struct { // container for a single HSV palette entry |
| 141 | + uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t. |
| 142 | + uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t. |
| 143 | + uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t. |
| 144 | + } hsv[N]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor |
| 145 | +} qgf_palette_v1_t; |
| 146 | +``` |
| 147 | +
|
| 148 | +## Frame delta block :id=qgf-frame-delta-descriptor |
| 149 | +
|
| 150 | +* _typeid_ = 0x04 |
| 151 | +* _length_ = 8 |
| 152 | +
|
| 153 | +This block describes where the delta frame should be drawn, with respect to the top left location of the image. |
| 154 | +
|
| 155 | +```c |
| 156 | +typedef struct __attribute__((packed)) qgf_delta_v1_t { |
| 157 | + qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 } |
| 158 | + uint16_t left; // The left pixel location to draw the delta image |
| 159 | + uint16_t top; // The top pixel location to draw the delta image |
| 160 | + uint16_t right; // The right pixel location to to draw the delta image |
| 161 | + uint16_t bottom; // The bottom pixel location to to draw the delta image |
| 162 | +} qgf_delta_v1_t; |
| 163 | +// _Static_assert(sizeof(qgf_delta_v1_t) == 13, "qgf_delta_v1_t must be 13 bytes in v1 of QGF"); |
| 164 | +``` |
| 165 | + |
| 166 | +## Frame data block :id=qgf-frame-data-descriptor |
| 167 | + |
| 168 | +* _typeid_ = 0x05 |
| 169 | +* _length_ = variable |
| 170 | + |
| 171 | +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: |
| 172 | + |
| 173 | +```c |
| 174 | +typedef struct __attribute__((packed)) qgf_data_v1_t { |
| 175 | + qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N } |
| 176 | + uint8_t data[N]; // N data octets |
| 177 | +} qgf_data_v1_t; |
| 178 | +``` |
0 commit comments