1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18pub enum Orientation {
19 #[default]
21 Normal,
22 FlipHorizontal,
24 Rotate180,
26 FlipVertical,
28 Transpose,
30 Rotate90,
32 Transverse,
34 Rotate270,
36}
37
38impl Orientation {
39 #[must_use]
42 pub const fn from_exif(value: u16) -> Option<Self> {
43 Some(match value {
44 1 => Self::Normal,
45 2 => Self::FlipHorizontal,
46 3 => Self::Rotate180,
47 4 => Self::FlipVertical,
48 5 => Self::Transpose,
49 6 => Self::Rotate90,
50 7 => Self::Transverse,
51 8 => Self::Rotate270,
52 _ => return None,
53 })
54 }
55
56 #[must_use]
58 pub const fn exif(self) -> u8 {
59 match self {
60 Self::Normal => 1,
61 Self::FlipHorizontal => 2,
62 Self::Rotate180 => 3,
63 Self::FlipVertical => 4,
64 Self::Transpose => 5,
65 Self::Rotate90 => 6,
66 Self::Transverse => 7,
67 Self::Rotate270 => 8,
68 }
69 }
70
71 #[must_use]
74 pub const fn from_parts(turns: u8, mirrored: bool) -> Self {
75 match (turns % 4, mirrored) {
76 (0, false) => Self::Normal,
77 (0, true) => Self::FlipHorizontal,
78 (1, false) => Self::Rotate90,
79 (1, true) => Self::Transpose,
82 (2, false) => Self::Rotate180,
83 (2, true) => Self::FlipVertical,
86 (3, false) => Self::Rotate270,
87 _ => Self::Transverse,
88 }
89 }
90
91 #[must_use]
93 pub const fn clockwise_turns(self) -> u8 {
94 match self {
95 Self::Normal | Self::FlipHorizontal => 0,
96 Self::Rotate90 | Self::Transpose => 1,
97 Self::Rotate180 | Self::FlipVertical => 2,
98 Self::Rotate270 | Self::Transverse => 3,
99 }
100 }
101
102 #[must_use]
104 pub const fn mirrored(self) -> bool {
105 matches!(
106 self,
107 Self::FlipHorizontal | Self::FlipVertical | Self::Transpose | Self::Transverse
108 )
109 }
110
111 #[must_use]
113 pub const fn transposes(self) -> bool {
114 self.clockwise_turns() % 2 == 1
115 }
116
117 #[must_use]
128 pub fn from_exif_block(exif: &[u8]) -> Option<Self> {
129 let tiff = exif.strip_prefix(b"Exif\0\0").unwrap_or(exif);
130
131 let big_endian = match tiff.get(..2)? {
132 b"MM" => true,
133 b"II" => false,
134 _ => return None,
135 };
136 let short = |at: usize| -> Option<u16> {
137 let bytes = [*tiff.get(at)?, *tiff.get(at.checked_add(1)?)?];
138 Some(if big_endian {
139 u16::from_be_bytes(bytes)
140 } else {
141 u16::from_le_bytes(bytes)
142 })
143 };
144 let long = |at: usize| -> Option<u32> {
145 let bytes = tiff.get(at..at.checked_add(4)?)?;
146 let bytes = [
147 *bytes.first()?,
148 *bytes.get(1)?,
149 *bytes.get(2)?,
150 *bytes.get(3)?,
151 ];
152 Some(if big_endian {
153 u32::from_be_bytes(bytes)
154 } else {
155 u32::from_le_bytes(bytes)
156 })
157 };
158
159 if short(2)? != 42 {
160 return None;
161 }
162 let ifd = usize::try_from(long(4)?).ok()?;
163 let entries = short(ifd)?;
164 for entry in 0..usize::from(entries) {
165 let at = ifd.checked_add(2)?.checked_add(entry.checked_mul(12)?)?;
166 if short(at)? == 0x0112 {
169 return Self::from_exif(short(at.checked_add(8)?)?);
170 }
171 }
172 None
173 }
174}
175
176#[cfg(test)]
177#[allow(
178 clippy::unwrap_used,
179 clippy::indexing_slicing,
180 reason = "tests assert on known-good values"
181)]
182mod tests {
183 use super::*;
184
185 const ALL: [Orientation; 8] = [
186 Orientation::Normal,
187 Orientation::FlipHorizontal,
188 Orientation::Rotate180,
189 Orientation::FlipVertical,
190 Orientation::Transpose,
191 Orientation::Rotate90,
192 Orientation::Transverse,
193 Orientation::Rotate270,
194 ];
195
196 fn by_parts(orientation: Orientation, w: usize, h: usize) -> (Vec<usize>, usize, usize) {
199 let mut grid: Vec<usize> = (0..w * h).collect();
200 let (mut w, mut h) = (w, h);
201 for _ in 0..orientation.clockwise_turns() {
202 let mut turned = vec![0; w * h];
204 for y in 0..w {
205 for x in 0..h {
206 turned[y * h + x] = grid[(h - 1 - x) * w + y];
207 }
208 }
209 grid = turned;
210 (w, h) = (h, w);
211 }
212 if orientation.mirrored() {
213 for row in grid.chunks_mut(w) {
214 row.reverse();
215 }
216 }
217 (grid, w, h)
218 }
219
220 fn by_definition(orientation: Orientation, w: usize, h: usize) -> (Vec<usize>, usize, usize) {
223 let (ow, oh) = if orientation.transposes() {
224 (h, w)
225 } else {
226 (w, h)
227 };
228 let mut grid = vec![0; w * h];
229 for y in 0..oh {
230 for x in 0..ow {
231 let (sx, sy) = match orientation {
232 Orientation::Normal => (x, y),
233 Orientation::FlipHorizontal => (w - 1 - x, y),
234 Orientation::Rotate180 => (w - 1 - x, h - 1 - y),
235 Orientation::FlipVertical => (x, h - 1 - y),
236 Orientation::Transpose => (y, x),
237 Orientation::Rotate90 => (y, h - 1 - x),
238 Orientation::Transverse => (w - 1 - y, h - 1 - x),
239 Orientation::Rotate270 => (w - 1 - y, x),
240 };
241 grid[y * ow + x] = sy * w + sx;
242 }
243 }
244 (grid, ow, oh)
245 }
246
247 #[test]
248 fn the_parts_decomposition_matches_the_exif_definitions() {
249 for orientation in ALL {
250 assert_eq!(
251 by_parts(orientation, 3, 2),
252 by_definition(orientation, 3, 2),
253 "{orientation:?}"
254 );
255 }
256 }
257
258 #[test]
259 fn parts_and_exif_values_round_trip() {
260 for orientation in ALL {
261 assert_eq!(
262 Orientation::from_parts(orientation.clockwise_turns(), orientation.mirrored()),
263 orientation
264 );
265 assert_eq!(
266 Orientation::from_exif(u16::from(orientation.exif())),
267 Some(orientation)
268 );
269 }
270 assert_eq!(Orientation::from_parts(5, false), Orientation::Rotate90);
271 assert_eq!(Orientation::from_exif(0), None);
272 assert_eq!(Orientation::from_exif(9), None);
273 }
274
275 #[test]
276 fn exif_orientation_is_read_from_both_byte_orders() {
277 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";
279 assert_eq!(
280 Orientation::from_exif_block(little),
281 Some(Orientation::Rotate90)
282 );
283
284 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";
285 assert_eq!(
286 Orientation::from_exif_block(big),
287 Some(Orientation::Rotate180)
288 );
289 }
290
291 #[test]
292 fn the_exif_identifier_is_optional() {
293 let bare = b"II*\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x08\0\0\0";
294 assert_eq!(
295 Orientation::from_exif_block(bare),
296 Some(Orientation::Rotate270)
297 );
298 }
299
300 #[test]
301 fn broken_exif_yields_no_orientation_rather_than_an_error() {
302 assert_eq!(
303 Orientation::from_exif_block(b"Exif\0\0XX*\0\x08\0\0\0"),
304 None
305 );
306 assert_eq!(Orientation::from_exif_block(b"Exif\0\0II*\0"), None);
307 assert_eq!(Orientation::from_exif_block(b"not exif at all"), None);
308 assert_eq!(Orientation::from_exif_block(b""), None);
309 assert_eq!(Orientation::from_exif_block(b"II*\0\xff\xff\xff\xff"), None);
311 assert_eq!(
313 Orientation::from_exif_block(b"II*\0\x08\0\0\0\xff\xff"),
314 None
315 );
316 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";
318 assert_eq!(Orientation::from_exif_block(bogus), None);
319 }
320
321 #[test]
322 fn a_block_without_the_tag_has_no_orientation() {
323 let other = b"II*\0\x08\0\0\0\x01\0\x00\x01\x03\0\x01\0\0\0\x06\0\0\0";
325 assert_eq!(Orientation::from_exif_block(other), None);
326 }
327}