Skip to main content

exiftool_rs_wrapper/
write.rs

1//! 写入操作构建器
2
3use crate::ExifTool;
4use crate::error::{Error, Result};
5use crate::types::TagId;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9/// 写入构建器
10pub 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    /// 创建新的写入构建器
23    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    /// 设置标签值
37    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    /// 设置标签值(使用 TagId)
43    pub fn tag_id(self, tag: TagId, value: impl Into<String>) -> Self {
44        self.tag(tag.name(), value)
45    }
46
47    /// 设置多个标签
48    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    /// 删除标签
56    pub fn delete(mut self, tag: impl Into<String>) -> Self {
57        // 删除标签通过设置空值实现
58        self.tags.insert(tag.into(), "".to_string());
59        self
60    }
61
62    /// 删除标签(使用 TagId)
63    pub fn delete_id(self, tag: TagId) -> Self {
64        self.delete(tag.name())
65    }
66
67    /// 覆盖原始文件(不创建备份)
68    pub fn overwrite_original(mut self, yes: bool) -> Self {
69        self.overwrite_original = yes;
70        self
71    }
72
73    /// 创建备份
74    pub fn backup(mut self, yes: bool) -> Self {
75        self.backup = yes;
76        self
77    }
78
79    /// 输出到不同文件
80    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    /// 设置条件(仅在条件满足时写入)
86    pub fn condition(mut self, expr: impl Into<String>) -> Self {
87        self.condition = Some(expr.into());
88        self
89    }
90
91    /// 添加原始参数(高级用法)
92    pub fn arg(mut self, arg: impl Into<String>) -> Self {
93        self.raw_args.push(arg.into());
94        self
95    }
96
97    /// 日期/时间偏移
98    ///
99    /// 示例: `.offset("DateTimeOriginal", "+1:0:0 0:0:0")` 表示增加 1 天
100    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    /// 从文件复制标签
107    ///
108    /// 从源文件复制所有标签到目标文件
109    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    /// 执行写入操作
117    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    /// 构建参数列表
136    fn build_args(&self) -> Vec<String> {
137        let mut args = Vec::new();
138
139        // 覆盖原始文件
140        if self.overwrite_original {
141            args.push("-overwrite_original".to_string());
142        }
143
144        // 不创建备份
145        if !self.backup {
146            args.push("-overwrite_original_in_place".to_string());
147        }
148
149        // 输出到不同文件
150        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        // 条件
156        if let Some(ref condition) = self.condition {
157            args.push(format!("-if {}", condition));
158        }
159
160        // 原始参数
161        args.extend(self.raw_args.clone());
162
163        // 标签写入
164        for (tag, value) in &self.tags {
165            if value.is_empty() {
166                // 删除标签
167                args.push(format!("-{}=", tag));
168            } else {
169                // 写入标签
170                args.push(format!("-{}={}", tag, value));
171            }
172        }
173
174        // 文件路径
175        args.push(self.path.to_string_lossy().to_string());
176
177        args
178    }
179}
180
181/// 写入操作结果
182#[derive(Debug, Clone)]
183pub struct WriteResult {
184    /// 被修改的文件路径
185    pub path: PathBuf,
186
187    /// ExifTool 输出信息
188    pub lines: Vec<String>,
189}
190
191impl WriteResult {
192    /// 检查是否成功
193    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    /// 获取修改的文件数量
200    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    /// 获取创建的备份文件路径
211    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        // 基础测试,不依赖 ExifTool 实例
227    }
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}