#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Orientation {
#[default]
Normal,
FlipHorizontal,
Rotate180,
FlipVertical,
Transpose,
Rotate90,
Transverse,
Rotate270,
}
impl Orientation {
pub fn from_tiff(value: u16) -> Self {
match value {
2 => Self::FlipHorizontal,
3 => Self::Rotate180,
4 => Self::FlipVertical,
5 => Self::Transpose,
6 => Self::Rotate90,
7 => Self::Transverse,
8 => Self::Rotate270,
_ => Self::Normal,
}
}
pub fn swaps_axes(self) -> bool {
matches!(
self,
Self::Transpose | Self::Rotate90 | Self::Transverse | Self::Rotate270
)
}
pub fn is_identity(self) -> bool {
self == Self::Normal
}
}
pub fn orientation_from_exif(data: &[u8]) -> Orientation {
parse_orientation(data).map_or(Orientation::Normal, Orientation::from_tiff)
}
fn parse_orientation(data: &[u8]) -> Option<u16> {
let tiff = match data.get(..6) {
Some(b"Exif\0\0") => data.get(6..)?,
_ => data,
};
let big_endian = match tiff.get(..2)? {
b"MM" => true,
b"II" => false,
_ => return None,
};
let u16_at = |off: usize| -> Option<u16> {
let b = tiff.get(off..off + 2)?;
Some(if big_endian {
u16::from_be_bytes([b[0], b[1]])
} else {
u16::from_le_bytes([b[0], b[1]])
})
};
let u32_at = |off: usize| -> Option<u32> {
let b = tiff.get(off..off + 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]])
})
};
if u16_at(2)? != 42 {
return None;
}
let ifd0 = u32_at(4)? as usize;
let entry_count = u16_at(ifd0)? as usize;
for i in 0..entry_count {
let entry = ifd0 + 2 + i * 12;
if u16_at(entry)? == 0x0112 {
return u16_at(entry + 8);
}
}
None
}
pub fn apply_orientation(
pixels: Vec<u8>,
width: u32,
height: u32,
orientation: Orientation,
) -> (Vec<u8>, u32, u32) {
if orientation.is_identity() {
return (pixels, width, height);
}
let (w, h) = (width as usize, height as usize);
if pixels.len() < w * h * 4 {
return (pixels, width, height);
}
let (dst_w, dst_h) = if orientation.swaps_axes() {
(h, w)
} else {
(w, h)
};
let mut out = vec![0u8; dst_w * dst_h * 4];
for y in 0..h {
for x in 0..w {
let (dx, dy) = match orientation {
Orientation::Normal => (x, y),
Orientation::FlipHorizontal => (w - 1 - x, y),
Orientation::Rotate180 => (w - 1 - x, h - 1 - y),
Orientation::FlipVertical => (x, h - 1 - y),
Orientation::Transpose => (y, x),
Orientation::Rotate90 => (h - 1 - y, x),
Orientation::Transverse => (h - 1 - y, w - 1 - x),
Orientation::Rotate270 => (y, w - 1 - x),
};
let src = (y * w + x) * 4;
let dst = (dy * dst_w + dx) * 4;
out[dst..dst + 4].copy_from_slice(&pixels[src..src + 4]);
}
}
(out, dst_w as u32, dst_h as u32)
}
#[cfg(test)]
mod tests {
use super::*;
fn exif_le(value: u16) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"Exif\0\0");
v.extend_from_slice(b"II"); v.extend_from_slice(&42u16.to_le_bytes());
v.extend_from_slice(&8u32.to_le_bytes()); v.extend_from_slice(&1u16.to_le_bytes()); v.extend_from_slice(&0x0112u16.to_le_bytes()); v.extend_from_slice(&3u16.to_le_bytes()); v.extend_from_slice(&1u32.to_le_bytes()); v.extend_from_slice(&value.to_le_bytes());
v.extend_from_slice(&[0, 0]); v
}
fn exif_be_bare(value: u16) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"MM");
v.extend_from_slice(&42u16.to_be_bytes());
v.extend_from_slice(&8u32.to_be_bytes());
v.extend_from_slice(&1u16.to_be_bytes());
v.extend_from_slice(&0x0112u16.to_be_bytes());
v.extend_from_slice(&3u16.to_be_bytes());
v.extend_from_slice(&1u32.to_be_bytes());
v.extend_from_slice(&value.to_be_bytes());
v.extend_from_slice(&[0, 0]);
v
}
#[test]
fn reads_little_endian_orientation() {
assert_eq!(orientation_from_exif(&exif_le(6)), Orientation::Rotate90);
}
#[test]
fn reads_big_endian_orientation_without_preamble() {
assert_eq!(
orientation_from_exif(&exif_be_bare(8)),
Orientation::Rotate270
);
}
#[test]
fn garbage_reads_as_normal() {
assert_eq!(orientation_from_exif(b"not exif"), Orientation::Normal);
assert_eq!(orientation_from_exif(&[]), Orientation::Normal);
let full = exif_le(6);
for cut in 0..full.len() {
let _ = orientation_from_exif(&full[..cut]);
}
}
#[test]
fn out_of_range_value_reads_as_normal() {
assert_eq!(orientation_from_exif(&exif_le(0)), Orientation::Normal);
assert_eq!(orientation_from_exif(&exif_le(9)), Orientation::Normal);
}
fn two_by_one() -> Vec<u8> {
vec![255, 0, 0, 255, 0, 255, 0, 255]
}
#[test]
fn flip_horizontal_swaps_the_two_pixels() {
let (out, w, h) = apply_orientation(two_by_one(), 2, 1, Orientation::FlipHorizontal);
assert_eq!((w, h), (2, 1));
assert_eq!(&out[0..4], &[0, 255, 0, 255]);
assert_eq!(&out[4..8], &[255, 0, 0, 255]);
}
#[test]
fn rotate90_swaps_dimensions_and_stacks_the_pixels() {
let (out, w, h) = apply_orientation(two_by_one(), 2, 1, Orientation::Rotate90);
assert_eq!((w, h), (1, 2));
assert_eq!(&out[0..4], &[255, 0, 0, 255], "left pixel goes to the top");
assert_eq!(&out[4..8], &[0, 255, 0, 255]);
}
#[test]
fn rotate270_is_the_inverse_of_rotate90() {
let src: Vec<u8> = (0..(3 * 2 * 4)).map(|i| i as u8).collect();
let (px, w, h) = apply_orientation(src.clone(), 3, 2, Orientation::Rotate90);
let (back, bw, bh) = apply_orientation(px, w, h, Orientation::Rotate270);
assert_eq!((bw, bh), (3, 2));
assert_eq!(back, src);
}
#[test]
fn normal_is_a_passthrough() {
let (out, w, h) = apply_orientation(two_by_one(), 2, 1, Orientation::Normal);
assert_eq!((w, h), (2, 1));
assert_eq!(out, two_by_one());
}
#[test]
fn every_orientation_preserves_the_pixel_count() {
for v in 1..=8u16 {
let o = Orientation::from_tiff(v);
let (out, w, h) = apply_orientation(vec![9u8; 6 * 4], 3, 2, o);
assert_eq!(out.len(), 6 * 4, "value {v}");
assert_eq!((w * h) as usize, 6, "value {v}");
assert_eq!(o.swaps_axes(), w == 2, "value {v}");
}
}
#[test]
fn rotating_four_times_by_90_returns_the_original() {
let src: Vec<u8> = (0..(3 * 2 * 4)).map(|i| i as u8).collect();
let (mut px, mut w, mut h) = (src.clone(), 3u32, 2u32);
for _ in 0..4 {
let r = apply_orientation(px, w, h, Orientation::Rotate90);
px = r.0;
w = r.1;
h = r.2;
}
assert_eq!((w, h), (3, 2));
assert_eq!(px, src);
}
}