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}