From 75816510111470d3f0fd6e64f5d7146d8fbd05d5 Mon Sep 17 00:00:00 2001 From: Hema2 <49586027+Hema2-official@users.noreply.github.com> Date: Sat, 7 Sep 2024 15:55:26 +0200 Subject: [PATCH] Add QOI to the standard library (#1409) Add QOI to the standard library --- lib/std/compression/qoi.c3 | 476 ++++++++++++++++++++++++++++ releasenotes.md | 1 + test/unit/stdlib/compression/qoi.c3 | 34 ++ 3 files changed, 511 insertions(+) create mode 100644 lib/std/compression/qoi.c3 create mode 100644 test/unit/stdlib/compression/qoi.c3 diff --git a/lib/std/compression/qoi.c3 b/lib/std/compression/qoi.c3 new file mode 100644 index 000000000..5ced08b72 --- /dev/null +++ b/lib/std/compression/qoi.c3 @@ -0,0 +1,476 @@ +module std::compression::qoi; + +const uint PIXELS_MAX = 400000000; + +/** + * Colorspace. + * Purely informative. It will be saved to the file header, + * but does not affect how chunks are en-/decoded. + */ +enum Colorspace : char (char id) +{ + SRGB = 0, // sRGB with linear alpha + LINEAR = 1 // all channels linear +} + +/** + * Channels. + * The channels used in an image. + * AUTO can be used when decoding to automatically determine + * the channels from the file's header. + */ +enum Channels : char (char id) +{ + AUTO = 0, + RGB = 3, + RGBA = 4 +} + +/** + * Descriptor. + * Contains information about an image. + */ +struct Desc +{ + uint width; + uint height; + Channels channels; + Colorspace colorspace; +} + +/** + * QOI Errors. + * These are all the possible bad outcomes. + */ +fault QOIError +{ + INVALID_PARAMETERS, + FILE_OPEN_FAILED, + FILE_WRITE_FAILED, + INVALID_DATA, + TOO_MANY_PIXELS +} + + +// Let the user decide if they want to use std::io +module std::compression::qoi @if(!$feature(QOI_NO_STDIO)); +import std::io; + +/** + * Encode raw RGB or RGBA pixels into a QOI image and write it to the + * file system. + * + * The desc struct must be filled with the image width, height, the + * used channels (qoi::Channels.RGB or RGBA) and the colorspace + * (qoi::Colorspace.SRGB or LINEAR). + * + * The function returns an optional, which can either be a QOIError + * or the number of bytes written on success. + * + * @param [in] filename `The file's name to write the image to` + * @param [in] input `The raw RGB or RGBA pixels to encode` + * @param [&in] desc `The descriptor of the image` + */ +fn usz! write(String filename, char[] input, Desc* desc) +{ + @pool() { + // encode data + char[] output = encode(input, desc)!; + + // open file + File! f = file::open(filename, "wb"); + if (catch f) { return QOIError.FILE_OPEN_FAILED?; } + + // write data to file and close it + usz! written = f.write(output); + if (catch written) { return QOIError.FILE_WRITE_FAILED?; } + if (catch f.close()) { return QOIError.FILE_WRITE_FAILED?; } + + return written; + }; +} + + +/** + * Read and decode a QOI image from the file system. + * + * If channels is set to qoi::Channels.AUTO, the function will + * automatically determine the channels from the file's header. + * However, if channels is RGB or RGBA, the output format will be + * forced into this number of channels. + * + * The desc struct will be filled with the width, height, + * channels and colorspace of the image. + * + * The function returns an optional, which can either be a QOIError + * or a char[] pointing to the decoded pixels on success. + * + * The returned pixel data should be free()d after use, or the decoding + * and use of the data should be wrapped in a @pool() { ... }; block. + * + * @param [in] filename `The file's name to read the image from` + * @param [&out] desc `The descriptor to fill with the image's info` + * @param channels `The channels to be used` + */ +fn char[]! read(String filename, Desc* desc, Channels channels = AUTO, Allocator allocator = allocator::heap()) +{ + // read file + char[]! data = file::load_new(filename); + if (catch data) return QOIError.FILE_OPEN_FAILED?; + defer mem::free(data); + + // pass data to decode function + return decode(data, desc, channels, allocator); +} + + +// Back to basic non-stdio mode +module std::compression::qoi; +import std::bits; + +/** + * Encode raw RGB or RGBA pixels into a QOI image in memory. + * + * The function returns an optional, which can either be a QOIError + * or a char[] pointing to the encoded data on success. + * + * The returned qoi data should be free()d after use, or the encoding + * and use of the data should be wrapped in a @pool() { ... }; block. + * See the write() function for an example. + * + * @param [in] input `The raw RGB or RGBA pixels to encode` + * @param [&in] desc `The descriptor of the image` + */ +fn char[]! encode(char[] input, Desc* desc, Allocator allocator = allocator::heap()) +{ + // check info in desc + if (desc.width == 0 || desc.height == 0) return QOIError.INVALID_PARAMETERS?; + if (desc.channels == AUTO) return QOIError.INVALID_PARAMETERS?; + uint pixels = desc.width * desc.height; + if (pixels > PIXELS_MAX) return QOIError.TOO_MANY_PIXELS?; + + // check input data size + uint image_size = pixels * desc.channels.id; + if (image_size != input.len) return QOIError.INVALID_DATA?; + + // allocate memory for encoded data (output) + // header + chunk tag and RGB(A) data for each pixel + end of stream + uint max_size = Header.sizeof + pixels + image_size + END_OF_STREAM.len; + char[] output = allocator::alloc_array(allocator, char, max_size); // no need to init + defer catch allocator::free(allocator, output); + + // write header + *(Header*)output.ptr = { + .be_magic = bswap('qoif'), + .be_width = bswap(desc.width), + .be_height = bswap(desc.height), + .channels = desc.channels.id, + .colorspace = desc.colorspace.id + }; + + uint pos = Header.sizeof; // Current position in output + uint loc; // Current position in image (top-left corner) + uint loc_end = image_size - desc.channels.id; // End of image data + char run_length = 0; // Length of the current run + + Pixel[64] palette; // Zero-initialized by default + Pixel prev = { 0, 0, 0, 255 }; + Pixel p = { 0, 0, 0, 255 }; + + ichar[<3>] diff; // pre-allocate for diff + ichar[<3>] luma; // ...and luma + + // write chunks + for (loc = 0; loc < image_size; loc += desc.channels.id) + { + // set previous pixel + prev = p; + + // get current pixel + p[:3] = input[loc:3]; // cutesy slices :3 + if (desc.channels == RGBA) p.a = input[loc + 3]; + + // check if we can run the previous pixel + if (prev == p) { + run_length++; + if (run_length == 62 || loc == loc_end) { + *@extract(OpRun, output, &pos) = { OP_RUN, run_length - 1 }; + run_length = 0; + } + } else { + // end last run if there was one + if (run_length > 0) { + *@extract(OpRun, output, &pos) = { OP_RUN, run_length - 1 }; + run_length = 0; + } + + switch { + // check if we can index the palette + case (palette[p.hash()] == p): + *@extract(OpIndex, output, &pos) = { + OP_INDEX, + p.hash() + }; + + // check if we can use diff or luma + case (prev != p && prev.a == p.a): + // diff the pixels + diff = p.rgb - prev.rgb; + if ( + diff.r > -3 && diff.r < 2 && + diff.g > -3 && diff.g < 2 && + diff.b > -3 && diff.b < 2 + ) { + *@extract(OpDiff, output, &pos) = { + OP_DIFF, + (char)diff.r + 2, + (char)diff.g + 2, + (char)diff.b + 2 + }; + palette[p.hash()] = p; + } else { + // check luma eligibility + luma = { diff.r - diff.g, diff.g, diff.b - diff.g }; + if ( + luma.r >= -8 && luma.r <= 7 && + luma.g >= -32 && luma.g <= 31 && + luma.b >= -8 && luma.b <= 7 + ) { + *@extract(OpLuma, output, &pos) = { + OP_LUMA, + (char)luma.g + 32, + (char)luma.r + 8, + (char)luma.b + 8 + }; + palette[p.hash()] = p; + } else { nextcase; } + } + + // worst case scenario: just encode the raw pixel + default: + if (prev.a != p.a) { + *@extract(OpRGBA, output, &pos) = { OP_RGBA, p.r, p.g, p.b, p.a }; + } else { + *@extract(OpRGB, output, &pos) = { OP_RGB, p.r, p.g, p.b }; + } + palette[p.hash()] = p; + } + } + } + + // write end of stream + output[pos:END_OF_STREAM.len] = END_OF_STREAM; + pos += END_OF_STREAM.len; + + return output[:pos]; +} + + +/** + * Decode a QOI image from memory. + * + * If channels is set to qoi::Channels.AUTO, the function will + * automatically determine the channels from the file's header. + * However, if channels is RGB or RGBA, the output format will be + * forced into this number of channels. + * + * The desc struct will be filled with the width, height, + * channels and colorspace of the image. + * + * The function returns an optional, which can either be a QOIError + * or a char[] pointing to the decoded pixels on success. + * + * The returned pixel data should be free()d after use, or the decoding + * and use of the data should be wrapped in a @pool() { ... }; block. + * + * @param [in] data `The QOI image data to decode` + * @param [&out] desc `The descriptor to fill with the image's info` + * @param channels `The channels to be used` + */ +fn char[]! decode(char[] data, Desc* desc, Channels channels = AUTO, Allocator allocator = allocator::heap()) +{ + // check input data + if (data.len < Header.sizeof + END_OF_STREAM.len) return QOIError.INVALID_DATA?; + + // get header + Header* header = (Header*)data.ptr; + + // check magic bytes (FourCC) + if (bswap(header.be_magic) != 'qoif') return QOIError.INVALID_DATA?; + + // copy header data to desc + desc.width = bswap(header.be_width); + desc.height = bswap(header.be_height); + desc.channels = @enumcast(Channels, header.channels)!; // Rethrow if invalid + desc.colorspace = @enumcast(Colorspace, header.colorspace)!; // Rethrow if invalid + if (desc.channels == Channels.AUTO) return QOIError.INVALID_DATA?; // Channels must be specified in the header + + // check width and height + if (desc.width == 0 || desc.height == 0) return QOIError.INVALID_DATA?; + + // check pixel count + ulong pixels = (ulong)desc.width * (ulong)desc.height; + if (pixels > PIXELS_MAX) return QOIError.TOO_MANY_PIXELS?; + + uint pos = Header.sizeof; // Current position in data + uint loc; // Current position in image (top-left corner) + char run_length = 0; // Length of the current run + char tag; // Current chunk tag + + Pixel[64] palette; // Zero-initialized by default + Pixel p = { 0, 0, 0, 255 }; + + if (channels == AUTO) channels = desc.channels; + + // allocate memory for image data + usz image_size = (usz)pixels * channels.id; + char[] image = allocator::alloc_array(allocator, char, image_size); + defer catch allocator::free(allocator, image); + + for (loc = 0; loc < image_size; loc += channels.id) + { + // get chunk tag + tag = data[pos]; + + // check for chunk type + switch + { + case run_length > 0: + run_length--; + + case tag == OP_RGB: + OpRGB* op = @extract(OpRGB, data, &pos); + p = { op.red, op.green, op.blue, p.a }; + palette[p.hash()] = p; + + case tag == OP_RGBA: + OpRGBA* op = @extract(OpRGBA, data, &pos); + p = { op.red, op.green, op.blue, op.alpha }; + palette[p.hash()] = p; + + case tag >> 6 == OP_INDEX: + OpIndex* op = @extract(OpIndex, data, &pos); + p = palette[op.index]; + + case tag >> 6 == OP_DIFF: + OpDiff* op = @extract(OpDiff, data, &pos); + p.r += op.diff_red - 2; + p.g += op.diff_green - 2; + p.b += op.diff_blue - 2; + palette[p.hash()] = p; + + case tag >> 6 == OP_LUMA: + OpLuma* op = @extract(OpLuma, data, &pos); + int diff_green = op.diff_green - 32; + p.r += (char)(op.diff_red_minus_green - 8 + diff_green); + p.g += (char)(diff_green); + p.b += (char)(op.diff_blue_minus_green - 8 + diff_green); + palette[p.hash()] = p; + + case tag >> 6 == OP_RUN: + OpRun* op = @extract(OpRun, data, &pos); + run_length = op.run; + } + + // draw the pixel + if (channels == RGBA) { image[loc:4] = p.rgba; } else { image[loc:3] = p.rgb; } + } + + return image; +} + + + +// *************************************************************************** +// *** *** +// *** Main functions are at the top to make the file more readable. *** +// *** From here on, helper functions and types are defined. *** +// *** *** +// *************************************************************************** +module std::compression::qoi @private; + +// 8-bit opcodes +const OP_RGB = 0b11111110; +const OP_RGBA = 0b11111111; +// 2-bit opcodes +const OP_INDEX = 0b00; +const OP_DIFF = 0b01; +const OP_LUMA = 0b10; +const OP_RUN = 0b11; + +struct Header @packed +{ + uint be_magic; // magic bytes "qoif" + uint be_width; // image width in pixels (BE) + uint be_height; // image height in pixels (BE) + + // informative fields + char channels; // 3 = RGB, 4 = RGB + char colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear +} + +const char[*] END_OF_STREAM = {0, 0, 0, 0, 0, 0, 0, 1}; + +// inefficient, but it's only run once at a time +macro @enumcast($Type, raw) +{ + foreach (value : $Type.values) { + if (value.id == raw) return value; + } + return QOIError.INVALID_DATA?; +} + +distinct Pixel = inline char[<4>]; +macro char Pixel.hash(Pixel p) { + return (p.r * 3 + p.g * 5 + p.b * 7 + p.a * 11) % 64; +} + +struct OpRGB // No need to use @packed here, the alignment is 1 anyways. +{ + char tag; + char red; + char green; + char blue; +} +struct OpRGBA @packed +{ + char tag; + char red; + char green; + char blue; + char alpha; +} +bitstruct OpIndex : char +{ + char tag : 6..7; + char index : 0..5; +} +bitstruct OpDiff : char +{ + char tag : 6..7; + char diff_red : 4..5; + char diff_green : 2..3; + char diff_blue : 0..1; +} +bitstruct OpLuma : ushort +{ + char tag : 6..7; + char diff_green : 0..5; + char diff_red_minus_green : 12..15; + char diff_blue_minus_green : 8..11; +} +bitstruct OpRun : char +{ + char tag : 6..7; + char run : 0..5; +} + +// Macro used to locate chunks in data buffers. +// Can be used both for reading and writing. +macro @extract($Type, char[] data, uint* pos) +{ + // slice data, then double cast + $Type* chunk = ($Type*)data[*pos : $Type.sizeof].ptr; + *pos += $Type.sizeof; + return chunk; +} diff --git a/releasenotes.md b/releasenotes.md index 26ba1843b..3b20420fd 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -20,6 +20,7 @@ ### Stdlib changes - Additional init functions for hashmap. - `format` functions are now functions and work better with splat. +- Add support for the QOI format. ## 0.6.2 Change list diff --git a/test/unit/stdlib/compression/qoi.c3 b/test/unit/stdlib/compression/qoi.c3 new file mode 100644 index 000000000..632bd5749 --- /dev/null +++ b/test/unit/stdlib/compression/qoi.c3 @@ -0,0 +1,34 @@ +module qoi_test @test; + +import std::io::file; +import std::compression::qoi; + + +const char[*] TEST_QOI_DATA = b64"cW9pZgAAAVQAAACpBABV/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/cl/Jv39/f392jXBJv39/f392TXBJv39/f392TXBJv39/f392TXBJv39/f392TXBJv39/f392TXBJv39/f392DXBJv39/f392TXBJv39/f392TXBJv39/f392TXBJv39/f392TXBJu81Jv39zzXAJv390jXBJu01wyb9/cg1xib9/dA1wibsNcQm/f3GNcgm0zXAJv33NcEm6zXHJv39wzXIJtQ1xCb99DXBJus1wibANcEm/f3BNcgm1TXHJv3yNcEm6jXCJsE1wSb9/cA1xibZNSbANcQm/fE1wSbpNcImwjXBJv39NcUm4DXCJv3xNcEm6DXCJsM1wSb9/TXEJuI1wib98DXBJuc1wibENcEm/fw1wyblNcIm/e41wibmNcImxTXBJv37NcMm5zXBJv3uNcIm5jXCJsU1wSb9+jXEJuc1wib97TXCJuU1wibGNcEm/fk1xCboNcIm/e01wSbmNcImxjXBJv35NcMm6DXCJv3uNcEm5jXBJsc1wSb9+DXDJuk1wib97jXBJuU1wibGNcIm/fg1wyboNcIm/e81wSblNcEmxzXBJv34NcMm6TXCJv3vNcEm5TXBJsc1wSb9+DXCJuk1wib98DXBJuQ1wibHNcEm/fc1wyboNcMm/fA1wSbkNcEmxzXCJv33NcMm5jXEJv3xNcEm5DXBJsc1wSb9+DXCJuY1xSb98TXBJuM1wibGNcIm/fc1wybmNcQm/fI1wSbjNcEmxjXCJv34NcIm5zXGJv3wNcEm4zXBJsU1wyb9+DXCJug1xib97jXCJuI1wibENcMm/fk1wibrNcQm/e01wibiNcImwjXDJv37NcIm7DXEJv3sNcEm4zXBJsE1xCb9wDUm+TXCJu41wyb96zXBJuM1wSY1xSbKNcUm6jXDJvc1wibvNcMm/eo1wSbjNccmyzXIJuY1xSb3NcIm7zXEJv3pNcEm4jXGJsw1yibLNcUm0DXGJvc1wibwNcMm/ek1wSbiNcQmzTXLJsk1xybDNcImxjXHJvc1wibxNcIm/ek1wSbhNcMmzzXDJsA1xSbGNcomwjXDJsQ1ySb2NcIm8TXCJv3pNcEm4TXCJs81wibCNcUmxDXLJsI1xSbCNcQmNcIm9zXDJvA1wib96DXCJuI1wSbONcMmwzXEJsM1xibANcImwjXGJsA1xCY1wyb4NcIm7zXDJv3oNcIm4jXBJs41wibENcQmwTXGJsE1wybCNccmNcMmNcMm+TXCJtA1wibZNcIm/ek1wSbjNcEmzjXBJsU1wybANcYmwzXCJsI11Cb5NcMmzTXDJtk1wyb96TXBJuM1wSbNNcImxTXMJsM1wybCNcImwDXNJvs1wibMNcMm2TXDJv3qNcEm4zXBJs01wibFNcsmxDXDJsE1wybBNcsm/DXDJsk1xCbZNcQm/eo1wSbjNcEmzDXCJsc1yCbGNcImwjXCJsM1yCb9wDXEJsY1xCbaNcQm/es1wSbjNcEmyzXDJsY1xibJNcImwTXDJsQ1wib9xjXQJto1xCb97DXBJuM1wSbKNcQmxjXCJs01wibANcMmxTXCJv3HNc4m2jXEJv3tNcEm4zXBJsk1xibENcImzjXCJsA1wybENcMm/cg1yybbNcQm/e41wSbjNcImxzXCJjXCJsQ1wibNNcMmNcMmxTXDJv3JNcgm3DXEJv3vNcEm5DXBJsQ1xCbANcwmzTXIJsY1wyb98DXEJtg1wCb91DXBJuQ1wyY1xibCNcomzjXIJsc1wib96DXBJsE1xSbWNcAm/dc1wSblNckmxTXIJs81xybINcImzTXBJv3VNcMmNcUm1TXAJv3ZNcEm5jXHJsg1wibTNcYmyTXDJss1wSb91TXLJtQ1wCb92zXBJug1wSblNcYmyTXDJsk1wyb91TXJJtM1wCb93jXBJv3UNcQmzDXEJsU1xCb91TXJJtI1wCb94DXBJv3UNcMmzjXGJjXGJv3WNccm0jXAJv3iNcEm/dU1wSbPNc4m/dc1xibQNcAm/eU1wSb96zXLJv3YNcQm0DXAJv3nNcIm/ew1xyb92zXBJtA1wCb96jXBJv3uNcIm/fE1wCb97DXBJv39/eU1wCb9/f39/dc1wCb9/f39/dg1wCb9/f39/dg1wCbxNcUm/f39/dw1wCbxNcgm/f39/dk1wCbxNcom/f39/dc1wCbyNcQmwjXBJv39/f3VNcAm9DXCJsQ1wSb9/f390jXBJvY1wSbENcIm/f39/dA1wSb4NcAmxTXCJv39/f3O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wybONcIm9zXDJss1JjXAJv36Lsg8wQbGLscmzDDGNjjANjA2OME2MMsm5TXCJs41wyb3NcQmyTXCJv37LsgGJTwGxi7HJswwxgo4wAo2OMIwzCb5NcIm+TXFJsQ1xCb9/C7KEAbEEC7IJswwxj44wG44wzDMJvg1wib7Nc8m/f0u3CbMMMYKOMU2MMwm+DXCJvw1zSb9/cAu3CbMMMY2OMQ2MM0m9zXCJv3ANcsm/f3tMMc4wzYwzib3NcIm/cM1xib9/e8w3Sb2NcIm/f39/f3XNcMm/f397f6NjY3BJv3iNcMm/f397Tj+JycnwSb93zXFJv39/dU4/lFRUT7DJtA+wib93zXEJv39/dQ4PsYmzzg+wTQm/d81wyb9/f3UPsgmzz7COCb94DXAJv39/dU+xjgmyDg0wDgmwjg+wibCOD7GJv39/f3tPsQmyD7FJsI+wjgmwT7IJs84ND7CJsU4NMA4Jv39/f3LPsImyT7GJsE0PsImwj7IJsE4PsI4JsY+xCbEPsMmxTg0hIg0OCb9/f39PsEmyj7GJsE+wjgmwj7COMA0PsEmwTQ+yCbBPsQmxD7DJsQ+xSb9/f38PsI4JsI4wSbBPsEQOD7BJsE+wTQmwz7BOD7EJsEQPsgmwT7DJsU+wTQ4JsM+xib9/f38PsomwRA+xSbAOD7BOCbDPsgmwRA+yCbBPsUmwz7CJsQ+whA+wSb9/f38PsomwTg+xSbAND7CNMA4JsA+xzgmwRA+wBAmyT7FJsI+wibEPsI4Jv39/f3BOD7IOCbCPsUmwDQ+xSbAPsUmxBA+wBAmyz7DJsI+wzgmwj7DOCb9/f390j7COCbAOD7FJsE4NMA4JsY0PsA0Jsk+xSbCOD7DJsI4PsM4Jv39/f3bPsE0OCbNOD7AOCbJPsQ4JsE+xibBPsQ4Jv39/f39wD7EJsI+xibBPsQ4Jv39/f39yj7GJsE+wzgm/f39/f3LODT+KysrPsA4JsM+wSb9/f39/f39/f3aAAAAAAAAAAE="; + + +fn void! test_qoi_all() +{ + @pool() { + // create a test descriptor + qoi::Desc test_desc; + + // decode the test data + char[] decoded = qoi::decode(TEST_QOI_DATA[..], &test_desc)!; + assert(test_desc.width == 340 && test_desc.height == 169, "Expected resolution of 340x169"); + + // encode the decoded data + char[] encoded = qoi::encode(decoded, &test_desc)!; + assert(encoded == TEST_QOI_DATA[..], "Encoder output should match the test data"); + + // encode and write the decoded data to a file + usz written = qoi::write("unittest.qoi", decoded, &test_desc)!; + + // read and decode the written data + char[] read = qoi::read("unittest.qoi", &test_desc)!; + assert(read == decoded, "Read data should match the decoded data"); + + // cleanup + file::delete("unittest.qoi")!; + }; +}