Skip to content

Commit

Permalink
Implement QOI image format
Browse files Browse the repository at this point in the history
  • Loading branch information
Rangi42 committed Nov 29, 2023
1 parent ce24a22 commit cc8913f
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/alpha.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Ordering is pure ASCII (that is, all uppercase letters come before all lowercase
- [`PLUM_IMAGE_NONE` constant](constants.md#image-types)
- [`PLUM_IMAGE_PNG` constant](constants.md#image-types)
- [`PLUM_IMAGE_PNM` constant](constants.md#image-types)
- [`PLUM_IMAGE_QOI` constant](constants.md#image-types)
- [`PLUM_MAX_MEMORY_SIZE` constant](constants.md#special-loading-and-storing-modes)
- [`PLUM_METADATA_BACKGROUND` constant](constants.md#metadata-node-types)
- [`PLUM_METADATA_COLOR_DEPTH` constant](constants.md#metadata-node-types)
Expand Down
1 change: 1 addition & 0 deletions docs/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ image used (when loading it) or will use (when storing it).
- `PLUM_IMAGE_JPEG`: JPEG (Joint Photographers Expert Group) file.
- `PLUM_IMAGE_PNM`: netpbm's PNM (Portable Anymap) format.
When loading, it represents any possible PNM file; however, only PPM and PAM files will be written.
- `PLUM_IMAGE_QOI`: QOI (Quite OK Image) file.

For more information, see the [Supported file formats][formats] page.

Expand Down
14 changes: 12 additions & 2 deletions docs/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ All supported formats are documented here, along with their restrictions and som
- [APNG](#apng)
- [JPEG](#jpeg)
- [PNM](#pnm)
- [QOI](#qoi)

## Definitions

Expand Down Expand Up @@ -67,8 +68,8 @@ The maximum width and height for an image is `0x7fffffff`; larger dimensions wil
Images using [indexed-color mode][indexed] are fully supported, but they cannot contain transparency.
Therefore, if an image uses transparency, the file will be generated without a palette.

If an image has a [true bit depth](#definitions) of 8 or less and it doesn't use transparency, the usual RGB888 format
is used for output.
If an image has a [true bit depth](#definitions) of 8 or less and it doesn't use transparency, 8-bit RGB components
are used for output.
Otherwise, variable bit masks are used, with each component having the width determined by its true bit depth.
Variable bit width files are limited to a total of 32 bits per color, so if the sum of the true bit depths for all
components exceeds 32, they are proportionally reduced to fit.
Expand Down Expand Up @@ -341,6 +342,15 @@ All [animation-related metadata][animation] will be ignored when generating a PN

PNM files don't support palettes; [indexed-color mode images][indexed] will be converted when a PNM file is generated.

## QOI

The Quite OK Image (QOI) format version 1.0 is supported.

QOI files only support a single frame; attempting to generate a file with two or more frames will fail with
[`PLUM_ERR_NO_MULTI_FRAME`][errors].

QOI files don't support palettes; [indexed-color mode images][indexed] will be converted when a QOI file is generated.

* * *

Prev: [C++ helper methods](methods.md)
Expand Down
1 change: 1 addition & 0 deletions header/enum.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ enum plum_image_types {
PLUM_IMAGE_APNG,
PLUM_IMAGE_JPEG,
PLUM_IMAGE_PNM,
PLUM_IMAGE_QOI,
PLUM_NUM_IMAGE_TYPES
};

Expand Down
10 changes: 10 additions & 0 deletions src/inline.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ static inline void * append_output_node (struct context * context, size_t size)
return node -> data;
}

static inline void * resize_output_node (struct context * context, void * data, size_t size) {
struct data_node * node = (struct data_node *) ((char *) data - offsetof(struct data_node, data));
if (node -> size != size) {
node = ctxrealloc(context, node, sizeof *node + size);
node -> size = size;
if (node -> previous) node -> previous -> next = node;
}
return node -> data;
}

static inline bool bit_depth_less_than (uint32_t depth, uint32_t target) {
// formally "less than or equal to", but that would be a very long name
return !((target - depth) & 0x80808080u);
Expand Down
2 changes: 2 additions & 0 deletions src/load.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ void load_image_buffer_data (struct context * context, unsigned long flags, size
else if (bytematch(context -> data, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
// APNG files disguise as PNG files, so handle them all as PNG and split them later
load_PNG_data(context, flags, limit);
else if (bytematch(context -> data, 0x71, 0x6f, 0x69, 0x66))
load_QOI_data(context, flags, limit);
else if (*context -> data == 0x50 && context -> data[1] >= 0x31 && context -> data[1] <= 0x37)
load_PNM_data(context, flags, limit);
else if (bytematch(context -> data, 0xef, 0xbb, 0xbf, 0x50) && context -> data[4] >= 0x31 && context -> data[4] <= 0x37)
Expand Down
3 changes: 2 additions & 1 deletion src/misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ const char * plum_get_file_format_name (unsigned format) {
[PLUM_IMAGE_PNG] = "PNG",
[PLUM_IMAGE_APNG] = "APNG",
[PLUM_IMAGE_JPEG] = "JPEG",
[PLUM_IMAGE_PNM] = "PNM"
[PLUM_IMAGE_PNM] = "PNM",
[PLUM_IMAGE_QOI] = "QOI"
};
if (format >= PLUM_NUM_IMAGE_TYPES) format = PLUM_IMAGE_NONE;
return formats[format];
Expand Down
6 changes: 6 additions & 0 deletions src/proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ internal size_t write_PNM_number(unsigned char * restrict, uint32_t);
internal void generate_PNM_frame_data(struct context *, const uint64_t *, uint32_t, uint32_t, unsigned, bool);
internal void generate_PNM_frame_data_from_palette(struct context *, const uint8_t *, const uint64_t *, uint32_t, uint32_t, unsigned, bool);

// qoiread.c
internal void load_QOI_data(struct context *, unsigned, size_t);

// qoiwrite.c
internal void generate_QOI_data(struct context *);

// sort.c
internal void sort_values(uint64_t * restrict, uint64_t);
internal void quicksort_values(uint64_t * restrict, uint64_t);
Expand Down
59 changes: 59 additions & 0 deletions src/qoiread.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include "proto.h"

#define pixelcolor(px) (((uint32_t) (px).a << 24) | ((uint32_t) (px).b << 16) | ((uint32_t) (px).g << 8) | (uint32_t) (px).r)

void load_QOI_data (struct context * context, unsigned flags, size_t limit) {
if (context -> size < 22) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
context -> image -> type = PLUM_IMAGE_QOI;
context -> image -> frames = 1;
context -> image -> width = read_be32_unaligned(context -> data + 4);
context -> image -> height = read_be32_unaligned(context -> data + 8);
validate_image_size(context, limit);
allocate_framebuffers(context, flags, false);
add_color_depth_metadata(context, 8, 8, 8, 8, 0);
size_t framesize = (size_t) context -> image -> width * context -> image -> height;
uint32_t * frame = ctxmalloc(context, sizeof *frame * framesize);
const unsigned char * data = context -> data + 14;
const unsigned char * dataend = context -> data + context -> size;
struct QOI_pixel lookup[64] = {0};
struct QOI_pixel px = {.r = 0, .g = 0, .b = 0, .a = 0xff};
for (size_t cell = 0; cell < framesize; cell ++) {
if (data + 1 < dataend) {
unsigned char v = *(data ++);
if (v == 0xfe && data + 3 < dataend) {
px.r = *(data ++);
px.g = *(data ++);
px.b = *(data ++);
} else if (v == 0xff && data + 4 < dataend) {
px.r = *(data ++);
px.g = *(data ++);
px.b = *(data ++);
px.a = *(data ++);
} else if (!(v & 0xc0) && v < sizeof lookup / sizeof *lookup)
px = lookup[v];
else if ((v & 0xc0) == 0x40) {
px.r += ((v >> 4) & 3) - 2;
px.g += ((v >> 2) & 3) - 2;
px.b += (v & 3) - 2;
} else if ((v & 0xc0) == 0x80 && data + 1 < dataend) {
unsigned char v2 = *(data ++);
int8_t dg = (v & 0x3f) - 32;
px.r += dg + ((v2 >> 4) & 0xf) - 8;
px.g += dg;
px.b += dg + (v2 & 0xf) - 8;
} else if ((v & 0xc0) == 0xc0) {
uint8_t run = v & 0x3f;
if (cell + run >= framesize) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
uint32_t color = pixelcolor(px);
while (run --) frame[cell ++] = color;
} else
throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
lookup[(px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11) % (sizeof lookup / sizeof *lookup)] = px;
}
frame[cell] = pixelcolor(px);
}
plum_convert_colors(context -> image -> data8, frame, framesize, flags, PLUM_COLOR_32 | PLUM_ALPHA_INVERT);
ctxfree(context, frame);
}

#undef pixelcolor
65 changes: 65 additions & 0 deletions src/qoiwrite.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#include "proto.h"

#define equalpixels(p1, p2) ((p1).r == (p2).r && (p1).g == (p2).g && (p1).b == (p2).b && (p1).a == (p2).a)

void generate_QOI_data (struct context * context) {
if (context -> source -> frames > 1) throw(context, PLUM_ERR_NO_MULTI_FRAME);
if (!(context -> source -> width && context -> source -> height)) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
unsigned char * header = append_output_node(context, 14);
bytewrite(header, 0x71, 0x6f, 0x69, 0x66);
write_be32_unaligned(header + 4, context -> source -> width);
write_be32_unaligned(header + 8, context -> source -> height);
uint8_t channels = 3 + image_has_transparency(context -> source);
bytewrite(header + 12, channels, 0);
size_t framesize = (size_t) context -> source -> width * context -> source -> height;
uint32_t * data;
if ((context -> source -> color_format & (PLUM_COLOR_MASK | PLUM_ALPHA_INVERT)) == (PLUM_COLOR_32 | PLUM_ALPHA_INVERT))
data = context -> source -> data;
else {
data = ctxmalloc(context, sizeof *data * framesize);
plum_convert_colors(data, context -> source -> data, framesize, PLUM_COLOR_32 | PLUM_ALPHA_INVERT, context -> source -> color_format);
}
unsigned char * node = append_output_node(context, framesize * (channels + 1) + 8);
unsigned char * output = node;
struct QOI_pixel lookup[64] = {0};
struct QOI_pixel prev = {.r = 0, .g = 0, .b = 0, .a = 0xff};
uint8_t run = 0;
for (size_t cell = 0; cell < framesize; cell ++) {
struct QOI_pixel px = {.r = data[cell], .g = data[cell] >> 8, .b = data[cell] >> 16, .a = data[cell] >> 24};
if (equalpixels(px, prev)) {
run ++;
if (run == 62 || cell == framesize - 1) {
*(output ++) = 0xc0 | (run - 1);
run = 0;
}
} else {
if (run) {
*(output ++) = 0xc0 | (run - 1);
run = 0;
}
uint8_t index = (px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11) % (sizeof lookup / sizeof *lookup);
if (equalpixels(px, lookup[index]))
*(output ++) = index;
else {
lookup[index] = px;
if (px.a == prev.a) {
int8_t dr = px.r - prev.r, dg = px.g - prev.g, db = px.b - prev.b;
int8_t drg = dr - dg, dbg = db - dg;
if (dr >= -2 && dr < 2 && dg >= -2 && dg < 2 && db >= -2 && db < 2)
*(output ++) = 0x40 | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2);
else if (drg >= -8 && drg < 8 && dg >= -32 && dg < 32 && dbg >= -8 && dbg < 8)
output += byteappend(output, 0x80 | (dg + 32), ((drg + 8) << 4) | (dbg + 8));
else
output += byteappend(output, 0xfe, px.r, px.g, px.b);
} else
output += byteappend(output, 0xff, px.r, px.g, px.b, px.a);
}
prev = px;
}
}
output += byteappend(output, 0, 0, 0, 0, 0, 0, 0, 1);
resize_output_node(context, node, output - node);
if (data != context -> source -> data) ctxfree(context, data);
}

#undef equalpixels
1 change: 1 addition & 0 deletions src/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ size_t plum_store_image (const struct plum_image * image, void * restrict buffer
case PLUM_IMAGE_APNG: generate_APNG_data(context); break;
case PLUM_IMAGE_JPEG: generate_JPEG_data(context); break;
case PLUM_IMAGE_PNM: generate_PNM_data(context); break;
case PLUM_IMAGE_QOI: generate_QOI_data(context); break;
default: throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
}
size_t output_size = get_total_output_size(context);
Expand Down
7 changes: 7 additions & 0 deletions src/struct.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,10 @@ struct PNM_image_header {
size_t datastart;
size_t datalength;
};

struct QOI_pixel {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};

0 comments on commit cc8913f

Please sign in to comment.