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    delete_tags: Vec<String>,
37    overwrite_original: bool,
38    backup: bool,
39    output_path: Option<PathBuf>,
40    condition: Option<String>,
41    ignore_minor_errors: bool,
42    preserve_time: bool,
43    quiet: bool,
44    zip_compression: bool,
45    fix_base_enabled: bool,
46    fix_base_offset: Option<i64>,
47    raw_args: Vec<String>,
48}
49
50impl<'et> WriteBuilder<'et> {
51    /// 创建新的写入构建器
52    pub(crate) fn new<P: AsRef<Path>>(exiftool: &'et ExifTool, path: P) -> Self {
53        Self {
54            exiftool,
55            path: path.as_ref().to_path_buf(),
56            tags: HashMap::new(),
57            delete_tags: Vec::new(),
58            overwrite_original: false,
59            backup: true,
60            output_path: None,
61            condition: None,
62            ignore_minor_errors: false,
63            preserve_time: false,
64            quiet: false,
65            zip_compression: false,
66            fix_base_enabled: false,
67            fix_base_offset: None,
68            raw_args: Vec::new(),
69        }
70    }
71
72    /// 设置标签值
73    pub fn tag(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
74        self.tags.insert(tag.into(), value.into());
75        self
76    }
77
78    /// 设置标签值(使用 TagId)
79    pub fn tag_id(self, tag: TagId, value: impl Into<String>) -> Self {
80        self.tag(tag.name(), value)
81    }
82
83    /// 设置多个标签
84    pub fn tags(mut self, tags: HashMap<impl Into<String>, impl Into<String>>) -> Self {
85        for (k, v) in tags {
86            self.tags.insert(k.into(), v.into());
87        }
88        self
89    }
90
91    /// 追加值到标签(`-TAG+=VALUE`)
92    ///
93    /// 使用 `+=` 运算符将值追加到现有标签值后面。
94    /// 适用于列表类型的标签,如 Keywords 等。
95    ///
96    /// # 示例
97    ///
98    /// ```rust,no_run
99    /// use exiftool_rs_wrapper::ExifTool;
100    ///
101    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
102    /// let exiftool = ExifTool::new()?;
103    ///
104    /// // 追加关键词到现有列表
105    /// exiftool.write("photo.jpg")
106    ///     .tag_append("Keywords", "landscape")
107    ///     .overwrite_original(true)
108    ///     .execute()?;
109    /// # Ok(())
110    /// # }
111    /// ```
112    pub fn tag_append(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
113        self.raw_args
114            .push(format!("-{}+={}", tag.into(), value.into()));
115        self
116    }
117
118    /// 从标签中移除值(`-TAG-=VALUE`)
119    ///
120    /// 使用 `-=` 运算符从现有标签值中移除指定值。
121    /// 适用于列表类型的标签。
122    ///
123    /// # 示例
124    ///
125    /// ```rust,no_run
126    /// use exiftool_rs_wrapper::ExifTool;
127    ///
128    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
129    /// let exiftool = ExifTool::new()?;
130    ///
131    /// // 从关键词列表中移除某个关键词
132    /// exiftool.write("photo.jpg")
133    ///     .tag_remove("Keywords", "old-keyword")
134    ///     .overwrite_original(true)
135    ///     .execute()?;
136    /// # Ok(())
137    /// # }
138    /// ```
139    pub fn tag_remove(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
140        self.raw_args
141            .push(format!("-{}-={}", tag.into(), value.into()));
142        self
143    }
144
145    /// 前置值到标签(`-TAG^=VALUE`)
146    ///
147    /// 使用 `^=` 运算符将值前置到现有标签值之前。
148    /// 适用于列表类型的标签。
149    ///
150    /// # 示例
151    ///
152    /// ```rust,no_run
153    /// use exiftool_rs_wrapper::ExifTool;
154    ///
155    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
156    /// let exiftool = ExifTool::new()?;
157    ///
158    /// // 在关键词列表前面插入关键词
159    /// exiftool.write("photo.jpg")
160    ///     .tag_prepend("Keywords", "important")
161    ///     .overwrite_original(true)
162    ///     .execute()?;
163    /// # Ok(())
164    /// # }
165    /// ```
166    pub fn tag_prepend(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
167        self.raw_args
168            .push(format!("-{}^={}", tag.into(), value.into()));
169        self
170    }
171
172    /// 从文件读取值写入标签(`-TAG<=FILE`)
173    ///
174    /// 使用 `<=` 运算符从指定文件中读取数据作为标签值。
175    /// 常用于写入二进制数据(如缩略图、预览图)。
176    ///
177    /// # 示例
178    ///
179    /// ```rust,no_run
180    /// use exiftool_rs_wrapper::ExifTool;
181    ///
182    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
183    /// let exiftool = ExifTool::new()?;
184    ///
185    /// // 从文件读取缩略图写入
186    /// exiftool.write("photo.jpg")
187    ///     .tag_from_file("ThumbnailImage", "thumb.jpg")
188    ///     .overwrite_original(true)
189    ///     .execute()?;
190    /// # Ok(())
191    /// # }
192    /// ```
193    pub fn tag_from_file(mut self, tag: impl Into<String>, file_path: impl Into<String>) -> Self {
194        self.raw_args
195            .push(format!("-{}<={}", tag.into(), file_path.into()));
196        self
197    }
198
199    /// 追加从文件读取的值到标签(`-TAG+<=FILE`)
200    ///
201    /// 使用 `+<=` 运算符从指定文件中读取数据追加到现有标签值。
202    ///
203    /// # 示例
204    ///
205    /// ```rust,no_run
206    /// use exiftool_rs_wrapper::ExifTool;
207    ///
208    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
209    /// let exiftool = ExifTool::new()?;
210    ///
211    /// // 从文件追加数据到标签
212    /// exiftool.write("photo.jpg")
213    ///     .tag_append_from_file("Comment", "comment.txt")
214    ///     .overwrite_original(true)
215    ///     .execute()?;
216    /// # Ok(())
217    /// # }
218    /// ```
219    pub fn tag_append_from_file(
220        mut self,
221        tag: impl Into<String>,
222        file_path: impl Into<String>,
223    ) -> Self {
224        self.raw_args
225            .push(format!("-{}+<={}", tag.into(), file_path.into()));
226        self
227    }
228
229    /// 删除标签
230    pub fn delete(mut self, tag: impl Into<String>) -> Self {
231        self.delete_tags.push(tag.into());
232        self
233    }
234
235    /// 删除标签(使用 TagId)
236    pub fn delete_id(self, tag: TagId) -> Self {
237        self.delete(tag.name())
238    }
239
240    /// 覆盖原始文件(不创建备份)
241    pub fn overwrite_original(mut self, yes: bool) -> Self {
242        self.overwrite_original = yes;
243        self
244    }
245
246    /// 创建备份
247    pub fn backup(mut self, yes: bool) -> Self {
248        self.backup = yes;
249        self
250    }
251
252    /// 输出到不同文件
253    pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
254        self.output_path = Some(path.as_ref().to_path_buf());
255        self
256    }
257
258    /// 设置条件(仅在条件满足时写入)
259    pub fn condition(mut self, expr: impl Into<String>) -> Self {
260        self.condition = Some(expr.into());
261        self
262    }
263
264    /// 添加原始参数(高级用法)
265    pub fn arg(mut self, arg: impl Into<String>) -> Self {
266        self.raw_args.push(arg.into());
267        self
268    }
269
270    /// 设置写入模式
271    ///
272    /// 使用 `-wm` 选项设置写入/创建标签的模式
273    ///
274    /// # 模式
275    ///
276    /// - `WriteMode::Write` (w) - 写入标签(默认)
277    /// - `WriteMode::Create` (c) - 仅创建标签(不修改现有标签)
278    /// - `WriteMode::WriteCreate` (wc) - 写入或创建
279    ///
280    /// # 示例
281    ///
282    /// ```rust,no_run
283    /// use exiftool_rs_wrapper::{ExifTool, WriteMode};
284    ///
285    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
286    /// let exiftool = ExifTool::new()?;
287    ///
288    /// // 仅创建新标签,不修改现有标签
289    /// exiftool.write("photo.jpg")
290    ///     .tag("NewTag", "value")
291    ///     .write_mode(WriteMode::Create)
292    ///     .execute()?;
293    /// # Ok(())
294    /// # }
295    /// ```
296    pub fn write_mode(mut self, mode: WriteMode) -> Self {
297        self.raw_args.push("-wm".to_string());
298        self.raw_args.push(mode.as_str().to_string());
299        self
300    }
301
302    /// 设置密码
303    ///
304    /// 使用 `-password` 选项处理受密码保护的文件
305    ///
306    /// # 安全性警告
307    ///
308    /// 密码将以纯文本形式传递给 ExifTool 进程。
309    ///
310    /// # 示例
311    ///
312    /// ```rust,no_run
313    /// use exiftool_rs_wrapper::ExifTool;
314    ///
315    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
316    /// let exiftool = ExifTool::new()?;
317    ///
318    /// // 写入受密码保护的 PDF
319    /// exiftool.write("protected.pdf")
320    ///     .tag("Title", "New Title")
321    ///     .password("secret123")
322    ///     .execute()?;
323    /// # Ok(())
324    /// # }
325    /// ```
326    pub fn password(mut self, passwd: impl Into<String>) -> Self {
327        self.raw_args.push("-password".to_string());
328        self.raw_args.push(passwd.into());
329        self
330    }
331
332    /// 设置列表项分隔符
333    ///
334    /// 使用 `-sep` 选项设置列表项的分隔符字符串
335    pub fn separator(mut self, sep: impl Into<String>) -> Self {
336        self.raw_args.push("-sep".to_string());
337        self.raw_args.push(sep.into());
338        self
339    }
340
341    /// 设置 API 选项
342    ///
343    /// 使用 `-api` 选项设置 ExifTool API 选项
344    pub fn api_option(mut self, opt: impl Into<String>, value: Option<impl Into<String>>) -> Self {
345        let option = opt.into();
346        self.raw_args.push("-api".to_string());
347        match value {
348            Some(v) => self.raw_args.push(format!("{}={}", option, v.into())),
349            None => self.raw_args.push(option),
350        }
351        self
352    }
353
354    /// 设置用户参数
355    ///
356    /// 使用 `-userParam` 选项设置用户参数
357    pub fn user_param(
358        mut self,
359        param: impl Into<String>,
360        value: Option<impl Into<String>>,
361    ) -> Self {
362        let param = param.into();
363        self.raw_args.push("-userParam".to_string());
364        match value {
365            Some(v) => self.raw_args.push(format!("{}={}", param, v.into())),
366            None => self.raw_args.push(param),
367        }
368        self
369    }
370
371    /// 忽略次要错误
372    ///
373    /// 使用 `-m` 选项忽略次要错误和警告,继续处理其他文件。
374    /// 这在批量处理时很有用,可以避免单个文件错误导致整个操作失败。
375    ///
376    /// # 示例
377    ///
378    /// ```rust,no_run
379    /// use exiftool_rs_wrapper::ExifTool;
380    ///
381    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
382    /// let exiftool = ExifTool::new()?;
383    ///
384    /// // 批量写入时忽略次要错误
385    /// exiftool.write("photo.jpg")
386    ///     .tag("Copyright", "© 2026")
387    ///     .ignore_minor_errors(true)
388    ///     .execute()?;
389    /// # Ok(())
390    /// # }
391    /// ```
392    pub fn ignore_minor_errors(mut self, yes: bool) -> Self {
393        self.ignore_minor_errors = yes;
394        self
395    }
396
397    /// 保留文件修改时间
398    ///
399    /// 使用 `-P` 选项保留文件的原始修改时间。
400    /// 默认情况下,ExifTool 会更新文件的修改时间为当前时间。
401    ///
402    /// # 示例
403    ///
404    /// ```rust,no_run
405    /// use exiftool_rs_wrapper::ExifTool;
406    ///
407    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
408    /// let exiftool = ExifTool::new()?;
409    ///
410    /// // 写入元数据但保留原始修改时间
411    /// exiftool.write("photo.jpg")
412    ///     .tag("Copyright", "© 2026")
413    ///     .preserve_time(true)
414    ///     .execute()?;
415    /// # Ok(())
416    /// # }
417    /// ```
418    pub fn preserve_time(mut self, yes: bool) -> Self {
419        self.preserve_time = yes;
420        self
421    }
422
423    /// 静默模式
424    ///
425    /// 使用 `-q` 选项启用静默模式,减少输出信息。
426    /// 可以使用多次来增加静默程度。
427    ///
428    /// # 示例
429    ///
430    /// ```rust,no_run
431    /// use exiftool_rs_wrapper::ExifTool;
432    ///
433    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
434    /// let exiftool = ExifTool::new()?;
435    ///
436    /// // 静默模式下写入
437    /// exiftool.write("photo.jpg")
438    ///     .tag("Copyright", "© 2026")
439    ///     .quiet(true)
440    ///     .execute()?;
441    /// # Ok(())
442    /// # }
443    /// ```
444    pub fn quiet(mut self, yes: bool) -> Self {
445        self.quiet = yes;
446        self
447    }
448
449    /// 启用 ZIP 压缩
450    ///
451    /// 使用 `-z` 选项读写压缩的元数据信息。
452    /// 某些文件格式支持压缩元数据存储。
453    ///
454    /// # 示例
455    ///
456    /// ```rust,no_run
457    /// use exiftool_rs_wrapper::ExifTool;
458    ///
459    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
460    /// let exiftool = ExifTool::new()?;
461    ///
462    /// // 使用压缩写入元数据
463    /// exiftool.write("photo.jpg")
464    ///     .tag("Copyright", "© 2026")
465    ///     .zip_compression(true)
466    ///     .execute()?;
467    /// # Ok(())
468    /// # }
469    /// ```
470    pub fn zip_compression(mut self, yes: bool) -> Self {
471        self.zip_compression = yes;
472        self
473    }
474
475    /// 修复 MakerNotes 偏移
476    ///
477    /// 使用 `-F` 选项修复 MakerNotes 的基准偏移。
478    /// 这在处理某些损坏或格式异常的图像文件时很有用。
479    ///
480    /// # 参数
481    ///
482    /// - `offset` - 可选的偏移量修正值(以字节为单位)
483    ///
484    /// # 示例
485    ///
486    /// ```rust,no_run
487    /// use exiftool_rs_wrapper::ExifTool;
488    ///
489    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
490    /// let exiftool = ExifTool::new()?;
491    ///
492    /// // 自动修复 MakerNotes 偏移
493    /// exiftool.write("photo.jpg")
494    ///     .tag("Copyright", "© 2026")
495    ///     .fix_base(None)
496    ///     .execute()?;
497    ///
498    /// // 指定偏移量修复
499    /// exiftool.write("photo.jpg")
500    ///     .fix_base(Some(1024))
501    ///     .execute()?;
502    /// # Ok(())
503    /// # }
504    /// ```
505    pub fn fix_base(mut self, offset: Option<i64>) -> Self {
506        self.fix_base_enabled = true;
507        self.fix_base_offset = offset;
508        self
509    }
510
511    /// 修复 MakerNotes 偏移(带指定偏移量)
512    ///
513    /// 使用 `-FOFFSET` 选项指定具体偏移量修复 MakerNotes。
514    /// 这是 `fix_base(Some(offset))` 的便捷方法。
515    pub fn fix_base_offset(mut self, offset: i64) -> Self {
516        self.fix_base_enabled = true;
517        self.fix_base_offset = Some(offset);
518        self
519    }
520
521    /// 全局时间偏移
522    ///
523    /// 对应 ExifTool 的 `-globalTimeShift` 选项,对所有日期/时间标签
524    /// 应用统一的时间偏移。格式为 `[+|-]Y:M:D H:M:S`。
525    ///
526    /// # 示例
527    ///
528    /// ```rust,no_run
529    /// use exiftool_rs_wrapper::ExifTool;
530    ///
531    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
532    /// let exiftool = ExifTool::new()?;
533    ///
534    /// // 将所有时间标签向前偏移 1 小时
535    /// exiftool.write("photo.jpg")
536    ///     .global_time_shift("+0:0:0 1:0:0")
537    ///     .execute()?;
538    /// # Ok(())
539    /// # }
540    /// ```
541    pub fn global_time_shift(mut self, shift: impl Into<String>) -> Self {
542        self.raw_args.push("-globalTimeShift".to_string());
543        self.raw_args.push(shift.into());
544        self
545    }
546
547    /// 日期/时间偏移
548    ///
549    /// 示例: `.offset("DateTimeOriginal", "+1:0:0 0:0:0")` 表示增加 1 天
550    pub fn offset(self, tag: impl Into<String>, offset: impl Into<String>) -> Self {
551        let tag = tag.into();
552        let offset = offset.into();
553        self.arg(format!("-{}+={}", tag, offset))
554    }
555
556    /// 从文件复制标签
557    ///
558    /// 从源文件复制所有标签到目标文件
559    pub fn copy_from<P: AsRef<Path>>(mut self, source: P) -> Self {
560        self.raw_args.push("-tagsFromFile".to_string());
561        self.raw_args
562            .push(source.as_ref().to_string_lossy().to_string());
563        self
564    }
565
566    /// 从文件复制标签(带重定向映射)
567    ///
568    /// 使用 `-tagsFromFile SRCFILE` 配合 `-DSTTAG<SRCTAG` 重定向语法。
569    /// 可以指定源标签到目标标签的映射关系。
570    ///
571    /// # 参数
572    ///
573    /// - `source` - 源文件路径
574    /// - `redirects` - 标签重定向列表,每项为 `(目标标签, 源标签)`。
575    ///   生成 `-DSTTAG<SRCTAG` 参数。
576    ///
577    /// # 示例
578    ///
579    /// ```rust,no_run
580    /// use exiftool_rs_wrapper::ExifTool;
581    ///
582    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
583    /// let exiftool = ExifTool::new()?;
584    ///
585    /// // 从源文件复制 Artist 到 Author,DateTimeOriginal 到 CreateDate
586    /// exiftool.write("target.jpg")
587    ///     .copy_from_with_redirect("source.jpg", &[
588    ///         ("Author", "Artist"),
589    ///         ("CreateDate", "DateTimeOriginal"),
590    ///     ])
591    ///     .overwrite_original(true)
592    ///     .execute()?;
593    /// # Ok(())
594    /// # }
595    /// ```
596    pub fn copy_from_with_redirect<P: AsRef<Path>>(
597        mut self,
598        source: P,
599        redirects: &[(&str, &str)],
600    ) -> Self {
601        self.raw_args.push("-tagsFromFile".to_string());
602        self.raw_args
603            .push(source.as_ref().to_string_lossy().to_string());
604        for (dst, src) in redirects {
605            self.raw_args.push(format!("-{}<{}", dst, src));
606        }
607        self
608    }
609
610    /// 从文件复制标签(带追加重定向)
611    ///
612    /// 使用 `-+DSTTAG<SRCTAG` 追加方式复制标签。
613    ///
614    /// # 参数
615    ///
616    /// - `source` - 源文件路径
617    /// - `redirects` - 标签重定向列表,每项为 `(目标标签, 源标签)`。
618    ///   生成 `-+DSTTAG<SRCTAG` 参数。
619    ///
620    /// # 示例
621    ///
622    /// ```rust,no_run
623    /// use exiftool_rs_wrapper::ExifTool;
624    ///
625    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
626    /// let exiftool = ExifTool::new()?;
627    ///
628    /// // 从源文件追加 Keywords
629    /// exiftool.write("target.jpg")
630    ///     .copy_from_with_append("source.jpg", &[
631    ///         ("Keywords", "Keywords"),
632    ///     ])
633    ///     .overwrite_original(true)
634    ///     .execute()?;
635    /// # Ok(())
636    /// # }
637    /// ```
638    pub fn copy_from_with_append<P: AsRef<Path>>(
639        mut self,
640        source: P,
641        redirects: &[(&str, &str)],
642    ) -> Self {
643        self.raw_args.push("-tagsFromFile".to_string());
644        self.raw_args
645            .push(source.as_ref().to_string_lossy().to_string());
646        for (dst, src) in redirects {
647            self.raw_args.push(format!("-+{}<{}", dst, src));
648        }
649        self
650    }
651
652    /// 条件过滤(带编号)
653    ///
654    /// 使用 `-ifNUM` 选项设置条件过滤。
655    /// `-if2` 在第一个条件失败时仍然继续检查。
656    ///
657    /// # 参数
658    ///
659    /// - `num` - 条件编号(如 2 表示 `-if2`)
660    /// - `expr` - 条件表达式
661    pub fn condition_num(mut self, num: u8, expr: impl Into<String>) -> Self {
662        self.raw_args.push(format!("-if{}", num));
663        self.raw_args.push(expr.into());
664        self
665    }
666
667    /// 详细模式
668    ///
669    /// 使用 `-v` 或 `-vNUM` 选项设置写入时的详细输出级别。
670    pub fn verbose(mut self, level: Option<u8>) -> Self {
671        match level {
672            Some(n) => self.raw_args.push(format!("-v{}", n)),
673            None => self.raw_args.push("-v".to_string()),
674        }
675        self
676    }
677
678    /// 自定义打印格式(不追加换行符)
679    ///
680    /// 使用 `-p-` 选项按指定格式打印输出,不自动追加换行。
681    pub fn print_format_no_newline(mut self, format: impl Into<String>) -> Self {
682        self.raw_args.push("-p-".to_string());
683        self.raw_args.push(format.into());
684        self
685    }
686
687    /// 自定义打印格式
688    ///
689    /// 使用 `-p` 选项按指定格式打印输出。
690    pub fn print_format(mut self, format: impl Into<String>) -> Self {
691        self.raw_args.push("-p".to_string());
692        self.raw_args.push(format.into());
693        self
694    }
695
696    /// 保存错误文件名到文件(带级别和强制标志)
697    ///
698    /// 使用 `-efileNUM` 或 `-efile!` 或 `-efileNUM!` 变体。
699    ///
700    /// # 参数
701    ///
702    /// - `filename` - 输出文件路径
703    /// - `num` - 可选级别(2、3 等),`None` 表示默认级别
704    /// - `force` - 是否使用 `!` 后缀(强制覆盖)
705    pub fn efile_variant(
706        mut self,
707        filename: impl Into<String>,
708        num: Option<u8>,
709        force: bool,
710    ) -> Self {
711        let num_str = num.map_or(String::new(), |n| n.to_string());
712        let force_str = if force { "!" } else { "" };
713        self.raw_args
714            .push(format!("-efile{}{}", num_str, force_str));
715        self.raw_args.push(filename.into());
716        self
717    }
718
719    /// 导入 JSON 文件中的标签
720    ///
721    /// 使用 `-j=JSONFILE` 选项从 JSON 文件中导入标签数据。
722    pub fn json_import(mut self, path: impl Into<String>) -> Self {
723        self.raw_args.push(format!("-j={}", path.into()));
724        self
725    }
726
727    /// 追加导入 JSON 文件中的标签
728    ///
729    /// 使用 `-j+=JSONFILE` 选项从 JSON 文件中追加导入标签数据。
730    pub fn json_append(mut self, path: impl Into<String>) -> Self {
731        self.raw_args.push(format!("-j+={}", path.into()));
732        self
733    }
734
735    /// 导入 CSV 文件中的标签
736    ///
737    /// 使用 `-csv=CSVFILE` 选项从 CSV 文件中导入标签数据。
738    pub fn csv_import(mut self, path: impl Into<String>) -> Self {
739        self.raw_args.push(format!("-csv={}", path.into()));
740        self
741    }
742
743    /// 追加导入 CSV 文件中的标签
744    ///
745    /// 使用 `-csv+=CSVFILE` 选项从 CSV 文件中追加导入标签数据。
746    pub fn csv_append(mut self, path: impl Into<String>) -> Self {
747        self.raw_args.push(format!("-csv+={}", path.into()));
748        self
749    }
750
751    /// 文本输出到文件(追加模式)
752    ///
753    /// 使用 `-w+` 选项将输出追加到已有文件。
754    pub fn text_out_append(mut self, ext: impl Into<String>) -> Self {
755        self.raw_args.push("-w+".to_string());
756        self.raw_args.push(ext.into());
757        self
758    }
759
760    /// 文本输出到文件(仅创建新文件)
761    ///
762    /// 使用 `-w!` 选项将输出写入新文件,但不覆盖已有文件。
763    pub fn text_out_create(mut self, ext: impl Into<String>) -> Self {
764        self.raw_args.push("-w!".to_string());
765        self.raw_args.push(ext.into());
766        self
767    }
768
769    /// 标签输出到文件(追加模式)
770    ///
771    /// 使用 `-W+` 选项为每个标签创建输出文件,追加到已有文件。
772    pub fn tag_out_append(mut self, format: impl Into<String>) -> Self {
773        self.raw_args.push("-W+".to_string());
774        self.raw_args.push(format.into());
775        self
776    }
777
778    /// 标签输出到文件(仅创建新文件)
779    ///
780    /// 使用 `-W!` 选项为每个标签创建输出文件,但不覆盖已有文件。
781    pub fn tag_out_create(mut self, format: impl Into<String>) -> Self {
782        self.raw_args.push("-W!".to_string());
783        self.raw_args.push(format.into());
784        self
785    }
786
787    /// 追加扩展名过滤
788    ///
789    /// 使用 `-ext+` 选项追加文件扩展名过滤。
790    pub fn extension_add(mut self, ext: impl Into<String>) -> Self {
791        self.raw_args.push("-ext+".to_string());
792        self.raw_args.push(ext.into());
793        self
794    }
795
796    /// 文件扩展名过滤
797    ///
798    /// 使用 `-ext` 选项只处理指定扩展名的文件。
799    pub fn extension(mut self, ext: impl Into<String>) -> Self {
800        self.raw_args.push("-ext".to_string());
801        self.raw_args.push(ext.into());
802        self
803    }
804
805    /// 递归处理子目录
806    ///
807    /// 使用 `-r` 选项递归处理目录中的所有文件。
808    pub fn recursive(mut self, yes: bool) -> Self {
809        if yes {
810            self.raw_args.push("-r".to_string());
811        }
812        self
813    }
814
815    /// 递归处理子目录(包含隐藏目录)
816    ///
817    /// 使用 `-r.` 选项递归处理时包含以 `.` 开头的隐藏目录。
818    pub fn recursive_hidden(mut self) -> Self {
819        self.raw_args.push("-r.".to_string());
820        self
821    }
822
823    /// 执行写入操作
824    pub fn execute(self) -> Result<WriteResult> {
825        let args = self.build_args();
826        let response = self.exiftool.execute_raw(&args)?;
827        parse_write_response(response, self.path)
828    }
829
830    /// 构建参数列表
831    fn build_args(&self) -> Vec<String> {
832        let mut args = Vec::new();
833
834        // 覆盖原始文件
835        if self.overwrite_original {
836            args.push("-overwrite_original".to_string());
837        }
838
839        // 不创建备份
840        if !self.backup {
841            args.push("-overwrite_original_in_place".to_string());
842        }
843
844        // 输出到不同文件
845        if let Some(ref output) = self.output_path {
846            args.push("-o".to_string());
847            args.push(output.to_string_lossy().to_string());
848        }
849
850        // 条件
851        if let Some(ref condition) = self.condition {
852            args.push("-if".to_string());
853            args.push(condition.clone());
854        }
855
856        // 忽略次要错误
857        if self.ignore_minor_errors {
858            args.push("-m".to_string());
859        }
860
861        // 保留文件修改时间
862        if self.preserve_time {
863            args.push("-P".to_string());
864        }
865
866        // 静默模式
867        if self.quiet {
868            args.push("-q".to_string());
869        }
870
871        // ZIP 压缩
872        if self.zip_compression {
873            args.push("-z".to_string());
874        }
875
876        // 修复 MakerNotes 偏移
877        if self.fix_base_enabled {
878            match self.fix_base_offset {
879                Some(offset) => args.push(format!("-F{}", offset)),
880                None => args.push("-F".to_string()),
881            }
882        }
883
884        // 原始参数
885        args.extend(self.raw_args.clone());
886
887        // 删除标签
888        for tag in &self.delete_tags {
889            args.push(format!("-{}=", tag));
890        }
891
892        // 标签写入
893        for (tag, value) in &self.tags {
894            args.push(format!("-{}={}", tag, value));
895        }
896
897        // 文件路径
898        args.push(self.path.to_string_lossy().to_string());
899
900        args
901    }
902}
903
904/// 异步写入执行方法
905///
906/// 在 `async` feature 开启时,为 `WriteBuilder` 提供异步执行方法。
907/// Builder 的链式调用仍然是同步的(仅收集参数),
908/// 只有最终的 `async_execute` 才通过 `spawn_blocking` 异步执行。
909#[cfg(feature = "async")]
910impl WriteBuilder<'_> {
911    /// 异步执行写入操作
912    ///
913    /// 内部先收集参数(纯数据),然后在阻塞线程池中执行 ExifTool 命令。
914    /// 适用于 `AsyncExifTool::write_builder()` 返回的构建器。
915    ///
916    /// # 示例
917    ///
918    /// ```rust,no_run
919    /// use exiftool_rs_wrapper::AsyncExifTool;
920    ///
921    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
922    /// let async_et = AsyncExifTool::new()?;
923    ///
924    /// let result = async_et.write_builder("photo.jpg")
925    ///     .tag("Artist", "Photographer")
926    ///     .tag("Copyright", "© 2026")
927    ///     .overwrite_original(true)
928    ///     .async_execute()
929    ///     .await?;
930    /// # Ok(())
931    /// # }
932    /// ```
933    pub async fn async_execute(self) -> Result<WriteResult> {
934        // 先收集参数(纯数据,无引用)
935        let args = self.build_args();
936        let path = self.path.clone();
937        // clone ExifTool(内部是 Arc,开销极小)
938        let exiftool = self.exiftool.clone();
939        tokio::task::spawn_blocking(move || {
940            let response = exiftool.execute_raw(&args)?;
941            parse_write_response(response, path)
942        })
943        .await
944        .map_err(|e| Error::process(format!("异步写入任务执行失败: {}", e)))?
945    }
946}
947
948/// 解析写入响应为 WriteResult
949///
950/// 从 ExifTool 的响应中解析写入结果。
951/// 此函数被同步 `execute` 和异步 `async_execute` 共用。
952fn parse_write_response(response: crate::process::Response, path: PathBuf) -> Result<WriteResult> {
953    if response.is_error() {
954        return Err(Error::process(
955            response
956                .error_message()
957                .unwrap_or_else(|| "Unknown write error".to_string()),
958        ));
959    }
960
961    Ok(WriteResult {
962        path,
963        lines: response.lines().to_vec(),
964    })
965}
966
967/// 写入操作结果
968#[derive(Debug, Clone)]
969pub struct WriteResult {
970    /// 被修改的文件路径
971    pub path: PathBuf,
972
973    /// ExifTool 输出信息
974    pub lines: Vec<String>,
975}
976
977impl WriteResult {
978    /// 检查是否成功
979    pub fn is_success(&self) -> bool {
980        self.lines.iter().any(|line| {
981            line.contains("image files updated") || line.contains("image files unchanged")
982        })
983    }
984
985    /// 获取修改的文件数量
986    pub fn updated_count(&self) -> Option<u32> {
987        for line in &self.lines {
988            if let Some(pos) = line.find("image files updated") {
989                let num_str: String = line[..pos].chars().filter(|c| c.is_ascii_digit()).collect();
990                return num_str.parse().ok();
991            }
992        }
993        None
994    }
995
996    /// 获取创建的备份文件路径
997    pub fn backup_path(&self) -> Option<PathBuf> {
998        let backup = self.path.with_extension(format!(
999            "{}_original",
1000            self.path.extension()?.to_string_lossy()
1001        ));
1002        if backup.exists() { Some(backup) } else { None }
1003    }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009    use crate::Error;
1010
1011    #[test]
1012    fn test_write_builder_args() {
1013        let exiftool = match crate::ExifTool::new() {
1014            Ok(et) => et,
1015            Err(Error::ExifToolNotFound) => return,
1016            Err(e) => panic!("Unexpected error: {:?}", e),
1017        };
1018
1019        let args = exiftool
1020            .write("photo.jpg")
1021            .write_mode(WriteMode::Create)
1022            .password("p")
1023            .separator(",")
1024            .api_option("QuickTimeUTC", Some("1"))
1025            .user_param("k", Some("v"))
1026            .condition("$FileType eq 'JPEG'")
1027            .delete("Comment")
1028            .tag("Artist", "Alice")
1029            .build_args();
1030
1031        assert!(args.windows(2).any(|w| w == ["-wm", "c"]));
1032        assert!(args.windows(2).any(|w| w == ["-password", "p"]));
1033        assert!(args.windows(2).any(|w| w == ["-sep", ","]));
1034        assert!(args.windows(2).any(|w| w == ["-api", "QuickTimeUTC=1"]));
1035        assert!(args.windows(2).any(|w| w == ["-userParam", "k=v"]));
1036        assert!(args.windows(2).any(|w| w == ["-if", "$FileType eq 'JPEG'"]));
1037        assert!(args.iter().any(|a| a == "-Comment="));
1038        assert!(args.iter().any(|a| a == "-Artist=Alice"));
1039    }
1040
1041    #[test]
1042    fn test_write_result_parsing() {
1043        let result = WriteResult {
1044            path: PathBuf::from("test.jpg"),
1045            lines: vec!["    1 image files updated".to_string()],
1046        };
1047
1048        assert!(result.is_success());
1049        assert_eq!(result.updated_count(), Some(1));
1050    }
1051
1052    /// 测试标签写入运算符变体:追加、移除、前置
1053    #[test]
1054    fn test_tag_write_operators() {
1055        let exiftool = match crate::ExifTool::new() {
1056            Ok(et) => et,
1057            Err(Error::ExifToolNotFound) => return,
1058            Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
1059        };
1060
1061        // 测试追加运算符 +=
1062        let args = exiftool
1063            .write("photo.jpg")
1064            .tag_append("Keywords", "landscape")
1065            .build_args();
1066        assert!(
1067            args.iter().any(|a| a == "-Keywords+=landscape"),
1068            "参数列表应包含 \"-Keywords+=landscape\",实际: {:?}",
1069            args
1070        );
1071
1072        // 测试移除运算符 -=
1073        let args = exiftool
1074            .write("photo.jpg")
1075            .tag_remove("Keywords", "old")
1076            .build_args();
1077        assert!(
1078            args.iter().any(|a| a == "-Keywords-=old"),
1079            "参数列表应包含 \"-Keywords-=old\",实际: {:?}",
1080            args
1081        );
1082
1083        // 测试前置运算符 ^=
1084        let args = exiftool
1085            .write("photo.jpg")
1086            .tag_prepend("Keywords", "important")
1087            .build_args();
1088        assert!(
1089            args.iter().any(|a| a == "-Keywords^=important"),
1090            "参数列表应包含 \"-Keywords^=important\",实际: {:?}",
1091            args
1092        );
1093    }
1094
1095    /// 测试从文件读取标签值写入
1096    #[test]
1097    fn test_tag_from_file() {
1098        let exiftool = match crate::ExifTool::new() {
1099            Ok(et) => et,
1100            Err(Error::ExifToolNotFound) => return,
1101            Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
1102        };
1103
1104        // 测试从文件读取 <=
1105        let args = exiftool
1106            .write("photo.jpg")
1107            .tag_from_file("ThumbnailImage", "thumb.jpg")
1108            .build_args();
1109        assert!(
1110            args.iter().any(|a| a == "-ThumbnailImage<=thumb.jpg"),
1111            "参数列表应包含 \"-ThumbnailImage<=thumb.jpg\",实际: {:?}",
1112            args
1113        );
1114
1115        // 测试追加从文件读取 +<=
1116        let args = exiftool
1117            .write("photo.jpg")
1118            .tag_append_from_file("Comment", "comment.txt")
1119            .build_args();
1120        assert!(
1121            args.iter().any(|a| a == "-Comment+<=comment.txt"),
1122            "参数列表应包含 \"-Comment+<=comment.txt\",实际: {:?}",
1123            args
1124        );
1125    }
1126
1127    /// 测试 global_time_shift 方法:验证 -globalTimeShift 参数构建正确
1128    #[test]
1129    fn test_global_time_shift() {
1130        let exiftool = match crate::ExifTool::new() {
1131            Ok(et) => et,
1132            Err(Error::ExifToolNotFound) => return,
1133            Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
1134        };
1135
1136        let args = exiftool
1137            .write("photo.jpg")
1138            .global_time_shift("+02:00")
1139            .build_args();
1140
1141        assert!(
1142            args.windows(2).any(|w| w == ["-globalTimeShift", "+02:00"]),
1143            "参数列表应包含 [\"-globalTimeShift\", \"+02:00\"],实际: {:?}",
1144            args
1145        );
1146    }
1147}