#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum Orientation {
#[default]
Identity = 1,
FlipH = 2,
Rotate180 = 3,
FlipV = 4,
Transpose = 5,
Rotate90 = 6,
Transverse = 7,
Rotate270 = 8,
}
impl Orientation {
pub const ALL: [Orientation; 8] = [
Self::Identity,
Self::FlipH,
Self::Rotate180,
Self::FlipV,
Self::Transpose,
Self::Rotate90,
Self::Transverse,
Self::Rotate270,
];
pub const fn from_exif(value: u8) -> Option<Self> {
match value {
1 => Some(Self::Identity),
2 => Some(Self::FlipH),
3 => Some(Self::Rotate180),
4 => Some(Self::FlipV),
5 => Some(Self::Transpose),
6 => Some(Self::Rotate90),
7 => Some(Self::Transverse),
8 => Some(Self::Rotate270),
_ => None,
}
}
pub const fn to_exif(self) -> u8 {
self as u8
}
pub const fn is_identity(self) -> bool {
matches!(self, Self::Identity)
}
pub const fn swaps_axes(self) -> bool {
matches!(
self,
Self::Transpose | Self::Rotate90 | Self::Transverse | Self::Rotate270
)
}
pub const fn is_row_local(self) -> bool {
matches!(self, Self::Identity | Self::FlipH)
}
pub const fn compose(self, other: Self) -> Self {
let (r1, f1) = self.decompose();
let (r2, f2) = other.decompose();
if !f1 {
Self::from_rotation_flip((r1 + r2) & 3, f2)
} else {
Self::from_rotation_flip(r1.wrapping_sub(r2) & 3, !f2)
}
}
pub const fn then(self, other: Self) -> Self {
self.compose(other)
}
pub const fn inverse(self) -> Self {
let (r, f) = self.decompose();
if !f {
Self::from_rotation_flip((4 - r) & 3, false)
} else {
self
}
}
pub const fn output_dimensions(self, w: u32, h: u32) -> (u32, u32) {
if self.swaps_axes() { (h, w) } else { (w, h) }
}
pub const fn forward_map(self, sx: u32, sy: u32, w: u32, h: u32) -> (u32, u32) {
match self {
Self::Identity => (sx, sy),
Self::FlipH => (w - 1 - sx, sy),
Self::Rotate90 => (h - 1 - sy, sx),
Self::Transpose => (sy, sx),
Self::Rotate180 => (w - 1 - sx, h - 1 - sy),
Self::FlipV => (sx, h - 1 - sy),
Self::Rotate270 => (sy, w - 1 - sx),
Self::Transverse => (h - 1 - sy, w - 1 - sx),
}
}
const fn decompose(self) -> (u8, bool) {
match self {
Self::Identity => (0, false),
Self::FlipH => (0, true),
Self::Rotate90 => (1, false),
Self::Transpose => (1, true),
Self::Rotate180 => (2, false),
Self::FlipV => (2, true),
Self::Rotate270 => (3, false),
Self::Transverse => (3, true),
}
}
const fn from_rotation_flip(rotation: u8, flip: bool) -> Self {
match (rotation & 3, flip) {
(0, false) => Self::Identity,
(0, true) => Self::FlipH,
(1, false) => Self::Rotate90,
(1, true) => Self::Transpose,
(2, false) => Self::Rotate180,
(2, true) => Self::FlipV,
(3, false) => Self::Rotate270,
(3, true) => Self::Transverse,
_ => unreachable!(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exif_round_trip() {
for v in 1..=8u8 {
let o = Orientation::from_exif(v).unwrap();
assert_eq!(o.to_exif(), v, "round-trip failed for EXIF {v}");
assert_eq!(o as u8, v, "repr(u8) mismatch for EXIF {v}");
}
}
#[test]
fn exif_invalid() {
assert!(Orientation::from_exif(0).is_none());
assert!(Orientation::from_exif(9).is_none());
assert!(Orientation::from_exif(255).is_none());
}
#[test]
fn default_is_identity() {
assert_eq!(Orientation::default(), Orientation::Identity);
}
#[test]
fn identity_properties() {
assert!(Orientation::Identity.is_identity());
for &o in &Orientation::ALL[1..] {
assert!(!o.is_identity());
}
}
#[test]
fn swaps_axes() {
for &o in &Orientation::ALL {
let expected = matches!(
o,
Orientation::Transpose
| Orientation::Rotate90
| Orientation::Transverse
| Orientation::Rotate270
);
assert_eq!(o.swaps_axes(), expected, "{o:?}");
}
}
#[test]
fn row_local() {
assert!(Orientation::Identity.is_row_local());
assert!(Orientation::FlipH.is_row_local());
for &o in &Orientation::ALL[2..] {
assert!(!o.is_row_local(), "{o:?} should not be row-local");
}
}
#[test]
fn output_dimensions() {
for &o in &Orientation::ALL {
let (dw, dh) = o.output_dimensions(100, 200);
if o.swaps_axes() {
assert_eq!((dw, dh), (200, 100), "{o:?}");
} else {
assert_eq!((dw, dh), (100, 200), "{o:?}");
}
}
}
#[test]
fn all_array_matches_exif_order() {
for (i, &o) in Orientation::ALL.iter().enumerate() {
assert_eq!(o.to_exif(), (i + 1) as u8);
}
}
#[test]
fn cayley_table() {
#[rustfmt::skip]
const CAYLEY: [[usize; 8]; 8] = [
[0,1,2,3,4,5,6,7], [1,0,5,6,7,2,3,4], [2,5,0,4,3,1,7,6], [3,4,6,0,1,7,2,5], [4,3,7,2,5,6,0,1], [5,2,1,7,6,0,4,3], [6,7,3,1,0,4,5,2], [7,6,4,5,2,3,1,0], ];
let zj = [
Orientation::Identity, Orientation::FlipH, Orientation::FlipV, Orientation::Transpose, Orientation::Rotate90, Orientation::Rotate180, Orientation::Rotate270, Orientation::Transverse, ];
for (i, row) in CAYLEY.iter().enumerate() {
for (j, &expected_idx) in row.iter().enumerate() {
let a = zj[i];
let b = zj[j];
let expected = zj[expected_idx];
let got = a.compose(b);
assert_eq!(
got, expected,
"Cayley: {a:?}.compose({b:?}) = {got:?}, expected {expected:?}"
);
}
}
}
#[test]
fn inverse_all() {
for &o in &Orientation::ALL {
let inv = o.inverse();
assert_eq!(
o.compose(inv),
Orientation::Identity,
"{o:?}.compose({inv:?}) should be Identity"
);
assert_eq!(
inv.compose(o),
Orientation::Identity,
"{inv:?}.compose({o:?}) should be Identity"
);
}
}
#[test]
fn associativity() {
for &a in &Orientation::ALL {
for &b in &Orientation::ALL {
for &c in &Orientation::ALL {
let ab_c = a.compose(b).compose(c);
let a_bc = a.compose(b.compose(c));
assert_eq!(ab_c, a_bc, "({a:?}*{b:?})*{c:?} != {a:?}*({b:?}*{c:?})");
}
}
}
}
#[test]
fn identity_is_neutral() {
let id = Orientation::Identity;
for &o in &Orientation::ALL {
assert_eq!(id.compose(o), o);
assert_eq!(o.compose(id), o);
}
}
#[test]
fn then_is_compose() {
for &a in &Orientation::ALL {
for &b in &Orientation::ALL {
assert_eq!(a.then(b), a.compose(b));
}
}
}
#[test]
fn forward_map_brute_force_4x3() {
let (sw, sh) = (4u32, 3u32);
for &o in &Orientation::ALL {
let (dw, dh) = o.output_dimensions(sw, sh);
for sy in 0..sh {
for sx in 0..sw {
let (dx, dy) = o.forward_map(sx, sy, sw, sh);
assert!(
dx < dw && dy < dh,
"{o:?}: ({sx},{sy}) mapped to ({dx},{dy}) outside {dw}x{dh}"
);
}
}
let mut seen = alloc::vec![false; (dw * dh) as usize];
for sy in 0..sh {
for sx in 0..sw {
let (dx, dy) = o.forward_map(sx, sy, sw, sh);
let idx = (dy * dw + dx) as usize;
assert!(!seen[idx], "{o:?}: output ({dx},{dy}) hit twice");
seen[idx] = true;
}
}
assert!(
seen.iter().all(|&s| s),
"{o:?}: not all output pixels covered"
);
}
}
#[test]
fn forward_map_inverse_round_trip() {
let (w, h) = (5u32, 3u32);
for &o in &Orientation::ALL {
let inv = o.inverse();
let (dw, dh) = o.output_dimensions(w, h);
for sy in 0..h {
for sx in 0..w {
let (dx, dy) = o.forward_map(sx, sy, w, h);
let (rx, ry) = inv.forward_map(dx, dy, dw, dh);
assert_eq!(
(rx, ry),
(sx, sy),
"{o:?}: ({sx},{sy}) -> ({dx},{dy}) -> ({rx},{ry})"
);
}
}
}
}
}