use zenpixels::Orientation;
const TAG_ORIENTATION: u16 = 0x0112;
const TIFF_SHORT: u16 = 3;
const TIFF_LONG: u16 = 4;
const MAX_IFD_ENTRIES: u16 = 1000;
const TIFF_HEADER_SIZE: usize = 8;
pub fn parse_exif_orientation(data: &[u8]) -> Option<Orientation> {
let tiff = if data.len() >= 6 && data[..6] == *b"Exif\0\0" {
&data[6..]
} else {
data
};
if tiff.len() < TIFF_HEADER_SIZE {
return None;
}
let be = match [tiff[0], tiff[1]] {
[b'M', b'M'] => true, [b'I', b'I'] => false, _ => return None,
};
if rd16(tiff, 2, be)? != 42 {
return None;
}
let ifd0 = rd32(tiff, 4, be)? as usize;
let entry_count = rd16(tiff, ifd0, be)?;
if entry_count > MAX_IFD_ENTRIES {
return None;
}
let entries_start = ifd0.checked_add(2)?;
for i in 0..entry_count as usize {
let off = entries_start.checked_add(i.checked_mul(12)?)?;
if off.checked_add(12)? > tiff.len() {
break;
}
let tag = rd16(tiff, off, be)?;
if tag > TAG_ORIENTATION {
break;
}
if tag != TAG_ORIENTATION {
continue;
}
let type_id = rd16(tiff, off + 2, be)?;
let count = rd32(tiff, off + 4, be)?;
if count < 1 {
return None;
}
let raw = match type_id {
TIFF_SHORT => rd16(tiff, off + 8, be)? as u32,
TIFF_LONG => rd32(tiff, off + 8, be)?,
_ => return None,
};
if raw > 8 {
return None;
}
return Orientation::from_exif(raw as u8);
}
None
}
fn rd16(data: &[u8], offset: usize, big_endian: bool) -> Option<u16> {
let b = data.get(offset..offset + 2)?;
Some(if big_endian {
u16::from_be_bytes([b[0], b[1]])
} else {
u16::from_le_bytes([b[0], b[1]])
})
}
fn rd32(data: &[u8], offset: usize, big_endian: bool) -> Option<u32> {
let b = data.get(offset..offset + 4)?;
Some(if big_endian {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
} else {
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
})
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
fn make_tiff(big_endian: bool, orientation: u16, type_id: u16) -> Vec<u8> {
let mut buf = Vec::new();
let w16 = |buf: &mut Vec<u8>, v: u16| {
if big_endian {
buf.extend_from_slice(&v.to_be_bytes());
} else {
buf.extend_from_slice(&v.to_le_bytes());
}
};
let w32 = |buf: &mut Vec<u8>, v: u32| {
if big_endian {
buf.extend_from_slice(&v.to_be_bytes());
} else {
buf.extend_from_slice(&v.to_le_bytes());
}
};
if big_endian {
buf.extend_from_slice(b"MM");
} else {
buf.extend_from_slice(b"II");
}
w16(&mut buf, 42); w32(&mut buf, 8);
w16(&mut buf, 1); w16(&mut buf, TAG_ORIENTATION); w16(&mut buf, type_id); w32(&mut buf, 1); if type_id == TIFF_LONG {
w32(&mut buf, orientation as u32); } else {
w16(&mut buf, orientation); w16(&mut buf, 0); }
buf
}
#[test]
fn identity_little_endian() {
let tiff = make_tiff(false, 1, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), Some(Orientation::Identity));
}
#[test]
fn rotate90_big_endian() {
let tiff = make_tiff(true, 6, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), Some(Orientation::Rotate90));
}
#[test]
fn all_orientations() {
let expected = [
(1, Orientation::Identity),
(2, Orientation::FlipH),
(3, Orientation::Rotate180),
(4, Orientation::FlipV),
(5, Orientation::Transpose),
(6, Orientation::Rotate90),
(7, Orientation::Transverse),
(8, Orientation::Rotate270),
];
for (val, orient) in expected {
let tiff = make_tiff(false, val, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), Some(orient), "value={val}");
}
}
#[test]
fn with_exif_prefix() {
let tiff = make_tiff(false, 6, TIFF_SHORT);
let mut app1 = b"Exif\0\0".to_vec();
app1.extend_from_slice(&tiff);
assert_eq!(parse_exif_orientation(&app1), Some(Orientation::Rotate90));
}
#[test]
fn raw_tiff_without_prefix() {
let tiff = make_tiff(true, 3, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), Some(Orientation::Rotate180));
}
#[test]
fn orientation_as_long() {
let tiff = make_tiff(false, 8, TIFF_LONG);
assert_eq!(parse_exif_orientation(&tiff), Some(Orientation::Rotate270));
}
#[test]
fn orientation_as_long_big_endian() {
let tiff = make_tiff(true, 5, TIFF_LONG);
assert_eq!(parse_exif_orientation(&tiff), Some(Orientation::Transpose));
}
#[test]
fn empty_input() {
assert_eq!(parse_exif_orientation(&[]), None);
}
#[test]
fn too_short() {
assert_eq!(parse_exif_orientation(&[0x49, 0x49, 42, 0]), None);
}
#[test]
fn bad_byte_order() {
let mut tiff = make_tiff(false, 1, TIFF_SHORT);
tiff[0] = b'X';
assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn bad_magic() {
let mut tiff = make_tiff(false, 1, TIFF_SHORT);
tiff[2] = 0;
tiff[3] = 0; assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn orientation_value_0_invalid() {
let tiff = make_tiff(false, 0, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn orientation_value_9_invalid() {
let tiff = make_tiff(false, 9, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn orientation_value_255_invalid() {
let tiff = make_tiff(false, 255, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn wrong_type_rejected() {
let tiff = make_tiff(false, 6, 2);
assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn ifd_offset_beyond_data() {
let mut tiff = make_tiff(false, 1, TIFF_SHORT);
tiff[4] = 0xFF;
tiff[5] = 0xFF;
tiff[6] = 0;
tiff[7] = 0;
assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn truncated_ifd_entry() {
let tiff = make_tiff(false, 6, TIFF_SHORT);
assert_eq!(parse_exif_orientation(&tiff[..12]), None);
}
#[test]
fn orientation_after_other_tags() {
let mut buf = Vec::new();
buf.extend_from_slice(b"II"); buf.extend_from_slice(&42u16.to_le_bytes());
buf.extend_from_slice(&8u32.to_le_bytes());
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&0x0100u16.to_le_bytes());
buf.extend_from_slice(&TIFF_SHORT.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&640u16.to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes());
buf.extend_from_slice(&TAG_ORIENTATION.to_le_bytes());
buf.extend_from_slice(&TIFF_SHORT.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&6u16.to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes());
assert_eq!(parse_exif_orientation(&buf), Some(Orientation::Rotate90));
}
#[test]
fn early_exit_on_higher_tag() {
let mut buf = Vec::new();
buf.extend_from_slice(b"II");
buf.extend_from_slice(&42u16.to_le_bytes());
buf.extend_from_slice(&8u32.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&0x011Au16.to_le_bytes());
buf.extend_from_slice(&TIFF_SHORT.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&72u16.to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes());
assert_eq!(parse_exif_orientation(&buf), None);
}
#[test]
fn excessive_entry_count_rejected() {
let mut tiff = make_tiff(false, 6, TIFF_SHORT);
tiff[8] = 0xE9;
tiff[9] = 0x03; assert_eq!(parse_exif_orientation(&tiff), None);
}
#[test]
fn max_entry_count_accepted() {
let mut tiff = make_tiff(false, 6, TIFF_SHORT);
tiff[8] = 0xE8;
tiff[9] = 0x03; let _ = parse_exif_orientation(&tiff);
}
#[test]
fn exif_prefix_only_no_tiff() {
assert_eq!(parse_exif_orientation(b"Exif\0\0"), None);
}
#[test]
fn exif_prefix_truncated() {
assert_eq!(parse_exif_orientation(b"Exif\0"), None);
}
#[test]
fn exif_prefix_with_garbage() {
let data = b"Exif\0\0GARBAGE".to_vec();
assert_eq!(parse_exif_orientation(&data), None);
}
}