mirror of
https://github.com/arabine/open-story-teller.git
synced 2025-12-06 17:09:06 +01:00
260 lines
7.6 KiB
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;
|
|
}
|