#include "pxl8_ase.h" #include #include #define MINIZ_NO_STDIO #define MINIZ_NO_TIME #define MINIZ_NO_ARCHIVE_APIS #define MINIZ_NO_ARCHIVE_WRITING_APIS #define MINIZ_NO_DEFLATE_APIS #include #include "pxl8_io.h" #include "pxl8_macros.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*)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 | (b << 16) | (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*)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*)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] = (a << 24) | (b << 16) | (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*)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 = (a << 24) | (b << 16) | (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*)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*)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*)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*)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) { u32 external_file_id = pxl8_read_u32(stream); u32 tileset_id = pxl8_read_u32(stream); (void)external_file_id; (void)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*)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); free(tileset->pixels); tileset->pixels = NULL; return PXL8_ERROR_ASE_MALFORMED_CHUNK; } } tileset->tile_user_data = (pxl8_ase_user_data*)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*)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); 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*)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); free(temp_buffer); return PXL8_ERROR_ASE_MALFORMED_CHUNK; } cel->tilemap.tiles = (u32*)calloc(tile_count, sizeof(u32)); if (!cel->tilemap.tiles) { 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]; } } 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; size_t 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*)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*)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: { ase_file->layers = (pxl8_ase_layer*)realloc(ase_file->layers, (ase_file->layer_count + 1) * sizeof(pxl8_ase_layer)); if (!ase_file->layers) { result = PXL8_ERROR_OUT_OF_MEMORY; break; } 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) { frame->cels = (pxl8_ase_cel*)realloc(frame->cels, (frame->cel_count + 1) * sizeof(pxl8_ase_cel)); if (!frame->cels) { result = PXL8_ERROR_OUT_OF_MEMORY; break; } 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) { free(ase_file->palette.colors); ase_file->palette.colors = NULL; } result = parse_palette_chunk(&stream, &ase_file->palette); break; case PXL8_ASE_CHUNK_TILESET: { ase_file->tilesets = (pxl8_ase_tileset*)realloc(ase_file->tilesets, (ase_file->tileset_count + 1) * sizeof(pxl8_ase_tileset)); if (!ase_file->tilesets) { result = PXL8_ERROR_OUT_OF_MEMORY; break; } 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) 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) { free(cel->image.pixels); } else if (cel->cel_type == 3 && cel->tilemap.tiles) { free(cel->tilemap.tiles); } } free(ase_file->frames[i].cels); } } free(ase_file->frames); } if (ase_file->palette.colors) { 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) { free(ase_file->layers[i].name); } } free(ase_file->layers); } if (ase_file->tilesets) { for (u32 i = 0; i < ase_file->tileset_count; i++) { if (ase_file->tilesets[i].name) free(ase_file->tilesets[i].name); if (ase_file->tilesets[i].pixels) 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) free(ud->text); if (ud->properties) { for (u32 k = 0; k < ud->property_count; k++) { if (ud->properties[k].name) free(ud->properties[k].name); if (ud->properties[k].type == 8 && ud->properties[k].string_val) { free(ud->properties[k].string_val); } } free(ud->properties); } } free(ase_file->tilesets[i].tile_user_data); } } free(ase_file->tilesets); } memset(ase_file, 0, sizeof(pxl8_ase_file)); }