image/
metadata.rs

1//! Types describing image metadata
2pub(crate) mod cicp;
3
4use std::io::{Cursor, Read};
5
6use byteorder_lite::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt};
7
8pub use self::cicp::{
9    Cicp, CicpColorPrimaries, CicpMatrixCoefficients, CicpTransferCharacteristics, CicpTransform,
10    CicpVideoFullRangeFlag,
11};
12
13/// Describes the transformations to be applied to the image.
14/// Compatible with [Exif orientation](https://web.archive.org/web/20200412005226/https://www.impulseadventure.com/photo/exif-orientation.html).
15///
16/// Orientation is specified in the file's metadata, and is often written by cameras.
17///
18/// You can apply it to an image via [`DynamicImage::apply_orientation`](crate::DynamicImage::apply_orientation).
19#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum Orientation {
22    /// Do not perform any transformations.
23    NoTransforms,
24    /// Rotate by 90 degrees clockwise.
25    Rotate90,
26    /// Rotate by 180 degrees. Can be performed in-place.
27    Rotate180,
28    /// Rotate by 270 degrees clockwise. Equivalent to rotating by 90 degrees counter-clockwise.
29    Rotate270,
30    /// Flip horizontally. Can be performed in-place.
31    FlipHorizontal,
32    /// Flip vertically. Can be performed in-place.
33    FlipVertical,
34    /// Rotate by 90 degrees clockwise and flip horizontally.
35    Rotate90FlipH,
36    /// Rotate by 270 degrees clockwise and flip horizontally.
37    Rotate270FlipH,
38}
39
40impl Orientation {
41    /// Converts from [Exif orientation](https://web.archive.org/web/20200412005226/https://www.impulseadventure.com/photo/exif-orientation.html)
42    #[must_use]
43    pub fn from_exif(exif_orientation: u8) -> Option<Self> {
44        match exif_orientation {
45            1 => Some(Self::NoTransforms),
46            2 => Some(Self::FlipHorizontal),
47            3 => Some(Self::Rotate180),
48            4 => Some(Self::FlipVertical),
49            5 => Some(Self::Rotate90FlipH),
50            6 => Some(Self::Rotate90),
51            7 => Some(Self::Rotate270FlipH),
52            8 => Some(Self::Rotate270),
53            0 | 9.. => None,
54        }
55    }
56
57    /// Converts into [Exif orientation](https://web.archive.org/web/20200412005226/https://www.impulseadventure.com/photo/exif-orientation.html)
58    #[must_use]
59    pub fn to_exif(self) -> u8 {
60        match self {
61            Self::NoTransforms => 1,
62            Self::FlipHorizontal => 2,
63            Self::Rotate180 => 3,
64            Self::FlipVertical => 4,
65            Self::Rotate90FlipH => 5,
66            Self::Rotate90 => 6,
67            Self::Rotate270FlipH => 7,
68            Self::Rotate270 => 8,
69        }
70    }
71
72    /// Extracts the image orientation from a raw Exif chunk.
73    ///
74    /// You can obtain the Exif chunk using
75    /// [ImageDecoder::exif_metadata](crate::ImageDecoder::exif_metadata).
76    ///
77    /// It is more convenient to use [ImageDecoder::orientation](crate::ImageDecoder::orientation)
78    /// than to invoke this function.
79    /// Only use this function if you extract and process the Exif chunk separately.
80    #[must_use]
81    pub fn from_exif_chunk(chunk: &[u8]) -> Option<Self> {
82        Self::from_exif_chunk_inner(chunk).map(|res| res.0)
83    }
84
85    /// Extracts the image orientation from a raw Exif chunk and sets the orientation in the Exif chunk to `Orientation::NoTransforms`.
86    /// This is useful if you want to apply the orientation yourself, and then encode the image with the rest of the Exif chunk intact.
87    ///
88    /// If the orientation data is not cleared from the Exif chunk after you apply the orientation data yourself,
89    /// the image will end up being rotated once again by any software that correctly handles Exif, leading to an incorrect result.
90    ///
91    /// If the Exif value is present but invalid, `None` is returned and the Exif chunk is not modified.
92    #[must_use]
93    pub fn remove_from_exif_chunk(chunk: &mut [u8]) -> Option<Self> {
94        if let Some((orientation, offset, endian)) = Self::from_exif_chunk_inner(chunk) {
95            let mut writer = Cursor::new(chunk);
96            writer.set_position(offset);
97            let no_orientation: u16 = Self::NoTransforms.to_exif().into();
98            match endian {
99                ExifEndian::Big => writer.write_u16::<BigEndian>(no_orientation).unwrap(),
100                ExifEndian::Little => writer.write_u16::<LittleEndian>(no_orientation).unwrap(),
101            }
102            Some(orientation)
103        } else {
104            None
105        }
106    }
107
108    /// Returns the orientation, the offset in the Exif chunk where it was found, and Exif chunk endianness
109    #[must_use]
110    fn from_exif_chunk_inner(chunk: &[u8]) -> Option<(Self, u64, ExifEndian)> {
111        let mut reader = Cursor::new(chunk);
112
113        let mut magic = [0; 4];
114        reader.read_exact(&mut magic).ok()?;
115
116        match magic {
117            [0x49, 0x49, 42, 0] => {
118                return Self::locate_orientation_entry::<LittleEndian>(&mut reader)
119                    .map(|(orient, offset)| (orient, offset, ExifEndian::Little));
120            }
121            [0x4d, 0x4d, 0, 42] => {
122                return Self::locate_orientation_entry::<BigEndian>(&mut reader)
123                    .map(|(orient, offset)| (orient, offset, ExifEndian::Big));
124            }
125            _ => {}
126        }
127        None
128    }
129
130    /// Extracted into a helper function to be generic over endianness
131    fn locate_orientation_entry<B>(reader: &mut Cursor<&[u8]>) -> Option<(Self, u64)>
132    where
133        B: byteorder_lite::ByteOrder,
134    {
135        let ifd_offset = reader.read_u32::<B>().ok()?;
136        reader.set_position(u64::from(ifd_offset));
137        let entries = reader.read_u16::<B>().ok()?;
138        for _ in 0..entries {
139            let tag = reader.read_u16::<B>().ok()?;
140            let format = reader.read_u16::<B>().ok()?;
141            let count = reader.read_u32::<B>().ok()?;
142            let value = reader.read_u16::<B>().ok()?;
143            let _padding = reader.read_u16::<B>().ok()?;
144            if tag == 0x112 && format == 3 && count == 1 {
145                let offset = reader.position() - 4; // we've read 4 bytes (2 * u16) past the start of the value
146                let orientation = Self::from_exif(value.min(255) as u8);
147                return orientation.map(|orient| (orient, offset));
148            }
149        }
150        // If we reached this point without returning early, there was no orientation
151        None
152    }
153}
154
155#[derive(Debug, Copy, Clone)]
156enum ExifEndian {
157    Big,
158    Little,
159}
160
161#[cfg(all(test, feature = "jpeg"))]
162mod tests {
163    use crate::{codecs::jpeg::JpegDecoder, ImageDecoder as _};
164
165    // This brings all the items from the parent module into scope,
166    // so you can directly use `add` instead of `super::add`.
167    use super::*;
168
169    const TEST_IMAGE: &[u8] = include_bytes!("../tests/images/jpg/portrait_2.jpg");
170
171    #[test] // This attribute marks the function as a test function.
172    fn test_extraction_and_clearing() {
173        let reader = Cursor::new(TEST_IMAGE);
174        let mut decoder = JpegDecoder::new(reader).expect("Failed to decode test image");
175        let mut exif_chunk = decoder
176            .exif_metadata()
177            .expect("Failed to extract Exif chunk")
178            .expect("No Exif chunk found in test image");
179
180        let orientation = Orientation::from_exif_chunk(&exif_chunk)
181            .expect("Failed to extract orientation from Exif chunk");
182        assert_eq!(orientation, Orientation::FlipHorizontal);
183
184        let orientation = Orientation::remove_from_exif_chunk(&mut exif_chunk)
185            .expect("Failed to remove orientation from Exif chunk");
186        assert_eq!(orientation, Orientation::FlipHorizontal);
187        // Now that the orientation has been cleared, any subsequent extractions should return NoTransforms
188        let orientation = Orientation::from_exif_chunk(&exif_chunk)
189            .expect("Failed to extract orientation from Exif chunk after clearing it");
190        assert_eq!(orientation, Orientation::NoTransforms);
191    }
192}