1 | //! A small BMP parser primarily for embedded, no-std environments but usable anywhere. |
2 | //! |
3 | //! This crate is primarily targeted at drawing BMP images to [`embedded_graphics`] [`DrawTarget`]s, |
4 | //! but can also be used to parse BMP files for other applications. |
5 | //! |
6 | //! # Examples |
7 | //! |
8 | //! ## Draw a BMP image to an embedded-graphics draw target |
9 | //! |
10 | //! The [`Bmp`] struct is used together with [`embedded_graphics`]' [`Image`] struct to display BMP |
11 | //! files on any draw target. |
12 | //! |
13 | //! ``` |
14 | //! # fn main() -> Result<(), core::convert::Infallible> { |
15 | //! use embedded_graphics::{image::Image, prelude::*}; |
16 | //! use tinybmp::Bmp; |
17 | //! # use embedded_graphics::mock_display::MockDisplay; |
18 | //! # use embedded_graphics::pixelcolor::Rgb565; |
19 | //! # let mut display: MockDisplay<Rgb565> = MockDisplay::default(); |
20 | //! |
21 | //! // Include the BMP file data. |
22 | //! let bmp_data = include_bytes!("../tests/chessboard-8px-color-16bit.bmp" ); |
23 | //! |
24 | //! // Parse the BMP file. |
25 | //! let bmp = Bmp::from_slice(bmp_data).unwrap(); |
26 | //! |
27 | //! // Draw the image with the top left corner at (10, 20) by wrapping it in |
28 | //! // an embedded-graphics `Image`. |
29 | //! Image::new(&bmp, Point::new(10, 20)).draw(&mut display)?; |
30 | //! # Ok::<(), core::convert::Infallible>(()) } |
31 | //! ``` |
32 | //! |
33 | //! ## Using the pixel iterator |
34 | //! |
35 | //! To access the image data for other applications the [`Bmp::pixels`] method returns an iterator |
36 | //! over all pixels in the BMP file. The colors inside the BMP file will automatically converted to |
37 | //! one of the [color types] in [`embedded_graphics`]. |
38 | //! |
39 | //! ``` |
40 | //! # fn main() -> Result<(), core::convert::Infallible> { |
41 | //! use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; |
42 | //! use tinybmp::Bmp; |
43 | //! |
44 | //! // Include the BMP file data. |
45 | //! let bmp_data = include_bytes!("../tests/chessboard-8px-24bit.bmp" ); |
46 | //! |
47 | //! // Parse the BMP file. |
48 | //! // Note that it is necessary to explicitly specify the color type which the colors in the BMP |
49 | //! // file will be converted into. |
50 | //! let bmp = Bmp::<Rgb888>::from_slice(bmp_data).unwrap(); |
51 | //! |
52 | //! for Pixel(position, color) in bmp.pixels() { |
53 | //! println!("R: {}, G: {}, B: {} @ ({})" , color.r(), color.g(), color.b(), position); |
54 | //! } |
55 | //! # Ok::<(), core::convert::Infallible>(()) } |
56 | //! ``` |
57 | //! |
58 | //! ## Accessing individual pixels |
59 | //! |
60 | //! [`Bmp::pixel`] can be used to get the color of individual pixels. The returned color will be automatically |
61 | //! converted to one of the [color types] in [`embedded_graphics`]. |
62 | //! |
63 | //! ``` |
64 | //! # fn main() -> Result<(), core::convert::Infallible> { |
65 | //! use embedded_graphics::{pixelcolor::Rgb888, image::GetPixel, prelude::*}; |
66 | //! use tinybmp::Bmp; |
67 | //! |
68 | //! // Include the BMP file data. |
69 | //! let bmp_data = include_bytes!("../tests/chessboard-8px-24bit.bmp" ); |
70 | //! |
71 | //! // Parse the BMP file. |
72 | //! // Note that it is necessary to explicitly specify the color type which the colors in the BMP |
73 | //! // file will be converted into. |
74 | //! let bmp = Bmp::<Rgb888>::from_slice(bmp_data).unwrap(); |
75 | //! |
76 | //! let pixel = bmp.pixel(Point::new(3, 2)); |
77 | //! |
78 | //! assert_eq!(pixel, Some(Rgb888::WHITE)); |
79 | //! # Ok::<(), core::convert::Infallible>(()) } |
80 | //! ``` |
81 | //! |
82 | //! ## Accessing the raw image data |
83 | //! |
84 | //! For most applications the higher level access provided by [`Bmp`] is sufficient. But in case |
85 | //! lower level access is necessary the [`RawBmp`] struct can be used to access BMP [header |
86 | //! information] and the [color table]. A [`RawBmp`] object can be created directly from image data |
87 | //! by using [`from_slice`] or by accessing the underlying raw object of a [`Bmp`] object with |
88 | //! [`Bmp::as_raw`]. |
89 | //! |
90 | //! Similar to [`Bmp::pixel`], [`RawBmp::pixel`] can be used to get raw pixel color values as a |
91 | //! `u32`. |
92 | //! |
93 | //! ``` |
94 | //! use embedded_graphics::prelude::*; |
95 | //! use tinybmp::{RawBmp, Bpp, Header, RawPixel, RowOrder}; |
96 | //! |
97 | //! let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-24bit.bmp" )) |
98 | //! .expect("Failed to parse BMP image" ); |
99 | //! |
100 | //! // Read the BMP header |
101 | //! assert_eq!( |
102 | //! bmp.header(), |
103 | //! &Header { |
104 | //! file_size: 314, |
105 | //! image_data_start: 122, |
106 | //! bpp: Bpp::Bits24, |
107 | //! image_size: Size::new(8, 8), |
108 | //! image_data_len: 192, |
109 | //! channel_masks: None, |
110 | //! row_order: RowOrder::BottomUp, |
111 | //! } |
112 | //! ); |
113 | //! |
114 | //! # // Check that raw image data slice is the correct length (according to parsed header) |
115 | //! # assert_eq!(bmp.image_data().len(), bmp.header().image_data_len as usize); |
116 | //! // Get an iterator over the pixel coordinates and values in this image and load into a vec |
117 | //! let pixels: Vec<RawPixel> = bmp.pixels().collect(); |
118 | //! |
119 | //! // Loaded example image is 8x8px |
120 | //! assert_eq!(pixels.len(), 8 * 8); |
121 | //! |
122 | //! // Individual raw pixel values can also be read |
123 | //! let pixel = bmp.pixel(Point::new(3, 2)); |
124 | //! |
125 | //! // The raw value for a white pixel in the source image |
126 | //! assert_eq!(pixel, Some(0xFFFFFFu32)); |
127 | //! ``` |
128 | //! |
129 | //! # Minimum supported Rust version |
130 | //! |
131 | //! The minimum supported Rust version for tinybmp is `1.61` or greater. Ensure you have the correct |
132 | //! version of Rust installed, preferably through <https://rustup.rs>. |
133 | //! |
134 | //! <!-- README-LINKS |
135 | //! [`Bmp`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html |
136 | //! [`Bmp::pixels`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html#method.pixels |
137 | //! [`Bmp::pixel`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html#method.pixel |
138 | //! [`Bmp::as_raw`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html#method.as_raw |
139 | //! [`RawBmp`]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html |
140 | //! [`RawBmp::pixel`]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.pixel |
141 | //! [header information]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.header |
142 | //! [color table]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.color_table |
143 | //! [`from_slice`]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.from_slice |
144 | //! |
145 | //! [`embedded_graphics`]: https://docs.rs/embedded_graphics |
146 | //! [color types]: https://docs.rs/embedded-graphics/latest/embedded_graphics/pixelcolor/index.html#structs |
147 | //! [`DrawTarget`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/draw_target/trait.DrawTarget.html |
148 | //! [`Image`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/image/struct.Image.html |
149 | //! README-LINKS --> |
150 | //! |
151 | //! [`DrawTarget`]: embedded_graphics::draw_target::DrawTarget |
152 | //! [`Image`]: embedded_graphics::image::Image |
153 | //! [color types]: embedded_graphics::pixelcolor#structs |
154 | //! [header information]: RawBmp::header |
155 | //! [color table]: RawBmp::color_table |
156 | //! [`from_slice`]: RawBmp::from_slice |
157 | |
158 | #![no_std ] |
159 | #![deny (missing_docs)] |
160 | #![deny (missing_debug_implementations)] |
161 | #![deny (missing_copy_implementations)] |
162 | #![deny (trivial_casts)] |
163 | #![deny (trivial_numeric_casts)] |
164 | #![deny (unsafe_code)] |
165 | #![deny (unstable_features)] |
166 | #![deny (unused_import_braces)] |
167 | #![deny (unused_qualifications)] |
168 | #![deny (rustdoc::broken_intra_doc_links)] |
169 | #![deny (rustdoc::private_intra_doc_links)] |
170 | |
171 | use core::marker::PhantomData; |
172 | |
173 | use embedded_graphics::{ |
174 | image::GetPixel, |
175 | pixelcolor::{ |
176 | raw::{RawU1, RawU16, RawU24, RawU32, RawU4, RawU8}, |
177 | Rgb555, Rgb565, Rgb888, |
178 | }, |
179 | prelude::*, |
180 | primitives::Rectangle, |
181 | }; |
182 | |
183 | mod color_table; |
184 | mod header; |
185 | mod iter; |
186 | mod parser; |
187 | mod raw_bmp; |
188 | mod raw_iter; |
189 | |
190 | use raw_bmp::ColorType; |
191 | use raw_iter::RawColors; |
192 | |
193 | pub use color_table::ColorTable; |
194 | pub use header::{Bpp, ChannelMasks, Header, RowOrder}; |
195 | pub use iter::Pixels; |
196 | pub use raw_bmp::RawBmp; |
197 | pub use raw_iter::{RawPixel, RawPixels}; |
198 | |
199 | /// A BMP-format bitmap. |
200 | /// |
201 | /// See the [crate-level documentation](crate) for more information. |
202 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
203 | pub struct Bmp<'a, C> { |
204 | raw_bmp: RawBmp<'a>, |
205 | color_type: PhantomData<C>, |
206 | } |
207 | |
208 | impl<'a, C> Bmp<'a, C> |
209 | where |
210 | C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>, |
211 | { |
212 | /// Creates a bitmap object from a byte slice. |
213 | /// |
214 | /// The created object keeps a shared reference to the input and does not dynamically allocate |
215 | /// memory. |
216 | pub fn from_slice(bytes: &'a [u8]) -> Result<Self, ParseError> { |
217 | let raw_bmp = RawBmp::from_slice(bytes)?; |
218 | |
219 | Ok(Self { |
220 | raw_bmp, |
221 | color_type: PhantomData, |
222 | }) |
223 | } |
224 | |
225 | /// Returns an iterator over the pixels in this image. |
226 | /// |
227 | /// The iterator always starts at the top left corner of the image, regardless of the row order |
228 | /// of the BMP file. The coordinate of the first pixel is `(0, 0)`. |
229 | pub fn pixels(&self) -> Pixels<'_, C> { |
230 | Pixels::new(self) |
231 | } |
232 | |
233 | /// Returns a reference to the raw BMP image. |
234 | /// |
235 | /// The [`RawBmp`] instance can be used to access lower level information about the BMP file. |
236 | pub const fn as_raw(&self) -> &RawBmp<'a> { |
237 | &self.raw_bmp |
238 | } |
239 | } |
240 | |
241 | impl<C> ImageDrawable for Bmp<'_, C> |
242 | where |
243 | C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>, |
244 | { |
245 | type Color = C; |
246 | |
247 | fn draw<D>(&self, target: &mut D) -> Result<(), D::Error> |
248 | where |
249 | D: DrawTarget<Color = C>, |
250 | { |
251 | let area = self.bounding_box(); |
252 | |
253 | match self.raw_bmp.color_type { |
254 | ColorType::Index1 => { |
255 | if let Some(color_table) = self.raw_bmp.color_table() { |
256 | let fallback_color = C::from(Rgb888::BLACK); |
257 | let color_table: [C; 2] = [ |
258 | color_table.get(0).map(Into::into).unwrap_or(fallback_color), |
259 | color_table.get(1).map(Into::into).unwrap_or(fallback_color), |
260 | ]; |
261 | |
262 | let colors = RawColors::<RawU1>::new(&self.raw_bmp).map(|index| { |
263 | color_table |
264 | .get(usize::from(index.into_inner())) |
265 | .copied() |
266 | .unwrap_or(fallback_color) |
267 | }); |
268 | target.fill_contiguous(&area, colors) |
269 | } else { |
270 | Ok(()) |
271 | } |
272 | } |
273 | ColorType::Index4 => { |
274 | if let Some(color_table) = self.raw_bmp.color_table() { |
275 | let fallback_color = C::from(Rgb888::BLACK); |
276 | |
277 | let colors = RawColors::<RawU4>::new(&self.raw_bmp).map(|index| { |
278 | color_table |
279 | .get(u32::from(index.into_inner())) |
280 | .map(Into::into) |
281 | .unwrap_or(fallback_color) |
282 | }); |
283 | |
284 | target.fill_contiguous(&area, colors) |
285 | } else { |
286 | Ok(()) |
287 | } |
288 | } |
289 | ColorType::Index8 => { |
290 | if let Some(color_table) = self.raw_bmp.color_table() { |
291 | let fallback_color = C::from(Rgb888::BLACK); |
292 | |
293 | let colors = RawColors::<RawU8>::new(&self.raw_bmp).map(|index| { |
294 | color_table |
295 | .get(u32::from(index.into_inner())) |
296 | .map(Into::into) |
297 | .unwrap_or(fallback_color) |
298 | }); |
299 | |
300 | target.fill_contiguous(&area, colors) |
301 | } else { |
302 | Ok(()) |
303 | } |
304 | } |
305 | ColorType::Rgb555 => target.fill_contiguous( |
306 | &area, |
307 | RawColors::<RawU16>::new(&self.raw_bmp).map(|raw| Rgb555::from(raw).into()), |
308 | ), |
309 | ColorType::Rgb565 => target.fill_contiguous( |
310 | &area, |
311 | RawColors::<RawU16>::new(&self.raw_bmp).map(|raw| Rgb565::from(raw).into()), |
312 | ), |
313 | ColorType::Rgb888 => target.fill_contiguous( |
314 | &area, |
315 | RawColors::<RawU24>::new(&self.raw_bmp).map(|raw| Rgb888::from(raw).into()), |
316 | ), |
317 | ColorType::Xrgb8888 => target.fill_contiguous( |
318 | &area, |
319 | RawColors::<RawU32>::new(&self.raw_bmp) |
320 | .map(|raw| Rgb888::from(RawU24::new(raw.into_inner())).into()), |
321 | ), |
322 | } |
323 | } |
324 | |
325 | fn draw_sub_image<D>(&self, target: &mut D, area: &Rectangle) -> Result<(), D::Error> |
326 | where |
327 | D: DrawTarget<Color = Self::Color>, |
328 | { |
329 | self.draw(&mut target.translated(-area.top_left).clipped(area)) |
330 | } |
331 | } |
332 | |
333 | impl<C> OriginDimensions for Bmp<'_, C> |
334 | where |
335 | C: PixelColor, |
336 | { |
337 | fn size(&self) -> Size { |
338 | self.raw_bmp.header().image_size |
339 | } |
340 | } |
341 | |
342 | impl<C> GetPixel for Bmp<'_, C> |
343 | where |
344 | C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>, |
345 | { |
346 | type Color = C; |
347 | |
348 | fn pixel(&self, p: Point) -> Option<Self::Color> { |
349 | match self.raw_bmp.color_type { |
350 | ColorType::Index1 => self |
351 | .raw_bmp |
352 | .color_table() |
353 | .and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?)) |
354 | .map(Into::into), |
355 | ColorType::Index4 => self |
356 | .raw_bmp |
357 | .color_table() |
358 | .and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?)) |
359 | .map(Into::into), |
360 | ColorType::Index8 => self |
361 | .raw_bmp |
362 | .color_table() |
363 | .and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?)) |
364 | .map(Into::into), |
365 | ColorType::Rgb555 => self |
366 | .raw_bmp |
367 | .pixel(p) |
368 | .map(|raw| Rgb555::from(RawU16::from_u32(raw)).into()), |
369 | ColorType::Rgb565 => self |
370 | .raw_bmp |
371 | .pixel(p) |
372 | .map(|raw| Rgb565::from(RawU16::from_u32(raw)).into()), |
373 | ColorType::Rgb888 => self |
374 | .raw_bmp |
375 | .pixel(p) |
376 | .map(|raw| Rgb888::from(RawU24::from_u32(raw)).into()), |
377 | ColorType::Xrgb8888 => self |
378 | .raw_bmp |
379 | .pixel(p) |
380 | .map(|raw| Rgb888::from(RawU24::from_u32(raw)).into()), |
381 | } |
382 | } |
383 | } |
384 | |
385 | /// Parse error. |
386 | #[derive (Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] |
387 | pub enum ParseError { |
388 | /// The image uses an unsupported bit depth. |
389 | UnsupportedBpp(u16), |
390 | |
391 | /// Unexpected end of file. |
392 | UnexpectedEndOfFile, |
393 | |
394 | /// Invalid file signatures. |
395 | /// |
396 | /// BMP files must start with `BM`. |
397 | InvalidFileSignature([u8; 2]), |
398 | |
399 | /// Unsupported compression method. |
400 | UnsupportedCompressionMethod(u32), |
401 | |
402 | /// Unsupported header length. |
403 | UnsupportedHeaderLength(u32), |
404 | |
405 | /// Unsupported channel masks. |
406 | UnsupportedChannelMasks, |
407 | |
408 | /// Invalid image dimensions. |
409 | InvalidImageDimensions, |
410 | } |
411 | |
412 | #[cfg (test)] |
413 | mod tests { |
414 | use super::*; |
415 | |
416 | const BMP_DATA: &[u8] = include_bytes!("../tests/chessboard-8px-1bit.bmp" ); |
417 | |
418 | fn bmp_data() -> [u8; 94] { |
419 | BMP_DATA.try_into().unwrap() |
420 | } |
421 | |
422 | #[test ] |
423 | fn error_unsupported_bpp() { |
424 | // Replace BPP value with an invalid value of 42. |
425 | let mut data = bmp_data(); |
426 | data[0x1C..0x1C + 2].copy_from_slice(&42u16.to_le_bytes()); |
427 | |
428 | assert_eq!( |
429 | Bmp::<Rgb888>::from_slice(&data), |
430 | Err(ParseError::UnsupportedBpp(42)) |
431 | ); |
432 | } |
433 | |
434 | #[test ] |
435 | fn error_empty_file() { |
436 | assert_eq!( |
437 | Bmp::<Rgb888>::from_slice(&[]), |
438 | Err(ParseError::UnexpectedEndOfFile) |
439 | ); |
440 | } |
441 | |
442 | #[test ] |
443 | fn error_truncated_header() { |
444 | let data = &BMP_DATA[0..10]; |
445 | |
446 | assert_eq!( |
447 | Bmp::<Rgb888>::from_slice(data), |
448 | Err(ParseError::UnexpectedEndOfFile) |
449 | ); |
450 | } |
451 | |
452 | #[test ] |
453 | fn error_truncated_image_data() { |
454 | let (_, data) = BMP_DATA.split_last().unwrap(); |
455 | |
456 | assert_eq!( |
457 | Bmp::<Rgb888>::from_slice(data), |
458 | Err(ParseError::UnexpectedEndOfFile) |
459 | ); |
460 | } |
461 | |
462 | #[test ] |
463 | fn error_invalid_signature() { |
464 | // Replace signature with "EG". |
465 | let mut data = bmp_data(); |
466 | data[0..2].copy_from_slice(b"EG" ); |
467 | |
468 | assert_eq!( |
469 | Bmp::<Rgb888>::from_slice(&data), |
470 | Err(ParseError::InvalidFileSignature([b'E' , b'G' ])) |
471 | ); |
472 | } |
473 | |
474 | #[test ] |
475 | fn error_compression_method() { |
476 | // Replace compression method with BI_JPEG (4). |
477 | let mut data = bmp_data(); |
478 | data[0x1E..0x1E + 4].copy_from_slice(&4u32.to_le_bytes()); |
479 | |
480 | assert_eq!( |
481 | Bmp::<Rgb888>::from_slice(&data), |
482 | Err(ParseError::UnsupportedCompressionMethod(4)) |
483 | ); |
484 | } |
485 | |
486 | #[test ] |
487 | fn error_header_length() { |
488 | // Replace header length with invalid length of 16. |
489 | let mut data = bmp_data(); |
490 | data[0x0E..0x0E + 4].copy_from_slice(&16u32.to_le_bytes()); |
491 | |
492 | assert_eq!( |
493 | Bmp::<Rgb888>::from_slice(&data), |
494 | Err(ParseError::UnsupportedHeaderLength(16)) |
495 | ); |
496 | } |
497 | } |
498 | |