1use crate::ExifTool;
4use crate::error::Result;
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum OutputFormat {
10 #[default]
12 Json,
13 Xml,
15 Csv,
17 Tsv,
19 Html,
21 Text,
23 Struct,
25}
26
27impl OutputFormat {
28 fn arg(&self) -> &'static str {
30 match self {
31 Self::Json => "-json",
32 Self::Xml => "-X", Self::Csv => "-csv",
34 Self::Tsv => "-t", Self::Html => "-h", Self::Text => "-s", Self::Struct => "-struct",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct ReadOptions {
45 pub format: OutputFormat,
47 pub exclude_tags: Vec<String>,
49 pub specific_tags: Vec<String>,
51 pub table_format: bool,
53 pub hex_dump: bool,
55 pub verbose: Option<u8>,
57 pub lang: Option<String>,
59 pub charset: Option<String>,
61 pub raw_values: bool,
63 pub recursive: bool,
65 pub extensions: Vec<String>,
67 pub condition: Option<String>,
69}
70
71impl ReadOptions {
72 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn format(mut self, format: OutputFormat) -> Self {
79 self.format = format;
80 self
81 }
82
83 pub fn exclude(mut self, tag: impl Into<String>) -> Self {
85 self.exclude_tags.push(tag.into());
86 self
87 }
88
89 pub fn tag(mut self, tag: impl Into<String>) -> Self {
91 self.specific_tags.push(tag.into());
92 self
93 }
94
95 pub fn table(mut self, yes: bool) -> Self {
97 self.table_format = yes;
98 self
99 }
100
101 pub fn hex(mut self, yes: bool) -> Self {
103 self.hex_dump = yes;
104 self
105 }
106
107 pub fn verbose(mut self, level: u8) -> Self {
109 self.verbose = Some(level.min(5));
110 self
111 }
112
113 pub fn lang(mut self, lang: impl Into<String>) -> Self {
115 self.lang = Some(lang.into());
116 self
117 }
118
119 pub fn charset(mut self, charset: impl Into<String>) -> Self {
121 self.charset = Some(charset.into());
122 self
123 }
124
125 pub fn raw(mut self, yes: bool) -> Self {
127 self.raw_values = yes;
128 self
129 }
130
131 pub fn recursive(mut self, yes: bool) -> Self {
133 self.recursive = yes;
134 self
135 }
136
137 pub fn extension(mut self, ext: impl Into<String>) -> Self {
139 self.extensions.push(ext.into());
140 self
141 }
142
143 pub fn condition(mut self, expr: impl Into<String>) -> Self {
145 self.condition = Some(expr.into());
146 self
147 }
148
149 pub(crate) fn build_args(&self, paths: &[impl AsRef<Path>]) -> Vec<String> {
151 let mut args = vec![self.format.arg().to_string()];
152
153 if self.table_format {
155 args.push("-T".to_string());
156 }
157
158 if self.hex_dump {
160 args.push("-H".to_string());
161 }
162
163 if let Some(level) = self.verbose {
165 args.push(format!("-v{}", level));
166 }
167
168 if let Some(ref lang) = self.lang {
171 args.push("-lang".to_string());
172 args.push(lang.clone());
173 }
174
175 if let Some(ref charset) = self.charset {
177 args.push("-charset".to_string());
178 args.push(charset.clone());
179 }
180
181 if self.raw_values {
183 args.push("-n".to_string());
184 }
185
186 if self.recursive {
188 args.push("-r".to_string());
189 }
190
191 for ext in &self.extensions {
193 args.push("-ext".to_string());
194 args.push(ext.clone());
195 }
196
197 if let Some(ref condition) = self.condition {
199 args.push("-if".to_string());
200 args.push(condition.clone());
201 }
202
203 for tag in &self.exclude_tags {
205 args.push(format!("-{}=", tag));
206 }
207
208 for tag in &self.specific_tags {
210 args.push(format!("-{}", tag));
211 }
212
213 for path in paths {
215 args.push(path.as_ref().to_string_lossy().to_string());
216 }
217
218 args
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct FormattedOutput {
225 pub format: OutputFormat,
227 pub content: String,
229}
230
231impl FormattedOutput {
232 pub fn to_json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
234 serde_json::from_str(&self.content).map_err(|e| e.into())
235 }
236
237 pub fn text(&self) -> &str {
239 &self.content
240 }
241}
242
243pub trait FormatOperations {
245 fn read_formatted<P: AsRef<Path>>(
247 &self,
248 path: P,
249 options: &ReadOptions,
250 ) -> Result<FormattedOutput>;
251
252 fn read_xml<P: AsRef<Path>>(&self, path: P) -> Result<String>;
254
255 fn read_csv<P: AsRef<Path>>(&self, path: P) -> Result<String>;
257
258 fn read_html<P: AsRef<Path>>(&self, path: P) -> Result<String>;
260
261 fn read_text<P: AsRef<Path>>(&self, path: P) -> Result<String>;
263
264 fn read_directory<P: AsRef<Path>>(
266 &self,
267 path: P,
268 options: &ReadOptions,
269 ) -> Result<Vec<FormattedOutput>>;
270}
271
272impl FormatOperations for ExifTool {
273 fn read_formatted<P: AsRef<Path>>(
274 &self,
275 path: P,
276 options: &ReadOptions,
277 ) -> Result<FormattedOutput> {
278 let args = options.build_args(&[path.as_ref()]);
279 let response = self.execute_raw(&args)?;
280
281 Ok(FormattedOutput {
282 format: options.format,
283 content: response.text(),
284 })
285 }
286
287 fn read_xml<P: AsRef<Path>>(&self, path: P) -> Result<String> {
288 let options = ReadOptions::new().format(OutputFormat::Xml);
289 let output = self.read_formatted(path, &options)?;
290 Ok(output.content)
291 }
292
293 fn read_csv<P: AsRef<Path>>(&self, path: P) -> Result<String> {
294 let options = ReadOptions::new().format(OutputFormat::Csv);
295 let output = self.read_formatted(path, &options)?;
296 Ok(output.content)
297 }
298
299 fn read_html<P: AsRef<Path>>(&self, path: P) -> Result<String> {
300 let options = ReadOptions::new().format(OutputFormat::Html);
301 let output = self.read_formatted(path, &options)?;
302 Ok(output.content)
303 }
304
305 fn read_text<P: AsRef<Path>>(&self, path: P) -> Result<String> {
306 let options = ReadOptions::new().format(OutputFormat::Text);
307 let output = self.read_formatted(path, &options)?;
308 Ok(output.content)
309 }
310
311 fn read_directory<P: AsRef<Path>>(
312 &self,
313 path: P,
314 options: &ReadOptions,
315 ) -> Result<Vec<FormattedOutput>> {
316 let mut opts = options.clone();
320 opts.recursive = true;
321 opts.format = OutputFormat::Json;
323
324 let args = opts.build_args(&[path.as_ref()]);
325 let response = self.execute_raw(&args)?;
326 let content = response.text();
327
328 let json_array: Vec<serde_json::Value> =
330 serde_json::from_str(content.trim()).unwrap_or_default();
331
332 let outputs: Vec<FormattedOutput> = json_array
333 .into_iter()
334 .map(|item| {
335 let file_content = match options.format {
337 OutputFormat::Json => {
338 serde_json::to_string_pretty(&vec![&item])
340 .unwrap_or_else(|_| item.to_string())
341 }
342 _ => {
343 serde_json::to_string_pretty(&item).unwrap_or_else(|_| item.to_string())
345 }
346 };
347 FormattedOutput {
348 format: options.format,
349 content: file_content,
350 }
351 })
352 .collect();
353
354 Ok(outputs)
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::ExifTool;
362 use crate::error::Error;
363
364 const TINY_JPEG: &[u8] = &[
366 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00,
367 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06,
368 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B,
369 0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
370 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, 0x2C, 0x30, 0x31,
371 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF,
372 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00,
373 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
374 0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
375 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
376 0x00, 0x00, 0x3F, 0x00, 0xD2, 0xCF, 0x20, 0xFF, 0xD9,
377 ];
378
379 #[test]
380 fn test_output_format() {
381 assert_eq!(OutputFormat::Json.arg(), "-json");
382 assert_eq!(OutputFormat::Xml.arg(), "-X");
383 assert_eq!(OutputFormat::Csv.arg(), "-csv");
384 }
385
386 #[test]
387 fn test_read_options() {
388 let opts = ReadOptions::new()
389 .format(OutputFormat::Json)
390 .tag("Make")
391 .tag("Model")
392 .verbose(2)
393 .raw(true);
394
395 assert_eq!(opts.format, OutputFormat::Json);
396 assert_eq!(opts.specific_tags.len(), 2);
397 assert_eq!(opts.verbose, Some(2));
398 assert!(opts.raw_values);
399 }
400
401 #[test]
402 fn test_read_options_build_args() {
403 let opts = ReadOptions::new()
404 .format(OutputFormat::Json)
405 .tag("Make")
406 .raw(true);
407
408 let args = opts.build_args(&[std::path::Path::new("test.jpg")]);
409
410 assert!(args.contains(&"-json".to_string()));
411 assert!(args.contains(&"-Make".to_string()));
412 assert!(args.contains(&"-n".to_string()));
413 assert!(args.contains(&"test.jpg".to_string()));
414 }
415
416 #[test]
417 fn test_build_args_split_options() {
418 let opts = ReadOptions::new()
420 .format(OutputFormat::Json)
421 .lang("zh-CN")
422 .charset("UTF8")
423 .extension("jpg")
424 .condition("$ImageWidth > 1000");
425
426 let args = opts.build_args(&[std::path::Path::new("test.jpg")]);
427
428 assert!(args.windows(2).any(|w| w == ["-lang", "zh-CN"]));
430 assert!(args.windows(2).any(|w| w == ["-charset", "UTF8"]));
432 assert!(args.windows(2).any(|w| w == ["-ext", "jpg"]));
434 assert!(args.windows(2).any(|w| w == ["-if", "$ImageWidth > 1000"]));
436 }
437
438 #[test]
439 fn test_read_xml_contains_xml_tags() {
440 let et = match ExifTool::new() {
442 Ok(et) => et,
443 Err(Error::ExifToolNotFound) => return,
444 Err(e) => panic!("创建 ExifTool 实例时出现意外错误: {:?}", e),
445 };
446
447 let tmp_dir = tempfile::tempdir().expect("创建临时目录失败");
449 let test_file = tmp_dir.path().join("format_xml.jpg");
450 std::fs::write(&test_file, TINY_JPEG).expect("写入临时 JPEG 文件失败");
451
452 let xml_output = et.read_xml(&test_file).expect("读取 XML 格式元数据失败");
454
455 assert!(
457 xml_output.contains('<') && xml_output.contains('>'),
458 "XML 输出应包含 XML 标签(尖括号),实际输出: {}",
459 &xml_output[..xml_output.len().min(200)]
460 );
461 assert!(
463 xml_output.contains("rdf:")
464 || xml_output.contains("<?xml")
465 || xml_output.contains("et:"),
466 "XML 输出应包含 XML 命名空间(如 rdf: 或 et:),实际输出: {}",
467 &xml_output[..xml_output.len().min(300)]
468 );
469 }
470
471 #[test]
472 fn test_read_text_non_empty() {
473 let et = match ExifTool::new() {
475 Ok(et) => et,
476 Err(Error::ExifToolNotFound) => return,
477 Err(e) => panic!("创建 ExifTool 实例时出现意外错误: {:?}", e),
478 };
479
480 let tmp_dir = tempfile::tempdir().expect("创建临时目录失败");
482 let test_file = tmp_dir.path().join("format_text.jpg");
483 std::fs::write(&test_file, TINY_JPEG).expect("写入临时 JPEG 文件失败");
484
485 let text_output = et.read_text(&test_file).expect("读取纯文本格式元数据失败");
487
488 let trimmed = text_output.trim();
490 assert!(!trimmed.is_empty(), "纯文本格式输出不应为空");
491
492 assert!(
495 trimmed.contains("FileName")
496 || trimmed.contains("FileSize")
497 || trimmed.contains("MIMEType"),
498 "纯文本输出应包含基本文件信息标签,实际输出: {}",
499 &trimmed[..trimmed.len().min(300)]
500 );
501 }
502}