Skip to main content

exiftool_rs_wrapper/
format.rs

1//! 输出格式支持模块
2
3use crate::ExifTool;
4use crate::error::Result;
5use std::path::Path;
6
7/// 输出格式枚举
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum OutputFormat {
10    /// JSON 格式(默认)
11    #[default]
12    Json,
13    /// XML 格式
14    Xml,
15    /// CSV 格式
16    Csv,
17    /// TSV 格式
18    Tsv,
19    /// HTML 表格格式
20    Html,
21    /// 纯文本格式
22    Text,
23    /// 结构化数据
24    Struct,
25}
26
27impl OutputFormat {
28    /// 获取格式对应的参数
29    fn arg(&self) -> &'static str {
30        match self {
31            Self::Json => "-json",
32            Self::Xml => "-X", // 或 -xml
33            Self::Csv => "-csv",
34            Self::Tsv => "-t",  // 或 -tab
35            Self::Html => "-h", // 或 -html
36            Self::Text => "-s", // 短格式
37            Self::Struct => "-struct",
38        }
39    }
40}
41
42/// 高级读取选项
43#[derive(Debug, Clone, Default)]
44pub struct ReadOptions {
45    /// 输出格式
46    pub format: OutputFormat,
47    /// 排除特定标签
48    pub exclude_tags: Vec<String>,
49    /// 仅显示特定标签
50    pub specific_tags: Vec<String>,
51    /// 表格格式输出
52    pub table_format: bool,
53    /// 十六进制转储
54    pub hex_dump: bool,
55    /// 详细级别 (0-5)
56    pub verbose: Option<u8>,
57    /// 语言设置
58    pub lang: Option<String>,
59    /// 字符集
60    pub charset: Option<String>,
61    /// 显示原始数值
62    pub raw_values: bool,
63    /// 递归处理目录
64    pub recursive: bool,
65    /// 文件扩展名过滤
66    pub extensions: Vec<String>,
67    /// 条件过滤
68    pub condition: Option<String>,
69}
70
71impl ReadOptions {
72    /// 创建新的读取选项
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// 设置输出格式
78    pub fn format(mut self, format: OutputFormat) -> Self {
79        self.format = format;
80        self
81    }
82
83    /// 排除标签
84    pub fn exclude(mut self, tag: impl Into<String>) -> Self {
85        self.exclude_tags.push(tag.into());
86        self
87    }
88
89    /// 仅显示特定标签
90    pub fn tag(mut self, tag: impl Into<String>) -> Self {
91        self.specific_tags.push(tag.into());
92        self
93    }
94
95    /// 表格格式输出
96    pub fn table(mut self, yes: bool) -> Self {
97        self.table_format = yes;
98        self
99    }
100
101    /// 十六进制转储
102    pub fn hex(mut self, yes: bool) -> Self {
103        self.hex_dump = yes;
104        self
105    }
106
107    /// 设置详细级别
108    pub fn verbose(mut self, level: u8) -> Self {
109        self.verbose = Some(level.min(5));
110        self
111    }
112
113    /// 设置语言
114    pub fn lang(mut self, lang: impl Into<String>) -> Self {
115        self.lang = Some(lang.into());
116        self
117    }
118
119    /// 设置字符集
120    pub fn charset(mut self, charset: impl Into<String>) -> Self {
121        self.charset = Some(charset.into());
122        self
123    }
124
125    /// 显示原始数值
126    pub fn raw(mut self, yes: bool) -> Self {
127        self.raw_values = yes;
128        self
129    }
130
131    /// 递归处理
132    pub fn recursive(mut self, yes: bool) -> Self {
133        self.recursive = yes;
134        self
135    }
136
137    /// 添加文件扩展名过滤
138    pub fn extension(mut self, ext: impl Into<String>) -> Self {
139        self.extensions.push(ext.into());
140        self
141    }
142
143    /// 条件过滤
144    pub fn condition(mut self, expr: impl Into<String>) -> Self {
145        self.condition = Some(expr.into());
146        self
147    }
148
149    /// 构建参数列表
150    pub(crate) fn build_args(&self, paths: &[impl AsRef<Path>]) -> Vec<String> {
151        let mut args = vec![self.format.arg().to_string()];
152
153        // 表格格式
154        if self.table_format {
155            args.push("-T".to_string());
156        }
157
158        // 十六进制转储
159        if self.hex_dump {
160            args.push("-H".to_string());
161        }
162
163        // 详细级别
164        if let Some(level) = self.verbose {
165            args.push(format!("-v{}", level));
166        }
167
168        // 语言
169        if let Some(ref lang) = self.lang {
170            args.push(format!("-lang {}", lang));
171        }
172
173        // 字符集
174        if let Some(ref charset) = self.charset {
175            args.push(format!("-charset {}", charset));
176        }
177
178        // 原始数值
179        if self.raw_values {
180            args.push("-n".to_string());
181        }
182
183        // 递归
184        if self.recursive {
185            args.push("-r".to_string());
186        }
187
188        // 文件扩展名过滤
189        for ext in &self.extensions {
190            args.push(format!("-ext {}", ext));
191        }
192
193        // 条件过滤
194        if let Some(ref condition) = self.condition {
195            args.push(format!("-if {}", condition));
196        }
197
198        // 排除标签
199        for tag in &self.exclude_tags {
200            args.push(format!("-{}=", tag));
201        }
202
203        // 特定标签
204        for tag in &self.specific_tags {
205            args.push(format!("-{}", tag));
206        }
207
208        // 文件路径
209        for path in paths {
210            args.push(path.as_ref().to_string_lossy().to_string());
211        }
212
213        args
214    }
215}
216
217/// 格式化输出结果
218#[derive(Debug, Clone)]
219pub struct FormattedOutput {
220    /// 输出格式
221    pub format: OutputFormat,
222    /// 内容
223    pub content: String,
224}
225
226impl FormattedOutput {
227    /// 解析为 JSON
228    pub fn to_json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
229        serde_json::from_str(&self.content).map_err(|e| e.into())
230    }
231
232    /// 获取纯文本内容
233    pub fn text(&self) -> &str {
234        &self.content
235    }
236}
237
238/// 扩展 ExifTool 以支持格式化输出
239pub trait FormatOperations {
240    /// 使用自定义格式读取元数据
241    fn read_formatted<P: AsRef<Path>>(
242        &self,
243        path: P,
244        options: &ReadOptions,
245    ) -> Result<FormattedOutput>;
246
247    /// 读取为 XML
248    fn read_xml<P: AsRef<Path>>(&self, path: P) -> Result<String>;
249
250    /// 读取为 CSV
251    fn read_csv<P: AsRef<Path>>(&self, path: P) -> Result<String>;
252
253    /// 读取为 HTML 表格
254    fn read_html<P: AsRef<Path>>(&self, path: P) -> Result<String>;
255
256    /// 读取为纯文本
257    fn read_text<P: AsRef<Path>>(&self, path: P) -> Result<String>;
258
259    /// 递归读取目录
260    fn read_directory<P: AsRef<Path>>(
261        &self,
262        path: P,
263        options: &ReadOptions,
264    ) -> Result<Vec<FormattedOutput>>;
265}
266
267impl FormatOperations for ExifTool {
268    fn read_formatted<P: AsRef<Path>>(
269        &self,
270        path: P,
271        options: &ReadOptions,
272    ) -> Result<FormattedOutput> {
273        let args = options.build_args(&[path.as_ref()]);
274        let response = self.execute_raw(&args)?;
275
276        Ok(FormattedOutput {
277            format: options.format,
278            content: response.text(),
279        })
280    }
281
282    fn read_xml<P: AsRef<Path>>(&self, path: P) -> Result<String> {
283        let options = ReadOptions::new().format(OutputFormat::Xml);
284        let output = self.read_formatted(path, &options)?;
285        Ok(output.content)
286    }
287
288    fn read_csv<P: AsRef<Path>>(&self, path: P) -> Result<String> {
289        let options = ReadOptions::new().format(OutputFormat::Csv);
290        let output = self.read_formatted(path, &options)?;
291        Ok(output.content)
292    }
293
294    fn read_html<P: AsRef<Path>>(&self, path: P) -> Result<String> {
295        let options = ReadOptions::new().format(OutputFormat::Html);
296        let output = self.read_formatted(path, &options)?;
297        Ok(output.content)
298    }
299
300    fn read_text<P: AsRef<Path>>(&self, path: P) -> Result<String> {
301        let options = ReadOptions::new().format(OutputFormat::Text);
302        let output = self.read_formatted(path, &options)?;
303        Ok(output.content)
304    }
305
306    fn read_directory<P: AsRef<Path>>(
307        &self,
308        path: P,
309        options: &ReadOptions,
310    ) -> Result<Vec<FormattedOutput>> {
311        let mut opts = options.clone();
312        opts.recursive = true;
313        let args = opts.build_args(&[path.as_ref()]);
314        let response = self.execute_raw(&args)?;
315
316        // 解析多文件响应
317        let content = response.text();
318        let outputs: Vec<FormattedOutput> = content
319            .split("\n[{\\n") // 简化的分隔符,实际可能更复杂
320            .filter(|s| !s.is_empty())
321            .map(|s| FormattedOutput {
322                format: options.format,
323                content: s.to_string(),
324            })
325            .collect();
326
327        Ok(outputs)
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_output_format() {
337        assert_eq!(OutputFormat::Json.arg(), "-json");
338        assert_eq!(OutputFormat::Xml.arg(), "-X");
339        assert_eq!(OutputFormat::Csv.arg(), "-csv");
340    }
341
342    #[test]
343    fn test_read_options() {
344        let opts = ReadOptions::new()
345            .format(OutputFormat::Json)
346            .tag("Make")
347            .tag("Model")
348            .verbose(2)
349            .raw(true);
350
351        assert_eq!(opts.format, OutputFormat::Json);
352        assert_eq!(opts.specific_tags.len(), 2);
353        assert_eq!(opts.verbose, Some(2));
354        assert!(opts.raw_values);
355    }
356
357    #[test]
358    fn test_read_options_build_args() {
359        let opts = ReadOptions::new()
360            .format(OutputFormat::Json)
361            .tag("Make")
362            .raw(true);
363
364        let args = opts.build_args(&[std::path::Path::new("test.jpg")]);
365
366        assert!(args.contains(&"-json".to_string()));
367        assert!(args.contains(&"-Make".to_string()));
368        assert!(args.contains(&"-n".to_string()));
369        assert!(args.contains(&"test.jpg".to_string()));
370    }
371}