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 {
170 args.push(format!("-lang {}", lang));
171 }
172
173 if let Some(ref charset) = self.charset {
175 args.push(format!("-charset {}", charset));
176 }
177
178 if self.raw_values {
180 args.push("-n".to_string());
181 }
182
183 if self.recursive {
185 args.push("-r".to_string());
186 }
187
188 for ext in &self.extensions {
190 args.push(format!("-ext {}", ext));
191 }
192
193 if let Some(ref condition) = self.condition {
195 args.push(format!("-if {}", condition));
196 }
197
198 for tag in &self.exclude_tags {
200 args.push(format!("-{}=", tag));
201 }
202
203 for tag in &self.specific_tags {
205 args.push(format!("-{}", tag));
206 }
207
208 for path in paths {
210 args.push(path.as_ref().to_string_lossy().to_string());
211 }
212
213 args
214 }
215}
216
217#[derive(Debug, Clone)]
219pub struct FormattedOutput {
220 pub format: OutputFormat,
222 pub content: String,
224}
225
226impl FormattedOutput {
227 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 pub fn text(&self) -> &str {
234 &self.content
235 }
236}
237
238pub trait FormatOperations {
240 fn read_formatted<P: AsRef<Path>>(
242 &self,
243 path: P,
244 options: &ReadOptions,
245 ) -> Result<FormattedOutput>;
246
247 fn read_xml<P: AsRef<Path>>(&self, path: P) -> Result<String>;
249
250 fn read_csv<P: AsRef<Path>>(&self, path: P) -> Result<String>;
252
253 fn read_html<P: AsRef<Path>>(&self, path: P) -> Result<String>;
255
256 fn read_text<P: AsRef<Path>>(&self, path: P) -> Result<String>;
258
259 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 let content = response.text();
318 let outputs: Vec<FormattedOutput> = content
319 .split("\n[{\\n") .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}