1use crate::ExifTool;
6use crate::error::Result;
7use crate::types::TagId;
8use std::path::Path;
9
10#[derive(Debug, Clone)]
12pub struct DiffResult {
13 pub is_identical: bool,
15 pub source_only: Vec<String>,
17 pub target_only: Vec<String>,
19 pub different: Vec<(String, String, String)>, }
22
23impl Default for DiffResult {
24 fn default() -> Self {
25 Self {
26 is_identical: true,
27 source_only: Vec::new(),
28 target_only: Vec::new(),
29 different: Vec::new(),
30 }
31 }
32}
33
34impl DiffResult {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn add_source_only(&mut self, tag: impl Into<String>) {
42 self.is_identical = false;
43 self.source_only.push(tag.into());
44 }
45
46 pub fn add_target_only(&mut self, tag: impl Into<String>) {
48 self.is_identical = false;
49 self.target_only.push(tag.into());
50 }
51
52 pub fn add_different(
54 &mut self,
55 tag: impl Into<String>,
56 source: impl Into<String>,
57 target: impl Into<String>,
58 ) {
59 self.is_identical = false;
60 self.different
61 .push((tag.into(), source.into(), target.into()));
62 }
63}
64
65pub trait ConfigOperations {
67 fn with_config<P: AsRef<Path>>(&self, config_path: P) -> ExifTool;
69
70 fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult>;
72
73 fn diff_tags<P: AsRef<Path>, Q: AsRef<Path>>(
75 &self,
76 source: P,
77 target: Q,
78 tags: &[TagId],
79 ) -> Result<DiffResult>;
80}
81
82impl ConfigOperations for ExifTool {
83 fn with_config<P: AsRef<Path>>(&self, config_path: P) -> ExifTool {
84 ExifTool::with_config(self, config_path)
85 }
86
87 fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult> {
88 let source_meta = self.query(&source).execute()?;
89 let target_meta = self.query(&target).execute()?;
90
91 compare_metadata(&source_meta, &target_meta)
92 }
93
94 fn diff_tags<P: AsRef<Path>, Q: AsRef<Path>>(
95 &self,
96 source: P,
97 target: Q,
98 tags: &[TagId],
99 ) -> Result<DiffResult> {
100 let mut source_query = self.query(&source);
101 let mut target_query = self.query(&target);
102
103 for tag in tags {
104 source_query = source_query.tag_id(*tag);
105 target_query = target_query.tag_id(*tag);
106 }
107
108 let source_meta = source_query.execute()?;
109 let target_meta = target_query.execute()?;
110
111 compare_metadata(&source_meta, &target_meta)
112 }
113}
114
115fn compare_metadata(
117 source: &crate::types::Metadata,
118 target: &crate::types::Metadata,
119) -> Result<DiffResult> {
120 let mut result = DiffResult::new();
121
122 let mut all_tags: std::collections::HashSet<String> = std::collections::HashSet::new();
124 for (tag, _) in source.iter() {
125 all_tags.insert(tag.clone());
126 }
127 for (tag, _) in target.iter() {
128 all_tags.insert(tag.clone());
129 }
130
131 for tag in all_tags {
133 match (source.get(&tag), target.get(&tag)) {
134 (Some(s), Some(t)) => {
135 if s != t {
136 result.add_different(&tag, s.to_string_lossy(), t.to_string_lossy());
137 }
138 }
139 (Some(_), None) => result.add_source_only(&tag),
140 (None, Some(_)) => result.add_target_only(&tag),
141 (None, None) => {} }
143 }
144
145 Ok(result)
146}
147
148#[derive(Debug, Clone, Default)]
153pub struct HexDumpOptions {
154 pub start_offset: Option<usize>,
156}
157
158impl HexDumpOptions {
159 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn start(mut self, offset: usize) -> Self {
166 self.start_offset = Some(offset);
167 self
168 }
169}
170
171pub trait HexDumpOperations {
173 fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String>;
175
176 fn hex_dump_tag<P: AsRef<Path>>(&self, path: P, tag: TagId) -> Result<String>;
178}
179
180impl HexDumpOperations for ExifTool {
181 fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String> {
182 let mut args = Vec::new();
183
184 if let Some(offset) = options.start_offset {
185 args.push(format!("-htmlDump{}", offset));
186 } else {
187 args.push("-htmlDump".to_string());
188 }
189
190 args.push(path.as_ref().to_string_lossy().to_string());
191
192 let response = self.execute_raw(&args)?;
193 Ok(response.text())
194 }
195
196 fn hex_dump_tag<P: AsRef<Path>>(&self, path: P, tag: TagId) -> Result<String> {
197 let args = vec![
198 "-H".to_string(),
199 format!("-{}", tag.name()),
200 path.as_ref().to_string_lossy().to_string(),
201 ];
202
203 let response = self.execute_raw(&args)?;
204 Ok(response.text())
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct VerboseOptions {
211 pub level: u8,
213 pub html_format: bool,
215}
216
217impl VerboseOptions {
218 pub fn new(level: u8) -> Self {
220 Self {
221 level: level.min(5),
222 html_format: false,
223 }
224 }
225
226 pub fn html(mut self) -> Self {
228 self.html_format = true;
229 self
230 }
231
232 pub fn args(&self) -> Vec<String> {
234 let mut args = vec![format!("-v{}", self.level)];
235
236 if self.html_format {
237 args.push("-htmlDump".to_string());
238 }
239
240 args
241 }
242}
243
244pub trait VerboseOperations {
246 fn verbose_dump<P: AsRef<Path>>(&self, path: P, options: &VerboseOptions) -> Result<String>;
248}
249
250impl VerboseOperations for ExifTool {
251 fn verbose_dump<P: AsRef<Path>>(&self, path: P, options: &VerboseOptions) -> Result<String> {
252 let mut args = options.args();
253 args.push(path.as_ref().to_string_lossy().to_string());
254
255 let response = self.execute_raw(&args)?;
256 Ok(response.text())
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::ExifTool;
264 use crate::error::Error;
265
266 const TINY_JPEG: &[u8] = &[
268 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00,
269 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06,
270 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B,
271 0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
272 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, 0x2C, 0x30, 0x31,
273 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF,
274 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00,
275 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
276 0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
277 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
278 0x00, 0x00, 0x3F, 0x00, 0xD2, 0xCF, 0x20, 0xFF, 0xD9,
279 ];
280
281 #[test]
282 fn test_diff_result() {
283 let mut diff = DiffResult::new();
284 assert!(diff.is_identical);
285
286 diff.add_source_only("Make");
287 assert!(!diff.is_identical);
288 assert_eq!(diff.source_only.len(), 1);
289
290 diff.add_different("Model", "Canon", "Nikon");
291 assert_eq!(diff.different.len(), 1);
292 }
293
294 #[test]
295 fn test_hex_dump_options() {
296 let opts = HexDumpOptions::new().start(100);
297
298 assert_eq!(opts.start_offset, Some(100));
299 }
300
301 #[test]
302 fn test_verbose_options() {
303 let opts = VerboseOptions::new(3);
304 let args = opts.args();
305 assert!(args.contains(&"-v3".to_string()));
306
307 let opts = VerboseOptions::new(2).html();
308 let args = opts.args();
309 assert!(args.contains(&"-v2".to_string()));
310 assert!(args.contains(&"-htmlDump".to_string()));
311 }
312
313 #[test]
314 fn test_diff_two_different_files() {
315 let et = match ExifTool::new() {
317 Ok(et) => et,
318 Err(Error::ExifToolNotFound) => return,
319 Err(e) => panic!("创建 ExifTool 实例时出现意外错误: {:?}", e),
320 };
321
322 let tmp_dir = tempfile::tempdir().expect("创建临时目录失败");
324 let file_a = tmp_dir.path().join("diff_a.jpg");
325 let file_b = tmp_dir.path().join("diff_b.jpg");
326 std::fs::write(&file_a, TINY_JPEG).expect("写入文件 A 失败");
327 std::fs::write(&file_b, TINY_JPEG).expect("写入文件 B 失败");
328
329 et.write(&file_a)
331 .tag("Artist", "Alice")
332 .tag("Copyright", "2026 Alice")
333 .overwrite_original(true)
334 .execute()
335 .expect("写入文件 A 的元数据失败");
336
337 et.write(&file_b)
338 .tag("Artist", "Bob")
339 .tag("Copyright", "2026 Bob")
340 .overwrite_original(true)
341 .execute()
342 .expect("写入文件 B 的元数据失败");
343
344 let diff = et.diff(&file_a, &file_b).expect("执行 diff 操作失败");
346
347 assert!(
349 !diff.is_identical,
350 "写入不同元数据后的两个文件的 diff 结果应为不相同"
351 );
352 assert!(
355 !diff.different.is_empty(),
356 "diff 结果的 different 列表不应为空,应至少包含 Artist/Copyright 的差异"
357 );
358 }
359}