const PLACEABLE_MAGIC: [u8; 4] = [0xD7, 0xCD, 0xC6, 0x9A];
const STANDARD_MAGIC: [u8; 4] = [0x01, 0x00, 0x09, 0x00];
const META_DIBBITBLT: u16 = 0x0940; const META_DIBSTRETCHBLT: u16 = 0x0B41; const META_STRETCHDIB: u16 = 0x0F43;
pub(super) fn is_wmf(data: &[u8]) -> bool {
data.len() >= 4 && (data[..4] == PLACEABLE_MAGIC || data[..4] == STANDARD_MAGIC)
}
struct DibBlit<'a> {
dib: &'a [u8],
x: i32,
y: i32,
w: i32,
h: i32,
}
fn collect_dib_blits(data: &[u8]) -> Option<Vec<DibBlit<'_>>> {
let header_skip = if data.starts_with(&PLACEABLE_MAGIC) {
22 + 18 } else if data.starts_with(&STANDARD_MAGIC) {
18
} else {
return None;
};
let mut blits = Vec::new();
let mut i = header_skip;
while i + 6 <= data.len() {
let size_words = u32::from_le_bytes(data[i..i + 4].try_into().ok()?) as usize;
let func = u16::from_le_bytes(data[i + 4..i + 6].try_into().ok()?);
let size_bytes = size_words.checked_mul(2)?;
if size_bytes < 6 || i.checked_add(size_bytes)? > data.len() {
return None;
}
let params_before_dib = match func {
META_DIBBITBLT => 16,
META_DIBSTRETCHBLT => 20,
META_STRETCHDIB => 22,
_ => {
i += size_bytes;
continue;
}
};
let dib_start = i + 6 + params_before_dib;
let dib_end = i + size_bytes;
if dib_start >= dib_end {
return None;
}
let params = &data[i + 6..dib_start];
let i16_at = |off: usize| -> i32 {
i16::from_le_bytes([params[off], params[off + 1]]) as i32
};
let (h, w, y, x) = match func {
META_DIBBITBLT => (i16_at(8), i16_at(10), i16_at(12), i16_at(14)),
META_DIBSTRETCHBLT => (i16_at(12), i16_at(14), i16_at(16), i16_at(18)),
_ => (i16_at(14), i16_at(16), i16_at(18), i16_at(20)),
};
blits.push(DibBlit {
dib: &data[dib_start..dib_end],
x,
y,
w,
h,
});
i += size_bytes;
}
Some(blits)
}
pub(super) fn wmf_to_raster(data: &[u8]) -> Option<Vec<u8>> {
let blits = collect_dib_blits(data)?;
match blits.as_slice() {
[] => None,
[only] => dib_to_bmp(only.dib),
many => composite_blits(many),
}
}
fn composite_blits(blits: &[DibBlit]) -> Option<Vec<u8>> {
let mut decoded: Vec<(image::RgbaImage, &DibBlit)> = Vec::new();
let (mut min_x, mut min_y, mut max_x, mut max_y) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
for b in blits {
if b.w <= 0 || b.h <= 0 {
continue;
}
let bmp = dib_to_bmp(b.dib)?;
let img = image::load_from_memory_with_format(&bmp, image::ImageFormat::Bmp)
.ok()?
.to_rgba8();
min_x = min_x.min(b.x);
min_y = min_y.min(b.y);
max_x = max_x.max(b.x + b.w);
max_y = max_y.max(b.y + b.h);
decoded.push((img, b));
}
if decoded.is_empty() {
return None;
}
let span_w = (max_x - min_x).max(1) as f32;
let span_h = (max_y - min_y).max(1) as f32;
let mut scale = 1.0_f32;
for (img, b) in &decoded {
scale = scale.max(img.width() as f32 / b.w as f32);
scale = scale.max(img.height() as f32 / b.h as f32);
}
scale = scale.clamp(1.0, 4.0);
let canvas_w = ((span_w * scale).round() as u32).clamp(1, 5000);
let canvas_h = ((span_h * scale).round() as u32).clamp(1, 5000);
let mut canvas = image::RgbaImage::from_pixel(canvas_w, canvas_h, image::Rgba([255, 255, 255, 255]));
for (img, b) in &decoded {
let dw = ((b.w as f32 * scale).round() as u32).max(1);
let dh = ((b.h as f32 * scale).round() as u32).max(1);
let resized = image::imageops::resize(img, dw, dh, image::imageops::FilterType::Triangle);
let px = ((b.x - min_x) as f32 * scale).round() as i64;
let py = ((b.y - min_y) as f32 * scale).round() as i64;
image::imageops::replace(&mut canvas, &resized, px, py);
}
let mut out = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(canvas)
.write_to(&mut out, image::ImageFormat::Png)
.ok()?;
Some(out.into_inner())
}
fn dib_to_bmp(dib: &[u8]) -> Option<Vec<u8>> {
if dib.len() < 20 {
return None;
}
let bih_size = u32::from_le_bytes(dib[0..4].try_into().ok()?) as usize;
if bih_size < 12 || bih_size > dib.len() {
return None;
}
let bpp = u16::from_le_bytes(dib[14..16].try_into().ok()?) as usize;
let palette_bytes = if bpp <= 8 { (1usize << bpp) * 4 } else { 0 };
if bih_size + palette_bytes > dib.len() {
return None;
}
let pixel_off = 14 + bih_size + palette_bytes;
let file_size = 14 + dib.len();
let mut bmp = Vec::with_capacity(file_size);
bmp.extend_from_slice(b"BM");
bmp.extend_from_slice(&(file_size as u32).to_le_bytes());
bmp.extend_from_slice(&0u16.to_le_bytes()); bmp.extend_from_slice(&0u16.to_le_bytes()); bmp.extend_from_slice(&(pixel_off as u32).to_le_bytes());
bmp.extend_from_slice(dib);
Some(bmp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_wmf() {
assert!(!is_wmf(b"\x89PNG\r\n\x1a\n"));
assert!(!is_wmf(b""));
assert!(wmf_to_raster(b"\x89PNG\r\n\x1a\n").is_none());
}
#[test]
fn dib_to_bmp_24bpp_roundtrip() {
let mut dib = Vec::new();
dib.extend_from_slice(&40u32.to_le_bytes()); dib.extend_from_slice(&2i32.to_le_bytes()); dib.extend_from_slice(&1i32.to_le_bytes()); dib.extend_from_slice(&1u16.to_le_bytes()); dib.extend_from_slice(&24u16.to_le_bytes()); dib.extend_from_slice(&0u32.to_le_bytes()); dib.extend_from_slice(&8u32.to_le_bytes()); dib.extend_from_slice(&0i32.to_le_bytes()); dib.extend_from_slice(&0i32.to_le_bytes()); dib.extend_from_slice(&0u32.to_le_bytes()); dib.extend_from_slice(&0u32.to_le_bytes()); dib.extend_from_slice(&[0xFF, 0, 0, 0, 0xFF, 0, 0, 0]);
let bmp = dib_to_bmp(&dib).expect("converts");
assert_eq!(&bmp[..2], b"BM");
let pixel_off = u32::from_le_bytes(bmp[10..14].try_into().unwrap());
assert_eq!(pixel_off as usize, 14 + 40); let file_size = u32::from_le_bytes(bmp[2..6].try_into().unwrap());
assert_eq!(file_size as usize, bmp.len());
}
}