#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Orientation {
#[default]
Upright,
FlipHorizontal,
Rotate180,
FlipVertical,
Transpose,
Rotate90,
Transverse,
Rotate270,
}
impl Orientation {
pub fn from_exif(value: u16) -> Orientation {
match value {
2 => Orientation::FlipHorizontal,
3 => Orientation::Rotate180,
4 => Orientation::FlipVertical,
5 => Orientation::Transpose,
6 => Orientation::Rotate90,
7 => Orientation::Transverse,
8 => Orientation::Rotate270,
_ => Orientation::Upright,
}
}
pub fn swaps_axes(self) -> bool {
matches!(
self,
Orientation::Transpose
| Orientation::Rotate90
| Orientation::Transverse
| Orientation::Rotate270
)
}
pub fn applied_size(self, width: u32, height: u32) -> (u32, u32) {
if self.swaps_axes() {
(height, width)
} else {
(width, height)
}
}
pub fn as_image(self) -> image::metadata::Orientation {
match self {
Orientation::Upright => image::metadata::Orientation::NoTransforms,
Orientation::FlipHorizontal => image::metadata::Orientation::FlipHorizontal,
Orientation::Rotate180 => image::metadata::Orientation::Rotate180,
Orientation::FlipVertical => image::metadata::Orientation::FlipVertical,
Orientation::Transpose => image::metadata::Orientation::Rotate90FlipH,
Orientation::Rotate90 => image::metadata::Orientation::Rotate90,
Orientation::Transverse => image::metadata::Orientation::Rotate270FlipH,
Orientation::Rotate270 => image::metadata::Orientation::Rotate270,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_portrait_phone_photo_is_tagged_as_a_quarter_turn() {
let o = Orientation::from_exif(6);
assert_eq!(o, Orientation::Rotate90);
assert!(o.swaps_axes());
assert_eq!(o.applied_size(4032, 3024), (3024, 4032));
}
#[test]
fn a_file_with_no_exif_at_all_is_upright() {
assert_eq!(Orientation::default(), Orientation::Upright);
assert_eq!(Orientation::Upright.applied_size(800, 600), (800, 600));
}
#[test]
fn a_value_nobody_defined_is_upright_rather_than_a_guess() {
for value in [0, 9, 42, u16::MAX] {
assert_eq!(Orientation::from_exif(value), Orientation::Upright, "{value}");
}
}
#[test]
fn every_defined_value_round_trips_to_a_distinct_transform() {
let all: Vec<Orientation> = (1..=8).map(Orientation::from_exif).collect();
for (i, a) in all.iter().enumerate() {
for b in &all[i + 1..] {
assert_ne!(a, b, "two EXIF values map to the same transform");
}
}
}
#[test]
fn only_the_quarter_turns_swap_width_and_height() {
assert!(!Orientation::from_exif(1).swaps_axes());
assert!(!Orientation::from_exif(2).swaps_axes());
assert!(!Orientation::from_exif(3).swaps_axes());
assert!(!Orientation::from_exif(4).swaps_axes());
assert!(Orientation::from_exif(5).swaps_axes());
assert!(Orientation::from_exif(6).swaps_axes());
assert!(Orientation::from_exif(7).swaps_axes());
assert!(Orientation::from_exif(8).swaps_axes());
}
}