| 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 | |