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}