Skip to content

Commit 80e87ff

Browse files
committed
Implement QOI image format
1 parent a809dea commit 80e87ff

13 files changed

+171
-6
lines changed

docs/alpha.md

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Ordering is pure ASCII (that is, all uppercase letters come before all lowercase
6868
- [`PLUM_IMAGE_NONE` constant](constants.md#image-types)
6969
- [`PLUM_IMAGE_PNG` constant](constants.md#image-types)
7070
- [`PLUM_IMAGE_PNM` constant](constants.md#image-types)
71+
- [`PLUM_IMAGE_QOI` constant](constants.md#image-types)
7172
- [`PLUM_MAX_MEMORY_SIZE` constant](constants.md#special-loading-and-storing-modes)
7273
- [`PLUM_METADATA_BACKGROUND` constant](constants.md#metadata-node-types)
7374
- [`PLUM_METADATA_COLOR_DEPTH` constant](constants.md#metadata-node-types)

docs/constants.md

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ image used (when loading it) or will use (when storing it).
6666
- `PLUM_IMAGE_JPEG`: JPEG (Joint Photographers Expert Group) file.
6767
- `PLUM_IMAGE_PNM`: netpbm's PNM (Portable Anymap) format.
6868
When loading, it represents any possible PNM file; however, only PPM and PAM files will be written.
69+
- `PLUM_IMAGE_QOI`: QOI (Quite OK Image) file.
6970

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

docs/formats.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All supported formats are documented here, along with their restrictions and som
1313
- [APNG](#apng)
1414
- [JPEG](#jpeg)
1515
- [PNM](#pnm)
16+
- [QOI](#qoi)
1617

1718
## Definitions
1819

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

70-
If an image has a [true bit depth](#definitions) of 8 or less and it doesn't use transparency, the usual RGB888 format
71-
is used for output.
71+
If an image has a [true bit depth](#definitions) of 8 or less and it doesn't use transparency, 8-bit RGB components
72+
are used for output.
7273
Otherwise, variable bit masks are used, with each component having the width determined by its true bit depth.
7374
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
7475
components exceeds 32, they are proportionally reduced to fit.
@@ -341,6 +342,15 @@ All [animation-related metadata][animation] will be ignored when generating a PN
341342

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

345+
## QOI
346+
347+
The Quite OK Image (QOI) format version 1.0 is supported.
348+
349+
QOI files only support a single frame; attempting to generate a file with two or more frames will fail with
350+
[`PLUM_ERR_NO_MULTI_FRAME`][errors].
351+
352+
QOI files don't support palettes; [indexed-color mode images][indexed] will be converted when a QOI file is generated.
353+
344354
* * *
345355

346356
Prev: [C++ helper methods](methods.md)

header/enum.h

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ enum plum_image_types {
3939
PLUM_IMAGE_APNG,
4040
PLUM_IMAGE_JPEG,
4141
PLUM_IMAGE_PNM,
42+
PLUM_IMAGE_QOI,
4243
PLUM_NUM_IMAGE_TYPES
4344
};
4445

src/bmpwrite.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,13 @@ void generate_BMP_RGB_data (struct context * context, unsigned char * offset_poi
342342
padding = 4 - (rowsize & 3);
343343
rowsize += padding;
344344
}
345-
unsigned char * out = append_output_node(context, rowsize * context -> source -> height);
345+
unsigned char * output = append_output_node(context, rowsize * context -> source -> height);
346346
uint_fast32_t row = context -> source -> height - 1;
347347
do {
348348
size_t pos = (size_t) row * context -> source -> width;
349349
for (uint_fast32_t remaining = context -> source -> width; remaining; pos ++, remaining --)
350-
out += byteappend(out, data[pos] >> 16, data[pos] >> 8, data[pos]);
351-
for (uint_fast32_t p = 0; p < padding; p ++) *(out ++) = 0;
350+
output += byteappend(output, data[pos] >> 16, data[pos] >> 8, data[pos]);
351+
for (uint_fast32_t p = 0; p < padding; p ++) *(output ++) = 0;
352352
} while (row --);
353353
if (data != context -> source -> data) ctxfree(context, data);
354354
}

src/inline.h

+10
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ static inline void * append_output_node (struct context * context, size_t size)
5151
return node -> data;
5252
}
5353

54+
static inline void * resize_output_node (struct context * context, void * data, size_t size) {
55+
struct data_node * node = (struct data_node *) ((char *) data - offsetof(struct data_node, data));
56+
if (node -> size != size) {
57+
node = ctxrealloc(context, node, sizeof *node + size);
58+
node -> size = size;
59+
if (node -> previous) node -> previous -> next = node;
60+
}
61+
return node -> data;
62+
}
63+
5464
static inline bool bit_depth_less_than (uint32_t depth, uint32_t target) {
5565
// formally "less than or equal to", but that would be a very long name
5666
return !((target - depth) & 0x80808080u);

src/load.c

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ void load_image_buffer_data (struct context * context, unsigned flags, size_t li
6060
else if (bytematch(context -> data, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
6161
// APNG files disguise as PNG files, so handle them all as PNG and split them later
6262
load_PNG_data(context, flags, limit);
63+
else if (bytematch(context -> data, 0x71, 0x6f, 0x69, 0x66))
64+
load_QOI_data(context, flags, limit);
6365
else if (*context -> data == 0x50 && context -> data[1] >= 0x31 && context -> data[1] <= 0x37)
6466
load_PNM_data(context, flags, limit);
6567
else if (bytematch(context -> data, 0xef, 0xbb, 0xbf, 0x50) && context -> data[4] >= 0x31 && context -> data[4] <= 0x37)

src/misc.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ const char * plum_get_file_format_name (unsigned format) {
7171
[PLUM_IMAGE_PNG] = "PNG",
7272
[PLUM_IMAGE_APNG] = "APNG",
7373
[PLUM_IMAGE_JPEG] = "JPEG",
74-
[PLUM_IMAGE_PNM] = "PNM"
74+
[PLUM_IMAGE_PNM] = "PNM",
75+
[PLUM_IMAGE_QOI] = "QOI"
7576
};
7677
if (format >= PLUM_NUM_IMAGE_TYPES) format = PLUM_IMAGE_NONE;
7778
return formats[format];

src/proto.h

+6
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,12 @@ internal size_t write_PNM_number(unsigned char * restrict, uint32_t);
340340
internal void generate_PNM_frame_data(struct context *, const uint64_t *, uint32_t, uint32_t, unsigned, bool);
341341
internal void generate_PNM_frame_data_from_palette(struct context *, const uint8_t *, const uint64_t *, uint32_t, uint32_t, unsigned, bool);
342342

343+
// qoiread.c
344+
internal void load_QOI_data(struct context *, unsigned, size_t);
345+
346+
// qoiwrite.c
347+
internal void generate_QOI_data(struct context *);
348+
343349
// sort.c
344350
internal void sort_values(uint64_t * restrict, uint64_t);
345351
internal void quicksort_values(uint64_t * restrict, uint64_t);

src/qoiread.c

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#include "proto.h"
2+
3+
void load_QOI_data (struct context * context, unsigned flags, size_t limit) {
4+
if (context -> size < 22) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
5+
context -> image -> type = PLUM_IMAGE_QOI;
6+
context -> image -> frames = 1;
7+
context -> image -> width = read_be32_unaligned(context -> data + 4);
8+
context -> image -> height = read_be32_unaligned(context -> data + 8);
9+
validate_image_size(context, limit);
10+
allocate_framebuffers(context, flags, false);
11+
add_color_depth_metadata(context, 8, 8, 8, 8, 0);
12+
uint64_t * frame = ctxmalloc(context, sizeof *frame * context -> source -> width * context -> source -> height);
13+
const unsigned char * data = context -> data + 14;
14+
const unsigned char * dataend = context -> data + context -> size - 22;
15+
struct QOI_pixel lookup[64] = {0};
16+
struct QOI_pixel px = {.r = 0, .g = 0, .b = 0, .a = 0xff};
17+
unsigned char run = 0;
18+
for (uint_fast64_t cell = 0; cell < context -> source -> width * context -> source -> height; cell ++) {
19+
if (run > 0)
20+
run --;
21+
else if (data + 1 < dataend) {
22+
unsigned char v = *(data ++);
23+
if (v == 0xfe && data + 3 < dataend) {
24+
px.r = *(data ++);
25+
px.g = *(data ++);
26+
px.b = *(data ++);
27+
} else if (v == 0xff && data + 4 < dataend) {
28+
px.r = *(data ++);
29+
px.g = *(data ++);
30+
px.b = *(data ++);
31+
px.a = *(data ++);
32+
} else if (!(v & 0xc0) && v < sizeof lookup)
33+
px = lookup[v];
34+
else if ((v & 0xc0) == 0x40) {
35+
px.r += ((v >> 4) & 3) - 2;
36+
px.g += ((v >> 2) & 3) - 2;
37+
px.b += (v & 3) - 2;
38+
} else if ((v & 0xc0) == 0x80 && data + 1 < dataend) {
39+
unsigned char v2 = *(data ++);
40+
int_fast16_t dg = (v & 0x3f) - 32;
41+
px.r += dg + ((v2 >> 4) & 0xf) - 8;
42+
px.g += dg;
43+
px.b += dg + (v2 & 0xf) - 8;
44+
} else if ((v & 0xc0) == 0xc0)
45+
run = v & 0x3f;
46+
else
47+
throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
48+
lookup[(px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11) % sizeof lookup] = px;
49+
}
50+
frame[cell] = (((uint64_t) (px.a ^ 0xff) << 48) | ((uint64_t) px.b << 32) | ((uint64_t) px.g << 16) | (uint64_t) px.r) * 0x101;
51+
}
52+
write_framebuffer_to_image(context -> image, frame, 0, flags);
53+
ctxfree(context, frame);
54+
}

src/qoiwrite.c

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#include "proto.h"
2+
3+
#define equalpixels(p1, p2) ((p1).r == (p2).r && (p1).g == (p2).g && (p1).b == (p2).b && (p1).a == (p2).a)
4+
5+
void generate_QOI_data (struct context * context) {
6+
if (context -> source -> frames > 1) throw(context, PLUM_ERR_NO_MULTI_FRAME);
7+
if (!(context -> source -> width && context -> source -> height)) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
8+
unsigned char * header = append_output_node(context, 14);
9+
bytewrite(header, 0x71, 0x6f, 0x69, 0x66);
10+
write_be32_unaligned(header + 4, context -> source -> width);
11+
write_be32_unaligned(header + 8, context -> source -> height);
12+
uint8_t channels = 3 + image_has_transparency(context -> source);
13+
header[12] = channels;
14+
header[13] = 0;
15+
uint32_t * data;
16+
if ((context -> source -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32)
17+
data = context -> source -> data;
18+
else {
19+
data = ctxmalloc(context, sizeof *data * context -> source -> width * context -> source -> height);
20+
plum_convert_colors(data, context -> source -> data, (size_t) context -> source -> width * context -> source -> height,
21+
PLUM_COLOR_32, context -> source -> color_format);
22+
}
23+
size_t max_size = context -> source -> width * context -> source -> height * (channels + 1) + 22;
24+
unsigned char * node = append_output_node(context, max_size);
25+
unsigned char * output = node;
26+
struct QOI_pixel lookup[64] = {0};
27+
struct QOI_pixel px = {.r = 0, .g = 0, .b = 0, .a = 0xff};
28+
struct QOI_pixel prev = px;
29+
unsigned char run = 0;
30+
for (uint_fast64_t cell = 0; cell < context -> source -> width * context -> source -> height; cell ++) {
31+
px.r = data[cell];
32+
px.g = data[cell] >> 8;
33+
px.b = data[cell] >> 16;
34+
px.a = (data[cell] >> 24) ^ 0xff;
35+
if (equalpixels(px, prev)) {
36+
run ++;
37+
if (run == 62 || cell == context -> source -> width * context -> source -> height - 1) {
38+
*(output ++) = 0xc0 | (run - 1);
39+
run = 0;
40+
}
41+
} else {
42+
if (run > 0) {
43+
*(output ++) = 0xc0 | (run - 1);
44+
run = 0;
45+
}
46+
uint8_t index = (px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11) % sizeof lookup;
47+
if (equalpixels(px, lookup[index]))
48+
*(output ++) = index;
49+
else {
50+
lookup[index] = px;
51+
if (px.a == prev.a) {
52+
int8_t dr = px.r - prev.r, dg = px.g - prev.g, db = px.b - prev.b;
53+
int8_t drg = dr - dg, dbg = db - dg;
54+
if (dr >= -2 && dr < 2 && dg >= -2 && dg < 2 && db >= -2 && db < 2)
55+
*(output ++) = 0x40 | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2);
56+
else if (drg >= -8 && drg < 8 && dg >= -32 && dg < 32 && dbg >= -8 && dbg < 8)
57+
output += byteappend(output, 0x80 | (dg + 32), ((drg + 8) << 4) | (dbg + 8));
58+
else
59+
output += byteappend(output, 0xfe, px.r, px.g, px.b);
60+
} else
61+
output += byteappend(output, 0xff, px.r, px.g, px.b, px.a);
62+
}
63+
}
64+
prev = px;
65+
}
66+
output += byteappend(output, 0, 0, 0, 0, 0, 0, 0, 1);
67+
resize_output_node(context, node, output - node);
68+
if (data != context -> source -> data) ctxfree(context, data);
69+
}
70+
71+
#undef equalpixels

src/store.c

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ size_t plum_store_image (const struct plum_image * image, void * restrict buffer
1919
case PLUM_IMAGE_APNG: generate_APNG_data(context); break;
2020
case PLUM_IMAGE_JPEG: generate_JPEG_data(context); break;
2121
case PLUM_IMAGE_PNM: generate_PNM_data(context); break;
22+
case PLUM_IMAGE_QOI: generate_QOI_data(context); break;
2223
default: throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
2324
}
2425
size_t output_size = get_total_output_size(context);

src/struct.h

+7
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,10 @@ struct PNM_image_header {
147147
size_t datastart;
148148
size_t datalength;
149149
};
150+
151+
struct QOI_pixel {
152+
uint8_t r;
153+
uint8_t g;
154+
uint8_t b;
155+
uint8_t a;
156+
};

0 commit comments

Comments
 (0)