#![no_std]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(missing_copy_implementations)]
#![deny(trivial_casts)]
#![deny(trivial_numeric_casts)]
#![deny(unsafe_code)]
#![deny(unstable_features)]
#![deny(unused_import_braces)]
#![deny(unused_qualifications)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_intra_doc_links)]
use core::marker::PhantomData;
use embedded_graphics::{
image::GetPixel,
pixelcolor::{
raw::{RawU1, RawU16, RawU24, RawU32, RawU4, RawU8},
Rgb555, Rgb565, Rgb888,
},
prelude::*,
primitives::Rectangle,
};
mod color_table;
mod header;
mod iter;
mod parser;
mod raw_bmp;
mod raw_iter;
macro_rules! try_const {
($e:expr) => {
match $e {
Ok(ok) => ok,
Err(e) => return Err(e),
}
};
}
pub(crate) use try_const;
use raw_bmp::ColorType;
pub use color_table::ColorTable;
pub use header::CompressionMethod;
pub use header::{Bpp, ChannelMasks, Header, RowOrder};
pub use iter::Pixels;
pub use raw_bmp::RawBmp;
pub use raw_iter::{DynamicRawColors, RawColors, RawPixel, RawPixels, Rle4Colors, Rle8Colors};
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct Bmp<'a, C> {
raw_bmp: RawBmp<'a>,
color_type: PhantomData<C>,
}
impl<'a, C> Bmp<'a, C>
where
C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>,
{
pub const fn from_slice(bytes: &'a [u8]) -> Result<Self, ParseError> {
let raw_bmp = try_const!(RawBmp::from_slice(bytes));
Ok(Self {
raw_bmp,
color_type: PhantomData,
})
}
pub fn pixels(&self) -> Pixels<'_, C> {
Pixels::new(self)
}
pub const fn as_raw(&self) -> &RawBmp<'a> {
&self.raw_bmp
}
}
impl<C> ImageDrawable for Bmp<'_, C>
where
C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>,
{
type Color = C;
fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
where
D: DrawTarget<Color = C>,
{
let area = self.bounding_box();
let slice_size = Size::new(area.size.width, 1);
match self.raw_bmp.color_type {
ColorType::Index1 => {
if let Some(color_table) = self.raw_bmp.color_table() {
let fallback_color = C::from(Rgb888::BLACK);
let color_table: [C; 2] = [
color_table.get(0).map(Into::into).unwrap_or(fallback_color),
color_table.get(1).map(Into::into).unwrap_or(fallback_color),
];
let colors = RawColors::<RawU1>::new(&self.raw_bmp).map(|index| {
color_table
.get(usize::from(index.into_inner()))
.copied()
.unwrap_or(fallback_color)
});
target.fill_contiguous(&area, colors)
} else {
Ok(())
}
}
ColorType::Index4 => {
let header = self.raw_bmp.header();
let fallback_color = C::from(Rgb888::BLACK);
if let Some(color_table) = self.raw_bmp.color_table() {
if header.compression_method == CompressionMethod::Rle4 {
let mut colors = Rle4Colors::new(&self.raw_bmp);
let map_color = |color: RawU4| {
color_table
.get(color.into_inner() as u32)
.map(Into::into)
.unwrap_or(fallback_color)
};
for y in (0..area.size.height).rev() {
colors.start_row();
let row = Rectangle::new(Point::new(0, y as i32), slice_size);
let colors = colors.by_ref().map(map_color);
target.fill_contiguous(&row, colors.take(area.size.width as usize))?;
}
Ok(())
} else {
let colors = RawColors::<RawU4>::new(&self.raw_bmp).map(|index| {
color_table
.get(u32::from(index.into_inner()))
.map(Into::into)
.unwrap_or(fallback_color)
});
target.fill_contiguous(&area, colors)
}
} else {
Ok(())
}
}
ColorType::Index8 => {
let header = self.raw_bmp.header();
let fallback_color = C::from(Rgb888::BLACK);
if let Some(color_table) = self.raw_bmp.color_table() {
if header.compression_method == CompressionMethod::Rle8 {
let mut colors = Rle8Colors::new(&self.raw_bmp);
let map_color = |color: RawU8| {
color_table
.get(color.into_inner() as u32)
.map(Into::into)
.unwrap_or(fallback_color)
};
for y in (0..area.size.height).rev() {
colors.start_row();
let row = Rectangle::new(Point::new(0, y as i32), slice_size);
let colors = colors.by_ref().map(map_color);
target.fill_contiguous(&row, colors.take(area.size.width as usize))?;
}
Ok(())
} else {
let colors = RawColors::<RawU8>::new(&self.raw_bmp).map(|index| {
color_table
.get(u32::from(index.into_inner()))
.map(Into::into)
.unwrap_or(fallback_color)
});
target.fill_contiguous(&area, colors)
}
} else {
Ok(())
}
}
ColorType::Rgb555 => target.fill_contiguous(
&area,
RawColors::<RawU16>::new(&self.raw_bmp).map(|raw| Rgb555::from(raw).into()),
),
ColorType::Rgb565 => target.fill_contiguous(
&area,
RawColors::<RawU16>::new(&self.raw_bmp).map(|raw| Rgb565::from(raw).into()),
),
ColorType::Rgb888 => target.fill_contiguous(
&area,
RawColors::<RawU24>::new(&self.raw_bmp).map(|raw| Rgb888::from(raw).into()),
),
ColorType::Xrgb8888 => target.fill_contiguous(
&area,
RawColors::<RawU32>::new(&self.raw_bmp)
.map(|raw| Rgb888::from(RawU24::new(raw.into_inner())).into()),
),
}
}
fn draw_sub_image<D>(&self, target: &mut D, area: &Rectangle) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
self.draw(&mut target.translated(-area.top_left).clipped(area))
}
}
impl<C> OriginDimensions for Bmp<'_, C>
where
C: PixelColor,
{
fn size(&self) -> Size {
self.raw_bmp.header().image_size
}
}
impl<C> GetPixel for Bmp<'_, C>
where
C: PixelColor + From<Rgb555> + From<Rgb565> + From<Rgb888>,
{
type Color = C;
fn pixel(&self, p: Point) -> Option<Self::Color> {
match self.raw_bmp.color_type {
ColorType::Index1 => self
.raw_bmp
.color_table()
.and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?))
.map(Into::into),
ColorType::Index4 => self
.raw_bmp
.color_table()
.and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?))
.map(Into::into),
ColorType::Index8 => self
.raw_bmp
.color_table()
.and_then(|color_table| color_table.get(self.raw_bmp.pixel(p)?))
.map(Into::into),
ColorType::Rgb555 => self
.raw_bmp
.pixel(p)
.map(|raw| Rgb555::from(RawU16::from_u32(raw)).into()),
ColorType::Rgb565 => self
.raw_bmp
.pixel(p)
.map(|raw| Rgb565::from(RawU16::from_u32(raw)).into()),
ColorType::Rgb888 => self
.raw_bmp
.pixel(p)
.map(|raw| Rgb888::from(RawU24::from_u32(raw)).into()),
ColorType::Xrgb8888 => self
.raw_bmp
.pixel(p)
.map(|raw| Rgb888::from(RawU24::from_u32(raw)).into()),
}
}
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum ParseError {
UnsupportedBpp(u16),
UnexpectedEndOfFile,
InvalidFileSignature([u8; 2]),
UnsupportedCompressionMethod(u32),
UnsupportedHeaderLength(u32),
UnsupportedChannelMasks,
InvalidImageDimensions,
}
#[cfg(test)]
mod tests {
use super::*;
const BMP_DATA: &[u8] = include_bytes!("../tests/chessboard-8px-1bit.bmp");
fn bmp_data() -> [u8; 94] {
BMP_DATA.try_into().unwrap()
}
#[test]
fn error_unsupported_bpp() {
let mut data = bmp_data();
data[0x1C..0x1C + 2].copy_from_slice(&42u16.to_le_bytes());
assert_eq!(
Bmp::<Rgb888>::from_slice(&data),
Err(ParseError::UnsupportedBpp(42))
);
}
#[test]
fn error_empty_file() {
assert_eq!(
Bmp::<Rgb888>::from_slice(&[]),
Err(ParseError::UnexpectedEndOfFile)
);
}
#[test]
fn error_truncated_header() {
let data = &BMP_DATA[0..10];
assert_eq!(
Bmp::<Rgb888>::from_slice(data),
Err(ParseError::UnexpectedEndOfFile)
);
}
#[test]
fn error_truncated_image_data() {
let (_, data) = BMP_DATA.split_last().unwrap();
assert_eq!(
Bmp::<Rgb888>::from_slice(data),
Err(ParseError::UnexpectedEndOfFile)
);
}
#[test]
fn error_invalid_signature() {
let mut data = bmp_data();
data[0..2].copy_from_slice(b"EG");
assert_eq!(
Bmp::<Rgb888>::from_slice(&data),
Err(ParseError::InvalidFileSignature([b'E', b'G']))
);
}
#[test]
fn error_compression_method() {
let mut data = bmp_data();
data[0x1E..0x1E + 4].copy_from_slice(&4u32.to_le_bytes());
assert_eq!(
Bmp::<Rgb888>::from_slice(&data),
Err(ParseError::UnsupportedCompressionMethod(4))
);
}
#[test]
fn error_header_length() {
let mut data = bmp_data();
data[0x0E..0x0E + 4].copy_from_slice(&16u32.to_le_bytes());
assert_eq!(
Bmp::<Rgb888>::from_slice(&data),
Err(ParseError::UnsupportedHeaderLength(16))
);
}
}