use crate::core::{ImageFormat, Pix, PixelDepth, pixel};
use crate::io::{IoError, IoResult, header::ImageHeader};
use std::io::{Read, Write};
const BMP_FILE_HEADER_SIZE: usize = 14;
pub fn read_header_bmp(data: &[u8]) -> IoResult<ImageHeader> {
if data.len() < BMP_FILE_HEADER_SIZE + 40 {
return Err(IoError::InvalidData("BMP data too short".to_string()));
}
if &data[0..2] != b"BM" {
return Err(IoError::InvalidData("not a BMP file".to_string()));
}
let info = &data[BMP_FILE_HEADER_SIZE..];
let width = i32::from_le_bytes(info[4..8].try_into().unwrap()).unsigned_abs();
let height = i32::from_le_bytes(info[8..12].try_into().unwrap()).unsigned_abs();
let bits_per_pixel = u16::from_le_bytes(info[14..16].try_into().unwrap());
let x_ppm = i32::from_le_bytes(info[24..28].try_into().unwrap());
let y_ppm = i32::from_le_bytes(info[28..32].try_into().unwrap());
let x_dpi = if x_ppm > 0 {
Some((x_ppm as f32 * 0.0254).round() as u32)
} else {
None
};
let y_dpi = if y_ppm > 0 {
Some((y_ppm as f32 * 0.0254).round() as u32)
} else {
None
};
let (depth, spp, has_colormap, num_colors) = match bits_per_pixel {
1 => (1u32, 1u32, true, 2u32),
4 => (4, 1, true, 16),
8 => (8, 1, true, 256),
24 => (32, 3, false, 0),
32 => (32, 4, false, 0),
_ => {
return Err(IoError::UnsupportedFormat(format!(
"unsupported BMP bit depth: {}",
bits_per_pixel
)));
}
};
Ok(ImageHeader {
width,
height,
depth,
bps: bits_per_pixel.min(8) as u32,
spp,
has_colormap,
num_colors,
format: ImageFormat::Bmp,
x_resolution: x_dpi,
y_resolution: y_dpi,
})
}
const BMP_INFO_HEADER_SIZE: u32 = 40;
pub fn read_bmp<R: Read>(mut reader: R) -> IoResult<Pix> {
let mut file_header = [0u8; BMP_FILE_HEADER_SIZE];
reader.read_exact(&mut file_header).map_err(IoError::Io)?;
if &file_header[0..2] != b"BM" {
return Err(IoError::InvalidData("not a BMP file".to_string()));
}
let pixel_offset = u32::from_le_bytes([
file_header[10],
file_header[11],
file_header[12],
file_header[13],
]) as usize;
let mut info_header = [0u8; 40];
reader.read_exact(&mut info_header).map_err(IoError::Io)?;
let header_size = u32::from_le_bytes([
info_header[0],
info_header[1],
info_header[2],
info_header[3],
]);
if header_size < BMP_INFO_HEADER_SIZE {
return Err(IoError::InvalidData(format!(
"unsupported BMP header size: {}",
header_size
)));
}
let width = i32::from_le_bytes([
info_header[4],
info_header[5],
info_header[6],
info_header[7],
]);
let height = i32::from_le_bytes([
info_header[8],
info_header[9],
info_header[10],
info_header[11],
]);
let planes = u16::from_le_bytes([info_header[12], info_header[13]]);
if planes != 1 {
return Err(IoError::InvalidData(format!(
"unsupported number of planes: {}",
planes
)));
}
let bits_per_pixel = u16::from_le_bytes([info_header[14], info_header[15]]);
let compression = u32::from_le_bytes([
info_header[16],
info_header[17],
info_header[18],
info_header[19],
]);
if compression != 0 && compression != 3 {
return Err(IoError::UnsupportedFormat(format!(
"unsupported BMP compression: {}",
compression
)));
}
let width = width.unsigned_abs();
let top_down = height < 0;
let height = height.unsigned_abs();
let depth = match bits_per_pixel {
1 => PixelDepth::Bit1,
4 => PixelDepth::Bit4,
8 => PixelDepth::Bit8,
24 | 32 => PixelDepth::Bit32,
_ => {
return Err(IoError::UnsupportedFormat(format!(
"unsupported BMP bit depth: {}",
bits_per_pixel
)));
}
};
let colormap = if bits_per_pixel <= 8 {
let num_colors = 1usize << bits_per_pixel;
let bytes_to_skip = header_size as usize - 40;
if bytes_to_skip > 0 {
let mut skip = vec![0u8; bytes_to_skip];
reader.read_exact(&mut skip).map_err(IoError::Io)?;
}
let mut palette = vec![0u8; num_colors * 4];
reader.read_exact(&mut palette).map_err(IoError::Io)?;
let mut cmap = crate::core::PixColormap::new(bits_per_pixel as u32)?;
for i in 0..num_colors {
let b = palette[i * 4];
let g = palette[i * 4 + 1];
let r = palette[i * 4 + 2];
cmap.add_rgb(r, g, b)?;
}
Some(cmap)
} else {
None
};
let current_pos =
BMP_FILE_HEADER_SIZE + header_size as usize + colormap.as_ref().map_or(0, |c| c.len() * 4);
if pixel_offset > current_pos {
let skip_bytes = pixel_offset - current_pos;
let mut skip = vec![0u8; skip_bytes];
reader.read_exact(&mut skip).map_err(IoError::Io)?;
}
let pix = Pix::new(width, height, depth)?;
let mut pix_mut = pix.try_into_mut().unwrap();
if let Some(cmap) = colormap {
pix_mut.set_colormap(Some(cmap))?;
}
let row_stride = (width as usize * bits_per_pixel as usize).div_ceil(32) * 4;
let mut row_buffer = vec![0u8; row_stride];
for row in 0..height {
reader.read_exact(&mut row_buffer).map_err(IoError::Io)?;
let y = if top_down { row } else { height - 1 - row };
match bits_per_pixel {
1 => {
for x in 0..width {
let byte_idx = (x / 8) as usize;
let bit_idx = 7 - (x % 8);
let val = (row_buffer[byte_idx] >> bit_idx) & 1;
pix_mut.set_pixel_unchecked(x, y, val as u32);
}
}
4 => {
for x in 0..width {
let byte_idx = (x / 2) as usize;
let val = if x % 2 == 0 {
(row_buffer[byte_idx] >> 4) & 0xF
} else {
row_buffer[byte_idx] & 0xF
};
pix_mut.set_pixel_unchecked(x, y, val as u32);
}
}
8 => {
for x in 0..width {
let val = row_buffer[x as usize];
pix_mut.set_pixel_unchecked(x, y, val as u32);
}
}
24 => {
for x in 0..width {
let idx = (x as usize) * 3;
let b = row_buffer[idx];
let g = row_buffer[idx + 1];
let r = row_buffer[idx + 2];
let pixel = pixel::compose_rgb(r, g, b);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
32 => {
for x in 0..width {
let idx = (x as usize) * 4;
let b = row_buffer[idx];
let g = row_buffer[idx + 1];
let r = row_buffer[idx + 2];
let a = row_buffer[idx + 3];
let pixel = pixel::compose_rgba(r, g, b, a);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
_ => unreachable!(),
}
}
Ok(pix_mut.into())
}
pub fn write_bmp<W: Write>(pix: &Pix, mut writer: W) -> IoResult<()> {
let width = pix.width();
let height = pix.height();
let depth = pix.depth();
let (bits_per_pixel, has_colormap): (u16, bool) = match depth {
PixelDepth::Bit1 => (1, pix.has_colormap()),
PixelDepth::Bit4 => (4, pix.has_colormap()),
PixelDepth::Bit8 => (8, true), PixelDepth::Bit32 => (24, false), _ => {
return Err(IoError::UnsupportedFormat(format!(
"cannot write {:?} as BMP",
depth
)));
}
};
let row_stride = (width as usize * bits_per_pixel as usize).div_ceil(32) * 4;
let pixel_data_size = row_stride * height as usize;
let colormap_size = if has_colormap {
(1usize << bits_per_pixel) * 4
} else {
0
};
let pixel_offset = BMP_FILE_HEADER_SIZE + BMP_INFO_HEADER_SIZE as usize + colormap_size;
let file_size = pixel_offset + pixel_data_size;
writer.write_all(b"BM").map_err(IoError::Io)?;
writer
.write_all(&(file_size as u32).to_le_bytes())
.map_err(IoError::Io)?;
writer.write_all(&[0u8; 4]).map_err(IoError::Io)?; writer
.write_all(&(pixel_offset as u32).to_le_bytes())
.map_err(IoError::Io)?;
writer
.write_all(&BMP_INFO_HEADER_SIZE.to_le_bytes())
.map_err(IoError::Io)?;
writer
.write_all(&(width as i32).to_le_bytes())
.map_err(IoError::Io)?;
writer
.write_all(&(height as i32).to_le_bytes())
.map_err(IoError::Io)?; writer.write_all(&1u16.to_le_bytes()).map_err(IoError::Io)?; writer
.write_all(&bits_per_pixel.to_le_bytes())
.map_err(IoError::Io)?;
writer.write_all(&0u32.to_le_bytes()).map_err(IoError::Io)?; writer
.write_all(&(pixel_data_size as u32).to_le_bytes())
.map_err(IoError::Io)?;
writer.write_all(&0i32.to_le_bytes()).map_err(IoError::Io)?; writer.write_all(&0i32.to_le_bytes()).map_err(IoError::Io)?; writer.write_all(&0u32.to_le_bytes()).map_err(IoError::Io)?; writer.write_all(&0u32.to_le_bytes()).map_err(IoError::Io)?;
if has_colormap {
let num_colors = 1usize << bits_per_pixel;
if let Some(cmap) = pix.colormap() {
for i in 0..num_colors {
let (r, g, b) = cmap.get_rgb(i).unwrap_or((0, 0, 0));
writer.write_all(&[b, g, r, 0]).map_err(IoError::Io)?;
}
} else {
for i in 0..num_colors {
let val = ((i * 255) / (num_colors - 1)) as u8;
writer.write_all(&[val, val, val, 0]).map_err(IoError::Io)?;
}
}
}
let mut row_buffer = vec![0u8; row_stride];
for row in 0..height {
let y = height - 1 - row;
match depth {
PixelDepth::Bit1 => {
row_buffer.fill(0);
for x in 0..width {
if let Some(val) = pix.get_pixel(x, y)
&& val != 0
{
let byte_idx = (x / 8) as usize;
let bit_idx = 7 - (x % 8);
row_buffer[byte_idx] |= 1 << bit_idx;
}
}
}
PixelDepth::Bit4 => {
row_buffer.fill(0);
for x in 0..width {
if let Some(val) = pix.get_pixel(x, y) {
let byte_idx = (x / 2) as usize;
if x % 2 == 0 {
row_buffer[byte_idx] |= ((val & 0xF) as u8) << 4;
} else {
row_buffer[byte_idx] |= (val & 0xF) as u8;
}
}
}
}
PixelDepth::Bit8 => {
for x in 0..width {
row_buffer[x as usize] = pix.get_pixel(x, y).unwrap_or(0) as u8;
}
}
PixelDepth::Bit32 => {
for x in 0..width {
let pixel = pix.get_pixel(x, y).unwrap_or(0);
let (r, g, b) = pixel::extract_rgb(pixel);
let idx = (x as usize) * 3;
row_buffer[idx] = b;
row_buffer[idx + 1] = g;
row_buffer[idx + 2] = r;
}
}
_ => {
return Err(IoError::UnsupportedFormat(format!(
"cannot write {:?} as BMP",
depth
)));
}
}
writer.write_all(&row_buffer).map_err(IoError::Io)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bmp_roundtrip_8bit() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
pix_mut.set_pixel(x, y, (x + y) * 10).unwrap();
}
}
let pix: Pix = pix_mut.into();
let mut buffer = Vec::new();
write_bmp(&pix, &mut buffer).unwrap();
let cursor = std::io::Cursor::new(buffer);
let pix2 = read_bmp(cursor).unwrap();
assert_eq!(pix2.width(), 10);
assert_eq!(pix2.height(), 10);
assert_eq!(pix2.depth(), PixelDepth::Bit8);
for y in 0..10 {
for x in 0..10 {
assert_eq!(pix2.get_pixel(x, y), pix.get_pixel(x, y));
}
}
}
#[test]
fn test_bmp_roundtrip_32bit() {
let pix = Pix::new(5, 5, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
pix_mut.set_rgb(0, 0, 255, 0, 0).unwrap(); pix_mut.set_rgb(1, 1, 0, 255, 0).unwrap(); pix_mut.set_rgb(2, 2, 0, 0, 255).unwrap();
let pix: Pix = pix_mut.into();
let mut buffer = Vec::new();
write_bmp(&pix, &mut buffer).unwrap();
let cursor = std::io::Cursor::new(buffer);
let pix2 = read_bmp(cursor).unwrap();
assert_eq!(pix2.get_rgb(0, 0), Some((255, 0, 0)));
assert_eq!(pix2.get_rgb(1, 1), Some((0, 255, 0)));
assert_eq!(pix2.get_rgb(2, 2), Some((0, 0, 255)));
}
}