1use crate::ExifTool;
4use crate::error::{Error, Result};
5use crate::types::TagId;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum WriteMode {
12 Write,
14 Create,
16 WriteCreate,
18}
19
20impl WriteMode {
21 fn as_str(&self) -> &'static str {
23 match self {
24 WriteMode::Write => "w",
25 WriteMode::Create => "c",
26 WriteMode::WriteCreate => "wc",
27 }
28 }
29}
30
31pub struct WriteBuilder<'et> {
33 exiftool: &'et ExifTool,
34 path: PathBuf,
35 tags: HashMap<String, String>,
36 overwrite_original: bool,
37 backup: bool,
38 output_path: Option<PathBuf>,
39 condition: Option<String>,
40 ignore_minor_errors: bool,
41 preserve_time: bool,
42 quiet: bool,
43 zip_compression: bool,
44 fix_base: Option<u32>,
45 raw_args: Vec<String>,
46}
47
48impl<'et> WriteBuilder<'et> {
49 pub(crate) fn new<P: AsRef<Path>>(exiftool: &'et ExifTool, path: P) -> Self {
51 Self {
52 exiftool,
53 path: path.as_ref().to_path_buf(),
54 tags: HashMap::new(),
55 overwrite_original: false,
56 backup: true,
57 output_path: None,
58 condition: None,
59 ignore_minor_errors: false,
60 preserve_time: false,
61 quiet: false,
62 zip_compression: false,
63 fix_base: None,
64 raw_args: Vec::new(),
65 }
66 }
67
68 pub fn tag(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
70 self.tags.insert(tag.into(), value.into());
71 self
72 }
73
74 pub fn tag_id(self, tag: TagId, value: impl Into<String>) -> Self {
76 self.tag(tag.name(), value)
77 }
78
79 pub fn tags(mut self, tags: HashMap<impl Into<String>, impl Into<String>>) -> Self {
81 for (k, v) in tags {
82 self.tags.insert(k.into(), v.into());
83 }
84 self
85 }
86
87 pub fn delete(mut self, tag: impl Into<String>) -> Self {
89 self.tags.insert(tag.into(), "".to_string());
91 self
92 }
93
94 pub fn delete_id(self, tag: TagId) -> Self {
96 self.delete(tag.name())
97 }
98
99 pub fn overwrite_original(mut self, yes: bool) -> Self {
101 self.overwrite_original = yes;
102 self
103 }
104
105 pub fn backup(mut self, yes: bool) -> Self {
107 self.backup = yes;
108 self
109 }
110
111 pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
113 self.output_path = Some(path.as_ref().to_path_buf());
114 self
115 }
116
117 pub fn condition(mut self, expr: impl Into<String>) -> Self {
119 self.condition = Some(expr.into());
120 self
121 }
122
123 pub fn arg(mut self, arg: impl Into<String>) -> Self {
125 self.raw_args.push(arg.into());
126 self
127 }
128
129 pub fn write_mode(mut self, mode: WriteMode) -> Self {
156 self.raw_args.push(format!("-wm {}", mode.as_str()));
157 self
158 }
159
160 pub fn password(mut self, passwd: impl Into<String>) -> Self {
185 self.raw_args.push(format!("-password {}", passwd.into()));
186 self
187 }
188
189 pub fn separator(mut self, sep: impl Into<String>) -> Self {
193 self.raw_args.push(format!("-sep {}", sep.into()));
194 self
195 }
196
197 pub fn api_option(mut self, opt: impl Into<String>, value: Option<impl Into<String>>) -> Self {
201 let arg = match value {
202 Some(v) => format!("-api {}={}", opt.into(), v.into()),
203 None => format!("-api {}", opt.into()),
204 };
205 self.raw_args.push(arg);
206 self
207 }
208
209 pub fn user_param(
213 mut self,
214 param: impl Into<String>,
215 value: Option<impl Into<String>>,
216 ) -> Self {
217 let arg = match value {
218 Some(v) => format!("-userParam {}={}", param.into(), v.into()),
219 None => format!("-userParam {}", param.into()),
220 };
221 self.raw_args.push(arg);
222 self
223 }
224
225 pub fn ignore_minor_errors(mut self, yes: bool) -> Self {
247 self.ignore_minor_errors = yes;
248 self
249 }
250
251 pub fn preserve_time(mut self, yes: bool) -> Self {
273 self.preserve_time = yes;
274 self
275 }
276
277 pub fn quiet(mut self, yes: bool) -> Self {
299 self.quiet = yes;
300 self
301 }
302
303 pub fn zip_compression(mut self, yes: bool) -> Self {
325 self.zip_compression = yes;
326 self
327 }
328
329 pub fn fix_base(mut self, offset: Option<u32>) -> Self {
360 self.fix_base = offset;
361 self
362 }
363
364 pub fn offset(self, tag: impl Into<String>, offset: impl Into<String>) -> Self {
368 let tag = tag.into();
369 let offset = offset.into();
370 self.arg(format!("-{}+={}", tag, offset))
371 }
372
373 pub fn copy_from<P: AsRef<Path>>(mut self, source: P) -> Self {
377 self.raw_args.push("-tagsFromFile".to_string());
378 self.raw_args
379 .push(source.as_ref().to_string_lossy().to_string());
380 self
381 }
382
383 pub fn execute(self) -> Result<WriteResult> {
385 let args = self.build_args();
386 let response = self.exiftool.execute_raw(&args)?;
387
388 if response.is_error() {
389 return Err(Error::process(
390 response
391 .error_message()
392 .unwrap_or_else(|| "Unknown write error".to_string()),
393 ));
394 }
395
396 Ok(WriteResult {
397 path: self.path,
398 lines: response.lines().to_vec(),
399 })
400 }
401
402 fn build_args(&self) -> Vec<String> {
404 let mut args = Vec::new();
405
406 if self.overwrite_original {
408 args.push("-overwrite_original".to_string());
409 }
410
411 if !self.backup {
413 args.push("-overwrite_original_in_place".to_string());
414 }
415
416 if let Some(ref output) = self.output_path {
418 args.push("-o".to_string());
419 args.push(output.to_string_lossy().to_string());
420 }
421
422 if let Some(ref condition) = self.condition {
424 args.push(format!("-if {}", condition));
425 }
426
427 if self.ignore_minor_errors {
429 args.push("-m".to_string());
430 }
431
432 if self.preserve_time {
434 args.push("-P".to_string());
435 }
436
437 if self.quiet {
439 args.push("-q".to_string());
440 }
441
442 if self.zip_compression {
444 args.push("-z".to_string());
445 }
446
447 if let Some(offset) = self.fix_base {
449 if offset == 0 {
450 args.push("-F".to_string());
451 } else {
452 args.push(format!("-F{}", offset));
453 }
454 }
455
456 args.extend(self.raw_args.clone());
458
459 for (tag, value) in &self.tags {
461 if value.is_empty() {
462 args.push(format!("-{}=", tag));
464 } else {
465 args.push(format!("-{}={}", tag, value));
467 }
468 }
469
470 args.push(self.path.to_string_lossy().to_string());
472
473 args
474 }
475}
476
477#[derive(Debug, Clone)]
479pub struct WriteResult {
480 pub path: PathBuf,
482
483 pub lines: Vec<String>,
485}
486
487impl WriteResult {
488 pub fn is_success(&self) -> bool {
490 self.lines.iter().any(|line| {
491 line.contains("image files updated") || line.contains("image files unchanged")
492 })
493 }
494
495 pub fn updated_count(&self) -> Option<u32> {
497 for line in &self.lines {
498 if let Some(pos) = line.find("image files updated") {
499 let num_str: String = line[..pos].chars().filter(|c| c.is_ascii_digit()).collect();
500 return num_str.parse().ok();
501 }
502 }
503 None
504 }
505
506 pub fn backup_path(&self) -> Option<PathBuf> {
508 let backup = self.path.with_extension(format!(
509 "{}_original",
510 self.path.extension()?.to_string_lossy()
511 ));
512 if backup.exists() { Some(backup) } else { None }
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_write_builder_args() {
522 }
524
525 #[test]
526 fn test_write_result_parsing() {
527 let result = WriteResult {
528 path: PathBuf::from("test.jpg"),
529 lines: vec![" 1 image files updated".to_string()],
530 };
531
532 assert!(result.is_success());
533 assert_eq!(result.updated_count(), Some(1));
534 }
535}