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
