#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Orientation {
#[default]
Normal,
FlipHorizontal,
Rotate180,
FlipVertical,
Transpose,
Rotate90,
Transverse,
Rotate270,
}
impl Orientation {
#[must_use]
pub const fn from_exif(value: u16) -> Option<Self> {
Some(match value {
1 => Self::Normal,
2 => Self::FlipHorizontal,
3 => Self::Rotate180,
4 => Self::FlipVertical,
5 => Self::Transpose,
6 => Self::Rotate90,
7 => Self::Transverse,
8 => Self::Rotate270,
_ => return None,
})
}
#[must_use]
pub const fn exif(self) -> u8 {
match self {
Self::Normal => 1,
Self::FlipHorizontal => 2,
Self::Rotate180 => 3,
Self::FlipVertical => 4,
Self::Transpose => 5,
Self::Rotate90 => 6,
Self::Transverse => 7,
Self::Rotate270 => 8,
}
}
#[must_use]
pub const fn from_parts(turns: u8, mirrored: bool) -> Self {
match (turns % 4, mirrored) {
(0, false) => Self::Normal,
(0, true) => Self::FlipHorizontal,
(1, false) => Self::Rotate90,
(1, true) => Self::Transpose,
(2, false) => Self::Rotate180,
(2, true) => Self::FlipVertical,
(3, false) => Self::Rotate270,
_ => Self::Transverse,
}
}
#[must_use]
pub const fn clockwise_turns(self) -> u8 {
match self {
Self::Normal | Self::FlipHorizontal => 0,
Self::Rotate90 | Self::Transpose => 1,
Self::Rotate180 | Self::FlipVertical => 2,
Self::Rotate270 | Self::Transverse => 3,
}
}
#[must_use]
pub const fn mirrored(self) -> bool {
matches!(
self,
Self::FlipHorizontal | Self::FlipVertical | Self::Transpose | Self::Transverse
)
}
#[must_use]
pub const fn transposes(self) -> bool {
self.clockwise_turns() % 2 == 1
}
#[must_use]
pub fn from_exif_block(exif: &[u8]) -> Option<Self> {
let tiff = exif.strip_prefix(b"Exif\0\0").unwrap_or(exif);
let big_endian = match tiff.get(..2)? {
b"MM" => true,
b"II" => false,
_ => return None,
};
let short = |at: usize| -> Option<u16> {
let bytes = [*tiff.get(at)?, *tiff.get(at.checked_add(1)?)?];
Some(if big_endian {
u16::from_be_bytes(bytes)
} else {
u16::from_le_bytes(bytes)
})
};
let long = |at: usize| -> Option<u32> {
let bytes = tiff.get(at..at.checked_add(4)?)?;
let bytes = [
*bytes.first()?,
*bytes.get(1)?,
*bytes.get(2)?,
*bytes.get(3)?,
];
Some(if big_endian {
u32::from_be_bytes(bytes)
} else {
u32::from_le_bytes(bytes)
})
};
if short(2)? != 42 {
return None;
}
let ifd = usize::try_from(long(4)?).ok()?;
let entries = short(ifd)?;
for entry in 0..usize::from(entries) {
let at = ifd.checked_add(2)?.checked_add(entry.checked_mul(12)?)?;
if short(at)? == 0x0112 {
return Self::from_exif(short(at.checked_add(8)?)?);
}
}
None
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests assert on known-good values"
)]
mod tests {
use super::*;
const ALL: [Orientation; 8] = [
Orientation::Normal,
Orientation::FlipHorizontal,
Orientation::Rotate180,
Orientation::FlipVertical,
Orientation::Transpose,
Orientation::Rotate90,
Orientation::Transverse,
Orientation::Rotate270,
];
fn by_parts(orientation: Orientation, w: usize, h: usize) -> (Vec<usize>, usize, usize) {
let mut grid: Vec<usize> = (0..w * h).collect();
let (mut w, mut h) = (w, h);
for _ in 0..orientation.clockwise_turns() {
let mut turned = vec![0; w * h];
for y in 0..w {
for x in 0..h {
turned[y * h + x] = grid[(h - 1 - x) * w + y];
}
}
grid = turned;
(w, h) = (h, w);
}
if orientation.mirrored() {
for row in grid.chunks_mut(w) {
row.reverse();
}
}
(grid, w, h)
}
fn by_definition(orientation: Orientation, w: usize, h: usize) -> (Vec<usize>, usize, usize) {
let (ow, oh) = if orientation.transposes() {
(h, w)
} else {
(w, h)
};
let mut grid = vec![0; w * h];
for y in 0..oh {
for x in 0..ow {
let (sx, sy) = 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 => (y, h - 1 - x),
Orientation::Transverse => (w - 1 - y, h - 1 - x),
Orientation::Rotate270 => (w - 1 - y, x),
};
grid[y * ow + x] = sy * w + sx;
}
}
(grid, ow, oh)
}
#[test]
fn the_parts_decomposition_matches_the_exif_definitions() {
for orientation in ALL {
assert_eq!(
by_parts(orientation, 3, 2),
by_definition(orientation, 3, 2),
"{orientation:?}"
);
}
}
#[test]
fn parts_and_exif_values_round_trip() {
for orientation in ALL {
assert_eq!(
Orientation::from_parts(orientation.clockwise_turns(), orientation.mirrored()),
orientation
);
assert_eq!(
Orientation::from_exif(u16::from(orientation.exif())),
Some(orientation)
);
}
assert_eq!(Orientation::from_parts(5, false), Orientation::Rotate90);
assert_eq!(Orientation::from_exif(0), None);
assert_eq!(Orientation::from_exif(9), None);
}
#[test]
fn exif_orientation_is_read_from_both_byte_orders() {
let little = b"Exif\0\0II*\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x06\0\0\0";
assert_eq!(
Orientation::from_exif_block(little),
Some(Orientation::Rotate90)
);
let big = b"Exif\0\0MM\0*\0\0\0\x08\0\x01\x01\x12\0\x03\0\0\0\x01\0\x03\0\0";
assert_eq!(
Orientation::from_exif_block(big),
Some(Orientation::Rotate180)
);
}
#[test]
fn the_exif_identifier_is_optional() {
let bare = b"II*\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x08\0\0\0";
assert_eq!(
Orientation::from_exif_block(bare),
Some(Orientation::Rotate270)
);
}
#[test]
fn broken_exif_yields_no_orientation_rather_than_an_error() {
assert_eq!(
Orientation::from_exif_block(b"Exif\0\0XX*\0\x08\0\0\0"),
None
);
assert_eq!(Orientation::from_exif_block(b"Exif\0\0II*\0"), None);
assert_eq!(Orientation::from_exif_block(b"not exif at all"), None);
assert_eq!(Orientation::from_exif_block(b""), None);
assert_eq!(Orientation::from_exif_block(b"II*\0\xff\xff\xff\xff"), None);
assert_eq!(
Orientation::from_exif_block(b"II*\0\x08\0\0\0\xff\xff"),
None
);
let bogus = b"Exif\0\0II*\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x09\0\0\0";
assert_eq!(Orientation::from_exif_block(bogus), None);
}
#[test]
fn a_block_without_the_tag_has_no_orientation() {
let other = b"II*\0\x08\0\0\0\x01\0\x00\x01\x03\0\x01\0\0\0\x06\0\0\0";
assert_eq!(Orientation::from_exif_block(other), None);
}
}