diff --git a/README.md b/README.md index c5ca32f..11b6103 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ Fully customizable and lightweight image viewer for Wayland based display server - EXR (via [OpenEXR](https://openexr.com)); - BMP (built-in); - PNM (built-in); - - TGA (built-in). + - TGA (built-in); + - QOI (built-in). - Fully customizable keyboard bindings, colors, and [many other](https://github.com/artemsen/swayimg/blob/master/extra/swayimgrc) parameters; - Loading images from files and pipes; - Gallery and viewer modes with slideshow and animation support; diff --git a/meson.build b/meson.build index a4c9397..2864d52 100644 --- a/meson.build +++ b/meson.build @@ -190,6 +190,7 @@ sources = [ 'src/viewer.c', 'src/formats/bmp.c', 'src/formats/pnm.c', + 'src/formats/qoi.c', 'src/formats/tga.c', xdg_shell_h, xdg_shell_c, diff --git a/src/formats/qoi.c b/src/formats/qoi.c new file mode 100644 index 0000000..4f451f7 --- /dev/null +++ b/src/formats/qoi.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +// QOI format decoder. +// Copyright (C) 2024 Artem Senichev + +#include "../loader.h" + +#include +#include + +// Chunk tags +#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 + +// Mask of second byte in steram +#define QOI_MASK_2 0xc0 + +// Size of color map +#define QOI_CLRMAP_SIZE 64 +// Calc color index in map +#define QOI_CLRMAP_INDEX(r, g, b, a) \ + ((r * 3 + g * 5 + b * 7 + a * 11) % QOI_CLRMAP_SIZE) + +// QOI signature +static const uint8_t signature[] = { 'q', 'o', 'i', 'f' }; + +// QOI file header +struct __attribute__((__packed__)) qoi_header { + uint8_t magic[4]; // Magic bytes "qoif" + uint32_t width; // Image width in pixels + uint32_t height; // Image height in pixels + uint8_t channels; // Number of color channels: 3 = RGB, 4 = RGBA + uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear +}; + +// QOI loader implementation +enum loader_status decode_qoi(struct image* ctx, const uint8_t* data, + size_t size) +{ + const struct qoi_header* qoi = (const struct qoi_header*)data; + argb_t color_map[QOI_CLRMAP_SIZE]; + struct pixmap* pm; + uint8_t a, r, g, b; + size_t total_pixels; + size_t rlen; + size_t pos; + + // check signature + if (size < sizeof(*qoi) || + memcmp(qoi->magic, signature, sizeof(signature))) { + return ldr_unsupported; + } + // check format + if (qoi->width == 0 || qoi->height == 0 || qoi->channels < 3 || + qoi->channels > 4) { + return ldr_fmterror; + } + + // allocate image buffer + pm = image_allocate_frame(ctx, htonl(qoi->width), htonl(qoi->height)); + if (!pm) { + return ldr_fmterror; + } + + // initialize decoder state + r = 0; + g = 0; + b = 0; + a = 0xff; + rlen = 0; + pos = sizeof(struct qoi_header); + total_pixels = pm->width * pm->height; + memset(color_map, 0, sizeof(color_map)); + + // decode image + for (size_t i = 0; i < total_pixels; ++i) { + if (rlen > 0) { + --rlen; + } else { + uint8_t tag; + if (pos >= size) { + break; + } + tag = data[pos++]; + + if (tag == QOI_OP_RGB) { + if (pos + 3 >= size) { + goto fail; + } + r = data[pos++]; + g = data[pos++]; + b = data[pos++]; + } else if (tag == QOI_OP_RGBA) { + if (pos + 4 >= size) { + goto fail; + } + r = data[pos++]; + g = data[pos++]; + b = data[pos++]; + a = data[pos++]; + } else if ((tag & QOI_MASK_2) == QOI_OP_INDEX) { + const argb_t clr = color_map[tag & 0x3f]; + a = ARGB_GET_A(clr); + r = ARGB_GET_R(clr); + g = ARGB_GET_G(clr); + b = ARGB_GET_B(clr); + } else if ((tag & QOI_MASK_2) == QOI_OP_DIFF) { + r += (int8_t)((tag >> 4) & 3) - 2; + g += (int8_t)((tag >> 2) & 3) - 2; + b += (int8_t)(tag & 3) - 2; + } else if ((tag & QOI_MASK_2) == QOI_OP_LUMA) { + uint8_t diff; + int8_t diff_green; + if (pos + 1 >= size) { + goto fail; + } + diff = data[pos++]; + diff_green = (int8_t)(tag & 0x3f) - 32; + r += diff_green - 8 + ((diff >> 4) & 0x0f); + g += diff_green; + b += diff_green - 8 + (diff & 0x0f); + } else if ((tag & QOI_MASK_2) == QOI_OP_RUN) { + rlen = (tag & 0x3f); + } + color_map[QOI_CLRMAP_INDEX(r, g, b, a)] = ARGB(a, r, g, b); + } + pm->data[i] = ARGB(a, r, g, b); + } + + image_set_format(ctx, "QOI %dbpp", qoi->channels * 8); + ctx->alpha = (qoi->channels == 4); + return ldr_success; + +fail: + image_free_frames(ctx); + return ldr_fmterror; +} diff --git a/src/loader.c b/src/loader.c index 52a5302..ae8b4d2 100644 --- a/src/loader.c +++ b/src/loader.c @@ -68,6 +68,7 @@ const char* supported_formats = "bmp, pnm, tga" // declaration of loaders LOADER_DECLARE(bmp); LOADER_DECLARE(pnm); +LOADER_DECLARE(qoi); LOADER_DECLARE(tga); #ifdef HAVE_LIBEXR LOADER_DECLARE(exr); @@ -133,7 +134,7 @@ static const image_decoder decoders[] = { #ifdef HAVE_LIBTIFF &LOADER_FUNCTION(tiff), #endif - &LOADER_FUNCTION(tga), + &LOADER_FUNCTION(qoi), &LOADER_FUNCTION(tga), }; /** Background thread loader queue. */ diff --git a/test/data/image.qoi b/test/data/image.qoi new file mode 100644 index 0000000..24ad6e2 Binary files /dev/null and b/test/data/image.qoi differ diff --git a/test/loader_test.cpp b/test/loader_test.cpp index 4af63c1..0bbc306 100644 --- a/test/loader_test.cpp +++ b/test/loader_test.cpp @@ -58,6 +58,7 @@ TEST_F(Loader, External) TEST_LOADER(bmp); TEST_LOADER(pnm); +TEST_LOADER(qoi); TEST_LOADER(tga); #ifdef HAVE_LIBEXR // TEST_LOADER(exr); diff --git a/test/meson.build b/test/meson.build index 1700d01..12b6aac 100644 --- a/test/meson.build +++ b/test/meson.build @@ -19,6 +19,7 @@ sources = [ '../src/pixmap.c', '../src/formats/bmp.c', '../src/formats/pnm.c', + '../src/formats/qoi.c', '../src/formats/tga.c', ] if exif.found()