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