open-story-teller/firmware/library/mini_qoi.c

260 lines
7.6 KiB
C

#include "mini_qoi.h"
// ==== utilities ====
static inline uint32_t beu32_get(const uint8_t *a)
{
uint32_t val = 0;
val |= (((uint32_t)a[0]) << 24);
val |= (((uint32_t)a[1]) << 16);
val |= (((uint32_t)a[2]) << 8);
val |= ((uint32_t)a[3]);
return val;
}
// ==== mqoi_desc_t ====
/*
Initializes an mQOI image descriptor object.
*/
void mqoi_desc_init(mqoi_desc_t *desc)
{
memset(desc, 0, sizeof(mqoi_desc_t));
}
/*
Pushes a byte to the mQOI image descriptor object.
*/
void mqoi_desc_push(mqoi_desc_t *desc, uint8_t byte)
{
((uint8_t *)desc)[++desc->head] = byte;
}
/*
Reads a byte from the mQOI image descriptor object.
Returns NULL when there are none left to read.
*/
uint8_t *mqoi_desc_pop(mqoi_desc_t *desc)
{
if (desc->head >= sizeof(mqoi_desc_t) - 1)
return NULL;
return (uint8_t *)(desc + (++desc->head));
}
/*
Checks if a mQOI image descriptor is valid, and reads out its width and height into w and h.
If it returns a nonzero code, it is invalid (use mqoi_desc_err_t to understand what it means)
*/
uint8_t mqoi_desc_verify(mqoi_desc_t *desc, uint32_t *w, uint32_t *h)
{
if (desc->magic[0] != 'q' || desc->magic[1] != 'o' || desc->magic[2] != 'i' || desc->magic[3] != 'f')
{
return MQOI_DESC_INVALID_MAGIC;
}
*w = beu32_get(desc->width);
*h = beu32_get(desc->height);
if (desc->channels != MQOI_CHANNELS_RGB && desc->channels != MQOI_CHANNELS_RGBA)
{
return MQOI_DESC_INVALID_CHANNELS;
}
if (desc->colorspace != MQOI_COLORSPACE_SRGB && desc->colorspace != MQOI_COLORSPACE_LINEAR)
{
return MQOI_DESC_INVALID_COLORSPACE;
}
return MQOI_DESC_OK;
}
/*
Returns true when the mQOI image descriptor object is completely populated.
*/
inline bool mqoi_desc_done(const mqoi_desc_t *desc)
{
return desc->head >= sizeof(mqoi_desc_t) - 1;
}
// ==== mqoi_dec_t ====
/*
Initializes an mQOI decoder object.
If number of pixels in the image are given (via n_pix), the mqoi_dec_done function will work.
*/
void mqoi_dec_init(mqoi_dec_t *dec, uint32_t n_pix)
{
memset(dec, 0, sizeof(mqoi_dec_t));
dec->prev_px.a = 0xff;
dec->pix_left = n_pix;
}
/*
Pushes a byte to the mQOI decoder.
Don't call this more than once unless all the pixels have been popped via mqoi_dec_pop!
Also, please don't intermingle this function with calls to mqoi_dec_take!
*/
void mqoi_dec_push(mqoi_dec_t *dec, uint8_t byte)
{
dec->curr_chunk.value[dec->curr_chunk_head++] = byte;
if (dec->curr_chunk_size == 0 && dec->curr_chunk_head == 1)
{ // if we have the head of a new chunk, get its size
switch (dec->curr_chunk.head)
{ // test 8-bit tags
case MQOI_OP8_RUN_RGBA:
dec->curr_chunk_size = 5;
break;
case MQOI_OP8_RUN_RGB:
dec->curr_chunk_size = 4;
break;
default:
{ // test 2-bit tags
// chunk size is 1 unless it's an OP_LUMA chunk
dec->curr_chunk_size = 1 + ((dec->curr_chunk.head & MQOI_MASK_OP_2B) == MQOI_OP2_LUMA);
break;
}
}
}
}
/*
Automatically read up to 5 bytes so that a subsequent call of mqoi_dec_pop will not return NULL.
Returns the number of bytes that have been "taken" from the bytes array.
Please don't intermingle this function with calls to mqoi_dec_push!
*/
uint8_t mqoi_dec_take(mqoi_dec_t *dec, const uint8_t *bytes)
{
dec->curr_chunk.value[dec->curr_chunk_head++] = *(bytes++);
switch (dec->curr_chunk.head)
{ // test 8-bit tags
case MQOI_OP8_RUN_RGBA:
dec->curr_chunk_size = 5;
break;
case MQOI_OP8_RUN_RGB:
dec->curr_chunk_size = 4;
break;
default:
{ // test 2-bit tags
// chunk size is 1 unless it's an OP_LUMA chunk
dec->curr_chunk_size = 1 + ((dec->curr_chunk.head & MQOI_MASK_OP_2B) == MQOI_OP2_LUMA);
break;
}
}
while (dec->curr_chunk_head < dec->curr_chunk_size)
{
dec->curr_chunk.value[dec->curr_chunk_head++] = *(bytes++);
}
return dec->curr_chunk_head;
}
/*
Pops a pixel from the mQOI decoder.
Returns NULL if more data is needed, otherwise it returns the address of the next pixel.
*/
mqoi_rgba_t *mqoi_dec_pop(mqoi_dec_t *dec)
{
if (dec->curr_chunk_size && dec->curr_chunk_head >= dec->curr_chunk_size)
{ // if we're at the end of the current chunk
mqoi_rgba_t px = {.a = dec->prev_px.a};
mqoi_rgba_t *px_ptr = NULL;
bool chunk_done = true;
switch (dec->curr_chunk.head)
{ // test 8-bit tags
case MQOI_OP8_RUN_RGBA: // rgba handled
px.a = dec->curr_chunk.rgba.a;
case MQOI_OP8_RUN_RGB: // rgb handled
px.r = dec->curr_chunk.rgb.r;
px.g = dec->curr_chunk.rgb.g;
px.b = dec->curr_chunk.rgb.b;
break;
default:
{ // test 2-bit tags
switch (dec->curr_chunk.head & MQOI_MASK_OP_2B)
{
case MQOI_OP2_INDEX:
px_ptr = &dec->hashtable[dec->curr_chunk.head]; // no need to mask bits because the top bits are zero
break;
case MQOI_OP2_DIFF:
// shift out each channel and compute difference
px.b = dec->prev_px.b - 2 + (dec->curr_chunk.head & 0b11); // read out db
dec->curr_chunk.head >>= 2; // shift in dg
px.g = dec->prev_px.g - 2 + (dec->curr_chunk.head & 0b11); // read out dg
dec->curr_chunk.head >>= 2; // shift in dr
px.r = dec->prev_px.r - 2 + (dec->curr_chunk.head & 0b11); // read out dr
break;
case MQOI_OP2_LUMA:
{
int8_t dg = (dec->curr_chunk.head & MQOI_MASK_OP_LUMA_DG) - 32;
px.g = dec->prev_px.g + dg;
px.b = dec->prev_px.b + (dec->curr_chunk.drdb & 0b1111) - 8 + dg;
dec->curr_chunk.drdb >>= 4;
px.r = dec->prev_px.r + (dec->curr_chunk.drdb & 0b1111) - 8 + dg;
break;
}
case MQOI_OP2_RUN:
{
uint8_t run = dec->curr_chunk.head & MQOI_MASK_OP_RUN;
px_ptr = &dec->prev_px;
if (run)
{ // if there are still pixels left to emit,
chunk_done = false; // don't reset the chunk (bring us back to the block)
dec->curr_chunk.head--;
}
break;
}
}
break;
}
}
if (px_ptr == NULL)
{ // if the return pixel is null (not in hashtable yet)
uint8_t px_hash = MQOI_RGBA_HASH(px); // hash it
px_ptr = &dec->hashtable[px_hash]; // get its hashed position
px_ptr->r = px.r;
px_ptr->g = px.g;
px_ptr->b = px.b;
px_ptr->a = px.a;
}
if (chunk_done)
{ // if the chunk is done, reset the head
dec->curr_chunk_head = 0;
dec->curr_chunk_size = 0;
}
dec->prev_px.r = px_ptr->r;
dec->prev_px.g = px_ptr->g;
dec->prev_px.b = px_ptr->b;
dec->prev_px.a = px_ptr->a;
dec->pix_left--;
return px_ptr;
}
return NULL;
}
/*
Returns true if the decoder has emitted all pixels necessary to complete the image being decoded.
Note that this function will only work if n_pix was given during the initialization of the decoder.
*/
inline bool mqoi_dec_done(const mqoi_dec_t *dec)
{
return dec->pix_left == 0;
}