tinybmp/
lib.rs

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//! Note that you currently cannot access invidual pixels when working with RLE4
83//! or RLE8 compressed indexed bitmaps. With these formats the `pixel()`
84//! function will always return `None`.
85//!
86//! ## Accessing the raw image data
87//!
88//! For most applications the higher level access provided by [`Bmp`] is sufficient. But in case
89//! lower level access is necessary the [`RawBmp`] struct can be used to access BMP [header
90//! information] and the [color table]. A [`RawBmp`] object can be created directly from image data
91//! by using [`from_slice`] or by accessing the underlying raw object of a [`Bmp`] object with
92//! [`Bmp::as_raw`].
93//!
94//! Similar to [`Bmp::pixel`], [`RawBmp::pixel`] can be used to get raw pixel color values as a
95//! `u32`.
96//!
97//! ```
98//! use embedded_graphics::prelude::*;
99//! use tinybmp::{RawBmp, Bpp, Header, RawPixel, RowOrder, CompressionMethod};
100//!
101//! let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-24bit.bmp"))
102//!     .expect("Failed to parse BMP image");
103//!
104//! // Read the BMP header
105//! assert_eq!(
106//!     bmp.header(),
107//!     &Header {
108//!         file_size: 314,
109//!         image_data_start: 122,
110//!         bpp: Bpp::Bits24,
111//!         image_size: Size::new(8, 8),
112//!         image_data_len: 192,
113//!         channel_masks: None,
114//!         row_order: RowOrder::BottomUp,
115//!         compression_method: CompressionMethod::Rgb,
116//!     }
117//! );
118//!
119//! # // Check that raw image data slice is the correct length (according to parsed header)
120//! # assert_eq!(bmp.image_data().len(), bmp.header().image_data_len as usize);
121//! // Get an iterator over the pixel coordinates and values in this image and load into a vec
122//! let pixels: Vec<RawPixel> = bmp.pixels().collect();
123//!
124//! // Loaded example image is 8x8px
125//! assert_eq!(pixels.len(), 8 * 8);
126//!
127//! // Individual raw pixel values can also be read
128//! let pixel = bmp.pixel(Point::new(3, 2));
129//!
130//! // The raw value for a white pixel in the source image
131//! assert_eq!(pixel, Some(0xFFFFFFu32));
132//! ```
133//!
134//! # Minimum supported Rust version
135//!
136//! The minimum supported Rust version for tinybmp is `1.71` or greater. Ensure you have the correct
137//! version of Rust installed, preferably through <https://rustup.rs>.
138//!
139//! <!-- README-LINKS
140//! [`Bmp`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html
141//! [`Bmp::pixels`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html#method.pixels
142//! [`Bmp::pixel`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html#method.pixel
143//! [`Bmp::as_raw`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html#method.as_raw
144//! [`RawBmp`]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html
145//! [`RawBmp::pixel`]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.pixel
146//! [header information]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.header
147//! [color table]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.color_table
148//! [`from_slice`]: https://docs.rs/tinybmp/latest/tinybmp/struct.RawBmp.html#method.from_slice
149//!
150//! [`embedded_graphics`]: https://docs.rs/embedded_graphics
151//! [color types]: https://docs.rs/embedded-graphics/latest/embedded_graphics/pixelcolor/index.html#structs
152//! [`DrawTarget`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/draw_target/trait.DrawTarget.html
153//! [`Image`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/image/struct.Image.html
154//! README-LINKS -->
155//!
156//! [`DrawTarget`]: embedded_graphics::draw_target::DrawTarget
157//! [`Image`]: embedded_graphics::image::Image
158//! [color types]: embedded_graphics::pixelcolor#structs
159//! [header information]: RawBmp::header
160//! [color table]: RawBmp::color_table
161//! [`from_slice`]: RawBmp::from_slice
162
163#![no_std]
164#![deny(missing_docs)]
165#![deny(missing_debug_implementations)]
166#![deny(missing_copy_implementations)]
167#![deny(trivial_casts)]
168#![deny(trivial_numeric_casts)]
169#![deny(unsafe_code)]
170#![deny(unstable_features)]
171#![deny(unused_import_braces)]
172#![deny(unused_qualifications)]
173#![deny(rustdoc::broken_intra_doc_links)]
174#![deny(rustdoc::private_intra_doc_links)]
175
176use core::marker::PhantomData;
177
178use embedded_graphics::{
179    image::GetPixel,
180    pixelcolor::{
181        raw::{RawU1, RawU16, RawU24, RawU32, RawU4, RawU8},
182        Rgb555, Rgb565, Rgb888,
183    },
184    prelude::*,
185    primitives::Rectangle,
186};
187
188mod color_table;
189mod header;
190mod iter;
191mod parser;
192mod raw_bmp;
193mod raw_iter;
194
195use raw_bmp::ColorType;
196use raw_iter::{RawColors, Rle4Pixels, Rle8Pixels};
197
198pub use color_table::ColorTable;
199pub use header::CompressionMethod;
200pub use header::{Bpp, ChannelMasks, Header, RowOrder};
201pub use iter::Pixels;
202pub use raw_bmp::RawBmp;
203pub use raw_iter::{RawPixel, RawPixels};
204
205/// A BMP-format bitmap.
206///
207/// See the [crate-level documentation](crate) for more information.
208#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
209pub struct Bmp<'a, C> {
210    raw_bmp: RawBmp<'a>,
211    color_type: PhantomData<C>,
212}
213
214impl<'a, C> Bmp<'a, C>
215where
216    C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>,
217{
218    /// Creates a bitmap object from a byte slice.
219    ///
220    /// The created object keeps a shared reference to the input and does not dynamically allocate
221    /// memory.
222    pub fn from_slice(bytes: &'a [u8]) -> Result<Self, ParseError> {
223        let raw_bmp = RawBmp::from_slice(bytes)?;
224
225        Ok(Self {
226            raw_bmp,
227            color_type: PhantomData,
228        })
229    }
230
231    /// Returns an iterator over the pixels in this image.
232    ///
233    /// The iterator always starts at the top left corner of the image, regardless of the row order
234    /// of the BMP file. The coordinate of the first pixel is `(0, 0)`.
235    pub fn pixels(&self) -> Pixels<'_, C> {
236        Pixels::new(self)
237    }
238
239    /// Returns a reference to the raw BMP image.
240    ///
241    /// The [`RawBmp`] instance can be used to access lower level information about the BMP file.
242    pub const fn as_raw(&self) -> &RawBmp<'a> {
243        &self.raw_bmp
244    }
245}
246
247impl<C> ImageDrawable for Bmp<'_, C>
248where
249    C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>,
250{
251    type Color = C;
252
253    fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
254    where
255        D: DrawTarget<Color = C>,
256    {
257        let area = self.bounding_box();
258        let slice_size = Size::new(area.size.width, 1);
259
260        match self.raw_bmp.color_type {
261            ColorType::Index1 => {
262                if let Some(color_table) = self.raw_bmp.color_table() {
263                    let fallback_color = C::from(Rgb888::BLACK);
264                    let color_table: [C; 2] = [
265                        color_table.get(0).map(Into::into).unwrap_or(fallback_color),
266                        color_table.get(1).map(Into::into).unwrap_or(fallback_color),
267                    ];
268
269                    let colors = RawColors::<RawU1>::new(&self.raw_bmp).map(|index| {
270                        color_table
271                            .get(usize::from(index.into_inner()))
272                            .copied()
273                            .unwrap_or(fallback_color)
274                    });
275                    target.fill_contiguous(&area, colors)
276                } else {
277                    Ok(())
278                }
279            }
280            ColorType::Index4 => {
281                let header = self.raw_bmp.header();
282                let fallback_color = C::from(Rgb888::BLACK);
283                if let Some(color_table) = self.raw_bmp.color_table() {
284                    if header.compression_method == CompressionMethod::Rle4 {
285                        let mut colors = Rle4Pixels::new(&self.raw_bmp).map(|raw_pixel| {
286                            color_table
287                                .get(raw_pixel.color)
288                                .map(Into::into)
289                                .unwrap_or(fallback_color)
290                        });
291                        // RLE produces pixels in bottom-up order, so we draw them line by line rather than the entire bitmap at once.
292                        for y in (0..area.size.height).rev() {
293                            let row = Rectangle::new(Point::new(0, y as i32), slice_size);
294                            target.fill_contiguous(
295                                &row,
296                                colors.by_ref().take(area.size.width as usize),
297                            )?;
298                        }
299                        Ok(())
300                    } else {
301                        // If we didn't detect a supported compression method, just intepret it as raw indexed nibbles.
302                        let colors = RawColors::<RawU4>::new(&self.raw_bmp).map(|index| {
303                            color_table
304                                .get(u32::from(index.into_inner()))
305                                .map(Into::into)
306                                .unwrap_or(fallback_color)
307                        });
308                        target.fill_contiguous(&area, colors)
309                    }
310                } else {
311                    Ok(())
312                }
313            }
314            ColorType::Index8 => {
315                let header = self.raw_bmp.header();
316                let fallback_color = C::from(Rgb888::BLACK);
317                if let Some(color_table) = self.raw_bmp.color_table() {
318                    if header.compression_method == CompressionMethod::Rle8 {
319                        let mut colors = Rle8Pixels::new(&self.raw_bmp).map(|raw_pixel| {
320                            color_table
321                                .get(raw_pixel.color)
322                                .map(Into::into)
323                                .unwrap_or(fallback_color)
324                        });
325                        // RLE produces pixels in bottom-up order, so we draw them line by line rather than the entire bitmap at once.
326                        for y in (0..area.size.height).rev() {
327                            let row = Rectangle::new(Point::new(0, y as i32), slice_size);
328                            target.fill_contiguous(
329                                &row,
330                                colors.by_ref().take(area.size.width as usize),
331                            )?;
332                        }
333                        Ok(())
334                    } else {
335                        // If we didn't detect a supported compression method, just intepret it as raw indexed bytes.
336                        let colors = RawColors::<RawU8>::new(&self.raw_bmp).map(|index| {
337                            color_table
338                                .get(u32::from(index.into_inner()))
339                                .map(Into::into)
340                                .unwrap_or(fallback_color)
341                        });
342                        target.fill_contiguous(&area, colors)
343                    }
344                } else {
345                    Ok(())
346                }
347            }
348            ColorType::Rgb555 => target.fill_contiguous(
349                &area,
350                RawColors::<RawU16>::new(&self.raw_bmp).map(|raw| Rgb555::from(raw).into()),
351            ),
352            ColorType::Rgb565 => target.fill_contiguous(
353                &area,
354                RawColors::<RawU16>::new(&self.raw_bmp).map(|raw| Rgb565::from(raw).into()),
355            ),
356            ColorType::Rgb888 => target.fill_contiguous(
357                &area,
358                RawColors::<RawU24>::new(&self.raw_bmp).map(|raw| Rgb888::from(raw).into()),
359            ),
360            ColorType::Xrgb8888 => target.fill_contiguous(
361                &area,
362                RawColors::<RawU32>::new(&self.raw_bmp)
363                    .map(|raw| Rgb888::from(RawU24::new(raw.into_inner())).into()),
364            ),
365        }
366    }
367
368    fn draw_sub_image<D>(&self, target: &mut D, area: &Rectangle) -> Result<(), D::Error>
369    where
370        D: DrawTarget<Color = Self::Color>,
371    {
372        self.draw(&mut target.translated(-area.top_left).clipped(area))
373    }
374}
375
376impl<C> OriginDimensions for Bmp<'_, C>
377where
378    C: PixelColor,
379{
380    fn size(&self) -> Size {
381        self.raw_bmp.header().image_size
382    }
383}
384
385impl<C> GetPixel for Bmp<'_, C>
386where
387    C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>,
388{
389    type Color = C;
390
391    fn pixel(&self, p: Point) -> Option<Self::Color> {
392        match self.raw_bmp.color_type {
393            ColorType::Index1 => self
394                .raw_bmp
395                .color_table()
396                .and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?))
397                .map(Into::into),
398            ColorType::Index4 => self
399                .raw_bmp
400                .color_table()
401                .and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?))
402                .map(Into::into),
403            ColorType::Index8 => self
404                .raw_bmp
405                .color_table()
406                .and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?))
407                .map(Into::into),
408            ColorType::Rgb555 => self
409                .raw_bmp
410                .pixel(p)
411                .map(|raw| Rgb555::from(RawU16::from_u32(raw)).into()),
412            ColorType::Rgb565 => self
413                .raw_bmp
414                .pixel(p)
415                .map(|raw| Rgb565::from(RawU16::from_u32(raw)).into()),
416            ColorType::Rgb888 => self
417                .raw_bmp
418                .pixel(p)
419                .map(|raw| Rgb888::from(RawU24::from_u32(raw)).into()),
420            ColorType::Xrgb8888 => self
421                .raw_bmp
422                .pixel(p)
423                .map(|raw| Rgb888::from(RawU24::from_u32(raw)).into()),
424        }
425    }
426}
427
428/// Parse error.
429#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
430pub enum ParseError {
431    /// The image uses an unsupported bit depth.
432    UnsupportedBpp(u16),
433
434    /// Unexpected end of file.
435    UnexpectedEndOfFile,
436
437    /// Invalid file signatures.
438    ///
439    /// BMP files must start with `BM`.
440    InvalidFileSignature([u8; 2]),
441
442    /// Unsupported compression method.
443    UnsupportedCompressionMethod(u32),
444
445    /// Unsupported header length.
446    UnsupportedHeaderLength(u32),
447
448    /// Unsupported channel masks.
449    UnsupportedChannelMasks,
450
451    /// Invalid image dimensions.
452    InvalidImageDimensions,
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    const BMP_DATA: &[u8] = include_bytes!("../tests/chessboard-8px-1bit.bmp");
460
461    fn bmp_data() -> [u8; 94] {
462        BMP_DATA.try_into().unwrap()
463    }
464
465    #[test]
466    fn error_unsupported_bpp() {
467        // Replace BPP value with an invalid value of 42.
468        let mut data = bmp_data();
469        data[0x1C..0x1C + 2].copy_from_slice(&42u16.to_le_bytes());
470
471        assert_eq!(
472            Bmp::<Rgb888>::from_slice(&data),
473            Err(ParseError::UnsupportedBpp(42))
474        );
475    }
476
477    #[test]
478    fn error_empty_file() {
479        assert_eq!(
480            Bmp::<Rgb888>::from_slice(&[]),
481            Err(ParseError::UnexpectedEndOfFile)
482        );
483    }
484
485    #[test]
486    fn error_truncated_header() {
487        let data = &BMP_DATA[0..10];
488
489        assert_eq!(
490            Bmp::<Rgb888>::from_slice(data),
491            Err(ParseError::UnexpectedEndOfFile)
492        );
493    }
494
495    #[test]
496    fn error_truncated_image_data() {
497        let (_, data) = BMP_DATA.split_last().unwrap();
498
499        assert_eq!(
500            Bmp::<Rgb888>::from_slice(data),
501            Err(ParseError::UnexpectedEndOfFile)
502        );
503    }
504
505    #[test]
506    fn error_invalid_signature() {
507        // Replace signature with "EG".
508        let mut data = bmp_data();
509        data[0..2].copy_from_slice(b"EG");
510
511        assert_eq!(
512            Bmp::<Rgb888>::from_slice(&data),
513            Err(ParseError::InvalidFileSignature([b'E', b'G']))
514        );
515    }
516
517    #[test]
518    fn error_compression_method() {
519        // Replace compression method with BI_JPEG (4).
520        let mut data = bmp_data();
521        data[0x1E..0x1E + 4].copy_from_slice(&4u32.to_le_bytes());
522
523        assert_eq!(
524            Bmp::<Rgb888>::from_slice(&data),
525            Err(ParseError::UnsupportedCompressionMethod(4))
526        );
527    }
528
529    #[test]
530    fn error_header_length() {
531        // Replace header length with invalid length of 16.
532        let mut data = bmp_data();
533        data[0x0E..0x0E + 4].copy_from_slice(&16u32.to_le_bytes());
534
535        assert_eq!(
536            Bmp::<Rgb888>::from_slice(&data),
537            Err(ParseError::UnsupportedHeaderLength(16))
538        );
539    }
540}