use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendingMethod {
AlphaBlend,
Overwrite,
}
impl BlendingMethod {
const fn from_bit(b: u8) -> Self {
match b & 0b1 {
0 => Self::AlphaBlend,
1 => Self::Overwrite,
_ => unreachable!(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisposalMethod {
None,
Background,
}
impl DisposalMethod {
const fn from_bit(d: u8) -> Self {
match d & 0b1 {
0 => Self::None,
1 => Self::Background,
_ => unreachable!(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnmfError {
PayloadTooShort {
got: usize,
},
}
impl fmt::Display for AnmfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PayloadTooShort { got } => write!(
f,
"ANMF payload must be at least 16 bytes per §2.7.1.1 Figure 9, got {got}"
),
}
}
}
impl std::error::Error for AnmfError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnmfHeader {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub duration_ms: u32,
pub blend: BlendingMethod,
pub dispose: DisposalMethod,
pub reserved: u8,
pub info_byte: u8,
}
impl AnmfHeader {
pub const HEADER_LEN: usize = 16;
pub fn parse(payload: &[u8]) -> Result<Self, AnmfError> {
if payload.len() < Self::HEADER_LEN {
return Err(AnmfError::PayloadTooShort { got: payload.len() });
}
let frame_x = read_u24_le(&payload[0..3]);
let frame_y = read_u24_le(&payload[3..6]);
let frame_w_minus_one = read_u24_le(&payload[6..9]);
let frame_h_minus_one = read_u24_le(&payload[9..12]);
let duration_ms = read_u24_le(&payload[12..15]);
let info = payload[15];
let reserved = (info >> 2) & 0b0011_1111;
let blend = BlendingMethod::from_bit(info >> 1);
let dispose = DisposalMethod::from_bit(info);
Ok(Self {
x: frame_x * 2,
y: frame_y * 2,
width: frame_w_minus_one + 1,
height: frame_h_minus_one + 1,
duration_ms,
blend,
dispose,
reserved,
info_byte: info,
})
}
pub const fn frame_data_offset(&self) -> usize {
Self::HEADER_LEN
}
}
const fn read_u24_le(b: &[u8]) -> u32 {
(b[0] as u32) | ((b[1] as u32) << 8) | ((b[2] as u32) << 16)
}
#[cfg(test)]
mod tests {
use super::*;
fn anmf(
frame_x: u32,
frame_y: u32,
width_minus_one: u32,
height_minus_one: u32,
duration_ms: u32,
flags_byte: u8,
) -> Vec<u8> {
let mut v = Vec::with_capacity(16);
v.extend_from_slice(&u24_le(frame_x));
v.extend_from_slice(&u24_le(frame_y));
v.extend_from_slice(&u24_le(width_minus_one));
v.extend_from_slice(&u24_le(height_minus_one));
v.extend_from_slice(&u24_le(duration_ms));
v.push(flags_byte);
v
}
fn u24_le(v: u32) -> [u8; 3] {
[
(v & 0xFF) as u8,
((v >> 8) & 0xFF) as u8,
((v >> 16) & 0xFF) as u8,
]
}
fn flags(reserved: u8, b: u8, d: u8) -> u8 {
((reserved & 0b0011_1111) << 2) | ((b & 0b1) << 1) | (d & 0b1)
}
#[test]
fn payload_under_sixteen_bytes_is_rejected() {
assert_eq!(
AnmfHeader::parse(&[]),
Err(AnmfError::PayloadTooShort { got: 0 })
);
assert_eq!(
AnmfHeader::parse(&[0u8; 15]),
Err(AnmfError::PayloadTooShort { got: 15 })
);
}
#[test]
fn exactly_sixteen_bytes_is_accepted_with_empty_frame_data() {
let h = AnmfHeader::parse(&[0u8; 16]).unwrap();
assert_eq!(h.x, 0);
assert_eq!(h.y, 0);
assert_eq!(h.width, 1);
assert_eq!(h.height, 1);
assert_eq!(h.duration_ms, 0);
assert_eq!(h.blend, BlendingMethod::AlphaBlend);
assert_eq!(h.dispose, DisposalMethod::None);
assert_eq!(h.reserved, 0);
assert_eq!(h.info_byte, 0);
assert_eq!(h.frame_data_offset(), 16);
}
#[test]
fn frame_x_and_frame_y_are_doubled_per_section_2_7_1_1() {
let h = AnmfHeader::parse(&anmf(3, 5, 0, 0, 0, 0)).unwrap();
assert_eq!(h.x, 6);
assert_eq!(h.y, 10);
}
#[test]
fn frame_width_and_height_are_one_based_per_section_2_7_1_1() {
let h = AnmfHeader::parse(&anmf(0, 0, 63, 127, 0, 0)).unwrap();
assert_eq!(h.width, 64);
assert_eq!(h.height, 128);
let h_min = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, 0)).unwrap();
assert_eq!(h_min.width, 1);
assert_eq!(h_min.height, 1);
}
#[test]
fn frame_duration_is_a_24_bit_little_endian_uint() {
let mut p = vec![0u8; 16];
p[12] = 0x56;
p[13] = 0x34;
p[14] = 0x12;
let h = AnmfHeader::parse(&p).unwrap();
assert_eq!(h.duration_ms, 0x12_3456);
}
#[test]
fn uint24_fields_decode_in_little_endian_byte_order() {
let mut p = vec![0u8; 16];
p[0..3].copy_from_slice(&[0x11, 0x22, 0x33]);
p[3..6].copy_from_slice(&[0x44, 0x55, 0x66]);
p[6..9].copy_from_slice(&[0x77, 0x88, 0x99]);
p[9..12].copy_from_slice(&[0xAA, 0xBB, 0xCC]);
p[12..15].copy_from_slice(&[0x64, 0x00, 0x00]);
let h = AnmfHeader::parse(&p).unwrap();
assert_eq!(h.x, 0x33_2211 * 2);
assert_eq!(h.y, 0x66_5544 * 2);
assert_eq!(h.width, 0x99_8877 + 1);
assert_eq!(h.height, 0xCC_BBAA + 1);
assert_eq!(h.duration_ms, 100);
}
#[test]
fn info_byte_disposal_bit_is_the_lsb() {
let h0 = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, 0b0000_0000)).unwrap();
assert_eq!(h0.dispose, DisposalMethod::None);
let h1 = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, 0b0000_0001)).unwrap();
assert_eq!(h1.dispose, DisposalMethod::Background);
}
#[test]
fn info_byte_blending_bit_is_bit_one() {
let h0 = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, 0b0000_0000)).unwrap();
assert_eq!(h0.blend, BlendingMethod::AlphaBlend);
let h1 = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, 0b0000_0010)).unwrap();
assert_eq!(h1.blend, BlendingMethod::Overwrite);
}
#[test]
fn info_byte_reserved_field_surfaces_six_high_bits_without_rejection() {
let raw = flags(0b10_1010, 0, 0);
let h = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, raw)).unwrap();
assert_eq!(h.reserved, 0b10_1010);
assert_eq!(h.blend, BlendingMethod::AlphaBlend);
assert_eq!(h.dispose, DisposalMethod::None);
assert_eq!(h.info_byte, raw);
}
#[test]
fn info_byte_all_three_subfields_decode_independently() {
let raw = flags(0b11_0011, 1, 1);
let h = AnmfHeader::parse(&anmf(0, 0, 0, 0, 0, raw)).unwrap();
assert_eq!(h.reserved, 0b11_0011);
assert_eq!(h.blend, BlendingMethod::Overwrite);
assert_eq!(h.dispose, DisposalMethod::Background);
}
#[test]
fn frame_data_offset_is_always_sixteen() {
let h = AnmfHeader::parse(&[0u8; 16]).unwrap();
assert_eq!(h.frame_data_offset(), 16);
let mut p = vec![0u8; 64];
p[6] = 63; p[9] = 63; let h_long = AnmfHeader::parse(&p).unwrap();
assert_eq!(h_long.frame_data_offset(), 16);
}
#[test]
fn trailing_frame_data_bytes_are_not_consumed_by_header_parse() {
let base = anmf(0, 0, 63, 63, 100, 0x02);
let with_tail = {
let mut v = base.clone();
v.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
v
};
assert_eq!(
AnmfHeader::parse(&base).unwrap(),
AnmfHeader::parse(&with_tail).unwrap()
);
}
#[test]
fn fixture_animated_with_alpha_frame_one_decodes_to_trace_values() {
let bytes = anmf(0, 0, 63, 63, 100, 0x02);
let h = AnmfHeader::parse(&bytes).unwrap();
assert_eq!(h.x, 0);
assert_eq!(h.y, 0);
assert_eq!(h.width, 64);
assert_eq!(h.height, 64);
assert_eq!(h.duration_ms, 100);
assert_eq!(h.blend, BlendingMethod::Overwrite);
assert_eq!(h.dispose, DisposalMethod::None);
assert_eq!(h.reserved, 0);
assert_eq!(h.info_byte, 0x02);
}
#[test]
fn fixture_animated_3_frames_rgb_frame_two_decodes_to_trace_values() {
let bytes = anmf(0, 0, 63, 63, 100, 0x00);
let h = AnmfHeader::parse(&bytes).unwrap();
assert_eq!(h.x, 0);
assert_eq!(h.y, 0);
assert_eq!(h.width, 64);
assert_eq!(h.height, 64);
assert_eq!(h.duration_ms, 100);
assert_eq!(h.blend, BlendingMethod::AlphaBlend);
assert_eq!(h.dispose, DisposalMethod::None);
}
#[test]
fn max_uint24_values_for_each_field() {
let max = 0xFF_FFFF_u32;
let bytes = anmf(max, max, max, max, max, 0x00);
let h = AnmfHeader::parse(&bytes).unwrap();
assert_eq!(h.x, 0xFF_FFFF * 2);
assert_eq!(h.y, 0xFF_FFFF * 2);
assert_eq!(h.width, 0xFF_FFFF + 1);
assert_eq!(h.height, 0xFF_FFFF + 1);
assert_eq!(h.duration_ms, 0xFF_FFFF);
}
#[test]
fn duration_zero_is_accepted_and_preserved_literally() {
let bytes = anmf(0, 0, 0, 0, 0, 0x00);
let h = AnmfHeader::parse(&bytes).unwrap();
assert_eq!(h.duration_ms, 0);
}
#[test]
fn header_len_const_is_sixteen() {
assert_eq!(AnmfHeader::HEADER_LEN, 16);
}
}