trail/workers/
image_decode.rs1use std::path::PathBuf;
15
16use image::GenericImageView;
17use tokio::sync::mpsc;
18
19use crate::preview::graphics::{self, ImagePreview};
20use crate::preview::provider::PreviewContent;
21use crate::workers::WorkerMsg;
22
23pub fn spawn_image_decode(path: PathBuf, generation: u64, tx: mpsc::Sender<WorkerMsg>) {
31 tokio::spawn(async move {
32 let path_clone = path.clone();
33 let content = tokio::task::spawn_blocking(move || decode_image_sync(&path_clone))
34 .await
35 .unwrap_or_else(|_| PreviewContent::Binary(vec!["[image decode error]".to_owned()]));
36
37 let msg = WorkerMsg::ImageMeta {
38 generation,
39 path: path.clone(),
40 content,
41 };
42 let _ = tx.send(msg).await;
44 });
45}
46
47fn decode_image_sync(path: &std::path::Path) -> PreviewContent {
55 let image = match image::open(path) {
56 Ok(image) => image,
57 Err(e) => {
58 return PreviewContent::Binary(vec![
59 format!(" Type : {} image", format_label(path)),
60 format!(" Size : {}", file_size(path)),
61 String::new(),
62 format!(" Cannot decode: {e}"),
63 ])
64 }
65 };
66
67 let (width, height) = image.dimensions();
68 let colour = image.color();
69 let (cell_width, cell_height) = graphics::cell_size();
70 let protocol = graphics::active();
71
72 let caption = format!(
73 " {} · {}×{} px · {} · {} {}×{}",
74 format_label(path),
75 width,
76 height,
77 file_size(path),
78 protocol.label(),
79 cell_width,
80 cell_height,
81 );
82
83 match graphics::build(image) {
84 Some(protocol) => PreviewContent::Image(ImagePreview::new(protocol, caption)),
85 None => PreviewContent::Binary(vec![
87 format!(" Type : {} image", format_label(path)),
88 format!(" Size : {}", file_size(path)),
89 format!(" Dims : {width}×{height} px"),
90 format!(" Colour : {colour:?}"),
91 String::new(),
92 " (image previews disabled: [preview] image_protocol = \"none\")".to_owned(),
93 ]),
94 }
95}
96
97fn format_label(path: &std::path::Path) -> String {
99 path.extension()
100 .and_then(|e| e.to_str())
101 .map(|e| e.to_uppercase())
102 .unwrap_or_else(|| "Unknown".to_owned())
103}
104
105fn file_size(path: &std::path::Path) -> String {
107 std::fs::metadata(path)
108 .map(|m| humansize::format_size(m.len(), humansize::DECIMAL))
109 .unwrap_or_else(|_| "unknown".to_owned())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use std::io::Write;
116
117 fn sample_png() -> tempfile::NamedTempFile {
119 let image = image::DynamicImage::new_rgb8(4, 3);
120 let mut bytes = std::io::Cursor::new(Vec::new());
121 image
122 .write_to(&mut bytes, image::ImageFormat::Png)
123 .expect("encode sample png");
124
125 let mut file = tempfile::Builder::new()
126 .suffix(".png")
127 .tempfile()
128 .expect("temp file");
129 file.write_all(&bytes.into_inner()).expect("write png");
130 file.flush().expect("flush png");
131 file
132 }
133
134 #[test]
135 fn decodes_a_real_image_into_an_image_preview() {
136 let file = sample_png();
137 match decode_image_sync(file.path()) {
140 PreviewContent::Image(preview) => {
141 assert!(
142 preview.caption.contains("4×3 px"),
143 "caption should carry the decoded dimensions, got {:?}",
144 preview.caption
145 );
146 }
147 other => panic!("expected an image preview, got {other:?}"),
148 }
149 }
150
151 #[test]
152 fn undecodable_file_falls_back_to_metadata() {
153 let mut file = tempfile::Builder::new()
154 .suffix(".png")
155 .tempfile()
156 .expect("temp file");
157 file.write_all(b"not actually a png").expect("write");
158 file.flush().expect("flush");
159
160 match decode_image_sync(file.path()) {
161 PreviewContent::Binary(lines) => {
162 assert!(
163 lines.iter().any(|l| l.contains("Cannot decode")),
164 "expected a decode failure line, got {lines:?}"
165 );
166 }
167 other => panic!("expected metadata fallback, got {other:?}"),
168 }
169 }
170
171 #[test]
172 fn format_label_falls_back_when_there_is_no_extension() {
173 assert_eq!(format_label(std::path::Path::new("photo.JpEg")), "JPEG");
174 assert_eq!(format_label(std::path::Path::new("photo")), "Unknown");
175 }
176}