1use crate::budget::{Budget, DecodePixels};
17use crate::error::ImageError;
18use crate::measure::{measure, Measured};
19use std::path::Path;
20
21#[derive(Clone, PartialEq, Eq)]
23pub struct Decoded {
24 pub pixels: Vec<u8>,
26 pub size: DecodePixels,
29 pub measured: Measured,
34}
35
36impl std::fmt::Debug for Decoded {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("Decoded")
45 .field("size", &self.size)
46 .field("bytes", &self.pixels.len())
47 .field("measured", &self.measured)
48 .finish()
49 }
50}
51
52pub fn decode_to_fit(path: &Path, budget: &Budget) -> Result<Decoded, ImageError> {
54 let measured = measure(path)?;
55
56 if !budget.allows_source(measured.source) {
57 return Err(ImageError::TooLarge {
58 path: path.to_path_buf(),
59 width: measured.source.width,
60 height: measured.source.height,
61 });
62 }
63
64 let undecodable = |source| ImageError::Undecodable { path: path.to_path_buf(), source };
65
66 let file = std::fs::File::open(path)
67 .map_err(|source| ImageError::Unreadable { path: path.to_path_buf(), source })?;
68 let mut reader = image::ImageReader::new(std::io::BufReader::new(file));
69 reader.set_format(measured.format);
70 reader.limits(budget.limits());
71 let mut decoded = reader.decode().map_err(undecodable)?;
72
73 decoded.apply_orientation(measured.orientation.as_image());
81
82 let target = budget.fit(crate::measure::SourcePixels {
83 width: decoded.width(),
84 height: decoded.height(),
85 });
86 let decoded = if target.width == decoded.width() && target.height == decoded.height() {
87 decoded
88 } else {
89 decoded.thumbnail_exact(target.width, target.height)
107 };
108
109 let rgba = decoded.into_rgba8();
110 Ok(Decoded {
111 size: DecodePixels { width: rgba.width(), height: rgba.height() },
112 pixels: rgba.into_raw(),
113 measured,
114 })
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::budget::ViewportPixels;
121 use crate::orientation::Orientation;
122 use std::path::PathBuf;
123
124 fn write_png(dir: &Path, name: &str, width: u32, height: u32) -> PathBuf {
125 let path = dir.join(name);
126 image::RgbaImage::from_pixel(width, height, image::Rgba([10, 20, 30, 255]))
127 .save(&path)
128 .unwrap();
129 path
130 }
131
132 #[test]
136 fn a_picture_larger_than_the_window_is_never_retained_at_full_size() {
137 let dir = tempfile::tempdir().unwrap();
138 let path = write_png(dir.path(), "big.png", 4000, 3000);
141 let budget = Budget::for_viewport(ViewportPixels { width: 800, height: 600 });
142 let decoded = decode_to_fit(&path, &budget).unwrap();
143
144 assert!(decoded.size.width < 4000, "{:?}", decoded.size);
145 assert_eq!(decoded.pixels.len() as u64, decoded.size.rgba_bytes());
146 assert!(decoded.pixels.len() as u64 <= budget.max_retained_bytes());
147 assert_eq!(decoded.measured.source.width, 4000);
149 }
150
151 #[test]
152 fn a_picture_that_already_fits_is_decoded_as_it_is() {
153 let dir = tempfile::tempdir().unwrap();
154 let path = write_png(dir.path(), "small.png", 320, 240);
155 let budget = Budget::for_viewport(ViewportPixels { width: 1920, height: 1080 });
156 let decoded = decode_to_fit(&path, &budget).unwrap();
157 assert_eq!(decoded.size, DecodePixels { width: 320, height: 240 });
158 }
159
160 #[test]
161 fn the_decoded_buffer_is_exactly_four_bytes_a_pixel() {
162 let dir = tempfile::tempdir().unwrap();
163 let path = write_png(dir.path(), "rgba.png", 7, 5);
164 let budget = Budget::for_edge(1000);
165 let decoded = decode_to_fit(&path, &budget).unwrap();
166 assert_eq!(decoded.pixels.len(), 7 * 5 * 4);
167 }
168
169 #[test]
172 fn debugging_a_decoded_picture_shows_sizes_and_not_pixels() {
173 let dir = tempfile::tempdir().unwrap();
174 let path = write_png(dir.path(), "d.png", 4, 4);
175 let decoded = decode_to_fit(&path, &Budget::for_edge(100)).unwrap();
176 let shown = format!("{decoded:?}");
177 assert!(shown.contains("bytes"));
178 assert!(shown.len() < 400, "{shown}");
179 }
180
181 #[test]
182 fn a_file_that_is_not_a_picture_is_reported_and_not_decoded() {
183 let dir = tempfile::tempdir().unwrap();
184 let path = dir.path().join("x.txt");
185 std::fs::write(&path, b"not a picture at all").unwrap();
186 assert!(decode_to_fit(&path, &Budget::for_edge(100)).is_err());
187 }
188
189 fn jpeg_with_orientation(dir: &Path, name: &str, exif: Option<u16>) -> PathBuf {
196 let mut rgb = image::RgbImage::new(32, 16);
197 for (_, y, pixel) in rgb.enumerate_pixels_mut() {
198 *pixel = if y < 8 { image::Rgb([255, 0, 0]) } else { image::Rgb([0, 0, 255]) };
199 }
200 let mut bytes = Vec::new();
201 image::DynamicImage::ImageRgb8(rgb)
202 .write_to(&mut std::io::Cursor::new(&mut bytes), image::ImageFormat::Jpeg)
203 .unwrap();
204
205 if let Some(value) = exif {
206 let mut app1: Vec<u8> = Vec::new();
213 app1.extend_from_slice(b"Exif\0\0");
214 app1.extend_from_slice(&[0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08]);
215 app1.extend_from_slice(&[0x00, 0x01]);
216 app1.extend_from_slice(&[0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01]);
217 app1.extend_from_slice(&value.to_be_bytes());
218 app1.extend_from_slice(&[0x00, 0x00]);
219 app1.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
220
221 let len = (app1.len() + 2) as u16;
222 let mut spliced = vec![0xFF, 0xD8, 0xFF, 0xE1];
223 spliced.extend_from_slice(&len.to_be_bytes());
224 spliced.extend_from_slice(&app1);
225 spliced.extend_from_slice(&bytes[2..]);
226 bytes = spliced;
227 }
228
229 let path = dir.join(name);
230 std::fs::write(&path, &bytes).unwrap();
231 path
232 }
233
234 fn pixel_at(decoded: &Decoded, x: u32, y: u32) -> (u8, u8, u8) {
235 let i = ((y * decoded.size.width + x) * 4) as usize;
236 (decoded.pixels[i], decoded.pixels[i + 1], decoded.pixels[i + 2])
237 }
238
239 fn is_reddish((r, _g, b): (u8, u8, u8)) -> bool {
240 r > 150 && b < 100
241 }
242
243 fn is_bluish((r, _g, b): (u8, u8, u8)) -> bool {
244 b > 150 && r < 100
245 }
246
247 #[test]
252 fn without_an_exif_tag_the_picture_is_not_turned() {
253 let dir = tempfile::tempdir().unwrap();
254 let path = jpeg_with_orientation(dir.path(), "plain.jpg", None);
255 let decoded = decode_to_fit(&path, &Budget::for_edge(1000)).unwrap();
256
257 assert_eq!((decoded.size.width, decoded.size.height), (32, 16));
258 assert!(is_reddish(pixel_at(&decoded, 16, 2)), "top should be red");
259 assert!(is_bluish(pixel_at(&decoded, 16, 13)), "bottom should be blue");
260 }
261
262 #[test]
266 fn a_portrait_photo_is_not_shown_sideways() {
267 let dir = tempfile::tempdir().unwrap();
268 let path = jpeg_with_orientation(dir.path(), "turned.jpg", Some(6));
269 let decoded = decode_to_fit(&path, &Budget::for_edge(1000)).unwrap();
270
271 assert_eq!((decoded.size.width, decoded.size.height), (16, 32));
273 assert!(is_reddish(pixel_at(&decoded, 13, 16)), "right column should be red");
275 assert!(is_bluish(pixel_at(&decoded, 2, 16)), "left column should be blue");
276 }
277
278 #[test]
281 fn a_turned_photo_reports_the_size_it_presents() {
282 let dir = tempfile::tempdir().unwrap();
283 let path = jpeg_with_orientation(dir.path(), "turned2.jpg", Some(6));
284 let measured = crate::measure::measure(&path).unwrap();
285 assert_eq!(measured.orientation, Orientation::Rotate90);
286 assert_eq!(measured.source, crate::measure::SourcePixels { width: 32, height: 16 });
287 assert_eq!(measured.display_size(), (16, 32));
288 }
289
290 #[test]
293 fn a_decoded_picture_reports_the_shape_it_will_be_shown_in() {
294 let dir = tempfile::tempdir().unwrap();
295 let path = write_png(dir.path(), "wide.png", 400, 200);
296 let decoded = decode_to_fit(&path, &Budget::for_edge(1000)).unwrap();
297 assert_eq!(decoded.measured.orientation, Orientation::Upright);
298 assert_eq!((decoded.size.width, decoded.size.height), decoded.measured.display_size());
299 }
300}