Skip to main content

otf_pixels_core/
orientation.rs

1//! [`Orientation`] — how a stored image must be turned to display upright.
2//!
3//! Cameras write pixels in sensor order and record the way the device was held
4//! as metadata rather than rotating the pixels: EXIF's `Orientation` tag in
5//! JPEG, TIFF, PNG and WebP, and the `irot`/`imir` properties in HEIF/AVIF.
6//! Decoders report it here; applying it is a pipeline decision (`auto_orient`,
7//! SPEC §Safety and limits), so a decoder never rotates its own output.
8
9/// One of the eight orientations the EXIF/TIFF `Orientation` tag (274) can
10/// name.
11///
12/// Each variant describes the transform that makes the stored image upright.
13/// All eight are a clockwise quarter-turn rotation followed, optionally, by a
14/// horizontal mirror, which is the form [`Orientation::clockwise_turns`] and
15/// [`Orientation::mirrored`] expose and [`Orientation::from_parts`] builds
16/// from. HEIF's rotate-then-mirror properties reduce to the same form.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18pub enum Orientation {
19    /// EXIF 1: already upright.
20    #[default]
21    Normal,
22    /// EXIF 2: mirror left to right.
23    FlipHorizontal,
24    /// EXIF 3: rotate 180 degrees.
25    Rotate180,
26    /// EXIF 4: mirror top to bottom.
27    FlipVertical,
28    /// EXIF 5: mirror across the top-left to bottom-right diagonal.
29    Transpose,
30    /// EXIF 6: rotate 90 degrees clockwise.
31    Rotate90,
32    /// EXIF 7: mirror across the top-right to bottom-left diagonal.
33    Transverse,
34    /// EXIF 8: rotate 270 degrees clockwise, i.e. 90 anticlockwise.
35    Rotate270,
36}
37
38impl Orientation {
39    /// The orientation an EXIF `Orientation` value names, or `None` for a value
40    /// outside 1–8.
41    #[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    /// The EXIF `Orientation` value, 1–8.
57    #[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    /// Rotate `turns` quarter turns clockwise (taken modulo 4), then mirror
72    /// left to right if `mirrored`.
73    #[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            // Transpose sends (x, y) to (y, x): a clockwise quarter turn sends
80            // it to (h - 1 - y, x), and the mirror then undoes the reflection.
81            (1, true) => Self::Transpose,
82            (2, false) => Self::Rotate180,
83            // A half turn mirrors both axes; mirroring left to right again
84            // leaves only the vertical one.
85            (2, true) => Self::FlipVertical,
86            (3, false) => Self::Rotate270,
87            _ => Self::Transverse,
88        }
89    }
90
91    /// Clockwise quarter turns to apply first, 0–3.
92    #[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    /// Whether a left-to-right mirror follows the rotation.
103    #[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    /// Whether displaying upright exchanges width and height.
112    #[must_use]
113    pub const fn transposes(self) -> bool {
114        self.clockwise_turns() % 2 == 1
115    }
116
117    /// Read the `Orientation` tag from an EXIF block.
118    ///
119    /// `exif` is the TIFF structure EXIF is made of, optionally behind the
120    /// `Exif\0\0` identifier a JPEG APP1 segment carries. PNG's `eXIf` and
121    /// WebP's `EXIF` chunk are specified without the identifier, but writers
122    /// that copy it across from a JPEG are common, so both are accepted.
123    ///
124    /// A missing tag, an out-of-range value or a malformed block all yield
125    /// `None` rather than an error: broken metadata is not a broken image,
126    /// and refusing to decode a photograph over it would be the wrong trade.
127    #[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            // 0x0112 is Orientation; a SHORT, so its single value sits in the
167            // first two bytes of the value field rather than at an offset.
168            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    /// Apply `orientation` to a `w` x `h` grid of distinct values by the
197    /// parts decomposition, one primitive at a time.
198    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            // Clockwise: output (x, y) comes from input (y, h - 1 - x).
203            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    /// The same, from the EXIF definitions directly: where the stored image's
221    /// 0th row and 0th column end up (TIFF 6.0 / EXIF 2.3 §4.6.4).
222    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        // Little-endian: II, 42, IFD at 8, one entry, tag 0x0112, SHORT, 1, 6.
278        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        // An IFD offset pointing far past the end.
310        assert_eq!(Orientation::from_exif_block(b"II*\0\xff\xff\xff\xff"), None);
311        // An entry count promising more entries than the block holds.
312        assert_eq!(
313            Orientation::from_exif_block(b"II*\0\x08\0\0\0\xff\xff"),
314            None
315        );
316        // An out-of-range orientation is metadata we decline to trust.
317        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        // One entry, tag 0x0100 (ImageWidth).
324        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}