exiftool_rs_wrapper/
write.rs1use crate::ExifTool;
4use crate::error::{Error, Result};
5use crate::types::TagId;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9pub struct WriteBuilder<'et> {
11 exiftool: &'et ExifTool,
12 path: PathBuf,
13 tags: HashMap<String, String>,
14 overwrite_original: bool,
15 backup: bool,
16 output_path: Option<PathBuf>,
17 condition: Option<String>,
18 raw_args: Vec<String>,
19}
20
21impl<'et> WriteBuilder<'et> {
22 pub(crate) fn new<P: AsRef<Path>>(exiftool: &'et ExifTool, path: P) -> Self {
24 Self {
25 exiftool,
26 path: path.as_ref().to_path_buf(),
27 tags: HashMap::new(),
28 overwrite_original: false,
29 backup: true,
30 output_path: None,
31 condition: None,
32 raw_args: Vec::new(),
33 }
34 }
35
36 pub fn tag(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
38 self.tags.insert(tag.into(), value.into());
39 self
40 }
41
42 pub fn tag_id(self, tag: TagId, value: impl Into<String>) -> Self {
44 self.tag(tag.name(), value)
45 }
46
47 pub fn tags(mut self, tags: HashMap<impl Into<String>, impl Into<String>>) -> Self {
49 for (k, v) in tags {
50 self.tags.insert(k.into(), v.into());
51 }
52 self
53 }
54
55 pub fn delete(mut self, tag: impl Into<String>) -> Self {
57 self.tags.insert(tag.into(), "".to_string());
59 self
60 }
61
62 pub fn delete_id(self, tag: TagId) -> Self {
64 self.delete(tag.name())
65 }
66
67 pub fn overwrite_original(mut self, yes: bool) -> Self {
69 self.overwrite_original = yes;
70 self
71 }
72
73 pub fn backup(mut self, yes: bool) -> Self {
75 self.backup = yes;
76 self
77 }
78
79 pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
81 self.output_path = Some(path.as_ref().to_path_buf());
82 self
83 }
84
85 pub fn condition(mut self, expr: impl Into<String>) -> Self {
87 self.condition = Some(expr.into());
88 self
89 }
90
91 pub fn arg(mut self, arg: impl Into<String>) -> Self {
93 self.raw_args.push(arg.into());
94 self
95 }
96
97 pub fn offset(self, tag: impl Into<String>, offset: impl Into<String>) -> Self {
101 let tag = tag.into();
102 let offset = offset.into();
103 self.arg(format!("-{}+={}", tag, offset))
104 }
105
106 pub fn copy_from<P: AsRef<Path>>(mut self, source: P) -> Self {
110 self.raw_args.push("-tagsFromFile".to_string());
111 self.raw_args
112 .push(source.as_ref().to_string_lossy().to_string());
113 self
114 }
115
116 pub fn execute(self) -> Result<WriteResult> {
118 let args = self.build_args();
119 let response = self.exiftool.execute_raw(&args)?;
120
121 if response.is_error() {
122 return Err(Error::process(
123 response
124 .error_message()
125 .unwrap_or_else(|| "Unknown write error".to_string()),
126 ));
127 }
128
129 Ok(WriteResult {
130 path: self.path,
131 lines: response.lines().to_vec(),
132 })
133 }
134
135 fn build_args(&self) -> Vec<String> {
137 let mut args = Vec::new();
138
139 if self.overwrite_original {
141 args.push("-overwrite_original".to_string());
142 }
143
144 if !self.backup {
146 args.push("-overwrite_original_in_place".to_string());
147 }
148
149 if let Some(ref output) = self.output_path {
151 args.push("-o".to_string());
152 args.push(output.to_string_lossy().to_string());
153 }
154
155 if let Some(ref condition) = self.condition {
157 args.push(format!("-if {}", condition));
158 }
159
160 args.extend(self.raw_args.clone());
162
163 for (tag, value) in &self.tags {
165 if value.is_empty() {
166 args.push(format!("-{}=", tag));
168 } else {
169 args.push(format!("-{}={}", tag, value));
171 }
172 }
173
174 args.push(self.path.to_string_lossy().to_string());
176
177 args
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct WriteResult {
184 pub path: PathBuf,
186
187 pub lines: Vec<String>,
189}
190
191impl WriteResult {
192 pub fn is_success(&self) -> bool {
194 self.lines.iter().any(|line| {
195 line.contains("image files updated") || line.contains("image files unchanged")
196 })
197 }
198
199 pub fn updated_count(&self) -> Option<u32> {
201 for line in &self.lines {
202 if let Some(pos) = line.find("image files updated") {
203 let num_str: String = line[..pos].chars().filter(|c| c.is_ascii_digit()).collect();
204 return num_str.parse().ok();
205 }
206 }
207 None
208 }
209
210 pub fn backup_path(&self) -> Option<PathBuf> {
212 let backup = self.path.with_extension(format!(
213 "{}_original",
214 self.path.extension()?.to_string_lossy()
215 ));
216 if backup.exists() { Some(backup) } else { None }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_write_builder_args() {
226 }
228
229 #[test]
230 fn test_write_result_parsing() {
231 let result = WriteResult {
232 path: PathBuf::from("test.jpg"),
233 lines: vec![" 1 image files updated".to_string()],
234 };
235
236 assert!(result.is_success());
237 assert_eq!(result.updated_count(), Some(1));
238 }
239}