///|
fn ase_chunk_kind_name(kind : Int) -> String {
match kind {
0x0004 => "old-palette"
0x0011 => "old-palette-2"
0x2004 => "layer"
0x2005 => "cel"
0x2006 => "cel-extra"
0x2007 => "color-profile"
0x2016 => "tags"
0x2017 => "path"
0x2018 => "palette"
0x2019 => "user-data"
0x2020 => "slice"
0x2022 => "tileset"
_ => "unknown"
}
}
///|
fn parse_ase_header(reader : ByteReader) -> AseParseResult {
if !reader.slice_can_read(0, 128) {
AseParseResult::failure(
PixelAnimError::new(ErrorTruncatedData, "ASE data is shorter than header"),
)
} else if !reader.matches_u16(4, 0xA5E0) {
AseParseResult::failure(
PixelAnimError::new(
ErrorNotAseprite,
"expected Aseprite magic 0xA5E0",
offset=4,
),
)
} else {
let header = {
file_size: reader.u32_le_at(0),
frame_count: reader.u16_le_at(6),
width: reader.u16_le_at(8),
height: reader.u16_le_at(10),
color_depth: reader.u16_le_at(12),
flags: reader.u32_le_at(14),
speed_ms: reader.u16_le_at(18),
transparent_index: reader.byte_at(26),
color_count: reader.u16_le_at(32),
pixel_width: reader.byte_at(34),
pixel_height: reader.byte_at(35),
}
AseParseResult::success({
header,
frames: [],
source_length: reader.length(),
})
}
}
///|
pub fn parse_aseprite_file(bytes : Array[Int]) -> AseParseResult {
let reader = ByteReader::new(bytes)
let header_result = parse_ase_header(reader)
if !header_result.ok {
header_result
} else {
let file = header_result.file
let frames : Array[AseFrame] = []
let mut offset = 128
for frame_index in 0.. 0 {
new_chunk_count
} else {
old_chunk_count
}
let chunks = parse_frame_chunks(
reader,
frame_index,
offset + 16,
offset + frame_size,
chunk_count,
)
frames.push(
AseFrame::new(
frame_index, offset, frame_size, duration, chunk_count, chunks,
),
)
offset = offset + frame_size
}
AseParseResult::success({
header: file.header,
frames,
source_length: reader.length(),
})
}
}
///|
fn parse_frame_chunks(
reader : ByteReader,
frame_index : Int,
start : Int,
frame_end : Int,
expected_count : Int,
) -> Array[AseChunk] {
let chunks : Array[AseChunk] = []
let mut offset = start
let mut count = 0
while offset + 6 <= frame_end && count < expected_count {
let size = reader.u32_le_at(offset)
let kind = reader.u16_le_at(offset + 4)
if size < 6 || offset + size > frame_end {
chunks.push(
AseChunk::new(
frame_index,
offset,
if size < 6 {
6
} else {
size
},
kind,
"invalid",
extra="chunk bounds exceed frame",
),
)
break
}
chunks.push(parse_chunk_payload(reader, frame_index, offset, size, kind))
offset = offset + size
count = count + 1
}
chunks
}
///|
fn parse_chunk_payload(
reader : ByteReader,
frame_index : Int,
offset : Int,
size : Int,
kind : Int,
) -> AseChunk {
let data = offset + 6
match kind {
0x2004 => parse_layer_chunk(reader, frame_index, offset, size, data, kind)
0x2005 => parse_cel_chunk(reader, frame_index, offset, size, data, kind)
0x2016 => parse_tags_chunk(reader, frame_index, offset, size, data, kind)
0x2018 => parse_palette_chunk(reader, frame_index, offset, size, data, kind)
0x2020 => parse_slice_chunk(reader, frame_index, offset, size, data, kind)
_ =>
AseChunk::new(
frame_index,
offset,
size,
kind,
ase_chunk_kind_name(kind),
extra="metadata skipped",
)
}
}
///|
fn parse_layer_chunk(
reader : ByteReader,
frame_index : Int,
offset : Int,
size : Int,
data : Int,
kind : Int,
) -> AseChunk {
let child_level = reader.u16_le_at(data + 2)
let opacity = reader.byte_at(data + 16)
let name_size = reader.u16_le_at(data + 18)
let name = reader.ascii_fixed_at(data + 20, name_size)
AseChunk::new(
frame_index,
offset,
size,
kind,
ase_chunk_kind_name(kind),
name~,
opacity~,
child_level~,
extra="layer metadata",
)
}
///|
fn parse_cel_chunk(
reader : ByteReader,
frame_index : Int,
offset : Int,
size : Int,
data : Int,
kind : Int,
) -> AseChunk {
let layer_index = reader.u16_le_at(data)
let opacity = reader.byte_at(data + 10)
let cel_type = reader.u16_le_at(data + 11)
let extra = match cel_type {
0 => "raw-cel"
1 => "linked-cel"
2 => "compressed-cel"
3 => "tilemap-cel"
_ => "unknown-cel"
}
AseChunk::new(
frame_index,
offset,
size,
kind,
ase_chunk_kind_name(kind),
layer_index~,
cel_type~,
opacity~,
extra~,
)
}
///|
fn parse_tags_chunk(
reader : ByteReader,
frame_index : Int,
offset : Int,
size : Int,
data : Int,
kind : Int,
) -> AseChunk {
let count = reader.u16_le_at(data)
let mut name = ""
if count > 0 {
let first = data + 10
let name_size = reader.u16_le_at(first + 9)
name = reader.ascii_fixed_at(first + 11, name_size)
}
AseChunk::new(
frame_index,
offset,
size,
kind,
ase_chunk_kind_name(kind),
name~,
extra="tag-count=\{count}",
)
}
///|
fn parse_palette_chunk(
reader : ByteReader,
frame_index : Int,
offset : Int,
size : Int,
data : Int,
kind : Int,
) -> AseChunk {
let entries = reader.u32_le_at(data)
let first = reader.u32_le_at(data + 4)
let last = reader.u32_le_at(data + 8)
AseChunk::new(
frame_index,
offset,
size,
kind,
ase_chunk_kind_name(kind),
extra="entries=\{entries}, range=\{first}..\{last}",
)
}
///|
fn parse_slice_chunk(
reader : ByteReader,
frame_index : Int,
offset : Int,
size : Int,
data : Int,
kind : Int,
) -> AseChunk {
let keys = reader.u32_le_at(data)
let flags = reader.u32_le_at(data + 4)
let name_size = reader.u16_le_at(data + 16)
let name = reader.ascii_fixed_at(data + 18, name_size)
AseChunk::new(
frame_index,
offset,
size,
kind,
ase_chunk_kind_name(kind),
name~,
extra="slice-keys=\{keys}, flags=\{flags}",
)
}
///|
pub fn ase_file_supports_runtime_manifest(file : AseFile) -> Bool {
file.header.width > 0 &&
file.header.height > 0 &&
file.frames.length() == file.header.frame_count
}
///|
pub fn collect_ase_chunk_names(file : AseFile) -> Array[String] {
let names : Array[String] = []
for frame in file.frames {
for chunk in frame.chunks {
names.push(chunk.kind_name)
}
}
names
}
///|
pub fn ase_has_compressed_cels(file : AseFile) -> Bool {
for frame in file.frames {
for chunk in frame.chunks {
if chunk.kind == 0x2005 && chunk.cel_type == 2 {
return true
}
}
}
false
}