1use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use crate::error::{Error, Result};
11use crate::file_type::{self, FileType};
12use crate::formats;
13use crate::metadata::exif::ByteOrderMark;
14use crate::tag::Tag;
15use crate::value::Value;
16use crate::writer::{exif_writer, iptc_writer, jpeg_writer, matroska_writer, mp4_writer, pdf_writer, png_writer, psd_writer, tiff_writer, webp_writer, xmp_writer};
17
18#[derive(Debug, Clone)]
20pub struct Options {
21 pub duplicates: bool,
23 pub print_conv: bool,
25 pub fast_scan: u8,
27 pub requested_tags: Vec<String>,
29}
30
31impl Default for Options {
32 fn default() -> Self {
33 Self {
34 duplicates: false,
35 print_conv: true,
36 fast_scan: 0,
37 requested_tags: Vec::new(),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
56pub struct NewValue {
57 pub tag: String,
59 pub group: Option<String>,
61 pub value: Option<String>,
63}
64
65pub struct ExifTool {
66 options: Options,
67 new_values: Vec<NewValue>,
68}
69
70pub type ImageInfo = HashMap<String, String>;
72
73impl ExifTool {
74 pub fn new() -> Self {
76 Self {
77 options: Options::default(),
78 new_values: Vec::new(),
79 }
80 }
81
82 pub fn with_options(options: Options) -> Self {
84 Self {
85 options,
86 new_values: Vec::new(),
87 }
88 }
89
90 pub fn options_mut(&mut self) -> &mut Options {
92 &mut self.options
93 }
94
95 pub fn options(&self) -> &Options {
97 &self.options
98 }
99
100 pub fn set_new_value(&mut self, tag: &str, value: Option<&str>) {
122 let (group, tag_name) = if let Some(colon_pos) = tag.find(':') {
123 (Some(tag[..colon_pos].to_string()), tag[colon_pos + 1..].to_string())
124 } else {
125 (None, tag.to_string())
126 };
127
128 self.new_values.push(NewValue {
129 tag: tag_name,
130 group,
131 value: value.map(|v| v.to_string()),
132 });
133 }
134
135 pub fn clear_new_values(&mut self) {
137 self.new_values.clear();
138 }
139
140 pub fn set_new_values_from_file<P: AsRef<Path>>(
145 &mut self,
146 src_path: P,
147 tags_to_copy: Option<&[&str]>,
148 ) -> Result<u32> {
149 let src_tags = self.extract_info(src_path)?;
150 let mut count = 0u32;
151
152 for tag in &src_tags {
153 if tag.group.family0 == "File" || tag.group.family0 == "Composite" {
155 continue;
156 }
157 if tag.print_value.starts_with("(Binary") || tag.print_value.starts_with("(Undefined") {
159 continue;
160 }
161 if tag.print_value.is_empty() {
162 continue;
163 }
164
165 if let Some(filter) = tags_to_copy {
167 let name_lower = tag.name.to_lowercase();
168 if !filter.iter().any(|f| f.to_lowercase() == name_lower) {
169 continue;
170 }
171 }
172
173 let _full_tag = format!("{}:{}", tag.group.family0, tag.name);
174 self.new_values.push(NewValue {
175 tag: tag.name.clone(),
176 group: Some(tag.group.family0.clone()),
177 value: Some(tag.print_value.clone()),
178 });
179 count += 1;
180 }
181
182 Ok(count)
183 }
184
185 pub fn set_file_name_from_tag<P: AsRef<Path>>(
187 &self,
188 path: P,
189 tag_name: &str,
190 template: &str,
191 ) -> Result<String> {
192 let path = path.as_ref();
193 let tags = self.extract_info(path)?;
194
195 let tag_value = tags
196 .iter()
197 .find(|t| t.name.to_lowercase() == tag_name.to_lowercase())
198 .map(|t| &t.print_value)
199 .ok_or_else(|| Error::TagNotFound(tag_name.to_string()))?;
200
201 let new_name = if template.contains('%') {
204 template.replace("%v", value_to_filename(tag_value).as_str())
205 } else {
206 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
208 let clean = value_to_filename(tag_value);
209 if ext.is_empty() {
210 clean
211 } else {
212 format!("{}.{}", clean, ext)
213 }
214 };
215
216 let parent = path.parent().unwrap_or(Path::new(""));
217 let new_path = parent.join(&new_name);
218
219 fs::rename(path, &new_path).map_err(Error::Io)?;
220 Ok(new_path.to_string_lossy().to_string())
221 }
222
223 pub fn write_info<P: AsRef<Path>, Q: AsRef<Path>>(&self, src_path: P, dst_path: Q) -> Result<u32> {
228 let src_path = src_path.as_ref();
229 let dst_path = dst_path.as_ref();
230 let data = fs::read(src_path).map_err(Error::Io)?;
231
232 let file_type = self.detect_file_type(&data, src_path)?;
233 let output = self.apply_changes(&data, file_type)?;
234
235 let temp_path = dst_path.with_extension("exiftool_tmp");
237 fs::write(&temp_path, &output).map_err(Error::Io)?;
238 fs::rename(&temp_path, dst_path).map_err(Error::Io)?;
239
240 Ok(self.new_values.len() as u32)
241 }
242
243 fn apply_changes(&self, data: &[u8], file_type: FileType) -> Result<Vec<u8>> {
245 match file_type {
246 FileType::Jpeg => self.write_jpeg(data),
247 FileType::Png => self.write_png(data),
248 FileType::Tiff | FileType::Dng | FileType::Cr2 | FileType::Nef
249 | FileType::Arw | FileType::Orf | FileType::Pef => self.write_tiff(data),
250 FileType::WebP => self.write_webp(data),
251 FileType::Mp4 | FileType::QuickTime | FileType::M4a
252 | FileType::ThreeGP | FileType::F4v => self.write_mp4(data),
253 FileType::Psd => self.write_psd(data),
254 FileType::Pdf => self.write_pdf(data),
255 FileType::Heif | FileType::Avif => self.write_mp4(data),
256 FileType::Mkv | FileType::WebM => self.write_matroska(data),
257 FileType::Gif => {
258 let comment = self.new_values.iter()
259 .find(|nv| nv.tag.to_lowercase() == "comment")
260 .and_then(|nv| nv.value.clone());
261 crate::writer::gif_writer::write_gif(data, comment.as_deref())
262 }
263 FileType::Flac => {
264 let changes: Vec<(&str, &str)> = self.new_values.iter()
265 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
266 .collect();
267 crate::writer::flac_writer::write_flac(data, &changes)
268 }
269 FileType::Mp3 | FileType::Aiff => {
270 let changes: Vec<(&str, &str)> = self.new_values.iter()
271 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
272 .collect();
273 crate::writer::id3_writer::write_id3(data, &changes)
274 }
275 FileType::Jp2 | FileType::Jxl => {
276 let new_xmp = if self.new_values.iter().any(|nv| nv.group.as_deref() == Some("XMP")) {
277 let refs: Vec<&NewValue> = self.new_values.iter()
278 .filter(|nv| nv.group.as_deref() == Some("XMP"))
279 .collect();
280 Some(self.build_new_xmp(&refs))
281 } else { None };
282 crate::writer::jp2_writer::write_jp2(data, new_xmp.as_deref(), None)
283 }
284 FileType::PostScript => {
285 let changes: Vec<(&str, &str)> = self.new_values.iter()
286 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
287 .collect();
288 crate::writer::ps_writer::write_postscript(data, &changes)
289 }
290 FileType::Ogg | FileType::Opus => {
291 let changes: Vec<(&str, &str)> = self.new_values.iter()
292 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
293 .collect();
294 crate::writer::ogg_writer::write_ogg(data, &changes)
295 }
296 FileType::Xmp => {
297 let props: Vec<xmp_writer::XmpProperty> = self.new_values.iter()
298 .filter_map(|nv| {
299 let val = nv.value.as_deref()?;
300 Some(xmp_writer::XmpProperty {
301 namespace: nv.group.clone().unwrap_or_else(|| "dc".into()),
302 property: nv.tag.clone(),
303 values: vec![val.to_string()],
304 prop_type: xmp_writer::XmpPropertyType::Simple,
305 })
306 })
307 .collect();
308 Ok(crate::writer::xmp_sidecar_writer::write_xmp_sidecar(&props))
309 }
310 _ => Err(Error::UnsupportedFileType(format!("writing not yet supported for {}", file_type))),
311 }
312 }
313
314 fn write_jpeg(&self, data: &[u8]) -> Result<Vec<u8>> {
316 let mut exif_values: Vec<&NewValue> = Vec::new();
318 let mut xmp_values: Vec<&NewValue> = Vec::new();
319 let mut iptc_values: Vec<&NewValue> = Vec::new();
320 let mut comment_value: Option<&str> = None;
321 let mut remove_exif = false;
322 let mut remove_xmp = false;
323 let mut remove_iptc = false;
324 let mut remove_comment = false;
325
326 for nv in &self.new_values {
327 let group = nv.group.as_deref().unwrap_or("");
328 let group_upper = group.to_uppercase();
329
330 if nv.value.is_none() && nv.tag == "*" {
332 match group_upper.as_str() {
333 "EXIF" => { remove_exif = true; continue; }
334 "XMP" => { remove_xmp = true; continue; }
335 "IPTC" => { remove_iptc = true; continue; }
336 _ => {}
337 }
338 }
339
340 match group_upper.as_str() {
341 "XMP" => xmp_values.push(nv),
342 "IPTC" => iptc_values.push(nv),
343 "EXIF" | "IFD0" | "EXIFIFD" | "GPS" => exif_values.push(nv),
344 "" => {
345 if nv.tag.to_lowercase() == "comment" {
347 if nv.value.is_none() {
348 remove_comment = true;
349 } else {
350 comment_value = nv.value.as_deref();
351 }
352 } else if is_xmp_tag(&nv.tag) {
353 xmp_values.push(nv);
354 } else {
355 exif_values.push(nv);
356 }
357 }
358 _ => exif_values.push(nv), }
360 }
361
362 let new_exif = if !exif_values.is_empty() {
364 Some(self.build_new_exif(data, &exif_values)?)
365 } else {
366 None
367 };
368
369 let new_xmp = if !xmp_values.is_empty() {
371 Some(self.build_new_xmp(&xmp_values))
372 } else {
373 None
374 };
375
376 let new_iptc_data = if !iptc_values.is_empty() {
378 let records: Vec<iptc_writer::IptcRecord> = iptc_values
379 .iter()
380 .filter_map(|nv| {
381 let value = nv.value.as_deref()?;
382 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
383 Some(iptc_writer::IptcRecord {
384 record,
385 dataset,
386 data: value.as_bytes().to_vec(),
387 })
388 })
389 .collect();
390 if records.is_empty() {
391 None
392 } else {
393 Some(iptc_writer::build_iptc(&records))
394 }
395 } else {
396 None
397 };
398
399 jpeg_writer::write_jpeg(
401 data,
402 new_exif.as_deref(),
403 new_xmp.as_deref(),
404 new_iptc_data.as_deref(),
405 comment_value,
406 remove_exif,
407 remove_xmp,
408 remove_iptc,
409 remove_comment,
410 )
411 }
412
413 fn build_new_exif(&self, jpeg_data: &[u8], values: &[&NewValue]) -> Result<Vec<u8>> {
415 let bo = ByteOrderMark::BigEndian;
416 let mut ifd0_entries = Vec::new();
417 let mut exif_entries = Vec::new();
418 let mut gps_entries = Vec::new();
419
420 let existing = extract_existing_exif_entries(jpeg_data, bo);
422 for entry in &existing {
423 match classify_exif_tag(entry.tag) {
424 ExifIfdGroup::Ifd0 => ifd0_entries.push(entry.clone()),
425 ExifIfdGroup::ExifIfd => exif_entries.push(entry.clone()),
426 ExifIfdGroup::Gps => gps_entries.push(entry.clone()),
427 }
428 }
429
430 let deleted_tags: Vec<u16> = values
432 .iter()
433 .filter(|nv| nv.value.is_none())
434 .filter_map(|nv| tag_name_to_id(&nv.tag))
435 .collect();
436
437 ifd0_entries.retain(|e| !deleted_tags.contains(&e.tag));
439 exif_entries.retain(|e| !deleted_tags.contains(&e.tag));
440 gps_entries.retain(|e| !deleted_tags.contains(&e.tag));
441
442 for nv in values {
444 if nv.value.is_none() {
445 continue;
446 }
447 let value_str = nv.value.as_deref().unwrap_or("");
448 let group = nv.group.as_deref().unwrap_or("");
449
450 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, value_str, group, bo) {
451 let entry = exif_writer::IfdEntry {
452 tag: tag_id,
453 format,
454 data: encoded,
455 };
456
457 let target = match group.to_uppercase().as_str() {
458 "GPS" => &mut gps_entries,
459 "EXIFIFD" => &mut exif_entries,
460 _ => match classify_exif_tag(tag_id) {
461 ExifIfdGroup::ExifIfd => &mut exif_entries,
462 ExifIfdGroup::Gps => &mut gps_entries,
463 ExifIfdGroup::Ifd0 => &mut ifd0_entries,
464 },
465 };
466
467 if let Some(existing) = target.iter_mut().find(|e| e.tag == tag_id) {
469 *existing = entry;
470 } else {
471 target.push(entry);
472 }
473 }
474 }
475
476 ifd0_entries.retain(|e| e.tag != 0x8769 && e.tag != 0x8825 && e.tag != 0xA005);
478
479 exif_writer::build_exif(&ifd0_entries, &exif_entries, &gps_entries, bo)
480 }
481
482 fn write_png(&self, data: &[u8]) -> Result<Vec<u8>> {
484 let mut new_text: Vec<(&str, &str)> = Vec::new();
485 let mut remove_text: Vec<&str> = Vec::new();
486
487 let owned_pairs: Vec<(String, String)> = self.new_values.iter()
490 .filter(|nv| nv.value.is_some())
491 .map(|nv| (nv.tag.clone(), nv.value.clone().unwrap()))
492 .collect();
493
494 for (tag, value) in &owned_pairs {
495 new_text.push((tag.as_str(), value.as_str()));
496 }
497
498 for nv in &self.new_values {
499 if nv.value.is_none() {
500 remove_text.push(&nv.tag);
501 }
502 }
503
504 png_writer::write_png(data, &new_text, None, &remove_text)
505 }
506
507 fn write_psd(&self, data: &[u8]) -> Result<Vec<u8>> {
509 let mut iptc_values = Vec::new();
510 let mut xmp_values = Vec::new();
511
512 for nv in &self.new_values {
513 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
514 match group.as_str() {
515 "XMP" => xmp_values.push(nv),
516 "IPTC" => iptc_values.push(nv),
517 _ => {
518 if is_xmp_tag(&nv.tag) { xmp_values.push(nv); }
519 else { iptc_values.push(nv); }
520 }
521 }
522 }
523
524 let new_iptc = if !iptc_values.is_empty() {
525 let records: Vec<_> = iptc_values.iter().filter_map(|nv| {
526 let value = nv.value.as_deref()?;
527 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
528 Some(iptc_writer::IptcRecord { record, dataset, data: value.as_bytes().to_vec() })
529 }).collect();
530 if records.is_empty() { None } else { Some(iptc_writer::build_iptc(&records)) }
531 } else { None };
532
533 let new_xmp = if !xmp_values.is_empty() {
534 let refs: Vec<&NewValue> = xmp_values.iter().copied().collect();
535 Some(self.build_new_xmp(&refs))
536 } else { None };
537
538 psd_writer::write_psd(data, new_iptc.as_deref(), new_xmp.as_deref())
539 }
540
541 fn write_matroska(&self, data: &[u8]) -> Result<Vec<u8>> {
543 let changes: Vec<(&str, &str)> = self.new_values.iter()
544 .filter_map(|nv| {
545 let value = nv.value.as_deref()?;
546 Some((nv.tag.as_str(), value))
547 })
548 .collect();
549
550 matroska_writer::write_matroska(data, &changes)
551 }
552
553 fn write_pdf(&self, data: &[u8]) -> Result<Vec<u8>> {
555 let changes: Vec<(&str, &str)> = self.new_values.iter()
556 .filter_map(|nv| {
557 let value = nv.value.as_deref()?;
558 Some((nv.tag.as_str(), value))
559 })
560 .collect();
561
562 pdf_writer::write_pdf(data, &changes)
563 }
564
565 fn write_mp4(&self, data: &[u8]) -> Result<Vec<u8>> {
567 let mut ilst_tags: Vec<([u8; 4], String)> = Vec::new();
568 let mut xmp_values: Vec<&NewValue> = Vec::new();
569
570 for nv in &self.new_values {
571 if nv.value.is_none() { continue; }
572 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
573 if group == "XMP" {
574 xmp_values.push(nv);
575 } else if let Some(key) = mp4_writer::tag_to_ilst_key(&nv.tag) {
576 ilst_tags.push((key, nv.value.clone().unwrap()));
577 }
578 }
579
580 let tag_refs: Vec<(&[u8; 4], &str)> = ilst_tags.iter()
581 .map(|(k, v)| (k, v.as_str()))
582 .collect();
583
584 let new_xmp = if !xmp_values.is_empty() {
585 let refs: Vec<&NewValue> = xmp_values.iter().copied().collect();
586 Some(self.build_new_xmp(&refs))
587 } else {
588 None
589 };
590
591 mp4_writer::write_mp4(data, &tag_refs, new_xmp.as_deref())
592 }
593
594 fn write_webp(&self, data: &[u8]) -> Result<Vec<u8>> {
596 let mut exif_values: Vec<&NewValue> = Vec::new();
597 let mut xmp_values: Vec<&NewValue> = Vec::new();
598 let mut remove_exif = false;
599 let mut remove_xmp = false;
600
601 for nv in &self.new_values {
602 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
603 if nv.value.is_none() && nv.tag == "*" {
604 if group == "EXIF" { remove_exif = true; }
605 if group == "XMP" { remove_xmp = true; }
606 continue;
607 }
608 match group.as_str() {
609 "XMP" => xmp_values.push(nv),
610 _ => exif_values.push(nv),
611 }
612 }
613
614 let new_exif = if !exif_values.is_empty() {
615 let bo = ByteOrderMark::BigEndian;
616 let mut entries = Vec::new();
617 for nv in &exif_values {
618 if let Some(ref v) = nv.value {
619 let group = nv.group.as_deref().unwrap_or("");
620 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, v, group, bo) {
621 entries.push(exif_writer::IfdEntry { tag: tag_id, format, data: encoded });
622 }
623 }
624 }
625 if !entries.is_empty() {
626 Some(exif_writer::build_exif(&entries, &[], &[], bo)?)
627 } else {
628 None
629 }
630 } else {
631 None
632 };
633
634 let new_xmp = if !xmp_values.is_empty() {
635 Some(self.build_new_xmp(&xmp_values.iter().map(|v| *v).collect::<Vec<_>>()))
636 } else {
637 None
638 };
639
640 webp_writer::write_webp(
641 data,
642 new_exif.as_deref(),
643 new_xmp.as_deref(),
644 remove_exif,
645 remove_xmp,
646 )
647 }
648
649 fn write_tiff(&self, data: &[u8]) -> Result<Vec<u8>> {
651 let bo = if data.starts_with(b"II") {
652 ByteOrderMark::LittleEndian
653 } else {
654 ByteOrderMark::BigEndian
655 };
656
657 let mut changes: Vec<(u16, Vec<u8>)> = Vec::new();
658 for nv in &self.new_values {
659 if let Some(ref value) = nv.value {
660 let group = nv.group.as_deref().unwrap_or("");
661 if let Some((tag_id, _format, encoded)) = encode_exif_tag(&nv.tag, value, group, bo) {
662 changes.push((tag_id, encoded));
663 }
664 }
665 }
666
667 tiff_writer::write_tiff(data, &changes)
668 }
669
670 fn build_new_xmp(&self, values: &[&NewValue]) -> Vec<u8> {
672 let mut properties = Vec::new();
673
674 for nv in values {
675 let value_str = match &nv.value {
676 Some(v) => v.clone(),
677 None => continue,
678 };
679
680 let ns = nv.group.as_deref().unwrap_or("dc").to_lowercase();
681 let ns = if ns == "xmp" { "xmp".to_string() } else { ns };
682
683 let prop_type = match nv.tag.to_lowercase().as_str() {
684 "title" | "description" | "rights" => xmp_writer::XmpPropertyType::LangAlt,
685 "subject" | "keywords" => xmp_writer::XmpPropertyType::Bag,
686 "creator" => xmp_writer::XmpPropertyType::Seq,
687 _ => xmp_writer::XmpPropertyType::Simple,
688 };
689
690 let values = if matches!(prop_type, xmp_writer::XmpPropertyType::Bag | xmp_writer::XmpPropertyType::Seq) {
691 value_str.split(',').map(|s| s.trim().to_string()).collect()
692 } else {
693 vec![value_str]
694 };
695
696 properties.push(xmp_writer::XmpProperty {
697 namespace: ns,
698 property: nv.tag.clone(),
699 values,
700 prop_type,
701 });
702 }
703
704 xmp_writer::build_xmp(&properties).into_bytes()
705 }
706
707 pub fn image_info<P: AsRef<Path>>(&self, path: P) -> Result<ImageInfo> {
715 let tags = self.extract_info(path)?;
716 Ok(self.get_info(&tags))
717 }
718
719 pub fn extract_info<P: AsRef<Path>>(&self, path: P) -> Result<Vec<Tag>> {
723 let path = path.as_ref();
724 let data = fs::read(path).map_err(Error::Io)?;
725
726 self.extract_info_from_bytes(&data, path)
727 }
728
729 pub fn extract_info_from_bytes(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
731 let file_type_result = self.detect_file_type(data, path);
732 let (file_type, mut tags) = match file_type_result {
733 Ok(ft) => {
734 let t = self.process_file(data, ft).or_else(|_| {
735 self.process_by_extension(data, path)
736 })?;
737 (Some(ft), t)
738 }
739 Err(_) => {
740 let t = self.process_by_extension(data, path)?;
742 (None, t)
743 }
744 };
745 let file_type = file_type.unwrap_or(FileType::Zip); tags.push(Tag {
749 id: crate::tag::TagId::Text("FileType".into()),
750 name: "FileType".into(),
751 description: "File Type".into(),
752 group: crate::tag::TagGroup {
753 family0: "File".into(),
754 family1: "File".into(),
755 family2: "Other".into(),
756 },
757 raw_value: Value::String(format!("{:?}", file_type)),
758 print_value: file_type.description().to_string(),
759 priority: 0,
760 });
761
762 tags.push(Tag {
763 id: crate::tag::TagId::Text("MIMEType".into()),
764 name: "MIMEType".into(),
765 description: "MIME Type".into(),
766 group: crate::tag::TagGroup {
767 family0: "File".into(),
768 family1: "File".into(),
769 family2: "Other".into(),
770 },
771 raw_value: Value::String(file_type.mime_type().to_string()),
772 print_value: file_type.mime_type().to_string(),
773 priority: 0,
774 });
775
776 if let Ok(metadata) = fs::metadata(path) {
777 tags.push(Tag {
778 id: crate::tag::TagId::Text("FileSize".into()),
779 name: "FileSize".into(),
780 description: "File Size".into(),
781 group: crate::tag::TagGroup {
782 family0: "File".into(),
783 family1: "File".into(),
784 family2: "Other".into(),
785 },
786 raw_value: Value::U32(metadata.len() as u32),
787 print_value: format_file_size(metadata.len()),
788 priority: 0,
789 });
790 }
791
792 let file_tag = |name: &str, val: Value| -> Tag {
794 Tag {
795 id: crate::tag::TagId::Text(name.to_string()),
796 name: name.to_string(), description: name.to_string(),
797 group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Other".into() },
798 raw_value: val.clone(), print_value: val.to_display_string(), priority: 0,
799 }
800 };
801
802 if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
803 tags.push(file_tag("FileName", Value::String(fname.to_string())));
804 }
805 if let Some(dir) = path.parent().and_then(|p| p.to_str()) {
806 tags.push(file_tag("Directory", Value::String(dir.to_string())));
807 }
808 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
809 tags.push(file_tag("FileTypeExtension", Value::String(ext.to_lowercase())));
810 }
811
812 #[cfg(unix)]
813 if let Ok(metadata) = fs::metadata(path) {
814 use std::os::unix::fs::MetadataExt;
815 let mode = metadata.mode();
816 tags.push(file_tag("FilePermissions", Value::String(format!("{:o}", mode & 0o7777))));
817
818 if let Ok(modified) = metadata.modified() {
820 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
821 let secs = dur.as_secs() as i64;
822 tags.push(file_tag("FileModifyDate", Value::String(unix_to_datetime(secs))));
823 }
824 }
825 if let Ok(accessed) = metadata.accessed() {
827 if let Ok(dur) = accessed.duration_since(std::time::UNIX_EPOCH) {
828 let secs = dur.as_secs() as i64;
829 tags.push(file_tag("FileAccessDate", Value::String(unix_to_datetime(secs))));
830 }
831 }
832 let ctime = metadata.ctime();
834 if ctime > 0 {
835 tags.push(file_tag("FileInodeChangeDate", Value::String(unix_to_datetime(ctime))));
836 }
837 }
838
839 if file_type == FileType::Jpeg || file_type == FileType::Tiff {
841 let bo_str = if data.len() > 8 {
842 let check = if data.starts_with(&[0xFF, 0xD8]) {
844 data.windows(6).position(|w| w == b"Exif\0\0")
846 .map(|p| &data[p+6..])
847 } else {
848 Some(&data[..])
849 };
850 if let Some(tiff) = check {
851 if tiff.starts_with(b"II") { "Little-endian (Intel, II)" }
852 else if tiff.starts_with(b"MM") { "Big-endian (Motorola, MM)" }
853 else { "" }
854 } else { "" }
855 } else { "" };
856 if !bo_str.is_empty() {
857 tags.push(file_tag("ExifByteOrder", Value::String(bo_str.to_string())));
858 }
859 }
860
861 tags.push(file_tag("ExifToolVersion", Value::String(crate::VERSION.to_string())));
862
863 let composite = crate::composite::compute_composite_tags(&tags);
865 tags.extend(composite);
866
867 {
873 let is_flir_fff = tags.iter().any(|t| t.group.family0 == "APP1"
874 && t.group.family1 == "FLIR");
875 if is_flir_fff {
876 tags.retain(|t| !(t.name == "LensID" && t.group.family0 == "Composite"));
877 }
878 }
879
880 {
885 let make = tags.iter().find(|t| t.name == "Make")
886 .map(|t| t.print_value.clone()).unwrap_or_default();
887 if !make.to_uppercase().contains("CANON") {
888 tags.retain(|t| t.name != "Lens" || t.group.family0 != "Composite");
889 }
890 }
891
892 if !self.options.requested_tags.is_empty() {
894 let requested: Vec<String> = self
895 .options
896 .requested_tags
897 .iter()
898 .map(|t| t.to_lowercase())
899 .collect();
900 tags.retain(|t| requested.contains(&t.name.to_lowercase()));
901 }
902
903 Ok(tags)
904 }
905
906 fn get_info(&self, tags: &[Tag]) -> ImageInfo {
910 let mut info = ImageInfo::new();
911 let mut seen: HashMap<String, usize> = HashMap::new();
912
913 for tag in tags {
914 let value = if self.options.print_conv {
915 &tag.print_value
916 } else {
917 &tag.raw_value.to_display_string()
918 };
919
920 let count = seen.entry(tag.name.clone()).or_insert(0);
921 *count += 1;
922
923 if *count == 1 {
924 info.insert(tag.name.clone(), value.clone());
925 } else if self.options.duplicates {
926 let key = format!("{} [{}:{}]", tag.name, tag.group.family0, tag.group.family1);
927 info.insert(key, value.clone());
928 }
929 }
930
931 info
932 }
933
934 fn detect_file_type(&self, data: &[u8], path: &Path) -> Result<FileType> {
936 let header_len = data.len().min(256);
938 if let Some(ft) = file_type::detect_from_magic(&data[..header_len]) {
939 return Ok(ft);
940 }
941
942 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
944 if let Some(ft) = file_type::detect_from_extension(ext) {
945 return Ok(ft);
946 }
947 }
948
949 let ext_str = path
950 .extension()
951 .and_then(|e| e.to_str())
952 .unwrap_or("unknown");
953 Err(Error::UnsupportedFileType(ext_str.to_string()))
954 }
955
956 fn process_file(&self, data: &[u8], file_type: FileType) -> Result<Vec<Tag>> {
958 match file_type {
959 FileType::Jpeg => formats::jpeg::read_jpeg(data),
960 FileType::Png | FileType::Mng => formats::png::read_png(data),
961 FileType::Tiff
963 | FileType::Btf
964 | FileType::Dng
965 | FileType::Cr2
966 | FileType::Nef
967 | FileType::Arw
968 | FileType::Sr2
969 | FileType::Orf
970 | FileType::Pef
971 | FileType::Erf
972 | FileType::Fff
973 | FileType::Iiq
974 | FileType::Rwl
975 | FileType::Mef
976 | FileType::Srw
977 | FileType::Gpr
978 | FileType::Arq
979 | FileType::ThreeFR
980 | FileType::Dcr
981 | FileType::Rw2
982 | FileType::Srf => formats::tiff::read_tiff(data),
983 FileType::Gif => formats::gif::read_gif(data),
985 FileType::Bmp => formats::bmp::read_bmp(data),
986 FileType::WebP | FileType::Avi | FileType::Wav => formats::riff::read_riff(data),
987 FileType::Psd => formats::psd::read_psd(data),
988 FileType::Mp3 => formats::id3::read_mp3(data),
990 FileType::Flac => formats::flac::read_flac(data),
991 FileType::Ogg | FileType::Opus => formats::ogg::read_ogg(data),
992 FileType::Aiff => formats::aiff::read_aiff(data),
993 FileType::Mp4
995 | FileType::QuickTime
996 | FileType::M4a
997 | FileType::ThreeGP
998 | FileType::Heif
999 | FileType::Avif
1000 | FileType::Cr3
1001 | FileType::F4v
1002 | FileType::Mqv
1003 | FileType::Lrv => formats::quicktime::read_quicktime(data),
1004 FileType::Mkv | FileType::WebM => formats::matroska::read_matroska(data),
1005 FileType::Asf | FileType::Wmv | FileType::Wma => formats::asf::read_asf(data),
1006 FileType::Crw => formats::canon_raw::read_crw(data),
1008 FileType::Raf => formats::raf::read_raf(data),
1009 FileType::Mrw => formats::mrw::read_mrw(data),
1010 FileType::Jp2 | FileType::J2c => formats::jp2::read_jp2(data),
1012 FileType::Jxl => formats::jp2::read_jxl(data),
1013 FileType::Ico => formats::ico::read_ico(data),
1014 FileType::Icc => formats::icc::read_icc(data),
1015 FileType::Pdf => formats::pdf::read_pdf(data),
1017 FileType::PostScript => formats::postscript::read_postscript(data),
1018 FileType::Zip | FileType::Docx | FileType::Xlsx | FileType::Pptx
1019 | FileType::Doc | FileType::Xls | FileType::Ppt => formats::zip::read_zip(data),
1020 FileType::Rtf => formats::rtf::read_rtf(data),
1021 FileType::Xmp => formats::xmp_file::read_xmp(data),
1023 FileType::Html => {
1024 let is_svg = data.windows(4).take(512).any(|w| w == b"<svg");
1026 if is_svg {
1027 formats::misc::read_svg(data)
1028 } else {
1029 formats::html::read_html(data)
1030 }
1031 }
1032 FileType::Exe => formats::exe::read_exe(data),
1033 FileType::Font => formats::font::read_font(data),
1034 FileType::Aac | FileType::Ape | FileType::Mpc | FileType::Audible
1036 | FileType::WavPack | FileType::Dsf => formats::id3::read_mp3(data),
1037 FileType::RealAudio | FileType::RealMedia => {
1038 formats::id3::read_mp3(data).or_else(|_| Ok(Vec::new()))
1040 }
1041 FileType::Dicom => formats::misc::read_dicom(data),
1043 FileType::Fits => formats::misc::read_fits(data),
1044 FileType::Flv => formats::misc::read_flv(data),
1045 FileType::Swf => formats::misc::read_swf(data),
1046 FileType::Hdr => formats::misc::read_hdr(data),
1047 FileType::DjVu => formats::misc::read_djvu(data),
1048 FileType::Flif => formats::misc::read_flif(data),
1049 FileType::Bpg => formats::misc::read_bpg(data),
1050 FileType::Pcx => formats::misc::read_pcx(data),
1051 FileType::Pict => formats::misc::read_pict(data),
1052 FileType::M2ts => formats::misc::read_m2ts(data),
1053 FileType::Gzip => formats::misc::read_gzip(data),
1054 FileType::Rar => formats::misc::read_rar(data),
1055 _ => Err(Error::UnsupportedFileType(format!("{}", file_type))),
1056 }
1057 }
1058
1059 fn process_by_extension(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
1061 let ext = path
1062 .extension()
1063 .and_then(|e| e.to_str())
1064 .unwrap_or("")
1065 .to_ascii_lowercase();
1066
1067 match ext.as_str() {
1068 "ppm" | "pgm" | "pbm" => formats::misc::read_ppm(data),
1069 "pfm" => {
1070 if data.len() >= 3 && data[0] == b'P' && (data[1] == b'f' || data[1] == b'F') {
1072 formats::misc::read_ppm(data)
1073 } else {
1074 Ok(Vec::new()) }
1076 }
1077 "json" => formats::misc::read_json(data),
1078 "svg" => formats::misc::read_svg(data),
1079 "txt" | "csv" | "log" | "igc" | "url" | "lnk" | "ram" => {
1080 Ok(Vec::new()) }
1082 "gpx" | "kml" | "xml" | "inx" => formats::xmp_file::read_xmp(data),
1083 "plist" | "aae" => {
1084 if data.starts_with(b"<?xml") || data.starts_with(b"bplist") {
1086 formats::xmp_file::read_xmp(data).or_else(|_| Ok(Vec::new()))
1087 } else {
1088 Ok(Vec::new())
1089 }
1090 }
1091 "vcf" | "ics" | "vcard" => Ok(Vec::new()), "xcf" => Ok(Vec::new()), "vrd" | "dr4" => Ok(Vec::new()), "indd" | "indt" => Ok(Vec::new()), "x3f" => Ok(Vec::new()), "mie" => Ok(Vec::new()), "exr" => Ok(Vec::new()), "dpx" | "dv" | "fpf" | "lfp" | "miff" | "moi" | "mrc"
1099 | "dss" | "mobi" | "pcapng" | "psp" | "pgf" | "raw"
1100 | "r3d" | "pmp" | "tnef" | "torrent" | "wpg" | "wtv"
1101 | "xisf" | "czi" | "iso" | "itc" | "macos" | "mxf"
1102 | "afm" | "pfb" | "ppt" | "dfont" => Ok(Vec::new()),
1103 _ => Err(Error::UnsupportedFileType(ext)),
1104 }
1105 }
1106}
1107
1108impl Default for ExifTool {
1109 fn default() -> Self {
1110 Self::new()
1111 }
1112}
1113
1114pub fn get_file_type<P: AsRef<Path>>(path: P) -> Result<FileType> {
1116 let path = path.as_ref();
1117 let mut file = fs::File::open(path).map_err(Error::Io)?;
1118 let mut header = [0u8; 256];
1119 use std::io::Read;
1120 let n = file.read(&mut header).map_err(Error::Io)?;
1121
1122 if let Some(ft) = file_type::detect_from_magic(&header[..n]) {
1123 return Ok(ft);
1124 }
1125
1126 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1127 if let Some(ft) = file_type::detect_from_extension(ext) {
1128 return Ok(ft);
1129 }
1130 }
1131
1132 Err(Error::UnsupportedFileType("unknown".into()))
1133}
1134
1135enum ExifIfdGroup {
1137 Ifd0,
1138 ExifIfd,
1139 Gps,
1140}
1141
1142fn classify_exif_tag(tag_id: u16) -> ExifIfdGroup {
1144 match tag_id {
1145 0x829A..=0x829D | 0x8822..=0x8827 | 0x8830 | 0x9000..=0x9292
1147 | 0xA000..=0xA435 => ExifIfdGroup::ExifIfd,
1148 0x0000..=0x001F if tag_id <= 0x001F => ExifIfdGroup::Gps,
1150 _ => ExifIfdGroup::Ifd0,
1152 }
1153}
1154
1155fn extract_existing_exif_entries(jpeg_data: &[u8], target_bo: ByteOrderMark) -> Vec<exif_writer::IfdEntry> {
1157 let mut entries = Vec::new();
1158
1159 let mut pos = 2; while pos + 4 <= jpeg_data.len() {
1162 if jpeg_data[pos] != 0xFF {
1163 pos += 1;
1164 continue;
1165 }
1166 let marker = jpeg_data[pos + 1];
1167 pos += 2;
1168
1169 if marker == 0xDA || marker == 0xD9 {
1170 break; }
1172 if marker == 0xFF || marker == 0x00 || marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
1173 continue;
1174 }
1175
1176 if pos + 2 > jpeg_data.len() {
1177 break;
1178 }
1179 let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
1180 if seg_len < 2 || pos + seg_len > jpeg_data.len() {
1181 break;
1182 }
1183
1184 let seg_data = &jpeg_data[pos + 2..pos + seg_len];
1185
1186 if marker == 0xE1 && seg_data.len() > 14 && seg_data.starts_with(b"Exif\0\0") {
1188 let tiff_data = &seg_data[6..];
1189 extract_ifd_entries(tiff_data, target_bo, &mut entries);
1190 break;
1191 }
1192
1193 pos += seg_len;
1194 }
1195
1196 entries
1197}
1198
1199fn extract_ifd_entries(
1201 tiff_data: &[u8],
1202 target_bo: ByteOrderMark,
1203 entries: &mut Vec<exif_writer::IfdEntry>,
1204) {
1205 use crate::metadata::exif::parse_tiff_header;
1206
1207 let header = match parse_tiff_header(tiff_data) {
1208 Ok(h) => h,
1209 Err(_) => return,
1210 };
1211
1212 let src_bo = header.byte_order;
1213
1214 read_ifd_for_merge(tiff_data, header.ifd0_offset as usize, src_bo, target_bo, entries);
1216
1217 let ifd0_offset = header.ifd0_offset as usize;
1219 if ifd0_offset + 2 > tiff_data.len() {
1220 return;
1221 }
1222 let count = read_u16_bo(tiff_data, ifd0_offset, src_bo) as usize;
1223 for i in 0..count {
1224 let eoff = ifd0_offset + 2 + i * 12;
1225 if eoff + 12 > tiff_data.len() {
1226 break;
1227 }
1228 let tag = read_u16_bo(tiff_data, eoff, src_bo);
1229 let value_off = read_u32_bo(tiff_data, eoff + 8, src_bo) as usize;
1230
1231 match tag {
1232 0x8769 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
1233 0x8825 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
1234 _ => {}
1235 }
1236 }
1237}
1238
1239fn read_ifd_for_merge(
1241 data: &[u8],
1242 offset: usize,
1243 src_bo: ByteOrderMark,
1244 target_bo: ByteOrderMark,
1245 entries: &mut Vec<exif_writer::IfdEntry>,
1246) {
1247 if offset + 2 > data.len() {
1248 return;
1249 }
1250 let count = read_u16_bo(data, offset, src_bo) as usize;
1251
1252 for i in 0..count {
1253 let eoff = offset + 2 + i * 12;
1254 if eoff + 12 > data.len() {
1255 break;
1256 }
1257
1258 let tag = read_u16_bo(data, eoff, src_bo);
1259 let dtype = read_u16_bo(data, eoff + 2, src_bo);
1260 let count_val = read_u32_bo(data, eoff + 4, src_bo);
1261
1262 if tag == 0x8769 || tag == 0x8825 || tag == 0xA005 || tag == 0x927C {
1264 continue;
1265 }
1266
1267 let type_size = match dtype {
1268 1 | 2 | 6 | 7 => 1usize,
1269 3 | 8 => 2,
1270 4 | 9 | 11 | 13 => 4,
1271 5 | 10 | 12 => 8,
1272 _ => continue,
1273 };
1274
1275 let total_size = type_size * count_val as usize;
1276 let raw_data = if total_size <= 4 {
1277 data[eoff + 8..eoff + 12].to_vec()
1278 } else {
1279 let voff = read_u32_bo(data, eoff + 8, src_bo) as usize;
1280 if voff + total_size > data.len() {
1281 continue;
1282 }
1283 data[voff..voff + total_size].to_vec()
1284 };
1285
1286 let final_data = if src_bo != target_bo && type_size > 1 {
1288 reencode_bytes(&raw_data, dtype, count_val as usize, src_bo, target_bo)
1289 } else {
1290 raw_data[..total_size].to_vec()
1291 };
1292
1293 let format = match dtype {
1294 1 => exif_writer::ExifFormat::Byte,
1295 2 => exif_writer::ExifFormat::Ascii,
1296 3 => exif_writer::ExifFormat::Short,
1297 4 => exif_writer::ExifFormat::Long,
1298 5 => exif_writer::ExifFormat::Rational,
1299 6 => exif_writer::ExifFormat::SByte,
1300 7 => exif_writer::ExifFormat::Undefined,
1301 8 => exif_writer::ExifFormat::SShort,
1302 9 => exif_writer::ExifFormat::SLong,
1303 10 => exif_writer::ExifFormat::SRational,
1304 11 => exif_writer::ExifFormat::Float,
1305 12 => exif_writer::ExifFormat::Double,
1306 _ => continue,
1307 };
1308
1309 entries.push(exif_writer::IfdEntry {
1310 tag,
1311 format,
1312 data: final_data,
1313 });
1314 }
1315}
1316
1317fn reencode_bytes(
1319 data: &[u8],
1320 dtype: u16,
1321 count: usize,
1322 src_bo: ByteOrderMark,
1323 dst_bo: ByteOrderMark,
1324) -> Vec<u8> {
1325 let mut out = Vec::with_capacity(data.len());
1326 match dtype {
1327 3 | 8 => {
1328 for i in 0..count {
1330 let v = read_u16_bo(data, i * 2, src_bo);
1331 match dst_bo {
1332 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
1333 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
1334 }
1335 }
1336 }
1337 4 | 9 | 11 | 13 => {
1338 for i in 0..count {
1340 let v = read_u32_bo(data, i * 4, src_bo);
1341 match dst_bo {
1342 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
1343 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
1344 }
1345 }
1346 }
1347 5 | 10 => {
1348 for i in 0..count {
1350 let n = read_u32_bo(data, i * 8, src_bo);
1351 let d = read_u32_bo(data, i * 8 + 4, src_bo);
1352 match dst_bo {
1353 ByteOrderMark::LittleEndian => {
1354 out.extend_from_slice(&n.to_le_bytes());
1355 out.extend_from_slice(&d.to_le_bytes());
1356 }
1357 ByteOrderMark::BigEndian => {
1358 out.extend_from_slice(&n.to_be_bytes());
1359 out.extend_from_slice(&d.to_be_bytes());
1360 }
1361 }
1362 }
1363 }
1364 12 => {
1365 for i in 0..count {
1367 let mut bytes = [0u8; 8];
1368 bytes.copy_from_slice(&data[i * 8..i * 8 + 8]);
1369 if src_bo != dst_bo {
1370 bytes.reverse();
1371 }
1372 out.extend_from_slice(&bytes);
1373 }
1374 }
1375 _ => out.extend_from_slice(data),
1376 }
1377 out
1378}
1379
1380fn read_u16_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1381 if offset + 2 > data.len() { return 0; }
1382 match bo {
1383 ByteOrderMark::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
1384 ByteOrderMark::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
1385 }
1386}
1387
1388fn read_u32_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1389 if offset + 4 > data.len() { return 0; }
1390 match bo {
1391 ByteOrderMark::LittleEndian => u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]),
1392 ByteOrderMark::BigEndian => u32::from_be_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]),
1393 }
1394}
1395
1396fn tag_name_to_id(name: &str) -> Option<u16> {
1398 encode_exif_tag(name, "", "", ByteOrderMark::BigEndian).map(|(id, _, _)| id)
1399}
1400
1401fn value_to_filename(value: &str) -> String {
1403 value
1404 .chars()
1405 .map(|c| match c {
1406 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1407 c if c.is_control() => '_',
1408 c => c,
1409 })
1410 .collect::<String>()
1411 .trim()
1412 .to_string()
1413}
1414
1415pub fn parse_date_shift(shift: &str) -> Option<(i32, u32, u32, u32)> {
1418 let (sign, rest) = if shift.starts_with('-') {
1419 (-1, &shift[1..])
1420 } else if shift.starts_with('+') {
1421 (1, &shift[1..])
1422 } else {
1423 (1, shift)
1424 };
1425
1426 let parts: Vec<&str> = rest.split(':').collect();
1427 match parts.len() {
1428 1 => {
1429 let h: u32 = parts[0].parse().ok()?;
1430 Some((sign, h, 0, 0))
1431 }
1432 2 => {
1433 let h: u32 = parts[0].parse().ok()?;
1434 let m: u32 = parts[1].parse().ok()?;
1435 Some((sign, h, m, 0))
1436 }
1437 3 => {
1438 let h: u32 = parts[0].parse().ok()?;
1439 let m: u32 = parts[1].parse().ok()?;
1440 let s: u32 = parts[2].parse().ok()?;
1441 Some((sign, h, m, s))
1442 }
1443 _ => None,
1444 }
1445}
1446
1447pub fn shift_datetime(datetime: &str, shift: &str) -> Option<String> {
1450 let (sign, hours, minutes, seconds) = parse_date_shift(shift)?;
1451
1452 if datetime.len() < 19 {
1454 return None;
1455 }
1456 let year: i32 = datetime[0..4].parse().ok()?;
1457 let month: u32 = datetime[5..7].parse().ok()?;
1458 let day: u32 = datetime[8..10].parse().ok()?;
1459 let hour: u32 = datetime[11..13].parse().ok()?;
1460 let min: u32 = datetime[14..16].parse().ok()?;
1461 let sec: u32 = datetime[17..19].parse().ok()?;
1462
1463 let total_secs = (hour * 3600 + min * 60 + sec) as i64
1465 + sign as i64 * (hours * 3600 + minutes * 60 + seconds) as i64;
1466
1467 let days_shift = if total_secs < 0 {
1468 -1 - (-total_secs - 1) as i64 / 86400
1469 } else {
1470 total_secs / 86400
1471 };
1472
1473 let time_secs = ((total_secs % 86400) + 86400) % 86400;
1474 let new_hour = (time_secs / 3600) as u32;
1475 let new_min = ((time_secs % 3600) / 60) as u32;
1476 let new_sec = (time_secs % 60) as u32;
1477
1478 let mut new_day = day as i32 + days_shift as i32;
1480 let mut new_month = month;
1481 let mut new_year = year;
1482
1483 let days_in_month = |m: u32, y: i32| -> i32 {
1484 match m {
1485 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1486 4 | 6 | 9 | 11 => 30,
1487 2 => if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 29 } else { 28 },
1488 _ => 30,
1489 }
1490 };
1491
1492 while new_day > days_in_month(new_month, new_year) {
1493 new_day -= days_in_month(new_month, new_year);
1494 new_month += 1;
1495 if new_month > 12 {
1496 new_month = 1;
1497 new_year += 1;
1498 }
1499 }
1500 while new_day < 1 {
1501 new_month = if new_month == 1 { 12 } else { new_month - 1 };
1502 if new_month == 12 {
1503 new_year -= 1;
1504 }
1505 new_day += days_in_month(new_month, new_year);
1506 }
1507
1508 Some(format!(
1509 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
1510 new_year, new_month, new_day, new_hour, new_min, new_sec
1511 ))
1512}
1513
1514fn unix_to_datetime(secs: i64) -> String {
1515 let days = secs / 86400;
1516 let time = secs % 86400;
1517 let h = time / 3600;
1518 let m = (time % 3600) / 60;
1519 let s = time % 60;
1520 let mut y = 1970i32;
1521 let mut rem = days;
1522 loop {
1523 let dy = if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 366 } else { 365 };
1524 if rem < dy { break; }
1525 rem -= dy;
1526 y += 1;
1527 }
1528 let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
1529 let months = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1530 let mut mo = 1;
1531 for &dm in &months {
1532 if rem < dm { break; }
1533 rem -= dm;
1534 mo += 1;
1535 }
1536 format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", y, mo, rem + 1, h, m, s)
1537}
1538
1539fn format_file_size(bytes: u64) -> String {
1540 if bytes < 1024 {
1541 format!("{} bytes", bytes)
1542 } else if bytes < 1024 * 1024 {
1543 format!("{:.1} kB", bytes as f64 / 1024.0)
1544 } else if bytes < 1024 * 1024 * 1024 {
1545 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
1546 } else {
1547 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
1548 }
1549}
1550
1551fn is_xmp_tag(tag: &str) -> bool {
1553 matches!(
1554 tag.to_lowercase().as_str(),
1555 "title" | "description" | "subject" | "creator" | "rights"
1556 | "keywords" | "rating" | "label" | "hierarchicalsubject"
1557 )
1558}
1559
1560fn encode_exif_tag(
1563 tag_name: &str,
1564 value: &str,
1565 _group: &str,
1566 bo: ByteOrderMark,
1567) -> Option<(u16, exif_writer::ExifFormat, Vec<u8>)> {
1568 let tag_lower = tag_name.to_lowercase();
1569
1570 let (tag_id, format): (u16, exif_writer::ExifFormat) = match tag_lower.as_str() {
1572 "imagedescription" => (0x010E, exif_writer::ExifFormat::Ascii),
1574 "make" => (0x010F, exif_writer::ExifFormat::Ascii),
1575 "model" => (0x0110, exif_writer::ExifFormat::Ascii),
1576 "software" => (0x0131, exif_writer::ExifFormat::Ascii),
1577 "modifydate" | "datetime" => (0x0132, exif_writer::ExifFormat::Ascii),
1578 "artist" => (0x013B, exif_writer::ExifFormat::Ascii),
1579 "copyright" => (0x8298, exif_writer::ExifFormat::Ascii),
1580 "orientation" => (0x0112, exif_writer::ExifFormat::Short),
1582 "xresolution" => (0x011A, exif_writer::ExifFormat::Rational),
1583 "yresolution" => (0x011B, exif_writer::ExifFormat::Rational),
1584 "resolutionunit" => (0x0128, exif_writer::ExifFormat::Short),
1585 "datetimeoriginal" => (0x9003, exif_writer::ExifFormat::Ascii),
1587 "createdate" | "datetimedigitized" => (0x9004, exif_writer::ExifFormat::Ascii),
1588 "usercomment" => (0x9286, exif_writer::ExifFormat::Undefined),
1589 "imageuniqueid" => (0xA420, exif_writer::ExifFormat::Ascii),
1590 "ownername" | "cameraownername" => (0xA430, exif_writer::ExifFormat::Ascii),
1591 "serialnumber" | "bodyserialnumber" => (0xA431, exif_writer::ExifFormat::Ascii),
1592 "lensmake" => (0xA433, exif_writer::ExifFormat::Ascii),
1593 "lensmodel" => (0xA434, exif_writer::ExifFormat::Ascii),
1594 "lensserialnumber" => (0xA435, exif_writer::ExifFormat::Ascii),
1595 _ => return None,
1596 };
1597
1598 let encoded = match format {
1599 exif_writer::ExifFormat::Ascii => exif_writer::encode_ascii(value),
1600 exif_writer::ExifFormat::Short => {
1601 let v: u16 = value.parse().ok()?;
1602 exif_writer::encode_u16(v, bo)
1603 }
1604 exif_writer::ExifFormat::Long => {
1605 let v: u32 = value.parse().ok()?;
1606 exif_writer::encode_u32(v, bo)
1607 }
1608 exif_writer::ExifFormat::Rational => {
1609 if let Some(slash) = value.find('/') {
1611 let num: u32 = value[..slash].trim().parse().ok()?;
1612 let den: u32 = value[slash + 1..].trim().parse().ok()?;
1613 exif_writer::encode_urational(num, den, bo)
1614 } else if let Ok(v) = value.parse::<f64>() {
1615 let den = 10000u32;
1617 let num = (v * den as f64).round() as u32;
1618 exif_writer::encode_urational(num, den, bo)
1619 } else {
1620 return None;
1621 }
1622 }
1623 exif_writer::ExifFormat::Undefined => {
1624 let mut data = vec![0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00]; data.extend_from_slice(value.as_bytes());
1627 data
1628 }
1629 _ => return None,
1630 };
1631
1632 Some((tag_id, format, encoded))
1633}