pub fn peek_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 8 {
return None;
}
if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return parse_png(bytes);
}
if bytes.starts_with(&[0xFF, 0xD8]) {
return parse_jpeg(bytes);
}
if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
return parse_webp(bytes);
}
if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
return parse_gif(bytes);
}
None
}
fn parse_png(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 24 {
return None;
}
if &bytes[12..16] != b"IHDR" {
return None;
}
let w = u32::from_be_bytes(bytes[16..20].try_into().ok()?);
let h = u32::from_be_bytes(bytes[20..24].try_into().ok()?);
Some((w, h))
}
fn parse_jpeg(bytes: &[u8]) -> Option<(u32, u32)> {
let mut i = 2usize;
while i + 9 < bytes.len() {
if bytes[i] != 0xFF {
return None;
}
let marker = bytes[i + 1];
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
let h = u16::from_be_bytes(bytes[i + 5..i + 7].try_into().ok()?);
let w = u16::from_be_bytes(bytes[i + 7..i + 9].try_into().ok()?);
return Some((w as u32, h as u32));
}
let seg_len = u16::from_be_bytes(bytes[i + 2..i + 4].try_into().ok()?) as usize;
i = i.checked_add(2)?.checked_add(seg_len)?;
}
None
}
fn parse_webp(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 30 {
return None;
}
match &bytes[12..16] {
b"VP8X" => {
let w_minus_1 = u32::from_le_bytes([bytes[24], bytes[25], bytes[26], 0]);
let h_minus_1 = u32::from_le_bytes([bytes[27], bytes[28], bytes[29], 0]);
Some((w_minus_1 + 1, h_minus_1 + 1))
}
b"VP8L" => {
if bytes.len() < 25 {
return None;
}
let b1 = bytes[21] as u32;
let b2 = bytes[22] as u32;
let b3 = bytes[23] as u32;
let b4 = bytes[24] as u32;
let w = (b1 | ((b2 & 0x3F) << 8)) + 1;
let h = (((b2 >> 6) & 0x03) | (b3 << 2) | ((b4 & 0x0F) << 10)) + 1;
Some((w, h))
}
b"VP8 " => {
if bytes.len() < 30 {
return None;
}
if bytes[23..26] != [0x9D, 0x01, 0x2A] {
return None;
}
let w = u16::from_le_bytes(bytes[26..28].try_into().ok()?) & 0x3FFF;
let h = u16::from_le_bytes(bytes[28..30].try_into().ok()?) & 0x3FFF;
Some((w as u32, h as u32))
}
_ => None,
}
}
fn parse_gif(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 10 {
return None;
}
let w = u16::from_le_bytes(bytes[6..8].try_into().ok()?);
let h = u16::from_le_bytes(bytes[8..10].try_into().ok()?);
Some((w as u32, h as u32))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn png_1x1() {
let png: [u8; 24] = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H',
b'D', b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
];
assert_eq!(peek_dimensions(&png), Some((1, 1)));
}
#[test]
fn png_200x200() {
let png: [u8; 24] = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H',
b'D', b'R', 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x00, 0xC8,
];
assert_eq!(peek_dimensions(&png), Some((200, 200)));
}
#[test]
fn jpeg_with_sof0() {
let jpeg: [u8; 20] = [
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x04, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x80, 0x00, 0xC0, 0x03, 0x01, 0x22, ];
assert_eq!(peek_dimensions(&jpeg), Some((192, 128)));
}
#[test]
fn gif_400x300() {
let gif: [u8; 10] = [
b'G', b'I', b'F', b'8', b'9', b'a', 0x90, 0x01, 0x2C, 0x01, ];
assert_eq!(peek_dimensions(&gif), Some((400, 300)));
}
#[test]
fn webp_vp8x_500x500() {
let mut webp = vec![0u8; 30];
webp[0..4].copy_from_slice(b"RIFF");
webp[8..12].copy_from_slice(b"WEBP");
webp[12..16].copy_from_slice(b"VP8X");
webp[24] = 0xF3;
webp[25] = 0x01;
webp[26] = 0x00;
webp[27] = 0xF3;
webp[28] = 0x01;
webp[29] = 0x00;
assert_eq!(peek_dimensions(&webp), Some((500, 500)));
}
#[test]
fn unknown_format_returns_none() {
assert_eq!(peek_dimensions(b"not an image"), None);
assert_eq!(peek_dimensions(&[]), None);
}
#[test]
fn truncated_png_header_returns_none() {
let bytes: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(peek_dimensions(&bytes), None);
}
}