//! `LSTA` — a sprite list: a header followed by N inline [`T8aD`](crate::t8ad) //! frames concatenated back-to-back. //! //! A `count` lives at `@0x04`, but a few entries disagree with the actual frame //! count, so we walk by the `T8aD` magic instead (robust) and decode each frame //! from its slice up to the next frame (or end). use crate::t8ad::{self, T8adImage, T8AD_MAGIC}; /// Magic at the start of an LSTA sprite list. pub const LSTA_MAGIC: [u8; 4] = *b"LSTA"; /// Whether `bytes` is an LSTA list. pub fn is_lsta(bytes: &[u8]) -> bool { bytes.len() >= 4 && bytes[0..4] == LSTA_MAGIC } /// Decode all inline T8aD frames. Frames that don't decode (unsupported T8aD /// variant) are skipped. Returns `None` only for non-LSTA input. pub fn parse(bytes: &[u8]) -> Option> { if !is_lsta(bytes) { return None; } // Offsets of each inline T8aD frame (search past the 4-byte magic). let mut offs = Vec::new(); let mut i = 4; while i + 4 <= bytes.len() { if bytes[i..i + 4] == T8AD_MAGIC { offs.push(i); i += 4; } else { i += 1; } } let mut frames = Vec::with_capacity(offs.len()); for (idx, &off) in offs.iter().enumerate() { let next = offs.get(idx + 1).copied().unwrap_or(bytes.len()); if let Some(img) = t8ad::parse(&bytes[off..next]) { frames.push(img); } } Some(frames) } #[cfg(test)] mod tests { use super::*; fn t8ad_frame(w: u32, h: u32) -> Vec { let mut b = vec![0u8; 64]; b[0..4].copy_from_slice(&T8AD_MAGIC); b[0x14..0x18].copy_from_slice(&w.to_be_bytes()); b[0x18..0x1c].copy_from_slice(&h.to_be_bytes()); b[0x1c..0x20].copy_from_slice(&1u32.to_be_bytes()); b.extend_from_slice(&vec![0x80u8; (w * h * 4) as usize]); b } #[test] fn walks_inline_frames() { let mut b = LSTA_MAGIC.to_vec(); b.extend_from_slice(&2u32.to_be_bytes()); b.extend_from_slice(&[0u8; 15]); // header remainder b.extend_from_slice(&t8ad_frame(4, 4)); b.extend_from_slice(&t8ad_frame(2, 3)); let frames = parse(&b).unwrap(); assert_eq!(frames.len(), 2); assert_eq!((frames[0].width, frames[0].height), (4, 4)); assert_eq!((frames[1].width, frames[1].height), (2, 3)); } #[test] fn rejects_non_lsta() { assert!(parse(b"T8aD....").is_none()); } }