use crate::image::format::ExifData;
pub fn extract_exif(data: &[u8]) -> ExifData {
let mut exif = ExifData::default();
if data.len() > 4 && data[0] == 0xFF && data[1] == 0xD8 {
let mut pos = 2;
while pos + 4 < data.len() {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
let seg_len = if pos + 4 <= data.len() {
u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize
} else {
break;
};
if marker == 0xE1 && seg_len >= 8 {
if pos + 10 < data.len() && &data[pos + 4..pos + 10] == b"Exif\x00\x00" {
let tiff_start = pos + 10;
let tiff_len = seg_len - 8;
if let Some(tiff_end) = tiff_start.checked_add(tiff_len) {
if tiff_end <= data.len() {
parse_tiff_exif(&data[tiff_start..tiff_end], &mut exif);
}
}
break;
}
}
if marker == 0xDA || marker == 0xD9 {
break; }
if marker != 0xD0
&& marker != 0xD1
&& marker != 0xD2
&& marker != 0xD3
&& marker != 0xD4
&& marker != 0xD5
&& marker != 0xD6
&& marker != 0xD7
&& marker != 0xD8
{
pos += 2 + seg_len;
} else {
pos += 2;
}
}
}
if data.len() > 8 && (&data[0..4] == b"II\x2a\x00" || &data[0..4] == b"MM\x00\x2a") {
parse_tiff_exif(data, &mut exif);
}
exif
}
fn read_u16_at(data: &[u8], off: usize, little_endian: bool) -> Option<u16> {
let bytes = data.get(off..off.checked_add(2)?)?;
Some(if little_endian {
u16::from_le_bytes([bytes[0], bytes[1]])
} else {
u16::from_be_bytes([bytes[0], bytes[1]])
})
}
fn read_u32_at(data: &[u8], off: usize, little_endian: bool) -> Option<u32> {
let bytes = data.get(off..off.checked_add(4)?)?;
Some(if little_endian {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
} else {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
})
}
fn type_size(field_type: u16) -> Option<usize> {
match field_type {
1 | 2 | 7 => Some(1), 3 => Some(2), 4 | 9 => Some(4), 5 | 10 => Some(8), _ => None,
}
}
fn entry_value<'a>(
data: &'a [u8],
entry: &'a [u8],
field_type: u16,
count: u32,
little_endian: bool,
) -> Option<&'a [u8]> {
let size = type_size(field_type)?;
let byte_len = (count as usize).checked_mul(size)?;
let value_field = entry.get(8..12)?;
if byte_len <= 4 {
value_field.get(0..byte_len)
} else {
let offset = read_u32_at(value_field, 0, little_endian)? as usize;
let end = offset.checked_add(byte_len)?;
data.get(offset..end)
}
}
fn for_each_ifd_entry(
data: &[u8],
ifd_offset: usize,
little_endian: bool,
visit: &mut dyn FnMut(u16, u16, u32, &[u8]),
) {
let Some(count) = read_u16_at(data, ifd_offset, little_endian) else { return };
for i in 0..count as usize {
let entry_off = match ifd_offset
.checked_add(2)
.and_then(|base| i.checked_mul(12).and_then(|step| base.checked_add(step)))
{
Some(off) => off,
None => return,
};
let entry = match data.get(entry_off..entry_off.saturating_add(12)) {
Some(slice) if slice.len() == 12 => slice,
_ => return,
};
let tag = match read_u16_at(entry, 0, little_endian) {
Some(t) => t,
None => return,
};
let field_type = match read_u16_at(entry, 2, little_endian) {
Some(t) => t,
None => return,
};
let count = match read_u32_at(entry, 4, little_endian) {
Some(c) => c,
None => return,
};
if let Some(value) = entry_value(data, entry, field_type, count, little_endian) {
visit(tag, field_type, count, value);
}
}
}
fn read_ascii(value: &[u8]) -> String {
let end = value.iter().position(|&b| b == 0).unwrap_or(value.len());
String::from_utf8_lossy(&value[..end]).to_string()
}
fn value_u16(value: &[u8], little_endian: bool) -> Option<u16> {
read_u16_at(value, 0, little_endian)
}
fn value_u32(value: &[u8], little_endian: bool) -> Option<u32> {
read_u32_at(value, 0, little_endian)
}
fn value_rational(value: &[u8], little_endian: bool) -> Option<f64> {
let numerator = read_u32_at(value, 0, little_endian)?;
let denominator = read_u32_at(value, 4, little_endian)?;
if denominator == 0 {
None
} else {
Some(numerator as f64 / denominator as f64)
}
}
fn apply_known_tag(tag: u16, value: &[u8], little_endian: bool, exif: &mut ExifData) {
match tag {
256 => exif.exif_width = value_u32(value, little_endian),
257 => exif.exif_height = value_u32(value, little_endian),
271 => exif.make = read_ascii(value),
272 => exif.model = read_ascii(value),
274 => exif.orientation = value_u16(value, little_endian).map(|v| v as u8),
306 => exif.date_time = Some(read_ascii(value)),
33434 => exif.exposure_time = value_rational(value, little_endian),
33437 => exif.aperture = value_rational(value, little_endian),
34855 => exif.iso = value_u16(value, little_endian).map(|v| v as u32),
37386 => exif.focal_length = value_rational(value, little_endian),
36867 | 36868 if exif.date_time.is_none() => {
exif.date_time = Some(read_ascii(value));
}
_ => {}
}
}
fn parse_gps_ifd(data: &[u8], ifd_offset: usize, little_endian: bool, exif: &mut ExifData) {
let mut lat_ref: Option<String> = None;
let mut lon_ref: Option<String> = None;
let mut lat: Option<f64> = None;
let mut lon: Option<f64> = None;
for_each_ifd_entry(data, ifd_offset, little_endian, &mut |tag, _t, _c, value| match tag {
1 => lat_ref = Some(read_ascii(value)),
3 => lon_ref = Some(read_ascii(value)),
2 => lat = gps_coordinate(value, little_endian),
4 => lon = gps_coordinate(value, little_endian),
_ => {}
});
exif.gps_latitude =
lat.map(|degrees| if lat_ref.as_deref() == Some("S") { -degrees } else { degrees });
exif.gps_longitude =
lon.map(|degrees| if lon_ref.as_deref() == Some("W") { -degrees } else { degrees });
}
fn gps_coordinate(value: &[u8], little_endian: bool) -> Option<f64> {
if value.len() < 24 {
return None;
}
let degrees = value_rational(value, little_endian)?;
let minutes = value_rational(&value[8..16], little_endian)?;
let seconds = value_rational(&value[16..24], little_endian)?;
Some(degrees + minutes / 60.0 + seconds / 3600.0)
}
fn parse_tiff_exif(data: &[u8], exif: &mut ExifData) {
if data.len() < 8 {
return;
}
let little_endian = &data[0..4] == b"II\x2a\x00";
let big_endian = &data[0..4] == b"MM\x00\x2a";
if !little_endian && !big_endian {
return;
}
let Some(ifd0) = read_u32_at(data, 4, little_endian) else { return };
let ifd0 = ifd0 as usize;
let mut exif_ifd: Option<usize> = None;
let mut gps_ifd: Option<usize> = None;
for_each_ifd_entry(
data,
ifd0,
little_endian,
&mut |tag, _field_type, _count, value| match tag {
34665 => exif_ifd = value_u32(value, little_endian).map(|v| v as usize),
34853 => gps_ifd = value_u32(value, little_endian).map(|v| v as usize),
_ => apply_known_tag(tag, value, little_endian, exif),
},
);
if let Some(offset) = exif_ifd.filter(|&o| o != ifd0) {
for_each_ifd_entry(data, offset, little_endian, &mut |tag, _t, _c, value| {
apply_known_tag(tag, value, little_endian, exif);
});
}
if let Some(offset) = gps_ifd.filter(|&o| o != ifd0) {
parse_gps_ifd(data, offset, little_endian, exif);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_exif_empty() {
let exif = extract_exif(b"");
assert_eq!(exif.make, "");
assert_eq!(exif.model, "");
}
#[test]
fn test_extract_exif_no_exif() {
let data = b"\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xFF\xD9";
let exif = extract_exif(data);
assert_eq!(exif.make, "");
}
#[test]
fn test_extract_exif_from_jpeg_with_app1() {
let mut jpeg = vec![0xFF, 0xD8];
jpeg.push(0xFF);
jpeg.push(0xE1);
let app1_data_len = 8 + 8 + 8; let seg_len: u16 = 2 + app1_data_len as u16;
jpeg.extend_from_slice(&seg_len.to_be_bytes());
jpeg.extend_from_slice(b"Exif\x00\x00");
jpeg.extend_from_slice(b"II\x2a\x00"); jpeg.extend_from_slice(&8u32.to_le_bytes()); jpeg.extend_from_slice(&0u16.to_le_bytes()); jpeg.extend_from_slice(&[0u8; 4]); jpeg.extend_from_slice(&[0xFF, 0xD9]);
let exif = extract_exif(&jpeg);
assert!(exif.make.is_empty());
}
#[test]
fn test_extract_exif_string_without_nul_uses_bounded_field() {
let mut tiff = Vec::new();
tiff.extend_from_slice(b"II\x2a\x00");
tiff.extend_from_slice(&8u32.to_le_bytes());
tiff.extend_from_slice(&1u16.to_le_bytes());
tiff.extend_from_slice(&271u16.to_le_bytes());
tiff.extend_from_slice(&2u16.to_le_bytes());
tiff.extend_from_slice(&32u32.to_le_bytes());
tiff.extend_from_slice(&26u32.to_le_bytes());
tiff.extend_from_slice(&[0u8; 4]);
tiff.extend_from_slice(b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
let exif = extract_exif(&tiff);
assert_eq!(exif.make, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}
#[test]
fn test_extract_exif_rejects_truncated_app1_without_panicking() {
let jpeg = b"\xFF\xD8\xFF\xE1\x00\x20Exif\x00\x00II\x2a\x00\x08\x00";
let exif = extract_exif(jpeg);
assert!(exif.make.is_empty());
assert!(exif.model.is_empty());
}
fn build_tiff_le(entries: &[(u16, u16, u32, Vec<u8>)]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"II\x2a\x00");
out.extend_from_slice(&8u32.to_le_bytes()); out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
let mut data_offset = 8 + 2 + entries.len() * 12 + 4;
for (tag, typ, count, value) in entries {
out.extend_from_slice(&tag.to_le_bytes());
out.extend_from_slice(&typ.to_le_bytes());
out.extend_from_slice(&count.to_le_bytes());
if value.len() <= 4 {
let mut field = [0u8; 4];
field[..value.len()].copy_from_slice(value);
out.extend_from_slice(&field);
} else {
out.extend_from_slice(&(data_offset as u32).to_le_bytes());
data_offset += value.len();
}
}
out.extend_from_slice(&0u32.to_le_bytes()); for (_tag, _typ, _count, value) in entries {
if value.len() > 4 {
out.extend_from_slice(value);
}
}
out
}
#[test]
fn exif_reads_rational_from_the_offset_it_points_to() {
let rational = [50u32.to_le_bytes(), 1u32.to_le_bytes()].concat();
let tiff = build_tiff_le(&[(37386, 5, 1, rational)]);
let exif = extract_exif(&tiff);
assert_eq!(exif.focal_length, Some(50.0));
}
#[test]
fn exif_reads_ascii_by_count_and_inline_values() {
let tiff =
build_tiff_le(&[(271, 2, 6, b"Canon\0".to_vec()), (272, 2, 4, b"ABC\0".to_vec())]);
let exif = extract_exif(&tiff);
assert_eq!(exif.make, "Canon");
assert_eq!(exif.model, "ABC");
}
#[test]
fn exif_traverses_the_exif_sub_ifd() {
let mut tiff = Vec::new();
tiff.extend_from_slice(b"II\x2a\x00");
tiff.extend_from_slice(&8u32.to_le_bytes()); tiff.extend_from_slice(&1u16.to_le_bytes()); tiff.extend_from_slice(&34665u16.to_le_bytes());
tiff.extend_from_slice(&4u16.to_le_bytes());
tiff.extend_from_slice(&1u32.to_le_bytes());
tiff.extend_from_slice(&26u32.to_le_bytes());
tiff.extend_from_slice(&0u32.to_le_bytes());
tiff.extend_from_slice(&1u16.to_le_bytes());
tiff.extend_from_slice(&33434u16.to_le_bytes());
tiff.extend_from_slice(&5u16.to_le_bytes());
tiff.extend_from_slice(&1u32.to_le_bytes());
tiff.extend_from_slice(&44u32.to_le_bytes());
tiff.extend_from_slice(&0u32.to_le_bytes());
tiff.extend_from_slice(&1u32.to_le_bytes());
tiff.extend_from_slice(&125u32.to_le_bytes());
let exif = extract_exif(&tiff);
assert_eq!(exif.exposure_time, Some(1.0 / 125.0));
}
#[test]
fn exif_skips_bad_type_and_truncated_offsets() {
let bad_type = build_tiff_le(&[(271, 0, 4, vec![0; 4])]);
assert_eq!(extract_exif(&bad_type).make, "");
let truncated = build_tiff_le(&[(271, 2, 40, vec![b'A'; 40])]);
let mut cut = truncated;
cut.truncate(cut.len() - 40);
assert_eq!(extract_exif(&cut).make, "");
}
#[test]
fn exif_big_endian_values_are_read_correctly() {
let mut tiff = Vec::new();
tiff.extend_from_slice(b"MM\x00\x2a");
tiff.extend_from_slice(&8u32.to_be_bytes()); tiff.extend_from_slice(&3u16.to_be_bytes());
tiff.extend_from_slice(&274u16.to_be_bytes());
tiff.extend_from_slice(&3u16.to_be_bytes());
tiff.extend_from_slice(&1u32.to_be_bytes());
tiff.extend_from_slice(&[0, 6, 0, 0]);
tiff.extend_from_slice(&256u16.to_be_bytes());
tiff.extend_from_slice(&4u16.to_be_bytes());
tiff.extend_from_slice(&1u32.to_be_bytes());
tiff.extend_from_slice(&1024u32.to_be_bytes());
tiff.extend_from_slice(&37386u16.to_be_bytes());
tiff.extend_from_slice(&5u16.to_be_bytes());
tiff.extend_from_slice(&1u32.to_be_bytes());
tiff.extend_from_slice(&50u32.to_be_bytes());
tiff.extend_from_slice(&0u32.to_be_bytes()); tiff.extend_from_slice(&2u32.to_be_bytes()); tiff.extend_from_slice(&3u32.to_be_bytes());
let exif = extract_exif(&tiff);
assert_eq!(exif.orientation, Some(6));
assert_eq!(exif.exif_width, Some(1024));
assert_eq!(exif.focal_length, Some(2.0 / 3.0));
}
}