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
171use core::marker::PhantomData;
172
173use 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
183mod color_table;
184mod header;
185mod iter;
186mod parser;
187mod raw_bmp;
188mod raw_iter;
189
190use raw_bmp::ColorType;
191use raw_iter::RawColors;
192
193pub use color_table::ColorTable;
194pub use header::{Bpp, ChannelMasks, Header, RowOrder};
195pub use iter::Pixels;
196pub use raw_bmp::RawBmp;
197pub 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)]
203pub struct Bmp<'a, C> {
204 raw_bmp: RawBmp<'a>,
205 color_type: PhantomData<C>,
206}
207
208impl<'a, C> Bmp<'a, C>
209where
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
241impl<C> ImageDrawable for Bmp<'_, C>
242where
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
333impl<C> OriginDimensions for Bmp<'_, C>
334where
335 C: PixelColor,
336{
337 fn size(&self) -> Size {
338 self.raw_bmp.header().image_size
339 }
340}
341
342impl<C> GetPixel for Bmp<'_, C>
343where
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)]
387pub 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)]
413mod 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