qmk_oled_compress compresses monochrome OLED animation frames and emits C arrays that can be embedded in QMK firmware.
It is built for the common QMK OLED memory layout where bytes represent vertical groups of 8 pixels. The tool loads image files, converts them to 1-bit grayscale, compresses each frame, and chooses between:
- encoding the frame directly, or
- encoding the XOR diff against the previous frame when that is smaller.
The result is printed to stdout as C source.
The CLI emits these symbols:
frames[]: all compressed frame payloads concatenated togetherframe_sizes[]: compressed byte length for each frame inframes[]diff_frames[]: frame indexes that must be XORed with the previous displayed frameheightandwidth: source image dimensions
- all input images must have the same width and height
- image width must be divisible by 8
- images should already match the target OLED resolution you want in QMK
- image paths are processed in the order given on the command line
Requires Zig 0.15.2.
zig buildBuilt binary:
./zig-out/bin/qmk_oled_compressRun through the build system:
zig build run -- frame1.png frame2.png frame3.pngOr run the built binary directly:
./zig-out/bin/qmk_oled_compress frame1.png frame2.png frame3.pngGenerate a header or source file for QMK:
./zig-out/bin/qmk_oled_compress assets/frame*.png > keymap/oled_frames.cThen include that generated file from your keymap or userspace code.
The encoder uses three block types:
- black runs: repeated zero bytes
- raw blocks: literal bytes copied as-is
- RLE blocks: repeated non-zero bytes
For frames after the first one, the CLI also tries compressing the XOR diff against the previous frame and keeps whichever representation is smaller.
This sample decoder matches the format generated by this project.
static uint16_t frame_offset(uint8_t frame_index) {
uint16_t offset = 0;
for (uint8_t i = 0; i < frame_index; ++i) {
offset += (uint8_t)pgm_read_byte(&frame_sizes[i]);
}
return offset;
}
static bool is_diff_frame(uint8_t frame_index) {
for (uint8_t i = 0; i < sizeof(diff_frames); ++i) {
if ((uint8_t)pgm_read_byte(&diff_frames[i]) == frame_index) {
return true;
}
}
return false;
}
void draw_frame_raw(const char *frame, uint8_t frame_size, bool diff_frame) {
uint16_t cursor = 0;
uint16_t i = 0;
while (i < frame_size) {
uint8_t ctrl = pgm_read_byte(&frame[i]);
i += 1;
if ((ctrl >> 7) == 0) {
uint8_t count = ctrl & 0x7F;
for (uint16_t j = 0; j < (uint16_t)count; ++j) {
if (!diff_frame) {
oled_write_raw_byte(0, cursor);
}
cursor += 1;
}
continue;
}
uint8_t count = ctrl & 0x3F;
if ((ctrl >> 6) == 0b10) {
if (!diff_frame) {
oled_set_cursor_exact(cursor);
oled_write_raw_P(&frame[i], count);
cursor += count;
i += count;
} else {
for (uint8_t j = 0; j < count; ++j) {
uint8_t value = pgm_read_byte(&frame[i]);
uint8_t prev = oled_read_raw_byte(cursor);
oled_write_raw_byte(prev ^ value, cursor);
cursor += 1;
i += 1;
}
}
} else {
uint8_t value = pgm_read_byte(&frame[i]);
i += 1;
for (uint8_t j = 0; j < count; ++j) {
if (!diff_frame) {
oled_write_raw_byte(value, cursor);
} else {
uint8_t prev = oled_read_raw_byte(cursor);
oled_write_raw_byte(prev ^ value, cursor);
}
cursor += 1;
}
}
}
}
void draw_frame(uint8_t frame_index) {
uint16_t offset = frame_offset(frame_index);
const char *frame = &frames[offset];
uint8_t size = (uint8_t)pgm_read_byte(&frame_sizes[frame_index]);
draw_frame_raw(frame, size, is_diff_frame(frame_index));
}The sample above relies on two helper functions that are not available in stock QMK OLED code. (Need to add them yourself in oled_driver.c.)
void oled_set_cursor_exact(uint16_t index) {
if (index >= OLED_MATRIX_SIZE) {
index = 0;
}
oled_cursor = &oled_buffer[index];
}
uint8_t oled_read_raw_byte(uint16_t index) {
if (index >= OLED_MATRIX_SIZE) {
index = 0;
}
return oled_buffer[index];
}The package also exposes the core codec from src/root.zig:
encode(allocator, bytes)decode(allocator, bytes)
Those functions operate on already packed OLED bytes, not on image files.