From c63c9711213d61c2e6bf1dfe59f75ac7a34fba98 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Mon, 17 Nov 2025 12:31:24 +0100 Subject: [PATCH 1/2] feat: use solid fills with rle compressed images --- src/lib.rs | 47 +++++++++----- src/raw_iter.rs | 164 ++++++++++++++++++++++++++++++------------------ 2 files changed, 135 insertions(+), 76 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 32fb510..8330590 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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. /// @@ -266,7 +268,6 @@ where D: DrawTarget, { let area = self.bounding_box(); - let slice_size = Size::new(area.size.width, 1); match self.raw_bmp.color_type { ColorType::Index1 => { @@ -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 { @@ -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 { diff --git a/src/raw_iter.rs b/src/raw_iter.rs index 168e528..280d3ae 100644 --- a/src/raw_iter.rs +++ b/src/raw_iter.rs @@ -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>), /// RLE encoded with 8 bits per pixel - Bpp8Rle(Rle8Colors<'a>), + Bpp8Rle(RleColors>), } impl core::fmt::Debug for DynamicRawColors<'_> { @@ -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> { + runs: I, + run: Option<(C, usize)>, +} + +impl> RleColors { + /// 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 + } +} + +impl> Iterator for RleColors { + type Item = C; + + fn next(&mut self) -> Option { + 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; + } + } + } +} + +/// 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, @@ -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 { loop { match self.rle_state { - RleState::EndOfBitmap => { - return None; - } + RleState::EndOfBitmap => return None, + RleState::Absolute { remaining, is_odd, @@ -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 + 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 @@ -320,11 +353,9 @@ 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 @@ -332,10 +363,10 @@ pub struct Rle4Colors<'a> { 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, @@ -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 { loop { match self.rle_state { - RleState::EndOfBitmap => { - return None; - } + RleState::EndOfBitmap => return None, RleState::Absolute { remaining, is_odd, @@ -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, @@ -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)); } - - 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 @@ -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; @@ -501,7 +539,8 @@ 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), @@ -509,7 +548,8 @@ impl<'a> RawPixels<'a> { } } 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), From 7774e7d4a726b2d4a8ca7e5a98e5b8179a94cae6 Mon Sep 17 00:00:00 2001 From: Oleksandr Babak Date: Mon, 17 Nov 2025 12:34:08 +0100 Subject: [PATCH 2/2] chore: changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dbfa3d..e216e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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