Skip to main content

exiftool_rs_wrapper/
lib.rs

1//! ExifTool Rust Wrapper
2//!
3//! 一个高性能、类型安全的 ExifTool Rust 封装库。
4//!
5//! # 特性
6//!
7//! - **`-stay_open` 模式**:保持进程运行以获得最佳性能
8//! - **类型安全**:完整的标签类型系统
9//! - **Builder 模式**:符合 Rust 习惯的 API
10//! - **线程安全**:支持多线程并发访问
11//! - **零拷贝**:最小化内存分配
12//!
13//! # 示例
14//!
15//! ```rust,no_run
16//! use exiftool_rs_wrapper::ExifTool;
17//!
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! // 创建 ExifTool 实例
20//! let exiftool = ExifTool::new()?;
21//!
22//! // 读取元数据
23//! let metadata = exiftool.query("photo.jpg").execute()?;
24//! println!("相机制造商: {:?}", metadata.get("Make"));
25//!
26//! // 写入元数据
27//! exiftool.write("photo.jpg")
28//!     .tag("Copyright", "© 2026")
29//!     .overwrite_original(true)
30//!     .execute()?;
31//! # Ok(())
32//! # }
33//! ```
34
35// 模块声明
36mod binary;
37mod config;
38mod error;
39mod file_ops;
40mod format;
41mod geo;
42mod pool;
43mod process;
44mod query;
45mod retry;
46mod stream;
47pub mod tags;
48mod types;
49mod write;
50
51/// Serde 结构体模块
52#[cfg(feature = "serde-structs")]
53pub mod structs;
54
55/// 高级功能模块
56mod advanced;
57
58/// 异步 API 模块
59#[cfg(feature = "async")]
60pub mod async_ext;
61
62// 公开导出
63pub use advanced::{
64    AdvancedWriteOperations, DateShiftDirection, DateTimeOffset, NumericOperation, TimeUnit,
65};
66pub use binary::{BinaryOperations, BinaryTag, BinaryWriteBuilder, BinaryWriteResult};
67pub use error::{Error, Result};
68pub use process::{CommandId, CommandRequest, Response};
69pub use query::{BatchQueryBuilder, EscapeFormat, QueryBuilder};
70pub use types::{Metadata, TagId, TagValue};
71pub use write::{WriteBuilder, WriteMode, WriteResult};
72
73// 连接池
74pub use pool::{ExifToolPool, PoolConnection, batch_with_pool, with_pool};
75
76// 格式化输出
77pub use format::{FormatOperations, FormattedOutput, OutputFormat, ReadOptions};
78
79// 文件操作
80pub use file_ops::{FileOperations, OrganizeOptions, RenamePattern};
81
82// 地理信息
83pub use geo::{GeoOperations, GpsCoordinate};
84
85// 配置和校验
86pub use config::{
87    ConfigOperations, DiffResult, HexDumpOperations, HexDumpOptions, VerboseOperations,
88    VerboseOptions,
89};
90
91// 流式处理和性能优化
92pub use stream::{
93    Cache, PerformanceStats, ProgressCallback, ProgressReader, ProgressTracker, StreamOptions,
94    StreamingOperations,
95};
96
97#[cfg(feature = "async")]
98/// 异步流式处理模块(需要 async feature)
99pub use stream::async_stream;
100
101// 错误恢复和重试
102pub use retry::{BatchResult, Recoverable, RetryPolicy, with_retry_sync};
103
104#[cfg(feature = "async")]
105pub use retry::with_retry;
106
107#[cfg(feature = "async")]
108pub use async_ext::{AsyncExifTool, process_files_parallel, read_metadata_parallel};
109
110use process::ExifToolInner;
111use std::path::{Path, PathBuf};
112use std::sync::{Arc, Mutex};
113use std::time::Duration;
114use tracing::{debug, info};
115
116/// ExifTool 主结构体
117///
118/// 使用 `-stay_open` 模式保持 ExifTool 进程运行,
119/// 避免每次操作都重新启动进程的开销。
120///
121/// # 线程安全
122///
123/// `ExifTool` 是线程安全的,可以在多个线程间共享。
124/// 内部使用 `Arc<Mutex>` 保护进程通信。
125#[derive(Debug, Clone)]
126pub struct ExifTool {
127    inner: Arc<Mutex<ExifToolInner>>,
128    global_args: Arc<Vec<String>>,
129}
130
131/// ExifTool 能力快照
132#[derive(Debug, Clone)]
133pub struct CapabilitySnapshot {
134    pub version: String,
135    pub tags_count: usize,
136    pub writable_tags_count: usize,
137    pub file_extensions_count: usize,
138    pub writable_file_extensions_count: usize,
139    pub groups_count: usize,
140    pub descriptions_count: usize,
141}
142
143/// ExifTool 构建器
144pub struct ExifToolBuilder {
145    executable: Option<std::path::PathBuf>,
146    response_timeout: Option<Duration>,
147    config_path: Option<std::path::PathBuf>,
148}
149
150impl ExifToolBuilder {
151    /// 创建新的构建器
152    pub fn new() -> Self {
153        Self {
154            executable: None,
155            response_timeout: None,
156            config_path: None,
157        }
158    }
159
160    /// 指定 exiftool 可执行文件路径
161    pub fn executable<P: Into<std::path::PathBuf>>(mut self, path: P) -> Self {
162        self.executable = Some(path.into());
163        self
164    }
165
166    /// 设置响应超时
167    pub fn response_timeout(mut self, timeout: Duration) -> Self {
168        self.response_timeout = Some(timeout);
169        self
170    }
171
172    /// 指定 ExifTool 配置文件路径(等价于 `-config`)
173    pub fn config<P: Into<std::path::PathBuf>>(mut self, path: P) -> Self {
174        self.config_path = Some(path.into());
175        self
176    }
177
178    /// 构建 ExifTool 实例
179    pub fn build(self) -> Result<ExifTool> {
180        let timeout = self
181            .response_timeout
182            .unwrap_or_else(|| Duration::from_secs(30));
183
184        let inner = if let Some(exe) = self.executable {
185            ExifToolInner::with_executable_and_timeout(exe, timeout)?
186        } else {
187            ExifToolInner::with_executable_and_timeout("exiftool", timeout)?
188        };
189
190        let mut global_args = Vec::new();
191        if let Some(config_path) = self.config_path {
192            global_args.push("-config".to_string());
193            global_args.push(config_path.to_string_lossy().to_string());
194        }
195
196        Ok(ExifTool {
197            inner: Arc::new(Mutex::new(inner)),
198            global_args: Arc::new(global_args),
199        })
200    }
201}
202
203impl Default for ExifToolBuilder {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl ExifTool {
210    /// 创建新的 ExifTool 实例
211    ///
212    /// 启动一个 `-stay_open` 模式的 ExifTool 进程。
213    ///
214    /// # 错误
215    ///
216    /// 如果 ExifTool 未安装或无法启动,返回 `Error::ExifToolNotFound`。
217    ///
218    /// # 示例
219    ///
220    /// ```rust,no_run
221    /// use exiftool_rs_wrapper::ExifTool;
222    ///
223    /// let exiftool = ExifTool::new()?;
224    /// # Ok::<(), exiftool_rs_wrapper::Error>(())
225    /// ```
226    pub fn new() -> Result<Self> {
227        info!("Creating new ExifTool instance");
228
229        let inner = ExifToolInner::new()?;
230
231        Ok(Self {
232            inner: Arc::new(Mutex::new(inner)),
233            global_args: Arc::new(Vec::new()),
234        })
235    }
236
237    /// 创建 ExifTool 构建器
238    ///
239    /// 使用 Builder 模式可以更灵活地配置 ExifTool 实例。
240    ///
241    /// # 示例
242    ///
243    /// ```rust,no_run
244    /// use exiftool_rs_wrapper::ExifTool;
245    ///
246    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
247    /// // 使用自定义路径
248    /// let exiftool = ExifTool::builder()
249    ///     .executable("/usr/local/bin/exiftool")
250    ///     .build()?;
251    /// # Ok(())
252    /// # }
253    /// ```
254    pub fn builder() -> ExifToolBuilder {
255        ExifToolBuilder::new()
256    }
257
258    /// 基于当前实例附加配置文件参数(`-config`)
259    pub fn with_config<P: AsRef<Path>>(&self, path: P) -> Self {
260        let mut global_args = self.global_args.as_ref().clone();
261        global_args.push("-config".to_string());
262        global_args.push(path.as_ref().to_string_lossy().to_string());
263
264        Self {
265            inner: Arc::clone(&self.inner),
266            global_args: Arc::new(global_args),
267        }
268    }
269
270    /// 查询单个文件的元数据
271    ///
272    /// 返回一个 `QueryBuilder`,可以使用 Builder 模式配置查询选项。
273    ///
274    /// # 示例
275    ///
276    /// ```rust,no_run
277    /// use exiftool_rs_wrapper::ExifTool;
278    ///
279    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
280    /// let exiftool = ExifTool::new()?;
281    ///
282    /// // 基本查询
283    /// let metadata = exiftool.query("photo.jpg").execute()?;
284    ///
285    /// // 高级查询
286    /// let metadata = exiftool.query("photo.jpg")
287    ///     .include_unknown(true)
288    ///     .tag("Make")
289    ///     .tag("Model")
290    ///     .execute()?;
291    /// # Ok(())
292    /// # }
293    /// ```
294    pub fn query<P: AsRef<Path>>(&self, path: P) -> QueryBuilder<'_> {
295        QueryBuilder::new(self, path)
296    }
297
298    /// 批量查询多个文件的元数据
299    ///
300    /// # 示例
301    ///
302    /// ```rust,no_run
303    /// use exiftool_rs_wrapper::ExifTool;
304    ///
305    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
306    /// let exiftool = ExifTool::new()?;
307    ///
308    /// let paths = vec!["photo1.jpg", "photo2.jpg", "photo3.jpg"];
309    /// let results = exiftool.query_batch(&paths)
310    ///     .tag("FileName")
311    ///     .tag("ImageSize")
312    ///     .execute()?;
313    ///
314    /// for (path, metadata) in results {
315    ///     println!("{}: {:?}", path.display(), metadata.get("FileName"));
316    /// }
317    /// # Ok(())
318    /// # }
319    /// ```
320    pub fn query_batch<P: AsRef<Path>>(&self, paths: &[P]) -> BatchQueryBuilder<'_> {
321        let path_bufs: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
322        BatchQueryBuilder::new(self, path_bufs)
323    }
324
325    /// 写入元数据到文件
326    ///
327    /// 返回一个 `WriteBuilder`,可以使用 Builder 模式配置写入选项。
328    ///
329    /// # 警告
330    ///
331    /// 默认情况下,ExifTool 会创建备份文件(`filename_original`)。
332    /// 使用 `overwrite_original(true)` 可以不创建备份直接覆盖原文件。
333    ///
334    /// # 示例
335    ///
336    /// ```rust,no_run
337    /// use exiftool_rs_wrapper::ExifTool;
338    ///
339    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
340    /// let exiftool = ExifTool::new()?;
341    ///
342    /// // 基本写入
343    /// exiftool.write("photo.jpg")
344    ///     .tag("Copyright", "© 2026 My Company")
345    ///     .execute()?;
346    ///
347    /// // 高级写入
348    /// exiftool.write("photo.jpg")
349    ///     .tag("Artist", "Photographer")
350    ///     .tag("Copyright", "© 2026")
351    ///     .delete("Comment")
352    ///     .overwrite_original(true)
353    ///     .execute()?;
354    /// # Ok(())
355    /// # }
356    /// ```
357    pub fn write<P: AsRef<Path>>(&self, path: P) -> WriteBuilder<'_> {
358        WriteBuilder::new(self, path)
359    }
360
361    /// 读取单个标签的值
362    ///
363    /// 这是 `query().tag().execute()` 的快捷方式。
364    ///
365    /// # 示例
366    ///
367    /// ```rust,no_run
368    /// use exiftool_rs_wrapper::ExifTool;
369    ///
370    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
371    /// let exiftool = ExifTool::new()?;
372    ///
373    /// let make: String = exiftool.read_tag("photo.jpg", "Make")?;
374    /// println!("相机制造商: {}", make);
375    ///
376    /// // 使用 TagId
377    /// use exiftool_rs_wrapper::TagId;
378    /// let model: String = exiftool.read_tag("photo.jpg", "Model")?;
379    /// # Ok(())
380    /// # }
381    /// ```
382    pub fn read_tag<T, P, S>(&self, path: P, tag: S) -> Result<T>
383    where
384        T: for<'de> serde::Deserialize<'de>,
385        P: AsRef<Path>,
386        S: AsRef<str>,
387    {
388        let metadata = self.query(path).tag(tag.as_ref()).execute()?;
389
390        let value = metadata
391            .get(tag.as_ref())
392            .ok_or_else(|| Error::TagNotFound(tag.as_ref().to_string()))?;
393
394        // 将 TagValue 转换为目标类型
395        let json = serde_json::to_value(value)?;
396        let result: T = serde_json::from_value(json)?;
397
398        Ok(result)
399    }
400
401    /// 读取文件元数据并反序列化为结构体
402    ///
403    /// 需要启用 `serde-structs` feature。
404    ///
405    /// # 示例
406    ///
407    /// ```rust,no_run
408    /// use exiftool_rs_wrapper::ExifTool;
409    /// use exiftool_rs_wrapper::structs::Metadata;
410    ///
411    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
412    /// let exiftool = ExifTool::new()?;
413    /// let meta: Metadata = exiftool.read_struct("photo.jpg")?;
414    /// println!("File: {}", meta.file.file_name);
415    /// # Ok(())
416    /// # }
417    /// ```
418    #[cfg(feature = "serde-structs")]
419    pub fn read_struct<T, P>(&self, path: P) -> Result<T>
420    where
421        T: for<'de> serde::Deserialize<'de>,
422        P: AsRef<Path>,
423    {
424        // 获取 JSON 格式的原始输出
425        let output = self.query(path).arg("-json").arg("-g2").execute_text()?;
426
427        // ExifTool 的 JSON 输出是数组,需要提取第一个元素
428        let json_array: Vec<serde_json::Value> = serde_json::from_str(&output)?;
429        if json_array.is_empty() {
430            return Err(Error::process("Empty JSON response from ExifTool"));
431        }
432
433        let result: T = serde_json::from_value(json_array[0].clone())?;
434        Ok(result)
435    }
436
437    /// 获取 ExifTool 版本
438    ///
439    /// # 示例
440    ///
441    /// ```rust,no_run
442    /// use exiftool_rs_wrapper::ExifTool;
443    ///
444    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
445    /// let exiftool = ExifTool::new()?;
446    /// let version = exiftool.version()?;
447    /// println!("ExifTool version: {}", version);
448    /// # Ok(())
449    /// # }
450    /// ```
451    pub fn version(&self) -> Result<String> {
452        let mut inner = self.inner.lock()?;
453        inner.send_line("-ver")?;
454        inner.send_line("-execute")?;
455        inner.flush()?;
456
457        let response = inner.read_response()?;
458        Ok(response.text().trim().to_string())
459    }
460
461    /// 获取所有支持的标签列表
462    pub fn list_tags(&self) -> Result<Vec<String>> {
463        let mut inner = self.inner.lock()?;
464        inner.send_line("-list")?;
465        inner.send_line("-execute")?;
466        inner.flush()?;
467
468        let response = inner.read_response()?;
469        let tags: Vec<String> = response
470            .lines()
471            .iter()
472            .map(|line| line.trim().to_string())
473            .filter(|line| {
474                !line.is_empty() && !line.starts_with('-') && !line.contains("Command-line")
475            })
476            .collect();
477
478        Ok(tags)
479    }
480
481    /// 获取可写标签列表(对应 `-listw`)
482    pub fn list_writable_tags(&self) -> Result<Vec<String>> {
483        let response = self.execute(&["-listw"])?;
484        Ok(parse_word_list(response.text()))
485    }
486
487    /// 获取支持的文件扩展名列表(对应 `-listf`)
488    pub fn list_file_extensions(&self) -> Result<Vec<String>> {
489        let response = self.execute(&["-listf"])?;
490        Ok(parse_word_list(response.text()))
491    }
492
493    /// 获取支持的分组列表(对应 `-listg`)
494    pub fn list_groups(&self) -> Result<Vec<String>> {
495        let response = self.execute(&["-listg"])?;
496        Ok(parse_word_list(response.text()))
497    }
498
499    /// 获取指定族的分组列表(对应 `-listg[NUM]`)
500    ///
501    /// ExifTool 将标签分组为不同的族(family),使用 `-listg[NUM]` 可以列出特定族的分组。
502    ///
503    /// # 参数
504    ///
505    /// - `family` - 分组族编号(0-7),对应 `-listg0` 到 `-listg7`
506    ///   - Family 0: 信息类型(EXIF、IPTC、XMP 等)
507    ///   - Family 1: 特定位置(MakerNotes、Composite 等)
508    ///   - Family 2: 类别(Image、Camera、Author 等)
509    ///   - Family 3-7: 其他分类方式
510    ///
511    /// # 示例
512    ///
513    /// ```rust,no_run
514    /// use exiftool_rs_wrapper::ExifTool;
515    ///
516    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
517    /// let exiftool = ExifTool::new()?;
518    ///
519    /// // 获取 Family 1 的分组列表(特定位置)
520    /// let family1_groups = exiftool.list_groups_family(1)?;
521    /// println!("Family 1 分组: {:?}", family1_groups);
522    ///
523    /// // 获取 Family 2 的分组列表(类别)
524    /// let family2_groups = exiftool.list_groups_family(2)?;
525    /// println!("Family 2 分组: {:?}", family2_groups);
526    /// # Ok(())
527    /// # }
528    /// ```
529    pub fn list_groups_family(&self, family: u8) -> Result<Vec<String>> {
530        let args = if family == 0 {
531            vec!["-listg".to_string()]
532        } else {
533            vec![format!("-listg{}", family)]
534        };
535        let response = self.execute(&args)?;
536        Ok(parse_word_list(response.text()))
537    }
538
539    /// 获取标签描述列表(对应 `-listd`)
540    pub fn list_descriptions(&self) -> Result<Vec<String>> {
541        let response = self.execute(&["-listd"])?;
542        Ok(parse_word_list(response.text()))
543    }
544
545    /// 获取可写文件类型扩展名列表(对应 `-listwf`)
546    ///
547    /// 返回 ExifTool 支持写入的文件类型扩展名列表。
548    /// 与 `list_file_extensions()` 不同,此方法仅返回可写入元数据的文件类型。
549    ///
550    /// # 示例
551    ///
552    /// ```rust,no_run
553    /// use exiftool_rs_wrapper::ExifTool;
554    ///
555    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
556    /// let exiftool = ExifTool::new()?;
557    ///
558    /// let writable_exts = exiftool.list_writable_file_extensions()?;
559    /// println!("可写文件类型数量: {}", writable_exts.len());
560    /// # Ok(())
561    /// # }
562    /// ```
563    pub fn list_writable_file_extensions(&self) -> Result<Vec<String>> {
564        let response = self.execute(&["-listwf"])?;
565        Ok(parse_word_list(response.text()))
566    }
567
568    /// 获取可读文件类型扩展名列表(对应 `-listr`)
569    ///
570    /// 返回 ExifTool 支持读取的文件类型扩展名列表。
571    ///
572    /// # 示例
573    ///
574    /// ```rust,no_run
575    /// use exiftool_rs_wrapper::ExifTool;
576    ///
577    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
578    /// let exiftool = ExifTool::new()?;
579    ///
580    /// let readable_exts = exiftool.list_readable_file_extensions()?;
581    /// println!("可读文件类型数量: {}", readable_exts.len());
582    /// # Ok(())
583    /// # }
584    /// ```
585    pub fn list_readable_file_extensions(&self) -> Result<Vec<String>> {
586        let response = self.execute(&["-listr"])?;
587        Ok(parse_word_list(response.text()))
588    }
589
590    /// 获取支持的 GPS 日志格式列表(对应 `-listgeo`)
591    ///
592    /// 返回 ExifTool 支持的地理标记日志文件格式列表。
593    ///
594    /// # 示例
595    ///
596    /// ```rust,no_run
597    /// use exiftool_rs_wrapper::ExifTool;
598    ///
599    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
600    /// let exiftool = ExifTool::new()?;
601    ///
602    /// let geo_formats = exiftool.list_geo_formats()?;
603    /// println!("支持的 GPS 日志格式: {:?}", geo_formats);
604    /// # Ok(())
605    /// # }
606    /// ```
607    pub fn list_geo_formats(&self) -> Result<Vec<String>> {
608        let response = self.execute(&["-listgeo"])?;
609        Ok(parse_word_list(response.text()))
610    }
611
612    /// 生成当前 ExifTool 的能力快照
613    pub fn capability_snapshot(&self) -> Result<CapabilitySnapshot> {
614        Ok(CapabilitySnapshot {
615            version: self.version()?,
616            tags_count: self.list_tags()?.len(),
617            writable_tags_count: self.list_writable_tags()?.len(),
618            file_extensions_count: self.list_file_extensions()?.len(),
619            writable_file_extensions_count: self.list_writable_file_extensions()?.len(),
620            groups_count: self.list_groups()?.len(),
621            descriptions_count: self.list_descriptions()?.len(),
622        })
623    }
624
625    /// 执行原始命令
626    ///
627    /// 这是高级 API,允许直接发送参数到 ExifTool。
628    ///
629    /// # 安全性
630    ///
631    /// 谨慎使用此功能,确保参数不包含恶意输入。
632    pub fn execute<S: AsRef<str>>(&self, args: &[S]) -> Result<Response> {
633        self.execute_raw(args)
634    }
635
636    pub(crate) fn execute_raw(&self, args: &[impl AsRef<str>]) -> Result<Response> {
637        debug!("Executing raw command with {} args", args.len());
638
639        let mut merged_args = self.global_args.as_ref().clone();
640        merged_args.extend(args.iter().map(|a| a.as_ref().to_string()));
641
642        let mut inner = self.inner.lock()?;
643        inner.execute(&merged_args)
644    }
645
646    /// 批量执行多个命令(原子多命令)
647    ///
648    /// 使用 `-executeNUM` 格式在一个事务中发送多个命令,
649    /// 通过编号区分各个命令的响应。
650    ///
651    /// # 优势
652    ///
653    /// - 减少进程间通信开销(一次性发送所有命令)
654    /// - 原子性:所有命令在一个批次中执行
655    /// - 支持复杂的读写链操作
656    ///
657    /// # 示例
658    ///
659    /// ```rust,no_run
660    /// use exiftool_rs_wrapper::ExifTool;
661    ///
662    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
663    /// let exiftool = ExifTool::new()?;
664    ///
665    /// // 批量执行多个命令
666    /// let commands = vec![
667    ///     vec!["-ver".to_string()],
668    ///     vec!["-list".to_string()],
669    /// ];
670    /// let responses = exiftool.execute_multiple(&commands)?;
671    ///
672    /// for (idx, response) in responses.iter().enumerate() {
673    ///     println!("命令 {}: {}", idx, response.text().trim());
674    /// }
675    /// # Ok(())
676    /// # }
677    /// ```
678    pub fn execute_multiple<S: AsRef<str>>(&self, commands: &[Vec<S>]) -> Result<Vec<Response>> {
679        debug!("Executing {} commands atomically", commands.len());
680
681        // 转换参数格式
682        let converted_commands: Vec<Vec<String>> = commands
683            .iter()
684            .map(|cmd| cmd.iter().map(|a| a.as_ref().to_string()).collect())
685            .collect();
686
687        // 合并全局参数到每个命令
688        let commands_with_global: Vec<Vec<String>> = converted_commands
689            .iter()
690            .map(|cmd| {
691                let mut merged = self.global_args.as_ref().clone();
692                merged.extend(cmd.iter().cloned());
693                merged
694            })
695            .collect();
696
697        let mut inner = self.inner.lock()?;
698        inner.execute_multiple(&commands_with_global)
699    }
700
701    /// 关闭 ExifTool 进程
702    ///
703    /// 优雅地关闭进程。通常不需要手动调用,
704    /// 因为 `Drop` 实现会自动处理。
705    pub fn close(&self) -> Result<()> {
706        let mut inner = self.inner.lock()?;
707        inner.close()
708    }
709
710    /// 删除备份文件
711    ///
712    /// 使用 `-delete_original` 选项删除 `_original` 备份文件。
713    ///
714    /// # 参数
715    ///
716    /// * `path` - 原始文件路径(ExifTool 会找到对应的备份文件)
717    /// * `force` - 是否强制删除(使用 `-delete_original!`)
718    ///
719    /// # 示例
720    ///
721    /// ```rust,no_run
722    /// use exiftool_rs_wrapper::ExifTool;
723    ///
724    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
725    /// let exiftool = ExifTool::new()?;
726    ///
727    /// // 删除 photo.jpg 的备份文件 photo.jpg_original
728    /// exiftool.delete_original("photo.jpg", false)?;
729    ///
730    /// // 强制删除备份文件
731    /// exiftool.delete_original("photo.jpg", true)?;
732    /// # Ok(())
733    /// # }
734    /// ```
735    pub fn delete_original<P: AsRef<Path>>(&self, path: P, force: bool) -> Result<()> {
736        let arg = if force {
737            "-delete_original!"
738        } else {
739            "-delete_original"
740        };
741        let args = vec![arg.to_string(), path.as_ref().to_string_lossy().to_string()];
742        self.execute_raw(&args)?;
743        Ok(())
744    }
745
746    /// 从备份恢复原始文件
747    ///
748    /// 使用 `-restore_original` 选项从 `_original` 备份文件恢复原始文件。
749    ///
750    /// # 参数
751    ///
752    /// * `path` - 文件路径
753    ///
754    /// # 示例
755    ///
756    /// ```rust,no_run
757    /// use exiftool_rs_wrapper::ExifTool;
758    ///
759    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
760    /// let exiftool = ExifTool::new()?;
761    ///
762    /// // 从 photo.jpg_original 恢复 photo.jpg
763    /// exiftool.restore_original("photo.jpg")?;
764    /// # Ok(())
765    /// # }
766    /// ```
767    pub fn restore_original<P: AsRef<Path>>(&self, path: P) -> Result<()> {
768        let args = vec![
769            "-restore_original".to_string(),
770            path.as_ref().to_string_lossy().to_string(),
771        ];
772        self.execute_raw(&args)?;
773        Ok(())
774    }
775
776    #[cfg(test)]
777    pub(crate) fn debug_global_args(&self) -> Vec<String> {
778        self.global_args.as_ref().clone()
779    }
780}
781
782fn parse_word_list(text: String) -> Vec<String> {
783    text.split_whitespace()
784        .filter(|s| !s.is_empty())
785        .map(|s| s.trim().to_string())
786        .collect()
787}
788
789#[cfg(test)]
790pub(crate) mod tests {
791    use super::*;
792
793    /// 最小有效 JPEG 文件数据,供测试使用
794    #[cfg(feature = "async")]
795    pub(crate) fn tiny_jpeg() -> &'static [u8] {
796        &[
797            0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00,
798            0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06,
799            0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D,
800            0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D,
801            0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28,
802            0x37, 0x29, 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
803            0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01,
804            0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
805            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00,
806            0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
807            0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00,
808            0xD2, 0xCF, 0x20, 0xFF, 0xD9,
809        ]
810    }
811
812    #[test]
813    fn test_exiftool_new() {
814        // 仅在 ExifTool 可用时运行
815        match ExifTool::new() {
816            Ok(_) => {
817                println!("✓ ExifTool is available");
818            }
819            Err(Error::ExifToolNotFound) => {
820                println!("⚠ ExifTool not found, skipping test");
821            }
822            Err(e) => panic!("Unexpected error: {:?}", e),
823        }
824    }
825
826    #[test]
827    fn test_version() {
828        match ExifTool::new() {
829            Ok(et) => {
830                let version = et.version().unwrap();
831                assert!(!version.is_empty());
832                println!("ExifTool version: {}", version);
833            }
834            Err(Error::ExifToolNotFound) => {
835                println!("⚠ ExifTool not found, skipping test");
836            }
837            Err(e) => panic!("Unexpected error: {:?}", e),
838        }
839    }
840
841    #[test]
842    fn test_builder_with_config_args() {
843        let et = match ExifTool::builder()
844            .config("/tmp/exiftool-test.config")
845            .build()
846        {
847            Ok(et) => et,
848            Err(Error::ExifToolNotFound) => return,
849            Err(e) => panic!("Unexpected error: {:?}", e),
850        };
851
852        let args = et.debug_global_args();
853        assert_eq!(args, vec!["-config", "/tmp/exiftool-test.config"]);
854    }
855
856    #[test]
857    fn test_with_config_clone_args() {
858        let et = match ExifTool::new() {
859            Ok(et) => et,
860            Err(Error::ExifToolNotFound) => return,
861            Err(e) => panic!("Unexpected error: {:?}", e),
862        };
863
864        let configured = et.with_config("/tmp/exiftool-test.config");
865        let args = configured.debug_global_args();
866        assert_eq!(args, vec!["-config", "/tmp/exiftool-test.config"]);
867    }
868
869    #[test]
870    fn test_public_execute_raw_passthrough() {
871        let et = match ExifTool::new() {
872            Ok(et) => et,
873            Err(Error::ExifToolNotFound) => return,
874            Err(e) => panic!("Unexpected error: {:?}", e),
875        };
876
877        let response = et.execute(&["-ver"]).expect("execute should succeed");
878        let version = response.text().trim().to_string();
879        assert!(!version.is_empty());
880    }
881
882    #[test]
883    fn test_list_writable_tags() {
884        let et = match ExifTool::new() {
885            Ok(et) => et,
886            Err(Error::ExifToolNotFound) => return,
887            Err(e) => panic!("Unexpected error: {:?}", e),
888        };
889
890        let tags = et
891            .list_writable_tags()
892            .expect("list writable tags should succeed");
893        assert!(!tags.is_empty());
894    }
895
896    #[test]
897    fn test_list_file_extensions() {
898        let et = match ExifTool::new() {
899            Ok(et) => et,
900            Err(Error::ExifToolNotFound) => return,
901            Err(e) => panic!("Unexpected error: {:?}", e),
902        };
903
904        let exts = et
905            .list_file_extensions()
906            .expect("list file extensions should succeed");
907        assert!(!exts.is_empty());
908    }
909
910    #[test]
911    fn test_list_groups() {
912        let et = match ExifTool::new() {
913            Ok(et) => et,
914            Err(Error::ExifToolNotFound) => return,
915            Err(e) => panic!("Unexpected error: {:?}", e),
916        };
917
918        let groups = et.list_groups().expect("list groups should succeed");
919        assert!(!groups.is_empty());
920    }
921
922    #[test]
923    fn test_list_descriptions() {
924        let et = match ExifTool::new() {
925            Ok(et) => et,
926            Err(Error::ExifToolNotFound) => return,
927            Err(e) => panic!("Unexpected error: {:?}", e),
928        };
929
930        let desc = et
931            .list_descriptions()
932            .expect("list descriptions should succeed");
933        assert!(!desc.is_empty());
934    }
935
936    #[test]
937    fn test_capability_snapshot() {
938        let et = match ExifTool::new() {
939            Ok(et) => et,
940            Err(Error::ExifToolNotFound) => return,
941            Err(e) => panic!("Unexpected error: {:?}", e),
942        };
943
944        let snapshot = et
945            .capability_snapshot()
946            .expect("capability snapshot should succeed");
947        assert!(!snapshot.version.is_empty());
948        assert!(snapshot.tags_count > 0);
949        assert!(snapshot.writable_tags_count > 0);
950        assert!(snapshot.file_extensions_count > 0);
951        assert!(snapshot.writable_file_extensions_count > 0);
952        assert!(snapshot.groups_count > 0);
953        assert!(snapshot.descriptions_count > 0);
954    }
955}