Skip to main content

ai_image/
metadata.rs

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