982 lines
26 KiB
C
982 lines
26 KiB
C
//
|
|
// Aseprite (.ase) file parser
|
|
//
|
|
// DEFLATE decoder based on Handmade Hero's png parser
|
|
//
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Shared constants
|
|
|
|
Global Readonly u32 ASE_huff_hclen_order[] = {
|
|
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
|
|
};
|
|
|
|
Global Readonly ASE_HuffEntry ASE_huff_length_table[] = {
|
|
{3, 0}, // 257
|
|
{4, 0}, // 258
|
|
{5, 0}, // 259
|
|
{6, 0}, // 260
|
|
{7, 0}, // 261
|
|
{8, 0}, // 262
|
|
{9, 0}, // 263
|
|
{10, 0}, // 264
|
|
{11, 1}, // 265
|
|
{13, 1}, // 266
|
|
{15, 1}, // 267
|
|
{17, 1}, // 268
|
|
{19, 2}, // 269
|
|
{23, 2}, // 270
|
|
{27, 2}, // 271
|
|
{31, 2}, // 272
|
|
{35, 3}, // 273
|
|
{43, 3}, // 274
|
|
{51, 3}, // 275
|
|
{59, 3}, // 276
|
|
{67, 4}, // 277
|
|
{83, 4}, // 278
|
|
{99, 4}, // 279
|
|
{115, 4}, // 280
|
|
{131, 5}, // 281
|
|
{163, 5}, // 282
|
|
{195, 5}, // 283
|
|
{227, 5}, // 284
|
|
{258, 0}, // 285
|
|
};
|
|
|
|
Global Readonly ASE_HuffEntry ASE_huff_dist_table[] = {
|
|
{1, 0}, // 0
|
|
{2, 0}, // 1
|
|
{3, 0}, // 2
|
|
{4, 0}, // 3
|
|
{5, 1}, // 4
|
|
{7, 1}, // 5
|
|
{9, 2}, // 6
|
|
{13, 2}, // 7
|
|
{17, 3}, // 8
|
|
{25, 3}, // 9
|
|
{33, 4}, // 10
|
|
{49, 4}, // 11
|
|
{65, 5}, // 12
|
|
{97, 5}, // 13
|
|
{129, 6}, // 14
|
|
{193, 6}, // 15
|
|
{257, 7}, // 16
|
|
{385, 7}, // 17
|
|
{513, 8}, // 18
|
|
{769, 8}, // 19
|
|
{1025, 9}, // 20
|
|
{1537, 9}, // 21
|
|
{2049, 10}, // 22
|
|
{3073, 10}, // 23
|
|
{4097, 11}, // 24
|
|
{6145, 11}, // 25
|
|
{8193, 12}, // 26
|
|
{12289, 12}, // 27
|
|
{16385, 13}, // 28
|
|
{24577, 13}, // 29
|
|
};
|
|
|
|
Global Readonly u32 ASE_huff_bl_counts[][2] = {
|
|
{143, 8},
|
|
{255, 9},
|
|
{279, 7},
|
|
{287, 8},
|
|
{319, 5},
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Ase bitbuff
|
|
|
|
u32 ASE_PeekBits(ASE_Bitbuff *bb, u32 nbits)
|
|
{
|
|
Assert(nbits <= 32);
|
|
|
|
u64 cur_byte = bb->cur_bit >> 3;
|
|
u8 bit_index = bb->cur_bit % 8;
|
|
u64 nbytes = (nbits + bit_index + 7) >> 3;
|
|
|
|
u64 val64 = 0;
|
|
CopyBytes(&val64, &bb->data[cur_byte], nbytes);
|
|
u32 val32 = (u32)(val64 >> bit_index);
|
|
val32 &= U32Max >> (32 - nbits);
|
|
|
|
return val32;
|
|
}
|
|
|
|
u32 ASE_ConsumeBits(ASE_Bitbuff *bb, u32 nbits)
|
|
{
|
|
u32 val = ASE_PeekBits(bb, nbits);
|
|
bb->cur_bit += nbits;
|
|
return val;
|
|
}
|
|
|
|
void ASE_SkipBits(ASE_Bitbuff *bb, u32 nbits)
|
|
{
|
|
bb->cur_bit += nbits;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Inflate
|
|
|
|
u32 ASE_ReverseBits(u32 v, u32 bit_count)
|
|
{
|
|
// 7 & 15 seem to be the most common bit_counts, so a
|
|
// more optimal path is layed out for them.
|
|
if (bit_count == 15)
|
|
{
|
|
u32 b1 = v & 0xFF;
|
|
b1 = (b1 & 0xF0) >> 4 | (b1 & 0x0F) << 4;
|
|
b1 = (b1 & 0xCC) >> 2 | (b1 & 0x33) << 2;
|
|
b1 = (b1 & 0xAA) >> 1 | (b1 & 0x55) << 1;
|
|
|
|
u32 b2 = (v & 0xFF00) >> 8;
|
|
b2 = (b2 & 0xF0) >> 4 | (b2 & 0x0F) << 4;
|
|
b2 = (b2 & 0xCC) >> 2 | (b2 & 0x33) << 2;
|
|
b2 = (b2 & 0xAA) >> 1 | (b2 & 0x55) << 1;
|
|
b2 >>= 1;
|
|
|
|
return (b1 << 7) | b2;
|
|
}
|
|
else if (bit_count == 7)
|
|
{
|
|
v = (v & 0xF0) >> 4 | (v & 0x0F) << 4;
|
|
v = (v & 0xCC) >> 2 | (v & 0x33) << 2;
|
|
v = (v & 0xAA) >> 1 | (v & 0x55) << 1;
|
|
return v >> 1;
|
|
|
|
}
|
|
else
|
|
{
|
|
u32 result = 0;
|
|
for (u32 i = 0; i <= (bit_count / 2); ++i)
|
|
{
|
|
u32 inv = (bit_count - (i + 1));
|
|
result |= ((v >> i) & 0x1) << inv;
|
|
result |= ((v >> inv) & 0x1) << i;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
ASE_HuffDict ASE_InitHuffDict(Arena *arena, u32 max_code_bits, u32 *bl_counts, u32 bl_counts_count)
|
|
{
|
|
ASE_HuffDict result = Zi;
|
|
result.max_code_bits = max_code_bits;
|
|
result.entries_count = (1 << max_code_bits);
|
|
result.entries = PushStructsNoZero(arena, ASE_HuffEntry, result.entries_count);
|
|
|
|
u32 code_length_hist[ASE_HuffBitCount] = Zi;
|
|
for (u32 i = 0; i < bl_counts_count; ++i)
|
|
{
|
|
u32 count = bl_counts[i];
|
|
Assert(count <= countof(code_length_hist));
|
|
++code_length_hist[count];
|
|
}
|
|
|
|
u32 next_code[ASE_HuffBitCount] = Zi;
|
|
next_code[0] = 0;
|
|
code_length_hist[0] = 0;
|
|
for (u32 i = 1; i < countof(next_code); ++i)
|
|
{
|
|
next_code[i] = ((next_code[i - 1] + code_length_hist[i - 1]) << 1);
|
|
}
|
|
|
|
for (u32 i = 0; i < bl_counts_count; ++i)
|
|
{
|
|
u32 code_bits = bl_counts[i];
|
|
if (code_bits)
|
|
{
|
|
Assert(code_bits < countof(next_code));
|
|
u32 code = next_code[code_bits]++;
|
|
u32 arbitrary_bits = result.max_code_bits - code_bits;
|
|
u32 entry_count = (1 << arbitrary_bits);
|
|
for (u32 entry_index = 0; entry_index < entry_count; ++entry_index)
|
|
{
|
|
// TODO: Optimize this. It's bloating up the loading times.
|
|
u32 base_index = (code << arbitrary_bits) | entry_index;
|
|
u32 index = ASE_ReverseBits(base_index, result.max_code_bits);
|
|
ASE_HuffEntry *entry = &result.entries[index];
|
|
entry->symbol = (u16)i;
|
|
entry->bits_used = (u16)code_bits;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
u16 ASE_DecodeHuffDict(ASE_HuffDict *huffman, ASE_Bitbuff *bb)
|
|
{
|
|
u32 index = ASE_PeekBits(bb, huffman->max_code_bits);
|
|
Assert(index < huffman->entries_count);
|
|
|
|
ASE_HuffEntry *entry = &huffman->entries[index];
|
|
u16 result = entry->symbol;
|
|
ASE_SkipBits(bb, entry->bits_used);
|
|
Assert(entry->bits_used > 0);
|
|
return result;
|
|
}
|
|
|
|
void ASE_Inflate(u8 *dst, u8 *encoded)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
|
|
ASE_Bitbuff bb = { .data = encoded };
|
|
|
|
// ZLIB header
|
|
u32 cm = ASE_ConsumeBits(&bb, 4);
|
|
u32 cinfo = ASE_ConsumeBits(&bb, 4);
|
|
Assert(cm == 8);
|
|
Assert(cinfo == 7);
|
|
|
|
u32 fcheck = ASE_ConsumeBits(&bb, 5);
|
|
u32 fdict = ASE_ConsumeBits(&bb, 1);
|
|
u32 flevl = ASE_ConsumeBits(&bb, 2);
|
|
Assert(fdict == 0);
|
|
|
|
u8 cmf = (u8)(cm | (cinfo << 4));
|
|
u8 flg = fcheck | (fdict << 5) | (flevl << 6);
|
|
Assert(((cmf * 256) + flg) % 31 == 0);
|
|
|
|
u8 bfinal = 0;
|
|
while (!bfinal)
|
|
{
|
|
bfinal = ASE_ConsumeBits(&bb, 1);
|
|
u8 btype = ASE_ConsumeBits(&bb, 2);
|
|
switch (btype)
|
|
{
|
|
case ASE_BlockType_Uncompressed:
|
|
{
|
|
ASE_SkipBits(&bb, (8 - (bb.cur_bit % 8)) % 8);
|
|
i16 len = ASE_ConsumeBits(&bb, 16);
|
|
i16 nlen = ASE_ConsumeBits(&bb, 16);
|
|
Assert(len == ~nlen); // Validation
|
|
while (len-- > 0)
|
|
{
|
|
*dst++ = ASE_ConsumeBits(&bb, 8);
|
|
}
|
|
} break;
|
|
|
|
case ASE_BlockType_CompressedFixed:
|
|
case ASE_BlockType_CompressedDynamic:
|
|
{
|
|
TempArena temp = BeginTempArena(scratch.arena);
|
|
u32 lit_len_dist_table[512] = Zi;
|
|
u32 hlit;
|
|
u32 hdist;
|
|
|
|
if (btype == ASE_BlockType_CompressedDynamic)
|
|
{
|
|
// Dynamic table
|
|
|
|
// Read huffman table
|
|
hlit = ASE_ConsumeBits(&bb, 5) + 257;
|
|
hdist = ASE_ConsumeBits(&bb, 5) + 1;
|
|
u32 hclen = ASE_ConsumeBits(&bb, 4) + 4;
|
|
|
|
// Init dict huffman (hclen)
|
|
u32 hclen_bl_counts[19] = Zi;
|
|
for (u32 i = 0; i < hclen; ++i)
|
|
{
|
|
u32 code = ASE_huff_hclen_order[i];
|
|
hclen_bl_counts[code] = ASE_ConsumeBits(&bb, 3);
|
|
}
|
|
ASE_HuffDict dict_huffman = ASE_InitHuffDict(temp.arena, 7, hclen_bl_counts, countof(hclen_bl_counts));
|
|
|
|
// Decode dict huffman
|
|
u32 lit_len_count = 0;
|
|
u32 len_count = hlit + hdist;
|
|
Assert(len_count <= countof(lit_len_dist_table));
|
|
while (lit_len_count < len_count)
|
|
{
|
|
u32 rep_count = 1;
|
|
u32 rep_val = 0;
|
|
u32 encoded_len = ASE_DecodeHuffDict(&dict_huffman, &bb);
|
|
if (encoded_len <= 15)
|
|
{
|
|
rep_val = encoded_len;
|
|
}
|
|
else if (encoded_len == 16)
|
|
{
|
|
rep_count = 3 + ASE_ConsumeBits(&bb, 2);
|
|
Assert(lit_len_count > 0);
|
|
rep_val = lit_len_dist_table[lit_len_count - 1];
|
|
}
|
|
else if (encoded_len == 17)
|
|
{
|
|
rep_count = 3 + ASE_ConsumeBits(&bb, 3);
|
|
}
|
|
else if (encoded_len == 18)
|
|
{
|
|
rep_count = 11 + ASE_ConsumeBits(&bb, 7);
|
|
}
|
|
else
|
|
{
|
|
// Invalid len
|
|
Assert(0);
|
|
}
|
|
|
|
while (rep_count--)
|
|
{
|
|
lit_len_dist_table[lit_len_count++] = rep_val;
|
|
}
|
|
}
|
|
Assert(lit_len_count == len_count);
|
|
}
|
|
else
|
|
{
|
|
// Fixed table
|
|
hlit = 288;
|
|
hdist = 32;
|
|
u32 index = 0;
|
|
for (u32 i = 0; i < countof(ASE_huff_bl_counts); ++i)
|
|
{
|
|
u32 bit_count = ASE_huff_bl_counts[i][1];
|
|
u32 last_valuie = ASE_huff_bl_counts[i][0];
|
|
while (index <= last_valuie)
|
|
{
|
|
lit_len_dist_table[index++] = bit_count;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode
|
|
ASE_HuffDict lit_len_huffman = ASE_InitHuffDict(temp.arena, 15, lit_len_dist_table, hlit);
|
|
ASE_HuffDict dist_huffman = ASE_InitHuffDict(temp.arena, 15, lit_len_dist_table + hlit, hdist);
|
|
|
|
for (;;)
|
|
{
|
|
u32 lit_len = ASE_DecodeHuffDict(&lit_len_huffman, &bb);
|
|
if (lit_len <= 255)
|
|
{
|
|
*dst++ = lit_len & 0xFF;
|
|
}
|
|
else if (lit_len >= 257)
|
|
{
|
|
u32 length_index = (lit_len - 257);
|
|
ASE_HuffEntry length_entry = ASE_huff_length_table[length_index];
|
|
u32 length = length_entry.symbol;
|
|
if (length_entry.bits_used > 0)
|
|
{
|
|
u32 extra_bits = ASE_ConsumeBits(&bb, length_entry.bits_used);
|
|
length += extra_bits;
|
|
}
|
|
|
|
u32 dist_index = ASE_DecodeHuffDict(&dist_huffman, &bb);
|
|
ASE_HuffEntry dist_entry = ASE_huff_dist_table[dist_index];
|
|
u32 distance = dist_entry.symbol;
|
|
if (dist_entry.bits_used > 0)
|
|
{
|
|
u32 extra_bits = ASE_ConsumeBits(&bb, dist_entry.bits_used);
|
|
distance += extra_bits;
|
|
}
|
|
u8 *src = dst - distance;
|
|
while (length--)
|
|
{
|
|
*dst++ = *src++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
EndTempArena(temp);
|
|
} break;
|
|
|
|
case ASE_BlockType_Reserved:
|
|
{
|
|
// TODO
|
|
Assert(0);
|
|
} break;
|
|
}
|
|
}
|
|
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Error helpers
|
|
|
|
void ASE_PushError(Arena *arena, ASE_ErrorList *list, String msg_src)
|
|
{
|
|
ASE_Error *e = PushStruct(arena, ASE_Error);
|
|
e->msg = PushString(arena, msg_src);
|
|
if (!list->first)
|
|
{
|
|
list->first = e;
|
|
}
|
|
else
|
|
{
|
|
list->last->next = e;
|
|
}
|
|
list->last = e;
|
|
++list->count;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Decode helpers
|
|
|
|
u32 ASE_BlendMulU8(u32 a, u32 b)
|
|
{
|
|
u32 t = (a * b) + 0x80;
|
|
return ((t >> 8) + t) >> 8;
|
|
}
|
|
|
|
u32 ASE_Blend(u32 src, u32 dst, u8 opacity)
|
|
{
|
|
u32 dst_r = (dst & 0xff);
|
|
u32 dst_g = (dst >> 8) & 0xff;
|
|
u32 dst_b = (dst >> 16) & 0xff;
|
|
u32 dst_a = (dst >> 24) & 0xff;
|
|
|
|
u32 src_r = (src & 0xff);
|
|
u32 src_g = (src >> 8) & 0xff;
|
|
u32 src_b = (src >> 16) & 0xff;
|
|
u32 src_a = (src >> 24) & 0xff;
|
|
|
|
src_a = (u8)ASE_BlendMulU8(src_a, opacity);
|
|
u32 a = src_a + dst_a - ASE_BlendMulU8(src_a, dst_a);
|
|
u32 r, g, b;
|
|
if (a == 0)
|
|
{
|
|
r = g = b = 0;
|
|
}
|
|
else
|
|
{
|
|
r = dst_r + (src_r - dst_r) * src_a / a;
|
|
g = dst_g + (src_g - dst_g) * src_a / a;
|
|
b = dst_b + (src_b - dst_b) * src_a / a;
|
|
}
|
|
|
|
return r | (g << 8) | (b << 16) | (a << 24);
|
|
}
|
|
|
|
void ASE_MakeDimensionsSquareish(ASE_Header *header, u32 *frames_x, u32 *frames_y, u64 *image_width, u64 *image_height)
|
|
{
|
|
// Try and get image resolution into as much of a square as possible by
|
|
// separating frames into multiple rows.
|
|
while (*frames_x > 1)
|
|
{
|
|
u64 new_frames_x = *frames_x - 1;
|
|
u64 new_frames_y = ((header->frames - 1) / new_frames_x) + 1;
|
|
u64 new_image_width = header->width * new_frames_x;
|
|
u64 new_image_height = header->height * new_frames_y;
|
|
if (new_image_width >= new_image_height)
|
|
{
|
|
*frames_x = new_frames_x;
|
|
*frames_y = new_frames_y;
|
|
*image_width = new_image_width;
|
|
*image_height = new_image_height;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Decode image
|
|
|
|
ASE_DecodedImage ASE_DecodeImage(Arena *arena, String encoded)
|
|
{
|
|
TempArena scratch = BeginScratch(arena);
|
|
ASE_DecodedImage result = Zi;
|
|
|
|
BB_Buff bb = BB_BuffFromString(encoded);
|
|
BB_Reader br = BB_ReaderFromBuffNoDebug(&bb);
|
|
ASE_Header ase_header;
|
|
BB_ReadBytes(&br, StringFromStruct(&ase_header));
|
|
|
|
if (ase_header.magic != 0xA5E0)
|
|
{
|
|
ASE_PushError(arena, &result.errors, Lit("Not a valid aseprite file"));
|
|
goto abort;
|
|
}
|
|
|
|
if (ase_header.color_depth != 32)
|
|
{
|
|
String msg = StringF(
|
|
scratch.arena,
|
|
"Only 32 bit rgba color mode is supported (got %F)",
|
|
FmtUint(ase_header.color_depth)
|
|
);
|
|
ASE_PushError(arena, &result.errors, msg);
|
|
goto abort;
|
|
}
|
|
|
|
u64 frame_width = ase_header.width;
|
|
u64 frame_height = ase_header.height;
|
|
|
|
u32 frames_x = ase_header.frames;
|
|
u32 frames_y = 1;
|
|
u64 image_width = frame_width * frames_x;
|
|
u64 image_height = frame_height * frames_y;
|
|
ASE_MakeDimensionsSquareish(&ase_header, &frames_x, &frames_y, &image_width, &image_height);
|
|
|
|
result.width = image_width;
|
|
result.height = image_height;
|
|
// TODO: Optimize this. Naive memzero is bloating the decode time for large images.
|
|
result.pixels = PushStructs(arena, u32, image_width * image_height);
|
|
|
|
u32 num_layers = 0;
|
|
ASE_Layer *layer_head = 0;
|
|
|
|
Ace_Cel *cel_head = 0;
|
|
Ace_Cel *cel_tail = 0;
|
|
|
|
//////////////////////////////
|
|
//- Iterate frames
|
|
|
|
u32 num_frames = 0;
|
|
for (u16 i = 0; i < ase_header.frames; ++i)
|
|
{
|
|
ASE_FrameHeader frame_header;
|
|
BB_ReadBytes(&br, StringFromStruct(&frame_header));
|
|
|
|
u32 num_chunks = frame_header.chunks_new;
|
|
if (num_chunks == 0)
|
|
{
|
|
Assert(frame_header.chunks_old != 0xFFFF);
|
|
num_chunks = frame_header.chunks_old;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Iterate chunks
|
|
|
|
for (u32 j = 0; j < num_chunks; ++j)
|
|
{
|
|
u32 chunk_size = BB_ReadUBits(&br, 32);
|
|
ASE_ChunkKind chunk_type = BB_ReadUBits(&br, 16);
|
|
|
|
// Chunk size includes size & type
|
|
Assert(chunk_size >= 6);
|
|
chunk_size -= 6;
|
|
|
|
u64 chunk_end_pos = BB_GetCurrentReaderByte(&br) + chunk_size;
|
|
|
|
switch (chunk_type)
|
|
{
|
|
default:
|
|
{
|
|
BB_ReadSeekToByte(&br, chunk_end_pos);
|
|
} break;
|
|
|
|
//////////////////////////////
|
|
//- Decode layer
|
|
|
|
case ASE_ChunkKind_Layer:
|
|
{
|
|
ASE_Layer *layer = PushStruct(scratch.arena, ASE_Layer);
|
|
layer->next = layer_head;
|
|
layer_head = layer;
|
|
|
|
layer->flags = BB_ReadUBits(&br, 16);
|
|
layer->type = BB_ReadUBits(&br, 16);
|
|
layer->child_level = BB_ReadUBits(&br, 16);
|
|
|
|
// Ignoring layer default width & height
|
|
BB_ReadSeekBytes(&br, sizeof(u16) * 2);
|
|
|
|
layer->blend_mode = BB_ReadUBits(&br, 16);
|
|
if (layer->blend_mode != 0)
|
|
{
|
|
ASE_PushError(
|
|
arena,
|
|
&result.errors,
|
|
Lit("Layer has unsupported blend mode (only 'Normal' mode is supported). Tip: Try using 'merge down' to create a normal layer as a workaround")
|
|
);
|
|
goto abort;
|
|
}
|
|
|
|
layer->opacity = BB_ReadUBits(&br, 8);
|
|
if (!(ase_header.flags & 1))
|
|
{
|
|
layer->opacity = 255;
|
|
}
|
|
|
|
BB_ReadSeekBytes(&br, sizeof(u8) * 3);
|
|
|
|
u16 str_len = BB_ReadUBits(&br, 16);
|
|
layer->name = (String) { str_len, PushStructsNoZero(scratch.arena, u8, str_len) };
|
|
BB_ReadBytes(&br, layer->name);
|
|
|
|
if (layer->type == 2)
|
|
{
|
|
layer->tileset_index = BB_ReadUBits(&br, 32);
|
|
}
|
|
|
|
layer->index = num_layers++;
|
|
} break;
|
|
|
|
//////////////////////////////
|
|
//- Decode cel
|
|
|
|
case ASE_ChunkKind_Cel:
|
|
{
|
|
Ace_Cel *cel = PushStruct(scratch.arena, Ace_Cel);
|
|
if (cel_tail)
|
|
{
|
|
cel_tail->next = cel;
|
|
}
|
|
else
|
|
{
|
|
cel_head = cel;
|
|
}
|
|
cel_tail = cel;
|
|
|
|
cel->layer_index = BB_ReadUBits(&br, 16);
|
|
cel->x_pos = BB_ReadIBits(&br, 16);
|
|
cel->y_pos = BB_ReadIBits(&br, 16);
|
|
cel->opacity = BB_ReadUBits(&br, 8);
|
|
cel->type = BB_ReadUBits(&br, 16);
|
|
cel->z_index = BB_ReadIBits(&br, 16);
|
|
BB_ReadSeekBytes(&br, sizeof(u8) * 5);
|
|
|
|
cel->frame_index = num_frames;
|
|
|
|
switch (cel->type)
|
|
{
|
|
case ASE_CelKind_RawImage:
|
|
{
|
|
// Unsupported
|
|
BB_ReadSeekToByte(&br, chunk_end_pos);
|
|
} break;
|
|
|
|
case ASE_CelKind_Linked:
|
|
{
|
|
cel->frame_pos = BB_ReadUBits(&br, 16);
|
|
// Actual linking happens later after iteration
|
|
} break;
|
|
|
|
case ASE_CelKind_CompressedImage:
|
|
{
|
|
cel->width = BB_ReadUBits(&br, 16);
|
|
cel->height = BB_ReadUBits(&br, 16);
|
|
|
|
cel->pixels = PushStructsNoZero(scratch.arena, u32, cel->width * cel->height);
|
|
u8 *huffman_encoded = BB_ReadBytesRaw(&br, chunk_end_pos - BB_GetCurrentReaderByte(&br));
|
|
if (huffman_encoded)
|
|
{
|
|
ASE_Inflate((u8 *)cel->pixels, huffman_encoded);
|
|
}
|
|
} break;
|
|
|
|
case ASE_CelKind_CompressedTilemap:
|
|
{
|
|
// Unsupported
|
|
ASE_PushError(arena, &result.errors, Lit("Tilemaps are not supported"));
|
|
goto abort;
|
|
} break;
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
++num_frames;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Create ordered layers array
|
|
|
|
ASE_Layer **layers_ordered = PushStructsNoZero(scratch.arena, ASE_Layer *, num_layers);
|
|
for (ASE_Layer *layer = layer_head; layer; layer = layer->next)
|
|
{
|
|
layers_ordered[layer->index] = layer;
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Link cels
|
|
|
|
Ace_Cel **cels_ordered = PushStructsNoZero(scratch.arena, Ace_Cel *, num_frames * num_layers);
|
|
for (Ace_Cel *cel = cel_head; cel; cel = cel->next)
|
|
{
|
|
cels_ordered[(cel->frame_index * num_layers) + cel->layer_index] = cel;
|
|
if (cel->type == ASE_CelKind_Linked)
|
|
{
|
|
Ace_Cel *ref_cel = cels_ordered[(cel->frame_pos * num_layers) + cel->layer_index];
|
|
cel->width = ref_cel->width;
|
|
cel->height = ref_cel->height;
|
|
cel->pixels = ref_cel->pixels;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////
|
|
//- Assemble image from cels
|
|
|
|
{
|
|
for (Ace_Cel *cel = cel_head; cel; cel = cel->next)
|
|
{
|
|
ASE_Layer *layer = layers_ordered[cel->layer_index];
|
|
// Only draw visible layers
|
|
if (layer->flags & 1)
|
|
{
|
|
u8 opacity = (cel->opacity / 255.0f) * (layer->opacity / 255.0f) * 255.0f;
|
|
|
|
i32 cel_width = cel->width;
|
|
i32 cel_height = cel->height;
|
|
|
|
i32 cel_left = 0;
|
|
i32 cel_right = cel_width;
|
|
i32 cel_top = 0;
|
|
i32 cel_bottom = cel_height;
|
|
|
|
i32 frame_left = cel->x_pos;
|
|
i32 frame_top = cel->y_pos;
|
|
|
|
i32 image_left = frame_left + ((cel->frame_index % frames_x) * frame_width);
|
|
i32 image_top = frame_top + ((cel->frame_index / frames_x) * frame_height);
|
|
|
|
// Adjust bounds to ensure pixels outside of frame boundaries
|
|
// aren't (aseprite keeps chunks outside of frame around in
|
|
// project file).
|
|
{
|
|
i32 frame_right = cel_width + frame_left;
|
|
i32 frame_bottom = frame_top + cel_height;
|
|
if (frame_left < 0)
|
|
{
|
|
cel_left += -frame_left;
|
|
frame_left = 0;
|
|
}
|
|
if (frame_top < 0)
|
|
{
|
|
cel_top += -frame_top;
|
|
frame_top = 0;
|
|
}
|
|
if (frame_right > (i32)frame_width)
|
|
{
|
|
cel_right -= (frame_right - frame_width);
|
|
frame_right = frame_width;
|
|
}
|
|
if (frame_bottom > (i32)frame_height)
|
|
{
|
|
cel_bottom -= (frame_bottom - frame_height);
|
|
frame_bottom = frame_height;
|
|
}
|
|
}
|
|
|
|
for (i32 cel_y = cel_top; cel_y < cel_bottom; ++cel_y)
|
|
{
|
|
i32 image_y = image_top + cel_y;
|
|
i32 cel_stride = cel_y * cel_width;
|
|
i32 image_stride = image_y * image_width;
|
|
for (i32 cel_x = cel_left; cel_x < cel_right; ++cel_x)
|
|
{
|
|
i32 image_x = image_left + cel_x;
|
|
u32 cel_pixel = cel->pixels[cel_x + cel_stride];
|
|
u32 *image_pixel = &result.pixels[image_x + image_stride];
|
|
*image_pixel = ASE_Blend(cel_pixel, *image_pixel, opacity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assert all data was read
|
|
Assert(BB_NumBytesRemaining(&br) == 0);
|
|
|
|
abort:
|
|
|
|
if (result.errors.count <= 0)
|
|
{
|
|
result.ok = 1;
|
|
}
|
|
|
|
EndScratch(scratch);
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
//~ Decode sheet
|
|
|
|
ASE_DecodedSheet ASE_DecodeSheet(Arena *arena, String encoded)
|
|
{
|
|
ASE_DecodedSheet result = Zi;
|
|
|
|
BB_Buff bb = BB_BuffFromString(encoded);
|
|
BB_Reader br = BB_ReaderFromBuffNoDebug(&bb);
|
|
ASE_Header ase_header;
|
|
BB_ReadBytes(&br, StringFromStruct(&ase_header));
|
|
|
|
if (ase_header.magic != 0xA5E0)
|
|
{
|
|
ASE_PushError(arena, &result.errors, Lit("Not a valid aseprite file"));
|
|
goto abort;
|
|
}
|
|
|
|
u64 frame_width = ase_header.width;
|
|
u64 frame_height = ase_header.height;
|
|
|
|
u32 frames_x = ase_header.frames;
|
|
u32 frames_y = 1;
|
|
u64 image_width = frame_width * frames_x;
|
|
u64 image_height = frame_height * frames_y;
|
|
ASE_MakeDimensionsSquareish(&ase_header, &frames_x, &frames_y, &image_width, &image_height);
|
|
|
|
u32 num_frames = 0;
|
|
ASE_Frame *first_frame = 0;
|
|
|
|
u32 num_spans = 0;
|
|
ASE_Span *first_span = 0;
|
|
|
|
u32 num_slice_keys = 0;
|
|
ASE_SliceKey *first_slice_key = 0;
|
|
|
|
//////////////////////////////
|
|
//- Iterate frames
|
|
|
|
for (u16 i = 0; i < ase_header.frames; ++i)
|
|
{
|
|
ASE_FrameHeader frame_header;
|
|
BB_ReadBytes(&br, StringFromStruct(&frame_header));
|
|
|
|
u32 num_chunks = frame_header.chunks_new;
|
|
if (num_chunks == 0)
|
|
{
|
|
Assert(frame_header.chunks_old != 0xFFFF);
|
|
num_chunks = frame_header.chunks_old;
|
|
}
|
|
|
|
ASE_Frame *frame = PushStruct(arena, ASE_Frame);
|
|
frame->next = first_frame;
|
|
first_frame = frame;
|
|
|
|
frame->index = i;
|
|
frame->duration = frame_header.frame_duration_ms / 1000.0;
|
|
|
|
u32 frame_tile_x = i % frames_x;
|
|
u32 frame_tile_y = i / frames_x;
|
|
|
|
Vec2I32 frame_p0 = VEC2I32(frame_tile_x * frame_width, frame_tile_y * frame_height);
|
|
Vec2I32 frame_p1 = AddVec2I32(frame_p0, VEC2I32(frame_width, frame_height));
|
|
frame->rect.p0 = frame_p0;
|
|
frame->rect.p1 = frame_p1;
|
|
|
|
frame->index = i;
|
|
frame->duration = frame_header.frame_duration_ms / 1000.0;
|
|
|
|
//////////////////////////////
|
|
//- Iterate chunks
|
|
|
|
for (u32 j = 0; j < num_chunks; ++j)
|
|
{
|
|
u32 chunk_size = BB_ReadUBits(&br, 32);
|
|
ASE_ChunkKind chunk_type = BB_ReadUBits(&br, 16);
|
|
|
|
// Chunk size includes size & type
|
|
Assert(chunk_size >= 6);
|
|
chunk_size -= 6;
|
|
|
|
u64 chunk_end_pos = BB_GetCurrentReaderByte(&br) + chunk_size;
|
|
|
|
switch (chunk_type)
|
|
{
|
|
default:
|
|
{
|
|
BB_ReadSeekToByte(&br, chunk_end_pos);
|
|
} break;
|
|
|
|
//- Decode tags
|
|
case ASE_ChunkKind_Tags:
|
|
{
|
|
u16 frame_span_count = BB_ReadUBits(&br, 16);
|
|
BB_ReadSeekBytes(&br, 8);
|
|
|
|
for (u16 k = 0; k < frame_span_count; ++k)
|
|
{
|
|
ASE_Span *span = PushStruct(arena, ASE_Span);
|
|
span->next = first_span;
|
|
first_span = span;
|
|
|
|
span->start = BB_ReadUBits(&br, 16);
|
|
span->end = BB_ReadUBits(&br, 16);
|
|
BB_ReadSeekBytes(&br, 13);
|
|
|
|
u16 str_len = BB_ReadUBits(&br, 16);
|
|
span->name = (String) { str_len, PushStructsNoZero(arena, u8, str_len) };
|
|
BB_ReadBytes(&br, span->name);
|
|
|
|
++num_spans;
|
|
}
|
|
|
|
} break;
|
|
|
|
//////////////////////////////
|
|
//- Decode slice
|
|
|
|
case ASE_ChunkKind_Slice:
|
|
{
|
|
ASE_SliceKey *slice_key = PushStruct(arena, ASE_SliceKey);
|
|
slice_key->next = first_slice_key;
|
|
first_slice_key = slice_key;
|
|
|
|
u32 num_slices = BB_ReadUBits(&br, 32);
|
|
slice_key->num_slices = num_slices;
|
|
|
|
u32 flags = BB_ReadUBits(&br, 32);
|
|
BB_ReadSeekBytes(&br, 4);
|
|
|
|
u16 str_len = BB_ReadUBits(&br, 16);
|
|
slice_key->name = (String) { str_len, PushStructsNoZero(arena, u8, str_len) };
|
|
BB_ReadBytes(&br, slice_key->name);
|
|
|
|
for (u32 k = 0; k < num_slices; ++k)
|
|
{
|
|
ASE_Slice *slice = PushStruct(arena, ASE_Slice);
|
|
slice->next = slice_key->first_slice;
|
|
slice_key->first_slice = slice;
|
|
|
|
u32 start = BB_ReadUBits(&br, 32);
|
|
i32 x = BB_ReadIBits(&br, 32);
|
|
i32 y = BB_ReadIBits(&br, 32);
|
|
u32 width = BB_ReadUBits(&br, 32);
|
|
u32 height = BB_ReadUBits(&br, 32);
|
|
if (flags & 0x01)
|
|
{
|
|
// Skip 9-patches info
|
|
BB_ReadSeekBytes(&br, 128);
|
|
}
|
|
if (flags & 0x02)
|
|
{
|
|
// Skip pivot info
|
|
BB_ReadSeekBytes(&br, 64);
|
|
}
|
|
|
|
slice->start = start;
|
|
slice->rect.p0 = VEC2I32(x, y);
|
|
slice->rect.p1 = VEC2I32(x + width, y + width);
|
|
}
|
|
|
|
++num_slice_keys;
|
|
} break;
|
|
|
|
// TODO
|
|
//case ASE_ChunkKind_Userdata
|
|
}
|
|
}
|
|
++num_frames;
|
|
}
|
|
|
|
// Assert all data was read
|
|
Assert(BB_NumBytesRemaining(&br) == 0);
|
|
|
|
result.image_size = VEC2(image_width, image_height);
|
|
result.frame_size = VEC2(frame_width, frame_height);
|
|
result.num_frames = num_frames;
|
|
result.num_spans = num_spans;
|
|
result.num_slice_keys = num_slice_keys;
|
|
result.first_frame = first_frame;
|
|
result.first_span = first_span;
|
|
result.first_slice_key = first_slice_key;
|
|
|
|
abort:
|
|
|
|
if (result.errors.count <= 0)
|
|
{
|
|
result.ok = 1;
|
|
}
|
|
|
|
return result;
|
|
}
|