936 lines
35 KiB
C
936 lines
35 KiB
C
#include "pxl8_ase.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define MINIZ_NO_STDIO
|
|
#define MINIZ_NO_TIME
|
|
#define MINIZ_NO_ARCHIVE_APIS
|
|
#define MINIZ_NO_ARCHIVE_WRITING_APIS
|
|
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
|
|
|
|
#include <miniz.h>
|
|
|
|
#include "pxl8_color.h"
|
|
#include "pxl8_io.h"
|
|
#include "pxl8_log.h"
|
|
#include "pxl8_mem.h"
|
|
|
|
static pxl8_result parse_ase_header(pxl8_stream* stream, pxl8_ase_header* header) {
|
|
header->file_size = pxl8_read_u32(stream);
|
|
header->magic = pxl8_read_u16(stream);
|
|
header->frames = pxl8_read_u16(stream);
|
|
header->width = pxl8_read_u16(stream);
|
|
header->height = pxl8_read_u16(stream);
|
|
header->color_depth = pxl8_read_u16(stream);
|
|
header->flags = pxl8_read_u32(stream);
|
|
header->speed = pxl8_read_u16(stream);
|
|
pxl8_skip_bytes(stream, 4);
|
|
header->transparent_index = pxl8_read_u32(stream);
|
|
header->n_colors = pxl8_read_u8(stream);
|
|
header->pixel_width = pxl8_read_u8(stream);
|
|
header->pixel_height = pxl8_read_u8(stream);
|
|
header->grid_x = pxl8_read_i16(stream);
|
|
header->grid_y = pxl8_read_i16(stream);
|
|
header->grid_width = pxl8_read_u16(stream);
|
|
header->grid_height = pxl8_read_u16(stream);
|
|
|
|
if (header->magic != PXL8_ASE_MAGIC) {
|
|
pxl8_error("Invalid ASE file magic: 0x%04X", header->magic);
|
|
return PXL8_ERROR_ASE_INVALID_MAGIC;
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
static pxl8_result parse_old_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* palette) {
|
|
u16 packet_count = pxl8_read_u16(stream);
|
|
|
|
u32 total_colors = 0;
|
|
u32 temp_pos = pxl8_stream_position(stream);
|
|
for (u16 packet = 0; packet < packet_count; packet++) {
|
|
u8 skip_colors = pxl8_read_u8(stream);
|
|
u8 colors_in_packet = pxl8_read_u8(stream);
|
|
u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet;
|
|
total_colors += skip_colors + actual_colors;
|
|
pxl8_skip_bytes(stream, actual_colors * 3);
|
|
}
|
|
|
|
palette->entry_count = total_colors;
|
|
palette->first_color = 0;
|
|
palette->last_color = total_colors - 1;
|
|
palette->colors = (u32*)pxl8_malloc(total_colors * sizeof(u32));
|
|
if (!palette->colors) {
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
pxl8_stream_seek(stream, temp_pos);
|
|
|
|
u32 color_index = 0;
|
|
for (u16 packet = 0; packet < packet_count; packet++) {
|
|
u8 skip_colors = pxl8_read_u8(stream);
|
|
u8 colors_in_packet = pxl8_read_u8(stream);
|
|
u32 actual_colors = (colors_in_packet == 0) ? 256 : colors_in_packet;
|
|
|
|
for (u32 skip = 0; skip < skip_colors && color_index < total_colors; skip++) {
|
|
palette->colors[color_index++] = 0xFF000000;
|
|
}
|
|
|
|
for (u32 color = 0; color < actual_colors && color_index < total_colors; color++) {
|
|
u8 r = pxl8_read_u8(stream);
|
|
u8 g = pxl8_read_u8(stream);
|
|
u8 b = pxl8_read_u8(stream);
|
|
palette->colors[color_index++] = 0xFF000000 | ((u32)b << 16) | ((u32)g << 8) | r;
|
|
}
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
static pxl8_result parse_layer_chunk(pxl8_stream* stream, pxl8_ase_layer* layer) {
|
|
layer->flags = pxl8_read_u16(stream);
|
|
layer->layer_type = pxl8_read_u16(stream);
|
|
layer->child_level = pxl8_read_u16(stream);
|
|
pxl8_skip_bytes(stream, 4);
|
|
layer->blend_mode = pxl8_read_u16(stream);
|
|
layer->opacity = pxl8_read_u8(stream);
|
|
pxl8_skip_bytes(stream, 3);
|
|
|
|
u16 name_len = pxl8_read_u16(stream);
|
|
if (name_len > 0) {
|
|
layer->name = (char*)pxl8_malloc(name_len + 1);
|
|
if (!layer->name) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
pxl8_read_bytes(stream, layer->name, name_len);
|
|
layer->name[name_len] = '\0';
|
|
} else {
|
|
layer->name = NULL;
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
static pxl8_result parse_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* palette) {
|
|
pxl8_skip_bytes(stream, 4);
|
|
palette->first_color = pxl8_read_u32(stream);
|
|
palette->last_color = pxl8_read_u32(stream);
|
|
pxl8_skip_bytes(stream, 8);
|
|
|
|
u32 color_count = palette->last_color - palette->first_color + 1;
|
|
|
|
if (color_count == 0 || color_count > 65536) {
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
|
|
palette->colors = (u32*)pxl8_malloc(color_count * sizeof(u32));
|
|
if (!palette->colors) {
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
palette->entry_count = color_count;
|
|
|
|
for (u32 i = 0; i < color_count; i++) {
|
|
u16 flags = pxl8_read_u16(stream);
|
|
u8 r = pxl8_read_u8(stream);
|
|
u8 g = pxl8_read_u8(stream);
|
|
u8 b = pxl8_read_u8(stream);
|
|
u8 a = pxl8_read_u8(stream);
|
|
|
|
palette->colors[i] = ((u32)a << 24) | ((u32)b << 16) | ((u32)g << 8) | r;
|
|
|
|
if (flags & 1) {
|
|
u16 name_len = pxl8_read_u16(stream);
|
|
pxl8_skip_bytes(stream, name_len);
|
|
}
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
static pxl8_result parse_user_data_chunk(pxl8_stream* stream, pxl8_ase_user_data* user_data) {
|
|
u32 flags = pxl8_read_u32(stream);
|
|
|
|
user_data->has_text = (flags & 1) != 0;
|
|
user_data->has_color = (flags & 2) != 0;
|
|
|
|
if (user_data->has_text) {
|
|
u16 text_len = pxl8_read_u16(stream);
|
|
if (text_len > 0) {
|
|
user_data->text = (char*)pxl8_malloc(text_len + 1);
|
|
if (!user_data->text) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
pxl8_read_bytes(stream, user_data->text, text_len);
|
|
user_data->text[text_len] = '\0';
|
|
}
|
|
}
|
|
|
|
if (user_data->has_color) {
|
|
u8 r = pxl8_read_u8(stream);
|
|
u8 g = pxl8_read_u8(stream);
|
|
u8 b = pxl8_read_u8(stream);
|
|
u8 a = pxl8_read_u8(stream);
|
|
user_data->color = ((u32)a << 24) | ((u32)b << 16) | ((u32)g << 8) | r;
|
|
}
|
|
|
|
if (flags & 4) {
|
|
pxl8_skip_bytes(stream, 4);
|
|
u32 num_properties = pxl8_read_u32(stream);
|
|
|
|
if (num_properties > 0) {
|
|
user_data->properties = (pxl8_ase_property*)pxl8_calloc(num_properties, sizeof(pxl8_ase_property));
|
|
if (!user_data->properties) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
user_data->property_count = num_properties;
|
|
|
|
for (u32 i = 0; i < num_properties; i++) {
|
|
u16 name_len = pxl8_read_u16(stream);
|
|
if (name_len > 0) {
|
|
user_data->properties[i].name = (char*)pxl8_malloc(name_len + 1);
|
|
if (!user_data->properties[i].name) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
pxl8_read_bytes(stream, user_data->properties[i].name, name_len);
|
|
user_data->properties[i].name[name_len] = '\0';
|
|
}
|
|
|
|
u16 type = pxl8_read_u16(stream);
|
|
user_data->properties[i].type = type;
|
|
|
|
switch (type) {
|
|
case 1:
|
|
user_data->properties[i].bool_val = pxl8_read_u8(stream) != 0;
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
case 4:
|
|
case 5:
|
|
user_data->properties[i].int_val = pxl8_read_i32(stream);
|
|
break;
|
|
case 6:
|
|
case 7:
|
|
user_data->properties[i].float_val = pxl8_read_f32(stream);
|
|
break;
|
|
case 8: {
|
|
u16 str_len = pxl8_read_u16(stream);
|
|
if (str_len > 0) {
|
|
user_data->properties[i].string_val = (char*)pxl8_malloc(str_len + 1);
|
|
if (!user_data->properties[i].string_val) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
pxl8_read_bytes(stream, user_data->properties[i].string_val, str_len);
|
|
user_data->properties[i].string_val[str_len] = '\0';
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
pxl8_skip_bytes(stream, 4);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
static pxl8_result parse_tileset_chunk(pxl8_stream* stream, pxl8_ase_tileset* tileset) {
|
|
tileset->id = pxl8_read_u32(stream);
|
|
tileset->flags = pxl8_read_u32(stream);
|
|
tileset->tile_count = pxl8_read_u32(stream);
|
|
tileset->tile_width = pxl8_read_u16(stream);
|
|
tileset->tile_height = pxl8_read_u16(stream);
|
|
tileset->base_index = pxl8_read_i16(stream);
|
|
pxl8_skip_bytes(stream, 14);
|
|
|
|
u16 name_len = pxl8_read_u16(stream);
|
|
if (name_len > 0) {
|
|
tileset->name = (char*)pxl8_malloc(name_len + 1);
|
|
if (!tileset->name) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
pxl8_read_bytes(stream, tileset->name, name_len);
|
|
tileset->name[name_len] = '\0';
|
|
}
|
|
|
|
if (tileset->flags & 1) {
|
|
pxl8_skip_bytes(stream, 8); // external_file_id + tileset_id
|
|
}
|
|
|
|
if (tileset->flags & 2) {
|
|
u32 compressed_size = pxl8_read_u32(stream);
|
|
tileset->pixels_size = tileset->tile_width * tileset->tile_height * tileset->tile_count;
|
|
tileset->pixels = (u8*)pxl8_malloc(tileset->pixels_size);
|
|
if (!tileset->pixels) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
|
mz_ulong dest_len = tileset->pixels_size;
|
|
i32 result = mz_uncompress(tileset->pixels, &dest_len, compressed_data, compressed_size);
|
|
if (result != MZ_OK) {
|
|
pxl8_error("Failed to decompress tileset data: miniz error %d", result);
|
|
pxl8_free(tileset->pixels);
|
|
tileset->pixels = NULL;
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
}
|
|
|
|
tileset->tile_user_data = (pxl8_ase_user_data*)pxl8_calloc(tileset->tile_count, sizeof(pxl8_ase_user_data));
|
|
if (!tileset->tile_user_data) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase_cel* cel) {
|
|
if (chunk_size < 9) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
|
|
cel->layer_index = pxl8_read_u16(stream);
|
|
cel->x = pxl8_read_i16(stream);
|
|
cel->y = pxl8_read_i16(stream);
|
|
cel->opacity = pxl8_read_u8(stream);
|
|
cel->cel_type = pxl8_read_u16(stream);
|
|
pxl8_skip_bytes(stream, 7);
|
|
|
|
if (cel->cel_type == 2) {
|
|
if (chunk_size < 20) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
|
|
cel->image.width = pxl8_read_u16(stream);
|
|
cel->image.height = pxl8_read_u16(stream);
|
|
|
|
u32 pixels_size = cel->image.width * cel->image.height;
|
|
u32 compressed_size = chunk_size - 20;
|
|
|
|
cel->image.pixels = (u8*)pxl8_malloc(pixels_size);
|
|
if (!cel->image.pixels) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
|
mz_ulong dest_len = pixels_size;
|
|
i32 result = mz_uncompress(cel->image.pixels, &dest_len, compressed_data, compressed_size);
|
|
if (result != MZ_OK) {
|
|
pxl8_error("Failed to decompress cel data: miniz error %d", result);
|
|
pxl8_free(cel->image.pixels);
|
|
cel->image.pixels = NULL;
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
|
|
if (dest_len != pixels_size) {
|
|
pxl8_warn("Decompressed size mismatch: expected %u, got %lu", pixels_size, dest_len);
|
|
}
|
|
} else if (cel->cel_type == 3) {
|
|
if (chunk_size < 36) return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
|
|
cel->tilemap.width = pxl8_read_u16(stream);
|
|
cel->tilemap.height = pxl8_read_u16(stream);
|
|
cel->tilemap.bits_per_tile = pxl8_read_u16(stream);
|
|
cel->tilemap.tile_id_mask = pxl8_read_u32(stream);
|
|
cel->tilemap.x_flip_mask = pxl8_read_u32(stream);
|
|
cel->tilemap.y_flip_mask = pxl8_read_u32(stream);
|
|
cel->tilemap.diag_flip_mask = pxl8_read_u32(stream);
|
|
pxl8_skip_bytes(stream, 10);
|
|
|
|
u32 tile_count = cel->tilemap.width * cel->tilemap.height;
|
|
u32 bytes_per_tile = (cel->tilemap.bits_per_tile + 7) / 8;
|
|
u32 uncompressed_size = tile_count * bytes_per_tile;
|
|
u32 compressed_size = chunk_size - 36;
|
|
|
|
u8* temp_buffer = (u8*)pxl8_malloc(uncompressed_size);
|
|
if (!temp_buffer) return PXL8_ERROR_OUT_OF_MEMORY;
|
|
|
|
const u8* compressed_data = pxl8_read_ptr(stream, compressed_size);
|
|
mz_ulong dest_len = uncompressed_size;
|
|
i32 result = mz_uncompress(temp_buffer, &dest_len, compressed_data, compressed_size);
|
|
if (result != MZ_OK) {
|
|
pxl8_error("Failed to decompress tilemap data: miniz error %d", result);
|
|
pxl8_free(temp_buffer);
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
|
|
cel->tilemap.tiles = (u32*)pxl8_calloc(tile_count, sizeof(u32));
|
|
if (!cel->tilemap.tiles) {
|
|
pxl8_free(temp_buffer);
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
for (u32 i = 0; i < tile_count; i++) {
|
|
if (cel->tilemap.bits_per_tile == 32) {
|
|
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 4 + 0]) |
|
|
((u32)temp_buffer[i * 4 + 1] << 8) |
|
|
((u32)temp_buffer[i * 4 + 2] << 16) |
|
|
((u32)temp_buffer[i * 4 + 3] << 24);
|
|
} else if (cel->tilemap.bits_per_tile == 16) {
|
|
cel->tilemap.tiles[i] = ((u32)temp_buffer[i * 2 + 0]) |
|
|
((u32)temp_buffer[i * 2 + 1] << 8);
|
|
} else if (cel->tilemap.bits_per_tile == 8) {
|
|
cel->tilemap.tiles[i] = temp_buffer[i];
|
|
}
|
|
}
|
|
|
|
pxl8_free(temp_buffer);
|
|
}
|
|
|
|
return PXL8_OK;
|
|
}
|
|
|
|
pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
|
|
if (!filepath || !ase_file) {
|
|
return PXL8_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
memset(ase_file, 0, sizeof(pxl8_ase_file));
|
|
|
|
u8* file_data;
|
|
usize file_size;
|
|
pxl8_result result = pxl8_io_read_binary_file(filepath, &file_data, &file_size);
|
|
if (result != PXL8_OK) {
|
|
return result;
|
|
}
|
|
|
|
if (file_size < 128) {
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_ASE_TRUNCATED_FILE;
|
|
}
|
|
|
|
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
|
|
|
|
result = parse_ase_header(&stream, &ase_file->header);
|
|
if (result != PXL8_OK) {
|
|
pxl8_io_free_binary_data(file_data);
|
|
return result;
|
|
}
|
|
|
|
ase_file->frame_count = ase_file->header.frames;
|
|
ase_file->frames = (pxl8_ase_frame*)pxl8_calloc(ase_file->frame_count, sizeof(pxl8_ase_frame));
|
|
if (!ase_file->frames) {
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
pxl8_stream_seek(&stream, 128);
|
|
|
|
for (u16 frame_idx = 0; frame_idx < ase_file->header.frames; frame_idx++) {
|
|
u32 frame_start = pxl8_stream_position(&stream);
|
|
|
|
pxl8_ase_frame_header frame_header;
|
|
frame_header.frame_bytes = pxl8_read_u32(&stream);
|
|
frame_header.magic = pxl8_read_u16(&stream);
|
|
u16 old_chunks = pxl8_read_u16(&stream);
|
|
frame_header.duration = pxl8_read_u16(&stream);
|
|
pxl8_skip_bytes(&stream, 2);
|
|
|
|
if (frame_header.magic != PXL8_ASE_FRAME_MAGIC) {
|
|
pxl8_error("Invalid frame magic: 0x%04X", frame_header.magic);
|
|
result = PXL8_ERROR_ASE_INVALID_FRAME_MAGIC;
|
|
break;
|
|
}
|
|
|
|
if (old_chunks == 0xFFFF || old_chunks == 0xFFFE) {
|
|
frame_header.chunks = pxl8_read_u32(&stream);
|
|
} else {
|
|
frame_header.chunks = old_chunks;
|
|
pxl8_skip_bytes(&stream, 4);
|
|
}
|
|
|
|
pxl8_ase_frame* frame = &ase_file->frames[frame_idx];
|
|
frame->frame_id = frame_idx;
|
|
frame->width = ase_file->header.width;
|
|
frame->height = ase_file->header.height;
|
|
frame->duration = frame_header.duration;
|
|
|
|
u32 pixel_count = frame->width * frame->height;
|
|
frame->pixels = (u8*)pxl8_calloc(pixel_count, sizeof(u8));
|
|
if (!frame->pixels) {
|
|
result = PXL8_ERROR_OUT_OF_MEMORY;
|
|
break;
|
|
}
|
|
|
|
u32 last_tileset_idx = 0xFFFFFFFF;
|
|
u32 tileset_tile_idx = 0;
|
|
|
|
for (u16 chunk_idx = 0; chunk_idx < frame_header.chunks; chunk_idx++) {
|
|
u32 chunk_start = pxl8_stream_position(&stream);
|
|
|
|
pxl8_ase_chunk_header chunk_header;
|
|
chunk_header.chunk_size = pxl8_read_u32(&stream);
|
|
chunk_header.chunk_type = pxl8_read_u16(&stream);
|
|
|
|
if (chunk_header.chunk_size < 6 || chunk_header.chunk_size > frame_header.frame_bytes) {
|
|
pxl8_error("Invalid chunk size %u in ASE file (frame_bytes=%u)",
|
|
chunk_header.chunk_size, frame_header.frame_bytes);
|
|
result = PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
break;
|
|
}
|
|
|
|
switch (chunk_header.chunk_type) {
|
|
case PXL8_ASE_CHUNK_OLD_PALETTE:
|
|
if (!ase_file->palette.colors) {
|
|
result = parse_old_palette_chunk(&stream, &ase_file->palette);
|
|
}
|
|
break;
|
|
|
|
case PXL8_ASE_CHUNK_LAYER: {
|
|
pxl8_ase_layer* new_layers =
|
|
(pxl8_ase_layer*)pxl8_realloc(ase_file->layers,
|
|
(ase_file->layer_count + 1) * sizeof(pxl8_ase_layer));
|
|
if (!new_layers) {
|
|
result = PXL8_ERROR_OUT_OF_MEMORY;
|
|
break;
|
|
}
|
|
ase_file->layers = new_layers;
|
|
|
|
result = parse_layer_chunk(&stream, &ase_file->layers[ase_file->layer_count]);
|
|
if (result == PXL8_OK) {
|
|
ase_file->layer_count++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PXL8_ASE_CHUNK_CEL: {
|
|
pxl8_ase_cel cel = {0};
|
|
result = parse_cel_chunk(&stream, chunk_header.chunk_size - 6, &cel);
|
|
if (result == PXL8_OK) {
|
|
pxl8_ase_cel* new_cels = (pxl8_ase_cel*)pxl8_realloc(frame->cels, (frame->cel_count + 1) * sizeof(pxl8_ase_cel));
|
|
if (!new_cels) {
|
|
result = PXL8_ERROR_OUT_OF_MEMORY;
|
|
break;
|
|
}
|
|
frame->cels = new_cels;
|
|
frame->cels[frame->cel_count] = cel;
|
|
frame->cel_count++;
|
|
|
|
if (cel.cel_type == 2 && cel.image.pixels) {
|
|
u32 copy_width = (cel.image.width < frame->width) ? cel.image.width : frame->width;
|
|
u32 copy_height = (cel.image.height < frame->height) ? cel.image.height : frame->height;
|
|
|
|
for (u32 y = 0; y < copy_height; y++) {
|
|
u32 src_offset = y * cel.image.width;
|
|
u32 dst_offset = (y + cel.y) * frame->width + cel.x;
|
|
if (dst_offset + copy_width <= pixel_count) {
|
|
for (u32 x = 0; x < copy_width; x++) {
|
|
u8 src_pixel = cel.image.pixels[src_offset + x];
|
|
bool is_transparent = false;
|
|
|
|
if (src_pixel < ase_file->palette.entry_count && ase_file->palette.colors) {
|
|
u32 color = ase_file->palette.colors[src_pixel];
|
|
is_transparent = ((color >> 24) & 0xFF) == 0;
|
|
}
|
|
|
|
if (!is_transparent) {
|
|
frame->pixels[dst_offset + x] = src_pixel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PXL8_ASE_CHUNK_PALETTE:
|
|
if (ase_file->palette.colors) {
|
|
pxl8_free(ase_file->palette.colors);
|
|
ase_file->palette.colors = NULL;
|
|
}
|
|
result = parse_palette_chunk(&stream, &ase_file->palette);
|
|
break;
|
|
|
|
case PXL8_ASE_CHUNK_TILESET: {
|
|
pxl8_ase_tileset* new_tilesets = (pxl8_ase_tileset*)pxl8_realloc(ase_file->tilesets,
|
|
(ase_file->tileset_count + 1) * sizeof(pxl8_ase_tileset));
|
|
if (!new_tilesets) {
|
|
result = PXL8_ERROR_OUT_OF_MEMORY;
|
|
break;
|
|
}
|
|
ase_file->tilesets = new_tilesets;
|
|
|
|
memset(&ase_file->tilesets[ase_file->tileset_count], 0, sizeof(pxl8_ase_tileset));
|
|
result = parse_tileset_chunk(&stream, &ase_file->tilesets[ase_file->tileset_count]);
|
|
if (result == PXL8_OK) {
|
|
last_tileset_idx = ase_file->tileset_count;
|
|
tileset_tile_idx = 0;
|
|
ase_file->tileset_count++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case PXL8_ASE_CHUNK_USER_DATA: {
|
|
if (last_tileset_idx != 0xFFFFFFFF) {
|
|
pxl8_ase_tileset* tileset = &ase_file->tilesets[last_tileset_idx];
|
|
if (tileset_tile_idx < tileset->tile_count) {
|
|
result = parse_user_data_chunk(&stream, &tileset->tile_user_data[tileset_tile_idx]);
|
|
tileset_tile_idx++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (result != PXL8_OK) break;
|
|
|
|
pxl8_stream_seek(&stream, chunk_start + chunk_header.chunk_size);
|
|
}
|
|
|
|
if (result != PXL8_OK) break;
|
|
|
|
pxl8_stream_seek(&stream, frame_start + frame_header.frame_bytes);
|
|
}
|
|
|
|
pxl8_io_free_binary_data(file_data);
|
|
|
|
if (result != PXL8_OK) {
|
|
pxl8_ase_destroy(ase_file);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
|
|
if (!ase_file) return;
|
|
|
|
if (ase_file->frames) {
|
|
for (u32 i = 0; i < ase_file->frame_count; i++) {
|
|
if (ase_file->frames[i].pixels) pxl8_free(ase_file->frames[i].pixels);
|
|
if (ase_file->frames[i].cels) {
|
|
for (u32 j = 0; j < ase_file->frames[i].cel_count; j++) {
|
|
pxl8_ase_cel* cel = &ase_file->frames[i].cels[j];
|
|
if (cel->cel_type == 2 && cel->image.pixels) {
|
|
pxl8_free(cel->image.pixels);
|
|
} else if (cel->cel_type == 3 && cel->tilemap.tiles) {
|
|
pxl8_free(cel->tilemap.tiles);
|
|
}
|
|
}
|
|
pxl8_free(ase_file->frames[i].cels);
|
|
}
|
|
}
|
|
pxl8_free(ase_file->frames);
|
|
}
|
|
|
|
if (ase_file->palette.colors) {
|
|
pxl8_free(ase_file->palette.colors);
|
|
}
|
|
|
|
if (ase_file->layers) {
|
|
for (u32 i = 0; i < ase_file->layer_count; i++) {
|
|
if (ase_file->layers[i].name) {
|
|
pxl8_free(ase_file->layers[i].name);
|
|
}
|
|
}
|
|
pxl8_free(ase_file->layers);
|
|
}
|
|
|
|
if (ase_file->tilesets) {
|
|
for (u32 i = 0; i < ase_file->tileset_count; i++) {
|
|
if (ase_file->tilesets[i].name) pxl8_free(ase_file->tilesets[i].name);
|
|
if (ase_file->tilesets[i].pixels) pxl8_free(ase_file->tilesets[i].pixels);
|
|
if (ase_file->tilesets[i].tile_user_data) {
|
|
for (u32 j = 0; j < ase_file->tilesets[i].tile_count; j++) {
|
|
pxl8_ase_user_data* ud = &ase_file->tilesets[i].tile_user_data[j];
|
|
if (ud->text) pxl8_free(ud->text);
|
|
if (ud->properties) {
|
|
for (u32 k = 0; k < ud->property_count; k++) {
|
|
if (ud->properties[k].name) pxl8_free(ud->properties[k].name);
|
|
if (ud->properties[k].type == 8 && ud->properties[k].string_val) {
|
|
pxl8_free(ud->properties[k].string_val);
|
|
}
|
|
}
|
|
pxl8_free(ud->properties);
|
|
}
|
|
}
|
|
pxl8_free(ase_file->tilesets[i].tile_user_data);
|
|
}
|
|
}
|
|
pxl8_free(ase_file->tilesets);
|
|
}
|
|
|
|
memset(ase_file, 0, sizeof(pxl8_ase_file));
|
|
}
|
|
|
|
pxl8_result pxl8_ase_load_palette(const char* filepath, u32* colors, u32* count) {
|
|
if (!filepath || !colors || !count) {
|
|
return PXL8_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
pxl8_ase_file ase;
|
|
pxl8_result result = pxl8_ase_load(filepath, &ase);
|
|
if (result != PXL8_OK) {
|
|
return result;
|
|
}
|
|
|
|
u32 n = ase.palette.entry_count;
|
|
if (n > 256) n = 256;
|
|
|
|
for (u32 i = 0; i < n; i++) {
|
|
colors[i] = ase.palette.colors[i];
|
|
}
|
|
*count = n;
|
|
|
|
pxl8_ase_destroy(&ase);
|
|
return PXL8_OK;
|
|
}
|
|
|
|
pxl8_result pxl8_ase_remap(const char* input_path, const char* output_path, const pxl8_ase_remap_config* config) {
|
|
if (!input_path || !output_path || !config) {
|
|
return PXL8_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
if (!config->palette || config->palette_count == 0) {
|
|
return PXL8_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
f32 hue_tol = config->hue_tolerance > 0.0f ? config->hue_tolerance : 0.08f;
|
|
|
|
u8* file_data;
|
|
usize file_size;
|
|
pxl8_result result = pxl8_io_read_binary_file(input_path, &file_data, &file_size);
|
|
if (result != PXL8_OK) {
|
|
return result;
|
|
}
|
|
|
|
if (file_size < 128) {
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_ASE_TRUNCATED_FILE;
|
|
}
|
|
|
|
u8* output_data = (u8*)pxl8_malloc(file_size + 65536);
|
|
if (!output_data) {
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
memcpy(output_data, file_data, file_size);
|
|
|
|
pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);
|
|
pxl8_stream_seek(&stream, 128);
|
|
|
|
u32 frame_start = 128;
|
|
u32 frame_size = pxl8_read_u32(&stream);
|
|
pxl8_skip_bytes(&stream, 2);
|
|
u16 old_chunks = pxl8_read_u16(&stream);
|
|
pxl8_skip_bytes(&stream, 2);
|
|
u16 num_chunks = old_chunks;
|
|
if (old_chunks == 0xFFFF || old_chunks == 0xFFFE) {
|
|
num_chunks = (u16)pxl8_read_u32(&stream);
|
|
} else {
|
|
pxl8_skip_bytes(&stream, 4);
|
|
}
|
|
|
|
u32 palette_chunk_offset = 0;
|
|
u32 palette_entry_start = 0;
|
|
u32 orig_colors[256] = {0};
|
|
|
|
u32 chunk_offset = frame_start + 16;
|
|
for (u16 c = 0; c < num_chunks; c++) {
|
|
pxl8_stream_seek(&stream, chunk_offset);
|
|
u32 chunk_size = pxl8_read_u32(&stream);
|
|
u16 chunk_type = pxl8_read_u16(&stream);
|
|
|
|
if (chunk_type == PXL8_ASE_CHUNK_PALETTE) {
|
|
palette_chunk_offset = chunk_offset;
|
|
pxl8_skip_bytes(&stream, 4);
|
|
u32 first_color = pxl8_read_u32(&stream);
|
|
u32 last_color = pxl8_read_u32(&stream);
|
|
pxl8_skip_bytes(&stream, 8);
|
|
palette_entry_start = pxl8_stream_position(&stream);
|
|
|
|
for (u32 i = first_color; i <= last_color && i < 256; i++) {
|
|
u16 flags = pxl8_read_u16(&stream);
|
|
u8 r = pxl8_read_u8(&stream);
|
|
u8 g = pxl8_read_u8(&stream);
|
|
u8 b = pxl8_read_u8(&stream);
|
|
pxl8_skip_bytes(&stream, 1);
|
|
orig_colors[i] = r | (g << 8) | (b << 16);
|
|
if (flags & 1) {
|
|
u16 name_len = pxl8_read_u16(&stream);
|
|
pxl8_skip_bytes(&stream, name_len);
|
|
}
|
|
}
|
|
}
|
|
chunk_offset += chunk_size;
|
|
}
|
|
|
|
if (palette_entry_start == 0) {
|
|
pxl8_free(output_data);
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
|
|
u8 remap[256];
|
|
bool used[256] = {0};
|
|
for (u32 i = 0; i < 256; i++) {
|
|
u32 src = orig_colors[i];
|
|
f32 src_hue = pxl8_color_hue(src);
|
|
f32 src_sat = pxl8_color_saturation(src);
|
|
f32 src_lum = pxl8_color_luminance(src);
|
|
|
|
u32 best_idx = 0;
|
|
f32 best_score = 999999.0f;
|
|
|
|
for (u32 j = 0; j < config->palette_count; j++) {
|
|
u32 tgt = config->palette[j];
|
|
f32 tgt_hue = pxl8_color_hue(tgt);
|
|
f32 tgt_sat = pxl8_color_saturation(tgt);
|
|
f32 tgt_lum = pxl8_color_luminance(tgt);
|
|
|
|
f32 hue_diff = pxl8_color_hue_diff(src_hue, tgt_hue);
|
|
f32 lum_diff = src_lum > tgt_lum ? src_lum - tgt_lum : tgt_lum - src_lum;
|
|
f32 sat_diff = src_sat > tgt_sat ? src_sat - tgt_sat : tgt_sat - src_sat;
|
|
|
|
f32 score;
|
|
if (src_sat < 0.1f) {
|
|
score = lum_diff + sat_diff * 100.0f;
|
|
} else if (hue_diff <= hue_tol) {
|
|
score = lum_diff + sat_diff * 50.0f;
|
|
} else {
|
|
score = hue_diff * 1000.0f + lum_diff;
|
|
}
|
|
|
|
if (score < best_score) {
|
|
best_score = score;
|
|
best_idx = j;
|
|
}
|
|
}
|
|
remap[i] = (u8)best_idx;
|
|
used[best_idx] = true;
|
|
}
|
|
|
|
u8 compact[256];
|
|
u32 compact_colors[256];
|
|
u32 compact_count = 0;
|
|
for (u32 i = 0; i < config->palette_count; i++) {
|
|
if (used[i]) {
|
|
compact[i] = (u8)compact_count;
|
|
compact_colors[compact_count] = config->palette[i];
|
|
compact_count++;
|
|
}
|
|
}
|
|
|
|
for (u32 i = 0; i < 256; i++) {
|
|
remap[i] = compact[remap[i]];
|
|
}
|
|
|
|
for (u32 i = 0; i < compact_count; i++) {
|
|
u32 offset = palette_entry_start + i * 6;
|
|
u32 color = compact_colors[i];
|
|
output_data[offset + 0] = 0;
|
|
output_data[offset + 1] = 0;
|
|
output_data[offset + 2] = color & 0xFF;
|
|
output_data[offset + 3] = (color >> 8) & 0xFF;
|
|
output_data[offset + 4] = (color >> 16) & 0xFF;
|
|
output_data[offset + 5] = 0xFF;
|
|
}
|
|
|
|
u32 new_last_color = compact_count > 0 ? compact_count - 1 : 0;
|
|
output_data[palette_chunk_offset + 6] = compact_count & 0xFF;
|
|
output_data[palette_chunk_offset + 7] = (compact_count >> 8) & 0xFF;
|
|
output_data[palette_chunk_offset + 8] = (compact_count >> 16) & 0xFF;
|
|
output_data[palette_chunk_offset + 9] = (compact_count >> 24) & 0xFF;
|
|
output_data[palette_chunk_offset + 14] = new_last_color & 0xFF;
|
|
output_data[palette_chunk_offset + 15] = (new_last_color >> 8) & 0xFF;
|
|
output_data[palette_chunk_offset + 16] = (new_last_color >> 16) & 0xFF;
|
|
output_data[palette_chunk_offset + 17] = (new_last_color >> 24) & 0xFF;
|
|
|
|
chunk_offset = frame_start + 16;
|
|
usize output_size = file_size;
|
|
|
|
for (u16 c = 0; c < num_chunks; c++) {
|
|
pxl8_stream_seek(&stream, chunk_offset);
|
|
u32 chunk_size = pxl8_read_u32(&stream);
|
|
u16 chunk_type = pxl8_read_u16(&stream);
|
|
|
|
if (chunk_type == PXL8_ASE_CHUNK_CEL) {
|
|
pxl8_skip_bytes(&stream, 7);
|
|
u16 cel_type = pxl8_read_u16(&stream);
|
|
|
|
if (cel_type == 2) {
|
|
pxl8_skip_bytes(&stream, 7);
|
|
u16 width = pxl8_read_u16(&stream);
|
|
u16 height = pxl8_read_u16(&stream);
|
|
u32 pixels_size = width * height;
|
|
u32 compressed_start = pxl8_stream_position(&stream);
|
|
u32 compressed_size = chunk_size - (compressed_start - chunk_offset);
|
|
|
|
u8* pixels = (u8*)pxl8_malloc(pixels_size);
|
|
if (!pixels) {
|
|
pxl8_free(output_data);
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
const u8* compressed_data = file_data + compressed_start;
|
|
mz_ulong dest_len = pixels_size;
|
|
i32 mz_result = mz_uncompress(pixels, &dest_len, compressed_data, compressed_size);
|
|
if (mz_result != MZ_OK) {
|
|
pxl8_free(pixels);
|
|
pxl8_free(output_data);
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
|
|
for (u32 i = 0; i < pixels_size; i++) {
|
|
pixels[i] = remap[pixels[i]];
|
|
}
|
|
|
|
mz_ulong new_compressed_size = mz_compressBound(pixels_size);
|
|
u8* new_compressed = (u8*)pxl8_malloc(new_compressed_size);
|
|
if (!new_compressed) {
|
|
pxl8_free(pixels);
|
|
pxl8_free(output_data);
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
mz_result = mz_compress2(new_compressed, &new_compressed_size, pixels, pixels_size, 6);
|
|
pxl8_free(pixels);
|
|
|
|
if (mz_result != MZ_OK) {
|
|
pxl8_free(new_compressed);
|
|
pxl8_free(output_data);
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_ASE_MALFORMED_CHUNK;
|
|
}
|
|
|
|
i32 size_diff = (i32)new_compressed_size - (i32)compressed_size;
|
|
|
|
u8* new_output = (u8*)pxl8_malloc(output_size + size_diff + 65536);
|
|
if (!new_output) {
|
|
pxl8_free(new_compressed);
|
|
pxl8_free(output_data);
|
|
pxl8_io_free_binary_data(file_data);
|
|
return PXL8_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
memcpy(new_output, output_data, compressed_start);
|
|
memcpy(new_output + compressed_start, new_compressed, new_compressed_size);
|
|
memcpy(new_output + compressed_start + new_compressed_size,
|
|
output_data + compressed_start + compressed_size,
|
|
output_size - compressed_start - compressed_size);
|
|
|
|
u32 new_chunk_size = chunk_size + size_diff;
|
|
new_output[chunk_offset + 0] = new_chunk_size & 0xFF;
|
|
new_output[chunk_offset + 1] = (new_chunk_size >> 8) & 0xFF;
|
|
new_output[chunk_offset + 2] = (new_chunk_size >> 16) & 0xFF;
|
|
new_output[chunk_offset + 3] = (new_chunk_size >> 24) & 0xFF;
|
|
|
|
u32 new_frame_size = frame_size + size_diff;
|
|
new_output[frame_start + 0] = new_frame_size & 0xFF;
|
|
new_output[frame_start + 1] = (new_frame_size >> 8) & 0xFF;
|
|
new_output[frame_start + 2] = (new_frame_size >> 16) & 0xFF;
|
|
new_output[frame_start + 3] = (new_frame_size >> 24) & 0xFF;
|
|
|
|
output_size += size_diff;
|
|
new_output[0] = output_size & 0xFF;
|
|
new_output[1] = (output_size >> 8) & 0xFF;
|
|
new_output[2] = (output_size >> 16) & 0xFF;
|
|
new_output[3] = (output_size >> 24) & 0xFF;
|
|
|
|
pxl8_free(new_compressed);
|
|
pxl8_free(output_data);
|
|
output_data = new_output;
|
|
|
|
break;
|
|
}
|
|
}
|
|
chunk_offset += chunk_size;
|
|
}
|
|
|
|
pxl8_io_free_binary_data(file_data);
|
|
|
|
result = pxl8_io_write_binary_file(output_path, output_data, output_size);
|
|
pxl8_free(output_data);
|
|
|
|
if (result == PXL8_OK) {
|
|
pxl8_info("Remapped %s -> %s", input_path, output_path);
|
|
}
|
|
|
|
return result;
|
|
}
|