/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 | | } |