image/
metadata.rs

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