1use 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
20pub 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}