hyprforge_image/
measure.rs1use crate::error::ImageError;
20use crate::orientation::Orientation;
21use std::path::{Path, PathBuf};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct SourcePixels {
30 pub width: u32,
31 pub height: u32,
32}
33
34impl SourcePixels {
35 pub fn count(self) -> u64 {
38 u64::from(self.width) * u64::from(self.height)
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Measured {
45 pub source: SourcePixels,
46 pub orientation: Orientation,
47 pub format: image::ImageFormat,
48}
49
50impl Measured {
51 pub fn display_size(&self) -> (u32, u32) {
54 self.orientation.applied_size(self.source.width, self.source.height)
55 }
56}
57
58pub fn measure(path: &Path) -> Result<Measured, ImageError> {
60 let unreadable = |source| ImageError::Unreadable { path: path.to_path_buf(), source };
61
62 let file = std::fs::File::open(path).map_err(unreadable)?;
63 let reader = image::ImageReader::new(std::io::BufReader::new(file))
64 .with_guessed_format()
65 .map_err(unreadable)?;
66
67 let format = reader.format().ok_or_else(|| ImageError::Undecodable {
68 path: path.to_path_buf(),
69 source: image::ImageError::Unsupported(
70 image::error::ImageFormatHint::Unknown.into(),
71 ),
72 })?;
73
74 let (width, height) = reader.into_dimensions().map_err(|source| ImageError::Undecodable {
75 path: path.to_path_buf(),
76 source,
77 })?;
78
79 Ok(Measured {
80 source: SourcePixels { width, height },
81 orientation: read_orientation(path),
82 format,
83 })
84}
85
86fn read_orientation(path: &Path) -> Orientation {
94 let Ok(file) = std::fs::File::open(path) else {
95 return Orientation::Upright;
96 };
97 let mut reader = std::io::BufReader::new(file);
98 match image::ImageReader::new(&mut reader).with_guessed_format() {
99 Ok(r) => match r.into_decoder() {
100 Ok(mut decoder) => match image::ImageDecoder::orientation(&mut decoder) {
101 Ok(o) => from_image_orientation(o),
102 Err(_) => Orientation::Upright,
103 },
104 Err(_) => Orientation::Upright,
105 },
106 Err(_) => Orientation::Upright,
107 }
108}
109
110fn from_image_orientation(o: image::metadata::Orientation) -> Orientation {
111 match o {
112 image::metadata::Orientation::NoTransforms => Orientation::Upright,
113 image::metadata::Orientation::Rotate90 => Orientation::Rotate90,
114 image::metadata::Orientation::Rotate180 => Orientation::Rotate180,
115 image::metadata::Orientation::Rotate270 => Orientation::Rotate270,
116 image::metadata::Orientation::FlipHorizontal => Orientation::FlipHorizontal,
117 image::metadata::Orientation::FlipVertical => Orientation::FlipVertical,
118 image::metadata::Orientation::Rotate90FlipH => Orientation::Transpose,
119 image::metadata::Orientation::Rotate270FlipH => Orientation::Transverse,
120 }
121}
122
123pub fn measured_path(path: &Path) -> PathBuf {
125 path.to_path_buf()
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn write_png(dir: &Path, name: &str, width: u32, height: u32) -> PathBuf {
133 let path = dir.join(name);
134 image::RgbaImage::from_pixel(width, height, image::Rgba([1, 2, 3, 255]))
135 .save(&path)
136 .unwrap();
137 path
138 }
139
140 #[test]
141 fn a_header_gives_the_size_without_decoding_the_picture() {
142 let dir = tempfile::tempdir().unwrap();
143 let path = write_png(dir.path(), "a.png", 640, 480);
144 let measured = measure(&path).unwrap();
145 assert_eq!(measured.source, SourcePixels { width: 640, height: 480 });
146 assert_eq!(measured.format, image::ImageFormat::Png);
147 assert_eq!(measured.display_size(), (640, 480));
148 }
149
150 #[test]
153 fn a_file_that_is_not_a_picture_is_undecodable_not_unreadable() {
154 let dir = tempfile::tempdir().unwrap();
155 let path = dir.path().join("notes.txt");
156 std::fs::write(&path, b"this is not a picture").unwrap();
157 assert!(matches!(measure(&path), Err(ImageError::Undecodable { .. })));
158 }
159
160 #[test]
161 fn a_file_that_is_not_there_is_unreadable() {
162 let dir = tempfile::tempdir().unwrap();
163 let path = dir.path().join("gone.png");
164 assert!(matches!(measure(&path), Err(ImageError::Unreadable { .. })));
165 }
166
167 #[test]
170 fn a_picture_with_no_exif_measures_as_upright() {
171 let dir = tempfile::tempdir().unwrap();
172 let path = write_png(dir.path(), "b.png", 100, 50);
173 assert_eq!(measure(&path).unwrap().orientation, Orientation::Upright);
174 }
175
176 #[test]
179 fn a_truncated_file_is_reported_rather_than_panicking() {
180 let dir = tempfile::tempdir().unwrap();
181 let full = write_png(dir.path(), "c.png", 200, 200);
182 let bytes = std::fs::read(&full).unwrap();
183 let cut = dir.path().join("cut.png");
184 std::fs::write(&cut, &bytes[..bytes.len() / 3]).unwrap();
185 let _ = measure(&cut);
188 }
189}