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
This block header is present for all blocks, including the graphics descriptor.
Block header format:
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
- 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:
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
- 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:
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
- typeid = 0x02
- length = 5
This block denotes the start of a frame.
Block format:
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 pixel0x01
: 2bpp grayscale, no palette,0
= black,3
= white, linear interpolation of brightness, LSb first pixel0x02
: 4bpp grayscale, no palette,0
= black,15
= white, linear interpolation of brightness, LSb first pixel0x03
: 8bpp grayscale, no palette,0
= black,255
= white, linear interpolation of brightness, LSb first pixel0x04
: 1bpp indexed palette, 2 colors, LSb first pixel0x05
: 2bpp indexed palette, 4 colors, LSb first pixel0x06
: 4bpp indexed palette, 16 colors, LSb first pixel0x07
: 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 compression0x01
: QMK RLE
Frame palette block
- 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:
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
- typeid = 0x04
- length = 8
This block describes where the delta frame should be drawn, with respect to the top left location of the image.
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
- 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:
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;