kenwood_thd75/sdcard/
capture.rs1use super::{SdCardError, read_u16_le, read_u32_le};
21
22const EXPECTED_WIDTH: u32 = 240;
24
25const EXPECTED_HEIGHT: u32 = 180;
27
28const EXPECTED_BPP: u16 = 24;
30
31const BMP_HEADER_SIZE: usize = 14;
33
34const MIN_DIB_HEADER_SIZE: u32 = 40;
36
37const MIN_BMP_SIZE: usize = BMP_HEADER_SIZE + MIN_DIB_HEADER_SIZE as usize;
39
40const BI_RGB: u32 = 0;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ScreenCapture {
49 pub width: u32,
51 pub height: u32,
53 pub bits_per_pixel: u16,
55 pub pixels: Vec<u8>,
61}
62
63fn read_i32_le(data: &[u8], offset: usize) -> i32 {
65 i32::from_le_bytes([
66 data[offset],
67 data[offset + 1],
68 data[offset + 2],
69 data[offset + 3],
70 ])
71}
72
73pub fn parse(data: &[u8]) -> Result<ScreenCapture, SdCardError> {
90 if data.len() < MIN_BMP_SIZE {
91 return Err(SdCardError::FileTooSmall {
92 expected: MIN_BMP_SIZE,
93 actual: data.len(),
94 });
95 }
96
97 if &data[0..2] != b"BM" {
99 return Err(SdCardError::InvalidBmpHeader {
100 detail: "missing BM magic bytes".to_owned(),
101 });
102 }
103
104 let pixel_offset = read_u32_le(data, 10) as usize;
106
107 let dib_size = read_u32_le(data, 14);
109 if dib_size < MIN_DIB_HEADER_SIZE {
110 return Err(SdCardError::InvalidBmpHeader {
111 detail: format!("DIB header size {dib_size} too small (minimum {MIN_DIB_HEADER_SIZE})"),
112 });
113 }
114
115 let raw_width = read_i32_le(data, 18);
118 let raw_height = read_i32_le(data, 22);
119
120 let Ok(width) = u32::try_from(raw_width) else {
121 return Err(SdCardError::InvalidBmpHeader {
122 detail: format!("invalid width {raw_width}"),
123 });
124 };
125 if width == 0 {
126 return Err(SdCardError::InvalidBmpHeader {
127 detail: "width is zero".to_owned(),
128 });
129 }
130
131 let height = raw_height.unsigned_abs();
132
133 let bits_per_pixel = read_u16_le(data, 28);
134
135 let compression = read_u32_le(data, 30);
137 if compression != BI_RGB {
138 return Err(SdCardError::InvalidBmpHeader {
139 detail: format!("unsupported compression type {compression} (expected 0 for BI_RGB)"),
140 });
141 }
142
143 if width != EXPECTED_WIDTH || height != EXPECTED_HEIGHT || bits_per_pixel != EXPECTED_BPP {
145 return Err(SdCardError::UnexpectedImageFormat {
146 width,
147 height,
148 bits_per_pixel,
149 });
150 }
151
152 let bytes_per_row = u32::from(bits_per_pixel) / 8 * width;
154 let row_stride = (bytes_per_row + 3) & !3;
155
156 let pixel_data_size = row_stride as usize * height as usize;
157 let required_size = pixel_offset + pixel_data_size;
158
159 if data.len() < required_size {
160 return Err(SdCardError::FileTooSmall {
161 expected: required_size,
162 actual: data.len(),
163 });
164 }
165
166 let pixels = if row_stride == bytes_per_row {
168 data[pixel_offset..pixel_offset + pixel_data_size].to_vec()
169 } else {
170 let mut pixels = Vec::with_capacity(bytes_per_row as usize * height as usize);
171 for row in 0..height as usize {
172 let row_start = pixel_offset + row * row_stride as usize;
173 let row_end = row_start + bytes_per_row as usize;
174 pixels.extend_from_slice(&data[row_start..row_end]);
175 }
176 pixels
177 };
178
179 Ok(ScreenCapture {
180 width,
181 height,
182 bits_per_pixel,
183 pixels,
184 })
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn build_bmp(width: u32, height: u32, bpp: u16) -> Vec<u8> {
193 let bytes_per_row = u32::from(bpp) / 8 * width;
194 let row_stride = (bytes_per_row + 3) & !3;
195 let pixel_data_size = row_stride * height;
196 let file_size = 54 + pixel_data_size;
197
198 let mut buf = Vec::with_capacity(file_size as usize);
199
200 buf.extend_from_slice(b"BM");
202 buf.extend_from_slice(&file_size.to_le_bytes());
203 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&54u32.to_le_bytes()); buf.extend_from_slice(&40u32.to_le_bytes()); #[allow(clippy::cast_possible_wrap)]
210 buf.extend_from_slice(&(width as i32).to_le_bytes());
211 #[allow(clippy::cast_possible_wrap)]
212 buf.extend_from_slice(&(height as i32).to_le_bytes());
213 buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&bpp.to_le_bytes());
215 buf.extend_from_slice(&BI_RGB.to_le_bytes()); buf.extend_from_slice(&pixel_data_size.to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); for row in 0..height {
224 for col in 0..width {
225 #[allow(clippy::cast_possible_truncation)]
226 let b = ((row + col) % 256) as u8;
227 #[allow(clippy::cast_possible_truncation)]
228 let g = ((row * 2 + col) % 256) as u8;
229 #[allow(clippy::cast_possible_truncation)]
230 let r = ((row + col * 2) % 256) as u8;
231 buf.push(b);
232 buf.push(g);
233 buf.push(r);
234 }
235 let padding = row_stride - bytes_per_row;
237 buf.extend(std::iter::repeat_n(0u8, padding as usize));
238 }
239
240 buf
241 }
242
243 #[test]
244 fn parse_valid_d75_capture() {
245 let bmp = build_bmp(240, 180, 24);
246 let cap = parse(&bmp).unwrap();
247
248 assert_eq!(cap.width, 240);
249 assert_eq!(cap.height, 180);
250 assert_eq!(cap.bits_per_pixel, 24);
251 assert_eq!(cap.pixels.len(), 240 * 180 * 3);
253 }
254
255 #[test]
256 fn pixel_data_correct() {
257 let bmp = build_bmp(240, 180, 24);
258 let cap = parse(&bmp).unwrap();
259
260 assert_eq!(cap.pixels[0], 0); assert_eq!(cap.pixels[1], 0); assert_eq!(cap.pixels[2], 0); assert_eq!(cap.pixels[3], 1);
267 assert_eq!(cap.pixels[4], 1);
268 assert_eq!(cap.pixels[5], 2);
269 }
270
271 #[test]
272 fn too_short_returns_error() {
273 let data = b"BM\x00\x00";
274 let err = parse(data).unwrap_err();
275 assert!(matches!(err, SdCardError::FileTooSmall { .. }));
276 }
277
278 #[test]
279 fn empty_returns_error() {
280 let err = parse(b"").unwrap_err();
281 assert!(matches!(err, SdCardError::FileTooSmall { .. }));
282 }
283
284 #[test]
285 fn wrong_magic_bytes() {
286 let mut bmp = build_bmp(240, 180, 24);
287 bmp[0..2].copy_from_slice(b"XX");
288 let err = parse(&bmp).unwrap_err();
289 assert!(matches!(err, SdCardError::InvalidBmpHeader { .. }));
290 }
291
292 #[test]
293 fn wrong_dimensions_rejected() {
294 let bmp = build_bmp(320, 240, 24);
295 let err = parse(&bmp).unwrap_err();
296 assert!(matches!(err, SdCardError::UnexpectedImageFormat { .. }));
297 }
298
299 #[test]
300 fn wrong_bit_depth_rejected() {
301 let bmp = build_bmp(240, 180, 32);
302 let err = parse(&bmp).unwrap_err();
303 assert!(matches!(err, SdCardError::UnexpectedImageFormat { .. }));
304 }
305
306 #[test]
307 fn compressed_bmp_rejected() {
308 let mut bmp = build_bmp(240, 180, 24);
309 bmp[30..34].copy_from_slice(&1u32.to_le_bytes());
311 let err = parse(&bmp).unwrap_err();
312 assert!(matches!(err, SdCardError::InvalidBmpHeader { .. }));
313 }
314
315 #[test]
316 fn truncated_pixel_data_rejected() {
317 let mut bmp = build_bmp(240, 180, 24);
318 bmp.truncate(60);
320 let err = parse(&bmp).unwrap_err();
321 assert!(matches!(err, SdCardError::FileTooSmall { .. }));
322 }
323}