use super::{read_u24_le, write_u24_le};
use crate::error::{Error, Result};
use crate::image::Dimensions;
pub const ANIM_PAYLOAD_LEN: usize = 6;
pub const ANMF_HEADER_LEN: usize = 16;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AnimChunk {
pub background: u32,
pub loop_count: u16,
}
impl AnimChunk {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() != ANIM_PAYLOAD_LEN {
return Err(Error::InvalidContainer);
}
Ok(Self {
background: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
loop_count: u16::from_le_bytes([data[4], data[5]]),
})
}
#[must_use]
pub const fn build(self) -> [u8; ANIM_PAYLOAD_LEN] {
let bg = self.background.to_le_bytes();
let lc = self.loop_count.to_le_bytes();
[bg[0], bg[1], bg[2], bg[3], lc[0], lc[1]]
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AnmfFlags(pub u8);
impl AnmfFlags {
const BLEND: u8 = 0x02;
const DISPOSE: u8 = 0x01;
#[must_use]
pub const fn do_not_blend(self) -> bool {
self.0 & Self::BLEND != 0
}
#[must_use]
pub const fn dispose_background(self) -> bool {
self.0 & Self::DISPOSE != 0
}
#[must_use]
pub const fn from_parts(do_not_blend: bool, dispose_background: bool) -> Self {
let mut bits = 0u8;
if do_not_blend {
bits |= Self::BLEND;
}
if dispose_background {
bits |= Self::DISPOSE;
}
Self(bits)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AnmfHeader {
pub x: u32,
pub y: u32,
pub dims: Dimensions,
pub duration_ms: u32,
pub flags: AnmfFlags,
}
impl AnmfHeader {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < ANMF_HEADER_LEN {
return Err(Error::Truncated);
}
let x = read_u24_le(data[0], data[1], data[2]) * 2;
let y = read_u24_le(data[3], data[4], data[5]) * 2;
let width = read_u24_le(data[6], data[7], data[8]) + 1;
let height = read_u24_le(data[9], data[10], data[11]) + 1;
let duration_ms = read_u24_le(data[12], data[13], data[14]);
let flags = AnmfFlags(data[15]);
let dims = Dimensions::new(width, height).map_err(|_| Error::InvalidContainer)?;
Ok(Self {
x,
y,
dims,
duration_ms,
flags,
})
}
#[must_use]
pub const fn build(self) -> [u8; ANMF_HEADER_LEN] {
let x = write_u24_le(self.x / 2);
let y = write_u24_le(self.y / 2);
let width = write_u24_le(self.dims.width() - 1);
let height = write_u24_le(self.dims.height() - 1);
let dur = write_u24_le(self.duration_ms);
[
x[0],
x[1],
x[2],
y[0],
y[1],
y[2],
width[0],
width[1],
width[2],
height[0],
height[1],
height[2],
dur[0],
dur[1],
dur[2],
self.flags.0,
]
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum BlendMode {
#[default]
Blend,
Overwrite,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DisposalMode {
#[default]
Keep,
Background,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FrameMeta {
pub x: u32,
pub y: u32,
pub dimensions: Dimensions,
pub duration_ms: u32,
pub blend: BlendMode,
pub dispose: DisposalMode,
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::{ANIM_PAYLOAD_LEN, ANMF_HEADER_LEN, AnimChunk, AnmfFlags, AnmfHeader};
use crate::error::Error;
use crate::image::Dimensions;
proptest! {
#[test]
fn anim_chunk_parse_build_round_trips(background in any::<u32>(), loop_count in any::<u16>()) {
let anim = AnimChunk {
background,
loop_count,
};
prop_assert_eq!(AnimChunk::parse(&anim.build()).unwrap(), anim);
}
#[test]
fn anmf_header_parse_build_round_trips(
x_half in 0u32..(1 << 23),
y_half in 0u32..(1 << 23),
w in 1u32..=16384,
h in 1u32..=16384,
duration_ms in 0u32..(1 << 24),
flags in any::<u8>(),
) {
let header = AnmfHeader {
x: x_half * 2,
y: y_half * 2,
dims: Dimensions::new(w, h).unwrap(),
duration_ms,
flags: AnmfFlags(flags),
};
prop_assert_eq!(AnmfHeader::parse(&header.build()).unwrap(), header);
}
}
#[test]
fn anim_chunk_round_trips() {
let anim = AnimChunk {
background: 0x80_11_22_33,
loop_count: 5,
};
assert_eq!(AnimChunk::parse(&anim.build()).unwrap(), anim);
}
#[test]
fn anim_background_is_stored_bgra_little_endian() {
let anim = AnimChunk {
background: 0x80_12_34_56,
loop_count: 0,
};
let bytes = anim.build();
assert_eq!(&bytes[0..4], &[0x56, 0x34, 0x12, 0x80]);
}
#[test]
fn anim_parse_rejects_wrong_length() {
assert_eq!(
AnimChunk::parse(&[0u8; ANIM_PAYLOAD_LEN - 1]).unwrap_err(),
Error::InvalidContainer
);
assert_eq!(
AnimChunk::parse(&[0u8; ANIM_PAYLOAD_LEN + 1]).unwrap_err(),
Error::InvalidContainer
);
}
#[test]
fn anmf_flags_bit_positions() {
assert_eq!(AnmfFlags::from_parts(false, false).0, 0x00);
assert_eq!(AnmfFlags::from_parts(true, false).0, 0x02); assert_eq!(AnmfFlags::from_parts(false, true).0, 0x01); assert_eq!(AnmfFlags::from_parts(true, true).0, 0x03);
assert!(AnmfFlags(0x02).do_not_blend() && !AnmfFlags(0x02).dispose_background());
assert!(AnmfFlags(0x01).dispose_background() && !AnmfFlags(0x01).do_not_blend());
}
#[test]
fn anmf_header_round_trips_with_doubled_offsets() {
let header = AnmfHeader {
x: 20,
y: 8,
dims: Dimensions::new(64, 48).unwrap(),
duration_ms: 100,
flags: AnmfFlags::from_parts(false, true),
};
let bytes = header.build();
assert_eq!(&bytes[0..3], &[10, 0, 0]);
assert_eq!(&bytes[3..6], &[4, 0, 0]);
assert_eq!(&bytes[6..9], &[63, 0, 0]);
assert_eq!(&bytes[9..12], &[47, 0, 0]);
assert_eq!(AnmfHeader::parse(&bytes).unwrap(), header);
}
#[test]
fn anmf_parse_reads_only_the_header_and_ignores_trailing_frame_data() {
let header = AnmfHeader {
x: 0,
y: 0,
dims: Dimensions::new(2, 2).unwrap(),
duration_ms: 40,
flags: AnmfFlags(0),
};
let mut bytes = header.build().to_vec();
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); assert_eq!(AnmfHeader::parse(&bytes).unwrap(), header);
}
#[test]
fn anmf_parse_rejects_short_header() {
assert_eq!(
AnmfHeader::parse(&[0u8; ANMF_HEADER_LEN - 1]).unwrap_err(),
Error::Truncated
);
}
}