Skip to content

Commit

Permalink
Add support for QOI file format
Browse files Browse the repository at this point in the history
Allows to decode QOI images.
See https://qoiformat.org for format description.

Resolves #195, #196.

Co-authored-by: sepi <[email protected]>
Co-authored-by: Sashko <[email protected]>
Signed-off-by: Artem Senichev <[email protected]>
  • Loading branch information
3 people committed Oct 12, 2024
1 parent d6c9820 commit 3866207
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
140 changes: 140 additions & 0 deletions src/formats/qoi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: MIT
// QOI format decoder.
// Copyright (C) 2024 Artem Senichev <[email protected]>

#include "../loader.h"

#include <arpa/inet.h>
#include <string.h>

// 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;
}
3 changes: 2 additions & 1 deletion src/loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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. */
Expand Down
Binary file added test/data/image.qoi
Binary file not shown.
1 change: 1 addition & 0 deletions test/loader_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 3866207

Please sign in to comment.