OLED bitmap compression for deflating QMK-compatible graphics
This tool compresses a 128x32 pixel bitmap generated with img2cpp by marking null bytes (0x0) and non-null bytes using a single bit in a smaller byte array. It works best when the image has lots of whitespace. The compressed images stop being smaller than the input images when the overhead (64 bytes for each 512 byte input) is greater than the savings. Most bitmap images break even or better, but it's worth checking the statistics after running the tool to make sure the savings are worth the effort.
One of the most popular NIBBLE keymaps was too large to enable VIA on. The frames had a lot of whitespace and compressing them seemed like a good idea. The firmware-side decompression algorithm is simple, takes up only a few bytes of flash, and fast enough that it doesn't slow down the matrix scan. After compressing the 7 original frames, there is over 1kB of free flash remaining for other cool firmware features, or the addition of extra frames for longer animations.
Generate a 128x32 byte array using img2cpp (black background, plain bytes, Vertical - 1 bit per pixel) and save only the raw bytes to input_bytes.tmp
. Optionally, users may save the raw bytes to another file of their choosing and use the -f
flag followed by their file name when running the program. Run python3 squeez-o.py
and copy the output block_list
and block_maps
into your keymap or keyboard code. For projects with multiple animations, do this for each frame.
Within the keyboard firmware, use the example decompression algorithm in decompress.c
to decompress the compressed frames and write to the OLED display.
Running on the included input_bytes.tmp
file yields this output:
// Compressed oled data structure
// This must be included ONCE along with the compressed data
typedef struct {
const uint16_t data_len;
const char* block_map;
const char* block_list;
} compressed_oled_frame_t;
static const char PROGMEM frame_map[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00,
0x00, 0x00, 0x00, 0x00
};
static const char PROGMEM frame_list[] = {
0x80, 0xe0, 0xe0, 0xf0, 0xf8, 0xf8, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfe, 0xfe, 0xfe, 0xfe,
0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe,
0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfc, 0xf8, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x0f, 0x07, 0x07, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x01, 0x1f, 0xbf, 0x9f, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xc0, 0xc0, 0xc0,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0,
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x80, 0x80, 0x0c, 0x3f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x3f
};
static const compressed_oled_frame_t PROGMEM frame = {512, frame_map, frame_list};
// Input was 512 bytes, deflated block list is now 158 bytes + 64 bytes overhead
// Space savings: 56.64%
Which can be used within QMK by calling oled_write_compressed_P(frame);
For more info, take a look at the bongo cat keymap.