use std::{
fs::File,
io::{Cursor, Read},
path::Path,
};
use image::{DynamicImage, ImageFormat, ImageReader};
use super::tone_map::apply_tone_map_if_needed;
const HEAD_LEN: usize = 54;
const MAX_REPAIR_SIZE: u64 = 512 * 1024 * 1024;
pub(crate) fn try_decode_bmp_repaired(path: &Path) -> Option<DynamicImage> {
let mut file = File::open(path).ok()?;
let file_size = file.metadata().ok()?.len();
if file_size < HEAD_LEN as u64 || file_size > MAX_REPAIR_SIZE {
return None;
}
let mut head = [0u8; HEAD_LEN];
file.read_exact(&mut head).ok()?;
if &head[..2] != b"BM" {
return None;
}
let bad_offset = u32::from_le_bytes(head[10..14].try_into().ok()?) as usize;
let dib_size = u32::from_le_bytes(head[14..18].try_into().ok()?);
if !(12..=124).contains(&dib_size) {
return None;
}
let (width, height, bpp, compression) = if dib_size == 12 {
(
u16::from_le_bytes(head[18..20].try_into().ok()?) as i32,
u16::from_le_bytes(head[20..22].try_into().ok()?) as i32,
u16::from_le_bytes(head[24..26].try_into().ok()?),
0u32,
)
} else {
(
i32::from_le_bytes(head[18..22].try_into().ok()?),
i32::from_le_bytes(head[22..26].try_into().ok()?),
u16::from_le_bytes(head[28..30].try_into().ok()?),
u32::from_le_bytes(head[30..34].try_into().ok()?),
)
};
if width <= 0 || height == 0 || width > 0xFFFF || height.unsigned_abs() > 0xFFFF {
return None;
}
let pixel_size = match compression {
0 | 3 => {
let row = (width as usize * bpp as usize).div_ceil(32) * 4;
row.checked_mul(height.unsigned_abs() as usize)?
}
_ => return None,
};
if bad_offset
.checked_add(pixel_size)
.is_some_and(|end| end <= file_size as usize)
{
return None;
}
let header_end = 14 + dib_size as usize;
let mut candidates = vec![
header_end + palette_size(dib_size, bpp, compression, &head),
54,
header_end,
];
candidates.sort_unstable();
candidates.dedup();
let mut data = std::fs::read(path).ok()?;
for off in candidates {
if off < header_end {
continue;
}
match off.checked_add(pixel_size) {
Some(end) if end <= data.len() => {}
_ => continue,
}
data[10..14].copy_from_slice(&(off as u32).to_le_bytes());
if let Ok(img) = ImageReader::with_format(Cursor::new(&data), ImageFormat::Bmp).decode() {
return Some(apply_tone_map_if_needed(img));
}
}
None
}
fn palette_size(dib_size: u32, bpp: u16, compression: u32, head: &[u8]) -> usize {
let mask_size: usize = if (compression == 3 || compression == 6) && (bpp == 16 || bpp == 32) {
12 } else {
0
};
let (entries, entry_size) = if dib_size == 12 {
(if bpp <= 8 { 1usize << bpp } else { 0 }, 3)
} else if bpp <= 8 {
let clr_used = head
.get(46..50)
.map(|b| u32::from_le_bytes(b.try_into().unwrap()) as usize)
.unwrap_or(0);
(if clr_used == 0 { 1usize << bpp } else { clr_used }, 4)
} else {
(0, 4)
};
mask_size.saturating_add(entries.saturating_mul(entry_size))
}
#[cfg(test)]
mod tests {
use super::*;
use image::{DynamicImage, Rgb, RgbImage};
#[test]
fn repairs_corrupted_pixel_offset() {
let mut img = RgbImage::new(33, 21);
for (x, y, px) in img.enumerate_pixels_mut() {
*px = Rgb([(x * 7 % 256) as u8, (y * 11 % 256) as u8, ((x + y) % 256) as u8]);
}
let mut encoded = Cursor::new(Vec::new());
DynamicImage::ImageRgb8(img.clone())
.write_to(&mut encoded, ImageFormat::Bmp)
.unwrap();
let mut data = encoded.into_inner();
data[10..14].copy_from_slice(&1024u32.to_le_bytes());
let dir = std::env::temp_dir().join("media-decode-bmp-repair-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("corrupted_offset.bmp");
std::fs::write(&path, &data).unwrap();
assert!(
ImageReader::open(&path)
.unwrap()
.with_guessed_format()
.unwrap()
.decode()
.is_err()
);
let repaired = try_decode_bmp_repaired(&path).expect("偏移坏值应可修复");
assert_eq!((repaired.width(), repaired.height()), (33, 21));
assert_eq!(repaired.to_rgb8().as_raw(), img.as_raw());
let _ = std::fs::remove_file(&path);
}
}