#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImageFormat {
Png,
Jpeg,
Gif,
Webp,
}
pub fn format(bytes: &[u8]) -> Option<ImageFormat> {
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
Some(ImageFormat::Png)
} else if bytes.starts_with(&[0xff, 0xd8]) {
Some(ImageFormat::Jpeg)
} else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
Some(ImageFormat::Gif)
} else if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") {
Some(ImageFormat::Webp)
} else {
None
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ImageDimensions {
pub width: u32,
pub height: u32,
}
pub fn dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
match format(bytes)? {
ImageFormat::Png => png_dimensions(bytes),
ImageFormat::Jpeg => jpeg_dimensions(bytes),
ImageFormat::Gif => gif_dimensions(bytes),
ImageFormat::Webp => webp_dimensions(bytes),
}
}
fn image_dimensions(width: u32, height: u32) -> Option<ImageDimensions> {
(width != 0 && height != 0).then_some(ImageDimensions { width, height })
}
fn png_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
if bytes.get(12..16)? != b"IHDR" {
return None;
}
let width = u32::from_be_bytes(bytes.get(16..20)?.try_into().ok()?);
let height = u32::from_be_bytes(bytes.get(20..24)?.try_into().ok()?);
image_dimensions(width, height)
}
fn jpeg_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
let mut at = 2;
while at < bytes.len() {
if bytes[at] != 0xff {
at += 1;
continue;
}
while bytes.get(at) == Some(&0xff) {
at += 1;
}
let marker = *bytes.get(at)?;
at += 1;
match marker {
0x00 => continue,
0xd9 | 0xda => return None,
0x01 | 0xd0..=0xd8 => continue,
_ => {},
}
let length = usize::from(u16::from_be_bytes(bytes.get(at..at + 2)?.try_into().ok()?));
if length < 2 {
return None;
}
let end = at.checked_add(length)?;
if end > bytes.len() {
return None;
}
if matches!(marker, 0xc0..=0xcf) && !matches!(marker, 0xc4 | 0xc8 | 0xcc) {
if length < 7 {
return None;
}
let height = u32::from(u16::from_be_bytes(bytes.get(at + 3..at + 5)?.try_into().ok()?));
let width = u32::from(u16::from_be_bytes(bytes.get(at + 5..at + 7)?.try_into().ok()?));
return image_dimensions(width, height);
}
at = end;
}
None
}
fn gif_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
let width = u32::from(u16::from_le_bytes(bytes.get(6..8)?.try_into().ok()?));
let height = u32::from(u16::from_le_bytes(bytes.get(8..10)?.try_into().ok()?));
image_dimensions(width, height)
}
fn webp_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
if bytes.get(8..12)? != b"WEBP" {
return None;
}
match bytes.get(12..16)? {
b"VP8 " => {
let width = u32::from(u16::from_le_bytes(bytes.get(26..28)?.try_into().ok()?) & 0x3fff);
let height = u32::from(u16::from_le_bytes(bytes.get(28..30)?.try_into().ok()?) & 0x3fff);
image_dimensions(width, height)
},
b"VP8L" => {
let packed = u32::from_le_bytes(bytes.get(21..25)?.try_into().ok()?);
let width = (packed & 0x3fff) + 1;
let height = ((packed >> 14) & 0x3fff) + 1;
image_dimensions(width, height)
},
b"VP8X" => {
let packed = bytes.get(24..30)?;
let width = u32::from(packed[0]) | u32::from(packed[1]) << 8 | u32::from(packed[2]) << 16;
let height = u32::from(packed[3]) | u32::from(packed[4]) << 8 | u32::from(packed[5]) << 16;
image_dimensions(width + 1, height + 1)
},
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_dimensions(bytes: &[u8], width: u32, height: u32) {
assert_eq!(dimensions(bytes), Some(ImageDimensions { width, height }));
}
#[test]
fn sniffs_supported_magic_bytes() {
assert_eq!(format(b"\x89PNG\r\n\x1a\n"), Some(ImageFormat::Png));
assert_eq!(format(&[0xff, 0xd8]), Some(ImageFormat::Jpeg));
assert_eq!(format(b"GIF89a"), Some(ImageFormat::Gif));
assert_eq!(format(b"RIFF\0\0\0\0WEBP"), Some(ImageFormat::Webp));
assert_eq!(format(b"RIFF\0\0\0\0WAVE"), None);
assert_eq!(format(b"garbage"), None);
}
#[test]
fn probes_png_ihdr() {
let mut bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec();
bytes.extend_from_slice(&640_u32.to_be_bytes());
bytes.extend_from_slice(&480_u32.to_be_bytes());
assert_dimensions(&bytes, 640, 480);
}
#[test]
fn probes_jpeg_baseline_and_progressive_sof() {
for marker in [0xc0, 0xc2] {
let bytes = [
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x04, 0, 0, 0xff, marker, 0x00, 0x08, 8, 0x01, 0xe0,
0x02, 0x80, 1,
];
assert_dimensions(&bytes, 640, 480);
}
}
#[test]
fn probes_gif87a_and_gif89a() {
for signature in [b"GIF87a", b"GIF89a"] {
let mut bytes = signature.to_vec();
bytes.extend_from_slice(&320_u16.to_le_bytes());
bytes.extend_from_slice(&200_u16.to_le_bytes());
assert_dimensions(&bytes, 320, 200);
}
}
#[test]
fn probes_webp_vp8() {
let mut bytes = vec![0; 30];
bytes[0..4].copy_from_slice(b"RIFF");
bytes[8..12].copy_from_slice(b"WEBP");
bytes[12..16].copy_from_slice(b"VP8 ");
bytes[26..28].copy_from_slice(&(0xc000_u16 | 0x03e8).to_le_bytes());
bytes[28..30].copy_from_slice(&(0x8000_u16 | 0x02bc).to_le_bytes());
assert_dimensions(&bytes, 1000, 700);
}
#[test]
fn probes_webp_vp8l() {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(b"RIFF");
bytes[8..12].copy_from_slice(b"WEBP");
bytes[12..16].copy_from_slice(b"VP8L");
let packed = (511_u32 - 1) | ((257_u32 - 1) << 14);
bytes[21..25].copy_from_slice(&packed.to_le_bytes());
assert_dimensions(&bytes, 511, 257);
}
#[test]
fn probes_webp_vp8x() {
let mut bytes = vec![0; 30];
bytes[0..4].copy_from_slice(b"RIFF");
bytes[8..12].copy_from_slice(b"WEBP");
bytes[12..16].copy_from_slice(b"VP8X");
bytes[24..27].copy_from_slice(&[0xff, 0x00, 0x01]);
bytes[27..30].copy_from_slice(&[0x01, 0x02, 0x03]);
assert_dimensions(&bytes, 65_792, 197_122);
}
#[test]
fn rejects_truncated_headers() {
let headers: &[&[u8]] = &[
b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0",
&[0xff, 0xd8, 0xff, 0xc0, 0, 8, 8],
b"GIF89a\x01",
b"RIFF\0\0\0\0WEBPVP8 ",
b"RIFF\0\0\0\0WEBPVP8L",
b"RIFF\0\0\0\0WEBPVP8X",
];
for header in headers {
assert_eq!(dimensions(header), None, "accepted truncated header: {header:?}");
}
}
}