use fovea::image::{Image, ImageView, PlainImage};
use fovea::pixel::{Indexed8, PlainPixel, Srgb8, Srgba8};
use crate::IoError;
pub enum BmpImage {
Indexed8 {
data: Image<Indexed8>,
palette: Box<[Srgba8; 256]>,
},
Srgb8(Image<Srgb8>),
Srgba8(Image<Srgba8>),
}
impl BmpImage {
#[must_use]
pub fn width(&self) -> usize {
use fovea::image::ImageView;
match self {
BmpImage::Indexed8 { data, .. } => data.width(),
BmpImage::Srgb8(img) => img.width(),
BmpImage::Srgba8(img) => img.width(),
}
}
#[must_use]
pub fn height(&self) -> usize {
use fovea::image::ImageView;
match self {
BmpImage::Indexed8 { data, .. } => data.height(),
BmpImage::Srgb8(img) => img.height(),
BmpImage::Srgba8(img) => img.height(),
}
}
#[must_use]
pub fn size(&self) -> fovea::Size {
use fovea::image::ImageView;
match self {
BmpImage::Indexed8 { data, .. } => data.size(),
BmpImage::Srgb8(img) => img.size(),
BmpImage::Srgba8(img) => img.size(),
}
}
}
impl std::fmt::Debug for BmpImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BmpImage::Indexed8 { data, .. } => {
write!(f, "Indexed8({}x{})", data.width(), data.height())
}
BmpImage::Srgb8(img) => write!(f, "Srgb8({}x{})", img.width(), img.height()),
BmpImage::Srgba8(img) => write!(f, "Srgba8({}x{})", img.width(), img.height()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BmpBitDepth {
One,
Four,
Eight,
Sixteen,
TwentyFour,
ThirtyTwo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BmpResolution {
pub x_pixels_per_meter: u32,
pub y_pixels_per_meter: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BmpColorSpace {
Srgb,
IccTagged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BmpCompression {
None,
Rle8,
Rle4,
Bitfields,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BmpHeaderVersion {
Core,
Info,
V4,
V5,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct BmpMetadata {
pub color_space: BmpColorSpace,
pub source_bit_depth: BmpBitDepth,
pub resolution: Option<BmpResolution>,
pub compression: BmpCompression,
pub header_version: BmpHeaderVersion,
pub icc_profile: Option<Box<[u8]>>,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct BmpDecoded {
pub image: BmpImage,
pub metadata: BmpMetadata,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default)]
pub struct BmpEncodeOptions {
pub resolution: Option<BmpResolution>,
}
mod bmp_pixel_sealed {
pub trait Sealed {}
}
pub trait BmpPixel: bmp_pixel_sealed::Sealed + PlainPixel {
const BMP_BYTES_PER_PIXEL: u32;
const BMP_BIT_COUNT: u16;
const BMP_HAS_ALPHA: bool;
#[doc(hidden)]
fn write_bmp_bytes(&self, out: &mut [u8]);
}
impl bmp_pixel_sealed::Sealed for Srgb8 {}
impl BmpPixel for Srgb8 {
const BMP_BYTES_PER_PIXEL: u32 = 3;
const BMP_BIT_COUNT: u16 = 24;
const BMP_HAS_ALPHA: bool = false;
#[inline]
fn write_bmp_bytes(&self, out: &mut [u8]) {
out[0] = self.b.0;
out[1] = self.g.0;
out[2] = self.r.0;
}
}
impl bmp_pixel_sealed::Sealed for Srgba8 {}
impl BmpPixel for Srgba8 {
const BMP_BYTES_PER_PIXEL: u32 = 4;
const BMP_BIT_COUNT: u16 = 32;
const BMP_HAS_ALPHA: bool = true;
#[inline]
fn write_bmp_bytes(&self, out: &mut [u8]) {
out[0] = self.b.0;
out[1] = self.g.0;
out[2] = self.r.0;
out[3] = self.a.0;
}
}
struct BmpFileHeader {
pixel_data_offset: u32,
}
struct BmpDibHeader {
width: u32,
height: u32,
top_down: bool,
bit_count: u16,
compression: u32,
header_version: BmpHeaderVersion,
x_pels_per_meter: u32,
y_pels_per_meter: u32,
colors_used: u32,
cs_type: u32,
header_size: u32,
v4_masks: Option<BitfieldMasks>,
icc_profile: Option<(u32, u32)>,
color_table_entry_size: u8,
}
#[derive(Debug, Clone, Copy)]
struct BitfieldMasks {
r: u32,
g: u32,
b: u32,
a: u32,
}
#[derive(Debug, Clone, Copy)]
struct MaskInfo {
shift: u8,
bits: u8,
}
#[inline]
fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
let bytes = data.get(offset..offset + 2)?;
Some(u16::from_le_bytes([bytes[0], bytes[1]]))
}
#[inline]
fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
let bytes = data.get(offset..offset + 4)?;
Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[inline]
fn read_i32_le(data: &[u8], offset: usize) -> Option<i32> {
let bytes = data.get(offset..offset + 4)?;
Some(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[inline]
fn write_u16_le(buf: &mut [u8], offset: usize, val: u16) {
buf[offset..offset + 2].copy_from_slice(&val.to_le_bytes());
}
#[inline]
fn write_u32_le(buf: &mut [u8], offset: usize, val: u32) {
buf[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
#[inline]
fn write_i32_le(buf: &mut [u8], offset: usize, val: i32) {
buf[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
fn parse_file_header(data: &[u8]) -> Result<BmpFileHeader, IoError> {
if data.len() < 14 {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for file header (need at least 14 bytes)",
});
}
if data[0] != 0x42 || data[1] != 0x4D {
return Err(IoError::InvalidFormat {
reason: "not a BMP file (missing 'BM' signature)",
});
}
let pixel_data_offset = read_u32_le(data, 10).unwrap();
Ok(BmpFileHeader { pixel_data_offset })
}
fn parse_dib_header(data: &[u8]) -> Result<BmpDibHeader, IoError> {
let dib_start = 14usize;
let header_size = read_u32_le(data, dib_start).ok_or(IoError::InvalidFormat {
reason: "BMP file too short for DIB header size field",
})?;
match header_size {
12 => parse_core_header(data, dib_start),
40 => parse_info_header(data, dib_start),
108 => parse_v4_header(data, dib_start),
124 => parse_v5_header(data, dib_start),
_ => Err(IoError::UnsupportedFeature {
reason: "unsupported DIB header size (only 12, 40, 108, 124 are supported)",
}),
}
}
fn parse_core_header(data: &[u8], offset: usize) -> Result<BmpDibHeader, IoError> {
if data.len() < offset + 12 {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for BITMAPCOREHEADER",
});
}
let width = read_u16_le(data, offset + 4).unwrap() as u32;
let height = read_u16_le(data, offset + 6).unwrap() as u32;
let planes = read_u16_le(data, offset + 8).unwrap();
let bit_count = read_u16_le(data, offset + 10).unwrap();
if planes != 1 {
return Err(IoError::InvalidFormat {
reason: "BMP planes field must be 1",
});
}
validate_bit_count_core(bit_count)?;
if width == 0 || height == 0 {
return Err(IoError::InvalidFormat {
reason: "BMP image dimensions must be non-zero",
});
}
Ok(BmpDibHeader {
width,
height,
top_down: false,
bit_count,
compression: 0, header_version: BmpHeaderVersion::Core,
x_pels_per_meter: 0,
y_pels_per_meter: 0,
colors_used: 0,
cs_type: 0,
header_size: 12,
v4_masks: None,
icc_profile: None,
color_table_entry_size: 3,
})
}
fn parse_info_header(data: &[u8], offset: usize) -> Result<BmpDibHeader, IoError> {
if data.len() < offset + 40 {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for BITMAPINFOHEADER",
});
}
let raw_width = read_i32_le(data, offset + 4).unwrap();
let raw_height = read_i32_le(data, offset + 8).unwrap();
let planes = read_u16_le(data, offset + 12).unwrap();
let bit_count = read_u16_le(data, offset + 14).unwrap();
let compression = read_u32_le(data, offset + 16).unwrap();
let x_pels = read_u32_le(data, offset + 24).unwrap();
let y_pels = read_u32_le(data, offset + 28).unwrap();
let colors_used = read_u32_le(data, offset + 32).unwrap();
if planes != 1 {
return Err(IoError::InvalidFormat {
reason: "BMP planes field must be 1",
});
}
validate_bit_count_info(bit_count)?;
let (width, height, top_down) = resolve_dimensions(raw_width, raw_height)?;
Ok(BmpDibHeader {
width,
height,
top_down,
bit_count,
compression,
header_version: BmpHeaderVersion::Info,
x_pels_per_meter: x_pels,
y_pels_per_meter: y_pels,
colors_used,
cs_type: 0,
header_size: 40,
v4_masks: None,
icc_profile: None,
color_table_entry_size: 4,
})
}
fn parse_v4_header(data: &[u8], offset: usize) -> Result<BmpDibHeader, IoError> {
if data.len() < offset + 108 {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for BITMAPV4HEADER",
});
}
let raw_width = read_i32_le(data, offset + 4).unwrap();
let raw_height = read_i32_le(data, offset + 8).unwrap();
let planes = read_u16_le(data, offset + 12).unwrap();
let bit_count = read_u16_le(data, offset + 14).unwrap();
let compression = read_u32_le(data, offset + 16).unwrap();
let x_pels = read_u32_le(data, offset + 24).unwrap();
let y_pels = read_u32_le(data, offset + 28).unwrap();
let colors_used = read_u32_le(data, offset + 32).unwrap();
let r_mask = read_u32_le(data, offset + 40).unwrap();
let g_mask = read_u32_le(data, offset + 44).unwrap();
let b_mask = read_u32_le(data, offset + 48).unwrap();
let a_mask = read_u32_le(data, offset + 52).unwrap();
let cs_type = read_u32_le(data, offset + 56).unwrap();
if planes != 1 {
return Err(IoError::InvalidFormat {
reason: "BMP planes field must be 1",
});
}
validate_bit_count_info(bit_count)?;
let (width, height, top_down) = resolve_dimensions(raw_width, raw_height)?;
Ok(BmpDibHeader {
width,
height,
top_down,
bit_count,
compression,
header_version: BmpHeaderVersion::V4,
x_pels_per_meter: x_pels,
y_pels_per_meter: y_pels,
colors_used,
cs_type,
header_size: 108,
v4_masks: Some(BitfieldMasks {
r: r_mask,
g: g_mask,
b: b_mask,
a: a_mask,
}),
icc_profile: None,
color_table_entry_size: 4,
})
}
fn parse_v5_header(data: &[u8], offset: usize) -> Result<BmpDibHeader, IoError> {
if data.len() < offset + 124 {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for BITMAPV5HEADER",
});
}
let raw_width = read_i32_le(data, offset + 4).unwrap();
let raw_height = read_i32_le(data, offset + 8).unwrap();
let planes = read_u16_le(data, offset + 12).unwrap();
let bit_count = read_u16_le(data, offset + 14).unwrap();
let compression = read_u32_le(data, offset + 16).unwrap();
let x_pels = read_u32_le(data, offset + 24).unwrap();
let y_pels = read_u32_le(data, offset + 28).unwrap();
let colors_used = read_u32_le(data, offset + 32).unwrap();
let r_mask = read_u32_le(data, offset + 40).unwrap();
let g_mask = read_u32_le(data, offset + 44).unwrap();
let b_mask = read_u32_le(data, offset + 48).unwrap();
let a_mask = read_u32_le(data, offset + 52).unwrap();
let cs_type = read_u32_le(data, offset + 56).unwrap();
let profile_data = read_u32_le(data, offset + 112).unwrap();
let profile_size = read_u32_le(data, offset + 116).unwrap();
if planes != 1 {
return Err(IoError::InvalidFormat {
reason: "BMP planes field must be 1",
});
}
validate_bit_count_info(bit_count)?;
let (width, height, top_down) = resolve_dimensions(raw_width, raw_height)?;
let icc = if cs_type == LCS_PROFILE_EMBEDDED && profile_size > 0 {
Some((profile_data, profile_size))
} else {
None
};
Ok(BmpDibHeader {
width,
height,
top_down,
bit_count,
compression,
header_version: BmpHeaderVersion::V5,
x_pels_per_meter: x_pels,
y_pels_per_meter: y_pels,
colors_used,
cs_type,
header_size: 124,
v4_masks: Some(BitfieldMasks {
r: r_mask,
g: g_mask,
b: b_mask,
a: a_mask,
}),
icc_profile: icc,
color_table_entry_size: 4,
})
}
fn validate_bit_count_core(bit_count: u16) -> Result<(), IoError> {
match bit_count {
1 | 4 | 8 | 24 => Ok(()),
_ => Err(IoError::InvalidFormat {
reason: "invalid bit count for BITMAPCOREHEADER (must be 1, 4, 8, or 24)",
}),
}
}
fn validate_bit_count_info(bit_count: u16) -> Result<(), IoError> {
match bit_count {
1 | 4 | 8 | 16 | 24 | 32 => Ok(()),
_ => Err(IoError::InvalidFormat {
reason: "invalid bit count (must be 1, 4, 8, 16, 24, or 32)",
}),
}
}
fn resolve_dimensions(raw_width: i32, raw_height: i32) -> Result<(u32, u32, bool), IoError> {
if raw_width <= 0 {
return Err(IoError::InvalidFormat {
reason: "BMP width must be positive",
});
}
if raw_height == 0 {
return Err(IoError::InvalidFormat {
reason: "BMP height must be non-zero",
});
}
let width = raw_width as u32;
let (height, top_down) = if raw_height < 0 {
let abs = (raw_height as i64).unsigned_abs() as u32;
(abs, true)
} else {
(raw_height as u32, false)
};
Ok((width, height, top_down))
}
const LCS_SRGB: u32 = 0x73524742;
const LCS_WINDOWS_COLOR_SPACE: u32 = 0x57696E20;
const LCS_PROFILE_EMBEDDED: u32 = 0x4D424544;
const BI_RGB: u32 = 0;
const BI_RLE8: u32 = 1;
const BI_RLE4: u32 = 2;
const BI_BITFIELDS: u32 = 3;
impl BitfieldMasks {
fn default_16bit() -> Self {
BitfieldMasks {
r: 0x7C00,
g: 0x03E0,
b: 0x001F,
a: 0,
}
}
fn default_32bit() -> Self {
BitfieldMasks {
r: 0x00FF0000,
g: 0x0000FF00,
b: 0x000000FF,
a: 0x00000000,
}
}
}
fn mask_info(mask: u32) -> MaskInfo {
if mask == 0 {
return MaskInfo { shift: 0, bits: 0 };
}
let shift = mask.trailing_zeros() as u8;
let bits = (mask >> shift).trailing_ones() as u8;
MaskInfo { shift, bits }
}
#[inline]
fn extract_channel(value: u32, info: MaskInfo) -> u8 {
if info.bits == 0 {
return 0;
}
let raw = (value >> info.shift) & ((1u32 << info.bits) - 1);
if info.bits >= 8 {
(raw >> (info.bits - 8)) as u8
} else {
let max_val = (1u32 << info.bits) - 1;
((raw * 255 + max_val / 2) / max_val) as u8
}
}
fn parse_bitfield_masks(data: &[u8], offset: usize) -> Result<BitfieldMasks, IoError> {
if data.len() < offset + 12 {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for bitfield masks",
});
}
let r = read_u32_le(data, offset).unwrap();
let g = read_u32_le(data, offset + 4).unwrap();
let b = read_u32_le(data, offset + 8).unwrap();
let a = if data.len() >= offset + 16 {
read_u32_le(data, offset + 12).unwrap()
} else {
0
};
Ok(BitfieldMasks { r, g, b, a })
}
fn parse_color_table(
data: &[u8],
offset: usize,
entry_size: u8,
count: usize,
) -> Result<Box<[Srgba8; 256]>, IoError> {
let mut palette = Box::new([Srgba8::new(0, 0, 0, 255); 256]);
for i in 0..count.min(256) {
let entry_offset = offset + i * entry_size as usize;
if data.len() < entry_offset + entry_size as usize {
return Err(IoError::InvalidFormat {
reason: "BMP file too short for color table",
});
}
let b = data[entry_offset];
let g = data[entry_offset + 1];
let r = data[entry_offset + 2];
palette[i] = Srgba8::new(r, g, b, 255);
}
Ok(palette)
}
fn decompress_rle8(data: &[u8], width: usize, height: usize) -> Result<Vec<u8>, IoError> {
let mut output = vec![0u8; width * height];
let mut x = 0usize;
let mut y = 0usize;
let mut pos = 0usize;
while pos < data.len() {
let first = data[pos];
pos += 1;
if pos >= data.len() {
break;
}
let second = data[pos];
pos += 1;
if first > 0 {
for _ in 0..first as usize {
if y >= height {
return Err(decode_failed("RLE8 data exceeds image bounds"));
}
if x < width {
output[y * width + x] = second;
}
x += 1;
}
} else {
match second {
0 => {
x = 0;
y += 1;
}
1 => {
break;
}
2 => {
if pos + 1 >= data.len() {
return Err(decode_failed("RLE8 delta truncated"));
}
let dx = data[pos] as usize;
let dy = data[pos + 1] as usize;
pos += 2;
x += dx;
y += dy;
}
n => {
let count = n as usize;
for _ in 0..count {
if pos >= data.len() {
return Err(decode_failed("RLE8 absolute mode truncated"));
}
if y >= height {
return Err(decode_failed("RLE8 data exceeds image bounds"));
}
if x < width {
output[y * width + x] = data[pos];
}
x += 1;
pos += 1;
}
if count & 1 != 0 {
pos += 1;
}
}
}
}
}
Ok(output)
}
fn decompress_rle4(data: &[u8], width: usize, height: usize) -> Result<Vec<u8>, IoError> {
let mut output = vec![0u8; width * height];
let mut x = 0usize;
let mut y = 0usize;
let mut pos = 0usize;
while pos < data.len() {
let first = data[pos];
pos += 1;
if pos >= data.len() {
break;
}
let second = data[pos];
pos += 1;
if first > 0 {
let hi = second >> 4;
let lo = second & 0x0F;
for i in 0..first as usize {
if y >= height {
return Err(decode_failed("RLE4 data exceeds image bounds"));
}
let val = if i & 1 == 0 { hi } else { lo };
if x < width {
output[y * width + x] = val;
}
x += 1;
}
} else {
match second {
0 => {
x = 0;
y += 1;
}
1 => {
break;
}
2 => {
if pos + 1 >= data.len() {
return Err(decode_failed("RLE4 delta truncated"));
}
let dx = data[pos] as usize;
let dy = data[pos + 1] as usize;
pos += 2;
x += dx;
y += dy;
}
n => {
let count = n as usize;
let byte_count = count.div_ceil(2);
for i in 0..count {
let byte_idx = i / 2;
if pos + byte_idx >= data.len() {
return Err(decode_failed("RLE4 absolute mode truncated"));
}
let byte = data[pos + byte_idx];
let val = if i & 1 == 0 { byte >> 4 } else { byte & 0x0F };
if y >= height {
return Err(decode_failed("RLE4 data exceeds image bounds"));
}
if x < width {
output[y * width + x] = val;
}
x += 1;
}
pos += byte_count;
if byte_count & 1 != 0 {
pos += 1;
}
}
}
}
}
Ok(output)
}
fn row_stride(width: u32, bits_per_pixel: u32) -> usize {
let bits = width as usize * bits_per_pixel as usize;
bits.div_ceil(32) * 4
}
pub fn decode(data: &[u8]) -> Result<BmpDecoded, IoError> {
let file_header = parse_file_header(data)?;
let dib = parse_dib_header(data)?;
match dib.compression {
BI_RGB | BI_RLE8 | BI_RLE4 | BI_BITFIELDS => {}
4 => {
return Err(IoError::UnsupportedFeature {
reason: "BI_JPEG compression is not supported",
});
}
5 => {
return Err(IoError::UnsupportedFeature {
reason: "BI_PNG compression is not supported",
});
}
_ => {
return Err(IoError::UnsupportedFeature {
reason: "unknown BMP compression type",
});
}
}
if dib.compression == BI_RLE8 && dib.bit_count != 8 {
return Err(IoError::InvalidFormat {
reason: "BI_RLE8 compression requires 8-bit indexed",
});
}
if dib.compression == BI_RLE4 && dib.bit_count != 4 {
return Err(IoError::InvalidFormat {
reason: "BI_RLE4 compression requires 4-bit indexed",
});
}
let width = dib.width as usize;
let height = dib.height as usize;
let pixel_offset = file_header.pixel_data_offset as usize;
let image = match dib.bit_count {
1 => decode_indexed(&dib, data, pixel_offset, width, height, 1)?,
4 => {
if dib.compression == BI_RLE4 {
decode_indexed_rle4(&dib, data, pixel_offset, width, height)?
} else {
decode_indexed(&dib, data, pixel_offset, width, height, 4)?
}
}
8 => {
if dib.compression == BI_RLE8 {
decode_indexed_rle8(&dib, data, pixel_offset, width, height)?
} else {
decode_indexed(&dib, data, pixel_offset, width, height, 8)?
}
}
16 => decode_16bit(&dib, data, pixel_offset, width, height)?,
24 => decode_24bit(&dib, data, pixel_offset, width, height)?,
32 => decode_32bit(&dib, data, pixel_offset, width, height)?,
_ => {
return Err(IoError::InvalidFormat {
reason: "unsupported BMP bit count",
});
}
};
let metadata = build_metadata(&dib, data);
Ok(BmpDecoded { image, metadata })
}
pub fn decode_reader(mut reader: impl std::io::Read) -> Result<BmpDecoded, IoError> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
decode(&buf)
}
fn decode_indexed(
dib: &BmpDibHeader,
data: &[u8],
pixel_offset: usize,
width: usize,
height: usize,
bits: u32,
) -> Result<BmpImage, IoError> {
let color_table_offset = 14 + dib.header_size as usize;
let color_count = if dib.colors_used > 0 {
dib.colors_used as usize
} else {
1usize << bits
};
let palette = parse_color_table(
data,
color_table_offset,
dib.color_table_entry_size,
color_count,
)?;
let stride = row_stride(dib.width, bits);
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let row_start = pixel_offset + src_row * stride;
match bits {
1 => {
for col in 0..width {
let byte_idx = row_start + col / 8;
if byte_idx >= data.len() {
return Err(decode_failed("BMP pixel data truncated"));
}
let bit_idx = 7 - (col % 8);
let index = (data[byte_idx] >> bit_idx) & 1;
pixels.push(Indexed8(index));
}
}
4 => {
for col in 0..width {
let byte_idx = row_start + col / 2;
if byte_idx >= data.len() {
return Err(decode_failed("BMP pixel data truncated"));
}
let index = if col % 2 == 0 {
data[byte_idx] >> 4
} else {
data[byte_idx] & 0x0F
};
pixels.push(Indexed8(index));
}
}
8 => {
if row_start + width > data.len() {
return Err(decode_failed("BMP pixel data truncated"));
}
for col in 0..width {
pixels.push(Indexed8(data[row_start + col]));
}
}
_ => unreachable!(),
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Indexed8 { data: img, palette })
}
fn decode_indexed_rle8(
dib: &BmpDibHeader,
data: &[u8],
pixel_offset: usize,
width: usize,
height: usize,
) -> Result<BmpImage, IoError> {
let color_table_offset = 14 + dib.header_size as usize;
let color_count = if dib.colors_used > 0 {
dib.colors_used as usize
} else {
256
};
let palette = parse_color_table(
data,
color_table_offset,
dib.color_table_entry_size,
color_count,
)?;
let rle_data = data.get(pixel_offset..).ok_or(IoError::InvalidFormat {
reason: "BMP pixel data offset beyond file",
})?;
let decompressed = decompress_rle8(rle_data, width, height)?;
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let start = src_row * width;
for col in 0..width {
pixels.push(Indexed8(decompressed[start + col]));
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Indexed8 { data: img, palette })
}
fn decode_indexed_rle4(
dib: &BmpDibHeader,
data: &[u8],
pixel_offset: usize,
width: usize,
height: usize,
) -> Result<BmpImage, IoError> {
let color_table_offset = 14 + dib.header_size as usize;
let color_count = if dib.colors_used > 0 {
dib.colors_used as usize
} else {
16
};
let palette = parse_color_table(
data,
color_table_offset,
dib.color_table_entry_size,
color_count,
)?;
let rle_data = data.get(pixel_offset..).ok_or(IoError::InvalidFormat {
reason: "BMP pixel data offset beyond file",
})?;
let decompressed = decompress_rle4(rle_data, width, height)?;
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let start = src_row * width;
for col in 0..width {
pixels.push(Indexed8(decompressed[start + col]));
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Indexed8 { data: img, palette })
}
fn decode_16bit(
dib: &BmpDibHeader,
data: &[u8],
pixel_offset: usize,
width: usize,
height: usize,
) -> Result<BmpImage, IoError> {
let masks = resolve_masks_with_data(dib, 16, data)?;
let r_info = mask_info(masks.r);
let g_info = mask_info(masks.g);
let b_info = mask_info(masks.b);
let stride = row_stride(dib.width, 16);
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let row_start = pixel_offset + src_row * stride;
for col in 0..width {
let pix_offset = row_start + col * 2;
let val = read_u16_le(data, pix_offset)
.ok_or_else(|| decode_failed("BMP 16-bit pixel data truncated"))?
as u32;
let r = extract_channel(val, r_info);
let g = extract_channel(val, g_info);
let b = extract_channel(val, b_info);
pixels.push(Srgb8::new(r, g, b));
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Srgb8(img))
}
fn decode_24bit(
dib: &BmpDibHeader,
data: &[u8],
pixel_offset: usize,
width: usize,
height: usize,
) -> Result<BmpImage, IoError> {
let stride = row_stride(dib.width, 24);
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let row_start = pixel_offset + src_row * stride;
for col in 0..width {
let pix_offset = row_start + col * 3;
if pix_offset + 3 > data.len() {
return Err(decode_failed("BMP 24-bit pixel data truncated"));
}
let b = data[pix_offset];
let g = data[pix_offset + 1];
let r = data[pix_offset + 2];
pixels.push(Srgb8::new(r, g, b));
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Srgb8(img))
}
fn decode_32bit(
dib: &BmpDibHeader,
data: &[u8],
pixel_offset: usize,
width: usize,
height: usize,
) -> Result<BmpImage, IoError> {
let masks = resolve_masks_with_data(dib, 32, data)?;
let has_alpha = masks.a != 0;
let r_info = mask_info(masks.r);
let g_info = mask_info(masks.g);
let b_info = mask_info(masks.b);
let a_info = mask_info(masks.a);
let stride = row_stride(dib.width, 32);
if has_alpha {
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let row_start = pixel_offset + src_row * stride;
for col in 0..width {
let pix_offset = row_start + col * 4;
let val = read_u32_le(data, pix_offset)
.ok_or_else(|| decode_failed("BMP 32-bit pixel data truncated"))?;
let r = extract_channel(val, r_info);
let g = extract_channel(val, g_info);
let b = extract_channel(val, b_info);
let a = extract_channel(val, a_info);
pixels.push(Srgba8::new(r, g, b, a));
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Srgba8(img))
} else {
let mut pixels = Vec::with_capacity(width * height);
for row in 0..height {
let src_row = if dib.top_down { row } else { height - 1 - row };
let row_start = pixel_offset + src_row * stride;
for col in 0..width {
let pix_offset = row_start + col * 4;
let val = read_u32_le(data, pix_offset)
.ok_or_else(|| decode_failed("BMP 32-bit pixel data truncated"))?;
let r = extract_channel(val, r_info);
let g = extract_channel(val, g_info);
let b = extract_channel(val, b_info);
pixels.push(Srgb8::new(r, g, b));
}
}
let img = Image::from_vec(width, height, pixels)
.map_err(|_| decode_failed("pixel count does not match image dimensions"))?;
Ok(BmpImage::Srgb8(img))
}
}
fn resolve_masks_with_data(
dib: &BmpDibHeader,
bit_count: u16,
data: &[u8],
) -> Result<BitfieldMasks, IoError> {
if dib.compression == BI_BITFIELDS {
if let Some(masks) = dib.v4_masks {
return Ok(masks);
}
let mask_offset = 14 + dib.header_size as usize;
return parse_bitfield_masks(data, mask_offset);
}
match bit_count {
16 => Ok(BitfieldMasks::default_16bit()),
32 => Ok(BitfieldMasks::default_32bit()),
_ => unreachable!("resolve_masks_with_data only called for 16-bit and 32-bit"),
}
}
fn build_metadata(dib: &BmpDibHeader, data: &[u8]) -> BmpMetadata {
let source_bit_depth = match dib.bit_count {
1 => BmpBitDepth::One,
4 => BmpBitDepth::Four,
8 => BmpBitDepth::Eight,
16 => BmpBitDepth::Sixteen,
24 => BmpBitDepth::TwentyFour,
32 => BmpBitDepth::ThirtyTwo,
_ => unreachable!("bit count validated earlier"),
};
let compression = match dib.compression {
BI_RGB => BmpCompression::None,
BI_RLE8 => BmpCompression::Rle8,
BI_RLE4 => BmpCompression::Rle4,
BI_BITFIELDS => BmpCompression::Bitfields,
_ => unreachable!("compression type validated earlier"),
};
let resolution = if dib.x_pels_per_meter > 0 || dib.y_pels_per_meter > 0 {
Some(BmpResolution {
x_pixels_per_meter: dib.x_pels_per_meter,
y_pixels_per_meter: dib.y_pels_per_meter,
})
} else {
None
};
let color_space = determine_color_space(dib);
let icc_profile = if let Some((profile_offset, profile_size)) = dib.icc_profile {
let start = 14 + profile_offset as usize;
let end = start + profile_size as usize;
if end <= data.len() {
Some(data[start..end].to_vec().into_boxed_slice())
} else {
None
}
} else {
None
};
BmpMetadata {
color_space,
source_bit_depth,
resolution,
compression,
header_version: dib.header_version,
icc_profile,
}
}
fn determine_color_space(dib: &BmpDibHeader) -> BmpColorSpace {
match dib.header_version {
BmpHeaderVersion::Core | BmpHeaderVersion::Info => BmpColorSpace::Srgb,
BmpHeaderVersion::V4 | BmpHeaderVersion::V5 => match dib.cs_type {
LCS_PROFILE_EMBEDDED => BmpColorSpace::IccTagged,
LCS_SRGB | LCS_WINDOWS_COLOR_SPACE => BmpColorSpace::Srgb,
_ => BmpColorSpace::Srgb,
},
}
}
fn decode_failed(msg: &'static str) -> IoError {
IoError::DecodeFailed { source: msg.into() }
}
pub fn encode<P: BmpPixel>(
image: &(impl ImageView<Pixel = P> + PlainImage),
options: &BmpEncodeOptions,
) -> Result<Vec<u8>, IoError> {
let mut buf = Vec::new();
encode_writer(image, &mut buf, options)?;
Ok(buf)
}
pub fn encode_writer<P: BmpPixel>(
image: &(impl ImageView<Pixel = P> + PlainImage),
writer: &mut impl std::io::Write,
options: &BmpEncodeOptions,
) -> Result<(), IoError> {
let width = image.width() as u32;
let height = image.height() as u32;
let bpp = P::BMP_BYTES_PER_PIXEL;
let stride = row_stride(width, P::BMP_BIT_COUNT as u32) as u32;
let pixel_data_size = stride * height;
let mask_size: u32 = if P::BMP_HAS_ALPHA { 16 } else { 0 };
let pixel_data_offset = 14 + 40 + mask_size;
let file_size = pixel_data_offset + pixel_data_size;
let mut file_header = [0u8; 14];
file_header[0] = 0x42; file_header[1] = 0x4D; write_u32_le(&mut file_header, 2, file_size);
write_u32_le(&mut file_header, 10, pixel_data_offset);
writer.write_all(&file_header).map_err(encode_error)?;
let mut info_header = [0u8; 40];
write_u32_le(&mut info_header, 0, 40); write_i32_le(&mut info_header, 4, width as i32); write_i32_le(&mut info_header, 8, height as i32); write_u16_le(&mut info_header, 12, 1); write_u16_le(&mut info_header, 14, P::BMP_BIT_COUNT);
let compression: u32 = if P::BMP_HAS_ALPHA {
BI_BITFIELDS
} else {
BI_RGB
};
write_u32_le(&mut info_header, 16, compression); write_u32_le(&mut info_header, 20, pixel_data_size);
let (x_res, y_res) = match &options.resolution {
Some(r) => (r.x_pixels_per_meter, r.y_pixels_per_meter),
None => (0, 0),
};
write_u32_le(&mut info_header, 24, x_res); write_u32_le(&mut info_header, 28, y_res); writer.write_all(&info_header).map_err(encode_error)?;
if P::BMP_HAS_ALPHA {
let mut masks = [0u8; 16];
write_u32_le(&mut masks, 0, 0x00FF0000); write_u32_le(&mut masks, 4, 0x0000FF00); write_u32_le(&mut masks, 8, 0x000000FF); write_u32_le(&mut masks, 12, 0xFF000000); writer.write_all(&masks).map_err(encode_error)?;
}
let pad_bytes = stride as usize - (width as usize * bpp as usize);
let padding = [0u8; 3];
let mut row_buf = vec![0u8; width as usize * bpp as usize];
for row in (0..height as usize).rev() {
for col in 0..width as usize {
let pixel = image.pixel_at(col, row);
let offset = col * bpp as usize;
pixel.write_bmp_bytes(&mut row_buf[offset..]);
}
writer.write_all(&row_buf).map_err(encode_error)?;
if pad_bytes > 0 {
writer
.write_all(&padding[..pad_bytes])
.map_err(encode_error)?;
}
}
Ok(())
}
pub fn encode_indexed(
image: &(impl ImageView<Pixel = Indexed8> + PlainImage),
palette: &[Srgba8],
options: &BmpEncodeOptions,
) -> Result<Vec<u8>, IoError> {
let mut buf = Vec::new();
encode_indexed_writer(image, palette, &mut buf, options)?;
Ok(buf)
}
fn encode_indexed_writer(
image: &(impl ImageView<Pixel = Indexed8> + PlainImage),
palette: &[Srgba8],
writer: &mut impl std::io::Write,
options: &BmpEncodeOptions,
) -> Result<(), IoError> {
if palette.is_empty() || palette.len() > 256 {
return Err(IoError::EncodeFailed {
source: "palette must have 1\u{2013}256 entries".into(),
});
}
let max_index = (palette.len() - 1) as u8;
for pixel in image.as_slice() {
if pixel.0 > max_index {
return Err(IoError::EncodeFailed {
source: "pixel index exceeds palette size".into(),
});
}
}
let width = image.width() as u32;
let height = image.height() as u32;
let stride = row_stride(width, 8) as u32;
let pixel_data_size = stride * height;
let color_table_size = (palette.len() as u32) * 4;
let pixel_data_offset = 14 + 40 + color_table_size;
let file_size = pixel_data_offset + pixel_data_size;
let mut file_header = [0u8; 14];
file_header[0] = 0x42;
file_header[1] = 0x4D;
write_u32_le(&mut file_header, 2, file_size);
write_u32_le(&mut file_header, 10, pixel_data_offset);
writer.write_all(&file_header).map_err(encode_error)?;
let mut info_header = [0u8; 40];
write_u32_le(&mut info_header, 0, 40);
write_i32_le(&mut info_header, 4, width as i32);
write_i32_le(&mut info_header, 8, height as i32);
write_u16_le(&mut info_header, 12, 1);
write_u16_le(&mut info_header, 14, 8); write_u32_le(&mut info_header, 16, BI_RGB);
write_u32_le(&mut info_header, 20, pixel_data_size);
let (x_res, y_res) = match &options.resolution {
Some(r) => (r.x_pixels_per_meter, r.y_pixels_per_meter),
None => (0, 0),
};
write_u32_le(&mut info_header, 24, x_res);
write_u32_le(&mut info_header, 28, y_res);
write_u32_le(&mut info_header, 32, palette.len() as u32); writer.write_all(&info_header).map_err(encode_error)?;
for entry in palette {
let ct_entry = [entry.b.0, entry.g.0, entry.r.0, 0];
writer.write_all(&ct_entry).map_err(encode_error)?;
}
let pad_bytes = stride as usize - width as usize;
let padding = [0u8; 3];
let mut row_data = vec![0u8; width as usize];
for row in (0..height as usize).rev() {
for (col, dst) in row_data.iter_mut().enumerate() {
*dst = image.pixel_at(col, row).0;
}
writer.write_all(&row_data).map_err(encode_error)?;
if pad_bytes > 0 {
writer
.write_all(&padding[..pad_bytes])
.map_err(encode_error)?;
}
}
Ok(())
}
pub fn encode_bmp_image(image: &BmpImage, options: &BmpEncodeOptions) -> Result<Vec<u8>, IoError> {
match image {
BmpImage::Indexed8 { data, palette } => encode_indexed(data, palette.as_ref(), options),
BmpImage::Srgb8(img) => encode(img, options),
BmpImage::Srgba8(img) => encode(img, options),
}
}
fn encode_error(e: std::io::Error) -> IoError {
IoError::EncodeFailed {
source: Box::new(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use fovea::image::ImageView;
#[test]
fn bmp_image_enum_is_compact() {
let size = std::mem::size_of::<BmpImage>();
assert!(size <= 48, "BmpImage is {} bytes, expected <= 48", size);
}
#[test]
fn bmp_decoded_field_access() {
let img = Image::fill(2, 2, Srgb8::new(10, 20, 30));
let decoded = BmpDecoded {
image: BmpImage::Srgb8(img),
metadata: BmpMetadata {
color_space: BmpColorSpace::Srgb,
source_bit_depth: BmpBitDepth::TwentyFour,
resolution: None,
compression: BmpCompression::None,
header_version: BmpHeaderVersion::Info,
icc_profile: None,
},
};
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
}
_ => panic!("expected Srgb8"),
}
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::Info);
}
#[test]
fn color_space_variants_are_constructible() {
let srgb = BmpColorSpace::Srgb;
let icc = BmpColorSpace::IccTagged;
assert_ne!(srgb, icc);
let _ = format!("{:?}", srgb);
let _ = format!("{:?}", icc);
}
#[test]
fn bit_depth_all_variants() {
let depths = [
BmpBitDepth::One,
BmpBitDepth::Four,
BmpBitDepth::Eight,
BmpBitDepth::Sixteen,
BmpBitDepth::TwentyFour,
BmpBitDepth::ThirtyTwo,
];
for d in &depths {
let _ = format!("{:?}", d);
}
assert_eq!(depths[0], BmpBitDepth::One);
assert_ne!(depths[0], depths[1]);
}
#[test]
fn compression_all_variants() {
let comps = [
BmpCompression::None,
BmpCompression::Rle8,
BmpCompression::Rle4,
BmpCompression::Bitfields,
];
for c in &comps {
let _ = format!("{:?}", c);
}
assert_eq!(comps[0], BmpCompression::None);
}
#[test]
fn header_version_all_variants() {
let versions = [
BmpHeaderVersion::Core,
BmpHeaderVersion::Info,
BmpHeaderVersion::V4,
BmpHeaderVersion::V5,
];
for v in &versions {
let _ = format!("{:?}", v);
}
assert_eq!(versions[1], BmpHeaderVersion::Info);
}
#[test]
fn resolution_constructible() {
let res = BmpResolution {
x_pixels_per_meter: 3780,
y_pixels_per_meter: 3780,
};
assert_eq!(res.x_pixels_per_meter, 3780);
assert_eq!(res, res);
let _ = format!("{:?}", res);
}
#[test]
fn bmp_encode_options_default() {
let opts = BmpEncodeOptions::default();
assert!(opts.resolution.is_none());
let _ = format!("{:?}", opts);
}
#[test]
fn bmp_image_debug_shows_variant_and_dimensions() {
let img = Image::fill(320, 240, Srgb8::new(0, 0, 0));
let bmp_img = BmpImage::Srgb8(img);
let dbg = format!("{:?}", bmp_img);
assert_eq!(dbg, "Srgb8(320x240)");
}
#[test]
fn bmp_image_debug_indexed() {
let img = Image::fill(4, 4, Indexed8(0));
let palette = Box::new([Srgba8::new(0, 0, 0, 255); 256]);
let bmp_img = BmpImage::Indexed8 { data: img, palette };
let dbg = format!("{:?}", bmp_img);
assert_eq!(dbg, "Indexed8(4x4)");
}
#[test]
fn bmp_image_debug_srgba8() {
let img = Image::fill(10, 5, Srgba8::new(0, 0, 0, 255));
let bmp_img = BmpImage::Srgba8(img);
let dbg = format!("{:?}", bmp_img);
assert_eq!(dbg, "Srgba8(10x5)");
}
#[test]
fn metadata_fully_populated() {
let meta = BmpMetadata {
color_space: BmpColorSpace::IccTagged,
source_bit_depth: BmpBitDepth::ThirtyTwo,
resolution: Some(BmpResolution {
x_pixels_per_meter: 2835,
y_pixels_per_meter: 2835,
}),
compression: BmpCompression::Bitfields,
header_version: BmpHeaderVersion::V5,
icc_profile: Some(vec![0u8; 100].into_boxed_slice()),
};
assert_eq!(meta.color_space, BmpColorSpace::IccTagged);
assert_eq!(meta.source_bit_depth, BmpBitDepth::ThirtyTwo);
assert!(meta.resolution.is_some());
assert_eq!(meta.compression, BmpCompression::Bitfields);
assert_eq!(meta.header_version, BmpHeaderVersion::V5);
assert!(meta.icc_profile.is_some());
}
#[test]
fn le_reader_u16() {
assert_eq!(read_u16_le(&[0x01, 0x02], 0), Some(0x0201));
assert_eq!(read_u16_le(&[0xFF, 0x00], 0), Some(0x00FF));
assert_eq!(read_u16_le(&[0x00], 0), None);
assert_eq!(read_u16_le(&[0x00, 0x01, 0x02], 1), Some(0x0201));
}
#[test]
fn le_reader_u32() {
assert_eq!(read_u32_le(&[0x01, 0x02, 0x03, 0x04], 0), Some(0x04030201));
assert_eq!(read_u32_le(&[0x00, 0x01, 0x02], 0), None);
}
#[test]
fn le_reader_i32() {
assert_eq!(read_i32_le(&[0xFF, 0xFF, 0xFF, 0xFF], 0), Some(-1));
assert_eq!(read_i32_le(&[0x04, 0x00, 0x00, 0x00], 0), Some(4));
}
#[test]
fn le_reader_bounds_checking() {
assert_eq!(read_u16_le(&[], 0), None);
assert_eq!(read_u32_le(&[0; 3], 0), None);
assert_eq!(read_i32_le(&[0; 3], 0), None);
assert_eq!(read_u16_le(&[0; 10], 9), None);
assert_eq!(read_u32_le(&[0; 10], 8), None);
}
#[test]
fn file_header_valid() {
let mut data = vec![0u8; 14];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, 100); write_u32_le(&mut data, 10, 54); let header = parse_file_header(&data).unwrap();
assert_eq!(header.pixel_data_offset, 54);
}
#[test]
fn file_header_reject_non_bmp() {
let data = vec![
0x89, 0x50, 0x4E, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let result = parse_file_header(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn file_header_reject_truncated() {
let data = vec![0x42, 0x4D, 0x00];
let result = parse_file_header(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
fn make_bmp_with_info_header(
width: i32,
height: i32,
bit_count: u16,
compression: u32,
) -> Vec<u8> {
let mut data = vec![0u8; 14 + 40];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 10, 54);
write_u32_le(&mut data, 14, 40); write_i32_le(&mut data, 18, width); write_i32_le(&mut data, 22, height); write_u16_le(&mut data, 26, 1); write_u16_le(&mut data, 28, bit_count); write_u32_le(&mut data, 30, compression); data
}
#[test]
fn dib_header_info_40_bytes() {
let data = make_bmp_with_info_header(4, 4, 24, 0);
let dib = parse_dib_header(&data).unwrap();
assert_eq!(dib.header_version, BmpHeaderVersion::Info);
assert_eq!(dib.width, 4);
assert_eq!(dib.height, 4);
assert_eq!(dib.bit_count, 24);
assert!(!dib.top_down);
}
#[test]
fn dib_header_negative_height_is_top_down() {
let data = make_bmp_with_info_header(4, -4, 24, 0);
let dib = parse_dib_header(&data).unwrap();
assert_eq!(dib.height, 4);
assert!(dib.top_down);
}
#[test]
fn dib_header_reject_zero_width() {
let data = make_bmp_with_info_header(0, 4, 24, 0);
let result = parse_dib_header(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn dib_header_reject_zero_height() {
let data = make_bmp_with_info_header(4, 0, 24, 0);
let result = parse_dib_header(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn dib_header_reject_invalid_bit_depth() {
let data = make_bmp_with_info_header(4, 4, 7, 0);
let result = parse_dib_header(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn dib_header_reject_invalid_planes() {
let mut data = make_bmp_with_info_header(4, 4, 24, 0);
write_u16_le(&mut data, 26, 2); let result = parse_dib_header(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn dib_header_reject_unsupported_size() {
let mut data = vec![0u8; 14 + 64];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 64); let result = parse_dib_header(&data);
assert!(matches!(result, Err(IoError::UnsupportedFeature { .. })));
}
#[test]
fn dib_header_core_12_bytes() {
let mut data = vec![0u8; 14 + 12];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 12); write_u16_le(&mut data, 18, 4); write_u16_le(&mut data, 20, 4); write_u16_le(&mut data, 22, 1); write_u16_le(&mut data, 24, 24); let dib = parse_dib_header(&data).unwrap();
assert_eq!(dib.header_version, BmpHeaderVersion::Core);
assert_eq!(dib.width, 4);
assert_eq!(dib.height, 4);
assert_eq!(dib.color_table_entry_size, 3);
}
#[test]
fn color_table_bgr_reorder() {
let data = [
0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, ];
let palette = parse_color_table(&data, 0, 4, 2).unwrap();
assert_eq!(palette[0], Srgba8::new(255, 0, 0, 255));
assert_eq!(palette[1], Srgba8::new(0, 0, 255, 255));
assert_eq!(palette[255], Srgba8::new(0, 0, 0, 255));
}
#[test]
fn color_table_3byte_core_entries() {
let data = [
0x00, 0xFF, 0x00, ];
let palette = parse_color_table(&data, 0, 3, 1).unwrap();
assert_eq!(palette[0], Srgba8::new(0, 255, 0, 255));
}
#[test]
fn color_table_partial() {
let data = [0xAA, 0xBB, 0xCC, 0x00];
let palette = parse_color_table(&data, 0, 4, 1).unwrap();
assert_eq!(palette[0], Srgba8::new(0xCC, 0xBB, 0xAA, 255));
assert_eq!(palette[1], Srgba8::new(0, 0, 0, 255));
}
#[test]
fn mask_info_555() {
let masks = BitfieldMasks::default_16bit();
let r = mask_info(masks.r); let g = mask_info(masks.g); let b = mask_info(masks.b);
assert_eq!(r.shift, 10);
assert_eq!(r.bits, 5);
assert_eq!(g.shift, 5);
assert_eq!(g.bits, 5);
assert_eq!(b.shift, 0);
assert_eq!(b.bits, 5);
}
#[test]
fn mask_info_32bit_standard() {
let masks = BitfieldMasks::default_32bit();
let r = mask_info(masks.r);
let g = mask_info(masks.g);
let b = mask_info(masks.b);
assert_eq!(r.shift, 16);
assert_eq!(r.bits, 8);
assert_eq!(g.shift, 8);
assert_eq!(g.bits, 8);
assert_eq!(b.shift, 0);
assert_eq!(b.bits, 8);
}
#[test]
fn extract_channel_5bit() {
let info = MaskInfo { shift: 10, bits: 5 };
assert_eq!(extract_channel(0x7C00, info), 255);
assert_eq!(extract_channel(0x0000, info), 0);
let val = extract_channel(16 << 10, info);
assert_eq!(val, ((16u32 * 255 + 15) / 31) as u8);
}
#[test]
fn extract_channel_8bit() {
let info = MaskInfo { shift: 16, bits: 8 };
assert_eq!(extract_channel(0x00FF0000, info), 255);
assert_eq!(extract_channel(0x00800000, info), 128);
assert_eq!(extract_channel(0x00000000, info), 0);
}
#[test]
fn extract_channel_zero_mask() {
let info = MaskInfo { shift: 0, bits: 0 };
assert_eq!(extract_channel(0xFFFFFFFF, info), 0);
}
#[test]
fn rle8_simple_run() {
let data = [3, 7, 0, 1];
let result = decompress_rle8(&data, 4, 1).unwrap();
assert_eq!(result[0..3], [7, 7, 7]);
assert_eq!(result[3], 0); }
#[test]
fn rle8_end_of_line() {
let data = [2, 1, 0, 0, 2, 2, 0, 1];
let result = decompress_rle8(&data, 3, 2).unwrap();
assert_eq!(result[0..2], [1, 1]);
assert_eq!(result[2], 0); assert_eq!(result[3..5], [2, 2]);
}
#[test]
fn rle8_delta() {
let data = [0, 2, 1, 1, 1, 5, 0, 1];
let result = decompress_rle8(&data, 3, 3).unwrap();
assert_eq!(result[3 + 1], 5);
}
#[test]
fn rle8_absolute_mode_even() {
let data = [0, 4, 10, 20, 30, 40, 0, 1];
let result = decompress_rle8(&data, 4, 1).unwrap();
assert_eq!(result, [10, 20, 30, 40]);
}
#[test]
fn rle8_absolute_mode_odd() {
let data = [0, 3, 10, 20, 30, 0, 0, 1]; let result = decompress_rle8(&data, 3, 1).unwrap();
assert_eq!(result, [10, 20, 30]);
}
#[test]
fn rle8_out_of_bounds_error() {
let data = [255, 7]; let result = decompress_rle8(&data, 2, 1);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn rle4_simple_run() {
let data = [4, 0xAB, 0, 1];
let result = decompress_rle4(&data, 4, 1).unwrap();
assert_eq!(result, [0xA, 0xB, 0xA, 0xB]);
}
#[test]
fn rle4_absolute_mode() {
let data = [0, 3, 0x12, 0x30, 0, 1];
let result = decompress_rle4(&data, 4, 1).unwrap();
assert_eq!(result[0], 1);
assert_eq!(result[1], 2);
assert_eq!(result[2], 3);
}
#[test]
fn rle4_out_of_bounds() {
let data = [255, 0xAB]; let result = decompress_rle4(&data, 2, 1);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn row_stride_24bit() {
assert_eq!(row_stride(1, 24), 4);
assert_eq!(row_stride(2, 24), 8);
assert_eq!(row_stride(4, 24), 12);
}
#[test]
fn row_stride_32bit() {
assert_eq!(row_stride(1, 32), 4);
assert_eq!(row_stride(3, 32), 12);
}
#[test]
fn row_stride_8bit() {
assert_eq!(row_stride(1, 8), 4);
assert_eq!(row_stride(4, 8), 4);
assert_eq!(row_stride(5, 8), 8);
}
#[test]
fn row_stride_1bit() {
assert_eq!(row_stride(1, 1), 4);
assert_eq!(row_stride(33, 1), 8);
}
fn make_24bit_bmp(width: u32, height: u32, pixels: &[Srgb8]) -> Vec<u8> {
let stride = row_stride(width, 24);
let pixel_data_size = stride * height as usize;
let file_size = 14 + 40 + pixel_data_size;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, 54);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32); write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 24);
write_u32_le(&mut data, 30, 0);
for row in 0..height as usize {
let src_row = height as usize - 1 - row; let row_offset = 54 + row * stride;
for col in 0..width as usize {
let pix = &pixels[src_row * width as usize + col];
let pix_offset = row_offset + col * 3;
data[pix_offset] = pix.b.0;
data[pix_offset + 1] = pix.g.0;
data[pix_offset + 2] = pix.r.0;
}
}
data
}
#[test]
fn decode_24bit_uncompressed() {
let pixels = vec![
Srgb8::new(255, 0, 0),
Srgb8::new(0, 255, 0),
Srgb8::new(0, 0, 255),
Srgb8::new(128, 128, 128),
];
let data = make_24bit_bmp(2, 2, &pixels);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
assert_eq!(img.pixel_at(0, 0), Srgb8::new(255, 0, 0));
assert_eq!(img.pixel_at(1, 0), Srgb8::new(0, 255, 0));
assert_eq!(img.pixel_at(0, 1), Srgb8::new(0, 0, 255));
assert_eq!(img.pixel_at(1, 1), Srgb8::new(128, 128, 128));
}
other => panic!("expected Srgb8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::TwentyFour);
assert_eq!(decoded.metadata.compression, BmpCompression::None);
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::Info);
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
}
#[test]
fn decode_24bit_bgr_reorder_correct() {
let pixels = vec![Srgb8::new(30, 20, 10)];
let data = make_24bit_bmp(1, 1, &pixels);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.pixel_at(0, 0), Srgb8::new(30, 20, 10));
}
_ => panic!("expected Srgb8"),
}
}
#[test]
fn decode_preserves_dimensions() {
let pixels = vec![Srgb8::new(0, 0, 0); 5 * 3];
let data = make_24bit_bmp(5, 3, &pixels);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.width(), 5);
assert_eq!(img.height(), 3);
}
_ => panic!("expected Srgb8"),
}
}
#[test]
fn decode_24bit_with_row_padding() {
let pixels = vec![
Srgb8::new(1, 2, 3),
Srgb8::new(4, 5, 6),
Srgb8::new(7, 8, 9),
];
let data = make_24bit_bmp(3, 1, &pixels);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.pixel_at(0, 0), Srgb8::new(1, 2, 3));
assert_eq!(img.pixel_at(1, 0), Srgb8::new(4, 5, 6));
assert_eq!(img.pixel_at(2, 0), Srgb8::new(7, 8, 9));
}
_ => panic!("expected Srgb8"),
}
}
#[test]
fn decode_top_down() {
let width = 2u32;
let height = 2u32;
let stride = row_stride(width, 24);
let pixel_data_size = stride * height as usize;
let file_size = 14 + 40 + pixel_data_size;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, 54);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, -(height as i32)); write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 24);
data[54] = 0;
data[55] = 0;
data[56] = 255; data[57] = 0;
data[58] = 255;
data[59] = 0; let row1_start = 54 + stride;
data[row1_start] = 255;
data[row1_start + 1] = 0;
data[row1_start + 2] = 0; data[row1_start + 3] = 255;
data[row1_start + 4] = 255;
data[row1_start + 5] = 255;
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.pixel_at(0, 0), Srgb8::new(255, 0, 0)); assert_eq!(img.pixel_at(1, 0), Srgb8::new(0, 255, 0)); assert_eq!(img.pixel_at(0, 1), Srgb8::new(0, 0, 255)); assert_eq!(img.pixel_at(1, 1), Srgb8::new(255, 255, 255)); }
_ => panic!("expected Srgb8"),
}
}
#[test]
fn decode_invalid_data_returns_error() {
let result = decode(&[0x00, 0x00, 0x00, 0x00]);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn decode_empty_data_returns_error() {
let result = decode(&[]);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn decode_bi_jpeg_unsupported() {
let data = make_bmp_with_info_header(4, 4, 24, 4); let result = decode(&data);
assert!(matches!(result, Err(IoError::UnsupportedFeature { .. })));
}
#[test]
fn decode_bi_png_unsupported() {
let data = make_bmp_with_info_header(4, 4, 24, 5); let result = decode(&data);
assert!(matches!(result, Err(IoError::UnsupportedFeature { .. })));
}
#[test]
fn decode_unsupported_dib_header_size() {
let mut data = vec![0u8; 14 + 16];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 16); let result = decode(&data);
assert!(matches!(result, Err(IoError::UnsupportedFeature { .. })));
}
#[test]
fn decode_truncated_pixel_data() {
let data = make_bmp_with_info_header(4, 4, 24, 0);
let result = decode(&data);
assert!(result.is_err());
}
#[test]
fn decode_reader_from_cursor() {
let pixels = vec![Srgb8::new(100, 150, 200), Srgb8::new(50, 60, 70)];
let data = make_24bit_bmp(2, 1, &pixels);
let decoded = decode_reader(std::io::Cursor::new(&data)).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.width(), 2);
assert_eq!(img.pixel_at(0, 0), Srgb8::new(100, 150, 200));
assert_eq!(img.pixel_at(1, 0), Srgb8::new(50, 60, 70));
}
_ => panic!("expected Srgb8"),
}
}
fn make_8bit_indexed_bmp(
width: u32,
height: u32,
palette: &[Srgba8],
indices: &[u8],
) -> Vec<u8> {
let stride = row_stride(width, 8);
let color_table_size = palette.len() * 4;
let pixel_data_size = stride * height as usize;
let pixel_offset = 14 + 40 + color_table_size;
let file_size = pixel_offset + pixel_data_size;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 30, 0); write_u32_le(&mut data, 46, palette.len() as u32);
for (i, entry) in palette.iter().enumerate() {
let offset = 54 + i * 4;
data[offset] = entry.b.0;
data[offset + 1] = entry.g.0;
data[offset + 2] = entry.r.0;
data[offset + 3] = 0; }
for row in 0..height as usize {
let src_row = height as usize - 1 - row;
let row_offset = pixel_offset + row * stride;
for col in 0..width as usize {
data[row_offset + col] = indices[src_row * width as usize + col];
}
}
data
}
#[test]
fn decode_8bit_indexed() {
let palette = vec![
Srgba8::new(255, 0, 0, 255), Srgba8::new(0, 255, 0, 255), Srgba8::new(0, 0, 255, 255), ];
let indices = vec![0, 1, 2, 1];
let data = make_8bit_indexed_bmp(2, 2, &palette, &indices);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 {
data: img,
palette: pal,
} => {
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
assert_eq!(img.pixel_at(0, 0).0, 0);
assert_eq!(img.pixel_at(1, 0).0, 1);
assert_eq!(img.pixel_at(0, 1).0, 2);
assert_eq!(img.pixel_at(1, 1).0, 1);
assert_eq!(pal[0], Srgba8::new(255, 0, 0, 255));
assert_eq!(pal[1], Srgba8::new(0, 255, 0, 255));
assert_eq!(pal[2], Srgba8::new(0, 0, 255, 255));
}
other => panic!("expected Indexed8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::Eight);
}
#[test]
fn decode_1bit_indexed() {
let palette = [
Srgba8::new(0, 0, 0, 255), Srgba8::new(255, 255, 255, 255), ];
let stride = row_stride(4, 1); let pixel_offset = 14 + 40 + 2 * 4; let file_size = pixel_offset + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 4);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 1);
write_u32_le(&mut data, 46, 2);
for (i, entry) in palette.iter().enumerate() {
let off = 54 + i * 4;
data[off] = entry.b.0;
data[off + 1] = entry.g.0;
data[off + 2] = entry.r.0;
data[off + 3] = 0;
}
data[pixel_offset] = 0b1010_0000;
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 1);
assert_eq!(img.pixel_at(1, 0).0, 0);
assert_eq!(img.pixel_at(2, 0).0, 1);
assert_eq!(img.pixel_at(3, 0).0, 0);
}
other => panic!("expected Indexed8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::One);
}
#[test]
fn decode_4bit_indexed() {
let palette: Vec<Srgba8> = (0..16)
.map(|i| Srgba8::new(i * 16, i * 16, i * 16, 255))
.collect();
let _ = palette.len();
let stride = row_stride(4, 4); let pixel_offset = 14 + 40 + 16 * 4;
let file_size = pixel_offset + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 4);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 4);
write_u32_le(&mut data, 46, 16);
for (i, entry) in palette.iter().enumerate() {
let off = 54 + i * 4;
data[off] = entry.b.0;
data[off + 1] = entry.g.0;
data[off + 2] = entry.r.0;
data[off + 3] = 0;
}
data[pixel_offset] = 0xAB;
data[pixel_offset + 1] = 0xCD;
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 0xA);
assert_eq!(img.pixel_at(1, 0).0, 0xB);
assert_eq!(img.pixel_at(2, 0).0, 0xC);
assert_eq!(img.pixel_at(3, 0).0, 0xD);
}
other => panic!("expected Indexed8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::Four);
}
#[test]
fn decode_32bit_no_alpha() {
let width = 2u32;
let height = 1u32;
let stride = row_stride(width, 32);
let pixel_offset = 54usize;
let file_size = pixel_offset + stride * height as usize;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 32);
write_u32_le(&mut data, 30, 0);
data[pixel_offset] = 10;
data[pixel_offset + 1] = 20;
data[pixel_offset + 2] = 30;
data[pixel_offset + 3] = 0;
data[pixel_offset + 4] = 100;
data[pixel_offset + 5] = 150;
data[pixel_offset + 6] = 200;
data[pixel_offset + 7] = 255;
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.pixel_at(0, 0), Srgb8::new(30, 20, 10));
assert_eq!(img.pixel_at(1, 0), Srgb8::new(200, 150, 100));
}
other => panic!("expected Srgb8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::ThirtyTwo);
}
#[test]
fn decode_32bit_with_alpha() {
let width = 1u32;
let height = 1u32;
let header_size = 108u32;
let pixel_offset = 14 + header_size;
let stride = row_stride(width, 32);
let file_size = pixel_offset as usize + stride * height as usize;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset);
write_u32_le(&mut data, 14, header_size);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 32);
write_u32_le(&mut data, 30, BI_BITFIELDS);
write_u32_le(&mut data, 54, 0x00FF0000); write_u32_le(&mut data, 58, 0x0000FF00); write_u32_le(&mut data, 62, 0x000000FF); write_u32_le(&mut data, 66, 0xFF000000); write_u32_le(&mut data, 70, LCS_SRGB);
let pix_off = pixel_offset as usize;
write_u32_le(&mut data, pix_off, 0xCC332211);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgba8(img) => {
let p = img.pixel_at(0, 0);
assert_eq!(p.r.0, 0x33);
assert_eq!(p.g.0, 0x22);
assert_eq!(p.b.0, 0x11);
assert_eq!(p.a.0, 0xCC);
}
other => panic!("expected Srgba8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::ThirtyTwo);
assert_eq!(decoded.metadata.compression, BmpCompression::Bitfields);
}
#[test]
fn decode_16bit_555() {
let width = 1u32;
let height = 1u32;
let stride = row_stride(width, 16);
let pixel_offset = 54usize;
let file_size = pixel_offset + stride * height as usize;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 16);
write_u32_le(&mut data, 30, 0);
write_u16_le(&mut data, pixel_offset, 0x7FFF);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
let p = img.pixel_at(0, 0);
assert_eq!(p.r.0, 255);
assert_eq!(p.g.0, 255);
assert_eq!(p.b.0, 255);
}
other => panic!("expected Srgb8, got {:?}", other),
}
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::Sixteen);
}
#[test]
fn decode_16bit_565_bitfields() {
let width = 1u32;
let height = 1u32;
let mask_size = 12usize; let pixel_offset = 54 + mask_size;
let stride = row_stride(width, 16);
let file_size = pixel_offset + stride * height as usize;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 16);
write_u32_le(&mut data, 30, BI_BITFIELDS);
write_u32_le(&mut data, 54, 0xF800); write_u32_le(&mut data, 58, 0x07E0); write_u32_le(&mut data, 62, 0x001F);
write_u16_le(&mut data, pixel_offset, 0xFFFF);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
let p = img.pixel_at(0, 0);
assert_eq!(p.r.0, 255);
assert_eq!(p.g.0, 255);
assert_eq!(p.b.0, 255);
}
other => panic!("expected Srgb8, got {:?}", other),
}
}
#[test]
fn decode_rle8_compressed() {
let palette = [Srgba8::new(0, 0, 0, 255), Srgba8::new(255, 0, 0, 255)];
let width = 3u32;
let height = 1u32;
let color_table_size = 2 * 4;
let pixel_offset = 14 + 40 + color_table_size;
let rle_data = [3u8, 1, 0, 1];
let file_size = pixel_offset + rle_data.len();
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 30, BI_RLE8);
write_u32_le(&mut data, 46, 2);
for (i, entry) in palette.iter().enumerate() {
let off = 54 + i * 4;
data[off] = entry.b.0;
data[off + 1] = entry.g.0;
data[off + 2] = entry.r.0;
data[off + 3] = 0;
}
data[pixel_offset..pixel_offset + rle_data.len()].copy_from_slice(&rle_data);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 1);
assert_eq!(img.pixel_at(1, 0).0, 1);
assert_eq!(img.pixel_at(2, 0).0, 1);
}
other => panic!("expected Indexed8, got {:?}", other),
}
assert_eq!(decoded.metadata.compression, BmpCompression::Rle8);
}
#[test]
fn decode_core_header_24bit() {
let width = 2u16;
let height = 1u16;
let stride = row_stride(width as u32, 24);
let pixel_offset = 14 + 12;
let file_size = pixel_offset + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 12); write_u16_le(&mut data, 18, width);
write_u16_le(&mut data, 20, height);
write_u16_le(&mut data, 22, 1); write_u16_le(&mut data, 24, 24);
data[pixel_offset] = 10; data[pixel_offset + 1] = 20; data[pixel_offset + 2] = 30; data[pixel_offset + 3] = 100; data[pixel_offset + 4] = 150; data[pixel_offset + 5] = 200;
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
assert_eq!(img.pixel_at(0, 0), Srgb8::new(30, 20, 10));
assert_eq!(img.pixel_at(1, 0), Srgb8::new(200, 150, 100));
}
other => panic!("expected Srgb8, got {:?}", other),
}
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::Core);
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
}
#[test]
fn encode_srgb8_roundtrip() {
let img = Image::fill(3, 2, Srgb8::new(100, 150, 200));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgb8(dec_img) => {
assert_eq!(dec_img.width(), 3);
assert_eq!(dec_img.height(), 2);
for y in 0..2 {
for x in 0..3 {
assert_eq!(
dec_img.pixel_at(x, y),
Srgb8::new(100, 150, 200),
"mismatch at ({}, {})",
x,
y
);
}
}
}
other => panic!("expected Srgb8, got {:?}", other),
}
}
#[test]
fn encode_srgba8_roundtrip() {
let img = Image::fill(2, 2, Srgba8::new(10, 20, 30, 128));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgba8(dec_img) => {
assert_eq!(dec_img.width(), 2);
assert_eq!(dec_img.height(), 2);
for y in 0..2 {
for x in 0..2 {
assert_eq!(
dec_img.pixel_at(x, y),
Srgba8::new(10, 20, 30, 128),
"mismatch at ({}, {})",
x,
y
);
}
}
}
other => panic!("expected Srgba8, got {:?}", other),
}
}
#[test]
fn encode_indexed_roundtrip() {
let palette = [
Srgba8::new(255, 0, 0, 255),
Srgba8::new(0, 255, 0, 255),
Srgba8::new(0, 0, 255, 255),
];
let img = Image::from_vec(
2,
2,
vec![Indexed8(0), Indexed8(1), Indexed8(2), Indexed8(1)],
)
.unwrap();
let bytes = encode_indexed(&img, &palette, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Indexed8 {
data: dec_img,
palette: dec_pal,
} => {
assert_eq!(dec_img.width(), 2);
assert_eq!(dec_img.height(), 2);
assert_eq!(dec_img.pixel_at(0, 0).0, 0);
assert_eq!(dec_img.pixel_at(1, 0).0, 1);
assert_eq!(dec_img.pixel_at(0, 1).0, 2);
assert_eq!(dec_img.pixel_at(1, 1).0, 1);
assert_eq!(dec_pal[0], Srgba8::new(255, 0, 0, 255));
assert_eq!(dec_pal[1], Srgba8::new(0, 255, 0, 255));
assert_eq!(dec_pal[2], Srgba8::new(0, 0, 255, 255));
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn encode_writer_byte_identical_to_encode() {
let img = Image::fill(4, 3, Srgb8::new(50, 100, 150));
let encode_result = encode(&img, &BmpEncodeOptions::default()).unwrap();
let mut writer_result = Vec::new();
encode_writer(&img, &mut writer_result, &BmpEncodeOptions::default()).unwrap();
assert_eq!(encode_result, writer_result);
}
#[test]
fn encode_dimensions_roundtrip() {
for (w, h) in [(1, 1), (2, 3), (5, 1), (1, 7), (100, 50)] {
let img = Image::fill(w, h, Srgb8::new(0, 0, 0));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgb8(dec_img) => {
assert_eq!(dec_img.width(), w);
assert_eq!(dec_img.height(), h);
}
_ => panic!("expected Srgb8"),
}
}
}
#[test]
fn encode_pixel_values_roundtrip() {
let img = Image::generate(4, 4, |x, y| {
Srgb8::new(
(x * 60 + y * 20) as u8,
(x * 30 + y * 40) as u8,
(x * 10 + y * 80) as u8,
)
});
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgb8(dec_img) => {
for y in 0..4 {
for x in 0..4 {
assert_eq!(
dec_img.pixel_at(x, y),
img.pixel_at(x, y),
"pixel mismatch at ({}, {})",
x,
y
);
}
}
}
_ => panic!("expected Srgb8"),
}
}
#[test]
fn encode_bgr_byte_order_correct() {
let img = Image::fill(1, 1, Srgb8::new(0xAA, 0xBB, 0xCC));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
assert_eq!(bytes[54], 0xCC); assert_eq!(bytes[55], 0xBB); assert_eq!(bytes[56], 0xAA); }
#[test]
fn encode_row_padding_correct() {
let img = Image::fill(1, 1, Srgb8::new(255, 0, 0));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
assert_eq!(bytes.len(), 58);
assert_eq!(bytes[57], 0);
}
#[test]
fn encode_rows_are_bottom_up() {
let img =
Image::from_vec(1, 2, vec![Srgb8::new(255, 0, 0), Srgb8::new(0, 0, 255)]).unwrap();
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let stride = row_stride(1, 24);
assert_eq!(bytes[54], 255); assert_eq!(bytes[55], 0); assert_eq!(bytes[56], 0); assert_eq!(bytes[54 + stride], 0); assert_eq!(bytes[54 + stride + 1], 0); assert_eq!(bytes[54 + stride + 2], 255); }
#[test]
fn encode_resolution_in_header() {
let img = Image::fill(1, 1, Srgb8::new(0, 0, 0));
let opts = BmpEncodeOptions {
resolution: Some(BmpResolution {
x_pixels_per_meter: 3780,
y_pixels_per_meter: 3780,
}),
..Default::default()
};
let bytes = encode(&img, &opts).unwrap();
assert_eq!(read_u32_le(&bytes, 38), Some(3780));
assert_eq!(read_u32_le(&bytes, 42), Some(3780));
}
#[test]
fn encode_bmp_image_dispatches_srgb8() {
let img = Image::fill(2, 2, Srgb8::new(1, 2, 3));
let bmp_img = BmpImage::Srgb8(img);
let bytes = encode_bmp_image(&bmp_img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgb8(dec_img) => {
assert_eq!(dec_img.pixel_at(0, 0), Srgb8::new(1, 2, 3));
}
_ => panic!("expected Srgb8"),
}
}
#[test]
fn encode_bmp_image_dispatches_srgba8() {
let img = Image::fill(2, 2, Srgba8::new(10, 20, 30, 128));
let bmp_img = BmpImage::Srgba8(img);
let bytes = encode_bmp_image(&bmp_img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgba8(_) => {}
_ => panic!("expected Srgba8"),
}
}
#[test]
fn encode_bmp_image_dispatches_indexed() {
let palette = Box::new([Srgba8::new(128, 64, 32, 255); 256]);
let img = Image::fill(2, 2, Indexed8(0));
let bmp_img = BmpImage::Indexed8 { data: img, palette };
let bytes = encode_bmp_image(&bmp_img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Indexed8 { .. } => {}
_ => panic!("expected Indexed8"),
}
}
#[test]
fn encode_indexed_empty_palette_error() {
let img = Image::fill(1, 1, Indexed8(0));
let result = encode_indexed(&img, &[], &BmpEncodeOptions::default());
assert!(matches!(result, Err(IoError::EncodeFailed { .. })));
}
#[test]
fn encode_indexed_palette_too_large_error() {
let img = Image::fill(1, 1, Indexed8(0));
let palette = vec![Srgba8::new(0, 0, 0, 255); 257];
let result = encode_indexed(&img, &palette, &BmpEncodeOptions::default());
assert!(matches!(result, Err(IoError::EncodeFailed { .. })));
}
#[test]
fn encode_indexed_index_out_of_range_error() {
let img = Image::fill(1, 1, Indexed8(5));
let palette = [Srgba8::new(0, 0, 0, 255); 3]; let result = encode_indexed(&img, &palette, &BmpEncodeOptions::default());
assert!(matches!(result, Err(IoError::EncodeFailed { .. })));
}
#[test]
fn encode_large_image_padding() {
let img = Image::fill(101, 50, Srgb8::new(42, 84, 126));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgb8(dec_img) => {
assert_eq!(dec_img.width(), 101);
assert_eq!(dec_img.height(), 50);
assert_eq!(dec_img.pixel_at(0, 0), Srgb8::new(42, 84, 126));
assert_eq!(dec_img.pixel_at(100, 49), Srgb8::new(42, 84, 126));
}
_ => panic!("expected Srgb8"),
}
}
#[test]
fn metadata_resolution_none_for_zero() {
let img = Image::fill(1, 1, Srgb8::new(0, 0, 0));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
assert!(decoded.metadata.resolution.is_none());
}
#[test]
fn metadata_resolution_roundtrip() {
let img = Image::fill(1, 1, Srgb8::new(0, 0, 0));
let opts = BmpEncodeOptions {
resolution: Some(BmpResolution {
x_pixels_per_meter: 2835,
y_pixels_per_meter: 2835,
}),
..Default::default()
};
let bytes = encode(&img, &opts).unwrap();
let decoded = decode(&bytes).unwrap();
assert_eq!(
decoded.metadata.resolution,
Some(BmpResolution {
x_pixels_per_meter: 2835,
y_pixels_per_meter: 2835,
})
);
}
#[test]
fn metadata_color_space_srgb_for_pre_v4() {
let img = Image::fill(1, 1, Srgb8::new(0, 0, 0));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
}
#[test]
fn bmp_decoded_implements_debug() {
let img = Image::fill(2, 2, Srgb8::new(0, 0, 0));
let decoded = BmpDecoded {
image: BmpImage::Srgb8(img),
metadata: BmpMetadata {
color_space: BmpColorSpace::Srgb,
source_bit_depth: BmpBitDepth::TwentyFour,
resolution: None,
compression: BmpCompression::None,
header_version: BmpHeaderVersion::Info,
icc_profile: None,
},
};
let dbg = format!("{:?}", decoded);
assert!(dbg.contains("Srgb8(2x2)"));
}
#[test]
fn encode_srgba8_bgra_byte_order() {
let img = Image::fill(1, 1, Srgba8::new(0xAA, 0xBB, 0xCC, 0xDD));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let pix_offset = 70;
assert_eq!(bytes[pix_offset], 0xCC); assert_eq!(bytes[pix_offset + 1], 0xBB); assert_eq!(bytes[pix_offset + 2], 0xAA); assert_eq!(bytes[pix_offset + 3], 0xDD); }
#[test]
fn encode_srgba8_pixel_exact_roundtrip() {
let img = Image::generate(3, 3, |x, y| {
Srgba8::new(
(x * 80 + 10) as u8,
(y * 60 + 20) as u8,
(x * 40 + y * 30) as u8,
((x + y) * 50 + 55) as u8,
)
});
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Srgba8(dec_img) => {
for y in 0..3 {
for x in 0..3 {
assert_eq!(
dec_img.pixel_at(x, y),
img.pixel_at(x, y),
"pixel mismatch at ({}, {})",
x,
y
);
}
}
}
_ => panic!("expected Srgba8"),
}
}
#[test]
fn encode_indexed_palette_values_roundtrip() {
let palette = [
Srgba8::new(0, 0, 0, 255),
Srgba8::new(255, 0, 0, 255),
Srgba8::new(0, 255, 0, 255),
Srgba8::new(0, 0, 255, 255),
Srgba8::new(128, 128, 128, 255),
];
let img = Image::from_vec(3, 1, vec![Indexed8(0), Indexed8(2), Indexed8(4)]).unwrap();
let bytes = encode_indexed(&img, &palette, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Indexed8 {
data: dec_img,
palette: dec_pal,
} => {
assert_eq!(dec_img.pixel_at(0, 0).0, 0);
assert_eq!(dec_img.pixel_at(1, 0).0, 2);
assert_eq!(dec_img.pixel_at(2, 0).0, 4);
for i in 0..5 {
assert_eq!(dec_pal[i], palette[i], "palette mismatch at index {}", i);
}
}
_ => panic!("expected Indexed8"),
}
}
#[test]
fn color_space_srgb_for_v4_lcs_srgb() {
let width = 1u32;
let height = 1u32;
let header_size = 108u32;
let pixel_offset = 14 + header_size;
let stride = row_stride(width, 24);
let file_size = pixel_offset as usize + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset);
write_u32_le(&mut data, 14, header_size);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 24);
write_u32_le(&mut data, 30, 0); write_u32_le(&mut data, 70, LCS_SRGB);
let off = pixel_offset as usize;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
let decoded = decode(&data).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::V4);
}
#[test]
fn color_space_icc_tagged_for_v5_profile_embedded() {
let width = 1u32;
let height = 1u32;
let header_size = 124u32;
let icc_profile = vec![0xDE, 0xAD, 0xBE, 0xEF]; let pixel_offset = 14 + header_size;
let icc_offset = pixel_offset + row_stride(width, 24) as u32;
let file_size = icc_offset as usize + icc_profile.len();
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset);
write_u32_le(&mut data, 14, header_size);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 24);
write_u32_le(&mut data, 30, 0); write_u32_le(&mut data, 70, LCS_PROFILE_EMBEDDED);
write_u32_le(&mut data, 14 + 112, icc_offset - 14); write_u32_le(&mut data, 14 + 116, icc_profile.len() as u32);
let off = pixel_offset as usize;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
data[icc_offset as usize..icc_offset as usize + icc_profile.len()]
.copy_from_slice(&icc_profile);
let decoded = decode(&data).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::IccTagged);
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::V5);
assert!(decoded.metadata.icc_profile.is_some());
assert_eq!(&*decoded.metadata.icc_profile.unwrap(), &icc_profile[..]);
}
#[test]
fn metadata_is_cloneable() {
let meta = BmpMetadata {
color_space: BmpColorSpace::Srgb,
source_bit_depth: BmpBitDepth::TwentyFour,
resolution: None,
compression: BmpCompression::None,
header_version: BmpHeaderVersion::Info,
icc_profile: None,
};
let cloned = meta.clone();
assert_eq!(cloned.color_space, meta.color_space);
assert_eq!(cloned.source_bit_depth, meta.source_bit_depth);
}
#[test]
fn decode_reader_matches_decode() {
let img = Image::fill(3, 2, Srgb8::new(42, 84, 126));
let bytes = encode(&img, &BmpEncodeOptions::default()).unwrap();
let d1 = decode(&bytes).unwrap();
let d2 = decode_reader(std::io::Cursor::new(&bytes)).unwrap();
match (&d1.image, &d2.image) {
(BmpImage::Srgb8(a), BmpImage::Srgb8(b)) => {
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
for y in 0..a.height() {
for x in 0..a.width() {
assert_eq!(a.pixel_at(x, y), b.pixel_at(x, y));
}
}
}
_ => panic!("variant mismatch"),
}
}
#[test]
fn all_metadata_types_constructible() {
let _ = BmpColorSpace::Srgb;
let _ = BmpColorSpace::IccTagged;
let _ = BmpBitDepth::One;
let _ = BmpBitDepth::Four;
let _ = BmpBitDepth::Eight;
let _ = BmpBitDepth::Sixteen;
let _ = BmpBitDepth::TwentyFour;
let _ = BmpBitDepth::ThirtyTwo;
let _ = BmpCompression::None;
let _ = BmpCompression::Rle8;
let _ = BmpCompression::Rle4;
let _ = BmpCompression::Bitfields;
let _ = BmpHeaderVersion::Core;
let _ = BmpHeaderVersion::Info;
let _ = BmpHeaderVersion::V4;
let _ = BmpHeaderVersion::V5;
let _ = BmpResolution {
x_pixels_per_meter: 0,
y_pixels_per_meter: 0,
};
let _ = BmpEncodeOptions::default();
}
#[test]
fn decode_rle4_compressed() {
let palette: Vec<Srgba8> = (0..16)
.map(|i| Srgba8::new(i * 16, i * 16, i * 16, 255))
.collect();
let width = 4u32;
let height = 1u32;
let color_table_size = 16 * 4;
let pixel_offset = 14 + 40 + color_table_size;
let rle_data: &[u8] = &[4, 0xAB, 0, 1];
let file_size = pixel_offset + rle_data.len();
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 4);
write_u32_le(&mut data, 30, BI_RLE4);
write_u32_le(&mut data, 46, 16);
for (i, entry) in palette.iter().enumerate() {
let off = 54 + i * 4;
data[off] = entry.b.0;
data[off + 1] = entry.g.0;
data[off + 2] = entry.r.0;
data[off + 3] = 0;
}
data[pixel_offset..pixel_offset + rle_data.len()].copy_from_slice(rle_data);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 0xA);
assert_eq!(img.pixel_at(1, 0).0, 0xB);
assert_eq!(img.pixel_at(2, 0).0, 0xA);
assert_eq!(img.pixel_at(3, 0).0, 0xB);
}
other => panic!("expected Indexed8, got {:?}", other),
}
assert_eq!(decoded.metadata.compression, BmpCompression::Rle4);
assert_eq!(decoded.metadata.source_bit_depth, BmpBitDepth::Four);
}
#[test]
fn parse_core_header_truncated() {
let mut data = vec![0u8; 14 + 8]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 12); let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn parse_info_header_truncated() {
let mut data = vec![0u8; 14 + 20]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 40); let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn parse_v4_header_truncated() {
let mut data = vec![0u8; 14 + 60]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 108);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn parse_v5_header_truncated() {
let mut data = vec![0u8; 14 + 80]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 124);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn v4_header_reject_invalid_planes() {
let mut data = vec![0u8; 14 + 108 + 32]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 108);
write_i32_le(&mut data, 18, 1);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 2); write_u16_le(&mut data, 28, 24);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn v5_header_reject_invalid_planes() {
let mut data = vec![0u8; 14 + 124 + 32];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 124);
write_i32_le(&mut data, 18, 1);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 2); write_u16_le(&mut data, 28, 24);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn core_header_reject_zero_dimensions() {
let mut data = vec![0u8; 14 + 12];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 12);
write_u16_le(&mut data, 18, 0); write_u16_le(&mut data, 20, 4);
write_u16_le(&mut data, 22, 1);
write_u16_le(&mut data, 24, 24);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn core_header_reject_invalid_planes() {
let mut data = vec![0u8; 14 + 12];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 12);
write_u16_le(&mut data, 18, 4);
write_u16_le(&mut data, 20, 4);
write_u16_le(&mut data, 22, 2); write_u16_le(&mut data, 24, 24);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn core_header_reject_invalid_bit_count() {
let mut data = vec![0u8; 14 + 12];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 14, 12);
write_u16_le(&mut data, 18, 4);
write_u16_le(&mut data, 20, 4);
write_u16_le(&mut data, 22, 1); write_u16_le(&mut data, 24, 16); let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn rle8_with_non_8bit_rejected() {
let data = make_bmp_with_info_header(4, 4, 24, BI_RLE8);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn rle4_with_non_4bit_rejected() {
let data = make_bmp_with_info_header(4, 4, 8, BI_RLE4);
let result = decode(&data);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn unknown_compression_type_rejected() {
let data = make_bmp_with_info_header(4, 4, 24, 99);
let result = decode(&data);
assert!(matches!(result, Err(IoError::UnsupportedFeature { .. })));
}
#[test]
fn bitfield_masks_truncated() {
let mut data = vec![0u8; 14 + 40]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 10, 66); write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 1);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 16);
write_u32_le(&mut data, 30, BI_BITFIELDS);
let result = decode(&data);
assert!(result.is_err());
}
#[test]
fn bitfield_masks_no_alpha_dword() {
let mask_size = 12usize;
let pixel_offset = 54 + mask_size;
let stride = row_stride(1, 16);
let file_size = pixel_offset + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 1);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 16);
write_u32_le(&mut data, 30, BI_BITFIELDS);
write_u32_le(&mut data, 54, 0xF800);
write_u32_le(&mut data, 58, 0x07E0);
write_u32_le(&mut data, 62, 0x001F);
write_u16_le(&mut data, pixel_offset, 0xFFFF);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Srgb8(img) => {
let p = img.pixel_at(0, 0);
assert_eq!(p.r.0, 255);
assert_eq!(p.g.0, 255);
assert_eq!(p.b.0, 255);
}
other => panic!("expected Srgb8, got {:?}", other),
}
}
#[test]
fn color_table_truncated_error() {
let mut data = vec![0u8; 14 + 40 + 2]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 10, 14 + 40 + 256 * 4); write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 1);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 30, 0);
let result = decode(&data);
assert!(result.is_err());
}
#[test]
fn decode_1bit_pixel_data_truncated() {
let palette_size = 2 * 4;
let pixel_offset = 14 + 40 + palette_size;
let mut data = vec![0u8; pixel_offset];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, pixel_offset as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 8);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 1);
write_u32_le(&mut data, 46, 2);
for i in 0..2 {
let off = 54 + i * 4;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
data[off + 3] = 0;
}
let result = decode(&data);
assert!(matches!(result, Err(IoError::DecodeFailed { .. })));
}
#[test]
fn decode_4bit_pixel_data_truncated() {
let palette_size = 16 * 4;
let pixel_offset = 14 + 40 + palette_size;
let mut data = vec![0u8; pixel_offset]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, pixel_offset as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 4);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 4);
write_u32_le(&mut data, 46, 16);
for i in 0..16 {
let off = 54 + i * 4;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
data[off + 3] = 0;
}
let result = decode(&data);
assert!(matches!(result, Err(IoError::DecodeFailed { .. })));
}
#[test]
fn decode_8bit_pixel_data_truncated() {
let palette_size = 4 * 4;
let pixel_offset = 14 + 40 + palette_size;
let mut data = vec![0u8; pixel_offset]; data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, pixel_offset as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 4);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 46, 4);
for i in 0..4 {
let off = 54 + i * 4;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
data[off + 3] = 0;
}
let result = decode(&data);
assert!(matches!(result, Err(IoError::DecodeFailed { .. })));
}
#[test]
fn decode_8bit_indexed_colors_used_zero() {
let palette_count = 256;
let palette_size = palette_count * 4;
let width = 1u32;
let height = 1u32;
let stride = row_stride(width, 8);
let pixel_offset = 14 + 40 + palette_size;
let file_size = pixel_offset + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 46, 0);
for i in 0..palette_count {
let off = 54 + i * 4;
data[off] = i as u8;
data[off + 1] = 0;
data[off + 2] = 0;
data[off + 3] = 0;
}
data[pixel_offset] = 42;
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 42);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn rle4_end_of_line_and_delta() {
let data_seq: &[u8] = &[
2, 0xAB, 0, 0, 0, 2, 1, 0, 1, 0xC0, 0, 1, ];
let result = decompress_rle4(data_seq, 3, 2).unwrap();
assert_eq!(result[0], 0xA);
assert_eq!(result[1], 0xB);
assert_eq!(result[2], 0);
assert_eq!(result[3], 0); assert_eq!(result[4], 0xC); }
#[test]
fn rle4_absolute_mode_odd_byte_pad() {
let data_seq: &[u8] = &[
0, 5, 0x12, 0x34, 0x50, 0, 0, 1, ];
let result = decompress_rle4(data_seq, 5, 1).unwrap();
assert_eq!(result[0], 1);
assert_eq!(result[1], 2);
assert_eq!(result[2], 3);
assert_eq!(result[3], 4);
assert_eq!(result[4], 5);
}
#[test]
fn rle4_exceeds_image_bounds_encoded_run() {
let data_seq: &[u8] = &[
2, 0xAB, 0, 0, 2, 0xAB, 0, 0, 2, 0xAB, ];
let result = decompress_rle4(data_seq, 2, 2);
assert!(result.is_err());
}
#[test]
fn rle4_delta_truncated() {
let data_seq: &[u8] = &[0, 2, 1]; let result = decompress_rle4(data_seq, 4, 4);
assert!(result.is_err());
}
#[test]
fn rle4_absolute_mode_truncated() {
let data_seq: &[u8] = &[0, 6, 0x12]; let result = decompress_rle4(data_seq, 8, 1);
assert!(result.is_err());
}
#[test]
fn rle8_exceeds_image_bounds_encoded_run() {
let data_seq: &[u8] = &[
2, 1, 0, 0, 2, 1, 0, 0, 2, 1, ];
let result = decompress_rle8(data_seq, 2, 2);
assert!(result.is_err());
}
#[test]
fn rle8_delta_truncated() {
let data_seq: &[u8] = &[0, 2, 1]; let result = decompress_rle8(data_seq, 4, 4);
assert!(result.is_err());
}
#[test]
fn rle8_absolute_mode_truncated() {
let data_seq: &[u8] = &[0, 5, 1, 2]; let result = decompress_rle8(data_seq, 8, 1);
assert!(result.is_err());
}
#[test]
fn rle8_absolute_mode_exceeds_bounds() {
let data_seq: &[u8] = &[
3, 1, 0, 0, 0, 0, 0, 3, 1, 2, 3, 0, 0, 1,
];
let result = decompress_rle8(data_seq, 3, 1);
assert!(result.is_err());
}
#[test]
fn icc_profile_beyond_file_end_is_none() {
let width = 1u32;
let height = 1u32;
let header_size = 124u32;
let pixel_offset = 14 + header_size;
let stride = row_stride(width, 24);
let file_size = pixel_offset as usize + stride; let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset);
write_u32_le(&mut data, 14, header_size);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 24);
write_u32_le(&mut data, 30, 0);
write_u32_le(&mut data, 70, LCS_PROFILE_EMBEDDED);
write_u32_le(&mut data, 14 + 112, 9999);
write_u32_le(&mut data, 14 + 116, 100);
let decoded = decode(&data).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::IccTagged);
assert!(decoded.metadata.icc_profile.is_none());
}
#[test]
fn encode_indexed_with_resolution() {
let img = Image::fill(1, 1, Indexed8(0));
let palette = [Srgba8::new(0, 0, 0, 255)];
let opts = BmpEncodeOptions {
resolution: Some(BmpResolution {
x_pixels_per_meter: 2835,
y_pixels_per_meter: 2835,
}),
..Default::default()
};
let bytes = encode_indexed(&img, &palette, &opts).unwrap();
let decoded = decode(&bytes).unwrap();
assert_eq!(
decoded.metadata.resolution,
Some(BmpResolution {
x_pixels_per_meter: 2835,
y_pixels_per_meter: 2835,
})
);
}
#[test]
fn encode_indexed_row_padding() {
let img = Image::from_vec(3, 1, vec![Indexed8(0), Indexed8(1), Indexed8(2)]).unwrap();
let palette = [
Srgba8::new(255, 0, 0, 255),
Srgba8::new(0, 255, 0, 255),
Srgba8::new(0, 0, 255, 255),
];
let bytes = encode_indexed(&img, &palette, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: dec_img, .. } => {
assert_eq!(dec_img.pixel_at(0, 0).0, 0);
assert_eq!(dec_img.pixel_at(1, 0).0, 1);
assert_eq!(dec_img.pixel_at(2, 0).0, 2);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn encode_indexed_multi_row() {
let img = Image::from_vec(
2,
3,
vec![
Indexed8(0),
Indexed8(1),
Indexed8(1),
Indexed8(0),
Indexed8(0),
Indexed8(0),
],
)
.unwrap();
let palette = [Srgba8::new(10, 20, 30, 255), Srgba8::new(40, 50, 60, 255)];
let bytes = encode_indexed(&img, &palette, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: dec_img, .. } => {
assert_eq!(dec_img.pixel_at(0, 0).0, 0);
assert_eq!(dec_img.pixel_at(1, 0).0, 1);
assert_eq!(dec_img.pixel_at(0, 1).0, 1);
assert_eq!(dec_img.pixel_at(1, 1).0, 0);
assert_eq!(dec_img.pixel_at(0, 2).0, 0);
assert_eq!(dec_img.pixel_at(1, 2).0, 0);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn encode_writer_io_error() {
struct FailWriter;
impl std::io::Write for FailWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("fail"))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other("fail"))
}
}
let img = Image::fill(1, 1, Srgb8::new(0, 0, 0));
let result = encode_writer(&img, &mut FailWriter, &BmpEncodeOptions::default());
assert!(matches!(result, Err(IoError::EncodeFailed { .. })));
}
#[test]
fn le_writer_roundtrip() {
let mut buf = [0u8; 10];
write_u16_le(&mut buf, 0, 0xBEEF);
assert_eq!(read_u16_le(&buf, 0), Some(0xBEEF));
write_u32_le(&mut buf, 2, 0xDEADBEEF);
assert_eq!(read_u32_le(&buf, 2), Some(0xDEADBEEF));
write_i32_le(&mut buf, 6, -42);
assert_eq!(read_i32_le(&buf, 6), Some(-42));
}
#[test]
fn v5_profile_embedded_zero_size() {
let width = 1u32;
let height = 1u32;
let header_size = 124u32;
let pixel_offset = 14 + header_size;
let stride = row_stride(width, 24);
let file_size = pixel_offset as usize + stride;
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset);
write_u32_le(&mut data, 14, header_size);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 24);
write_u32_le(&mut data, 30, 0);
write_u32_le(&mut data, 70, LCS_PROFILE_EMBEDDED);
write_u32_le(&mut data, 14 + 112, 0);
write_u32_le(&mut data, 14 + 116, 0);
let decoded = decode(&data).unwrap();
assert!(decoded.metadata.icc_profile.is_none());
}
#[test]
fn rle8_pixel_offset_beyond_file() {
let color_table_size = 4;
let pixel_offset = 14 + 40 + color_table_size;
let file_len = pixel_offset - 1;
let mut data = vec![0u8; file_len];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_len as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, 1);
write_i32_le(&mut data, 22, 1);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 30, BI_RLE8);
write_u32_le(&mut data, 46, 1);
if data.len() >= 58 {
data[54] = 0;
data[55] = 0;
data[56] = 0;
data[57] = 0;
}
let result = decode(&data);
assert!(result.is_err());
}
#[test]
fn extract_channel_more_than_8_bits() {
let info = MaskInfo { shift: 0, bits: 10 };
assert_eq!(extract_channel(1023, info), 255);
assert_eq!(extract_channel(512, info), 128);
assert_eq!(extract_channel(0, info), 0);
}
#[test]
fn mask_info_zero() {
let info = mask_info(0);
assert_eq!(info.shift, 0);
assert_eq!(info.bits, 0);
}
#[test]
fn parse_bitfield_masks_no_alpha_dword_direct() {
let mut buf = [0u8; 12];
write_u32_le(&mut buf, 0, 0xF800);
write_u32_le(&mut buf, 4, 0x07E0);
write_u32_le(&mut buf, 8, 0x001F);
let masks = parse_bitfield_masks(&buf, 0).unwrap();
assert_eq!(masks.r, 0xF800);
assert_eq!(masks.g, 0x07E0);
assert_eq!(masks.b, 0x001F);
assert_eq!(masks.a, 0); }
#[test]
fn rle8_data_ends_after_first_byte() {
let data_seq: &[u8] = &[5];
let result = decompress_rle8(data_seq, 4, 1).unwrap();
assert_eq!(result, vec![0; 4]);
}
#[test]
fn rle4_data_ends_after_first_byte() {
let data_seq: &[u8] = &[5];
let result = decompress_rle4(data_seq, 4, 1).unwrap();
assert_eq!(result, vec![0; 4]);
}
#[test]
fn rle4_absolute_mode_exceeds_bounds_in_nibbles() {
let data_seq: &[u8] = &[
2, 0xAB, 0, 0, 0, 3, 0x12, 0x30, 0, 1,
];
let result = decompress_rle4(data_seq, 2, 1);
assert!(result.is_err());
}
#[test]
fn decode_rle8_colors_used_zero() {
let width = 2u32;
let height = 1u32;
let palette_count = 256;
let color_table_size = palette_count * 4;
let pixel_offset = 14 + 40 + color_table_size;
let rle_data: &[u8] = &[2, 0, 0, 1];
let file_size = pixel_offset + rle_data.len();
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 8);
write_u32_le(&mut data, 30, BI_RLE8);
write_u32_le(&mut data, 46, 0);
for i in 0..palette_count {
let off = 54 + i * 4;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
data[off + 3] = 0;
}
data[pixel_offset..pixel_offset + rle_data.len()].copy_from_slice(rle_data);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 0);
assert_eq!(img.pixel_at(1, 0).0, 0);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn decode_rle4_colors_used_zero() {
let width = 2u32;
let height = 1u32;
let palette_count = 16;
let color_table_size = palette_count * 4;
let pixel_offset = 14 + 40 + color_table_size;
let rle_data: &[u8] = &[2, 0x01, 0, 1];
let file_size = pixel_offset + rle_data.len();
let mut data = vec![0u8; file_size];
data[0] = 0x42;
data[1] = 0x4D;
write_u32_le(&mut data, 2, file_size as u32);
write_u32_le(&mut data, 10, pixel_offset as u32);
write_u32_le(&mut data, 14, 40);
write_i32_le(&mut data, 18, width as i32);
write_i32_le(&mut data, 22, height as i32);
write_u16_le(&mut data, 26, 1);
write_u16_le(&mut data, 28, 4);
write_u32_le(&mut data, 30, BI_RLE4);
write_u32_le(&mut data, 46, 0);
for i in 0..palette_count {
let off = 54 + i * 4;
data[off] = 0;
data[off + 1] = 0;
data[off + 2] = 0;
data[off + 3] = 0;
}
data[pixel_offset..pixel_offset + rle_data.len()].copy_from_slice(rle_data);
let decoded = decode(&data).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.pixel_at(0, 0).0, 0);
assert_eq!(img.pixel_at(1, 0).0, 1);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn rle4_pixel_offset_beyond_file() {
let mut bmp = vec![0u8; 14 + 40];
bmp[0] = 0x42;
bmp[1] = 0x4D;
let file_size = bmp.len() as u32;
write_u32_le(&mut bmp, 2, file_size);
write_u32_le(&mut bmp, 10, 9999); write_u32_le(&mut bmp, 14, 40);
write_i32_le(&mut bmp, 18, 2); write_i32_le(&mut bmp, 22, 2); write_u16_le(&mut bmp, 26, 1); write_u16_le(&mut bmp, 28, 4); write_u32_le(&mut bmp, 30, BI_RLE4); bmp.extend(vec![0u8; 16 * 4]);
let result = decode(&bmp);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("offset") || msg.contains("truncated") || msg.contains("beyond"),
"unexpected error: {msg}"
);
}
#[test]
fn determine_color_space_v4_lcs_windows() {
let mut bmp = vec![0u8; 14 + 108];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 108);
write_i32_le(&mut bmp, 18, 1); write_i32_le(&mut bmp, 22, 1); write_u16_le(&mut bmp, 26, 1); write_u16_le(&mut bmp, 28, 24); write_u32_le(&mut bmp, 30, BI_RGB);
write_u32_le(&mut bmp, 70, 0x57696E20); let pixel_offset = 14 + 108;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
write_u32_le(&mut bmp, 2, (pixel_offset + 4) as u32); bmp.extend(&[0u8; 4]);
let decoded = decode(&bmp).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::V4);
}
#[test]
fn determine_color_space_v4_lcs_srgb_explicit() {
let mut bmp = vec![0u8; 14 + 108];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 108);
write_i32_le(&mut bmp, 18, 1);
write_i32_le(&mut bmp, 22, 1);
write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 24);
write_u32_le(&mut bmp, 30, BI_RGB);
write_u32_le(&mut bmp, 70, 0x73524742); let pixel_offset = 14 + 108;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
write_u32_le(&mut bmp, 2, (pixel_offset + 4) as u32);
bmp.extend(&[0u8; 4]);
let decoded = decode(&bmp).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
}
#[test]
fn decode_16bit_pixel_data_truncated() {
let mut bmp = vec![0u8; 14 + 40];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 40);
write_i32_le(&mut bmp, 18, 4); write_i32_le(&mut bmp, 22, 1); write_u16_le(&mut bmp, 26, 1); write_u16_le(&mut bmp, 28, 16); write_u32_le(&mut bmp, 30, BI_RGB);
let pixel_offset = 14 + 40;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
write_u32_le(&mut bmp, 2, (pixel_offset + 2) as u32); bmp.extend(&[0u8; 2]); let result = decode(&bmp);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("truncated") || msg.contains("decode failed"),
"unexpected error: {msg}"
);
}
#[test]
fn decode_32bit_pixel_data_truncated() {
let mut bmp = vec![0u8; 14 + 40];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 40);
write_i32_le(&mut bmp, 18, 2); write_i32_le(&mut bmp, 22, 1); write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 32); write_u32_le(&mut bmp, 30, BI_RGB);
let pixel_offset = 14 + 40;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
write_u32_le(&mut bmp, 2, (pixel_offset + 4) as u32); bmp.extend(&[0u8; 4]); let result = decode(&bmp);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("truncated") || msg.contains("decode failed"),
"unexpected error: {msg}"
);
}
#[test]
fn decode_top_down_rle8() {
let mut bmp = vec![0u8; 14 + 40];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 40);
write_i32_le(&mut bmp, 18, 2); write_i32_le(&mut bmp, 22, -2); write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 8); write_u32_le(&mut bmp, 30, BI_RLE8);
bmp.extend(&[255, 0, 0, 0]); bmp.extend(&[0, 255, 0, 0]); let pixel_offset = bmp.len();
bmp.push(2);
bmp.push(0);
bmp.push(0);
bmp.push(0);
bmp.push(2);
bmp.push(1);
bmp.push(0);
bmp.push(1);
write_u32_le(&mut bmp, 10, pixel_offset as u32);
let file_size = bmp.len() as u32;
write_u32_le(&mut bmp, 2, file_size);
write_u32_le(&mut bmp, 46, 2); let decoded = decode(&bmp).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
assert_eq!(img.pixel_at(0, 0).0, 0);
assert_eq!(img.pixel_at(1, 0).0, 0);
assert_eq!(img.pixel_at(0, 1).0, 1);
assert_eq!(img.pixel_at(1, 1).0, 1);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn decode_top_down_rle4() {
let mut bmp = vec![0u8; 14 + 40];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 40);
write_i32_le(&mut bmp, 18, 2); write_i32_le(&mut bmp, 22, -2); write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 4); write_u32_le(&mut bmp, 30, BI_RLE4);
let mut ct = vec![0u8; 16 * 4];
ct[0] = 255; ct[4 + 1] = 255; bmp.extend_from_slice(&ct);
let pixel_offset = bmp.len();
bmp.push(2);
bmp.push(0x00);
bmp.push(0);
bmp.push(0);
bmp.push(2);
bmp.push(0x11);
bmp.push(0);
bmp.push(1);
write_u32_le(&mut bmp, 10, pixel_offset as u32);
let file_size = bmp.len() as u32;
write_u32_le(&mut bmp, 2, file_size);
let decoded = decode(&bmp).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: img, .. } => {
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn encode_indexed_writer_io_error() {
struct FailWriter;
impl std::io::Write for FailWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "boom"))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "boom"))
}
}
let img = Image::fill(1, 1, Indexed8(0));
let palette = [Srgba8::new(0, 0, 0, 255)];
let result = encode_indexed_writer(
&img,
&palette,
&mut FailWriter,
&BmpEncodeOptions::default(),
);
assert!(result.is_err());
}
#[test]
fn encode_indexed_width_multiple_of_4_no_padding() {
let img = Image::from_vec(
4,
2,
vec![
Indexed8(0),
Indexed8(1),
Indexed8(2),
Indexed8(3),
Indexed8(3),
Indexed8(2),
Indexed8(1),
Indexed8(0),
],
)
.unwrap();
let palette = [
Srgba8::new(10, 20, 30, 255),
Srgba8::new(40, 50, 60, 255),
Srgba8::new(70, 80, 90, 255),
Srgba8::new(100, 110, 120, 255),
];
let bytes = encode_indexed(&img, &palette, &BmpEncodeOptions::default()).unwrap();
let decoded = decode(&bytes).unwrap();
match &decoded.image {
BmpImage::Indexed8 { data: dec_img, .. } => {
assert_eq!(dec_img.width(), 4);
assert_eq!(dec_img.height(), 2);
assert_eq!(dec_img.pixel_at(0, 0).0, 0);
assert_eq!(dec_img.pixel_at(3, 0).0, 3);
assert_eq!(dec_img.pixel_at(0, 1).0, 3);
assert_eq!(dec_img.pixel_at(3, 1).0, 0);
}
other => panic!("expected Indexed8, got {:?}", other),
}
}
#[test]
fn decode_24bit_truncated_pixel_data() {
let mut bmp = vec![0u8; 14 + 40];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 40);
write_i32_le(&mut bmp, 18, 4); write_i32_le(&mut bmp, 22, 2); write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 24); write_u32_le(&mut bmp, 30, BI_RGB);
let pixel_offset = 14 + 40;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
bmp.extend(&[0u8; 6]);
let file_size = bmp.len() as u32;
write_u32_le(&mut bmp, 2, file_size);
let result = decode(&bmp);
assert!(result.is_err());
}
#[test]
fn determine_color_space_v4_lcs_calibrated_rgb() {
let mut bmp = vec![0u8; 14 + 108];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 108);
write_i32_le(&mut bmp, 18, 1);
write_i32_le(&mut bmp, 22, 1);
write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 24);
write_u32_le(&mut bmp, 30, BI_RGB);
write_u32_le(&mut bmp, 70, 0x00000000);
let pixel_offset = 14 + 108;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
let file_size = (pixel_offset + 4) as u32;
write_u32_le(&mut bmp, 2, file_size);
bmp.extend(&[0u8; 4]);
let decoded = decode(&bmp).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::V4);
}
#[test]
fn v5_header_lcs_windows_color_space() {
let mut bmp = vec![0u8; 14 + 124];
bmp[0] = 0x42;
bmp[1] = 0x4D;
write_u32_le(&mut bmp, 14, 124);
write_i32_le(&mut bmp, 18, 1);
write_i32_le(&mut bmp, 22, 1);
write_u16_le(&mut bmp, 26, 1);
write_u16_le(&mut bmp, 28, 24);
write_u32_le(&mut bmp, 30, BI_RGB);
write_u32_le(&mut bmp, 70, 0x57696E20); let pixel_offset = 14 + 124;
write_u32_le(&mut bmp, 10, pixel_offset as u32);
bmp.extend(&[0u8; 4]); let file_size = bmp.len() as u32;
write_u32_le(&mut bmp, 2, file_size);
let decoded = decode(&bmp).unwrap();
assert_eq!(decoded.metadata.color_space, BmpColorSpace::Srgb);
assert_eq!(decoded.metadata.header_version, BmpHeaderVersion::V5);
}
}