1use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct ImageMetadata {
11 pub exif: Option<Vec<u8>>,
13 pub icc_profile: Option<Vec<u8>>,
15}
16
17pub fn extract_metadata(path: &Path) -> Option<ImageMetadata> {
28 let bytes = std::fs::read(path).ok()?;
29
30 if let Some(meta) = extract_metadata_jpeg(&bytes) {
32 return Some(meta);
33 }
34
35 if let Some(meta) = extract_metadata_png(&bytes) {
37 return Some(meta);
38 }
39
40 #[cfg(feature = "raw")]
42 {
43 if crate::decode::is_raw_extension(path) {
44 if let Some(meta) = extract_metadata_raw_tiff(path) {
45 return Some(meta);
46 }
47 }
48 }
49
50 #[cfg(feature = "raw")]
52 {
53 if crate::decode::is_raw_extension(path) {
54 if let Some(exif_bytes) = crate::decode::raw::extract_raw_metadata(path) {
55 return Some(ImageMetadata {
56 exif: Some(exif_bytes),
57 icc_profile: None,
58 });
59 }
60 }
61 }
62
63 None
64}
65
66fn extract_metadata_jpeg(bytes: &[u8]) -> Option<ImageMetadata> {
67 use img_parts::{ImageEXIF, ImageICC};
68
69 let jpeg = img_parts::jpeg::Jpeg::from_bytes(bytes.to_vec().into()).ok()?;
70 let exif = jpeg.exif().map(|b| b.to_vec());
71 let icc = jpeg.icc_profile().map(|b| b.to_vec());
72 if exif.is_some() || icc.is_some() {
73 return Some(ImageMetadata {
74 exif,
75 icc_profile: icc,
76 });
77 }
78 None
79}
80
81fn extract_metadata_png(bytes: &[u8]) -> Option<ImageMetadata> {
82 use img_parts::{ImageEXIF, ImageICC};
83
84 let png = img_parts::png::Png::from_bytes(bytes.to_vec().into()).ok()?;
85 let exif = png.exif().map(|b| b.to_vec());
86 let icc = png.icc_profile().map(|b| b.to_vec());
87 if exif.is_some() || icc.is_some() {
88 return Some(ImageMetadata {
89 exif,
90 icc_profile: icc,
91 });
92 }
93 None
94}
95
96#[cfg(feature = "raw")]
101fn extract_metadata_raw_tiff(path: &Path) -> Option<ImageMetadata> {
102 let file = std::fs::File::open(path).ok()?;
103 let mut reader = std::io::BufReader::new(file);
104 let exif = exif::Reader::new().read_from_container(&mut reader).ok()?;
105 let raw_buf = exif.buf();
106 if raw_buf.is_empty() {
107 return None;
108 }
109 let exif_bytes = if raw_buf.starts_with(b"Exif\0\0") {
112 raw_buf.to_vec()
113 } else {
114 let mut prefixed = b"Exif\0\0".to_vec();
115 prefixed.extend_from_slice(raw_buf);
116 prefixed
117 };
118 Some(ImageMetadata {
119 exif: Some(exif_bytes),
120 icc_profile: None,
121 })
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn extract_metadata_from_jpeg_with_no_exif() {
130 use image::{ImageBuffer, Rgb};
131
132 let temp_path = std::env::temp_dir().join("agx_test_no_exif.jpg");
133 let img: ImageBuffer<Rgb<u8>, Vec<u8>> =
134 ImageBuffer::from_pixel(4, 4, Rgb([128u8, 128, 128]));
135 img.save(&temp_path).unwrap();
136
137 let meta = extract_metadata(&temp_path);
138 if let Some(m) = meta {
139 assert!(m.exif.is_none() || !m.exif.as_ref().unwrap().is_empty());
140 }
141
142 let _ = std::fs::remove_file(&temp_path);
143 }
144
145 #[test]
146 fn extract_metadata_nonexistent_file_returns_none() {
147 let meta = extract_metadata(std::path::Path::new("/nonexistent/file.jpg"));
148 assert!(meta.is_none());
149 }
150
151 #[test]
152 fn extract_metadata_from_png() {
153 use image::{ImageBuffer, Rgb};
154
155 let temp_path = std::env::temp_dir().join("agx_test_meta.png");
156 let img: ImageBuffer<Rgb<u8>, Vec<u8>> =
157 ImageBuffer::from_pixel(4, 4, Rgb([128u8, 128, 128]));
158 img.save(&temp_path).unwrap();
159
160 let _meta = extract_metadata(&temp_path);
161 let _ = std::fs::remove_file(&temp_path);
163 }
164}
165
166#[cfg(all(test, feature = "raw"))]
167mod raw_metadata_tests {
168 use super::*;
169
170 #[test]
171 fn extract_metadata_raw_tiff_nonexistent_returns_none() {
172 let meta = extract_metadata_raw_tiff(std::path::Path::new("/nonexistent/photo.cr2"));
173 assert!(meta.is_none());
174 }
175
176 #[test]
177 fn extract_metadata_raw_tiff_non_tiff_file_returns_none() {
178 let temp_path = std::env::temp_dir().join("agx_test_not_tiff_raw.jpg");
179 let img: image::ImageBuffer<image::Rgb<u8>, Vec<u8>> =
180 image::ImageBuffer::from_pixel(4, 4, image::Rgb([128u8, 128, 128]));
181 img.save(&temp_path).unwrap();
182
183 let _meta = extract_metadata_raw_tiff(&temp_path);
184 let _ = std::fs::remove_file(&temp_path);
186 }
187
188 #[test]
189 fn extract_metadata_falls_through_to_none_for_unknown() {
190 let temp_path = std::env::temp_dir().join("agx_test_unknown.bmp");
191 let img: image::ImageBuffer<image::Rgb<u8>, Vec<u8>> =
192 image::ImageBuffer::from_pixel(4, 4, image::Rgb([128u8, 128, 128]));
193 img.save(&temp_path).unwrap();
194 let meta = extract_metadata(&temp_path);
195 assert!(meta.is_none());
196 let _ = std::fs::remove_file(&temp_path);
197 }
198}