1use crate::db;
2use regex::{self, Regex};
3use std::sync::mpsc;
4use std::{
5 collections::HashMap,
6 path::PathBuf,
7 process::{Command, Output, Stdio},
8 thread,
9 time::Instant,
10};
11
12pub const CHUNK_SIZE: &usize = &500;
14pub const METADATA_PROFILE_DESCRIPTION: &str = "Profile Description";
15pub const METADATA_ORIENTATION: &str = "Orientation";
16pub const METADATA_DIRECTORY: &str = "Directory";
17pub const METADATA_DATE: &str = "Date/Time Original";
18
19pub enum Orientation {
20 Normal,
21 MirrorHorizontal,
22 Rotate180,
23 MirrorVertical,
24 MirrorHorizontalRotate270,
25 Rotate90CW,
26 MirrorHorizontalRotate90CW,
27 Rotate270CW,
28}
29
30impl Orientation {
31 pub fn from_orientation_metadata(orientation: &str) -> Orientation {
32 match orientation {
33 "Horizontal (normal)" => Orientation::Normal,
34 "Mirror horizontal" => Orientation::MirrorHorizontal,
35 "Rotate 180" => Orientation::Rotate180,
36 "Mirror vertical" => Orientation::MirrorVertical,
37 "Mirror horizontal and rotate 270 CW" => Orientation::MirrorHorizontalRotate270,
38 "Rotate 90 CW" => Orientation::Rotate90CW,
39 "Mirror horizontal and rotate 90 CW" => Orientation::MirrorHorizontalRotate90CW,
40 "Rotate 270 CW" => Orientation::Rotate270CW,
41 _ => Orientation::Normal,
42 }
43 }
44}
45
46pub struct Metadata {}
47
48impl Metadata {
49 pub fn cache_metadata_for_images_in_background(image_paths: &[PathBuf]) {
50 let image_paths = image_paths.to_vec();
51 thread::spawn(move || {
52 Self::cache_metadata_for_images(&image_paths);
53 });
54 }
55
56 pub fn cache_metadata_for_images(image_paths: &[PathBuf]) {
57 let timer = Instant::now();
58
59 let mut image_paths = image_paths
60 .iter()
61 .map(|p| p.to_string_lossy().to_string())
62 .collect::<Vec<String>>();
63
64 let cached_paths = match db::Db::get_cached_images_by_paths(&image_paths) {
65 Ok(cached_paths) => cached_paths,
66 Err(e) => {
67 println!("Failure fetching cached metadata paths, aborting caching process {e}");
68 return;
69 }
70 };
71
72 image_paths.retain(|x| !cached_paths.contains(x));
73
74 let single_image_path = if image_paths.len() == 1 {
76 Some(&image_paths[0])
77 } else {
78 None
79 };
80
81 let chunks: Vec<&[String]> = image_paths.chunks(*CHUNK_SIZE).collect();
82 let total_chunks = chunks.len();
83 let mut total_elapsed_time_ms = 0u128;
84
85 println!(
86 "Caching a total of {} imgs in {} chunks",
87 image_paths.len(),
88 total_chunks
89 );
90
91 for (i, chunk) in chunks.iter().enumerate() {
92 println!("Caching chunk {i} of {}", chunks.len());
93
94 let chunk_timer = Instant::now();
95
96 let (tx, rx) = mpsc::channel();
97 let mut handles = vec![];
98 let chunks: Vec<&[String]> = chunk.chunks(*CHUNK_SIZE / 4).collect();
101 for chunk in chunks {
102 let tx = tx.clone();
103 let chunk = chunk.to_vec();
104 let handle = thread::spawn(move || {
105 let cmd = Command::new("exiftool")
106 .args(chunk)
107 .stdout(Stdio::piped())
108 .spawn();
109
110 match cmd {
111 Ok(cmd) => match cmd.wait_with_output() {
112 Ok(output) => {
113 tx.send(output).unwrap();
114 }
115 Err(e) => println!("Error fetching metadata -> {e}"),
116 },
117 Err(e) => println!("Error fetching metadata -> {e}"),
118 };
119 });
120
121 handles.push(handle);
122 }
123
124 for handle in handles {
125 handle.join().unwrap(); }
127
128 drop(tx);
129
130 for output in rx {
131 Self::parse_exiftool_output(&output, single_image_path);
132 }
133
134 let chunk_elapsed_ms = chunk_timer.elapsed().as_millis();
135 total_elapsed_time_ms += chunk_elapsed_ms;
136 let processed_chunks_count = (i + 1) as u128;
137
138 println!(
139 "Cached metadata chunk containing {} images in {}ms",
140 chunk.len(),
141 chunk_elapsed_ms
142 );
143
144 if processed_chunks_count < total_chunks as u128 {
145 let avg_time_per_chunk_ms = total_elapsed_time_ms / processed_chunks_count;
146 let remaining_chunks = (total_chunks as u128) - processed_chunks_count;
147 let estimated_remaining_ms = avg_time_per_chunk_ms * remaining_chunks;
148
149 let estimated_remaining_seconds = estimated_remaining_ms / 1000;
150 let estimated_remaining_minutes = estimated_remaining_seconds / 60;
151 let estimated_remaining_seconds_remainder = estimated_remaining_seconds % 60;
152
153 println!(
154 "Estimated time remaining: {}m {}s",
155 estimated_remaining_minutes, estimated_remaining_seconds_remainder
156 );
157 }
158 }
159
160 println!(
161 "Finished caching metadata for all images in {}ms",
162 timer.elapsed().as_millis()
163 );
164 }
165
166 pub fn parse_exiftool_output(output: &Output, path: Option<&String>) {
167 let re = regex::Regex::new(r"========").unwrap();
169
170 let string_output = String::from_utf8_lossy(&output.stdout);
171
172 let mut metadata_to_insert: Vec<(String, String)> = vec![];
173 for image_metadata in re.split(&string_output) {
174 if let Some((path, tags)) = Self::parse_exiftool_output_str(image_metadata) {
175 let metadata_json = match serde_json::to_string(&tags) {
176 Ok(json) => json,
177 Err(e) => {
178 println!("Failure serializing metadata into json -> {e}");
179 continue;
180 }
181 };
182 metadata_to_insert.push((path, metadata_json))
183 }
184 }
185
186 if let Some(path) = path {
189 metadata_to_insert[0].0 = path.clone()
190 }
191
192 match db::Db::insert_files_metadata(metadata_to_insert) {
193 Ok(_) => {}
194 Err(e) => {
195 println!("Failure inserting metadata into db -> {e}");
196 }
197 }
198 }
199
200 pub fn parse_exiftool_output_str(output: &str) -> Option<(String, HashMap<String, String>)> {
201 let lines: Vec<&str> = output.split('\n').collect();
202 let file_path = lines.first()?;
203
204 if file_path.is_empty() {
205 return None;
206 }
207
208 let tags = output
209 .lines()
210 .filter(|x| !x.is_empty() && x.contains(':'))
211 .filter_map(|x| {
212 let split: Vec<&str> = x.split(':').collect();
213
214 if split.len() < 2 {
215 return None;
216 }
217
218 let first = String::from(split[0].trim());
219 let last = String::from(split[1..].join(":").trim());
220
221 Some((first, last))
222 })
223 .collect();
224
225 Some((file_path.trim().to_string(), tags))
226 }
227
228 pub fn get_image_metadata(path: &str) -> Option<HashMap<String, String>> {
229 match db::Db::get_image_metadata(path) {
230 Ok(opt) => {
231 if let Some(data) = opt {
232 return Some(serde_json::from_str(&data).unwrap_or_default());
233 }
234 }
235 Err(e) => println!("Error fetching image metadata from db -> {e}"),
236 };
237
238 println!("Metadata not yet in database, fetching for {path}");
239
240 let cmd = Command::new("exiftool")
244 .arg(path)
245 .stdout(Stdio::piped())
246 .spawn();
247
248 let output = match cmd {
249 Ok(cmd) => match cmd.wait_with_output() {
250 Ok(output) => output,
251 Err(e) => {
252 println!("Failure waiting for exiftool process -> {e}");
253 return None;
254 }
255 },
256 Err(e) => {
257 println!("Failure spawning exiftool process -> {e}");
258 return None;
259 }
260 };
261
262 Self::parse_exiftool_output_str(String::from_utf8_lossy(&output.stdout).as_ref())
263 .map(|(_, metadata)| metadata)
264 }
265
266 pub fn extract_icc_from_image(path: &PathBuf) -> Option<Vec<u8>> {
267 let cmd = Command::new("exiftool")
268 .arg("-icc_profile")
269 .arg("-b")
270 .arg(path)
271 .stdout(Stdio::piped())
272 .spawn();
273
274 match cmd {
275 Ok(cmd) => match cmd.wait_with_output() {
276 Ok(output) => {
277 if !output.stdout.is_empty() {
278 Some(output.stdout)
279 } else {
280 None
281 }
282 }
283 Err(e) => {
284 println!("Error fetching image icc -> {e}");
285 None
286 }
287 },
288 Err(e) => {
289 println!("Error fetching image icc -> {e}");
290 None
291 }
292 }
293 }
294
295 pub fn format_string_with_metadata(input: &str, metadata: &HashMap<String, String>) -> String {
296 let mut output = String::from(input);
297
298 let tag_regex = Regex::new("(\\$\\(([^\\(\\)]*#([\\w \\s]*)#[^\\(\\)]*)\\))").unwrap();
299
300 for cap_group in tag_regex.captures_iter(input) {
301 let expression = match cap_group.get(0) {
303 Some(m) => m.as_str(),
304 None => continue,
305 };
306
307 let string_to_format = match cap_group.get(2) {
309 Some(m) => m.as_str(),
310 None => continue,
311 };
312
313 let metadata_tag = match cap_group.get(3) {
315 Some(m) => m.as_str(),
316 None => continue,
317 };
318
319 let to_replace = if let Some(metadata_value) = metadata.get(metadata_tag) {
320 string_to_format.replace(&format!("#{metadata_tag}#"), metadata_value)
321 } else {
322 "".to_string()
323 };
324
325 output = output.replace(expression, &to_replace);
326 }
327
328 output
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_format_string_with_metadata() {
338 let input = "$(#File Name#)$( • ƒ#Aperture#)$( • #Shutter Speed#)$( • #ISO# ISO)";
339 let mut metadata: HashMap<String, String> = HashMap::new();
340 metadata.insert("File Name".to_string(), "test.jpg".to_string());
341 metadata.insert("Aperture".to_string(), "5.0".to_string());
342 metadata.insert("ISO".to_string(), "500".to_string());
343
344 assert_eq!(
345 Metadata::format_string_with_metadata(input, &metadata),
346 "test.jpg • ƒ5.0 • 500 ISO".to_string()
347 );
348 }
349}