Skip to main content

flashraw_core/
orientation.rs

1//! Where an image's orientation comes from, and how competing records are resolved.
2//!
3//! A RAW file can state its orientation in more than one place: the standard
4//! TIFF/EXIF `Orientation` tag and a vendor record (a MakerNote field such as Canon's
5//! CameraOrientation, the rotation saved by a vendor's own editing software, or the
6//! EXIF block of the camera's own embedded JPEG). The records do not always agree,
7//! and which one describes the image depends on how the camera (or software) wrote
8//! them, so the choice is made per camera with an [`OrientationPolicy`].
9//!
10//! [`OrientationMetadata`] keeps both records, the chosen value and why it was
11//! chosen. [`RawMetadata::orientation`](crate::RawMetadata::orientation) stays the
12//! resolved value.
13
14use crate::types::Orientation;
15
16/// Which record the resolved orientation was taken from.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[non_exhaustive]
20pub enum OrientationSource {
21    /// The standard TIFF/EXIF `Orientation` tag (or the container's own standard
22    /// orientation field for non-TIFF containers).
23    Exif,
24    /// A vendor record: MakerNote, vendor edit data or the camera's embedded JPEG.
25    MakerNote,
26    /// A fixed per-camera rule (e.g. a sensor that is mounted rotated in the body).
27    CameraProfileOverride,
28    /// Nothing usable was recorded; the image is shown as stored.
29    #[default]
30    Default,
31}
32
33impl OrientationSource {
34    /// Stable lower-case name (`"exif"`, `"maker-note"`, `"camera-profile"`, `"default"`).
35    pub fn name(self) -> &'static str {
36        match self {
37            OrientationSource::Exif => "exif",
38            OrientationSource::MakerNote => "maker-note",
39            OrientationSource::CameraProfileOverride => "camera-profile",
40            OrientationSource::Default => "default",
41        }
42    }
43}
44
45/// How the standard and vendor orientation records of a camera are combined.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48#[non_exhaustive]
49pub enum OrientationPolicy {
50    /// Only the standard tag is used; vendor records are informational.
51    #[default]
52    ExifOnly,
53    /// The vendor record wins whenever it is present.
54    MakerNotePreferred,
55    /// The vendor record is used when the standard tag is Normal, missing or invalid
56    /// (for cameras that only write the rotation into the vendor record).
57    MakerNoteWhenExifNormal,
58    /// Canon: the camera writes the standard tag from its orientation sensor only when
59    /// its Auto Rotate setting is on, and always records the sensor in the MakerNote
60    /// (CameraOrientation). A Normal standard tag is therefore what the camera chose
61    /// unless the recorded Auto Rotate setting says it was on; the MakerNote replaces
62    /// the standard tag only when that tag is missing or invalid, or when Auto Rotate
63    /// was on and the tag still says Normal.
64    CanonAutoRotateAware,
65}
66
67impl OrientationPolicy {
68    /// Stable kebab-case name, as used in the camera database.
69    pub fn name(self) -> &'static str {
70        match self {
71            OrientationPolicy::ExifOnly => "exif-only",
72            OrientationPolicy::MakerNotePreferred => "makernote-preferred",
73            OrientationPolicy::MakerNoteWhenExifNormal => "makernote-when-exif-normal",
74            OrientationPolicy::CanonAutoRotateAware => "canon-auto-rotate-aware",
75        }
76    }
77
78    /// Parse a name produced by [`OrientationPolicy::name`].
79    pub fn parse(s: &str) -> Option<Self> {
80        Some(match s.trim() {
81            "exif-only" => OrientationPolicy::ExifOnly,
82            "makernote-preferred" => OrientationPolicy::MakerNotePreferred,
83            "makernote-when-exif-normal" => OrientationPolicy::MakerNoteWhenExifNormal,
84            "canon-auto-rotate-aware" => OrientationPolicy::CanonAutoRotateAware,
85            _ => return None,
86        })
87    }
88}
89
90/// Orientation records of a file and the value resolved from them.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93#[non_exhaustive]
94pub struct OrientationMetadata {
95    /// The standard TIFF/EXIF `Orientation` tag; `None` when missing or not a valid
96    /// value (1-8).
97    pub exif: Option<Orientation>,
98    /// The vendor orientation record (MakerNote or equivalent), when present.
99    pub maker_note: Option<Orientation>,
100    /// The camera's Auto Rotate setting (`Some(true)` = on), when recorded.
101    pub auto_rotate: Option<bool>,
102    /// The orientation to apply to the image (equal to `RawMetadata::orientation`).
103    pub resolved: Orientation,
104    /// Which record `resolved` comes from.
105    pub source: OrientationSource,
106    /// The policy that chose `resolved`.
107    pub policy: OrientationPolicy,
108}
109
110impl OrientationMetadata {
111    /// Combine the records of a file under `policy`.
112    pub fn resolve(
113        exif: Option<Orientation>,
114        maker_note: Option<Orientation>,
115        auto_rotate: Option<bool>,
116        policy: OrientationPolicy,
117    ) -> Self {
118        use OrientationSource as S;
119        let normal = Orientation::Normal;
120        let from_exif = || exif.map(|o| (o, S::Exif));
121        let from_mn = || maker_note.map(|o| (o, S::MakerNote));
122        let chosen = match policy {
123            OrientationPolicy::ExifOnly => from_exif(),
124            OrientationPolicy::MakerNotePreferred => from_mn().or_else(from_exif),
125            OrientationPolicy::MakerNoteWhenExifNormal => match exif {
126                Some(o) if o != normal => from_exif(),
127                _ => from_mn().or_else(from_exif),
128            },
129            OrientationPolicy::CanonAutoRotateAware => match (exif, maker_note) {
130                (Some(o), _) if o != normal => from_exif(),
131                (None, _) => from_mn(),
132                (Some(_), Some(m)) if m != normal && auto_rotate == Some(true) => from_mn(),
133                (Some(_), _) => from_exif(),
134            },
135        };
136        let (resolved, source) = chosen.unwrap_or((normal, S::Default));
137        OrientationMetadata { exif, maker_note, auto_rotate, resolved, source, policy }
138    }
139
140    /// Only the container's standard orientation field is known (`Normal` is treated
141    /// as "nothing recorded").
142    pub fn from_container(orientation: Orientation) -> Self {
143        let exif = (orientation != Orientation::Normal).then_some(orientation);
144        Self::resolve(exif, None, None, OrientationPolicy::ExifOnly)
145    }
146
147    /// A fixed per-camera orientation that replaces whatever the file records.
148    pub fn camera_override(mut self, orientation: Orientation) -> Self {
149        self.resolved = orientation;
150        self.source = OrientationSource::CameraProfileOverride;
151        self
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use Orientation::*;
159    use OrientationPolicy as P;
160    use OrientationSource as S;
161
162    fn r(e: Option<Orientation>, m: Option<Orientation>, a: Option<bool>, p: P) -> (Orientation, S) {
163        let o = OrientationMetadata::resolve(e, m, a, p);
164        assert_eq!((o.exif, o.maker_note, o.auto_rotate, o.policy), (e, m, a, p));
165        (o.resolved, o.source)
166    }
167
168    #[test]
169    fn exif_only_ignores_vendor_records() {
170        assert_eq!(r(Some(Normal), Some(Rotate90), None, P::ExifOnly), (Normal, S::Exif));
171        assert_eq!(r(Some(Rotate270), Some(Rotate90), None, P::ExifOnly), (Rotate270, S::Exif));
172        assert_eq!(r(None, Some(Rotate90), None, P::ExifOnly), (Normal, S::Default));
173        assert_eq!(r(None, None, None, P::ExifOnly), (Normal, S::Default));
174    }
175
176    #[test]
177    fn maker_note_preferred() {
178        assert_eq!(r(Some(Rotate270), Some(Rotate90), None, P::MakerNotePreferred), (Rotate90, S::MakerNote));
179        assert_eq!(r(Some(Rotate270), Some(Normal), None, P::MakerNotePreferred), (Normal, S::MakerNote));
180        assert_eq!(r(Some(Rotate180), None, None, P::MakerNotePreferred), (Rotate180, S::Exif));
181        assert_eq!(r(None, None, None, P::MakerNotePreferred), (Normal, S::Default));
182    }
183
184    #[test]
185    fn maker_note_when_exif_normal() {
186        let p = P::MakerNoteWhenExifNormal;
187        assert_eq!(r(Some(Normal), Some(Rotate90), None, p), (Rotate90, S::MakerNote));
188        assert_eq!(r(None, Some(Rotate270), None, p), (Rotate270, S::MakerNote));
189        assert_eq!(r(Some(Rotate180), Some(Rotate90), None, p), (Rotate180, S::Exif));
190        assert_eq!(r(Some(Normal), None, None, p), (Normal, S::Exif));
191        assert_eq!(r(Some(Normal), Some(Normal), None, p), (Normal, S::MakerNote));
192    }
193
194    #[test]
195    fn canon_auto_rotate_aware() {
196        let p = P::CanonAutoRotateAware;
197        // The camera tagged the rotation itself.
198        assert_eq!(r(Some(Rotate270), Some(Rotate270), Some(true), p), (Rotate270, S::Exif));
199        assert_eq!(r(Some(Rotate90), Some(Normal), None, p), (Rotate90, S::Exif));
200        // Auto Rotate off: the camera deliberately left the image unrotated.
201        assert_eq!(r(Some(Normal), Some(Rotate270), Some(false), p), (Normal, S::Exif));
202        // Setting not recorded: a Normal tag is the camera's choice.
203        assert_eq!(r(Some(Normal), Some(Rotate90), None, p), (Normal, S::Exif));
204        // Auto Rotate on but the tag says Normal: the sensor record is authoritative.
205        assert_eq!(r(Some(Normal), Some(Rotate90), Some(true), p), (Rotate90, S::MakerNote));
206        // Missing or invalid tag: the MakerNote fills in.
207        assert_eq!(r(None, Some(Rotate270), None, p), (Rotate270, S::MakerNote));
208        assert_eq!(r(None, Some(Normal), Some(false), p), (Normal, S::MakerNote));
209        assert_eq!(r(None, None, None, p), (Normal, S::Default));
210    }
211
212    #[test]
213    fn container_and_override() {
214        let c = OrientationMetadata::from_container(Rotate90);
215        assert_eq!((c.exif, c.resolved, c.source), (Some(Rotate90), Rotate90, S::Exif));
216        let c = OrientationMetadata::from_container(Normal);
217        assert_eq!((c.exif, c.resolved, c.source), (None, Normal, S::Default));
218        let o = c.camera_override(Rotate90);
219        assert_eq!((o.resolved, o.source), (Rotate90, S::CameraProfileOverride));
220    }
221
222    #[test]
223    fn policy_names_round_trip() {
224        for p in [P::ExifOnly, P::MakerNotePreferred, P::MakerNoteWhenExifNormal, P::CanonAutoRotateAware] {
225            assert_eq!(P::parse(p.name()), Some(p));
226        }
227        assert_eq!(P::parse("bogus"), None);
228    }
229}