Skip to main content

exiftool_rs_wrapper/
binary.rs

1//! 二进制数据处理模块
2//!
3//! # 注意事项
4//!
5//! 在 `-stay_open` 模式下,ExifTool 通过 stdout 文本协议传输数据,
6//! 使用 `{ready}` 作为响应结束标记。如果二进制内容恰好包含 `{ready}`
7//! 字符串,会导致响应解析提前终止,返回截断的数据。
8//!
9//! 因此,`read_binary()` 采用临时文件方案:先用 `-b -TAG -w TMPFILE`
10//! 将二进制数据输出到临时文件,再读取文件内容,避免文本协议的限制。
11
12use crate::ExifTool;
13use crate::error::{Error, Result};
14use std::path::{Path, PathBuf};
15
16/// 二进制数据写入构建器
17pub 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    /// 创建新的二进制写入构建器
27    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    /// 设置缩略图
38    pub fn thumbnail(mut self, data: Vec<u8>) -> Self {
39        self.binary_tags.push((BinaryTag::Thumbnail, data));
40        self
41    }
42
43    /// 设置预览图
44    pub fn preview(mut self, data: Vec<u8>) -> Self {
45        self.binary_tags.push((BinaryTag::Preview, data));
46        self
47    }
48
49    /// 设置 JPEG 预览
50    pub fn jpeg_preview(mut self, data: Vec<u8>) -> Self {
51        self.binary_tags.push((BinaryTag::JpegPreview, data));
52        self
53    }
54
55    /// 覆盖原始文件
56    pub fn overwrite_original(mut self, yes: bool) -> Self {
57        self.overwrite_original = yes;
58        self
59    }
60
61    /// 创建备份
62    pub fn backup(mut self, yes: bool) -> Self {
63        self.backup = yes;
64        self
65    }
66
67    /// 执行写入
68    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        // 基础选项
76        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        // 为每个二进制标签创建临时文件
85        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        // 添加目标文件
95        args.push(self.path.to_string_lossy().to_string());
96
97        // 执行命令
98        let response = self.exiftool.execute_raw(&args)?;
99
100        // 临时文件会在 temp_files 被 drop 时自动删除
101        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/// 二进制写入结果
119#[derive(Debug, Clone)]
120pub struct BinaryWriteResult {
121    /// 被修改的文件路径
122    pub path: PathBuf,
123
124    /// 写入的二进制标签
125    pub written_tags: Vec<BinaryTag>,
126}
127
128/// 二进制标签类型
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum BinaryTag {
131    /// 缩略图
132    Thumbnail,
133
134    /// 预览图
135    Preview,
136
137    /// JPEG 预览
138    JpegPreview,
139
140    /// 其他自定义标签
141    Other(&'static str),
142}
143
144impl BinaryTag {
145    /// 获取标签名称
146    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
167/// 扩展 ExifTool 以支持二进制操作
168pub trait BinaryOperations {
169    /// 读取二进制数据
170    fn read_binary<P: AsRef<Path>>(&self, path: P, tag: BinaryTag) -> Result<Vec<u8>>;
171
172    /// 写入二进制数据
173    fn write_binary<P: AsRef<Path>>(&self, path: P) -> BinaryWriteBuilder<'_>;
174
175    /// 提取缩略图到文件
176    fn extract_thumbnail<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, dest: Q) -> Result<()>;
177
178    /// 提取预览图到文件
179    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        // 警告:不能直接通过 stay_open stdout 文本协议传输二进制数据,
185        // 因为二进制内容可能包含 `{ready}` 字符串,导致响应解析提前终止。
186        // 这里采用临时文件方案:使用 `-b -TAG -w` 输出到临时文件,再读取文件内容。
187        let tmp_dir = tempfile::tempdir()?;
188        // 使用固定文件名模板,ExifTool 的 -w 选项会将输出写入此目录
189        // %f 代表原始文件名(不含扩展名),%s 代表原始扩展名
190        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        // 检查 ExifTool 是否报告错误
203        if response.is_error() {
204            return Err(Error::process(
205                response
206                    .error_message()
207                    .unwrap_or_else(|| "读取二进制数据失败".to_string()),
208            ));
209        }
210
211        // 读取临时文件中的二进制数据
212        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        // 临时目录会在 tmp_dir 被 drop 时自动清理
221        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    /// 最小有效 JPEG 文件字节数组,用于创建临时测试文件
267    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    /// Base64 解码(仅用于测试)
291    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        // 测试简单 Base64 解码
302        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        // 检查 ExifTool 是否可用,不可用则跳过
310        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        // 创建临时 JPEG 文件
317        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        // 先使用 write_binary 写入一个缩略图数据
322        // 使用 TINY_JPEG 本身作为缩略图数据
323        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                // 缩略图写入成功,尝试提取
332                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                        // 验证提取的文件存在且非空
338                        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                        // 最小 JPEG 可能不支持缩略图嵌入,记录但不使测试失败
349                        eprintln!("缩略图提取失败(最小 JPEG 可能不支持缩略图嵌入): {:?}", e);
350                    }
351                }
352            }
353            Err(e) => {
354                // 缩略图写入失败(最小 JPEG 可能不支持),记录但不使测试失败
355                eprintln!("缩略图写入失败(最小 JPEG 可能不支持缩略图嵌入): {:?}", e);
356            }
357        }
358    }
359}