videre_core/
image_decode.rs1use std::path::Path;
14
15use image::ImageDecoder;
16
17pub fn decode_oriented_file(path: &Path) -> image::ImageResult<image::DynamicImage> {
18 let (img, orientation) = decode_raw_with_orientation(path)?;
19 Ok(apply(img, orientation))
20}
21
22pub fn decode_oriented_reader<R: std::io::BufRead + std::io::Seek>(
26 reader: R,
27) -> image::ImageResult<image::DynamicImage> {
28 let (img, orientation) = decode_raw_with_orientation_reader(reader)?;
29 Ok(apply(img, orientation))
30}
31
32pub fn decode_oriented_bytes(bytes: &[u8]) -> Option<image::DynamicImage> {
33 let decoder = image::ImageReader::new(std::io::Cursor::new(bytes))
34 .with_guessed_format()
35 .ok()?
36 .into_decoder()
37 .ok()?;
38 let (img, orientation) = decode_decoder(decoder).ok()?;
39 Some(apply(img, orientation))
40}
41
42pub fn decode_raw_with_orientation(
43 path: &Path,
44) -> image::ImageResult<(image::DynamicImage, image::metadata::Orientation)> {
45 let reader = std::io::BufReader::new(std::fs::File::open(path)?);
46 decode_raw_with_orientation_reader(reader)
47}
48
49pub fn decode_raw_with_orientation_reader<R: std::io::BufRead + std::io::Seek>(
50 reader: R,
51) -> image::ImageResult<(image::DynamicImage, image::metadata::Orientation)> {
52 let decoder = image::ImageReader::new(reader)
53 .with_guessed_format()?
54 .into_decoder()?;
55 decode_decoder(decoder)
56}
57
58fn decode_decoder<D: ImageDecoder>(
59 mut decoder: D,
60) -> image::ImageResult<(image::DynamicImage, image::metadata::Orientation)> {
61 let orientation = decoder
62 .orientation()
63 .unwrap_or(image::metadata::Orientation::NoTransforms);
64 let img = image::DynamicImage::from_decoder(decoder)?;
65 Ok((img, orientation))
66}
67
68fn apply(
69 mut img: image::DynamicImage,
70 orientation: image::metadata::Orientation,
71) -> image::DynamicImage {
72 img.apply_orientation(orientation);
73 img
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use image::GenericImage;
80
81 fn jpeg_with_orientation(orientation: u16) -> Vec<u8> {
85 let mut img = image::DynamicImage::new_rgb8(4, 2);
86 for y in 0..2u32 {
89 for x in 0..4u32 {
90 img.put_pixel(
91 x,
92 y,
93 if y == 0 {
94 image::Rgba([255, 255, 255, 255])
95 } else {
96 image::Rgba([0, 0, 0, 255])
97 },
98 );
99 }
100 }
101 let mut jpeg = Vec::new();
102 img.write_to(
103 &mut std::io::Cursor::new(&mut jpeg),
104 image::ImageFormat::Jpeg,
105 )
106 .unwrap();
107
108 let mut tiff = Vec::new();
109 tiff.extend_from_slice(b"MM");
110 tiff.extend_from_slice(&42u16.to_be_bytes());
111 tiff.extend_from_slice(&8u32.to_be_bytes()); tiff.extend_from_slice(&1u16.to_be_bytes()); tiff.extend_from_slice(&0x0112u16.to_be_bytes()); tiff.extend_from_slice(&3u16.to_be_bytes()); tiff.extend_from_slice(&1u32.to_be_bytes()); tiff.extend_from_slice(&(orientation as u16).to_be_bytes());
117 tiff.extend_from_slice(&0u16.to_be_bytes()); tiff.extend_from_slice(&0u32.to_be_bytes()); let mut app1 = Vec::new();
121 app1.extend_from_slice(b"Exif\0\0");
122 app1.extend_from_slice(&tiff);
123
124 let mut out = Vec::new();
125 out.extend_from_slice(&jpeg[0..2]); out.extend_from_slice(&[0xFF, 0xE1]); out.extend_from_slice(&(app1.len() as u16 + 2).to_be_bytes());
128 out.extend_from_slice(&app1);
129 out.extend_from_slice(&jpeg[2..]);
130 out
131 }
132
133 fn top_left_is_white(img: &image::DynamicImage) -> bool {
134 let rgb = img.to_rgb8();
135 rgb.get_pixel(0, 0)[0] > 128
136 }
137
138 #[test]
139 fn untagged_file_passes_through_untouched() {
140 let mut plain = Vec::new();
141 let img = image::DynamicImage::new_rgb8(4, 2);
142 img.write_to(
143 &mut std::io::Cursor::new(&mut plain),
144 image::ImageFormat::Jpeg,
145 )
146 .unwrap();
147 let decoded = decode_oriented_bytes(&plain).unwrap();
148 assert_eq!((decoded.width(), decoded.height()), (4, 2));
149 }
150
151 #[test]
152 fn orientation6_yields_the_display_canvas() {
153 let bytes = jpeg_with_orientation(6);
156 let decoded = decode_oriented_bytes(&bytes).unwrap();
157 assert_eq!((decoded.width(), decoded.height()), (2, 4));
158 let rgb = decoded.to_rgb8();
159 assert!(
160 rgb.get_pixel(1, 0)[0] > 128,
161 "white row must move to the right column"
162 );
163 }
164
165 #[test]
166 fn file_and_bytes_variants_agree() {
167 let dir = tempfile::tempdir().unwrap();
168 let path = dir.path().join("o6.jpg");
169 std::fs::write(&path, jpeg_with_orientation(6)).unwrap();
170 let via_file = decode_oriented_file(&path).unwrap();
171 let via_bytes = decode_oriented_bytes(&jpeg_with_orientation(6)).unwrap();
172 assert_eq!(
173 (via_file.width(), via_file.height()),
174 (via_bytes.width(), via_bytes.height())
175 );
176 assert_eq!(top_left_is_white(&via_file), top_left_is_white(&via_bytes));
177 }
178
179 #[test]
180 fn raw_variant_returns_the_tag_for_the_caller_to_apply() {
181 let dir = tempfile::tempdir().unwrap();
182 let path = dir.path().join("o6.jpg");
183 std::fs::write(&path, jpeg_with_orientation(6)).unwrap();
184 let (img, orientation) = decode_raw_with_orientation(&path).unwrap();
185 assert_eq!(
186 (img.width(), img.height()),
187 (4, 2),
188 "raw canvas is untouched"
189 );
190 assert!(matches!(
191 orientation,
192 image::metadata::Orientation::Rotate90
193 ));
194 }
195}