Skip to main content

badterm_core/
iff.rs

1//! IFF ILBM pictures, the Amiga's: a SkyPix brush sent by XMODEM (see
2//! [`crate::skypix`]). Bit planes, interleaved a row at a time,
3//! uncompressed or ByteRun1, with an optional mask plane; the result is a
4//! pen (palette index) per pixel.
5
6fn be16(d: &[u8], at: usize) -> Option<usize> {
7    Some(u16::from_be_bytes(d.get(at..at + 2)?.try_into().ok()?) as usize)
8}
9
10/// The chunks of an IFF `FORM` of `kind`: (ID, data).
11pub fn chunks<'a>(data: &'a [u8], kind: &[u8]) -> Option<Vec<(&'a [u8], &'a [u8])>> {
12    if !data.starts_with(b"FORM") || data.get(8..12)? != kind {
13        return None;
14    }
15    let mut out = Vec::new();
16    let mut at = 12;
17    while at + 8 <= data.len() {
18        let id = &data[at..at + 4];
19        let len = u32::from_be_bytes(data[at + 4..at + 8].try_into().ok()?) as usize;
20        let body = data.get(at + 8..(at + 8 + len).min(data.len()))?;
21        out.push((id, body));
22        at += 8 + len + (len & 1);
23    }
24    Some(out)
25}
26
27/// ByteRun1 (PackBits): `n` (0-127) literal n+1 bytes, -1..-127 the next
28/// byte -n+1 times.
29fn unpack(src: &[u8], want: usize) -> Vec<u8> {
30    let mut out = Vec::with_capacity(want);
31    let mut i = 0;
32    while out.len() < want && i < src.len() {
33        let n = src[i] as i8;
34        i += 1;
35        if n >= 0 {
36            let end = (i + n as usize + 1).min(src.len());
37            out.extend_from_slice(&src[i..end]);
38            i = end;
39        } else if n != -128 {
40            if let Some(&b) = src.get(i) {
41                out.extend(std::iter::repeat_n(b, (-(n as i32) + 1) as usize));
42            }
43            i += 1;
44        }
45    }
46    out.resize(want, 0);
47    out
48}
49
50/// An ILBM's (width, height, pens).
51pub fn ilbm(data: &[u8]) -> Option<(usize, usize, Vec<u8>)> {
52    let chunks = chunks(data, b"ILBM")?;
53    let bmhd = chunks.iter().find(|(id, _)| *id == b"BMHD")?.1;
54    let body = chunks.iter().find(|(id, _)| *id == b"BODY")?.1;
55    let (w, h) = (be16(bmhd, 0)?, be16(bmhd, 2)?);
56    let (planes, masking, compression) = (*bmhd.get(8)? as usize, *bmhd.get(9)?, *bmhd.get(10)?);
57    if w == 0 || h == 0 || w > 4096 || h > 4096 || !(1..=8).contains(&planes) {
58        return None;
59    }
60    let row = w.div_ceil(16) * 2;
61    let stored = planes + usize::from(masking == 1);
62    let bits = if compression == 1 { unpack(body, row * stored * h) } else { body.to_vec() };
63    let mut pens = vec![0u8; w * h];
64    for y in 0..h {
65        for p in 0..planes {
66            let start = (y * stored + p) * row;
67            let Some(line) = bits.get(start..start + row) else { break };
68            for x in 0..w {
69                if line[x / 8] & (0x80 >> (x % 8)) != 0 {
70                    pens[y * w + x] |= 1 << p;
71                }
72            }
73        }
74    }
75    Some((w, h, pens))
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn decodes_planes_compressed_or_not() {
84        let form = |compression: u8, body: &[u8]| {
85            let mut bmhd = vec![0, 4, 0, 2, 0, 0, 0, 0, 2, 0, compression, 0, 0, 0, 10, 11, 0, 4, 0, 2];
86            bmhd.truncate(20);
87            let mut f = b"FORM\0\0\0\0ILBM".to_vec();
88            for (id, d) in [(b"BMHD", &bmhd[..]), (b"BODY", body)] {
89                f.extend(id);
90                f.extend((d.len() as u32).to_be_bytes());
91                f.extend(d);
92                if d.len() % 2 == 1 {
93                    f.push(0);
94                }
95            }
96            f
97        };
98        // 4x2, two planes: row 0 pens 1,2,3,0; row 1 all 3.
99        let raw = [0b1010_0000, 0, 0b0110_0000, 0, 0xF0, 0, 0xF0, 0];
100        let want = vec![1, 2, 3, 0, 3, 3, 3, 3];
101        assert_eq!(ilbm(&form(0, &raw)), Some((4, 2, want.clone())));
102        // ByteRun1: literal runs; and a byte repeated.
103        let packed = [3, 0b1010_0000, 0, 0b0110_0000, 0, 1, 0xF0, 0, 1, 0xF0, 0];
104        assert_eq!(ilbm(&form(1, &packed)), Some((4, 2, want)));
105        assert_eq!(unpack(&[(-2i8) as u8, 7, 0, 9], 4), [7, 7, 7, 9]);
106        assert!(ilbm(b"FORM\0\0\0\x04ILBM").is_none());
107    }
108}