#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddedJpeg {
pub start: usize,
pub end: usize,
pub width: u16,
pub height: u16,
pub has_exif: bool,
}
impl EmbeddedJpeg {
pub fn pixels(&self) -> u64 {
self.width as u64 * self.height as u64
}
pub fn long_edge(&self) -> u16 {
self.width.max(self.height)
}
}
pub fn find_jpegs(data: &[u8]) -> Vec<EmbeddedJpeg> {
let mut found = Vec::new();
let mut i = 0;
while i + 3 < data.len() {
let Some(offset) = data[i..].windows(3).position(|w| w == [0xFF, 0xD8, 0xFF]) else {
break;
};
let start = i + offset;
match parse_jpeg(data, start) {
Some(jpeg) => {
i = jpeg.end;
found.push(jpeg);
}
None => i = start + 1,
}
}
found
}
fn u16_be(data: &[u8], at: usize) -> Option<u16> {
Some(u16::from_be_bytes([*data.get(at)?, *data.get(at + 1)?]))
}
fn parse_jpeg(data: &[u8], start: usize) -> Option<EmbeddedJpeg> {
let mut p = start + 2;
let mut size: Option<(u16, u16)> = None;
let mut has_exif = false;
loop {
if *data.get(p)? != 0xFF {
return None;
}
while *data.get(p + 1)? == 0xFF {
p += 1;
}
let marker = *data.get(p + 1)?;
match marker {
0x01 | 0xD0..=0xD7 => {
p += 2;
continue;
}
0xD8 | 0xD9 => return None,
0xC0..=0xFE => {}
_ => return None,
}
let len = u16_be(data, p + 2)? as usize;
if len < 2 {
return None;
}
let segment = data.get(p + 4..p + 2 + len)?;
match marker {
0xC0..=0xC2 => {
if segment.len() < 6 || segment[0] != 8 {
return None;
}
let height = u16::from_be_bytes([segment[1], segment[2]]);
let width = u16::from_be_bytes([segment[3], segment[4]]);
if width == 0 || height == 0 {
return None;
}
size = Some((width, height));
}
0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None,
0xE1 if segment.starts_with(b"Exif\0\0") => has_exif = true,
_ => {}
}
p += 2 + len;
if marker == 0xDA {
size?;
loop {
let offset = data.get(p..)?.iter().position(|&b| b == 0xFF)?;
p += offset;
match *data.get(p + 1)? {
0x00 | 0xD0..=0xD7 => p += 2,
0xFF => p += 1,
0xD9 => {
let (width, height) = size?;
return Some(EmbeddedJpeg {
start,
end: p + 2,
width,
height,
has_exif,
});
}
_ => break,
}
}
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub fn fake_jpeg(sof: u8, width: u16, height: u16, exif: Option<&[u8]>) -> Vec<u8> {
let mut j = vec![0xFF, 0xD8];
if let Some(exif) = exif {
j.extend([0xFF, 0xE1]);
j.extend(((exif.len() + 2) as u16).to_be_bytes());
j.extend(exif);
}
j.extend([0xFF, 0xDB, 0x00, 0x43, 0x00]);
j.extend([1u8; 64]);
j.extend([0xFF, sof, 0x00, 0x0B, 0x08]);
j.extend(height.to_be_bytes());
j.extend(width.to_be_bytes());
j.extend([0x01, 0x01, 0x11, 0x00]);
j.extend([0xFF, 0xC4, 0x00, 0x03, 0x00]);
j.extend([0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00]);
j.extend([0x12, 0xFF, 0x00, 0x34, 0xFF, 0xD0, 0x56]);
j.extend([0xFF, 0xD9]);
j
}
#[test]
fn finds_streams_and_skips_noise() {
let big = fake_jpeg(0xC0, 6000, 4000, None);
let small = fake_jpeg(0xC2, 160, 120, None);
let mut data = b"junk \xFF\xD8\xFF not a jpeg".to_vec();
let big_start = data.len();
data.extend(&big);
data.extend(b"more junk");
let small_start = data.len();
data.extend(&small);
data.extend(b"tail");
let found = find_jpegs(&data);
assert_eq!(found.len(), 2);
assert_eq!(found[0].start, big_start);
assert_eq!(found[0].end, big_start + big.len());
assert_eq!((found[0].width, found[0].height), (6000, 4000));
assert_eq!(found[1].start, small_start);
assert_eq!((found[1].width, found[1].height), (160, 120));
}
#[test]
fn rejects_lossless_sensor_data() {
let data = fake_jpeg(0xC3, 6000, 4000, None);
assert!(find_jpegs(&data).is_empty());
}
#[test]
fn nested_thumbnail_is_not_listed() {
let thumb = fake_jpeg(0xC0, 160, 120, None);
let mut exif = b"Exif\0\0".to_vec();
exif.extend(&thumb);
let data = fake_jpeg(0xC0, 1920, 1280, Some(&exif));
let found = find_jpegs(&data);
assert_eq!(found.len(), 1);
assert_eq!(found[0].width, 1920);
assert!(found[0].has_exif);
}
#[test]
fn truncated_stream_is_ignored() {
let mut data = fake_jpeg(0xC0, 1920, 1280, None);
data.truncate(data.len() - 2);
assert!(find_jpegs(&data).is_empty());
}
}