Skip to content

NullSeile/qmk-oled-compress

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

qmk_oled_compress

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.

What it outputs

The CLI emits these symbols:

  • frames[]: all compressed frame payloads concatenated together
  • frame_sizes[]: compressed byte length for each frame in frames[]
  • diff_frames[]: frame indexes that must be XORed with the previous displayed frame
  • height and width: source image dimensions

Input requirements

  • 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

Build

Requires Zig 0.15.2.

zig build

Built binary:

./zig-out/bin/qmk_oled_compress

Usage

Run through the build system:

zig build run -- frame1.png frame2.png frame3.png

Or run the built binary directly:

./zig-out/bin/qmk_oled_compress frame1.png frame2.png frame3.png

Example workflow

Generate a header or source file for QMK:

./zig-out/bin/qmk_oled_compress assets/frame*.png > keymap/oled_frames.c

Then include that generated file from your keymap or userspace code.

Compression format

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.

Using the generated data in QMK

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];
}

Zig API

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.

About

Tool to compress animations for usage in QMK

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages