Skip to content
Merged
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
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

- [#55](https://github.com/embedded-graphics/tinybmp/pull/55) Added methods `CompressionMethod::is_compressed` and `Header::bytes_per_row`. The latter is essential information when walking through the bytes yourself.

## [0.7.0] - 2026-01-14

### Fixed
Expand Down
31 changes: 28 additions & 3 deletions src/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,30 @@ impl Header {
))
}

/// Returns the row length in bytes.
/// [`Self::bytes_per_row`] without checking the compression.
///
/// Each row in a BMP file is a multiple of 4 bytes long.
pub(crate) const fn bytes_per_row(&self) -> usize {
/// Only returns useful values for uncompressed formats.
pub(crate) const fn bytes_per_row_uncompressed(&self) -> usize {
let bits_per_row = self.image_size.width as usize * self.bpp.bits() as usize;

(bits_per_row + 31) / 32 * (32 / 8)
}

/// Calculates the row length in bytes for uncompressed BMP files.
///
/// This value, which is also called the stride, is the advancement from one
/// row to the next and may include padding bytes. It is determined by
/// multiplying the width of the image with the number of bytes per pixel
/// and rounding the result up to the next 4-byte boundary.
///
/// Returns `None` for compressed formats where the row lengths vary.
pub const fn bytes_per_row(&self) -> Option<usize> {
if self.compression_method.is_compressed() {
None
} else {
Some(self.bytes_per_row_uncompressed())
}
}
}

/// Bit masks for the color channels.
Expand Down Expand Up @@ -239,4 +255,13 @@ impl CompressionMethod {
let (input, value) = try_const!(le_u32(input));
Ok((input, try_const!(Self::new(value))))
}

/// Returns `true` if a BMP using this compression method is in fact
/// compressed.
pub const fn is_compressed(&self) -> bool {
match self {
Self::Rgb | Self::Bitfields => false,
Self::Rle8 | Self::Rle4 => true,
}
}
}
6 changes: 4 additions & 2 deletions src/raw_bmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ impl<'a> RawBmp<'a> {
// so we should calculate width x height instead.
let height = header.image_size.height as usize;

let Some(data_length) = header.bytes_per_row().checked_mul(height) else {
let Some(data_length) = header.bytes_per_row_uncompressed().checked_mul(height) else {
return Err(ParseError::UnexpectedEndOfFile);
};
data_length
Expand Down Expand Up @@ -139,7 +139,9 @@ impl<'a> RawBmp<'a> {
// The specialized implementations of `Iterator::nth` for `Chunks` and
// `RawDataSlice::IntoIter` are `O(1)`, which also makes this method `O(1)`.

let mut row_chunks = self.image_data.chunks_exact(self.header.bytes_per_row());
let mut row_chunks = self
.image_data
.chunks_exact(self.header.bytes_per_row_uncompressed());
let row = match self.header.row_order {
RowOrder::BottomUp => row_chunks.nth_back(p.y as usize),
RowOrder::TopDown => row_chunks.nth(p.y as usize),
Expand Down
4 changes: 3 additions & 1 deletion src/raw_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ where
let width = header.image_size.width as usize;

Self {
rows: raw_bmp.image_data().chunks_exact(header.bytes_per_row()),
rows: raw_bmp
.image_data()
.chunks_exact(header.bytes_per_row_uncompressed()),
row_order: raw_bmp.header().row_order,
current_row: RawDataSlice::new(&[]).into_iter().take(0),
width,
Expand Down
30 changes: 29 additions & 1 deletion tests/embedded_graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use embedded_graphics::{
prelude::*,
primitives::Rectangle,
};
use tinybmp::{Bmp, RowOrder};
use tinybmp::{Bmp, RawBmp, RowOrder};

#[test]
fn negative_top_left() {
Expand All @@ -31,6 +31,34 @@ fn dimensions() {
);
}

#[test]
fn bytes_per_row() {
const fn stride(width: usize, bpp: usize) -> usize {
// Formula from <https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader#calculating-surface-stride>.
((width * bpp + 31) & !31) >> 3
}

#[rustfmt::skip]
const TESTS: &[(&[u8], u32, Option<usize>)] = &[
(include_bytes!("stride-1px-32bpp.bmp"), 1, Some(stride(1, 32))),
(include_bytes!("stride-3px-24bpp.bmp"), 3, Some(stride(3, 24))),
(include_bytes!("stride-5px-16bpp-rgb565-bitfields.bmp"), 5, Some(stride(5, 16))),
(include_bytes!("stride-7px-8bpp.bmp"), 7, Some(stride(7, 8))),
(include_bytes!("stride-9px-4bpp.bmp"), 9, Some(stride(9, 4))),
(include_bytes!("stride-11px-1bpp.bmp"), 11, Some(stride(11, 1))),
(include_bytes!("logo-indexed-8bpp-rle8.bmp"), 240, None),
(include_bytes!("logo-indexed-4bpp-rle4.bmp"), 240, None),
];

for &(file, width, bytes_per_row) in TESTS {
let image = RawBmp::from_slice(file).unwrap();
let header = image.header();

assert_eq!(header.image_size.width, width);
assert_eq!(header.bytes_per_row(), bytes_per_row);
}
}

fn expected_image_color<C>() -> MockDisplay<C>
where
C: PixelColor + ColorMapping,
Expand Down
Binary file added tests/stride-11px-1bpp.bmp
Binary file not shown.
Binary file added tests/stride-1px-32bpp.bmp
Binary file not shown.
Binary file added tests/stride-3px-24bpp.bmp
Binary file not shown.
Binary file added tests/stride-5px-16bpp-rgb565-bitfields.bmp
Binary file not shown.
Binary file added tests/stride-7px-8bpp.bmp
Binary file not shown.
Binary file added tests/stride-9px-4bpp.bmp
Binary file not shown.
Loading