:: commit fac13667fc23a2a607a66a7bd0713094fdf61ad3

Kamila Szewczyk <27734421+iczelia@users.noreply.github.com> — 2026-04-26 18:45

parents: b2848fb580

lib/image: add support for qoi (#561)

diff --git a/CONFIG.md b/CONFIG.md
index 3b26d2ea..20ec33a2 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -99,7 +99,7 @@ Miscellaneous:
   (UEFI only).
 * `graphics` - If set to `no`, force text mode for the boot menu, else use
   a video mode.
-* `wallpaper` - Path to a file to use as a wallpaper. BMP, PNG, and JPEG
+* `wallpaper` - Path to a file to use as a wallpaper. BMP, PNG, JPEG, and QOI
   formats are supported. There can be multiple of this option, in which case
   the wallpaper will be randomly selected from the provided options.
 * `wallpaper_style` - The style which will be used to display the wallpaper
diff --git a/common/lib/image.c b/common/lib/image.c
index 805764c9..62ec6309 100644
--- a/common/lib/image.c
+++ b/common/lib/image.c
@@ -4,6 +4,7 @@
 #include <lib/config.h>
 #include <lib/misc.h>
 #include <mm/pmm.h>
+#include <lib/qoi.h>
 #include <lib/stb_image.h>
 
 void image_make_centered(struct image *image, int frame_x_size, int frame_y_size, uint32_t back_colour) {
@@ -22,6 +23,14 @@ void image_make_stretched(struct image *image, int new_x_size, int new_y_size) {
     image->y_size = new_y_size;
 }
 
+static void free_image_data(struct image *image) {
+    if (image->isQoi) {
+        qoi_free(image->img);
+    } else {
+        stbi_image_free(image->img);
+    }
+}
+
 struct image *image_open(struct file_handle *file) {
     struct image *image = ext_mem_alloc(sizeof(struct image));
 
@@ -31,33 +40,44 @@ struct image *image_open(struct file_handle *file) {
 
     fread(file, src, 0, file->size);
 
-    int x, y, bpp;
-
-    image->img = stbi_load_from_memory(src, file->size, &x, &y, &bpp, 4);
+    int x = 0, y = 0;
+    image->isQoi = file->size >= 4
+                  && ((const uint8_t *)src)[0] == 'q'
+                  && ((const uint8_t *)src)[1] == 'o'
+                  && ((const uint8_t *)src)[2] == 'i'
+                  && ((const uint8_t *)src)[3] == 'f';
+
+    if (image->isQoi) {
+        image->img = qoi_decode(src, file->size, &x, &y);
+    } else {
+        int bpp;
+        image->img = stbi_load_from_memory(src, file->size, &x, &y, &bpp, 4);
+    }
 
     pmm_free(src, file->size);
 
     if (image->img == NULL || x == 0 || y == 0) {
-        if (image->img != NULL) {
-            // stbi allocated but dimensions are degenerate
-            stbi_image_free(image->img);
-        }
+        free_image_data(image);
         pmm_free(image, sizeof(struct image));
         return NULL;
     }
 
-    // Convert ABGR to XRGB
-    uint32_t *pptr = (void *)image->img;
-    size_t pixel_count = CHECKED_MUL((size_t)x, (size_t)y,
-        ({ stbi_image_free(image->img); pmm_free(image, sizeof(struct image)); return NULL; }));
-    for (size_t i = 0; i < pixel_count; i++) {
-        pptr[i] = (pptr[i] & 0x0000ff00) | ((pptr[i] & 0x00ff0000) >> 16) | ((pptr[i] & 0x000000ff) << 16);
+    // stb_image returns RGBA bytes (little-endian uint32 ABGR); convert to
+    // the framebuffer-native XRGB layout. The QOI decoder already produces
+    // XRGB, so this step is skipped on that path.
+    if (!image->isQoi) {
+        uint32_t *pptr = (void *)image->img;
+        size_t pixel_count = CHECKED_MUL((size_t)x, (size_t)y,
+            ({ free_image_data(image); pmm_free(image, sizeof(struct image)); return NULL; }));
+        for (size_t i = 0; i < pixel_count; i++) {
+            pptr[i] = (pptr[i] & 0x0000ff00) | ((pptr[i] & 0x00ff0000) >> 16) | ((pptr[i] & 0x000000ff) << 16);
+        }
     }
 
     image->x_size = x;
     image->y_size = y;
     image->pitch = (int)CHECKED_MUL((size_t)x, 4,
-        ({ stbi_image_free(image->img); pmm_free(image, sizeof(struct image)); return NULL; }));
+        ({ free_image_data(image); pmm_free(image, sizeof(struct image)); return NULL; }));
     image->bpp = 32;
     image->img_width = x;
     image->img_height = y;
@@ -66,6 +86,6 @@ struct image *image_open(struct file_handle *file) {
 }
 
 void image_close(struct image *image) {
-    stbi_image_free(image->img);
+    free_image_data(image);
     pmm_free(image, sizeof(struct image));
 }
diff --git a/common/lib/image.h b/common/lib/image.h
index e5cc2961..af0a5c7c 100644
--- a/common/lib/image.h
+++ b/common/lib/image.h
@@ -16,12 +16,11 @@ struct image {
     int64_t x_displacement;
     int64_t y_displacement;
     uint32_t back_colour;
+    char isQoi;
 };
 
 enum {
-    IMAGE_TILED,
-    IMAGE_CENTERED,
-    IMAGE_STRETCHED
+    IMAGE_TILED, IMAGE_CENTERED, IMAGE_STRETCHED
 };
 
 void image_make_centered(struct image *image, int frame_x_size, int frame_y_size, uint32_t back_colour);
diff --git a/common/lib/qoi.c b/common/lib/qoi.c
new file mode 100644
index 00000000..40599cbe
--- /dev/null
+++ b/common/lib/qoi.c
@@ -0,0 +1,98 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <lib/qoi.h>
+#include <lib/misc.h>
+#include <mm/pmm.h>
+
+#define QOI_OP_INDEX 0x00
+#define QOI_OP_DIFF  0x40
+#define QOI_OP_LUMA  0x80
+#define QOI_OP_RUN   0xc0
+#define QOI_OP_RGB   0xfe
+#define QOI_OP_RGBA  0xff
+#define QOI_OP_MASK  0xc0
+#define QOI_HEADER_SIZE  14
+#define QOI_PADDING_SIZE  8
+#define QOI_MAX_DIM    65536u
+#define QOI_MAX_PIXELS ((size_t)400000000)
+#define QOI_HASH(r, g, b, a) \
+    ((((unsigned)(r) * 3u) + ((unsigned)(g) * 5u) + \
+        ((unsigned)(b) * 7u) + ((unsigned)(a) * 11u)) & 63u)
+
+static uint32_t qoi_be32(const uint8_t *p) {
+    return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
+           ((uint32_t)p[2] <<  8) |  (uint32_t)p[3];
+}
+
+/*  The decoded buffer is laid out as [total_bytes][padding to 16][pixels...].
+    The 16-byte header lets qoi_free() recover the original allocation size
+    without the caller having to track it.  */
+static uint32_t *qoi_alloc_xrgb(size_t pixels) {
+    size_t bytes = CHECKED_MUL(pixels, (size_t)4, return NULL);
+    size_t total = CHECKED_ADD(bytes, (size_t)16, return NULL);
+    void *raw = ext_mem_alloc(total);
+    *(size_t *) raw = total;
+    return (uint32_t *)((uint8_t *) raw + 16);
+}
+
+void qoi_free(uint8_t *buf) {
+    if (buf) { uint8_t *raw = buf - 16;  pmm_free(raw, *(size_t *) raw); }
+}
+
+uint8_t *qoi_decode(const void *src, size_t src_size,
+            int *out_w, int *out_h) {
+    if (!src || src_size < QOI_HEADER_SIZE + QOI_PADDING_SIZE) return NULL;
+    const uint8_t * p = src;
+    if (p[0] != 'q' || p[1] != 'o' || p[2] != 'i' || p[3] != 'f')
+        return NULL;
+    uint32_t w = qoi_be32(p + 4), h = qoi_be32(p + 8);
+    uint8_t channels = p[12];  /* p[13] is the colorspace tag, ignored. */
+    if (w == 0 || h == 0 || w > QOI_MAX_DIM || h > QOI_MAX_DIM) return NULL;
+    if (channels != 3 && channels != 4) return NULL;
+    size_t pixels = CHECKED_MUL((size_t)w, (size_t)h, return NULL);
+    if (pixels > QOI_MAX_PIXELS) return NULL;
+    uint32_t *out = qoi_alloc_xrgb(pixels);
+    if (out == NULL) return NULL;
+    uint32_t index[64] = { 0 }, v;  int run = 0, dg;
+    uint8_t r = 0, g = 0, b = 0, a = 0xff, b2;
+    size_t pos = QOI_HEADER_SIZE, end = src_size - QOI_PADDING_SIZE;
+    for (size_t px = 0; px < pixels; px++) {
+        if (run > 0) run--; else {
+            if (pos >= end) goto fail;
+            uint8_t op = p[pos++];
+            if (op == QOI_OP_RGB) {
+                if (end - pos < 3) goto fail;
+                r = p[pos++]; g = p[pos++]; b = p[pos++];
+            } else if (op == QOI_OP_RGBA) {
+                if (end - pos < 4) goto fail;
+                r = p[pos++]; g = p[pos++]; b = p[pos++]; a = p[pos++];
+            } else switch (op & QOI_OP_MASK) {
+                case QOI_OP_INDEX:
+                    v = index[op & 0x3f];
+                    r = v;  g = v >> 8;  b = v >> 16;  a = v >> 24;
+                    break;
+                case QOI_OP_DIFF:
+                    r += (int)((op >> 4) & 3u) - 2;
+                    g += (int)((op >> 2) & 3u) - 2;
+                    b += (int)( op       & 3u) - 2;
+                    break;
+                case QOI_OP_LUMA:
+                    if (pos >= end) goto fail;
+                    b2 = p[pos++];
+                    dg = (int)(op & 0x3f) - 32;
+                    r += dg + (int)((b2 >> 4) & 0xf) - 8;
+                    g += dg;
+                    b += dg + (int)( b2       & 0xf) - 8;
+                    break;
+                case QOI_OP_RUN: run = op & 0x3f; break;
+            }
+            index[QOI_HASH(r, g, b, a)] =
+              (uint32_t)r        | ((uint32_t)g << 8) |
+             ((uint32_t)b << 16) | ((uint32_t)a << 24);
+        }
+        out[px] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
+    }
+    *out_w = (int)w;  *out_h = (int)h;  return (uint8_t *) out;
+fail:
+    qoi_free((uint8_t *) out);  return NULL;
+}
diff --git a/common/lib/qoi.h b/common/lib/qoi.h
new file mode 100644
index 00000000..e9801a64
--- /dev/null
+++ b/common/lib/qoi.h
@@ -0,0 +1,19 @@
+#ifndef LIB__QOI_H__
+#define LIB__QOI_H__
+
+#include <stdint.h>
+#include <stddef.h>
+
+/*  Decodes a QOI image (https://qoiformat.org) from `src` (size `src_size`)
+    into a freshly allocated XRGB8 pixel buffer. Each output pixel is a 32-bit
+    little-endian word laid out as 0x00RRGGBB; the QOI alpha is dropped.
+    On success, returns the buffer and writes the decoded width/height into
+    *out_w / *out_h. Returns NULL on malformed input. The returned buffer
+    must be released with qoi_free().  */
+uint8_t *qoi_decode(const void *src, size_t src_size,
+    int *out_w, int *out_h);
+
+/*  Releases a buffer returned by qoi_decode(). NULL is accepted.  */
+void qoi_free(uint8_t *buf);
+
+#endif
tab: 248 wrap: offon