1mod 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#[cfg(feature = "serde-structs")]
53pub mod structs;
54
55mod advanced;
57
58#[cfg(feature = "async")]
60pub mod async_ext;
61
62pub 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
73pub use pool::{ExifToolPool, PoolConnection, batch_with_pool, with_pool};
75
76pub use format::{FormatOperations, FormattedOutput, OutputFormat, ReadOptions};
78
79pub use file_ops::{FileOperations, OrganizeOptions, RenamePattern};
81
82pub use geo::{GeoOperations, GpsCoordinate};
84
85pub use config::{
87 ConfigOperations, DiffResult, HexDumpOperations, HexDumpOptions, VerboseOperations,
88 VerboseOptions,
89};
90
91pub use stream::{
93 Cache, PerformanceStats, ProgressCallback, ProgressReader, ProgressTracker, StreamOptions,
94 StreamingOperations,
95};
96
97#[cfg(feature = "async")]
98pub use stream::async_stream;
100
101pub 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#[derive(Debug, Clone)]
126pub struct ExifTool {
127 inner: Arc<Mutex<ExifToolInner>>,
128 global_args: Arc<Vec<String>>,
129}
130
131#[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
143pub 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 pub fn new() -> Self {
153 Self {
154 executable: None,
155 response_timeout: None,
156 config_path: None,
157 }
158 }
159
160 pub fn executable<P: Into<std::path::PathBuf>>(mut self, path: P) -> Self {
162 self.executable = Some(path.into());
163 self
164 }
165
166 pub fn response_timeout(mut self, timeout: Duration) -> Self {
168 self.response_timeout = Some(timeout);
169 self
170 }
171
172 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 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 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 pub fn builder() -> ExifToolBuilder {
255 ExifToolBuilder::new()
256 }
257
258 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 pub fn query<P: AsRef<Path>>(&self, path: P) -> QueryBuilder<'_> {
295 QueryBuilder::new(self, path)
296 }
297
298 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 pub fn write<P: AsRef<Path>>(&self, path: P) -> WriteBuilder<'_> {
358 WriteBuilder::new(self, path)
359 }
360
361 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 let json = serde_json::to_value(value)?;
396 let result: T = serde_json::from_value(json)?;
397
398 Ok(result)
399 }
400
401 #[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 let output = self.query(path).arg("-json").arg("-g2").execute_text()?;
426
427 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 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 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 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 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 pub fn list_groups(&self) -> Result<Vec<String>> {
495 let response = self.execute(&["-listg"])?;
496 Ok(parse_word_list(response.text()))
497 }
498
499 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 pub fn list_descriptions(&self) -> Result<Vec<String>> {
541 let response = self.execute(&["-listd"])?;
542 Ok(parse_word_list(response.text()))
543 }
544
545 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 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 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 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 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 pub fn execute_multiple<S: AsRef<str>>(&self, commands: &[Vec<S>]) -> Result<Vec<Response>> {
679 debug!("Executing {} commands atomically", commands.len());
680
681 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 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 pub fn close(&self) -> Result<()> {
706 let mut inner = self.inner.lock()?;
707 inner.close()
708 }
709
710 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 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 #[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 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}