1use crate::ExifTool;
13use crate::error::{Error, Result};
14use std::path::{Path, PathBuf};
15
16pub struct BinaryWriteBuilder<'et> {
18 exiftool: &'et ExifTool,
19 path: PathBuf,
20 binary_tags: Vec<(BinaryTag, Vec<u8>)>,
21 overwrite_original: bool,
22 backup: bool,
23}
24
25impl<'et> BinaryWriteBuilder<'et> {
26 pub(crate) fn new<P: AsRef<Path>>(exiftool: &'et ExifTool, path: P) -> Self {
28 Self {
29 exiftool,
30 path: path.as_ref().to_path_buf(),
31 binary_tags: Vec::new(),
32 overwrite_original: false,
33 backup: true,
34 }
35 }
36
37 pub fn thumbnail(mut self, data: Vec<u8>) -> Self {
39 self.binary_tags.push((BinaryTag::Thumbnail, data));
40 self
41 }
42
43 pub fn preview(mut self, data: Vec<u8>) -> Self {
45 self.binary_tags.push((BinaryTag::Preview, data));
46 self
47 }
48
49 pub fn jpeg_preview(mut self, data: Vec<u8>) -> Self {
51 self.binary_tags.push((BinaryTag::JpegPreview, data));
52 self
53 }
54
55 pub fn overwrite_original(mut self, yes: bool) -> Self {
57 self.overwrite_original = yes;
58 self
59 }
60
61 pub fn backup(mut self, yes: bool) -> Self {
63 self.backup = yes;
64 self
65 }
66
67 pub fn execute(self) -> Result<BinaryWriteResult> {
69 use std::io::Write;
70 use tempfile::NamedTempFile;
71
72 let mut temp_files = Vec::with_capacity(self.binary_tags.len());
73 let mut args = Vec::new();
74
75 if self.overwrite_original {
77 args.push("-overwrite_original".to_string());
78 }
79
80 if !self.backup {
81 args.push("-overwrite_original_in_place".to_string());
82 }
83
84 for (tag, data) in &self.binary_tags {
86 let mut temp_file = NamedTempFile::new()?;
87 temp_file.write_all(data)?;
88 let temp_path = temp_file.path().to_path_buf();
89 temp_files.push(temp_file);
90
91 args.push(format!("-{}={}", tag.tag_name(), temp_path.display()));
92 }
93
94 args.push(self.path.to_string_lossy().to_string());
96
97 let response = self.exiftool.execute_raw(&args)?;
99
100 drop(temp_files);
102
103 if response.is_error() {
104 return Err(Error::process(
105 response
106 .error_message()
107 .unwrap_or_else(|| "Unknown binary write error".to_string()),
108 ));
109 }
110
111 Ok(BinaryWriteResult {
112 path: self.path,
113 written_tags: self.binary_tags.into_iter().map(|(tag, _)| tag).collect(),
114 })
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct BinaryWriteResult {
121 pub path: PathBuf,
123
124 pub written_tags: Vec<BinaryTag>,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum BinaryTag {
131 Thumbnail,
133
134 Preview,
136
137 JpegPreview,
139
140 Other(&'static str),
142}
143
144impl BinaryTag {
145 pub fn tag_name(&self) -> &str {
147 match self {
148 Self::Thumbnail => "ThumbnailImage",
149 Self::Preview => "PreviewImage",
150 Self::JpegPreview => "JpgFromRaw",
151 Self::Other(name) => name,
152 }
153 }
154}
155
156impl From<&'static str> for BinaryTag {
157 fn from(name: &'static str) -> Self {
158 match name {
159 "ThumbnailImage" => Self::Thumbnail,
160 "PreviewImage" => Self::Preview,
161 "JpgFromRaw" => Self::JpegPreview,
162 _ => Self::Other(name),
163 }
164 }
165}
166
167pub trait BinaryOperations {
169 fn read_binary<P: AsRef<Path>>(&self, path: P, tag: BinaryTag) -> Result<Vec<u8>>;
171
172 fn write_binary<P: AsRef<Path>>(&self, path: P) -> BinaryWriteBuilder<'_>;
174
175 fn extract_thumbnail<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, dest: Q) -> Result<()>;
177
178 fn extract_preview<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, dest: Q) -> Result<()>;
180}
181
182impl BinaryOperations for ExifTool {
183 fn read_binary<P: AsRef<Path>>(&self, path: P, tag: BinaryTag) -> Result<Vec<u8>> {
184 let tmp_dir = tempfile::tempdir()?;
188 let out_pattern = format!("{}/bin_out.dat", tmp_dir.path().display());
191
192 let args = vec![
193 "-b".to_string(),
194 format!("-{}", tag.tag_name()),
195 "-w".to_string(),
196 out_pattern.clone(),
197 path.as_ref().to_string_lossy().to_string(),
198 ];
199
200 let response = self.execute_raw(&args)?;
201
202 if response.is_error() {
204 return Err(Error::process(
205 response
206 .error_message()
207 .unwrap_or_else(|| "读取二进制数据失败".to_string()),
208 ));
209 }
210
211 let data = std::fs::read(&out_pattern).map_err(|e| {
213 Error::process(format!(
214 "读取临时文件失败(标签 {} 可能不存在): {}",
215 tag.tag_name(),
216 e
217 ))
218 })?;
219
220 Ok(data)
222 }
223
224 fn write_binary<P: AsRef<Path>>(&self, path: P) -> BinaryWriteBuilder<'_> {
225 BinaryWriteBuilder::new(self, path)
226 }
227
228 fn extract_thumbnail<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, dest: Q) -> Result<()> {
229 use std::fs::File;
230 use std::io::Write;
231
232 let data = self.read_binary(source, BinaryTag::Thumbnail)?;
233
234 if data.is_empty() {
235 return Err(Error::TagNotFound("ThumbnailImage".to_string()));
236 }
237
238 let mut file = File::create(dest)?;
239 file.write_all(&data)?;
240
241 Ok(())
242 }
243
244 fn extract_preview<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, dest: Q) -> Result<()> {
245 use std::fs::File;
246 use std::io::Write;
247
248 let data = self.read_binary(source, BinaryTag::Preview)?;
249
250 if data.is_empty() {
251 return Err(Error::TagNotFound("PreviewImage".to_string()));
252 }
253
254 let mut file = File::create(dest)?;
255 file.write_all(&data)?;
256
257 Ok(())
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::ExifTool;
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_binary_tag() {
283 assert_eq!(BinaryTag::Thumbnail.tag_name(), "ThumbnailImage");
284 assert_eq!(BinaryTag::Preview.tag_name(), "PreviewImage");
285
286 let tag: BinaryTag = "ThumbnailImage".into();
287 assert_eq!(tag, BinaryTag::Thumbnail);
288 }
289
290 fn base64_decode(input: &str) -> Result<Vec<u8>> {
292 use base64::{Engine, engine::general_purpose::STANDARD};
293
294 STANDARD
295 .decode(input)
296 .map_err(|e| Error::parse(format!("Base64 解码错误: {}", e)))
297 }
298
299 #[test]
300 fn test_base64_decode() {
301 let encoded = "SGVsbG8gV29ybGQh";
303 let decoded = base64_decode(encoded).unwrap();
304 assert_eq!(decoded, b"Hello World!");
305 }
306
307 #[test]
308 fn test_extract_thumbnail_with_embedded_image() {
309 let et = match ExifTool::new() {
311 Ok(et) => et,
312 Err(crate::error::Error::ExifToolNotFound) => return,
313 Err(e) => panic!("创建 ExifTool 实例时出现意外错误: {:?}", e),
314 };
315
316 let tmp_dir = tempfile::tempdir().expect("创建临时目录失败");
318 let src_file = tmp_dir.path().join("thumb_source.jpg");
319 std::fs::write(&src_file, TINY_JPEG).expect("写入临时 JPEG 文件失败");
320
321 let write_result = et
324 .write_binary(&src_file)
325 .thumbnail(TINY_JPEG.to_vec())
326 .overwrite_original(true)
327 .execute();
328
329 match write_result {
330 Ok(_) => {
331 let dest_file = tmp_dir.path().join("extracted_thumb.jpg");
333 let extract_result = et.extract_thumbnail(&src_file, &dest_file);
334
335 match extract_result {
336 Ok(()) => {
337 assert!(
339 dest_file.exists(),
340 "提取的缩略图文件应存在: {:?}",
341 dest_file
342 );
343 let thumb_data =
344 std::fs::read(&dest_file).expect("读取提取的缩略图文件失败");
345 assert!(!thumb_data.is_empty(), "提取的缩略图文件不应为空");
346 }
347 Err(e) => {
348 eprintln!("缩略图提取失败(最小 JPEG 可能不支持缩略图嵌入): {:?}", e);
350 }
351 }
352 }
353 Err(e) => {
354 eprintln!("缩略图写入失败(最小 JPEG 可能不支持缩略图嵌入): {:?}", e);
356 }
357 }
358 }
359}