Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@

- [#51](https://github.com/embedded-graphics/tinybmp/pull/51) Added `RawBmp::colors` method to iterator of the raw color values in a file.
- [#51](https://github.com/embedded-graphics/tinybmp/pull/51) Added `DynamicRawColors`, `RawColors`, `Rle4Colors`, and `Rle8Colors` iterators.
- [#52](https://github.com/embedded-graphics/tinybmp/pull/52) Added `Rle{4,8}Runs`, useful in combination with `fill_solid`.

### Changed

- **(breaking)** [#49](https://github.com/embedded-graphics/tinybmp/pull/41) Use 1.81 as the MSRV.
- [#46](https://github.com/embedded-graphics/tinybmp/pull/46) `Bmp::from_slice` is now `const`, so BMPs can be put in `const`s and `static`s.
- [#47](https://github.com/embedded-graphics/tinybmp/pull/47) Ignore compressed data length on RGB compression method.
- [#52](https://github.com/embedded-graphics/tinybmp/pull/52) Combined `Rle4Colors` and `Rle8Colors` into a single `RleColors` generic over `Rle{4,8}Runs`.

## [0.6.0] - 2024-06-11

Expand Down
47 changes: 33 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ 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};
pub use raw_iter::{
DynamicRawColors, RawColors, RawPixel, RawPixels, Rle4Runs, Rle8Runs, RleColors,
};

/// A BMP-format bitmap.
///
Expand Down Expand Up @@ -266,7 +268,6 @@ 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 => {
Expand All @@ -293,20 +294,29 @@ where
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 mut runs = Rle4Runs::new(&self.raw_bmp);
let map_color = |color: RawU4| {
color_table
.get(color.into_inner() as u32)
.map(Into::into)
.unwrap_or(fallback_color)
};

// RLE produces pixels in bottom-up order, so we draw them line by line rather than the entire bitmap at once.
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))?;
runs.start_row();

let mut point = Point::new(0, y as i32);
for (raw_color, count) in runs.by_ref() {
let size = Size::new(count as u32, 1);
let color = map_color(raw_color);
target.fill_solid(&Rectangle::new(point, size), color)?;
point.x += count as i32;

if point.x >= area.size.width as i32 {
break;
}
}
}
Ok(())
} else {
Expand All @@ -328,20 +338,29 @@ where
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 mut runs = Rle8Runs::new(&self.raw_bmp);
let map_color = |color: RawU8| {
color_table
.get(color.into_inner() as u32)
.map(Into::into)
.unwrap_or(fallback_color)
};

// RLE produces pixels in bottom-up order, so we draw them line by line rather than the entire bitmap at once.
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))?;
runs.start_row();

let mut point = Point::new(0, y as i32);
for (raw_color, count) in runs.by_ref() {
let size = Size::new(count as u32, 1);
let color = map_color(raw_color);
target.fill_solid(&Rectangle::new(point, size), color)?;
point.x += count as i32;

if point.x >= area.size.width as i32 {
break;
}
}
}
Ok(())
} else {
Expand Down
164 changes: 102 additions & 62 deletions src/raw_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ pub enum DynamicRawColors<'a> {
/// 32 bits per pixel
Bpp32(RawColors<'a, RawU32>),
/// RLE encoded with 4 bits per pixel
Bpp4Rle(Rle4Colors<'a>),
Bpp4Rle(RleColors<RawU4, Rle4Runs<'a>>),
/// RLE encoded with 8 bits per pixel
Bpp8Rle(Rle8Colors<'a>),
Bpp8Rle(RleColors<RawU8, Rle8Runs<'a>>),
}

impl core::fmt::Debug for DynamicRawColors<'_> {
Expand Down Expand Up @@ -193,22 +193,59 @@ impl Iterator for PixelPoints {
}
}

/// Iterator over individual BMP RLE8 encoded pixels.
///
/// Each pixel is returned as a `u32` regardless of the bit depth of the source image.
/// Iterator over individual BMP RLE encoded pixels.
#[derive(Debug)]
pub struct Rle8Colors<'a> {
/// Our source data
pub struct RleColors<C, I: Iterator<Item = (C, usize)>> {
runs: I,
run: Option<(C, usize)>,
}

impl<C, I: Iterator<Item = (C, usize)>> RleColors<C, I> {
/// Create a new RLE pixel iterator.
pub(crate) fn new(runs: I) -> Self {
Self { runs, run: None }
}

/// Get a mutable reference to the underlying runs iterator.
pub fn runs(&mut self) -> &mut I {
&mut self.runs
}
Comment on lines +210 to +212
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called as_runs and a to_runs method could also be useful:

Suggested change
pub fn runs(&mut self) -> &mut I {
&mut self.runs
}
pub fn as_runs(&mut self) -> &mut I {
&mut self.runs
}
pub fn to_runs(self) -> I {
self.runs
}

}

impl<C: Copy, I: Iterator<Item = (C, usize)>> Iterator for RleColors<C, I> {
type Item = C;

fn next(&mut self) -> Option<Self::Item> {
loop {
let (raw, count) = match self.run.as_mut() {
Some(run) => run,
None => {
self.run = Some(self.runs.next()?);
self.run.as_mut().unwrap()
}
};
if *count > 0 {
*count -= 1;
return Some(*raw);
} else {
self.run = None;
}
}
Comment on lines +219 to +233
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to use an Option for the current run, because count == 0 can be used to describe the same state as None.

By changing the struct to:

pub struct RleColors<C, I: Iterator<Item = (C, usize)>> {
    runs: I,
    color: C,
    count: usize,
}

(color can be initialized to C::default() in the constructor)
the next method can be simplified to:

Suggested change
loop {
let (raw, count) = match self.run.as_mut() {
Some(run) => run,
None => {
self.run = Some(self.runs.next()?);
self.run.as_mut().unwrap()
}
};
if *count > 0 {
*count -= 1;
return Some(*raw);
} else {
self.run = None;
}
}
loop {
if self.count == 0 {
(self.color, self.count) = self.runs.next()?;
}
if self.count > 0 {
self.count -= 1;
return Some(self.color);
}
}

}
}

/// Iterator over BMP RLE8 runs.
#[derive(Debug)]
pub struct Rle8Runs<'a> {
data: &'a [u8],
/// Our state
rle_state: RleState,
start_of_row: bool,
}

impl<'a> Rle8Colors<'a> {
impl<'a> Rle8Runs<'a> {
/// Create a new RLE pixel iterator.
pub(crate) fn new(raw_bmp: &RawBmp<'a>) -> Rle8Colors<'a> {
Rle8Colors {
pub(crate) fn new(raw_bmp: &RawBmp<'a>) -> Rle8Runs<'a> {
Rle8Runs {
data: raw_bmp.image_data(),
rle_state: RleState::Starting,
start_of_row: false,
Expand All @@ -221,15 +258,14 @@ impl<'a> Rle8Colors<'a> {
}
}

impl<'a> Iterator for Rle8Colors<'a> {
type Item = RawU8;
impl Iterator for Rle8Runs<'_> {
type Item = (RawU8, usize);

fn next(&mut self) -> Option<Self::Item> {
loop {
match self.rle_state {
RleState::EndOfBitmap => {
return None;
}
RleState::EndOfBitmap => return None,

RleState::Absolute {
remaining,
is_odd,
Expand All @@ -250,28 +286,25 @@ impl<'a> Iterator for Rle8Colors<'a> {
} else {
self.data = self.data.get(1..)?;
}
return Some(RawU8::from(value));
return Some((RawU8::from(value), 1));
}
RleState::Running {
remaining,
value,
is_odd,
is_odd: _,
} => {
if remaining == 0 {
self.rle_state = RleState::Starting;
} else {
self.rle_state = RleState::Running {
remaining: remaining.saturating_sub(1),
value,
is_odd,
};
}
return Some(RawU8::from(value));
// total pixels represented by this run
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for the RLE8 case I don't think we need the RLE state anymore. If you take a look at, for example, the way RleState::Running is handled. The state is set to running in RleState::Starting, just to be immediately reset back to Starting in the next loop iteration. And the number of pixels is first decremented by one to then be incremented by one again.

let total_pixels = (remaining as usize) + 1;

// consume whole run
self.rle_state = RleState::Starting;

return Some((RawU8::from(value), total_pixels));
}

RleState::Starting => {
let length = *self.data.get(0)?;
let param = *self.data.get(1)?;
self.data = &self.data.get(2..)?;
let (&[length, param], rest) = self.data.split_first_chunk()?;
self.data = rest;
match length {
0 => {
// The first byte of the pair can be set to zero to
Expand Down Expand Up @@ -320,22 +353,20 @@ impl<'a> Iterator for Rle8Colors<'a> {
}
}

/// Iterator over individual BMP RLE4 encoded pixels.
///
/// Each pixel is returned as a `u32` regardless of the bit depth of the source image.
/// Iterator over BMP RLE4 runs.
#[derive(Debug)]
pub struct Rle4Colors<'a> {
pub struct Rle4Runs<'a> {
/// Our source data
data: &'a [u8],
/// Our state
rle_state: RleState,
start_of_row: bool,
}

impl<'a> Rle4Colors<'a> {
impl<'a> Rle4Runs<'a> {
/// Create a new RLE pixel iterator.
pub(crate) fn new(raw_bmp: &RawBmp<'a>) -> Rle4Colors<'a> {
Rle4Colors {
pub(crate) fn new(raw_bmp: &RawBmp<'a>) -> Rle4Runs<'a> {
Rle4Runs {
data: raw_bmp.image_data(),
rle_state: RleState::Starting,
start_of_row: false,
Expand All @@ -348,15 +379,13 @@ impl<'a> Rle4Colors<'a> {
}
}

impl<'a> Iterator for Rle4Colors<'a> {
type Item = RawU4;
impl<'a> Iterator for Rle4Runs<'a> {
type Item = (RawU4, usize);

fn next(&mut self) -> Option<Self::Item> {
loop {
match self.rle_state {
RleState::EndOfBitmap => {
return None;
}
RleState::EndOfBitmap => return None,
RleState::Absolute {
remaining,
is_odd,
Expand Down Expand Up @@ -398,7 +427,8 @@ impl<'a> Iterator for Rle4Colors<'a> {
// remove the padding byte too
self.data = self.data.get(1..)?;
}
return Some(RawU4::from(nibble_value));

return Some((RawU4::from(nibble_value), 1));
}
RleState::Running {
remaining,
Expand All @@ -419,25 +449,35 @@ impl<'a> Iterator for Rle4Colors<'a> {

let remaining_is_odd = (remaining % 2) != 0;
let want_left = remaining_is_odd != is_odd;

let nibble_value = if want_left { value >> 4 } else { value & 0x0F };

if remaining == 0 {
// total pixels represented by this run
let total_pixels = (remaining as usize) + 1;

let hi = value >> 4;
let lo = value & 0x0F;

if hi == lo {
// entire run is a single repeated nibble -> consume whole run
self.rle_state = RleState::Starting;
return Some((RawU4::from(nibble_value), total_pixels));
} else {
self.rle_state = RleState::Running {
remaining: remaining.saturating_sub(1),
value,
is_odd,
};
// alternating pattern -> only one pixel of this color now
if remaining == 0 {
self.rle_state = RleState::Starting;
} else {
self.rle_state = RleState::Running {
remaining: remaining.saturating_sub(1),
value,
is_odd,
};
}
return Some((RawU4::from(nibble_value), 1));
Comment on lines +465 to +475
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the alternating case it might be worth looking into returning an enum instead of the tuple from the runs iterators. Something like:

enum RleRun<R> {
    Solid {
        color: R,
        count: usize,
    },
    Alternating {
        colors: [R; 2],
        count: usize,
    }
}

This would make it possible to draw the alternating pattern with DrawTarget::draw_contiguous, instead of drawing individual pixels.

}

return Some(RawU4::from(nibble_value));
}
RleState::Starting => {
let length = *self.data.get(0)?;
let param = *self.data.get(1)?;
self.data = &self.data.get(2..)?;
let (&[length, param], rest) = self.data.split_first_chunk()?;
self.data = rest;
match length {
0 => {
// The first byte of the pair can be set to zero to
Expand All @@ -452,10 +492,8 @@ impl<'a> Iterator for Rle4Colors<'a> {
return None;
}
}
1 => {
// End of bitmap
self.rle_state = RleState::EndOfBitmap;
}
// End of bitmap
1 => self.rle_state = RleState::EndOfBitmap,
2 => {
// Delta encoding is unsupported.
return None;
Expand Down Expand Up @@ -501,15 +539,17 @@ impl<'a> RawPixels<'a> {
let header = raw_bmp.header();
match header.compression_method {
CompressionMethod::Rle4 => {
let colors = Rle4Colors::new(raw_bmp);
let runs = Rle4Runs::new(raw_bmp);
let colors = RleColors::new(runs);
let points = PixelPoints::new(header.image_size, RowOrder::BottomUp);
Self {
colors: DynamicRawColors::Bpp4Rle(colors),
points,
}
}
CompressionMethod::Rle8 => {
let colors = Rle8Colors::new(raw_bmp);
let runs = Rle8Runs::new(raw_bmp);
let colors = RleColors::new(runs);
let points = PixelPoints::new(header.image_size, RowOrder::BottomUp);
Self {
colors: DynamicRawColors::Bpp8Rle(colors),
Expand Down