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/// 写入模式
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum WriteMode {
12    /// w - 写入标签(默认值)
13    Write,
14    /// c - 仅创建标签(不修改现有标签)
15    Create,
16    /// wc - 写入或创建
17    WriteCreate,
18}
19
20impl WriteMode {
21    /// 获取模式字符串
22    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
31/// 写入构建器
32pub 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    /// 创建新的写入构建器
50    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    /// 设置标签值
69    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    /// 设置标签值(使用 TagId)
75    pub fn tag_id(self, tag: TagId, value: impl Into<String>) -> Self {
76        self.tag(tag.name(), value)
77    }
78
79    /// 设置多个标签
80    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    /// 删除标签
88    pub fn delete(mut self, tag: impl Into<String>) -> Self {
89        // 删除标签通过设置空值实现
90        self.tags.insert(tag.into(), "".to_string());
91        self
92    }
93
94    /// 删除标签(使用 TagId)
95    pub fn delete_id(self, tag: TagId) -> Self {
96        self.delete(tag.name())
97    }
98
99    /// 覆盖原始文件(不创建备份)
100    pub fn overwrite_original(mut self, yes: bool) -> Self {
101        self.overwrite_original = yes;
102        self
103    }
104
105    /// 创建备份
106    pub fn backup(mut self, yes: bool) -> Self {
107        self.backup = yes;
108        self
109    }
110
111    /// 输出到不同文件
112    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    /// 设置条件(仅在条件满足时写入)
118    pub fn condition(mut self, expr: impl Into<String>) -> Self {
119        self.condition = Some(expr.into());
120        self
121    }
122
123    /// 添加原始参数(高级用法)
124    pub fn arg(mut self, arg: impl Into<String>) -> Self {
125        self.raw_args.push(arg.into());
126        self
127    }
128
129    /// 设置写入模式
130    ///
131    /// 使用 `-wm` 选项设置写入/创建标签的模式
132    ///
133    /// # 模式
134    ///
135    /// - `WriteMode::Write` (w) - 写入标签(默认)
136    /// - `WriteMode::Create` (c) - 仅创建标签(不修改现有标签)
137    /// - `WriteMode::WriteCreate` (wc) - 写入或创建
138    ///
139    /// # 示例
140    ///
141    /// ```rust,no_run
142    /// use exiftool_rs_wrapper::{ExifTool, WriteMode};
143    ///
144    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
145    /// let exiftool = ExifTool::new()?;
146    ///
147    /// // 仅创建新标签,不修改现有标签
148    /// exiftool.write("photo.jpg")
149    ///     .tag("NewTag", "value")
150    ///     .write_mode(WriteMode::Create)
151    ///     .execute()?;
152    /// # Ok(())
153    /// # }
154    /// ```
155    pub fn write_mode(mut self, mode: WriteMode) -> Self {
156        self.raw_args.push(format!("-wm {}", mode.as_str()));
157        self
158    }
159
160    /// 设置密码
161    ///
162    /// 使用 `-password` 选项处理受密码保护的文件
163    ///
164    /// # 安全性警告
165    ///
166    /// 密码将以纯文本形式传递给 ExifTool 进程。
167    ///
168    /// # 示例
169    ///
170    /// ```rust,no_run
171    /// use exiftool_rs_wrapper::ExifTool;
172    ///
173    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
174    /// let exiftool = ExifTool::new()?;
175    ///
176    /// // 写入受密码保护的 PDF
177    /// exiftool.write("protected.pdf")
178    ///     .tag("Title", "New Title")
179    ///     .password("secret123")
180    ///     .execute()?;
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn password(mut self, passwd: impl Into<String>) -> Self {
185        self.raw_args.push(format!("-password {}", passwd.into()));
186        self
187    }
188
189    /// 设置列表项分隔符
190    ///
191    /// 使用 `-sep` 选项设置列表项的分隔符字符串
192    pub fn separator(mut self, sep: impl Into<String>) -> Self {
193        self.raw_args.push(format!("-sep {}", sep.into()));
194        self
195    }
196
197    /// 设置 API 选项
198    ///
199    /// 使用 `-api` 选项设置 ExifTool API 选项
200    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    /// 设置用户参数
210    ///
211    /// 使用 `-userParam` 选项设置用户参数
212    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    /// 忽略次要错误
226    ///
227    /// 使用 `-m` 选项忽略次要错误和警告,继续处理其他文件。
228    /// 这在批量处理时很有用,可以避免单个文件错误导致整个操作失败。
229    ///
230    /// # 示例
231    ///
232    /// ```rust,no_run
233    /// use exiftool_rs_wrapper::ExifTool;
234    ///
235    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
236    /// let exiftool = ExifTool::new()?;
237    ///
238    /// // 批量写入时忽略次要错误
239    /// exiftool.write("photo.jpg")
240    ///     .tag("Copyright", "© 2026")
241    ///     .ignore_minor_errors(true)
242    ///     .execute()?;
243    /// # Ok(())
244    /// # }
245    /// ```
246    pub fn ignore_minor_errors(mut self, yes: bool) -> Self {
247        self.ignore_minor_errors = yes;
248        self
249    }
250
251    /// 保留文件修改时间
252    ///
253    /// 使用 `-P` 选项保留文件的原始修改时间。
254    /// 默认情况下,ExifTool 会更新文件的修改时间为当前时间。
255    ///
256    /// # 示例
257    ///
258    /// ```rust,no_run
259    /// use exiftool_rs_wrapper::ExifTool;
260    ///
261    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
262    /// let exiftool = ExifTool::new()?;
263    ///
264    /// // 写入元数据但保留原始修改时间
265    /// exiftool.write("photo.jpg")
266    ///     .tag("Copyright", "© 2026")
267    ///     .preserve_time(true)
268    ///     .execute()?;
269    /// # Ok(())
270    /// # }
271    /// ```
272    pub fn preserve_time(mut self, yes: bool) -> Self {
273        self.preserve_time = yes;
274        self
275    }
276
277    /// 静默模式
278    ///
279    /// 使用 `-q` 选项启用静默模式,减少输出信息。
280    /// 可以使用多次来增加静默程度。
281    ///
282    /// # 示例
283    ///
284    /// ```rust,no_run
285    /// use exiftool_rs_wrapper::ExifTool;
286    ///
287    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
288    /// let exiftool = ExifTool::new()?;
289    ///
290    /// // 静默模式下写入
291    /// exiftool.write("photo.jpg")
292    ///     .tag("Copyright", "© 2026")
293    ///     .quiet(true)
294    ///     .execute()?;
295    /// # Ok(())
296    /// # }
297    /// ```
298    pub fn quiet(mut self, yes: bool) -> Self {
299        self.quiet = yes;
300        self
301    }
302
303    /// 启用 ZIP 压缩
304    ///
305    /// 使用 `-z` 选项读写压缩的元数据信息。
306    /// 某些文件格式支持压缩元数据存储。
307    ///
308    /// # 示例
309    ///
310    /// ```rust,no_run
311    /// use exiftool_rs_wrapper::ExifTool;
312    ///
313    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
314    /// let exiftool = ExifTool::new()?;
315    ///
316    /// // 使用压缩写入元数据
317    /// exiftool.write("photo.jpg")
318    ///     .tag("Copyright", "© 2026")
319    ///     .zip_compression(true)
320    ///     .execute()?;
321    /// # Ok(())
322    /// # }
323    /// ```
324    pub fn zip_compression(mut self, yes: bool) -> Self {
325        self.zip_compression = yes;
326        self
327    }
328
329    /// 修复 MakerNotes 偏移
330    ///
331    /// 使用 `-F` 选项修复 MakerNotes 的基准偏移。
332    /// 这在处理某些损坏或格式异常的图像文件时很有用。
333    ///
334    /// # 参数
335    ///
336    /// - `offset` - 可选的偏移量修正值(以字节为单位)
337    ///
338    /// # 示例
339    ///
340    /// ```rust,no_run
341    /// use exiftool_rs_wrapper::ExifTool;
342    ///
343    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
344    /// let exiftool = ExifTool::new()?;
345    ///
346    /// // 自动修复 MakerNotes 偏移
347    /// exiftool.write("photo.jpg")
348    ///     .tag("Copyright", "© 2026")
349    ///     .fix_base(None)
350    ///     .execute()?;
351    ///
352    /// // 指定偏移量修复
353    /// exiftool.write("photo.jpg")
354    ///     .fix_base(Some(1024))
355    ///     .execute()?;
356    /// # Ok(())
357    /// # }
358    /// ```
359    pub fn fix_base(mut self, offset: Option<u32>) -> Self {
360        self.fix_base = offset;
361        self
362    }
363
364    /// 日期/时间偏移
365    ///
366    /// 示例: `.offset("DateTimeOriginal", "+1:0:0 0:0:0")` 表示增加 1 天
367    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    /// 从文件复制标签
374    ///
375    /// 从源文件复制所有标签到目标文件
376    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    /// 执行写入操作
384    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    /// 构建参数列表
403    fn build_args(&self) -> Vec<String> {
404        let mut args = Vec::new();
405
406        // 覆盖原始文件
407        if self.overwrite_original {
408            args.push("-overwrite_original".to_string());
409        }
410
411        // 不创建备份
412        if !self.backup {
413            args.push("-overwrite_original_in_place".to_string());
414        }
415
416        // 输出到不同文件
417        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        // 条件
423        if let Some(ref condition) = self.condition {
424            args.push(format!("-if {}", condition));
425        }
426
427        // 忽略次要错误
428        if self.ignore_minor_errors {
429            args.push("-m".to_string());
430        }
431
432        // 保留文件修改时间
433        if self.preserve_time {
434            args.push("-P".to_string());
435        }
436
437        // 静默模式
438        if self.quiet {
439            args.push("-q".to_string());
440        }
441
442        // ZIP 压缩
443        if self.zip_compression {
444            args.push("-z".to_string());
445        }
446
447        // 修复 MakerNotes 偏移
448        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        // 原始参数
457        args.extend(self.raw_args.clone());
458
459        // 标签写入
460        for (tag, value) in &self.tags {
461            if value.is_empty() {
462                // 删除标签
463                args.push(format!("-{}=", tag));
464            } else {
465                // 写入标签
466                args.push(format!("-{}={}", tag, value));
467            }
468        }
469
470        // 文件路径
471        args.push(self.path.to_string_lossy().to_string());
472
473        args
474    }
475}
476
477/// 写入操作结果
478#[derive(Debug, Clone)]
479pub struct WriteResult {
480    /// 被修改的文件路径
481    pub path: PathBuf,
482
483    /// ExifTool 输出信息
484    pub lines: Vec<String>,
485}
486
487impl WriteResult {
488    /// 检查是否成功
489    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    /// 获取修改的文件数量
496    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    /// 获取创建的备份文件路径
507    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        // 基础测试,不依赖 ExifTool 实例
523    }
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}