From 7b0e23a87b4d6090da21d3edcb8fe98fd97eebe9 Mon Sep 17 00:00:00 2001 From: Rangi42 Date: Mon, 27 Nov 2023 22:06:05 -0500 Subject: [PATCH] Implement QOI image format --- docs/alpha.md | 1 + docs/constants.md | 1 + docs/formats.md | 14 ++++++++-- header/enum.h | 1 + src/bmpwrite.c | 6 ++-- src/inline.h | 10 +++++++ src/load.c | 2 ++ src/misc.c | 3 +- src/proto.h | 6 ++++ src/qoiread.c | 54 +++++++++++++++++++++++++++++++++++ src/qoiwrite.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++ src/store.c | 1 + src/struct.h | 7 +++++ 13 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 src/qoiread.c create mode 100644 src/qoiwrite.c diff --git a/docs/alpha.md b/docs/alpha.md index 5fb01a6..a846faa6 100644 --- a/docs/alpha.md +++ b/docs/alpha.md @@ -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) diff --git a/docs/constants.md b/docs/constants.md index 0d761b1..f8cdc18 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -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. diff --git a/docs/formats.md b/docs/formats.md index eb2a838..ab376af 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -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 @@ -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. @@ -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) diff --git a/header/enum.h b/header/enum.h index 8763095..b5bab34 100644 --- a/header/enum.h +++ b/header/enum.h @@ -39,6 +39,7 @@ enum plum_image_types { PLUM_IMAGE_APNG, PLUM_IMAGE_JPEG, PLUM_IMAGE_PNM, + PLUM_IMAGE_QOI, PLUM_NUM_IMAGE_TYPES }; diff --git a/src/bmpwrite.c b/src/bmpwrite.c index 3fc431e..327a720 100644 --- a/src/bmpwrite.c +++ b/src/bmpwrite.c @@ -342,13 +342,13 @@ void generate_BMP_RGB_data (struct context * context, unsigned char * offset_poi padding = 4 - (rowsize & 3); rowsize += padding; } - unsigned char * out = append_output_node(context, rowsize * context -> source -> height); + unsigned char * output = append_output_node(context, rowsize * context -> source -> height); uint_fast32_t row = context -> source -> height - 1; do { size_t pos = (size_t) row * context -> source -> width; for (uint_fast32_t remaining = context -> source -> width; remaining; pos ++, remaining --) - out += byteappend(out, data[pos] >> 16, data[pos] >> 8, data[pos]); - for (uint_fast32_t p = 0; p < padding; p ++) *(out ++) = 0; + output += byteappend(output, data[pos] >> 16, data[pos] >> 8, data[pos]); + for (uint_fast32_t p = 0; p < padding; p ++) *(output ++) = 0; } while (row --); if (data != context -> source -> data) ctxfree(context, data); } diff --git a/src/inline.h b/src/inline.h index cc74dfd..d00fd46 100644 --- a/src/inline.h +++ b/src/inline.h @@ -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); diff --git a/src/load.c b/src/load.c index b19d90a..2dba3fc 100644 --- a/src/load.c +++ b/src/load.c @@ -60,6 +60,8 @@ void load_image_buffer_data (struct context * context, unsigned flags, size_t li 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) diff --git a/src/misc.c b/src/misc.c index 0a9a2d8..51e2bb8 100644 --- a/src/misc.c +++ b/src/misc.c @@ -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]; diff --git a/src/proto.h b/src/proto.h index 7d5d2c0..9bfcffc 100644 --- a/src/proto.h +++ b/src/proto.h @@ -340,6 +340,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); diff --git a/src/qoiread.c b/src/qoiread.c new file mode 100644 index 0000000..64e1317 --- /dev/null +++ b/src/qoiread.c @@ -0,0 +1,54 @@ +#include "proto.h" + +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); + uint64_t * frame = ctxmalloc(context, sizeof *frame * context -> source -> width * context -> source -> height); + const unsigned char * data = context -> data + 14; + const unsigned char * dataend = context -> data + context -> size - 22; + struct QOI_pixel lookup[64] = {0}; + struct QOI_pixel px = {.r = 0, .g = 0, .b = 0, .a = 0xff}; + unsigned char run = 0; + for (uint_fast64_t cell = 0; cell < context -> source -> width * context -> source -> height; cell ++) { + if (run > 0) + run --; + else 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) + 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 ++); + int_fast16_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) + run = v & 0x3f; + else + throw(context, PLUM_ERR_INVALID_FILE_FORMAT); + lookup[(px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11) % sizeof lookup] = px; + } + frame[cell] = (((uint64_t) px.a << 48) | ((uint64_t) px.b << 32) | ((uint64_t) px.g << 16) | (uint64_t) px.r) * 0x101; + } + write_framebuffer_to_image(context -> image, frame, 0, flags); + ctxfree(context, frame); +} diff --git a/src/qoiwrite.c b/src/qoiwrite.c new file mode 100644 index 0000000..d9e6c09 --- /dev/null +++ b/src/qoiwrite.c @@ -0,0 +1,71 @@ +#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); + header[12] = channels; + header[13] = 0; + uint32_t * data; + if ((context -> source -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32) + data = context -> source -> data; + else { + data = ctxmalloc(context, sizeof *data * context -> source -> width * context -> source -> height); + plum_convert_colors(data, context -> source -> data, (size_t) context -> source -> width * context -> source -> height, + PLUM_COLOR_32, context -> source -> color_format); + } + size_t max_size = context -> source -> width * context -> source -> height * (channels + 1) + 22; + unsigned char * node = append_output_node(context, max_size); + unsigned char * output = node; + struct QOI_pixel lookup[64] = {0}; + struct QOI_pixel px = {.r = 0, .g = 0, .b = 0, .a = 0xff}; + struct QOI_pixel prev = px; + unsigned char run = 0; + for (uint_fast64_t cell = 0; cell < context -> source -> width * context -> source -> height; cell ++) { + px.r = data[cell]; + px.g = data[cell] >> 8; + px.b = data[cell] >> 16; + px.a = data[cell] >> 24; + if (equalpixels(px, prev)) { + run ++; + if (run == 62 || cell == context -> source -> width * context -> source -> height - 1) { + *(output ++) = 0xc0 | (run - 1); + run = 0; + } + } else { + if (run > 0) { + *(output ++) = 0xc0 | (run - 1); + run = 0; + } + uint8_t index = (px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11) % 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 diff --git a/src/store.c b/src/store.c index 9e3ccef..9809676 100644 --- a/src/store.c +++ b/src/store.c @@ -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); diff --git a/src/struct.h b/src/struct.h index 4468c67..fd09c17 100644 --- a/src/struct.h +++ b/src/struct.h @@ -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; +};