From a117d971947c0f20ed7f142376dd174da908f7d8 Mon Sep 17 00:00:00 2001 From: Enyium <123484196+Enyium@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:44:01 +0200 Subject: [PATCH 1/4] feat: add `CompressionMethod::is_compressed()` --- src/header/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/header/mod.rs b/src/header/mod.rs index 0f5a1ac..2df779f 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -239,4 +239,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, + } + } } From 77f3088928b98c91036e9caaed0a46f7990c69e2 Mon Sep 17 00:00:00 2001 From: Enyium <123484196+Enyium@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:51:09 +0200 Subject: [PATCH 2/4] feat: make `Header::bytes_per_row()` public This is essential information for the user when walking through the bytes themselves. The original crate-private function is kept with the `_uncompressed` suffix. The new public one returns an `Option`. --- src/header/mod.rs | 22 +++++++++++++++++++--- src/raw_bmp.rs | 6 ++++-- src/raw_iter.rs | 4 +++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/header/mod.rs b/src/header/mod.rs index 2df779f..959535b 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -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 { + if self.compression_method.is_compressed() { + None + } else { + Some(self.bytes_per_row_uncompressed()) + } + } } /// Bit masks for the color channels. diff --git a/src/raw_bmp.rs b/src/raw_bmp.rs index ac0b01f..e4e8622 100644 --- a/src/raw_bmp.rs +++ b/src/raw_bmp.rs @@ -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 @@ -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), diff --git a/src/raw_iter.rs b/src/raw_iter.rs index 168e528..17d0389 100644 --- a/src/raw_iter.rs +++ b/src/raw_iter.rs @@ -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, From d13ef309b529b22739a213b810767bb196feda6c Mon Sep 17 00:00:00 2001 From: Enyium <123484196+Enyium@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:52:51 +0200 Subject: [PATCH 3/4] test: add test `bytes_per_row` --- tests/embedded_graphics.rs | 30 +++++++++++++++++++- tests/stride-11px-1bpp.bmp | Bin 0 -> 66 bytes tests/stride-1px-32bpp.bmp | Bin 0 -> 58 bytes tests/stride-3px-24bpp.bmp | Bin 0 -> 66 bytes tests/stride-5px-16bpp-rgb565-bitfields.bmp | Bin 0 -> 82 bytes tests/stride-7px-8bpp.bmp | Bin 0 -> 1086 bytes tests/stride-9px-4bpp.bmp | Bin 0 -> 126 bytes 7 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/stride-11px-1bpp.bmp create mode 100644 tests/stride-1px-32bpp.bmp create mode 100644 tests/stride-3px-24bpp.bmp create mode 100644 tests/stride-5px-16bpp-rgb565-bitfields.bmp create mode 100644 tests/stride-7px-8bpp.bmp create mode 100644 tests/stride-9px-4bpp.bmp diff --git a/tests/embedded_graphics.rs b/tests/embedded_graphics.rs index 21d7b0a..3ff63d4 100644 --- a/tests/embedded_graphics.rs +++ b/tests/embedded_graphics.rs @@ -5,7 +5,7 @@ use embedded_graphics::{ prelude::*, primitives::Rectangle, }; -use tinybmp::{Bmp, RowOrder}; +use tinybmp::{Bmp, RawBmp, RowOrder}; #[test] fn negative_top_left() { @@ -31,6 +31,34 @@ fn dimensions() { ); } +#[test] +fn bytes_per_row() { + const fn stride(width: usize, bpp: usize) -> usize { + // Formula from . + ((width * bpp + 31) & !31) >> 3 + } + + #[rustfmt::skip] + const TESTS: &[(&[u8], u32, Option)] = &[ + (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() -> MockDisplay where C: PixelColor + ColorMapping, diff --git a/tests/stride-11px-1bpp.bmp b/tests/stride-11px-1bpp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..e3d9bf6d2fce0a0f2839c2d904a4f16d495c6e8f GIT binary patch literal 66 pcmZ?rbz*=3J0PV2#N0s42*w~10uJ*rFn};J`2YVu!>R?}4FQvP2v-0A literal 0 HcmV?d00001 diff --git a/tests/stride-1px-32bpp.bmp b/tests/stride-1px-32bpp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f788bc0e7510f869a4533aad35d5863cd98ba44f GIT binary patch literal 58 icmZ?rwPJt(Ga#h_#EfvP0AxYHVLk>15Qc&OKmY(~Z3gK8 literal 0 HcmV?d00001 diff --git a/tests/stride-3px-24bpp.bmp b/tests/stride-3px-24bpp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..3737863a474fff3ff780b9cc3f2b5f10d1895db7 GIT binary patch literal 66 ocmZ?rbz*=3Ga#h_#LPg<2*wgX5&{nMF))BI4E+E9AIOA|0H@yw=>Px# literal 0 HcmV?d00001 diff --git a/tests/stride-5px-16bpp-rgb565-bitfields.bmp b/tests/stride-5px-16bpp-rgb565-bitfields.bmp new file mode 100644 index 0000000000000000000000000000000000000000..73d9d96d3da11d7b0f6cb6c8215941110d371be5 GIT binary patch literal 82 zcmZ?r4Pt-*Hy~vJ#H>Kf2*v^o%s`q4h>!3=FpvoWKNuJuurn~oL%IL|Gcdp~02S5? AA^-pY literal 0 HcmV?d00001 diff --git a/tests/stride-7px-8bpp.bmp b/tests/stride-7px-8bpp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..5288fabab7a5dfa2a939734587d4a9e89267878c GIT binary patch literal 1086 zcmX|#2RIN40EB-b6iSg$G|?U;4Ki9PTBNBVLdYmBD^zHg5)Dd4($G?oLS+WM z6-8!V?|tv?`|s{MJwt6F{z{{}RlAo+Hv)eV{rmqDNs|Ba|Is}J1O({OqX&Y5f(Qu- zAuKFR&z?OI5fMRDR1`5WF?#jtMep9d5f>LnLP7#bNlE(j=|kVXeUXxqqF=v$^zYvv zX=!O>WMmjHU;whRvJ4zJ5IH$HBR$zCN>O&t}e?In13qmwEH%F@OGi3=9lduwVfT7cOMcqD2@Q8e(K*#Nx$^ zS+ZmaOP4NX*|KFUU%nh;V`EHAOjxmE1*WE^tX#R0RjXF9di847tXad_wQE_oZXIT3 zW~^Voo(&r|uyNx?Hf`F3xw$!;H*dzm!U9W6OSWv;f|ZpO*4Eb8*x0aj>sD-SZLzbn z!`|MWZQHit;NXCxqa#jEPB=R|vwiz^cI?=Ji;D|8ckX1@u3hZjy_-FI_ON&FUR+&W zadUIS-Q67z4-Y&&J=wQ!A6{Nwczb)}mwz7e7Bg{Qdnobm$O= z4aZa2#!O4>+Id$q30RaJ=K7E=qXU-597|7YPX9)@lA~-mh zbLY-+{``3^T)4o+ix&wA2_ZBzluMT`aryFP!otG1a^(tFuU_TawQGcjhZ7MI!S(Cc ziHwZo#*G_9MMZJ*=1p$hy2b6=x4CoY4$;xk+`W63d-v{f|Nea*Jb1vvhYyK~iQ&11SN@b>Ln-o1NAW@aW?Sy^OfXOokYLvC&^d3kx{ z=jT&UP{8~5?t7<>gdV zR8U!2NmW%9)z#J1)YMR0TT5MC9bdnGrM|wNhK2^def!4u@89|H;|Gn6jWjhi@$=_T pe*OAIb8|B-EiL^1{hQX-R@&OyXm4+)qoae)&Q7|zy7 Date: Tue, 28 Apr 2026 11:53:49 +0200 Subject: [PATCH 4/4] docs: added changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08f8324..a630eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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