exiftool_rs_wrapper/query.rs
1//! 查询构建器 - Builder 模式实现
2
3use std::path::{Path, PathBuf};
4
5use crate::ExifTool;
6use crate::error::{Error, Result};
7use crate::process::Response;
8use crate::types::{Metadata, TagId};
9
10/// 转义格式
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum EscapeFormat {
13 /// HTML 转义 (-E)
14 Html,
15 /// XML 转义 (-ex)
16 Xml,
17 /// C 语言转义 (-ec)
18 C,
19}
20
21/// 查询构建器
22pub struct QueryBuilder<'et> {
23 exiftool: &'et ExifTool,
24 path: PathBuf,
25 args: Vec<String>,
26 include_unknown: bool,
27 include_duplicates: bool,
28 raw_values: bool,
29 group_by_category: bool,
30 no_composite: bool,
31 extract_embedded: Option<u8>,
32 extensions: Vec<String>,
33 ignore_dirs: Vec<String>,
34 recursive: bool,
35 progress_interval: Option<u32>,
36 progress_title: Option<String>,
37 specific_tags: Vec<String>,
38 excluded_tags: Vec<String>,
39 // 输出格式选项
40 decimal: bool,
41 escape_format: Option<EscapeFormat>,
42 force_print: bool,
43 group_names: Option<u8>,
44 html_format: bool,
45 hex: bool,
46 long_format: bool,
47 latin: bool,
48 short_format: Option<u8>,
49 tab_format: bool,
50 table_format: bool,
51 text_out: Option<String>,
52 tag_out: Option<String>,
53 tag_out_ext: Vec<String>,
54 list_item: Option<u32>,
55 file_order: Option<(String, bool)>,
56 quiet: bool,
57 // 高级输出选项
58 html_dump_enabled: bool,
59 html_dump_offset: Option<u32>,
60 php_format: bool,
61 plot_format: bool,
62 args_format: bool,
63 // 其他选项
64 common_args: Vec<String>,
65 echo: Vec<(String, Option<String>)>,
66 efile: Option<String>,
67}
68
69impl<'et> QueryBuilder<'et> {
70 /// 创建新的查询构建器
71 pub(crate) fn new<P: AsRef<Path>>(exiftool: &'et ExifTool, path: P) -> Self {
72 Self {
73 exiftool,
74 path: path.as_ref().to_path_buf(),
75 args: Vec::new(),
76 include_unknown: false,
77 include_duplicates: false,
78 raw_values: false,
79 group_by_category: false,
80 no_composite: false,
81 extract_embedded: None,
82 extensions: Vec::new(),
83 ignore_dirs: Vec::new(),
84 recursive: false,
85 progress_interval: None,
86 progress_title: None,
87 specific_tags: Vec::new(),
88 excluded_tags: Vec::new(),
89 // 输出格式选项
90 decimal: false,
91 escape_format: None,
92 force_print: false,
93 group_names: None,
94 html_format: false,
95 hex: false,
96 long_format: false,
97 latin: false,
98 short_format: None,
99 tab_format: false,
100 table_format: false,
101 text_out: None,
102 tag_out: None,
103 tag_out_ext: Vec::new(),
104 list_item: None,
105 file_order: None,
106 quiet: false,
107 // 高级输出选项
108 html_dump_enabled: false,
109 html_dump_offset: None,
110 php_format: false,
111 plot_format: false,
112 args_format: false,
113 // 其他选项
114 common_args: Vec::new(),
115 echo: Vec::new(),
116 efile: None,
117 }
118 }
119
120 /// 包含未知标签
121 pub fn include_unknown(mut self, yes: bool) -> Self {
122 self.include_unknown = yes;
123 self
124 }
125
126 /// 包含重复标签
127 pub fn include_duplicates(mut self, yes: bool) -> Self {
128 self.include_duplicates = yes;
129 self
130 }
131
132 /// 显示原始数值(而非格式化后的值)
133 pub fn raw_values(mut self, yes: bool) -> Self {
134 self.raw_values = yes;
135 self
136 }
137
138 /// 按类别分组(-g1 选项)
139 pub fn group_by_category(mut self, yes: bool) -> Self {
140 self.group_by_category = yes;
141 self
142 }
143
144 /// 禁用复合标签生成
145 ///
146 /// 使用 `-e` 选项禁用复合标签(Composite tags)的生成。
147 /// 复合标签是由 ExifTool 根据其他标签计算得出的派生标签。
148 ///
149 /// # 示例
150 ///
151 /// ```rust,no_run
152 /// use exiftool_rs_wrapper::ExifTool;
153 ///
154 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
155 /// let exiftool = ExifTool::new()?;
156 ///
157 /// // 只读取原始标签,不生成复合标签
158 /// let metadata = exiftool.query("photo.jpg")
159 /// .no_composite(true)
160 /// .execute()?;
161 /// # Ok(())
162 /// # }
163 /// ```
164 pub fn no_composite(mut self, yes: bool) -> Self {
165 self.no_composite = yes;
166 self
167 }
168
169 /// 提取嵌入文件信息
170 ///
171 /// 使用 `-ee` 选项从文件中提取嵌入的文件信息。
172 /// 例如从 RAW 文件中提取 JPEG 预览图的元数据。
173 ///
174 /// # 级别
175 ///
176 /// - `None` - 不提取嵌入文件(默认)
177 /// - `Some(1)` - `-ee` 提取直接嵌入的文件
178 /// - `Some(2)` - `-ee2` 提取所有层级的嵌入文件
179 /// - `Some(3+)` - `-ee3` 及以上更深入的提取
180 ///
181 /// # 示例
182 ///
183 /// ```rust,no_run
184 /// use exiftool_rs_wrapper::ExifTool;
185 ///
186 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
187 /// let exiftool = ExifTool::new()?;
188 ///
189 /// // 提取嵌入文件信息
190 /// let metadata = exiftool.query("photo.raw")
191 /// .extract_embedded(Some(1))
192 /// .execute()?;
193 /// # Ok(())
194 /// # }
195 /// ```
196 pub fn extract_embedded(mut self, level: Option<u8>) -> Self {
197 self.extract_embedded = level;
198 self
199 }
200
201 /// 设置文件扩展名过滤
202 ///
203 /// 使用 `-ext` 选项只处理指定扩展名的文件。
204 /// 可以使用多次来指定多个扩展名。
205 ///
206 /// # 示例
207 ///
208 /// ```rust,no_run
209 /// use exiftool_rs_wrapper::ExifTool;
210 ///
211 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
212 /// let exiftool = ExifTool::new()?;
213 ///
214 /// // 只处理 jpg 文件
215 /// let metadata = exiftool.query("/photos")
216 /// .extension("jpg")
217 /// .recursive(true)
218 /// .execute()?;
219 ///
220 /// // 处理多个扩展名
221 /// let metadata = exiftool.query("/photos")
222 /// .extension("jpg")
223 /// .extension("png")
224 /// .extension("raw")
225 /// .recursive(true)
226 /// .execute()?;
227 /// # Ok(())
228 /// # }
229 /// ```
230 pub fn extension(mut self, ext: impl Into<String>) -> Self {
231 self.extensions.push(ext.into());
232 self
233 }
234
235 /// 设置要忽略的目录
236 ///
237 /// 使用 `-i` 选项忽略指定的目录名称。
238 /// 在递归处理时,匹配的目录将被跳过。
239 ///
240 /// # 示例
241 ///
242 /// ```rust,no_run
243 /// use exiftool_rs_wrapper::ExifTool;
244 ///
245 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
246 /// let exiftool = ExifTool::new()?;
247 ///
248 /// // 忽略 .git 和 node_modules 目录
249 /// let metadata = exiftool.query("/project")
250 /// .ignore(".git")
251 /// .ignore("node_modules")
252 /// .recursive(true)
253 /// .execute()?;
254 /// # Ok(())
255 /// # }
256 /// ```
257 pub fn ignore(mut self, dir: impl Into<String>) -> Self {
258 self.ignore_dirs.push(dir.into());
259 self
260 }
261
262 /// 递归处理子目录
263 ///
264 /// 使用 `-r` 选项递归处理目录中的所有文件。
265 ///
266 /// # 示例
267 ///
268 /// ```rust,no_run
269 /// use exiftool_rs_wrapper::ExifTool;
270 ///
271 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
272 /// let exiftool = ExifTool::new()?;
273 ///
274 /// // 递归处理整个目录树
275 /// let metadata = exiftool.query("/photos")
276 /// .recursive(true)
277 /// .extension("jpg")
278 /// .execute()?;
279 /// # Ok(())
280 /// # }
281 /// ```
282 pub fn recursive(mut self, yes: bool) -> Self {
283 self.recursive = yes;
284 self
285 }
286
287 /// 启用进度显示
288 ///
289 /// 使用 `-progress` 选项在处理文件时显示进度信息。
290 /// 可以指定间隔(每隔多少文件显示一次)和标题。
291 ///
292 /// # 参数
293 ///
294 /// - `interval` - 每隔多少文件显示一次进度(None 表示每个文件都显示)
295 /// - `title` - 进度信息的标题(可选)
296 ///
297 /// # 示例
298 ///
299 /// ```rust,no_run
300 /// use exiftool_rs_wrapper::ExifTool;
301 ///
302 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
303 /// let exiftool = ExifTool::new()?;
304 ///
305 /// // 每 10 个文件显示一次进度
306 /// let metadata = exiftool.query("/photos")
307 /// .recursive(true)
308 /// .progress(Some(10), Some("Processing"))
309 /// .execute()?;
310 /// # Ok(())
311 /// # }
312 /// ```
313 pub fn progress(mut self, interval: Option<u32>, title: Option<impl Into<String>>) -> Self {
314 self.progress_interval = interval;
315 self.progress_title = title.map(|t| t.into());
316 self
317 }
318
319 /// 添加特定标签查询
320 pub fn tag(mut self, tag: impl Into<String>) -> Self {
321 self.specific_tags.push(tag.into());
322 self
323 }
324
325 /// 添加多个标签查询
326 pub fn tags(mut self, tags: &[impl AsRef<str>]) -> Self {
327 for tag in tags {
328 self.specific_tags.push(tag.as_ref().to_string());
329 }
330 self
331 }
332
333 /// 添加特定标签查询(使用 TagId)
334 pub fn tag_id(self, tag: TagId) -> Self {
335 self.tag(tag.name())
336 }
337
338 /// 排除特定标签
339 pub fn exclude(mut self, tag: impl Into<String>) -> Self {
340 self.excluded_tags.push(tag.into());
341 self
342 }
343
344 /// 排除多个标签
345 pub fn excludes(mut self, tags: &[impl AsRef<str>]) -> Self {
346 for tag in tags {
347 self.excluded_tags.push(tag.as_ref().to_string());
348 }
349 self
350 }
351
352 /// 排除特定标签(使用 TagId)
353 pub fn exclude_id(self, tag: TagId) -> Self {
354 self.exclude(tag.name())
355 }
356
357 /// 使用特定编码
358 pub fn charset(mut self, charset: impl Into<String>) -> Self {
359 self.args.push("-charset".to_string());
360 self.args.push(charset.into());
361 self
362 }
363
364 /// 使用特定语言
365 pub fn lang(mut self, lang: impl Into<String>) -> Self {
366 self.args.push("-lang".to_string());
367 self.args.push(lang.into());
368 self
369 }
370
371 /// 设置 GPS 坐标格式
372 ///
373 /// 使用 `-c` 选项设置 GPS 坐标的输出格式
374 ///
375 /// # 示例
376 ///
377 /// ```rust,no_run
378 /// use exiftool_rs_wrapper::ExifTool;
379 ///
380 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
381 /// let exiftool = ExifTool::new()?;
382 ///
383 /// // 使用小数度格式
384 /// let metadata = exiftool.query("photo.jpg")
385 /// .coord_format("%.6f")
386 /// .execute()?;
387 ///
388 /// // 使用度分秒格式
389 /// let metadata = exiftool.query("photo.jpg")
390 /// .coord_format("%d deg %d' %.2f\"")
391 /// .execute()?;
392 /// # Ok(())
393 /// # }
394 /// ```
395 pub fn coord_format(mut self, format: impl Into<String>) -> Self {
396 self.args.push("-c".to_string());
397 self.args.push(format.into());
398 self
399 }
400
401 /// 设置日期/时间格式
402 ///
403 /// 使用 `-d` 选项设置日期/时间值的输出格式
404 ///
405 /// # 预设格式
406 ///
407 /// - `"%Y:%m:%d %H:%M:%S"` - 标准 EXIF 格式(默认)
408 /// - `"%Y-%m-%d"` - ISO 日期格式
409 /// - `"%c"` - 本地时间格式
410 /// - `"%F %T"` - ISO 8601 格式
411 ///
412 /// # 示例
413 ///
414 /// ```rust,no_run
415 /// use exiftool_rs_wrapper::ExifTool;
416 ///
417 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
418 /// let exiftool = ExifTool::new()?;
419 ///
420 /// // 使用 ISO 格式
421 /// let metadata = exiftool.query("photo.jpg")
422 /// .date_format("%Y-%m-%d %H:%M:%S")
423 /// .execute()?;
424 /// # Ok(())
425 /// # }
426 /// ```
427 pub fn date_format(mut self, format: impl Into<String>) -> Self {
428 self.args.push("-d".to_string());
429 self.args.push(format.into());
430 self
431 }
432
433 /// 添加原始参数(高级用法)
434 pub fn arg(mut self, arg: impl Into<String>) -> Self {
435 self.args.push(arg.into());
436 self
437 }
438
439 /// 从文件读取参数
440 ///
441 /// 对应 ExifTool 的 `-@` 选项,从指定文件中读取命令行参数,
442 /// 每行一个参数。
443 ///
444 /// # 示例
445 ///
446 /// ```rust,no_run
447 /// use exiftool_rs_wrapper::ExifTool;
448 ///
449 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
450 /// let exiftool = ExifTool::new()?;
451 ///
452 /// let metadata = exiftool.query("photo.jpg")
453 /// .args_file("args.txt")
454 /// .execute()?;
455 /// # Ok(())
456 /// # }
457 /// ```
458 pub fn args_file(mut self, path: impl Into<String>) -> Self {
459 self.args.push("-@".to_string());
460 self.args.push(path.into());
461 self
462 }
463
464 /// 设置 CSV 分隔符
465 ///
466 /// 对应 ExifTool 的 `-csvDelim` 选项,设置 CSV 输出中使用的分隔符字符。
467 ///
468 /// # 示例
469 ///
470 /// ```rust,no_run
471 /// use exiftool_rs_wrapper::ExifTool;
472 ///
473 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
474 /// let exiftool = ExifTool::new()?;
475 ///
476 /// let output = exiftool.query("photo.jpg")
477 /// .csv_delimiter("\t")
478 /// .execute_text()?;
479 /// # Ok(())
480 /// # }
481 /// ```
482 pub fn csv_delimiter(mut self, delim: impl Into<String>) -> Self {
483 self.args.push("-csvDelim".to_string());
484 self.args.push(delim.into());
485 self
486 }
487
488 /// 加载替代文件的标签信息
489 ///
490 /// 对应 ExifTool 的 `-fileNUM` 选项,从替代文件中加载标签。
491 /// `num` 为文件编号(1-9),`path` 为替代文件路径。
492 ///
493 /// # 示例
494 ///
495 /// ```rust,no_run
496 /// use exiftool_rs_wrapper::ExifTool;
497 ///
498 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
499 /// let exiftool = ExifTool::new()?;
500 ///
501 /// let metadata = exiftool.query("photo.jpg")
502 /// .alternate_file(1, "other.jpg")
503 /// .execute()?;
504 /// # Ok(())
505 /// # }
506 /// ```
507 pub fn alternate_file(mut self, num: u8, path: impl Into<String>) -> Self {
508 self.args.push(format!("-file{}", num));
509 self.args.push(path.into());
510 self
511 }
512
513 /// 递归处理子目录(包含隐藏目录)
514 ///
515 /// 对应 ExifTool 的 `-r.` 选项,递归处理时包含以 `.` 开头的隐藏目录。
516 ///
517 /// # 示例
518 ///
519 /// ```rust,no_run
520 /// use exiftool_rs_wrapper::ExifTool;
521 ///
522 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
523 /// let exiftool = ExifTool::new()?;
524 ///
525 /// let metadata = exiftool.query("/photos")
526 /// .recursive_hidden()
527 /// .execute()?;
528 /// # Ok(())
529 /// # }
530 /// ```
531 pub fn recursive_hidden(mut self) -> Self {
532 self.args.push("-r.".to_string());
533 self
534 }
535
536 /// 设置源文件格式
537 ///
538 /// 对应 ExifTool 的 `-srcfile` 选项,指定处理时使用的源文件格式字符串。
539 /// 支持使用 `%d`、`%f`、`%e` 等占位符来匹配不同的源文件。
540 ///
541 /// # 示例
542 ///
543 /// ```rust,no_run
544 /// use exiftool_rs_wrapper::ExifTool;
545 ///
546 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
547 /// let exiftool = ExifTool::new()?;
548 ///
549 /// // 从 XMP sidecar 文件读取标签
550 /// let metadata = exiftool.query("photo.jpg")
551 /// .source_file("%d%f.xmp")
552 /// .execute()?;
553 /// # Ok(())
554 /// # }
555 /// ```
556 pub fn source_file(mut self, fmt: impl Into<String>) -> Self {
557 self.args.push("-srcfile".to_string());
558 self.args.push(fmt.into());
559 self
560 }
561
562 /// 提取未知二进制标签
563 ///
564 /// 对应 ExifTool 的 `-U` 选项,提取未知的二进制标签值。
565 /// 比 `-u` 更激进,会尝试解码未知的二进制数据。
566 ///
567 /// # 示例
568 ///
569 /// ```rust,no_run
570 /// use exiftool_rs_wrapper::ExifTool;
571 ///
572 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
573 /// let exiftool = ExifTool::new()?;
574 ///
575 /// let metadata = exiftool.query("photo.jpg")
576 /// .unknown_binary()
577 /// .execute()?;
578 /// # Ok(())
579 /// # }
580 /// ```
581 pub fn unknown_binary(mut self) -> Self {
582 self.args.push("-U".to_string());
583 self
584 }
585
586 /// 加载 ExifTool 插件模块
587 ///
588 /// 对应 ExifTool 的 `-use` 选项,加载指定的 ExifTool 插件模块。
589 ///
590 /// # 示例
591 ///
592 /// ```rust,no_run
593 /// use exiftool_rs_wrapper::ExifTool;
594 ///
595 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
596 /// let exiftool = ExifTool::new()?;
597 ///
598 /// let metadata = exiftool.query("photo.jpg")
599 /// .use_module("MWG")
600 /// .execute()?;
601 /// # Ok(())
602 /// # }
603 /// ```
604 pub fn use_module(mut self, module: impl Into<String>) -> Self {
605 self.args.push("-use".to_string());
606 self.args.push(module.into());
607 self
608 }
609
610 /// 设置自定义打印格式
611 ///
612 /// 使用 `-p` 选项按指定格式打印输出。
613 /// 使用 `$TAGNAME` 语法引用标签值。
614 ///
615 /// # 格式语法
616 ///
617 /// - `$TAGNAME` - 插入标签值
618 /// - `$TAGNAME#` - 插入原始数值(无格式化)
619 /// - `${TAGNAME:FMT}` - 使用指定格式
620 /// - `$$` - 插入 `$` 字符
621 ///
622 /// # 示例
623 ///
624 /// ```rust,no_run
625 /// use exiftool_rs_wrapper::ExifTool;
626 ///
627 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
628 /// let exiftool = ExifTool::new()?;
629 ///
630 /// // 自定义输出格式
631 /// let output = exiftool.query("photo.jpg")
632 /// .print_format("$FileName: $DateTimeOriginal ($Make $Model)")
633 /// .execute_text()?;
634 ///
635 /// println!("{}", output);
636 /// # Ok(())
637 /// # }
638 /// ```
639 pub fn print_format(mut self, format: impl Into<String>) -> Self {
640 self.args.push("-p".to_string());
641 self.args.push(format.into());
642 self
643 }
644
645 /// 禁用打印转换
646 ///
647 /// 使用 `-n` 选项禁用所有打印转换,显示原始数值。
648 /// 与 `raw_values()` 功能相同,但提供更直观的命名。
649 ///
650 /// # 示例
651 ///
652 /// ```rust,no_run
653 /// use exiftool_rs_wrapper::ExifTool;
654 ///
655 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
656 /// let exiftool = ExifTool::new()?;
657 ///
658 /// // 禁用打印转换,获取原始数值
659 /// let metadata = exiftool.query("photo.jpg")
660 /// .no_print_conv()
661 /// .execute()?;
662 /// # Ok(())
663 /// # }
664 /// ```
665 pub fn no_print_conv(mut self) -> Self {
666 self.raw_values = true;
667 self
668 }
669
670 /// 二进制输出
671 ///
672 /// 使用 `-b` 选项以二进制格式输出标签值。
673 /// 通常用于提取缩略图、预览图等二进制数据。
674 ///
675 /// # 示例
676 ///
677 /// ```rust,no_run
678 /// use exiftool_rs_wrapper::ExifTool;
679 ///
680 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
681 /// let exiftool = ExifTool::new()?;
682 ///
683 /// // 以二进制格式输出
684 /// let output = exiftool.query("photo.jpg")
685 /// .binary()
686 /// .tag("ThumbnailImage")
687 /// .execute_text()?;
688 /// # Ok(())
689 /// # }
690 /// ```
691 pub fn binary(mut self) -> Self {
692 self.args.push("-b".to_string());
693 self
694 }
695
696 /// 按组分类输出
697 ///
698 /// 使用 `-g` 选项按组分类显示标签。
699 /// 可选参数指定分组级别(0-7),默认为 0。
700 ///
701 /// # 示例
702 ///
703 /// ```rust,no_run
704 /// use exiftool_rs_wrapper::ExifTool;
705 ///
706 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
707 /// let exiftool = ExifTool::new()?;
708 ///
709 /// // 按默认分组级别显示
710 /// let metadata = exiftool.query("photo.jpg")
711 /// .group_headings(None)
712 /// .execute()?;
713 ///
714 /// // 按指定分组级别显示
715 /// let metadata = exiftool.query("photo.jpg")
716 /// .group_headings(Some(1))
717 /// .execute()?;
718 /// # Ok(())
719 /// # }
720 /// ```
721 pub fn group_headings(mut self, num: Option<u8>) -> Self {
722 match num {
723 Some(n) => self.args.push(format!("-g{}", n)),
724 None => self.args.push("-g".to_string()),
725 }
726 self
727 }
728
729 /// 按多个组级别分类输出
730 ///
731 /// 使用 `-gNUM:NUM:...` 选项按多个组级别分类显示标签。
732 /// 例如 `group_headings_multi(&[0, 1])` 生成 `-g0:1`。
733 ///
734 /// # 示例
735 ///
736 /// ```rust,no_run
737 /// use exiftool_rs_wrapper::ExifTool;
738 ///
739 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
740 /// let exiftool = ExifTool::new()?;
741 ///
742 /// // 按多组级别分类显示(-g0:1:2)
743 /// let metadata = exiftool.query("photo.jpg")
744 /// .group_headings_multi(&[0, 1, 2])
745 /// .execute()?;
746 /// # Ok(())
747 /// # }
748 /// ```
749 pub fn group_headings_multi(mut self, groups: &[u8]) -> Self {
750 if groups.is_empty() {
751 self.args.push("-g".to_string());
752 } else {
753 let nums: Vec<String> = groups.iter().map(|n| n.to_string()).collect();
754 self.args.push(format!("-g{}", nums.join(":")));
755 }
756 self
757 }
758
759 /// 短输出格式
760 ///
761 /// 使用 `-s` 选项以短格式输出标签名。
762 /// 可选参数指定短格式级别:
763 /// - `None` 或 `Some(1)` - `-s` 使用标签名而非描述
764 /// - `Some(2)` - `-s2` 更短的输出
765 /// - `Some(3)` - `-s3` 最短输出
766 ///
767 /// # 示例
768 ///
769 /// ```rust,no_run
770 /// use exiftool_rs_wrapper::ExifTool;
771 ///
772 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
773 /// let exiftool = ExifTool::new()?;
774 ///
775 /// let metadata = exiftool.query("photo.jpg")
776 /// .short(Some(2))
777 /// .execute()?;
778 /// # Ok(())
779 /// # }
780 /// ```
781 pub fn short(mut self, level: Option<u8>) -> Self {
782 match level {
783 Some(n) if n > 1 => self.args.push(format!("-s{}", n)),
784 _ => self.args.push("-s".to_string()),
785 }
786 self
787 }
788
789 /// 显式指定短格式级别
790 ///
791 /// 使用 `-sNUM` 选项显式指定短格式级别,支持 `-s1`、`-s2`、`-s3`。
792 ///
793 /// # 参数
794 ///
795 /// - `level` - 短格式级别(1、2 或 3)
796 ///
797 /// # 示例
798 ///
799 /// ```rust,no_run
800 /// use exiftool_rs_wrapper::ExifTool;
801 ///
802 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
803 /// let exiftool = ExifTool::new()?;
804 ///
805 /// // 使用 -s1 显式指定级别 1
806 /// let metadata = exiftool.query("photo.jpg")
807 /// .short_format_level(1)
808 /// .execute()?;
809 ///
810 /// // 使用 -s3 最短格式
811 /// let metadata = exiftool.query("photo.jpg")
812 /// .short_format_level(3)
813 /// .execute()?;
814 /// # Ok(())
815 /// # }
816 /// ```
817 pub fn short_format_level(mut self, level: u8) -> Self {
818 if (1..=3).contains(&level) {
819 self.args.push(format!("-s{}", level));
820 }
821 self
822 }
823
824 /// 极短输出格式
825 ///
826 /// 使用 `-S` 选项以极短格式输出(仅标签名和值)
827 ///
828 /// # 示例
829 ///
830 /// ```rust,no_run
831 /// use exiftool_rs_wrapper::ExifTool;
832 ///
833 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
834 /// let exiftool = ExifTool::new()?;
835 ///
836 /// let output = exiftool.query("photo.jpg")
837 /// .very_short()
838 /// .execute_text()?;
839 /// # Ok(())
840 /// # }
841 /// ```
842 pub fn very_short(mut self) -> Self {
843 self.args.push("-S".to_string());
844 self
845 }
846
847 /// 允许重复标签
848 ///
849 /// 使用 `-a` 选项允许输出中包含重复的标签。
850 /// 与 `include_duplicates(true)` 功能相同。
851 ///
852 /// # 示例
853 ///
854 /// ```rust,no_run
855 /// use exiftool_rs_wrapper::ExifTool;
856 ///
857 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
858 /// let exiftool = ExifTool::new()?;
859 ///
860 /// let metadata = exiftool.query("photo.jpg")
861 /// .allow_duplicates()
862 /// .execute()?;
863 /// # Ok(())
864 /// # }
865 /// ```
866 pub fn allow_duplicates(mut self) -> Self {
867 self.include_duplicates = true;
868 self
869 }
870
871 /// 提取未知标签
872 ///
873 /// 使用 `-u` 选项提取未知标签。
874 /// 与 `include_unknown(true)` 功能相同。
875 ///
876 /// # 示例
877 ///
878 /// ```rust,no_run
879 /// use exiftool_rs_wrapper::ExifTool;
880 ///
881 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
882 /// let exiftool = ExifTool::new()?;
883 ///
884 /// let metadata = exiftool.query("photo.jpg")
885 /// .unknown()
886 /// .execute()?;
887 /// # Ok(())
888 /// # }
889 /// ```
890 pub fn unknown(mut self) -> Self {
891 self.include_unknown = true;
892 self
893 }
894
895 /// XML 格式输出
896 ///
897 /// 使用 `-X` 选项以 XML/RDF 格式输出。
898 /// 通常与 `execute_text()` 配合使用获取 XML 文本。
899 ///
900 /// # 示例
901 ///
902 /// ```rust,no_run
903 /// use exiftool_rs_wrapper::ExifTool;
904 ///
905 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
906 /// let exiftool = ExifTool::new()?;
907 ///
908 /// let xml = exiftool.query("photo.jpg")
909 /// .xml_format()
910 /// .execute_text()?;
911 /// # Ok(())
912 /// # }
913 /// ```
914 pub fn xml_format(mut self) -> Self {
915 self.args.push("-X".to_string());
916 self
917 }
918
919 /// 忽略次要错误
920 ///
921 /// 使用 `-m` 选项忽略次要错误和警告,继续处理。
922 ///
923 /// # 示例
924 ///
925 /// ```rust,no_run
926 /// use exiftool_rs_wrapper::ExifTool;
927 ///
928 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
929 /// let exiftool = ExifTool::new()?;
930 ///
931 /// let metadata = exiftool.query("photo.jpg")
932 /// .ignore_minor_errors()
933 /// .execute()?;
934 /// # Ok(())
935 /// # }
936 /// ```
937 pub fn ignore_minor_errors(mut self) -> Self {
938 self.args.push("-m".to_string());
939 self
940 }
941
942 /// 按字母顺序排序输出
943 ///
944 /// 使用 `-sort` 选项对标签进行字母排序
945 pub fn sort(mut self, yes: bool) -> Self {
946 if yes {
947 self.args.push("-sort".to_string());
948 }
949 self
950 }
951
952 /// 设置列表项分隔符
953 ///
954 /// 使用 `-sep` 选项设置列表项的分隔符字符串
955 ///
956 /// # 示例
957 ///
958 /// ```rust,no_run
959 /// use exiftool_rs_wrapper::ExifTool;
960 ///
961 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
962 /// let exiftool = ExifTool::new()?;
963 ///
964 /// // 使用逗号分隔列表项
965 /// let metadata = exiftool.query("photo.jpg")
966 /// .separator(", ")
967 /// .execute()?;
968 /// # Ok(())
969 /// # }
970 /// ```
971 pub fn separator(mut self, sep: impl Into<String>) -> Self {
972 self.args.push("-sep".to_string());
973 self.args.push(sep.into());
974 self
975 }
976
977 /// 启用快速模式
978 ///
979 /// 使用 `-fast` 选项提高元数据提取速度。
980 /// 这会跳过某些处理步骤,可能遗漏某些信息。
981 ///
982 /// # 级别
983 ///
984 /// - `None` - 不使用快速模式(默认)
985 /// - `Some(1)` - `-fast` 基础快速模式
986 /// - `Some(2)` - `-fast2` 更激进的快速模式
987 ///
988 /// # 示例
989 ///
990 /// ```rust,no_run
991 /// use exiftool_rs_wrapper::ExifTool;
992 ///
993 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
994 /// let exiftool = ExifTool::new()?;
995 ///
996 /// // 使用快速模式处理大量文件
997 /// let metadata = exiftool.query("photo.jpg")
998 /// .fast(Some(1))
999 /// .execute()?;
1000 /// # Ok(())
1001 /// # }
1002 /// ```
1003 pub fn fast(mut self, level: Option<u8>) -> Self {
1004 match level {
1005 Some(1) => self.args.push("-fast".to_string()),
1006 Some(l) if l > 1 => self.args.push(format!("-fast{}", l)),
1007 _ => {}
1008 }
1009 self
1010 }
1011
1012 /// 强制扫描 XMP 数据
1013 ///
1014 /// 使用 `-scanForXMP` 选项暴力扫描文件中的 XMP 数据
1015 pub fn scan_for_xmp(mut self, yes: bool) -> Self {
1016 if yes {
1017 self.args.push("-scanForXMP".to_string());
1018 }
1019 self
1020 }
1021
1022 /// 设置 API 选项
1023 ///
1024 /// 使用 `-api` 选项设置 ExifTool API 选项。
1025 /// 常见选项包括:`QuickTimeUTC`, `SystemTags`, `largefilesupport`
1026 ///
1027 /// # 示例
1028 ///
1029 /// ```rust,no_run
1030 /// use exiftool_rs_wrapper::ExifTool;
1031 ///
1032 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1033 /// let exiftool = ExifTool::new()?;
1034 ///
1035 /// // 启用 QuickTimeUTC
1036 /// let metadata = exiftool.query("video.mp4")
1037 /// .api_option("QuickTimeUTC", None::<&str>)
1038 /// .execute()?;
1039 /// # Ok(())
1040 /// # }
1041 /// ```
1042 pub fn api_option(mut self, opt: impl Into<String>, value: Option<impl Into<String>>) -> Self {
1043 let option = opt.into();
1044 self.args.push("-api".to_string());
1045 match value {
1046 Some(v) => self.args.push(format!("{}={}", option, v.into())),
1047 None => self.args.push(option),
1048 }
1049 self
1050 }
1051
1052 /// 设置 API 选项(使用 `^=` 赋值语义)
1053 ///
1054 /// 使用 `-api OPT^=VAL` 设置 API 选项。
1055 /// 与 `=` 赋值的区别:省略值时 `=` 设为 undef,`^=` 设为空字符串。
1056 ///
1057 /// # 示例
1058 ///
1059 /// ```rust,no_run
1060 /// use exiftool_rs_wrapper::ExifTool;
1061 ///
1062 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1063 /// let exiftool = ExifTool::new()?;
1064 ///
1065 /// // 设置 API 选项为空字符串(-api OPT^=)
1066 /// let metadata = exiftool.query("photo.jpg")
1067 /// .api_option_empty("LargeFileSupport", None::<&str>)
1068 /// .execute()?;
1069 /// # Ok(())
1070 /// # }
1071 /// ```
1072 pub fn api_option_empty(
1073 mut self,
1074 opt: impl Into<String>,
1075 value: Option<impl Into<String>>,
1076 ) -> Self {
1077 let option = opt.into();
1078 self.args.push("-api".to_string());
1079 match value {
1080 Some(v) => self.args.push(format!("{}^={}", option, v.into())),
1081 None => self.args.push(format!("{}^=", option)),
1082 }
1083 self
1084 }
1085
1086 /// 设置用户参数
1087 ///
1088 /// 使用 `-userParam` 选项设置用户参数,可在配置文件中使用
1089 ///
1090 /// # 示例
1091 ///
1092 /// ```rust,no_run
1093 /// use exiftool_rs_wrapper::ExifTool;
1094 ///
1095 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1096 /// let exiftool = ExifTool::new()?;
1097 ///
1098 /// // 设置自定义参数
1099 /// let metadata = exiftool.query("photo.jpg")
1100 /// .user_param("MyParam", Some("value"))
1101 /// .execute()?;
1102 /// # Ok(())
1103 /// # }
1104 /// ```
1105 pub fn user_param(
1106 mut self,
1107 param: impl Into<String>,
1108 value: Option<impl Into<String>>,
1109 ) -> Self {
1110 let param = param.into();
1111 self.args.push("-userParam".to_string());
1112 match value {
1113 Some(v) => self.args.push(format!("{}={}", param, v.into())),
1114 None => self.args.push(param),
1115 }
1116 self
1117 }
1118
1119 /// 设置用户参数(使用 `^=` 赋值语义)
1120 ///
1121 /// 使用 `-userParam PARAM^=VAL` 设置用户参数。
1122 /// 与 `=` 赋值的区别:省略值时 `=` 设为 undef,`^=` 设为空字符串。
1123 ///
1124 /// # 示例
1125 ///
1126 /// ```rust,no_run
1127 /// use exiftool_rs_wrapper::ExifTool;
1128 ///
1129 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1130 /// let exiftool = ExifTool::new()?;
1131 ///
1132 /// // 设置用户参数为空字符串
1133 /// let metadata = exiftool.query("photo.jpg")
1134 /// .user_param_empty("MyParam", None::<&str>)
1135 /// .execute()?;
1136 /// # Ok(())
1137 /// # }
1138 /// ```
1139 pub fn user_param_empty(
1140 mut self,
1141 param: impl Into<String>,
1142 value: Option<impl Into<String>>,
1143 ) -> Self {
1144 let param = param.into();
1145 self.args.push("-userParam".to_string());
1146 match value {
1147 Some(v) => self.args.push(format!("{}^={}", param, v.into())),
1148 None => self.args.push(format!("{}^=", param)),
1149 }
1150 self
1151 }
1152
1153 /// 设置密码
1154 ///
1155 /// 使用 `-password` 选项处理受密码保护的文件
1156 ///
1157 /// # 安全性警告
1158 ///
1159 /// 密码将以纯文本形式传递给 ExifTool 进程。
1160 /// 在多用户系统中使用时请注意安全性。
1161 ///
1162 /// # 示例
1163 ///
1164 /// ```rust,no_run
1165 /// use exiftool_rs_wrapper::ExifTool;
1166 ///
1167 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1168 /// let exiftool = ExifTool::new()?;
1169 ///
1170 /// // 读取受密码保护的 PDF
1171 /// let metadata = exiftool.query("protected.pdf")
1172 /// .password("secret123")
1173 /// .execute()?;
1174 /// # Ok(())
1175 /// # }
1176 /// ```
1177 pub fn password(mut self, passwd: impl Into<String>) -> Self {
1178 self.args.push("-password".to_string());
1179 self.args.push(passwd.into());
1180 self
1181 }
1182
1183 /// 十进制显示标签 ID
1184 ///
1185 /// 使用 `-D` 选项以十进制格式显示标签 ID 编号
1186 pub fn decimal(mut self, yes: bool) -> Self {
1187 self.decimal = yes;
1188 self
1189 }
1190
1191 /// 转义格式
1192 ///
1193 /// 使用 `-E`、`-ex` 或 `-ec` 选项转义标签值
1194 pub fn escape(mut self, format: EscapeFormat) -> Self {
1195 self.escape_format = Some(format);
1196 self
1197 }
1198
1199 /// HTML 转义输出
1200 ///
1201 /// 使用 `-E` 选项对标签值进行 HTML 实体转义。
1202 /// 这是 `escape(EscapeFormat::Html)` 的快捷方法。
1203 ///
1204 /// # 示例
1205 ///
1206 /// ```rust,no_run
1207 /// use exiftool_rs_wrapper::ExifTool;
1208 ///
1209 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1210 /// let exiftool = ExifTool::new()?;
1211 ///
1212 /// let metadata = exiftool.query("photo.jpg")
1213 /// .escape_html()
1214 /// .execute()?;
1215 /// # Ok(())
1216 /// # }
1217 /// ```
1218 pub fn escape_html(self) -> Self {
1219 self.escape(EscapeFormat::Html)
1220 }
1221
1222 /// XML 转义输出
1223 ///
1224 /// 使用 `-ex` 选项对标签值进行 XML 转义。
1225 /// 这是 `escape(EscapeFormat::Xml)` 的快捷方法。
1226 ///
1227 /// # 示例
1228 ///
1229 /// ```rust,no_run
1230 /// use exiftool_rs_wrapper::ExifTool;
1231 ///
1232 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1233 /// let exiftool = ExifTool::new()?;
1234 ///
1235 /// let metadata = exiftool.query("photo.jpg")
1236 /// .escape_xml()
1237 /// .execute()?;
1238 /// # Ok(())
1239 /// # }
1240 /// ```
1241 pub fn escape_xml(self) -> Self {
1242 self.escape(EscapeFormat::Xml)
1243 }
1244
1245 /// C 语言转义输出
1246 ///
1247 /// 使用 `-ec` 选项对标签值进行 C 语言风格转义。
1248 /// 这是 `escape(EscapeFormat::C)` 的快捷方法。
1249 ///
1250 /// # 示例
1251 ///
1252 /// ```rust,no_run
1253 /// use exiftool_rs_wrapper::ExifTool;
1254 ///
1255 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1256 /// let exiftool = ExifTool::new()?;
1257 ///
1258 /// let metadata = exiftool.query("photo.jpg")
1259 /// .escape_c()
1260 /// .execute()?;
1261 /// # Ok(())
1262 /// # }
1263 /// ```
1264 pub fn escape_c(self) -> Self {
1265 self.escape(EscapeFormat::C)
1266 }
1267
1268 /// 强制打印
1269 ///
1270 /// 使用 `-f` 选项强制打印所有指定标签
1271 pub fn force_print(mut self, yes: bool) -> Self {
1272 self.force_print = yes;
1273 self
1274 }
1275
1276 /// 打印组名
1277 ///
1278 /// 使用 `-G` 选项打印每个标签的组名。
1279 /// 单个级别参数,生成 `-G` 或 `-GN`。
1280 pub fn group_names(mut self, level: Option<u8>) -> Self {
1281 self.group_names = level;
1282 self
1283 }
1284
1285 /// 打印多个组级别的组名
1286 ///
1287 /// 使用 `-GNUM:NUM:...` 选项打印多组级别的组名。
1288 /// 例如 `group_names_multi(&[0, 1])` 生成 `-G0:1`。
1289 ///
1290 /// # 示例
1291 ///
1292 /// ```rust,no_run
1293 /// use exiftool_rs_wrapper::ExifTool;
1294 ///
1295 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1296 /// let exiftool = ExifTool::new()?;
1297 ///
1298 /// // 按多组级别打印组名(-G0:1:2)
1299 /// let metadata = exiftool.query("photo.jpg")
1300 /// .group_names_multi(&[0, 1, 2])
1301 /// .execute()?;
1302 /// # Ok(())
1303 /// # }
1304 /// ```
1305 pub fn group_names_multi(mut self, groups: &[u8]) -> Self {
1306 if groups.is_empty() {
1307 self.args.push("-G".to_string());
1308 } else {
1309 let nums: Vec<String> = groups.iter().map(|n| n.to_string()).collect();
1310 self.args.push(format!("-G{}", nums.join(":")));
1311 }
1312 self
1313 }
1314
1315 /// HTML 格式
1316 ///
1317 /// 使用 `-h` 选项以 HTML 格式输出
1318 pub fn html_format(mut self, yes: bool) -> Self {
1319 self.html_format = yes;
1320 self
1321 }
1322
1323 /// 十六进制显示
1324 ///
1325 /// 使用 `-H` 选项以十六进制显示标签 ID
1326 pub fn hex(mut self, yes: bool) -> Self {
1327 self.hex = yes;
1328 self
1329 }
1330
1331 /// 长格式输出
1332 ///
1333 /// 使用 `-l` 选项以长格式(2行)输出
1334 pub fn long_format(mut self, yes: bool) -> Self {
1335 self.long_format = yes;
1336 self
1337 }
1338
1339 /// Latin1 编码
1340 ///
1341 /// 使用 `-L` 选项使用 Windows Latin1 编码
1342 pub fn latin(mut self, yes: bool) -> Self {
1343 self.latin = yes;
1344 self
1345 }
1346
1347 /// 短格式输出
1348 ///
1349 /// 使用 `-s` 或 `-S` 选项以短格式输出
1350 pub fn short_format(mut self, level: Option<u8>) -> Self {
1351 self.short_format = level;
1352 self
1353 }
1354
1355 /// Tab 分隔格式
1356 ///
1357 /// 使用 `-t` 选项以 Tab 分隔格式输出
1358 pub fn tab_format(mut self, yes: bool) -> Self {
1359 self.tab_format = yes;
1360 self
1361 }
1362
1363 /// 表格格式
1364 ///
1365 /// 使用 `-T` 选项以表格格式输出
1366 pub fn table_format(mut self, yes: bool) -> Self {
1367 self.table_format = yes;
1368 self
1369 }
1370
1371 /// 文本输出到文件
1372 ///
1373 /// 使用 `-w` 选项将输出写入文件
1374 pub fn text_out(mut self, ext: impl Into<String>) -> Self {
1375 self.text_out = Some(ext.into());
1376 self
1377 }
1378
1379 /// 标签输出到文件
1380 ///
1381 /// 使用 `-W` 选项为每个标签创建输出文件
1382 pub fn tag_out(mut self, format: impl Into<String>) -> Self {
1383 self.tag_out = Some(format.into());
1384 self
1385 }
1386
1387 /// 标签输出扩展名过滤
1388 ///
1389 /// 使用 `-Wext` 选项指定 `-W` 输出的文件类型
1390 pub fn tag_out_ext(mut self, ext: impl Into<String>) -> Self {
1391 self.tag_out_ext.push(ext.into());
1392 self
1393 }
1394
1395 /// 提取列表项
1396 ///
1397 /// 使用 `-listItem` 选项提取列表中的特定项
1398 pub fn list_item(mut self, index: u32) -> Self {
1399 self.list_item = Some(index);
1400 self
1401 }
1402
1403 /// 列出目录而非文件
1404 ///
1405 /// 使用 `-list_dir` 选项让 ExifTool 列出目录名而非处理文件。
1406 /// 与 `-r` 递归选项配合使用时,输出目录树结构。
1407 ///
1408 /// # 示例
1409 ///
1410 /// ```rust,no_run
1411 /// use exiftool_rs_wrapper::ExifTool;
1412 ///
1413 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1414 /// let exiftool = ExifTool::new()?;
1415 ///
1416 /// // 列出目录而非文件
1417 /// let output = exiftool.query("/photos")
1418 /// .list_dir()
1419 /// .recursive(true)
1420 /// .execute_text()?;
1421 /// # Ok(())
1422 /// # }
1423 /// ```
1424 pub fn list_dir(mut self) -> Self {
1425 self.args.push("-list_dir".to_string());
1426 self
1427 }
1428
1429 /// 文件处理顺序
1430 ///
1431 /// 使用 `-fileOrder` 选项设置文件处理顺序
1432 pub fn file_order(mut self, tag: impl Into<String>, descending: bool) -> Self {
1433 self.file_order = Some((tag.into(), descending));
1434 self
1435 }
1436
1437 /// 静默模式
1438 ///
1439 /// 使用 `-q` 选项减少输出信息
1440 pub fn quiet(mut self, yes: bool) -> Self {
1441 self.quiet = yes;
1442 self
1443 }
1444
1445 /// HTML 二进制转储
1446 ///
1447 /// 使用 `-htmlDump` 选项生成 HTML 格式的二进制转储。
1448 /// 可以指定可选的偏移量。
1449 ///
1450 /// # 参数
1451 ///
1452 /// - `offset` - 可选的起始偏移量。`None` 表示无偏移(纯 `-htmlDump`),
1453 /// `Some(n)` 表示带偏移量(`-htmlDumpN`)
1454 pub fn html_dump(mut self, offset: Option<u32>) -> Self {
1455 self.html_dump_enabled = true;
1456 self.html_dump_offset = offset;
1457 self
1458 }
1459
1460 /// HTML 二进制转储(带指定偏移量)
1461 ///
1462 /// 使用 `-htmlDumpOFFSET` 选项生成带偏移量的 HTML 二进制转储。
1463 /// 这是 `html_dump(Some(offset))` 的便捷方法。
1464 pub fn html_dump_offset(mut self, offset: u32) -> Self {
1465 self.html_dump_enabled = true;
1466 self.html_dump_offset = Some(offset);
1467 self
1468 }
1469
1470 /// PHP数组格式输出
1471 ///
1472 /// 使用 `-php` 选项导出为PHP数组格式
1473 pub fn php_format(mut self, yes: bool) -> Self {
1474 self.php_format = yes;
1475 self
1476 }
1477
1478 /// SVG plot格式输出
1479 ///
1480 /// 使用 `-plot` 选项输出为SVG plot文件
1481 pub fn plot_format(mut self, yes: bool) -> Self {
1482 self.plot_format = yes;
1483 self
1484 }
1485
1486 /// 格式化为exiftool参数
1487 ///
1488 /// 使用 `-args` 选项将元数据格式化为exiftool参数格式
1489 pub fn args_format(mut self, yes: bool) -> Self {
1490 self.args_format = yes;
1491 self
1492 }
1493
1494 /// 设置公共参数
1495 ///
1496 /// 使用 `-common_args` 选项定义在多个命令之间共享的参数
1497 pub fn common_args(mut self, args: &[impl AsRef<str>]) -> Self {
1498 for arg in args {
1499 self.common_args.push(arg.as_ref().to_string());
1500 }
1501 self
1502 }
1503
1504 /// 输出文本到stdout或stderr
1505 ///
1506 /// 使用 `-echo` 选项在处理期间输出文本
1507 /// - `text`: 要输出的文本
1508 /// - `target`: 输出目标,None表示stdout,Some("stderr")表示stderr
1509 pub fn echo(mut self, text: impl Into<String>, target: Option<impl Into<String>>) -> Self {
1510 self.echo.push((text.into(), target.map(|t| t.into())));
1511 self
1512 }
1513
1514 /// 指定级别的echo输出
1515 ///
1516 /// 使用 `-echoNUM` 选项在处理期间输出文本到指定级别的输出流。
1517 /// - 级别 1: stdout (等价于 `-echo`)
1518 /// - 级别 2: stderr (等价于 `-echo2`)
1519 /// - 级别 3: 额外输出级别3
1520 /// - 级别 4: 额外输出级别4
1521 ///
1522 /// # 参数
1523 ///
1524 /// - `level` - echo级别 (1-4)
1525 /// - `text` - 要输出的文本
1526 ///
1527 /// # 示例
1528 ///
1529 /// ```rust,no_run
1530 /// use exiftool_rs_wrapper::ExifTool;
1531 ///
1532 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1533 /// let exiftool = ExifTool::new()?;
1534 ///
1535 /// // 使用 -echo3 输出到级别3
1536 /// let metadata = exiftool.query("photo.jpg")
1537 /// .echo_level(3, "Processing...")
1538 /// .execute()?;
1539 /// # Ok(())
1540 /// # }
1541 /// ```
1542 pub fn echo_level(mut self, level: u8, text: impl Into<String>) -> Self {
1543 if (1..=4).contains(&level) {
1544 self.args.push(format!("-echo{}", level));
1545 self.args.push(text.into());
1546 }
1547 self
1548 }
1549
1550 /// 保存错误文件名到文件
1551 ///
1552 /// 使用 `-efile` 选项将处理失败的文件名保存到指定文件
1553 pub fn efile(mut self, filename: impl Into<String>) -> Self {
1554 self.efile = Some(filename.into());
1555 self
1556 }
1557
1558 /// 保存错误文件名到文件(带级别和强制标志)
1559 ///
1560 /// 使用 `-efileNUM` 或 `-efile!` 或 `-efileNUM!` 变体。
1561 ///
1562 /// # 参数
1563 ///
1564 /// - `filename` - 输出文件路径
1565 /// - `num` - 可选级别(2、3 等),`None` 表示默认级别
1566 /// - `force` - 是否使用 `!` 后缀(强制覆盖)
1567 pub fn efile_variant(
1568 mut self,
1569 filename: impl Into<String>,
1570 num: Option<u8>,
1571 force: bool,
1572 ) -> Self {
1573 let num_str = num.map_or(String::new(), |n| n.to_string());
1574 let force_str = if force { "!" } else { "" };
1575 self.args.push(format!("-efile{}{}", num_str, force_str));
1576 self.args.push(filename.into());
1577 self
1578 }
1579
1580 /// 详细模式
1581 ///
1582 /// 使用 `-v` 或 `-vNUM` 选项设置详细输出级别(0-5)。
1583 ///
1584 /// # 参数
1585 ///
1586 /// - `level` - 详细级别,`None` 表示 `-v`(默认级别 1),
1587 /// `Some(n)` 表示 `-vN`
1588 pub fn verbose(mut self, level: Option<u8>) -> Self {
1589 match level {
1590 Some(n) => self.args.push(format!("-v{}", n)),
1591 None => self.args.push("-v".to_string()),
1592 }
1593 self
1594 }
1595
1596 /// 追加扩展名过滤
1597 ///
1598 /// 使用 `-ext+` 选项追加文件扩展名过滤(而非替换已有列表)。
1599 /// 与 `extension()` 不同,此方法会在已有扩展名列表基础上追加。
1600 pub fn extension_add(mut self, ext: impl Into<String>) -> Self {
1601 self.args.push("-ext+".to_string());
1602 self.args.push(ext.into());
1603 self
1604 }
1605
1606 /// 次级文件处理顺序
1607 ///
1608 /// 使用 `-fileOrderNUM` 选项设置次级排序。
1609 /// `num` 为排序优先级(如 2 表示 `-fileOrder2`)。
1610 pub fn file_order_secondary(
1611 mut self,
1612 num: u8,
1613 tag: impl Into<String>,
1614 descending: bool,
1615 ) -> Self {
1616 let order = if descending { "-" } else { "" };
1617 self.args.push(format!("-fileOrder{}", num));
1618 self.args.push(format!("{}{}", order, tag.into()));
1619 self
1620 }
1621
1622 /// 条件过滤(带编号)
1623 ///
1624 /// 使用 `-ifNUM` 选项设置条件过滤。
1625 /// `-if2` 在第一个条件失败时仍然继续检查。
1626 /// `-if3` 及更高编号用于更细粒度的控制。
1627 ///
1628 /// # 参数
1629 ///
1630 /// - `num` - 条件编号(如 2 表示 `-if2`)
1631 /// - `expr` - 条件表达式
1632 pub fn condition_num(mut self, num: u8, expr: impl Into<String>) -> Self {
1633 self.args.push(format!("-if{}", num));
1634 self.args.push(expr.into());
1635 self
1636 }
1637
1638 /// 标签输出到文件(追加模式)
1639 ///
1640 /// 使用 `-W+` 选项为每个标签创建输出文件,追加到已有文件。
1641 pub fn tag_out_append(mut self, format: impl Into<String>) -> Self {
1642 self.args.push("-W+".to_string());
1643 self.args.push(format.into());
1644 self
1645 }
1646
1647 /// 标签输出到文件(仅创建新文件)
1648 ///
1649 /// 使用 `-W!` 选项为每个标签创建输出文件,但不覆盖已有文件。
1650 pub fn tag_out_create(mut self, format: impl Into<String>) -> Self {
1651 self.args.push("-W!".to_string());
1652 self.args.push(format.into());
1653 self
1654 }
1655
1656 /// 文本输出到文件(追加模式)
1657 ///
1658 /// 使用 `-w+` 选项将输出追加到已有文件。
1659 pub fn text_out_append(mut self, ext: impl Into<String>) -> Self {
1660 self.args.push("-w+".to_string());
1661 self.args.push(ext.into());
1662 self
1663 }
1664
1665 /// 文本输出到文件(仅创建新文件)
1666 ///
1667 /// 使用 `-w!` 选项将输出写入新文件,但不覆盖已有文件。
1668 pub fn text_out_create(mut self, ext: impl Into<String>) -> Self {
1669 self.args.push("-w!".to_string());
1670 self.args.push(ext.into());
1671 self
1672 }
1673
1674 /// 自定义打印格式(不追加换行符)
1675 ///
1676 /// 使用 `-p-` 选项按指定格式打印输出,但不在每行末尾追加换行符。
1677 /// 与 `print_format()` 的区别在于输出不会自动追加换行。
1678 pub fn print_format_no_newline(mut self, format: impl Into<String>) -> Self {
1679 self.args.push("-p-".to_string());
1680 self.args.push(format.into());
1681 self
1682 }
1683
1684 /// 导入 JSON 文件中的标签
1685 ///
1686 /// 使用 `-j=JSONFILE` 选项从 JSON 文件中导入标签数据。
1687 pub fn json_import(mut self, path: impl Into<String>) -> Self {
1688 self.args.push(format!("-j={}", path.into()));
1689 self
1690 }
1691
1692 /// 追加导入 JSON 文件中的标签
1693 ///
1694 /// 使用 `-j+=JSONFILE` 选项从 JSON 文件中追加导入标签数据。
1695 pub fn json_append(mut self, path: impl Into<String>) -> Self {
1696 self.args.push(format!("-j+={}", path.into()));
1697 self
1698 }
1699
1700 /// 导入 CSV 文件中的标签
1701 ///
1702 /// 使用 `-csv=CSVFILE` 选项从 CSV 文件中导入标签数据。
1703 pub fn csv_import(mut self, path: impl Into<String>) -> Self {
1704 self.args.push(format!("-csv={}", path.into()));
1705 self
1706 }
1707
1708 /// 追加导入 CSV 文件中的标签
1709 ///
1710 /// 使用 `-csv+=CSVFILE` 选项从 CSV 文件中追加导入标签数据。
1711 pub fn csv_append(mut self, path: impl Into<String>) -> Self {
1712 self.args.push(format!("-csv+={}", path.into()));
1713 self
1714 }
1715
1716 /// 修复 MakerNotes 基准偏移
1717 ///
1718 /// 使用 `-F` 选项修复 MakerNotes 的基准偏移。
1719 /// 不带参数时自动修复,带偏移量时使用指定偏移。
1720 ///
1721 /// # 参数
1722 ///
1723 /// - `offset` - 可选的偏移量修正值。`None` 表示自动修复(`-F`),
1724 /// `Some(n)` 表示指定偏移(`-Fn`)
1725 pub fn fix_base(mut self, offset: Option<i64>) -> Self {
1726 match offset {
1727 Some(n) => self.args.push(format!("-F{}", n)),
1728 None => self.args.push("-F".to_string()),
1729 }
1730 self
1731 }
1732
1733 /// 修复 MakerNotes 基准偏移(带指定偏移量)
1734 ///
1735 /// 使用 `-FOFFSET` 选项修复 MakerNotes 偏移。
1736 /// 这是 `fix_base(Some(offset))` 的便捷方法。
1737 pub fn fix_base_offset(mut self, offset: i64) -> Self {
1738 self.args.push(format!("-F{}", offset));
1739 self
1740 }
1741
1742 /// 执行查询
1743 pub fn execute(self) -> Result<Metadata> {
1744 let args = self.build_args(true);
1745
1746 // 发送命令并获取响应
1747 let response = self.exiftool.execute_raw(&args)?;
1748
1749 // 解析响应
1750 self.parse_response(response)
1751 }
1752
1753 /// 执行查询并返回 JSON
1754 pub fn execute_json(self) -> Result<serde_json::Value> {
1755 let args = self.build_args(true);
1756 let response = self.exiftool.execute_raw(&args)?;
1757 response.json()
1758 }
1759
1760 /// 执行查询并反序列化为自定义类型
1761 pub fn execute_as<T: serde::de::DeserializeOwned>(self) -> Result<T> {
1762 let args = self.build_args(true);
1763 let response = self.exiftool.execute_raw(&args)?;
1764 response.json()
1765 }
1766
1767 /// 执行查询并返回纯文本
1768 ///
1769 /// 当使用 `-p` (print_format) 选项时,
1770 /// 使用此方法获取纯文本输出而非 JSON
1771 ///
1772 /// # 示例
1773 ///
1774 /// ```rust,no_run
1775 /// use exiftool_rs_wrapper::ExifTool;
1776 ///
1777 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1778 /// let exiftool = ExifTool::new()?;
1779 ///
1780 /// let output = exiftool.query("photo.jpg")
1781 /// .print_format("$FileName: $DateTimeOriginal")
1782 /// .execute_text()?;
1783 ///
1784 /// println!("{}", output);
1785 /// # Ok(())
1786 /// # }
1787 /// ```
1788 pub fn execute_text(self) -> Result<String> {
1789 let args = self.build_args(false);
1790 let response = self.exiftool.execute_raw(&args)?;
1791 Ok(response.text().trim().to_string())
1792 }
1793
1794 /// 构建参数列表
1795 fn build_args(&self, json_output: bool) -> Vec<String> {
1796 let mut args = Vec::new();
1797
1798 if json_output {
1799 args.push("-json".to_string());
1800 }
1801
1802 // 分组选项
1803 if self.group_by_category {
1804 args.push("-g1".to_string());
1805 }
1806
1807 // 未知标签
1808 if self.include_unknown {
1809 args.push("-u".to_string());
1810 }
1811
1812 // 重复标签
1813 if self.include_duplicates {
1814 args.push("-a".to_string());
1815 }
1816
1817 // 原始数值
1818 if self.raw_values {
1819 args.push("-n".to_string());
1820 }
1821
1822 // 十进制显示
1823 if self.decimal {
1824 args.push("-D".to_string());
1825 }
1826
1827 // 转义格式
1828 if let Some(format) = self.escape_format {
1829 let flag = match format {
1830 EscapeFormat::Html => "-E",
1831 EscapeFormat::Xml => "-ex",
1832 EscapeFormat::C => "-ec",
1833 };
1834 args.push(flag.to_string());
1835 }
1836
1837 // 强制打印
1838 if self.force_print {
1839 args.push("-f".to_string());
1840 }
1841
1842 // 组名(-G0, -G1, -G2 等)
1843 if let Some(level) = self.group_names {
1844 args.push(format!("-G{}", level));
1845 }
1846
1847 // HTML 格式
1848 if self.html_format {
1849 args.push("-h".to_string());
1850 }
1851
1852 // 十六进制
1853 if self.hex {
1854 args.push("-H".to_string());
1855 }
1856
1857 // 长格式
1858 if self.long_format {
1859 args.push("-l".to_string());
1860 }
1861
1862 // Latin1 编码
1863 if self.latin {
1864 args.push("-L".to_string());
1865 }
1866
1867 // 短格式
1868 if let Some(level) = self.short_format {
1869 if level == 0 {
1870 args.push("-S".to_string());
1871 } else {
1872 args.push(format!("-s{}", level));
1873 }
1874 }
1875
1876 // Tab 格式
1877 if self.tab_format {
1878 args.push("-t".to_string());
1879 }
1880
1881 // 表格格式
1882 if self.table_format {
1883 args.push("-T".to_string());
1884 }
1885
1886 // 文本输出
1887 if let Some(ref ext) = self.text_out {
1888 args.push("-w".to_string());
1889 args.push(ext.clone());
1890 }
1891
1892 // 标签输出
1893 if let Some(ref format) = self.tag_out {
1894 args.push("-W".to_string());
1895 args.push(format.clone());
1896 }
1897
1898 // 标签输出扩展名
1899 for ext in &self.tag_out_ext {
1900 args.push("-Wext".to_string());
1901 args.push(ext.clone());
1902 }
1903
1904 // 列表项
1905 if let Some(index) = self.list_item {
1906 args.push("-listItem".to_string());
1907 args.push(index.to_string());
1908 }
1909
1910 // 文件顺序
1911 if let Some((ref tag, descending)) = self.file_order {
1912 let order = if descending { "-" } else { "" };
1913 args.push("-fileOrder".to_string());
1914 args.push(format!("{}{}", order, tag));
1915 }
1916
1917 // 静默模式
1918 if self.quiet {
1919 args.push("-q".to_string());
1920 }
1921
1922 // HTML 二进制转储
1923 if self.html_dump_enabled {
1924 match self.html_dump_offset {
1925 Some(offset) => args.push(format!("-htmlDump{}", offset)),
1926 None => args.push("-htmlDump".to_string()),
1927 }
1928 }
1929
1930 // PHP数组格式
1931 if self.php_format {
1932 args.push("-php".to_string());
1933 }
1934
1935 // SVG plot格式
1936 if self.plot_format {
1937 args.push("-plot".to_string());
1938 }
1939
1940 // 格式化为exiftool参数
1941 if self.args_format {
1942 args.push("-args".to_string());
1943 }
1944
1945 // 公共参数
1946 for arg in &self.common_args {
1947 args.push("-common_args".to_string());
1948 args.push(arg.clone());
1949 }
1950
1951 // echo输出
1952 for (text, target) in &self.echo {
1953 let option = match target {
1954 Some(t) if t == "stderr" => "-echo2",
1955 _ => "-echo",
1956 };
1957 args.push(option.to_string());
1958 args.push(text.clone());
1959 }
1960
1961 // 错误文件
1962 if let Some(ref filename) = self.efile {
1963 args.push("-efile".to_string());
1964 args.push(filename.clone());
1965 }
1966
1967 // 禁用复合标签
1968 if self.no_composite {
1969 args.push("-e".to_string());
1970 }
1971
1972 // 提取嵌入文件
1973 if let Some(level) = self.extract_embedded {
1974 if level == 1 {
1975 args.push("-ee".to_string());
1976 } else {
1977 args.push(format!("-ee{}", level));
1978 }
1979 }
1980
1981 // 文件扩展名过滤
1982 for ext in &self.extensions {
1983 args.push("-ext".to_string());
1984 args.push(ext.clone());
1985 }
1986
1987 // 忽略目录
1988 for dir in &self.ignore_dirs {
1989 args.push("-i".to_string());
1990 args.push(dir.clone());
1991 }
1992
1993 // 递归处理
1994 if self.recursive {
1995 args.push("-r".to_string());
1996 }
1997
1998 // 进度显示
1999 if let Some(interval) = self.progress_interval {
2000 let progress_arg = if let Some(ref title) = self.progress_title {
2001 format!("-progress{}:{}", interval, title)
2002 } else {
2003 format!("-progress{}", interval)
2004 };
2005 args.push(progress_arg);
2006 }
2007
2008 // 添加自定义参数
2009 args.extend(self.args.clone());
2010
2011 // 特定标签
2012 for tag in &self.specific_tags {
2013 args.push(format!("-{}", tag));
2014 }
2015
2016 // 排除标签
2017 for tag in &self.excluded_tags {
2018 args.push(format!("--{}", tag));
2019 }
2020
2021 // 文件路径
2022 args.push(self.path.to_string_lossy().to_string());
2023
2024 args
2025 }
2026
2027 /// 解析响应
2028 fn parse_response(&self, response: Response) -> Result<Metadata> {
2029 parse_query_response(response)
2030 }
2031}
2032
2033/// 异步查询执行方法
2034///
2035/// 在 `async` feature 开启时,为 `QueryBuilder` 提供异步执行方法。
2036/// Builder 的链式调用仍然是同步的(仅收集参数),
2037/// 只有最终的 `async_execute` 才通过 `spawn_blocking` 异步执行。
2038#[cfg(feature = "async")]
2039impl QueryBuilder<'_> {
2040 /// 异步执行查询
2041 ///
2042 /// 内部先收集参数(纯数据),然后在阻塞线程池中执行 ExifTool 命令。
2043 /// 适用于 `AsyncExifTool::query_builder()` 返回的构建器。
2044 ///
2045 /// # 示例
2046 ///
2047 /// ```rust,no_run
2048 /// use exiftool_rs_wrapper::AsyncExifTool;
2049 ///
2050 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2051 /// let async_et = AsyncExifTool::new()?;
2052 ///
2053 /// let metadata = async_et.query_builder("photo.jpg")
2054 /// .binary()
2055 /// .group_headings(None)
2056 /// .tag("Make")
2057 /// .async_execute()
2058 /// .await?;
2059 /// # Ok(())
2060 /// # }
2061 /// ```
2062 pub async fn async_execute(self) -> Result<Metadata> {
2063 // 先收集参数(纯数据,无引用)
2064 let args = self.build_args(true);
2065 // clone ExifTool(内部是 Arc,开销极小)
2066 let exiftool = self.exiftool.clone();
2067 tokio::task::spawn_blocking(move || {
2068 let response = exiftool.execute_raw(&args)?;
2069 parse_query_response(response)
2070 })
2071 .await
2072 .map_err(|e| Error::process(format!("异步查询任务执行失败: {}", e)))?
2073 }
2074
2075 /// 异步执行查询并返回 JSON
2076 ///
2077 /// 与 `execute_json` 相同,但在阻塞线程池中异步执行。
2078 pub async fn async_execute_json(self) -> Result<serde_json::Value> {
2079 let args = self.build_args(true);
2080 let exiftool = self.exiftool.clone();
2081 tokio::task::spawn_blocking(move || {
2082 let response = exiftool.execute_raw(&args)?;
2083 response.json()
2084 })
2085 .await
2086 .map_err(|e| Error::process(format!("异步查询任务执行失败: {}", e)))?
2087 }
2088
2089 /// 异步执行查询并反序列化为自定义类型
2090 ///
2091 /// 与 `execute_as` 相同,但在阻塞线程池中异步执行。
2092 pub async fn async_execute_as<T: serde::de::DeserializeOwned + Send + 'static>(
2093 self,
2094 ) -> Result<T> {
2095 let args = self.build_args(true);
2096 let exiftool = self.exiftool.clone();
2097 tokio::task::spawn_blocking(move || {
2098 let response = exiftool.execute_raw(&args)?;
2099 response.json()
2100 })
2101 .await
2102 .map_err(|e| Error::process(format!("异步查询任务执行失败: {}", e)))?
2103 }
2104
2105 /// 异步执行查询并返回纯文本
2106 ///
2107 /// 与 `execute_text` 相同,但在阻塞线程池中异步执行。
2108 pub async fn async_execute_text(self) -> Result<String> {
2109 let args = self.build_args(false);
2110 let exiftool = self.exiftool.clone();
2111 tokio::task::spawn_blocking(move || {
2112 let response = exiftool.execute_raw(&args)?;
2113 Ok(response.text().trim().to_string())
2114 })
2115 .await
2116 .map_err(|e| Error::process(format!("异步查询任务执行失败: {}", e)))?
2117 }
2118}
2119
2120/// 解析查询响应为 Metadata
2121///
2122/// 从 ExifTool 的 JSON 响应中解析出 Metadata 对象。
2123/// 此函数被同步 `execute` 和异步 `async_execute` 共用。
2124fn parse_query_response(response: Response) -> Result<Metadata> {
2125 if response.is_error() {
2126 return Err(Error::process(
2127 response
2128 .error_message()
2129 .unwrap_or_else(|| "Unknown error".to_string()),
2130 ));
2131 }
2132
2133 let json_value: Vec<serde_json::Value> = response.json()?;
2134
2135 if json_value.is_empty() {
2136 return Ok(Metadata::new());
2137 }
2138
2139 // 取第一个(也是唯一一个)结果
2140 let metadata: Metadata = serde_json::from_value(json_value[0].clone())?;
2141
2142 Ok(metadata)
2143}
2144
2145/// 批量查询构建器
2146pub struct BatchQueryBuilder<'et> {
2147 exiftool: &'et ExifTool,
2148 paths: Vec<PathBuf>,
2149 args: Vec<String>,
2150 include_unknown: bool,
2151 include_duplicates: bool,
2152 raw_values: bool,
2153 group_by_category: bool,
2154 specific_tags: Vec<String>,
2155}
2156
2157impl<'et> BatchQueryBuilder<'et> {
2158 /// 创建新的批量查询构建器
2159 pub(crate) fn new(exiftool: &'et ExifTool, paths: Vec<PathBuf>) -> Self {
2160 Self {
2161 exiftool,
2162 paths,
2163 args: Vec::new(),
2164 include_unknown: false,
2165 include_duplicates: false,
2166 raw_values: false,
2167 group_by_category: false,
2168 specific_tags: Vec::new(),
2169 }
2170 }
2171
2172 /// 包含未知标签
2173 pub fn include_unknown(mut self, yes: bool) -> Self {
2174 self.include_unknown = yes;
2175 self
2176 }
2177
2178 /// 包含重复标签
2179 pub fn include_duplicates(mut self, yes: bool) -> Self {
2180 self.include_duplicates = yes;
2181 self
2182 }
2183
2184 /// 显示原始数值
2185 pub fn raw_values(mut self, yes: bool) -> Self {
2186 self.raw_values = yes;
2187 self
2188 }
2189
2190 /// 按类别分组
2191 pub fn group_by_category(mut self, yes: bool) -> Self {
2192 self.group_by_category = yes;
2193 self
2194 }
2195
2196 /// 添加特定标签查询
2197 pub fn tag(mut self, tag: impl Into<String>) -> Self {
2198 self.specific_tags.push(tag.into());
2199 self
2200 }
2201
2202 /// 添加多个标签查询
2203 pub fn tags(mut self, tags: &[impl AsRef<str>]) -> Self {
2204 for tag in tags {
2205 self.specific_tags.push(tag.as_ref().to_string());
2206 }
2207 self
2208 }
2209
2210 /// 设置 GPS 坐标格式
2211 pub fn coord_format(mut self, format: impl Into<String>) -> Self {
2212 self.args.push("-c".to_string());
2213 self.args.push(format.into());
2214 self
2215 }
2216
2217 /// 设置日期/时间格式
2218 pub fn date_format(mut self, format: impl Into<String>) -> Self {
2219 self.args.push("-d".to_string());
2220 self.args.push(format.into());
2221 self
2222 }
2223
2224 /// 设置密码
2225 pub fn password(mut self, passwd: impl Into<String>) -> Self {
2226 self.args.push("-password".to_string());
2227 self.args.push(passwd.into());
2228 self
2229 }
2230
2231 /// 启用快速模式
2232 pub fn fast(mut self, level: Option<u8>) -> Self {
2233 match level {
2234 Some(1) => self.args.push("-fast".to_string()),
2235 Some(l) if l > 1 => self.args.push(format!("-fast{}", l)),
2236 _ => {}
2237 }
2238 self
2239 }
2240
2241 /// 强制扫描 XMP 数据
2242 pub fn scan_for_xmp(mut self, yes: bool) -> Self {
2243 if yes {
2244 self.args.push("-scanForXMP".to_string());
2245 }
2246 self
2247 }
2248
2249 /// 设置 API 选项
2250 pub fn api_option(mut self, opt: impl Into<String>, value: Option<impl Into<String>>) -> Self {
2251 let option = opt.into();
2252 self.args.push("-api".to_string());
2253 match value {
2254 Some(v) => self.args.push(format!("{}={}", option, v.into())),
2255 None => self.args.push(option),
2256 }
2257 self
2258 }
2259
2260 /// 设置用户参数
2261 pub fn user_param(
2262 mut self,
2263 param: impl Into<String>,
2264 value: Option<impl Into<String>>,
2265 ) -> Self {
2266 let param = param.into();
2267 self.args.push("-userParam".to_string());
2268 match value {
2269 Some(v) => self.args.push(format!("{}={}", param, v.into())),
2270 None => self.args.push(param),
2271 }
2272 self
2273 }
2274
2275 /// 设置自定义打印格式
2276 pub fn print_format(mut self, format: impl Into<String>) -> Self {
2277 self.args.push("-p".to_string());
2278 self.args.push(format.into());
2279 self
2280 }
2281
2282 /// 按字母顺序排序输出
2283 pub fn sort(mut self, yes: bool) -> Self {
2284 if yes {
2285 self.args.push("-sort".to_string());
2286 }
2287 self
2288 }
2289
2290 /// 设置列表项分隔符
2291 pub fn separator(mut self, sep: impl Into<String>) -> Self {
2292 self.args.push("-sep".to_string());
2293 self.args.push(sep.into());
2294 self
2295 }
2296
2297 /// 执行批量查询
2298 pub fn execute(self) -> Result<Vec<(PathBuf, Metadata)>> {
2299 if self.paths.is_empty() {
2300 return Ok(Vec::new());
2301 }
2302
2303 let args = self.build_args();
2304 let response = self.exiftool.execute_raw(&args)?;
2305
2306 // 解析 JSON 数组响应
2307 let json_values: Vec<serde_json::Value> = response.json()?;
2308
2309 let mut results = Vec::with_capacity(json_values.len());
2310
2311 for (i, json) in json_values.into_iter().enumerate() {
2312 let path = self.paths.get(i).cloned().unwrap_or_default();
2313 let metadata: Metadata = serde_json::from_value(json)?;
2314 results.push((path, metadata));
2315 }
2316
2317 Ok(results)
2318 }
2319
2320 /// 构建参数列表
2321 fn build_args(&self) -> Vec<String> {
2322 let mut args = vec!["-json".to_string()];
2323
2324 if self.group_by_category {
2325 args.push("-g1".to_string());
2326 }
2327
2328 if self.include_unknown {
2329 args.push("-u".to_string());
2330 }
2331
2332 if self.include_duplicates {
2333 args.push("-a".to_string());
2334 }
2335
2336 if self.raw_values {
2337 args.push("-n".to_string());
2338 }
2339
2340 args.extend(self.args.clone());
2341
2342 for tag in &self.specific_tags {
2343 args.push(format!("-{}", tag));
2344 }
2345
2346 // 添加所有文件路径
2347 for path in &self.paths {
2348 args.push(path.to_string_lossy().to_string());
2349 }
2350
2351 args
2352 }
2353}
2354
2355#[cfg(test)]
2356mod tests {
2357 use crate::Error;
2358
2359 #[test]
2360 fn test_response_warning_not_error() {
2361 let response = crate::Response::new(vec!["Warning: test".to_string()]);
2362 assert!(!response.is_error());
2363 }
2364
2365 #[test]
2366 fn test_query_builder_args() {
2367 let exiftool = match crate::ExifTool::new() {
2368 Ok(et) => et,
2369 Err(Error::ExifToolNotFound) => return,
2370 Err(e) => panic!("Unexpected error: {:?}", e),
2371 };
2372
2373 let args = exiftool
2374 .query("photo.jpg")
2375 .charset("utf8")
2376 .lang("zh")
2377 .coord_format("%.6f")
2378 .date_format("%Y-%m-%d")
2379 .print_format("$FileName")
2380 .separator(",")
2381 .api_option("QuickTimeUTC", Some("1"))
2382 .user_param("k", Some("v"))
2383 .password("p")
2384 .build_args(true);
2385
2386 assert!(args.windows(2).any(|w| w == ["-charset", "utf8"]));
2387 assert!(args.windows(2).any(|w| w == ["-lang", "zh"]));
2388 assert!(args.windows(2).any(|w| w == ["-c", "%.6f"]));
2389 assert!(args.windows(2).any(|w| w == ["-d", "%Y-%m-%d"]));
2390 assert!(args.windows(2).any(|w| w == ["-p", "$FileName"]));
2391 assert!(args.windows(2).any(|w| w == ["-sep", ","]));
2392 assert!(args.windows(2).any(|w| w == ["-api", "QuickTimeUTC=1"]));
2393 assert!(args.windows(2).any(|w| w == ["-userParam", "k=v"]));
2394 assert!(args.windows(2).any(|w| w == ["-password", "p"]));
2395 }
2396
2397 /// 测试 args_file 方法:验证 -@ 参数文件选项构建正确
2398 #[test]
2399 fn test_args_file() {
2400 let exiftool = match crate::ExifTool::new() {
2401 Ok(et) => et,
2402 Err(Error::ExifToolNotFound) => return,
2403 Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
2404 };
2405
2406 // 创建临时参数文件,写入 -FileName
2407 let tmp_dir = std::env::temp_dir();
2408 let args_path = tmp_dir.join("exiftool_test_args.txt");
2409 std::fs::write(&args_path, "-FileName\n").expect("写入临时参数文件失败");
2410
2411 let args = exiftool
2412 .query("photo.jpg")
2413 .args_file(args_path.to_string_lossy().as_ref())
2414 .build_args(false);
2415
2416 // 验证 args 包含 ["-@", "<路径>"]
2417 assert!(
2418 args.windows(2)
2419 .any(|w| w[0] == "-@" && w[1] == args_path.to_string_lossy().as_ref()),
2420 "参数列表应包含 -@ 和参数文件路径,实际: {:?}",
2421 args
2422 );
2423
2424 // 清理临时文件
2425 let _ = std::fs::remove_file(&args_path);
2426 }
2427
2428 /// 测试 csv_delimiter 方法:验证 -csvDelim 参数构建正确
2429 #[test]
2430 fn test_csv_delimiter() {
2431 let exiftool = match crate::ExifTool::new() {
2432 Ok(et) => et,
2433 Err(Error::ExifToolNotFound) => return,
2434 Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
2435 };
2436
2437 let args = exiftool
2438 .query("photo.jpg")
2439 .csv_delimiter(",")
2440 .build_args(false);
2441
2442 assert!(
2443 args.windows(2).any(|w| w == ["-csvDelim", ","]),
2444 "参数列表应包含 [\"-csvDelim\", \",\"],实际: {:?}",
2445 args
2446 );
2447 }
2448
2449 /// 测试 unknown_binary 方法:验证 -U 参数构建正确
2450 #[test]
2451 fn test_unknown_binary() {
2452 let exiftool = match crate::ExifTool::new() {
2453 Ok(et) => et,
2454 Err(Error::ExifToolNotFound) => return,
2455 Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
2456 };
2457
2458 let args = exiftool
2459 .query("photo.jpg")
2460 .unknown_binary()
2461 .build_args(false);
2462
2463 assert!(
2464 args.iter().any(|a| a == "-U"),
2465 "参数列表应包含 \"-U\",实际: {:?}",
2466 args
2467 );
2468 }
2469
2470 /// 测试 recursive_hidden 方法:验证 -r. 参数构建正确
2471 #[test]
2472 fn test_recursive_hidden() {
2473 let exiftool = match crate::ExifTool::new() {
2474 Ok(et) => et,
2475 Err(Error::ExifToolNotFound) => return,
2476 Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
2477 };
2478
2479 let args = exiftool
2480 .query("/photos")
2481 .recursive_hidden()
2482 .build_args(false);
2483
2484 assert!(
2485 args.iter().any(|a| a == "-r."),
2486 "参数列表应包含 \"-r.\",实际: {:?}",
2487 args
2488 );
2489 }
2490
2491 /// 测试 source_file 方法:验证 -srcfile 参数构建正确
2492 #[test]
2493 fn test_source_file() {
2494 let exiftool = match crate::ExifTool::new() {
2495 Ok(et) => et,
2496 Err(Error::ExifToolNotFound) => return,
2497 Err(e) => panic!("创建 ExifTool 实例时发生意外错误: {:?}", e),
2498 };
2499
2500 let args = exiftool
2501 .query("photo.jpg")
2502 .source_file("%d%f.xmp")
2503 .build_args(false);
2504
2505 assert!(
2506 args.windows(2).any(|w| w == ["-srcfile", "%d%f.xmp"]),
2507 "参数列表应包含 [\"-srcfile\", \"%d%f.xmp\"],实际: {:?}",
2508 args
2509 );
2510 }
2511}