Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6470940
Add support for 1bpp color table mapping
jamwaffles Mar 15, 2022
e908bcf
Add an inverted test
jamwaffles Mar 15, 2022
a3dd79f
Cleanup
jamwaffles Mar 15, 2022
1c00f77
Changelog entry
jamwaffles Mar 15, 2022
3ae13e6
Regenerate readme
jamwaffles Mar 15, 2022
471bd12
Refactor header parsing to better support color tables
jamwaffles Mar 19, 2022
d4f6f79
Cleanups and changelog update
jamwaffles Mar 19, 2022
b510dc4
Improve indexed test with image <-> image compare
jamwaffles Mar 22, 2022
c1ee8d2
Use variable to make test clearer
jamwaffles Mar 22, 2022
3c7b82a
Apply suggestions from code review
jamwaffles Mar 22, 2022
8a09599
Use checked subtraction
jamwaffles Mar 22, 2022
a4697e4
Make color table size non-optional
jamwaffles Mar 22, 2022
1e44346
Move color table into RawBmp
jamwaffles Mar 22, 2022
1090814
use variable instead of 0 in other test
jamwaffles Mar 22, 2022
c7b8dc8
Use slice syntax for 1.51.0
jamwaffles Mar 22, 2022
a99a26a
Regenerate readme
jamwaffles Mar 22, 2022
a62bedd
Move color table out of RawPixels
jamwaffles Mar 25, 2022
31b4cfb
Move color table mapping to RawBmp::draw
jamwaffles Mar 27, 2022
4fcb53b
Cleanup/DRY
jamwaffles Mar 27, 2022
daf69e1
Nit
jamwaffles Mar 27, 2022
e34b8d7
Apply suggestions from code review
jamwaffles Mar 27, 2022
dc0cde9
Code review suggestion fixups
jamwaffles Mar 27, 2022
a3809d6
Move MSRV comment
jamwaffles Mar 27, 2022
8361bcd
Make color table non-optional
jamwaffles Mar 27, 2022
b8311db
Revert to having an optional color table
jamwaffles Mar 28, 2022
02cdc3c
Don't draw <= 8 BPP images without color table
rfuest Mar 28, 2022
5c2f59d
Tweak changelog
jamwaffles Mar 28, 2022
10b0427
Add ColorTable struct
rfuest Mar 29, 2022
838b5e9
Attempt to fix 1.40.0 compile
jamwaffles Mar 29, 2022
94f14c9
Un-pub ColorTable struct
jamwaffles Apr 14, 2022
365a558
Tweak changelog, add TODO
jamwaffles Apr 15, 2022
7010a58
Refactor color table entries from comments
jamwaffles Apr 16, 2022
304c2b4
Undo some import reordering changes
jamwaffles Apr 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## [Unreleased] - ReleaseDate

### Added

- [#19](https://github.com/embedded-graphics/tinybmp/pull/19) Added support for color mapped 1bpp and 8bpp images. This change now also requires 1bpp and 8bpp images to contain a color table.

## [0.3.1] - 2021-06-16

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ assert_eq!(
image_size: Size::new(8, 8),
image_data_len: 192,
channel_masks: None,
row_order: RowOrder::BottomUp
row_order: RowOrder::BottomUp,
}
);

Expand Down
97 changes: 97 additions & 0 deletions src/color_table.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use core::convert::TryInto;
use embedded_graphics::prelude::{PixelColor, RawData};

/// Color table.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ColorTable<'a> {
data: &'a [u8],
}

impl<'a> ColorTable<'a> {
pub(crate) fn new(data: &'a [u8]) -> Self {
Self { data }
}

/// Returns the number of entries.
// Only used in tests, hence the allow
#[allow(unused)]
fn len(&self) -> usize {
return self.data.len() / 4;
}

/// Returns the raw value of a color table entry.
///
/// `None` is returned if `index` is out of bounds.
pub fn get_raw<R: RawData>(&self, index: u32) -> Option<R> {
// MSRV: Experiment with slice::as_chunks when it's stabilized

let offset = index as usize * 4;
let bytes = self.data.get(offset..offset + 4)?;

let raw = u32::from_le_bytes(bytes.try_into().unwrap());

Some(R::from_u32(raw))
}

/// Returns a color table entry.
///
/// `None` is returned if `index` is out of bounds.
pub fn get<C: PixelColor + From<<C as PixelColor>::Raw>>(&self, index: u32) -> Option<C> {
self.get_raw::<C::Raw>(index).map(Into::into)
}
}

// TODO: When the color table is made public, the tests below should be moved into the corresponding
// files under `tests/`.
#[cfg(test)]
mod tests {
use crate::{Bmp, RawBmp};
use embedded_graphics::pixelcolor::{raw::RawU32, Rgb888};

#[test]
fn chessboard_8px_1bit() {
let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-1bit.bmp"))
.expect("Failed to parse");

let color_table = bmp.color_table().unwrap();
assert_eq!(color_table.len(), 2);
assert_eq!(color_table.get_raw(0), Some(RawU32::new(0x00000000)));
assert_eq!(color_table.get_raw(1), Some(RawU32::new(0xFFFFFFFF)));
assert_eq!(color_table.get_raw(2), Option::<RawU32>::None);

assert_eq!(bmp.image_data().len(), 94 - 62);
}

#[test]
fn chessboard_8px_16bit() {
let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-color-16bit.bmp"))
.expect("Failed to parse");

assert!(
bmp.color_table().is_none(),
"there should be no color table for this image"
);
}

#[test]
fn chessboard_8px_24bit() {
let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-24bit.bmp"))
.expect("Failed to parse");

assert!(
bmp.color_table().is_none(),
"there should be no color table for this image"
);
}

#[test]
fn colors_8bpp_indexed() {
let bmp = Bmp::<'_, Rgb888>::from_slice(include_bytes!("../tests/colors_8bpp_indexed.bmp"))
.expect("Failed to parse");

assert!(
bmp.as_raw().color_table().is_some(),
"there should be a color table for this image"
);
}
}
136 changes: 136 additions & 0 deletions src/header/dib_header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! Device Independent Bitmap (DIB) header.

use crate::{header::CompressionMethod, Bpp, ChannelMasks, RowOrder};
use embedded_graphics::geometry::Size;
use nom::{
combinator::map_opt,
error::{ErrorKind, ParseError},
multi::length_data,
number::complete::{le_i32, le_u16, le_u32},
IResult,
};

const DIB_INFO_HEADER_SIZE: usize = 40;
const DIB_V3_HEADER_SIZE: usize = 56;
const DIB_V4_HEADER_SIZE: usize = 108;
const DIB_V5_HEADER_SIZE: usize = 124;

/// Device Independent Bitmap (DIB) header.
#[derive(Debug)]
pub struct DibHeader {
pub image_size: Size,
pub bpp: Bpp,
pub compression: CompressionMethod,
pub image_data_len: u32,
pub channel_masks: Option<ChannelMasks>,
pub header_type: HeaderType,
pub row_order: RowOrder,
/// Entry length of color table (NOT length in bytes)
pub color_table_num_entries: u32,
}

impl DibHeader {
pub fn parse(input: &[u8]) -> IResult<&[u8], Self> {
// The header size in the BMP includes its own u32, so we strip it out by subtracting 4
// bytes to get the right final offset to the end of the header.
let (input, dib_header_data) =
length_data(map_opt(le_u32, |len| len.checked_sub(4)))(input)?;

// Add 4 back on so the constants remain the correct size relative to the BMP
// documentation/specs.
let header_type = match dib_header_data.len() + 4 {
DIB_V3_HEADER_SIZE => HeaderType::V3,
DIB_V4_HEADER_SIZE => HeaderType::V4,
DIB_V5_HEADER_SIZE => HeaderType::V5,
DIB_INFO_HEADER_SIZE => HeaderType::Info,
_ => {
return Err(nom::Err::Failure(nom::error::Error::from_error_kind(
dib_header_data,
ErrorKind::LengthValue,
)))
}
};

// Fields common to all DIB variants
let (dib_header_data, image_width) = le_u32(dib_header_data)?;
let (dib_header_data, image_height) = le_i32(dib_header_data)?;
let (dib_header_data, _color_planes) = le_u16(dib_header_data)?;
let (dib_header_data, bpp) = Bpp::parse(dib_header_data)?;

// Extra fields defined by DIB variants
// Variants are described in
// <https://www.liquisearch.com/bmp_file_format/file_structure/dib_header_bitmap_information_header>
// and <https://docs.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types>
let (dib_header_data, compression_method) = CompressionMethod::parse(dib_header_data)?;
let (dib_header_data, image_data_len) = le_u32(dib_header_data)?;
let (dib_header_data, _pels_per_meter_x) = le_u32(dib_header_data)?;
let (dib_header_data, _pels_per_meter_y) = le_u32(dib_header_data)?;
let (dib_header_data, colors_used) = le_u32(dib_header_data)?;
let (dib_header_data, _colors_important) = le_u32(dib_header_data)?;

let (_dib_header_data, channel_masks) = if header_type.is_at_least(HeaderType::V3)
&& compression_method == CompressionMethod::Bitfields
{
let (dib_header_data, mask_red) = le_u32(dib_header_data)?;
let (dib_header_data, mask_green) = le_u32(dib_header_data)?;
let (dib_header_data, mask_blue) = le_u32(dib_header_data)?;
let (dib_header_data, mask_alpha) = le_u32(dib_header_data)?;

(
dib_header_data,
Some(ChannelMasks {
red: mask_red,
green: mask_green,
blue: mask_blue,
alpha: mask_alpha,
}),
)
} else {
(dib_header_data, None)
};

let color_table_num_entries: u32 = if colors_used == 0 {
if bpp.bits() < 16 {
bpp.bits().pow(2).into()
} else {
0
}
} else {
colors_used
};

let row_order = if image_height < 0 {
RowOrder::TopDown
} else {
RowOrder::BottomUp
};

Ok((
input,
Self {
header_type,
image_size: Size::new(image_width, image_height.abs() as u32),
image_data_len,
bpp,
channel_masks,
compression: compression_method,
row_order,
color_table_num_entries,
},
))
}
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum HeaderType {
Info,
V3,
V4,
V5,
}

impl HeaderType {
fn is_at_least(self, header_type: HeaderType) -> bool {
self as u8 >= header_type as u8
}
}
91 changes: 41 additions & 50 deletions src/header.rs → src/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
//! Information gleaned from [wikipedia](https://en.wikipedia.org/wiki/BMP_file_format) and
//! [this website](http://paulbourke.net/dataformats/bmp/)

mod dib_header;

use crate::{color_table::ColorTable, header::dib_header::DibHeader};
use embedded_graphics::prelude::*;
use nom::{
bytes::complete::tag,
combinator::map_opt,
number::complete::{le_i32, le_u16, le_u32},
bytes::complete::{tag, take},
combinator::{map, map_opt},
error::{ErrorKind, ParseError},
number::complete::{le_u16, le_u32},
IResult,
};

Expand Down Expand Up @@ -97,67 +101,54 @@ pub struct Header {
}

impl Header {
pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Header> {
pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], (Header, Option<ColorTable<'_>>)> {
// File header
let (input, _) = tag("BM")(input)?;
let (input, file_size) = le_u32(input)?;
let (input, _reserved_1) = le_u16(input)?;
let (input, _reserved_2) = le_u16(input)?;
let (input, image_data_start) = le_u32(input)?;
let (input, header_size) = le_u32(input)?;
let (input, image_width) = le_u32(input)?;
let (input, image_height) = le_i32(input)?;
let (input, _color_planes) = le_u16(input)?;
let (input, bpp) = Bpp::parse(input)?;
let (input, compression_method) = CompressionMethod::parse(input)?;
let (input, image_data_len) = le_u32(input)?;

let row_order = if image_height < 0 {
RowOrder::TopDown
} else {
RowOrder::BottomUp
};

let (input, channel_masks) = if compression_method == CompressionMethod::Bitfields {
// BMP header versions can be distinguished by the header length.
// The color bit masks are only included in headers with at least version 3.
if header_size >= 56 {
let (input, _pels_per_meter_x) = le_u32(input)?;
let (input, _pels_per_meter_y) = le_u32(input)?;
let (input, _clr_used) = le_u32(input)?;
let (input, _clr_important) = le_u32(input)?;
let (input, mask_red) = le_u32(input)?;
let (input, mask_green) = le_u32(input)?;
let (input, mask_blue) = le_u32(input)?;
let (input, mask_alpha) = le_u32(input)?;

(
// DIB header
let (input, dib_header) = DibHeader::parse(input)?;

match dib_header.bpp {
// Images with BPP <= 8 MUST include a color table
Bpp::Bits1 | Bpp::Bits8 if dib_header.color_table_num_entries == 0 => {
return Err(nom::Err::Failure(nom::error::Error::from_error_kind(
input,
Some(ChannelMasks {
red: mask_red,
green: mask_green,
blue: mask_blue,
alpha: mask_alpha,
}),
)
} else {
// TODO: this should probably be an error
(input, None)
ErrorKind::LengthValue,
)));
}
_ => (),
}

let (input, color_table) = if dib_header.color_table_num_entries > 0 {
// Each color table entry is 4 bytes long
let (input, table) = map(
take(dib_header.color_table_num_entries as usize * 4),
|data| ColorTable::new(data),
)(input)?;

(input, Some(table))
} else {
(input, None)
};

Ok((
input,
Header {
file_size,
image_data_start: image_data_start as usize,
image_size: Size::new(image_width, image_height.abs() as u32),
image_data_len,
bpp,
channel_masks,
row_order,
},
(
Header {
file_size,
image_data_start: image_data_start as usize,
image_size: dib_header.image_size,
image_data_len: dib_header.image_data_len,
bpp: dib_header.bpp,
channel_masks: dib_header.channel_masks,
row_order: dib_header.row_order,
},
color_table,
),
))
}
}
Expand Down
Loading