Skip to main content

rgpre/import/
anm.rs

1//! Deluxe Paint LPF animation pages and RunSkipDump records. See docs/formats/ANM.md.
2
3use super::indexed_image::{IndexedImage, pixel_count};
4use crate::{Result, error::Error};
5use serde::Serialize;
6
7#[derive(Debug, Clone, Serialize)]
8pub struct AnmPage {
9    pub base_record: u16,
10    pub record_count: u16,
11    pub data_size: u16,
12}
13
14#[derive(Debug, Clone)]
15pub struct AnmFile<'a> {
16    pub declared_frame_count: u32,
17    pub frames_per_second: u16,
18    pub last_delta_valid: bool,
19    pub pages: Vec<AnmPage>,
20    pub records: Vec<&'a [u8]>,
21    pub loop_delta: Option<&'a [u8]>,
22    pub frames: Vec<IndexedImage>,
23}
24
25fn word(data: &[u8], i: usize) -> u16 {
26    u16::from_le_bytes([data[i], data[i + 1]])
27}
28
29fn dword(data: &[u8], i: usize) -> u32 {
30    u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]])
31}
32
33/// Decode single-bitmap, 8-bit LPF animations. Split records and other codecs
34/// are rejected. Frames are limited to 16 Mi pixels and 256 MiB of indexed pixels in total.
35pub fn parse_anm_file(data: &[u8]) -> Result<AnmFile<'_>> {
36    if data.len() < 1280 || &data[..4] != b"LPF " || &data[16..20] != b"ANIM" {
37        return Err(Error::Parse("ANM: invalid or truncated LPF header".into()));
38    }
39    let page_count = usize::from(word(data, 6));
40    if word(data, 4) != 256
41        || page_count == 0
42        || page_count > 256
43        || data[24] != 0
44        || data[26] > 1
45        || data[27] > 1
46        || data[28] != 0
47        || data[29] != 1
48        || data[30] != 0
49        || data[31] != 1
50    {
51        return Err(Error::Parse("ANM: unsupported LPF layout or codec".into()));
52    }
53    let width = word(data, 20);
54    let height = word(data, 22);
55    let count = pixel_count(width, height)?;
56    let record_count = usize::try_from(dword(data, 8))
57        .map_err(|_| Error::Parse("ANM: record count overflow".into()))?;
58    let frame_count = record_count
59        .checked_sub(usize::from(data[26]))
60        .filter(|&n| n > 0 && n <= data.len() / 7 && n <= (256 * 1024 * 1024) / count)
61        .ok_or_else(|| Error::Parse("ANM: invalid or excessive frame count".into()))?;
62    let fps = word(data, 68);
63    if fps == 0 {
64        return Err(Error::Parse("ANM: zero frame rate".into()));
65    }
66    let table_offset = usize::from(word(data, 14));
67    if table_offset < 1280 {
68        return Err(Error::Parse("ANM: page table overlaps palette".into()));
69    }
70    let table = data
71        .get(table_offset..table_offset + 256 * 6)
72        .ok_or_else(|| Error::Parse("ANM: truncated page table".into()))?;
73    let mut pages = Vec::with_capacity(page_count);
74    let mut records = vec![None; record_count];
75    for i in 0..page_count {
76        let descriptor = &table[i * 6..i * 6 + 6];
77        let page = AnmPage {
78            base_record: word(descriptor, 0),
79            record_count: word(descriptor, 2),
80            data_size: word(descriptor, 4),
81        };
82        if page.record_count & 0xc000 != 0 || page.record_count == 0 {
83            return Err(Error::Parse("ANM: empty page or split record".into()));
84        }
85        let n = usize::from(page.record_count);
86        let base = usize::from(page.base_record);
87        if base + n > record_count {
88            return Err(Error::Parse("ANM: page record range outside index".into()));
89        }
90        let start = table_offset + 256 * 6 + i * 65536;
91        let end = start + 8 + n * 2 + usize::from(page.data_size);
92        if end > start + 65536 {
93            return Err(Error::Parse("ANM: page exceeds 64 KiB".into()));
94        }
95        let payload = data
96            .get(start..end)
97            .ok_or_else(|| Error::Parse("ANM: truncated page".into()))?;
98        if &payload[..6] != descriptor {
99            return Err(Error::Parse("ANM: page header disagrees with table".into()));
100        }
101        let mut cursor = 8 + n * 2;
102        for j in 0..n {
103            let size = usize::from(word(payload, 8 + j * 2));
104            let record = payload
105                .get(cursor..cursor + size)
106                .filter(|r| r.len() >= 7)
107                .ok_or_else(|| Error::Parse("ANM: truncated bitmap record".into()))?;
108            if records[base + j].replace(record).is_some() {
109                return Err(Error::Parse("ANM: overlapping record ranges".into()));
110            }
111            cursor += size;
112        }
113        if cursor != payload.len() {
114            return Err(Error::Parse(
115                "ANM: page size disagrees with record sizes".into(),
116            ));
117        }
118        pages.push(page);
119    }
120    let records: Vec<&[u8]> = records
121        .into_iter()
122        .map(|r| r.ok_or_else(|| Error::Parse("ANM: missing record".into())))
123        .collect::<Result<_>>()?;
124    let palette = std::array::from_fn(|i| {
125        [
126            data[256 + i * 4 + 2],
127            data[256 + i * 4 + 1],
128            data[256 + i * 4],
129        ]
130    });
131    let mut pixels = vec![0; count];
132    let mut frames = Vec::with_capacity(frame_count);
133    for record in &records[..frame_count] {
134        decode_record(record, &mut pixels)?;
135        frames.push(IndexedImage {
136            width,
137            height,
138            palette,
139            pixels: pixels.clone(),
140            transparent_index: None,
141        });
142    }
143    let loop_delta = if data[26] != 0 {
144        records.last().copied()
145    } else {
146        None
147    };
148    if data[27] != 0
149        && let Some(record) = loop_delta
150    {
151        decode_record(record, &mut pixels)?;
152    }
153    Ok(AnmFile {
154        declared_frame_count: dword(data, 64),
155        frames_per_second: fps,
156        last_delta_valid: data[27] != 0,
157        pages,
158        records,
159        loop_delta,
160        frames,
161    })
162}
163
164fn decode_record(record: &[u8], pixels: &mut [u8]) -> Result<()> {
165    if record.len() < 7 || record[0] != b'B' || record[1] != 0 {
166        return Err(Error::Parse("ANM: unsupported bitmap record".into()));
167    }
168    let mut input = 4;
169    let mut output = 0;
170    loop {
171        let control = *record
172            .get(input)
173            .ok_or_else(|| Error::Parse("ANM: missing record terminator".into()))?;
174        input += 1;
175        let (kind, count) = match control {
176            0 => {
177                let count = *record
178                    .get(input)
179                    .ok_or_else(|| Error::Parse("ANM: truncated short run".into()))?;
180                input += 1;
181                (3, usize::from(count))
182            }
183            1..=127 => (2, usize::from(control)),
184            129..=255 => (0, usize::from(control & 0x7f)),
185            128 => {
186                let bytes = record
187                    .get(input..input + 2)
188                    .ok_or_else(|| Error::Parse("ANM: truncated long operation".into()))?;
189                input += 2;
190                let value = u16::from_le_bytes([bytes[0], bytes[1]]);
191                if value == 0 {
192                    return Ok(());
193                }
194                let kind = value >> 14;
195                let count = usize::from(value & 0x3fff);
196                if kind == 2 && count == 0 {
197                    return Err(Error::Parse("ANM: invalid long operation".into()));
198                }
199                if kind != 0 && count == 0 {
200                    continue;
201                }
202                (kind, count + if kind == 1 { 0x4000 } else { 0 })
203            }
204        };
205        let end = output + count;
206        let target = pixels
207            .get_mut(output..end)
208            .ok_or_else(|| Error::Parse("ANM: operation exceeds frame bounds".into()))?;
209        if kind == 2 {
210            let source = record
211                .get(input..input + count)
212                .ok_or_else(|| Error::Parse("ANM: truncated literal data".into()))?;
213            target.copy_from_slice(source);
214            input += count;
215        } else if kind == 3 {
216            let value = *record
217                .get(input)
218                .ok_or_else(|| Error::Parse("ANM: missing run value".into()))?;
219            input += 1;
220            target.fill(value);
221        }
222        output = end;
223    }
224}