Coverage Report

Created: 2026-08-13 08:17

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