Skip to main content

rgpre/import/
lbm.rs

1//! IFF PBM chunky bitmaps. See docs/formats/LBM.md.
2
3use super::indexed_image::{IndexedImage, pixel_count};
4use crate::{Result, error::Error};
5
6#[derive(Debug, Clone)]
7pub struct LbmChunk<'a> {
8    pub tag: [u8; 4],
9    pub data: &'a [u8],
10}
11
12#[derive(Debug, Clone)]
13pub struct LbmFile<'a> {
14    pub compression: u8,
15    pub masking: u8,
16    pub image: IndexedImage,
17    pub chunks: Vec<LbmChunk<'a>>,
18}
19
20/// Decode an 8-bit PBM FORM with raw or ByteRun1 rows, retaining unknown chunks.
21/// Planar ILBM and mask planes are rejected. Images are limited to 16 Mi pixels.
22pub fn parse_lbm_file(data: &[u8]) -> Result<LbmFile<'_>> {
23    if data.len() < 12 || &data[..4] != b"FORM" || &data[8..12] != b"PBM " {
24        return Err(Error::Parse("LBM: expected IFF PBM FORM".into()));
25    }
26    let form_size = u32::from_be_bytes(data[4..8].try_into().unwrap());
27    if u64::from(form_size) + 8 != data.len() as u64 {
28        return Err(Error::Parse("LBM: FORM size does not match file".into()));
29    }
30    let mut chunks = Vec::new();
31    let mut cursor = 12;
32    while cursor < data.len() {
33        let header = data
34            .get(cursor..cursor + 8)
35            .ok_or_else(|| Error::Parse("LBM: truncated chunk header".into()))?;
36        let size = usize::try_from(u32::from_be_bytes(header[4..8].try_into().unwrap()))
37            .map_err(|_| Error::Parse("LBM: chunk size overflow".into()))?;
38        cursor += 8;
39        let end = cursor
40            .checked_add(size)
41            .ok_or_else(|| Error::Parse("LBM: chunk range overflow".into()))?;
42        let payload = data
43            .get(cursor..end)
44            .ok_or_else(|| Error::Parse("LBM: truncated chunk".into()))?;
45        chunks.push(LbmChunk {
46            tag: header[..4].try_into().unwrap(),
47            data: payload,
48        });
49        cursor = end
50            .checked_add(size & 1)
51            .filter(|&n| n <= data.len())
52            .ok_or_else(|| Error::Parse("LBM: missing chunk padding".into()))?;
53    }
54    let required = |tag: &[u8; 4]| -> Result<&[u8]> {
55        let mut matches = chunks.iter().filter(|chunk| &chunk.tag == tag);
56        let payload = matches
57            .next()
58            .ok_or_else(|| Error::Parse(format!("LBM: missing {}", String::from_utf8_lossy(tag))))?
59            .data;
60        if matches.next().is_some() {
61            return Err(Error::Parse("LBM: duplicate bitmap chunk".into()));
62        }
63        Ok(payload)
64    };
65    let header = required(b"BMHD")?;
66    if header.len() != 20 || header[8] != 8 || !matches!(header[9], 0 | 2) || header[10] > 1 {
67        return Err(Error::Parse(
68            "LBM: unsupported or truncated bitmap header".into(),
69        ));
70    }
71    let width = u16::from_be_bytes([header[0], header[1]]);
72    let height = u16::from_be_bytes([header[2], header[3]]);
73    let count = pixel_count(width, height)?;
74    let transparent = u16::from_be_bytes([header[12], header[13]]);
75    let transparent_index = if header[9] == 2 {
76        Some(
77            u8::try_from(transparent)
78                .map_err(|_| Error::Parse("LBM: invalid transparent index".into()))?,
79        )
80    } else {
81        None
82    };
83    let colors = required(b"CMAP")?;
84    if colors.is_empty() || colors.len() > 768 || !colors.len().is_multiple_of(3) {
85        return Err(Error::Parse("LBM: invalid color map".into()));
86    }
87    let mut palette = [[0; 3]; 256];
88    for (color, bytes) in palette.iter_mut().zip(colors.as_chunks::<3>().0) {
89        color.copy_from_slice(bytes);
90    }
91    let body = required(b"BODY")?;
92    let stride = (usize::from(width) + 1) & !1;
93    let mut cursor = 0;
94    let mut pixels = Vec::with_capacity(count);
95    for _ in 0..height {
96        let mut row = Vec::with_capacity(stride);
97        while row.len() < stride {
98            let control = *body
99                .get(cursor)
100                .ok_or_else(|| Error::Parse("LBM: truncated BODY".into()))?;
101            cursor += 1;
102            if header[10] == 0 {
103                row.push(control);
104                continue;
105            }
106            if control == 128 {
107                continue;
108            }
109            let run = if control < 128 {
110                usize::from(control) + 1
111            } else {
112                257 - usize::from(control)
113            };
114            if row.len() + run > stride {
115                return Err(Error::Parse("LBM: ByteRun1 crosses row boundary".into()));
116            }
117            if control < 128 {
118                let bytes = body
119                    .get(cursor..cursor + run)
120                    .ok_or_else(|| Error::Parse("LBM: truncated literal run".into()))?;
121                row.extend_from_slice(bytes);
122                cursor += run;
123            } else {
124                let value = *body
125                    .get(cursor)
126                    .ok_or_else(|| Error::Parse("LBM: truncated repeat run".into()))?;
127                cursor += 1;
128                row.resize(row.len() + run, value);
129            }
130        }
131        pixels.extend_from_slice(&row[..usize::from(width)]);
132    }
133    if body[cursor..].iter().any(|&b| header[10] != 1 || b != 128) {
134        return Err(Error::Parse("LBM: trailing BODY data".into()));
135    }
136    Ok(LbmFile {
137        compression: header[10],
138        masking: header[9],
139        image: IndexedImage {
140            width,
141            height,
142            palette,
143            pixels,
144            transparent_index,
145        },
146        chunks,
147    })
148}