pub(crate) mod decode;
mod encode;
mod utils;
use crate::decode::DecodeOutput;
use crate::error::BitmapError;
use crate::limits::Limits;
use crate::pixel::PixelLayout;
use alloc::vec::Vec;
pub use decode::BmpPermissiveness;
use enough::Stop;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct BmpMetadata {
pub width: u32,
pub height: u32,
pub layout: PixelLayout,
pub dpi_x: Option<f32>,
pub dpi_y: Option<f32>,
pub color_table: Option<Vec<[u8; 4]>>,
}
pub(crate) fn decode<'a>(
data: &'a [u8],
limits: Option<&Limits>,
stop: &dyn Stop,
) -> Result<DecodeOutput<'a>, BitmapError> {
decode_with_permissiveness(data, limits, BmpPermissiveness::Standard, stop)
}
pub(crate) fn decode_with_permissiveness<'a>(
data: &'a [u8],
limits: Option<&Limits>,
permissiveness: BmpPermissiveness,
stop: &dyn Stop,
) -> Result<DecodeOutput<'a>, BitmapError> {
let header = decode::parse_bmp_header(data)?;
check_limits(limits, header.width, header.height, &header.layout)?;
stop.check()?;
let (pixels, layout) = decode::decode_bmp_pixels(data, permissiveness, stop)?;
Ok(DecodeOutput::owned(
pixels,
header.width,
header.height,
layout,
))
}
pub(crate) fn decode_native<'a>(
data: &'a [u8],
limits: Option<&Limits>,
stop: &dyn Stop,
) -> Result<DecodeOutput<'a>, BitmapError> {
let header = decode::parse_bmp_header(data)?;
check_limits(limits, header.width, header.height, &header.layout)?;
stop.check()?;
let (pixels, native_layout) =
decode::decode_bmp_pixels_native(data, BmpPermissiveness::Standard, stop)?;
Ok(DecodeOutput::owned(
pixels,
header.width,
header.height,
native_layout,
))
}
fn check_limits(
limits: Option<&Limits>,
width: u32,
height: u32,
layout: &PixelLayout,
) -> Result<(), BitmapError> {
if let Some(limits) = limits {
limits.check(width, height)?;
}
let out_bytes = (width as usize)
.checked_mul(height as usize)
.and_then(|px| px.checked_mul(layout.bytes_per_pixel()))
.ok_or_else(|| BitmapError::LimitExceeded("output size overflows usize".into()))?;
crate::limits::check_output_size(out_bytes, limits)?;
Ok(())
}
pub(crate) fn probe(data: &[u8]) -> Result<BmpMetadata, BitmapError> {
let header = decode::parse_bmp_header(data)?;
fn pels_to_dpi(pels: u32) -> Option<f32> {
if pels == 0 {
None
} else {
Some(pels as f32 * 0.0254)
}
}
Ok(BmpMetadata {
width: header.width,
height: header.height,
layout: header.layout,
dpi_x: pels_to_dpi(header.x_pels_per_meter),
dpi_y: pels_to_dpi(header.y_pels_per_meter),
color_table: header.color_table,
})
}
pub(crate) fn encode(
pixels: &[u8],
width: u32,
height: u32,
layout: PixelLayout,
alpha: bool,
stop: &dyn Stop,
) -> Result<Vec<u8>, BitmapError> {
encode::encode_bmp(pixels, width, height, layout, alpha, stop)
}
#[cfg(test)]
mod tests {
use super::*;
use enough::Unstoppable;
fn make_bmp_with_resolution(x_pels: u32, y_pels: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"BM");
buf.extend_from_slice(&58u32.to_le_bytes());
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&54u32.to_le_bytes());
buf.extend_from_slice(&40u32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&24u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&4u32.to_le_bytes()); buf.extend_from_slice(&x_pels.to_le_bytes()); buf.extend_from_slice(&y_pels.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&[0xFF, 0x00, 0x00, 0x00]);
buf
}
fn make_indexed_bmp() -> Vec<u8> {
let mut buf = Vec::new();
let palette_bytes = 4 * 4; let row_stride = 4; let pixel_data_size = row_stride; let data_offset = 14 + 40 + palette_bytes;
let file_size = data_offset + pixel_data_size;
buf.extend_from_slice(b"BM");
buf.extend_from_slice(&(file_size as u32).to_le_bytes());
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&(data_offset as u32).to_le_bytes());
buf.extend_from_slice(&40u32.to_le_bytes()); buf.extend_from_slice(&2i32.to_le_bytes()); buf.extend_from_slice(&1i32.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&8u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(pixel_data_size as u32).to_le_bytes());
buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&4u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&[0xFF, 0x00, 0x00, 0x00]); buf.extend_from_slice(&[0x00, 0xFF, 0x00, 0x00]); buf.extend_from_slice(&[0x00, 0x00, 0xFF, 0x00]); buf.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x00]);
buf.extend_from_slice(&[0x00, 0x02, 0x00, 0x00]);
buf
}
#[test]
fn probe_dpi_72() {
let bmp = make_bmp_with_resolution(2835, 2835);
let meta = probe(&bmp).unwrap();
let dpi_x = meta.dpi_x.unwrap();
let dpi_y = meta.dpi_y.unwrap();
assert!((dpi_x - 72.0).abs() < 0.1, "expected ~72 DPI, got {dpi_x}");
assert!((dpi_y - 72.0).abs() < 0.1, "expected ~72 DPI, got {dpi_y}");
assert_eq!(meta.width, 1);
assert_eq!(meta.height, 1);
assert!(meta.color_table.is_none()); }
#[test]
fn probe_dpi_300() {
let bmp = make_bmp_with_resolution(11811, 11811);
let meta = probe(&bmp).unwrap();
let dpi_x = meta.dpi_x.unwrap();
let dpi_y = meta.dpi_y.unwrap();
assert!(
(dpi_x - 300.0).abs() < 0.1,
"expected ~300 DPI, got {dpi_x}"
);
assert!(
(dpi_y - 300.0).abs() < 0.1,
"expected ~300 DPI, got {dpi_y}"
);
}
#[test]
fn probe_dpi_zero() {
let bmp = make_bmp_with_resolution(0, 0);
let meta = probe(&bmp).unwrap();
assert_eq!(meta.dpi_x, None);
assert_eq!(meta.dpi_y, None);
}
#[test]
fn probe_dpi_asymmetric() {
let bmp = make_bmp_with_resolution(3937, 7874);
let meta = probe(&bmp).unwrap();
let dpi_x = meta.dpi_x.unwrap();
let dpi_y = meta.dpi_y.unwrap();
assert!(
(dpi_x - 100.0).abs() < 0.1,
"expected ~100 DPI, got {dpi_x}"
);
assert!(
(dpi_y - 200.0).abs() < 0.1,
"expected ~200 DPI, got {dpi_y}"
);
}
#[test]
fn probe_color_table() {
let bmp = make_indexed_bmp();
let meta = probe(&bmp).unwrap();
let table = meta.color_table.as_ref().expect("expected color table");
assert_eq!(table.len(), 4);
assert_eq!(table[0], [0xFF, 0x00, 0x00, 0xFF]); assert_eq!(table[1], [0x00, 0xFF, 0x00, 0xFF]); assert_eq!(table[2], [0x00, 0x00, 0xFF, 0xFF]); assert_eq!(table[3], [0xFF, 0xFF, 0xFF, 0xFF]); }
#[test]
fn probe_no_color_table_for_24bit() {
let bmp = make_bmp_with_resolution(2835, 2835);
let meta = probe(&bmp).unwrap();
assert!(meta.color_table.is_none());
}
#[test]
fn roundtrip_encode_preserves_dpi() {
let pixels = [0xFF, 0x00, 0x00]; let encoded = encode(&pixels, 1, 1, PixelLayout::Rgb8, false, &Unstoppable).unwrap();
let meta = probe(&encoded).unwrap();
let dpi_x = meta.dpi_x.unwrap();
assert!(
(dpi_x - 72.0).abs() < 0.1,
"expected ~72 DPI in encoder output, got {dpi_x}"
);
}
}