Skip to content

Commit 2477185

Browse files
thejpsterrfuest
andauthored
Add support for RLE encoded bitmaps. (#41)
* Add experimental support for RLE8 encoded bitmaps. * Update CHANGELOG and README. * Now you can display RLE8 bitmaps. Also adds tests and benchmarks. * Add RLE4 support. The count is of nibbles, but the padding only applies when there's an odd number of bytes. And the sequences alternate between nibbles. But otherwise it's similar to RLE8. * Increase MSRV to 1.71 * Applying review comments. * Faster RLE rendering. We can't render a whole frame as a contiguous block, we but we can render each line contiguously - we just have to start at the bottom line and work up. --------- Co-authored-by: Ralf Fuest <[email protected]>
1 parent ea2d38a commit 2477185

File tree

14 files changed

+528
-50
lines changed

14 files changed

+528
-50
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Check that everything (tests, benches, etc) builds in std environments
22
precheck_steps: &precheck_steps
33
docker: &docker
4-
- image: jamwaffles/circleci-embedded-graphics:1.61.0-0
4+
- image: jamwaffles/circleci-embedded-graphics:1.71.1-0
55
auth:
66
username: jamwaffles
77
password: $DOCKERHUB_PASSWORD

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
## [Unreleased] - ReleaseDate
88

9+
### Added
10+
11+
- [#41](https://github.com/embedded-graphics/tinybmp/pull/41) Added support for RLE8 and RLE4 encoded bitmaps
12+
13+
### Changed
14+
15+
- **(breaking)** [#41](https://github.com/embedded-graphics/tinybmp/pull/41) Use 1.71 as the MSRV.
16+
917
## [0.5.0] - 2023-05-17
1018

1119
### Changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ let pixel = bmp.pixel(Point::new(3, 2));
7979
assert_eq!(pixel, Some(Rgb888::WHITE));
8080
```
8181

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+
8286
### Accessing the raw image data
8387

8488
For most applications the higher level access provided by [`Bmp`] is sufficient. But in case
@@ -92,7 +96,7 @@ Similar to [`Bmp::pixel`], [`RawBmp::pixel`] can be used to get raw pixel color
9296

9397
```rust
9498
use embedded_graphics::prelude::*;
95-
use tinybmp::{RawBmp, Bpp, Header, RawPixel, RowOrder};
99+
use tinybmp::{RawBmp, Bpp, Header, RawPixel, RowOrder, CompressionMethod};
96100

97101
let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-24bit.bmp"))
98102
.expect("Failed to parse BMP image");
@@ -108,6 +112,7 @@ assert_eq!(
108112
image_data_len: 192,
109113
channel_masks: None,
110114
row_order: RowOrder::BottomUp,
115+
compression_method: CompressionMethod::Rgb,
111116
}
112117
);
113118

@@ -126,7 +131,7 @@ assert_eq!(pixel, Some(0xFFFFFFu32));
126131

127132
## Minimum supported Rust version
128133

129-
The minimum supported Rust version for tinybmp is `1.61` or greater. Ensure you have the correct
134+
The minimum supported Rust version for tinybmp is `1.71` or greater. Ensure you have the correct
130135
version of Rust installed, preferably through <https://rustup.rs>.
131136

132137
[`Bmp`]: https://docs.rs/tinybmp/latest/tinybmp/struct.Bmp.html

benches/draw.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ fn parser_benchmarks(c: &mut Criterion) {
9898
})
9999
});
100100

101+
c.bench_function("draw indexed 4BPP RLE4", |b| {
102+
let mut fb = Framebuffer::<Rgb888>::new();
103+
b.iter(|| {
104+
let bmp =
105+
Bmp::<Rgb888>::from_slice(include_bytes!("../tests/logo-indexed-4bpp-rle4.bmp"))
106+
.unwrap();
107+
Image::new(&bmp, Point::zero()).draw(&mut fb).unwrap();
108+
})
109+
});
110+
101111
c.bench_function("draw indexed 8BPP", |b| {
102112
let mut fb = Framebuffer::<Rgb888>::new();
103113
b.iter(|| {
@@ -107,6 +117,16 @@ fn parser_benchmarks(c: &mut Criterion) {
107117
})
108118
});
109119

120+
c.bench_function("draw indexed 8BPP RLE8", |b| {
121+
let mut fb = Framebuffer::<Rgb888>::new();
122+
b.iter(|| {
123+
let bmp =
124+
Bmp::<Rgb888>::from_slice(include_bytes!("../tests/logo-indexed-8bpp-rle8.bmp"))
125+
.unwrap();
126+
Image::new(&bmp, Point::zero()).draw(&mut fb).unwrap();
127+
})
128+
});
129+
110130
c.bench_function("draw dynamic RGB565 to RGB888", |b| {
111131
let mut fb = Framebuffer::<Rgb888>::new();
112132
b.iter(|| {

src/header/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ pub struct Header {
104104

105105
/// Row order of the image data within the file
106106
pub row_order: RowOrder,
107+
108+
/// The compression method
109+
pub compression_method: CompressionMethod,
107110
}
108111

109112
impl Header {
@@ -144,6 +147,7 @@ impl Header {
144147
bpp: dib_header.bpp,
145148
channel_masks: dib_header.channel_masks,
146149
row_order: dib_header.row_order,
150+
compression_method: dib_header.compression,
147151
},
148152
color_table,
149153
),
@@ -199,16 +203,29 @@ impl ChannelMasks {
199203
};
200204
}
201205

206+
/// Describes how the BMP file is compressed.
202207
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
203208
pub enum CompressionMethod {
209+
/// The bitmap is in uncompressed RGB and doesn't use color masks
204210
Rgb,
211+
/// The bitmap is in uncompressed RGB, using color masks
205212
Bitfields,
213+
/// The bitmap is compressed using run-length encoding (RLE) compression,
214+
/// with 8 bits per pixel. The compression uses a 2-byte format consisting
215+
/// of a count byte followed by a byte containing a color index.
216+
Rle8,
217+
/// The bitmap is compressed using run-length encoding (RLE) compression,
218+
/// with 4 bits per pixel. The compression uses a 2-byte format consisting
219+
/// of a count byte followed by two word-length color indexes.
220+
Rle4,
206221
}
207222

208223
impl CompressionMethod {
209224
const fn new(value: u32) -> Result<Self, ParseError> {
210225
Ok(match value {
211226
0 => Self::Rgb,
227+
1 => Self::Rle8,
228+
2 => Self::Rle4,
212229
3 => Self::Bitfields,
213230
_ => return Err(ParseError::UnsupportedCompressionMethod(value)),
214231
})

src/lib.rs

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@
7979
//! # Ok::<(), core::convert::Infallible>(()) }
8080
//! ```
8181
//!
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+
//!
8286
//! ## Accessing the raw image data
8387
//!
8488
//! For most applications the higher level access provided by [`Bmp`] is sufficient. But in case
@@ -92,7 +96,7 @@
9296
//!
9397
//! ```
9498
//! use embedded_graphics::prelude::*;
95-
//! use tinybmp::{RawBmp, Bpp, Header, RawPixel, RowOrder};
99+
//! use tinybmp::{RawBmp, Bpp, Header, RawPixel, RowOrder, CompressionMethod};
96100
//!
97101
//! let bmp = RawBmp::from_slice(include_bytes!("../tests/chessboard-8px-24bit.bmp"))
98102
//! .expect("Failed to parse BMP image");
@@ -108,6 +112,7 @@
108112
//! image_data_len: 192,
109113
//! channel_masks: None,
110114
//! row_order: RowOrder::BottomUp,
115+
//! compression_method: CompressionMethod::Rgb,
111116
//! }
112117
//! );
113118
//!
@@ -128,7 +133,7 @@
128133
//!
129134
//! # Minimum supported Rust version
130135
//!
131-
//! The minimum supported Rust version for tinybmp is `1.61` or greater. Ensure you have the correct
136+
//! The minimum supported Rust version for tinybmp is `1.71` or greater. Ensure you have the correct
132137
//! version of Rust installed, preferably through <https://rustup.rs>.
133138
//!
134139
//! <!-- README-LINKS
@@ -188,9 +193,10 @@ mod raw_bmp;
188193
mod raw_iter;
189194

190195
use raw_bmp::ColorType;
191-
use raw_iter::RawColors;
196+
use raw_iter::{RawColors, Rle4Pixels, Rle8Pixels};
192197

193198
pub use color_table::ColorTable;
199+
pub use header::CompressionMethod;
194200
pub use header::{Bpp, ChannelMasks, Header, RowOrder};
195201
pub use iter::Pixels;
196202
pub use raw_bmp::RawBmp;
@@ -249,6 +255,7 @@ where
249255
D: DrawTarget<Color = C>,
250256
{
251257
let area = self.bounding_box();
258+
let slice_size = Size::new(area.size.width, 1);
252259

253260
match self.raw_bmp.color_type {
254261
ColorType::Index1 => {
@@ -271,33 +278,69 @@ where
271278
}
272279
}
273280
ColorType::Index4 => {
281+
let header = self.raw_bmp.header();
282+
let fallback_color = C::from(Rgb888::BLACK);
274283
if let Some(color_table) = self.raw_bmp.color_table() {
275-
let fallback_color = C::from(Rgb888::BLACK);
276-
277-
let colors = RawColors::<RawU4>::new(&self.raw_bmp).map(|index| {
278-
color_table
279-
.get(u32::from(index.into_inner()))
280-
.map(Into::into)
281-
.unwrap_or(fallback_color)
282-
});
283-
284-
target.fill_contiguous(&area, colors)
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+
}
285310
} else {
286311
Ok(())
287312
}
288313
}
289314
ColorType::Index8 => {
315+
let header = self.raw_bmp.header();
316+
let fallback_color = C::from(Rgb888::BLACK);
290317
if let Some(color_table) = self.raw_bmp.color_table() {
291-
let fallback_color = C::from(Rgb888::BLACK);
292-
293-
let colors = RawColors::<RawU8>::new(&self.raw_bmp).map(|index| {
294-
color_table
295-
.get(u32::from(index.into_inner()))
296-
.map(Into::into)
297-
.unwrap_or(fallback_color)
298-
});
299-
300-
target.fill_contiguous(&area, colors)
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+
}
301344
} else {
302345
Ok(())
303346
}

src/raw_bmp.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,14 @@ impl<'a> RawBmp<'a> {
4444

4545
let color_type = ColorType::from_header(&header)?;
4646

47-
let height = header
48-
.image_size
49-
.height
50-
.try_into()
51-
.map_err(|_| ParseError::InvalidImageDimensions)?;
52-
53-
let data_length = header
54-
.bytes_per_row()
55-
.checked_mul(height)
56-
.ok_or(ParseError::InvalidImageDimensions)?;
47+
// Believe what the bitmap tells us rather than multiplying width by
48+
// height by bits-per-pixel, because the image data might be compressed.
49+
let compressed_data_length = header.image_data_len as usize;
5750

5851
// The `get` is split into two calls to prevent an possible integer overflow.
5952
let image_data = &bytes
6053
.get(header.image_data_start..)
61-
.and_then(|bytes| bytes.get(..data_length))
54+
.and_then(|bytes| bytes.get(..compressed_data_length))
6255
.ok_or(ParseError::UnexpectedEndOfFile)?;
6356

6457
Ok(Self {
@@ -97,7 +90,19 @@ impl<'a> RawBmp<'a> {
9790
///
9891
/// Returns `None` if `p` is outside the image bounding box. Note that this function doesn't
9992
/// apply a color map, if the image contains one.
93+
///
94+
/// This routine always returns `None` if the bitmap is RLE compressed, as RLE compressed
95+
/// bitmaps don't easily allow direct access to any given pixel.
10096
pub fn pixel(&self, p: Point) -> Option<u32> {
97+
if matches!(
98+
self.header.compression_method,
99+
crate::header::CompressionMethod::Rle8 | crate::header::CompressionMethod::Rle4
100+
) {
101+
// TODO implement direct access by counting `0x00, 0x00` pairs,
102+
// which uniquely mark the end of a line.
103+
return None;
104+
}
105+
101106
let width = self.header.image_size.width as i32;
102107
let height = self.header.image_size.height as i32;
103108

0 commit comments

Comments
 (0)