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, TagGroup, MAIN_DOCUMENT};
15use crate::value::Value;
16use crate::writer::{
17 exif_writer, iptc_writer, jpeg_writer, matroska_writer, mp4_writer, pdf_writer, png_writer,
18 psd_writer, tiff_writer, webp_writer, xmp_writer,
19};
20
21#[derive(Debug, Clone)]
23pub struct Options {
24 pub duplicates: bool,
26 pub print_conv: bool,
28 pub fast_scan: u8,
30 pub requested_tags: Vec<String>,
32 pub extract_embedded: u8,
34 pub show_unknown: u8,
36 pub process_compressed: bool,
38 pub use_mwg: bool,
40 pub geolocation: bool,
43}
44
45impl Default for Options {
46 fn default() -> Self {
47 Self {
48 duplicates: false,
49 print_conv: true,
50 fast_scan: 0,
51 requested_tags: Vec::new(),
52 extract_embedded: 0,
53 show_unknown: 0,
54 process_compressed: false,
55 use_mwg: false,
56 geolocation: false,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
75pub struct NewValue {
76 pub tag: String,
78 pub group: Option<String>,
80 pub value: Option<String>,
82}
83
84pub struct ExifTool {
113 options: Options,
114 new_values: Vec<NewValue>,
115}
116
117pub type ImageInfo = HashMap<String, String>;
119
120impl ExifTool {
121 pub fn new() -> Self {
123 Self {
124 options: Options::default(),
125 new_values: Vec::new(),
126 }
127 }
128
129 pub fn with_options(options: Options) -> Self {
131 Self {
132 options,
133 new_values: Vec::new(),
134 }
135 }
136
137 pub fn options_mut(&mut self) -> &mut Options {
139 &mut self.options
140 }
141
142 pub fn options(&self) -> &Options {
144 &self.options
145 }
146
147 pub fn set_new_value(&mut self, tag: &str, value: Option<&str>) {
169 let (group, tag_name) = if let Some(colon_pos) = tag.find(':') {
170 (
171 Some(tag[..colon_pos].to_string()),
172 tag[colon_pos + 1..].to_string(),
173 )
174 } else {
175 (None, tag.to_string())
176 };
177
178 self.new_values.push(NewValue {
179 tag: tag_name,
180 group,
181 value: value.map(|v| v.to_string()),
182 });
183 }
184
185 pub fn clear_new_values(&mut self) {
187 self.new_values.clear();
188 }
189
190 pub fn set_new_values_from_file<P: AsRef<Path>>(
195 &mut self,
196 src_path: P,
197 tags_to_copy: Option<&[&str]>,
198 ) -> Result<u32> {
199 let src_tags = self.extract_info(src_path)?;
200 let mut count = 0u32;
201
202 for tag in &src_tags {
203 if tag.group.family0 == "File" || tag.group.family0 == "Composite" {
205 continue;
206 }
207 if tag.print_value.starts_with("(Binary") || tag.print_value.starts_with("(Undefined") {
209 continue;
210 }
211 if tag.print_value.is_empty() {
212 continue;
213 }
214
215 if let Some(filter) = tags_to_copy {
217 let name_lower = tag.name.to_lowercase();
218 if !filter.iter().any(|f| f.to_lowercase() == name_lower) {
219 continue;
220 }
221 }
222
223 let _full_tag = format!("{}:{}", tag.group.family0, tag.name);
224 self.new_values.push(NewValue {
225 tag: tag.name.clone(),
226 group: Some(tag.group.family0.clone()),
227 value: Some(tag.print_value.clone()),
228 });
229 count += 1;
230 }
231
232 Ok(count)
233 }
234
235 pub fn set_file_name_from_tag<P: AsRef<Path>>(
237 &self,
238 path: P,
239 tag_name: &str,
240 template: &str,
241 ) -> Result<String> {
242 let path = path.as_ref();
243 let tags = self.extract_info(path)?;
244
245 let tag_value = tags
246 .iter()
247 .find(|t| t.name.to_lowercase() == tag_name.to_lowercase())
248 .map(|t| &t.print_value)
249 .ok_or_else(|| Error::TagNotFound(tag_name.to_string()))?;
250
251 let new_name = if template.contains('%') {
254 template.replace("%v", value_to_filename(tag_value).as_str())
255 } else {
256 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
258 let clean = value_to_filename(tag_value);
259 if ext.is_empty() {
260 clean
261 } else {
262 format!("{}.{}", clean, ext)
263 }
264 };
265
266 let parent = path.parent().unwrap_or(Path::new(""));
267 let new_path = parent.join(&new_name);
268
269 fs::rename(path, &new_path).map_err(Error::Io)?;
270 Ok(new_path.to_string_lossy().to_string())
271 }
272
273 pub fn write_info<P: AsRef<Path>, Q: AsRef<Path>>(
278 &self,
279 src_path: P,
280 dst_path: Q,
281 ) -> Result<u32> {
282 let src_path = src_path.as_ref();
283 let dst_path = dst_path.as_ref();
284 let data = fs::read(src_path).map_err(Error::Io)?;
285
286 let file_type = self.detect_file_type(&data, src_path)?;
287 let output = self.apply_changes(&data, file_type)?;
288
289 let temp_path = dst_path.with_extension("exiftool_tmp");
291 fs::write(&temp_path, &output).map_err(Error::Io)?;
292 fs::rename(&temp_path, dst_path).map_err(Error::Io)?;
293
294 Ok(self.new_values.len() as u32)
295 }
296
297 fn apply_changes(&self, data: &[u8], file_type: FileType) -> Result<Vec<u8>> {
299 match file_type {
300 FileType::Jpeg => self.write_jpeg(data),
301 FileType::Png => self.write_png(data),
302 FileType::Tiff
303 | FileType::Dng
304 | FileType::Cr2
305 | FileType::Nef
306 | FileType::Arw
307 | FileType::Orf
308 | FileType::Pef => self.write_tiff(data),
309 FileType::WebP => self.write_webp(data),
310 FileType::Mp4
311 | FileType::QuickTime
312 | FileType::M4a
313 | FileType::ThreeGP
314 | FileType::F4v => self.write_mp4(data),
315 FileType::Psd => self.write_psd(data),
316 FileType::Pdf => self.write_pdf(data),
317 FileType::Heif | FileType::Avif => self.write_mp4(data),
318 FileType::Mkv | FileType::WebM => self.write_matroska(data),
319 FileType::Gif => {
320 let comment = self
321 .new_values
322 .iter()
323 .find(|nv| nv.tag.to_lowercase() == "comment")
324 .and_then(|nv| nv.value.clone());
325 crate::writer::gif_writer::write_gif(data, comment.as_deref())
326 }
327 FileType::Flac => {
328 let changes: Vec<(&str, &str)> = self
329 .new_values
330 .iter()
331 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
332 .collect();
333 crate::writer::flac_writer::write_flac(data, &changes)
334 }
335 FileType::Mp3 | FileType::Aiff => {
336 let changes: Vec<(&str, &str)> = self
337 .new_values
338 .iter()
339 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
340 .collect();
341 crate::writer::id3_writer::write_id3(data, &changes)
342 }
343 FileType::Jp2 | FileType::Jxl => {
344 let new_xmp = if self
345 .new_values
346 .iter()
347 .any(|nv| nv.group.as_deref() == Some("XMP"))
348 {
349 let refs: Vec<&NewValue> = self
350 .new_values
351 .iter()
352 .filter(|nv| nv.group.as_deref() == Some("XMP"))
353 .collect();
354 Some(self.build_new_xmp(&refs))
355 } else {
356 None
357 };
358 crate::writer::jp2_writer::write_jp2(data, new_xmp.as_deref(), None)
359 }
360 FileType::PostScript => {
361 let changes: Vec<(&str, &str)> = self
362 .new_values
363 .iter()
364 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
365 .collect();
366 crate::writer::ps_writer::write_postscript(data, &changes)
367 }
368 FileType::Ogg | FileType::Opus => {
369 let changes: Vec<(&str, &str)> = self
370 .new_values
371 .iter()
372 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
373 .collect();
374 crate::writer::ogg_writer::write_ogg(data, &changes)
375 }
376 FileType::Xmp => {
377 let props: Vec<xmp_writer::XmpProperty> = self
378 .new_values
379 .iter()
380 .filter_map(|nv| {
381 let val = nv.value.as_deref()?;
382 Some(xmp_writer::XmpProperty {
383 namespace: nv.group.clone().unwrap_or_else(|| "dc".into()),
384 property: nv.tag.clone(),
385 values: vec![val.to_string()],
386 prop_type: xmp_writer::XmpPropertyType::Simple,
387 })
388 })
389 .collect();
390 Ok(crate::writer::xmp_sidecar_writer::write_xmp_sidecar(&props))
391 }
392 _ => Err(Error::UnsupportedFileType(format!(
393 "writing not yet supported for {}",
394 file_type
395 ))),
396 }
397 }
398
399 pub fn writable_tags(file_type: FileType) -> Option<std::collections::HashSet<&'static str>> {
403 use std::collections::HashSet;
404
405 const EXIF_TAGS: &[&str] = &[
407 "imagedescription",
408 "make",
409 "model",
410 "orientation",
411 "xresolution",
412 "yresolution",
413 "resolutionunit",
414 "software",
415 "modifydate",
416 "datetime",
417 "artist",
418 "copyright",
419 "datetimeoriginal",
420 "createdate",
421 "datetimedigitized",
422 "usercomment",
423 "imageuniqueid",
424 "ownername",
425 "cameraownername",
426 "serialnumber",
427 "bodyserialnumber",
428 "lensmake",
429 "lensmodel",
430 "lensserialnumber",
431 ];
432
433 const IPTC_TAGS: &[&str] = &[
435 "objectname",
436 "title",
437 "urgency",
438 "category",
439 "supplementalcategories",
440 "keywords",
441 "specialinstructions",
442 "datecreated",
443 "timecreated",
444 "by-line",
445 "author",
446 "byline",
447 "by-linetitle",
448 "authorsposition",
449 "bylinetitle",
450 "city",
451 "sub-location",
452 "sublocation",
453 "province-state",
454 "state",
455 "provincestate",
456 "country-primarylocationcode",
457 "countrycode",
458 "country-primarylocationname",
459 "country",
460 "headline",
461 "credit",
462 "source",
463 "copyrightnotice",
464 "contact",
465 "caption-abstract",
466 "caption",
467 "description",
468 "writer-editor",
469 "captionwriter",
470 ];
471
472 const XMP_AUTO_TAGS: &[&str] = &[
474 "title",
475 "description",
476 "subject",
477 "creator",
478 "rights",
479 "keywords",
480 "rating",
481 "label",
482 "hierarchicalsubject",
483 ];
484
485 const ID3_TAGS: &[&str] = &[
487 "title",
488 "artist",
489 "album",
490 "year",
491 "date",
492 "track",
493 "genre",
494 "comment",
495 "composer",
496 "albumartist",
497 "encoder",
498 "encodedby",
499 "publisher",
500 "copyright",
501 "bpm",
502 "lyrics",
503 ];
504
505 const MP4_TAGS: &[&str] = &[
507 "title",
508 "artist",
509 "album",
510 "year",
511 "date",
512 "comment",
513 "genre",
514 "composer",
515 "writer",
516 "encoder",
517 "encodedby",
518 "grouping",
519 "lyrics",
520 "description",
521 "albumartist",
522 "copyright",
523 ];
524
525 const PDF_TAGS: &[&str] = &[
527 "title", "author", "subject", "keywords", "creator", "producer",
528 ];
529
530 const PS_TAGS: &[&str] = &[
532 "title",
533 "creator",
534 "author",
535 "for",
536 "creationdate",
537 "createdate",
538 ];
539
540 match file_type {
541 FileType::Png
543 | FileType::Flac
544 | FileType::Mkv
545 | FileType::WebM
546 | FileType::Ogg
547 | FileType::Opus
548 | FileType::Xmp => None,
549
550 FileType::Jpeg => {
552 let mut set: HashSet<&str> = HashSet::new();
553 set.extend(EXIF_TAGS);
554 set.extend(IPTC_TAGS);
555 set.extend(XMP_AUTO_TAGS);
556 set.insert("comment");
557 Some(set)
558 }
559
560 FileType::Tiff
562 | FileType::Dng
563 | FileType::Cr2
564 | FileType::Nef
565 | FileType::Arw
566 | FileType::Orf
567 | FileType::Pef => {
568 let mut set: HashSet<&str> = HashSet::new();
569 set.extend(EXIF_TAGS);
570 Some(set)
571 }
572
573 FileType::WebP => {
575 let mut set: HashSet<&str> = HashSet::new();
576 set.extend(EXIF_TAGS);
577 set.extend(XMP_AUTO_TAGS);
578 Some(set)
579 }
580
581 FileType::Mp4
583 | FileType::QuickTime
584 | FileType::M4a
585 | FileType::ThreeGP
586 | FileType::F4v
587 | FileType::Heif
588 | FileType::Avif => {
589 let mut set: HashSet<&str> = HashSet::new();
590 set.extend(MP4_TAGS);
591 set.extend(XMP_AUTO_TAGS);
592 Some(set)
593 }
594
595 FileType::Psd => {
597 let mut set: HashSet<&str> = HashSet::new();
598 set.extend(IPTC_TAGS);
599 set.extend(XMP_AUTO_TAGS);
600 Some(set)
601 }
602
603 FileType::Pdf => Some(PDF_TAGS.iter().copied().collect()),
604 FileType::PostScript => Some(PS_TAGS.iter().copied().collect()),
605
606 FileType::Mp3 | FileType::Aiff => Some(ID3_TAGS.iter().copied().collect()),
607
608 FileType::Gif => {
609 let mut set: HashSet<&str> = HashSet::new();
610 set.insert("comment");
611 Some(set)
612 }
613
614 FileType::Jp2 | FileType::Jxl => Some(XMP_AUTO_TAGS.iter().copied().collect()),
616
617 _ => Some(HashSet::new()),
619 }
620 }
621
622 fn write_jpeg(&self, data: &[u8]) -> Result<Vec<u8>> {
624 let mut exif_values: Vec<&NewValue> = Vec::new();
626 let mut xmp_values: Vec<&NewValue> = Vec::new();
627 let mut iptc_values: Vec<&NewValue> = Vec::new();
628 let mut comment_value: Option<&str> = None;
629 let mut remove_exif = false;
630 let mut remove_xmp = false;
631 let mut remove_iptc = false;
632 let mut remove_comment = false;
633
634 for nv in &self.new_values {
635 let group = nv.group.as_deref().unwrap_or("");
636 let group_upper = group.to_uppercase();
637
638 if nv.value.is_none() && nv.tag == "*" {
640 match group_upper.as_str() {
641 "EXIF" => {
642 remove_exif = true;
643 continue;
644 }
645 "XMP" => {
646 remove_xmp = true;
647 continue;
648 }
649 "IPTC" => {
650 remove_iptc = true;
651 continue;
652 }
653 _ => {}
654 }
655 }
656
657 match group_upper.as_str() {
658 "XMP" => xmp_values.push(nv),
659 "IPTC" => iptc_values.push(nv),
660 "EXIF" | "IFD0" | "EXIFIFD" | "GPS" => exif_values.push(nv),
661 "" => {
662 if nv.tag.to_lowercase() == "comment" {
664 if nv.value.is_none() {
665 remove_comment = true;
666 } else {
667 comment_value = nv.value.as_deref();
668 }
669 } else if is_xmp_tag(&nv.tag) {
670 xmp_values.push(nv);
671 } else {
672 exif_values.push(nv);
673 }
674 }
675 _ => exif_values.push(nv), }
677 }
678
679 let new_exif = if !exif_values.is_empty() {
681 Some(self.build_new_exif(data, &exif_values)?)
682 } else {
683 None
684 };
685
686 let new_xmp = if !xmp_values.is_empty() {
688 Some(self.build_new_xmp(&xmp_values))
689 } else {
690 None
691 };
692
693 let new_iptc_data = if iptc_values.is_empty() {
696 None
697 } else {
698 let existing = jpeg_writer::extract_jpeg_iptc_iim(data);
699 self.build_new_iptc(existing.as_deref(), &iptc_values)
700 };
701
702 jpeg_writer::write_jpeg(
704 data,
705 new_exif.as_deref(),
706 new_xmp.as_deref(),
707 new_iptc_data.as_deref(),
708 comment_value,
709 remove_exif,
710 remove_xmp,
711 remove_iptc,
712 remove_comment,
713 )
714 }
715
716 fn build_new_iptc(&self, existing: Option<&[u8]>, values: &[&NewValue]) -> Option<Vec<u8>> {
723 let mut records = existing.map(iptc_writer::parse_iim).unwrap_or_default();
724 let utf8 = records.iter().any(|r| {
726 r.record == 1 && r.dataset == 90 && r.data.windows(3).any(|w| w == [0x1B, 0x25, 0x47])
727 });
728 for nv in values {
729 let Some((record, dataset)) = iptc_writer::tag_name_to_iptc(&nv.tag) else {
730 continue;
731 };
732 match nv.value.as_deref() {
733 Some(value) => {
737 let data = if utf8 {
738 value.as_bytes().to_vec()
739 } else {
740 crate::encoding::encode_latin1(value)
741 };
742 let mut updated = false;
743 records.retain_mut(|r| {
744 if r.record == record && r.dataset == dataset {
745 if updated {
746 return false; }
748 r.data = data.clone();
749 updated = true;
750 }
751 true
752 });
753 if !updated {
754 records.push(iptc_writer::IptcRecord {
755 record,
756 dataset,
757 data,
758 });
759 }
760 }
761 None => records.retain(|r| !(r.record == record && r.dataset == dataset)),
763 }
764 }
765 if records.is_empty() {
766 return None;
767 }
768 Some(iptc_writer::build_iptc(&records))
769 }
770
771 fn build_new_exif(&self, jpeg_data: &[u8], values: &[&NewValue]) -> Result<Vec<u8>> {
773 let bo = ByteOrderMark::BigEndian;
774 let mut ifd0_entries = Vec::new();
775 let mut exif_entries = Vec::new();
776 let mut gps_entries = Vec::new();
777
778 let existing = extract_existing_exif_entries(jpeg_data, bo);
780 for entry in &existing {
781 match classify_exif_tag(entry.tag) {
782 ExifIfdGroup::Ifd0 => ifd0_entries.push(entry.clone()),
783 ExifIfdGroup::ExifIfd => exif_entries.push(entry.clone()),
784 ExifIfdGroup::Gps => gps_entries.push(entry.clone()),
785 }
786 }
787
788 let deleted_tags: Vec<u16> = values
790 .iter()
791 .filter(|nv| nv.value.is_none())
792 .filter_map(|nv| tag_name_to_id(&nv.tag))
793 .collect();
794
795 ifd0_entries.retain(|e| !deleted_tags.contains(&e.tag));
797 exif_entries.retain(|e| !deleted_tags.contains(&e.tag));
798 gps_entries.retain(|e| !deleted_tags.contains(&e.tag));
799
800 for nv in values {
802 if nv.value.is_none() {
803 continue;
804 }
805 let value_str = nv.value.as_deref().unwrap_or("");
806 let group = nv.group.as_deref().unwrap_or("");
807
808 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, value_str, group, bo)
809 {
810 let entry = exif_writer::IfdEntry {
811 tag: tag_id,
812 format,
813 data: encoded,
814 };
815
816 let target = match group.to_uppercase().as_str() {
817 "GPS" => &mut gps_entries,
818 "EXIFIFD" => &mut exif_entries,
819 _ => match classify_exif_tag(tag_id) {
820 ExifIfdGroup::ExifIfd => &mut exif_entries,
821 ExifIfdGroup::Gps => &mut gps_entries,
822 ExifIfdGroup::Ifd0 => &mut ifd0_entries,
823 },
824 };
825
826 if let Some(existing) = target.iter_mut().find(|e| e.tag == tag_id) {
828 *existing = entry;
829 } else {
830 target.push(entry);
831 }
832 }
833 }
834
835 ifd0_entries.retain(|e| e.tag != 0x8769 && e.tag != 0x8825 && e.tag != 0xA005);
837
838 exif_writer::build_exif(&ifd0_entries, &exif_entries, &gps_entries, bo)
839 }
840
841 fn write_png(&self, data: &[u8]) -> Result<Vec<u8>> {
843 let mut new_text: Vec<(&str, &str)> = Vec::new();
844 let mut remove_text: Vec<&str> = Vec::new();
845
846 let owned_pairs: Vec<(String, String)> = self
849 .new_values
850 .iter()
851 .filter(|nv| nv.value.is_some())
852 .map(|nv| (nv.tag.clone(), nv.value.clone().unwrap()))
853 .collect();
854
855 for (tag, value) in &owned_pairs {
856 new_text.push((tag.as_str(), value.as_str()));
857 }
858
859 for nv in &self.new_values {
860 if nv.value.is_none() {
861 remove_text.push(&nv.tag);
862 }
863 }
864
865 png_writer::write_png(data, &new_text, None, &remove_text)
866 }
867
868 fn write_psd(&self, data: &[u8]) -> Result<Vec<u8>> {
870 let mut iptc_values = Vec::new();
871 let mut xmp_values = Vec::new();
872
873 for nv in &self.new_values {
874 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
875 match group.as_str() {
876 "XMP" => xmp_values.push(nv),
877 "IPTC" => iptc_values.push(nv),
878 _ => {
879 if is_xmp_tag(&nv.tag) {
880 xmp_values.push(nv);
881 } else {
882 iptc_values.push(nv);
883 }
884 }
885 }
886 }
887
888 let new_iptc = if !iptc_values.is_empty() {
889 let records: Vec<_> = iptc_values
890 .iter()
891 .filter_map(|nv| {
892 let value = nv.value.as_deref()?;
893 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
894 Some(iptc_writer::IptcRecord {
895 record,
896 dataset,
897 data: crate::encoding::encode_latin1(value),
901 })
902 })
903 .collect();
904 if records.is_empty() {
905 None
906 } else {
907 Some(iptc_writer::build_iptc(&records))
908 }
909 } else {
910 None
911 };
912
913 let new_xmp = if !xmp_values.is_empty() {
914 let refs: Vec<&NewValue> = xmp_values.to_vec();
915 Some(self.build_new_xmp(&refs))
916 } else {
917 None
918 };
919
920 psd_writer::write_psd(data, new_iptc.as_deref(), new_xmp.as_deref())
921 }
922
923 fn write_matroska(&self, data: &[u8]) -> Result<Vec<u8>> {
925 let changes: Vec<(&str, &str)> = self
926 .new_values
927 .iter()
928 .filter_map(|nv| {
929 let value = nv.value.as_deref()?;
930 Some((nv.tag.as_str(), value))
931 })
932 .collect();
933
934 matroska_writer::write_matroska(data, &changes)
935 }
936
937 fn write_pdf(&self, data: &[u8]) -> Result<Vec<u8>> {
939 let changes: Vec<(&str, &str)> = self
940 .new_values
941 .iter()
942 .filter_map(|nv| {
943 let value = nv.value.as_deref()?;
944 Some((nv.tag.as_str(), value))
945 })
946 .collect();
947
948 pdf_writer::write_pdf(data, &changes)
949 }
950
951 fn write_mp4(&self, data: &[u8]) -> Result<Vec<u8>> {
953 let mut ilst_tags: Vec<([u8; 4], String)> = Vec::new();
954 let mut xmp_values: Vec<&NewValue> = Vec::new();
955
956 for nv in &self.new_values {
957 if nv.value.is_none() {
958 continue;
959 }
960 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
961 if group == "XMP" {
962 xmp_values.push(nv);
963 } else if let Some(key) = mp4_writer::tag_to_ilst_key(&nv.tag) {
964 ilst_tags.push((key, nv.value.clone().unwrap()));
965 }
966 }
967
968 let tag_refs: Vec<(&[u8; 4], &str)> =
969 ilst_tags.iter().map(|(k, v)| (k, v.as_str())).collect();
970
971 let new_xmp = if !xmp_values.is_empty() {
972 let refs: Vec<&NewValue> = xmp_values.to_vec();
973 Some(self.build_new_xmp(&refs))
974 } else {
975 None
976 };
977
978 mp4_writer::write_mp4(data, &tag_refs, new_xmp.as_deref())
979 }
980
981 fn write_webp(&self, data: &[u8]) -> Result<Vec<u8>> {
983 let mut exif_values: Vec<&NewValue> = Vec::new();
984 let mut xmp_values: Vec<&NewValue> = Vec::new();
985 let mut remove_exif = false;
986 let mut remove_xmp = false;
987
988 for nv in &self.new_values {
989 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
990 if nv.value.is_none() && nv.tag == "*" {
991 if group == "EXIF" {
992 remove_exif = true;
993 }
994 if group == "XMP" {
995 remove_xmp = true;
996 }
997 continue;
998 }
999 match group.as_str() {
1000 "XMP" => xmp_values.push(nv),
1001 _ => exif_values.push(nv),
1002 }
1003 }
1004
1005 let new_exif = if !exif_values.is_empty() {
1006 let bo = ByteOrderMark::BigEndian;
1007 let mut entries = Vec::new();
1008 for nv in &exif_values {
1009 if let Some(ref v) = nv.value {
1010 let group = nv.group.as_deref().unwrap_or("");
1011 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, v, group, bo)
1012 {
1013 entries.push(exif_writer::IfdEntry {
1014 tag: tag_id,
1015 format,
1016 data: encoded,
1017 });
1018 }
1019 }
1020 }
1021 if !entries.is_empty() {
1022 Some(exif_writer::build_exif(&entries, &[], &[], bo)?)
1023 } else {
1024 None
1025 }
1026 } else {
1027 None
1028 };
1029
1030 let new_xmp = if !xmp_values.is_empty() {
1031 Some(self.build_new_xmp(&xmp_values.to_vec()))
1032 } else {
1033 None
1034 };
1035
1036 webp_writer::write_webp(
1037 data,
1038 new_exif.as_deref(),
1039 new_xmp.as_deref(),
1040 remove_exif,
1041 remove_xmp,
1042 )
1043 }
1044
1045 fn write_tiff(&self, data: &[u8]) -> Result<Vec<u8>> {
1047 let bo = if data.starts_with(b"II") {
1048 ByteOrderMark::LittleEndian
1049 } else {
1050 ByteOrderMark::BigEndian
1051 };
1052
1053 let mut changes: Vec<(u16, Vec<u8>)> = Vec::new();
1054 for nv in &self.new_values {
1055 if let Some(ref value) = nv.value {
1056 let group = nv.group.as_deref().unwrap_or("");
1057 if let Some((tag_id, _format, encoded)) = encode_exif_tag(&nv.tag, value, group, bo)
1058 {
1059 changes.push((tag_id, encoded));
1060 }
1061 }
1062 }
1063
1064 tiff_writer::write_tiff(data, &changes)
1065 }
1066
1067 fn build_new_xmp(&self, values: &[&NewValue]) -> Vec<u8> {
1069 let mut properties = Vec::new();
1070
1071 for nv in values {
1072 let value_str = match &nv.value {
1073 Some(v) => v.clone(),
1074 None => continue,
1075 };
1076
1077 let ns = nv.group.as_deref().unwrap_or("dc").to_lowercase();
1078 let ns = if ns == "xmp" { "xmp".to_string() } else { ns };
1079
1080 let prop_type = match nv.tag.to_lowercase().as_str() {
1081 "title" | "description" | "rights" => xmp_writer::XmpPropertyType::LangAlt,
1082 "subject" | "keywords" => xmp_writer::XmpPropertyType::Bag,
1083 "creator" => xmp_writer::XmpPropertyType::Seq,
1084 _ => xmp_writer::XmpPropertyType::Simple,
1085 };
1086
1087 let values = if matches!(
1088 prop_type,
1089 xmp_writer::XmpPropertyType::Bag | xmp_writer::XmpPropertyType::Seq
1090 ) {
1091 value_str.split(',').map(|s| s.trim().to_string()).collect()
1092 } else {
1093 vec![value_str]
1094 };
1095
1096 properties.push(xmp_writer::XmpProperty {
1097 namespace: ns,
1098 property: nv.tag.clone(),
1099 values,
1100 prop_type,
1101 });
1102 }
1103
1104 xmp_writer::build_xmp(&properties).into_bytes()
1105 }
1106
1107 pub fn image_info<P: AsRef<Path>>(&self, path: P) -> Result<ImageInfo> {
1115 let tags = self.extract_info(path)?;
1116 Ok(self.get_info(&tags))
1117 }
1118
1119 pub fn extract_info<P: AsRef<Path>>(&self, path: P) -> Result<Vec<Tag>> {
1123 let path = path.as_ref();
1124 let data = map_file_for_read(path)?;
1130 self.extract_info_from_bytes(&data, path)
1131 }
1132
1133 pub fn extract_info_from_bytes(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
1135 crate::metadata::exif::set_show_unknown(self.options.show_unknown);
1137 crate::metadata::exif::set_keep_duplicates(
1141 self.options.duplicates || self.options.extract_embedded > 0,
1142 );
1143 crate::formats::pdf::set_process_compressed(self.options.process_compressed);
1145
1146 let file_type_result = self.detect_file_type(data, path);
1147 let (file_type, mut tags) = match file_type_result {
1148 Ok(ft) => {
1149 let t = self
1150 .process_file(data, ft)
1151 .or_else(|_| self.process_by_extension(data, path))?;
1152 (Some(ft), t)
1153 }
1154 Err(_) => {
1155 let t = self.process_by_extension(data, path)?;
1157 (None, t)
1158 }
1159 };
1160 let file_type = file_type.unwrap_or(FileType::Zip); let default_tags = || {
1165 (
1166 file_type.code().to_string(),
1167 file_type.mime_type().to_string(),
1168 file_type
1169 .extensions()
1170 .first()
1171 .copied()
1172 .unwrap_or("")
1173 .to_string(),
1174 )
1175 };
1176 let ooxml = if file_type == FileType::Zip {
1181 crate::formats::zip::detect_ooxml_type(data, path.extension().and_then(|e| e.to_str()))
1182 } else {
1183 None
1184 };
1185 let (ft_code, mime_str, ext_str): (String, String, String) = if file_type == FileType::Exe {
1186 exe_subtype(data)
1187 .map(|(ft, mime, ext)| (ft.to_string(), mime.to_string(), ext.to_string()))
1188 .unwrap_or_else(default_tags)
1189 } else if let Some(triple) = ooxml {
1190 triple
1191 } else if let Some((code, mime)) = refine_filetype_by_content(file_type, data) {
1192 let (_, _, ext) = default_tags();
1193 (code, mime, ext)
1194 } else {
1195 default_tags()
1196 };
1197
1198 let mut pre: Vec<Tag> = Vec::new();
1213
1214 let file_tag = |name: &str, val: Value| -> Tag {
1221 Tag {
1222 id: crate::tag::TagId::Text(name.to_string()),
1223 name: name.to_string(),
1224 description: name.to_string(),
1225 group: crate::tag::TagGroup {
1226 family0: "File".into(),
1227 family1: "File".into(),
1228 family2: "Other".into(),
1229 family3: "Main".into(),
1230 },
1231 raw_value: val.clone(),
1232 print_value: val.to_display_string(),
1233 priority: 1,
1234 }
1235 };
1236
1237 pre.push(file_tag(
1238 "ExifToolVersion",
1239 Value::String(crate::VERSION.to_string()),
1240 ));
1241
1242 if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
1243 pre.push(file_tag("FileName", Value::String(fname.to_string())));
1244 }
1245 if let Some(dir) = path.parent().and_then(|p| p.to_str()) {
1246 pre.push(file_tag("Directory", Value::String(dir.to_string())));
1247 }
1248
1249 if let Ok(metadata) = fs::metadata(path) {
1250 pre.push(Tag {
1251 id: crate::tag::TagId::Text("FileSize".into()),
1252 name: "FileSize".into(),
1253 description: "File Size".into(),
1254 group: crate::tag::TagGroup {
1255 family0: "File".into(),
1256 family1: "File".into(),
1257 family2: "Other".into(),
1258 family3: "Main".into(),
1259 },
1260 raw_value: Value::String(metadata.len().to_string()),
1263 print_value: format_file_size(metadata.len()),
1264 priority: 0,
1265 });
1266 }
1267
1268 #[cfg(unix)]
1269 if let Ok(metadata) = fs::metadata(path) {
1270 use std::os::unix::fs::MetadataExt;
1271 let mode = metadata.mode();
1272 use crate::formats::gzip::gzip_unix_to_datetime;
1275 if let Ok(modified) = metadata.modified() {
1277 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
1278 let secs = dur.as_secs() as i64;
1279 pre.push(file_tag(
1280 "FileModifyDate",
1281 Value::String(gzip_unix_to_datetime(secs)),
1282 ));
1283 }
1284 }
1285 if let Ok(accessed) = metadata.accessed() {
1287 if let Ok(dur) = accessed.duration_since(std::time::UNIX_EPOCH) {
1288 let secs = dur.as_secs() as i64;
1289 pre.push(file_tag(
1290 "FileAccessDate",
1291 Value::String(gzip_unix_to_datetime(secs)),
1292 ));
1293 }
1294 }
1295 let ctime = metadata.ctime();
1297 if ctime > 0 {
1298 pre.push(file_tag(
1299 "FileInodeChangeDate",
1300 Value::String(gzip_unix_to_datetime(ctime)),
1301 ));
1302 }
1303
1304 pre.push(Tag {
1308 id: crate::tag::TagId::Text("FilePermissions".into()),
1309 name: "FilePermissions".into(),
1310 description: "FilePermissions".into(),
1311 group: crate::tag::TagGroup {
1312 family0: "File".into(),
1313 family1: "File".into(),
1314 family2: "Other".into(),
1315 family3: "Main".into(),
1316 },
1317 raw_value: Value::String(format!("{:o}", mode)),
1318 print_value: format_file_permissions(mode),
1319 priority: 1,
1320 });
1321 }
1322
1323 pre.push(Tag {
1324 id: crate::tag::TagId::Text("FileType".into()),
1325 name: "FileType".into(),
1326 description: "File Type".into(),
1327 group: crate::tag::TagGroup {
1328 family0: "File".into(),
1329 family1: "File".into(),
1330 family2: "Other".into(),
1331 family3: "Main".into(),
1332 },
1333 raw_value: Value::String(format!("{:?}", file_type)),
1334 print_value: ft_code.clone(),
1337 priority: 1,
1338 });
1339
1340 if !ext_str.is_empty() || file_type == FileType::Exe {
1343 pre.push(file_tag(
1344 "FileTypeExtension",
1345 Value::String(ext_str.clone()),
1346 ));
1347 }
1348
1349 pre.push(Tag {
1350 id: crate::tag::TagId::Text("MIMEType".into()),
1351 name: "MIMEType".into(),
1352 description: "MIME Type".into(),
1353 group: crate::tag::TagGroup {
1354 family0: "File".into(),
1355 family1: "File".into(),
1356 family2: "Other".into(),
1357 family3: "Main".into(),
1358 },
1359 raw_value: Value::String(mime_str.clone()),
1360 print_value: mime_str.clone(),
1361 priority: 1,
1362 });
1363
1364 {
1366 let bo_str = if data.len() > 8 {
1367 let check: Option<&[u8]> = if data.starts_with(&[0xFF, 0xD8]) {
1369 data.windows(6)
1371 .position(|w| w == b"Exif\0\0")
1372 .map(|p| &data[p + 6..])
1373 } else if data.starts_with(b"FUJIFILMCCD-RAW") && data.len() >= 0x60 {
1374 let jpeg_offset =
1376 u32::from_be_bytes([data[0x54], data[0x55], data[0x56], data[0x57]])
1377 as usize;
1378 let jpeg_length =
1379 u32::from_be_bytes([data[0x58], data[0x59], data[0x5A], data[0x5B]])
1380 as usize;
1381 if jpeg_offset > 0 && jpeg_offset + jpeg_length <= data.len() {
1382 let jpeg = &data[jpeg_offset..jpeg_offset + jpeg_length];
1383 jpeg.windows(6)
1384 .position(|w| w == b"Exif\0\0")
1385 .map(|p| &jpeg[p + 6..])
1386 } else {
1387 None
1388 }
1389 } else if data.starts_with(b"RIFF") && data.len() >= 12 {
1390 let mut riff_bo: Option<&[u8]> = None;
1392 let mut pos = 12usize;
1393 while pos + 8 <= data.len() {
1394 let cid = &data[pos..pos + 4];
1395 let csz = u32::from_le_bytes([
1396 data[pos + 4],
1397 data[pos + 5],
1398 data[pos + 6],
1399 data[pos + 7],
1400 ]) as usize;
1401 let cstart = pos + 8;
1402 let cend = (cstart + csz).min(data.len());
1403 if cid == b"EXIF" && cend > cstart {
1404 let exif_data = &data[cstart..cend];
1405 let tiff = if exif_data.starts_with(b"Exif\0\0") {
1406 &exif_data[6..]
1407 } else {
1408 exif_data
1409 };
1410 riff_bo = Some(tiff);
1411 break;
1412 }
1413 if cid == b"LIST" && cend >= cstart + 4 {
1415 }
1417 pos = cend + (csz & 1);
1418 }
1419 riff_bo
1420 } else if data.starts_with(&[0x00, 0x00, 0x00, 0x0C, b'J', b'X', b'L', b' ']) {
1421 None
1427 } else if data.starts_with(&[0x00, b'M', b'R', b'M']) {
1428 let mrw_data_offset = if data.len() >= 8 {
1430 u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize + 8
1431 } else {
1432 0
1433 };
1434 let mut mrw_bo: Option<&[u8]> = None;
1435 let mut mpos = 8usize;
1436 while mpos + 8 <= mrw_data_offset.min(data.len()) {
1437 let seg_tag = &data[mpos..mpos + 4];
1438 let seg_len = u32::from_be_bytes([
1439 data[mpos + 4],
1440 data[mpos + 5],
1441 data[mpos + 6],
1442 data[mpos + 7],
1443 ]) as usize;
1444 if seg_tag == b"\x00TTW" && mpos + 8 + seg_len <= data.len() {
1445 mrw_bo = Some(&data[mpos + 8..mpos + 8 + seg_len]);
1446 break;
1447 }
1448 mpos += 8 + seg_len;
1449 }
1450 mrw_bo
1451 } else {
1452 Some(data)
1453 };
1454 if let Some(tiff) = check {
1455 if tiff.starts_with(b"II") {
1456 "Little-endian (Intel, II)"
1457 } else if tiff.starts_with(b"MM") {
1458 "Big-endian (Motorola, MM)"
1459 } else {
1460 ""
1461 }
1462 } else {
1463 ""
1464 }
1465 } else {
1466 ""
1467 };
1468 let already_has_exifbyteorder = tags.iter().any(|t| t.name == "ExifByteOrder");
1471 if !bo_str.is_empty()
1472 && !already_has_exifbyteorder
1473 && file_type != FileType::Btf
1474 && file_type != FileType::Dr4
1475 && file_type != FileType::Vrd
1476 && file_type != FileType::Crw
1477 {
1478 pre.push(file_tag("ExifByteOrder", Value::String(bo_str.to_string())));
1479 }
1480 }
1481
1482 tags.splice(0..0, pre);
1484
1485 {
1491 let is_mime = |t: &Tag| {
1494 t.name == "MIMEType"
1495 && t.group.family0 == "File"
1496 && t.group.family3 == crate::tag::MAIN_DOCUMENT
1497 };
1498 if tags.iter().filter(|t| is_mime(t)).count() > 1 {
1499 let last = tags.iter().rposition(is_mime).unwrap();
1500 let (value, print) = (tags[last].raw_value.clone(), tags[last].print_value.clone());
1501 let first = tags.iter().position(is_mime).unwrap();
1502 tags[first].raw_value = value;
1503 tags[first].print_value = print;
1504 let mut seen = false;
1505 tags.retain(|t| {
1506 !is_mime(t) || {
1507 let keep = !seen;
1508 seen = true;
1509 keep
1510 }
1511 });
1512 }
1513 }
1514
1515 {
1526 const SPECIAL_WINS: &[(&str, &str)] =
1527 &[("Kodak", "FNumber"), ("Kodak", "ExposureTime")];
1528 let keep_dups = self.options.duplicates || self.options.extract_embedded > 0;
1537 for (grp, name) in SPECIAL_WINS {
1538 if !tags
1539 .iter()
1540 .any(|t| t.name == *name && t.group.family1 == *grp)
1541 {
1542 continue;
1543 }
1544 if keep_dups {
1545 for t in tags.iter_mut() {
1546 if t.name == *name && t.group.family1 == *grp {
1547 t.priority = t.priority_rank() + 1;
1548 }
1549 }
1550 } else {
1551 tags.retain(|t| t.name != *name || t.group.family1 == *grp);
1552 }
1553 }
1554 }
1555
1556 let gps = crate::composite::gps_coordinates(&tags);
1561 tags.extend(gps);
1562
1563 let composite = crate::composite::compute_composite_tags(&tags);
1565 tags.extend(composite);
1566
1567 if !(self.options.duplicates || self.options.extract_embedded > 0) {
1575 let has_composite_alt = tags
1576 .iter()
1577 .any(|t| t.name == "GPSAltitude" && t.group.family0 == "Composite");
1578 let has_alt_ref = tags.iter().any(|t| t.name == "GPSAltitudeRef");
1579 if !has_composite_alt && has_alt_ref {
1580 tags.retain(|t| {
1581 !(t.name == "GPSAltitude"
1582 && t.group.family0 == "EXIF"
1583 && t.print_value == "undef")
1584 });
1585 }
1586 }
1587
1588 if self.options.geolocation {
1596 if let Some(geo) = crate::composite::compute_geolocation(&tags) {
1597 tags.extend(geo);
1598 }
1599 }
1600
1601 if self.options.use_mwg {
1603 let mwg = crate::composite::compute_mwg_composites(&tags);
1604 tags.extend(mwg);
1605 }
1606
1607 {
1613 let is_flir_fff = tags
1614 .iter()
1615 .any(|t| t.group.family0 == "APP1" && t.group.family1 == "FLIR");
1616 if is_flir_fff {
1617 tags.retain(|t| !(t.name == "LensID" && t.group.family0 == "Composite"));
1618 }
1619 }
1620
1621 {
1626 let make = tags
1627 .iter()
1628 .find(|t| t.name == "Make")
1629 .map(|t| t.print_value.clone())
1630 .unwrap_or_default();
1631 if !make.to_uppercase().contains("CANON") {
1632 tags.retain(|t| t.name != "Lens" || t.group.family0 != "Composite");
1633 }
1634 }
1635
1636 let collapse_duplicates = !self.options.duplicates && self.options.extract_embedded == 0;
1645 if collapse_duplicates {
1646 {
1655 let mut seen: std::collections::HashSet<&str> = tags
1656 .iter()
1657 .filter(|t| t.group.family3 == MAIN_DOCUMENT)
1658 .map(|t| t.name.as_str())
1659 .collect();
1660 let mut keep = Vec::with_capacity(tags.len());
1661 for t in &tags {
1662 keep.push(t.group.family3 == MAIN_DOCUMENT || seen.insert(t.name.as_str()));
1663 }
1664 let mut it = keep.into_iter();
1665 tags.retain(|_| it.next().unwrap_or(true));
1666 }
1667
1668 {
1673 const SPECIAL_WINS: &[(&str, &str)] = &[
1674 ("GoPro", "WhiteBalance"),
1675 ("GoPro", "Sharpness"),
1676 ("GoPro", "ExposureCompensation"),
1677 ("ID3v2_4", "Comment"),
1682 ("ID3v2_3", "Comment"),
1683 ("ID3v2_2", "Comment"),
1684 ("MinoltaRaw", "Contrast"),
1687 ("MinoltaRaw", "Saturation"),
1688 ("MinoltaRaw", "Sharpness"),
1689 ("MinoltaRaw", "ISOSetting"),
1690 ("Kodak", "FNumber"),
1692 ("Kodak", "ExposureTime"),
1693 ("Sigma", "X3FillLight"),
1695 ];
1696 for (grp, name) in SPECIAL_WINS {
1697 if tags
1698 .iter()
1699 .any(|t| t.name == *name && t.group.family1 == *grp)
1700 {
1701 tags.retain(|t| t.name != *name || t.group.family1 == *grp);
1702 }
1703 }
1704 }
1705
1706 let mut best_priority: HashMap<String, i32> = HashMap::new();
1707 for tag in &tags {
1708 let entry = best_priority
1709 .entry(tag.name.clone())
1710 .or_insert_with(|| tag.priority_rank());
1711 if tag.priority_rank() > *entry {
1712 *entry = tag.priority_rank();
1713 }
1714 }
1715 tags.retain(|t| t.priority_rank() >= *best_priority.get(&t.name).unwrap_or(&0));
1716
1717 {
1721 let is_native_doc = |g1: &str| matches!(g1, "PDF" | "PostScript" | "DjVu");
1727 let other_names: std::collections::HashSet<String> = tags
1728 .iter()
1729 .filter(|t| !is_native_doc(&t.group.family1) && !t.print_value.is_empty())
1730 .map(|t| t.name.clone())
1731 .collect();
1732 tags.retain(|t| {
1733 t.name == "Trapped"
1735 || !is_native_doc(&t.group.family1)
1736 || !other_names.contains(&t.name)
1737 });
1738 }
1739
1740 {
1778 #[rustfmt::skip]
1804 const LOW_PRIORITY_TAGS: &[(&str, &str)] = &[
1805 ("Canon", "BaseISO"), ("Canon", "FNumber"),
1812 ("Canon", "ExposureTime"),
1813 ("Canon", "FocalLength"),
1817 ("CIFF", "FocalLength"),
1818 ("Sigma", "Contrast"),
1822 ("Sigma", "Shadow"),
1823 ("Sigma", "Highlight"),
1824 ("Sigma", "Saturation"),
1825 ("Sigma", "Sharpness"),
1826 ];
1827 const LOW_PRIORITY_GROUPS1: &[&str] = &["PictureInfo", "XML"];
1840 #[rustfmt::skip]
1844 const SIGMARAW_PROPERTIES: &[&str] = &[
1845 "AFArea", "AFInFocus", "ApertureDisplayed", "BracketShot",
1846 "BurstShot", "CameraName", "ColorSpace", "DateTimeOriginal",
1847 "DriveMode", "EvalState", "ExposureCompensation",
1848 "ExposureProgram", "ExposureTime", "FNumber", "FirmwareVersion",
1849 "FlashExpComp", "FlashMode", "FlashPower", "FlashTTLMode",
1850 "FlashType", "FocalLength", "FocalLengthIn35mmFormat", "Focus",
1851 "FocusMode", "ISO", "ImageBoardID", "ImagerBoardID",
1852 "IntegrationTime", "LensApertureRange", "LensFocalRange",
1853 "LensType", "Make", "MeteringMode", "Model",
1854 "NetExposureCompensation", "Quality", "SceneCaptureType",
1855 "SensorID", "SensorTemperature", "SerialNumber",
1856 "ShutterSpeedDisplayed", "VersionBF", "WhiteBalance",
1857 ];
1858 let ifd1_low = matches!(ft_code.as_str(), "JPEG" | "JPS" | "MPO" | "ARW");
1871 let is_low_priority_source = |g: &TagGroup, name: &str| -> bool {
1872 let g1 = g.family1.as_str();
1873 if g.family2 == "Unknown" {
1877 return true;
1878 }
1879 if g.family3 != MAIN_DOCUMENT {
1883 return true;
1884 }
1885 if LOW_PRIORITY_TAGS.contains(&(g1, name))
1886 || LOW_PRIORITY_GROUPS1.contains(&g1)
1887 || (g1 == "SigmaRaw" && SIGMARAW_PROPERTIES.contains(&name))
1888 {
1889 return true;
1890 }
1891 match g.family0.as_str() {
1892 "XMP" => {
1897 crate::tags::priority0_generated::xmp_is_priority0(g1, name)
1898 || crate::tags::group2::xmp_property_is_unknown(g1, name)
1899 }
1900 "QuickTime" => {
1909 g1 == "QuickTime"
1910 && matches!(name, "AverageBitrate" | "BufferSize" | "MaxBitrate")
1911 }
1912 "EXIF" | "MakerNotes" => g1 == "PreviewIFD" || (ifd1_low && g1 == "IFD1"),
1916 "RAF" => true,
1923 "IPTC" => g1 != "IPTC",
1929 _ => matches!(g1, "Jpeg2000" | "PhotoMechanic" | "DjVu"),
1932 }
1933 };
1934 let priority_dir: Option<String> = tags
1942 .iter()
1943 .find(|t| {
1944 t.group.family0 == "EXIF"
1945 && matches!(t.name.as_str(), "SubfileType" | "OldSubfileType")
1946 && t.print_value == "Full-resolution image"
1947 })
1948 .map(|t| t.group.family1.clone());
1949 let xmp_is_priority_dir = matches!(
1957 file_type,
1958 FileType::Mp4
1959 | FileType::QuickTime
1960 | FileType::M4a
1961 | FileType::ThreeGP
1962 | FileType::Avif
1963 | FileType::Cr3
1964 | FileType::Crm
1965 | FileType::F4v
1966 | FileType::Mqv
1967 | FileType::Lrv
1968 ) || (file_type == FileType::Heif && ft_code != "HEIC");
1969 use std::collections::HashMap as HM;
1970 let mut by_name: HM<&str, Vec<usize>> = HM::new();
1977 for (i, t) in tags.iter().enumerate() {
1978 by_name.entry(t.name.as_str()).or_default().push(i);
1979 }
1980 let mut drop: std::collections::HashSet<usize> = std::collections::HashSet::new();
1981 for idxs in by_name.values() {
1982 if idxs.len() < 2 {
1983 continue;
1984 }
1985 let eff = |i: usize| -> i32 {
1991 let t = &tags[i];
1992 let in_priority_dir = priority_dir.as_deref()
1996 == Some(t.group.family1.as_str())
1997 || (xmp_is_priority_dir && t.group.family0 == "XMP");
1998 if t.group.family0 == "XMP"
2011 && crate::tags::priority0_generated::xmp_is_below_priority0(
2012 &t.group.family1,
2013 &t.name,
2014 )
2015 {
2016 return -1;
2017 }
2018 if t.priority == crate::tag::PRIORITY_EXPLICIT_ZERO {
2019 if t.group.family3 != MAIN_DOCUMENT {
2020 return 0;
2021 }
2022 return i32::from(in_priority_dir);
2023 }
2024 if t.priority == 0 && is_low_priority_source(&t.group, &t.name) {
2025 i32::from(
2032 in_priority_dir
2033 && t.group.family0 == "XMP"
2034 && t.group.family3 == MAIN_DOCUMENT,
2035 )
2036 } else {
2037 t.priority.max(1)
2038 }
2039 };
2040 let promoted = |p: i32| if p == 0 { 1 } else { p };
2044 let mut winner = idxs[0];
2045 for &i in &idxs[1..] {
2046 if eff(i) >= promoted(eff(winner)) {
2047 winner = i;
2048 }
2049 }
2050 for &i in idxs {
2051 if i != winner {
2052 drop.insert(i);
2053 }
2054 }
2055 }
2056 if !drop.is_empty() {
2057 let mut i = 0usize;
2058 tags.retain(|_| {
2059 let keep = !drop.contains(&i);
2060 i += 1;
2061 keep
2062 });
2063 }
2064 }
2065 }
2066
2067 for tag in &mut tags {
2073 if let Some((f0, f1, f2)) = file_level_group(&tag.name) {
2074 tag.group.family0 = f0.to_string();
2075 tag.group.family1 = f1.to_string();
2076 tag.group.family2 = f2.to_string();
2077 }
2078 }
2079
2080 for tag in &mut tags {
2088 if file_level_group(&tag.name).is_some() {
2089 continue;
2090 }
2091 if let Some(f2) = crate::tags::group2::family2_for(
2092 &tag.group.family0,
2093 &tag.group.family1,
2094 &tag.name,
2095 &tag.group.family2,
2096 ) {
2097 if f2 != tag.group.family2 {
2098 tag.group.family2 = f2.to_string();
2099 }
2100 }
2101 }
2102
2103 let is_ws = |c: char| c.is_ascii_whitespace();
2111 for tag in &mut tags {
2112 let pv = tag.print_value.as_str();
2113 let dirty = pv.ends_with(is_ws)
2114 || pv.chars().any(|c| {
2115 let u = c as u32;
2116 u == 0 || (0x01..=0x1f).contains(&u) || u == 0x7f
2117 });
2118 if !dirty {
2119 continue;
2120 }
2121 let mapped: String = pv
2122 .chars()
2123 .filter_map(|c| {
2124 let u = c as u32;
2125 if u == 0 {
2126 None
2127 } else if (0x01..=0x1f).contains(&u) || u == 0x7f {
2128 Some('.')
2129 } else {
2130 Some(c)
2131 }
2132 })
2133 .collect();
2134 tag.print_value = mapped.trim_end_matches(is_ws).to_string();
2135 }
2136
2137 if !self.options.requested_tags.is_empty() {
2141 tags.retain(|t| {
2142 self.options
2143 .requested_tags
2144 .iter()
2145 .any(|req| Self::tag_matches_request(t, req))
2146 });
2147 }
2148
2149 Ok(tags)
2150 }
2151
2152 fn tag_matches_request(tag: &Tag, request: &str) -> bool {
2156 let req = request.to_lowercase();
2157 let (group, name) = match req.split_once(':') {
2158 Some((g, n)) => (Some(g), n),
2159 None => (None, req.as_str()),
2160 };
2161 if name != "*" && tag.name.to_lowercase() != name {
2162 return false;
2163 }
2164 match group {
2165 None => true,
2166 Some(g) => {
2167 let grp = &tag.group;
2168 grp.family0.to_lowercase() == g
2169 || grp.family1.to_lowercase() == g
2170 || grp.family2.to_lowercase() == g
2171 }
2172 }
2173 }
2174
2175 fn get_info(&self, tags: &[Tag]) -> ImageInfo {
2179 let mut info = ImageInfo::new();
2180 let mut seen: HashMap<String, (usize, i32)> = HashMap::new(); for tag in tags {
2183 let value = if self.options.print_conv {
2184 &tag.print_value
2185 } else {
2186 &tag.raw_value.to_display_string()
2187 };
2188
2189 let entry = seen.entry(tag.name.clone()).or_insert((0, i32::MIN));
2190 entry.0 += 1;
2191
2192 if entry.0 == 1 {
2193 entry.1 = tag.priority_rank();
2194 info.insert(tag.name.clone(), value.clone());
2195 } else if tag.priority_rank() > entry.1 {
2196 entry.1 = tag.priority_rank();
2198 info.insert(tag.name.clone(), value.clone());
2199 } else if self.options.duplicates {
2200 let key = format!("{} [{}:{}]", tag.name, tag.group.family0, tag.group.family1);
2201 info.insert(key, value.clone());
2202 }
2203 }
2204
2205 info
2206 }
2207
2208 fn detect_file_type(&self, data: &[u8], path: &Path) -> Result<FileType> {
2210 let header_len = data.len().min(256);
2212 if let Some(ft) = file_type::detect_from_magic(&data[..header_len]) {
2213 if ft == FileType::Ico {
2215 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2216 if ext.eq_ignore_ascii_case("dfont") {
2217 return Ok(FileType::Dfont);
2218 }
2219 }
2220 }
2221 if ft == FileType::Jpeg {
2223 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2224 if ext.eq_ignore_ascii_case("jps") {
2225 return Ok(FileType::Jps);
2226 }
2227 }
2228 }
2229 if ft == FileType::Plist {
2231 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2232 if ext.eq_ignore_ascii_case("aae") {
2233 return Ok(FileType::Aae);
2234 }
2235 }
2236 }
2237 if ft == FileType::Xmp || ft == FileType::Xml {
2239 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2240 if ext.eq_ignore_ascii_case("plist") {
2241 return Ok(FileType::Plist);
2242 }
2243 if ext.eq_ignore_ascii_case("aae") {
2244 return Ok(FileType::Aae);
2245 }
2246 }
2247 }
2248 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2250 if ext.eq_ignore_ascii_case("pcd")
2251 && data.len() >= 2056
2252 && &data[2048..2055] == b"PCD_IPI"
2253 {
2254 return Ok(FileType::PhotoCd);
2255 }
2256 }
2257 if ft == FileType::Mp3 {
2259 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2260 if ext.eq_ignore_ascii_case("mpc") {
2261 return Ok(FileType::Mpc);
2262 }
2263 if ext.eq_ignore_ascii_case("ape") {
2264 return Ok(FileType::Ape);
2265 }
2266 if ext.eq_ignore_ascii_case("wv") {
2267 return Ok(FileType::WavPack);
2268 }
2269 }
2270 }
2271 if ft == FileType::Asf {
2273 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2274 if ext.eq_ignore_ascii_case("wmv") {
2275 return Ok(FileType::Wmv);
2276 }
2277 if ext.eq_ignore_ascii_case("wma") {
2278 return Ok(FileType::Wma);
2279 }
2280 }
2281 }
2282 if ft == FileType::Ogg {
2284 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2285 if ext.eq_ignore_ascii_case("opus") {
2286 return Ok(FileType::Opus);
2287 }
2288 }
2289 }
2290 if ft == FileType::Tiff {
2293 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2294 if let Some(ext_ft) = file_type::detect_from_extension(ext) {
2295 if ext_ft != FileType::Tiff && is_tiff_based(ext_ft) {
2296 return Ok(ext_ft);
2297 }
2298 }
2299 }
2300 }
2301 if ft == FileType::Zip {
2303 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2305 if ext.eq_ignore_ascii_case("eip") {
2306 return Ok(FileType::Eip);
2307 }
2308 }
2309 if let Some(iw) = detect_iwork_type(data, path) {
2312 return Ok(iw);
2313 }
2314 if let Some(od_type) = detect_opendocument_type(data) {
2315 return Ok(od_type);
2316 }
2317 }
2318 if ft == FileType::Doc {
2321 if let Some(ole) = detect_ole2_type(data) {
2322 return Ok(ole);
2323 }
2324 }
2325 return Ok(ft);
2326 }
2327
2328 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2330 if let Some(ft) = file_type::detect_from_extension(ext) {
2331 return Ok(ft);
2332 }
2333 }
2334
2335 let ext_str = path
2336 .extension()
2337 .and_then(|e| e.to_str())
2338 .unwrap_or("unknown");
2339 Err(Error::UnsupportedFileType(ext_str.to_string()))
2340 }
2341
2342 fn process_file(&self, data: &[u8], file_type: FileType) -> Result<Vec<Tag>> {
2344 match file_type {
2345 FileType::Jpeg | FileType::Jps => {
2346 formats::jpeg::read_jpeg_with_ee(data, self.options.extract_embedded)
2347 }
2348 FileType::Png | FileType::Mng => formats::png::read_png(data),
2349 FileType::Tiff
2351 | FileType::Btf
2352 | FileType::Dng
2353 | FileType::Cr2
2354 | FileType::Nef
2355 | FileType::Arw
2356 | FileType::Sr2
2357 | FileType::Orf
2358 | FileType::Pef
2359 | FileType::Erf
2360 | FileType::Fff
2361 | FileType::Rwl
2362 | FileType::Mef
2363 | FileType::Srw
2364 | FileType::Gpr
2365 | FileType::Arq
2366 | FileType::ThreeFR
2367 | FileType::Dcr
2368 | FileType::Rw2
2369 | FileType::Srf => formats::tiff::read_tiff(data),
2370 FileType::Iiq => formats::iiq::read_iiq(
2372 data,
2373 !self.options.duplicates && self.options.extract_embedded == 0,
2374 ),
2375 FileType::Gif => formats::gif::read_gif(data),
2377 FileType::Bmp => formats::bmp::read_bmp(data),
2378 FileType::WebP | FileType::Avi | FileType::Wav => formats::riff::read_riff(data),
2379 FileType::Psd => formats::psd::read_psd(data),
2380 FileType::Mp3 => formats::id3::read_mp3(data),
2382 FileType::Flac => formats::flac::read_flac(data),
2383 FileType::Ogg | FileType::Opus => formats::ogg::read_ogg(data),
2384 FileType::Aiff => formats::aiff::read_aiff(data),
2385 FileType::Mp4
2387 | FileType::QuickTime
2388 | FileType::M4a
2389 | FileType::ThreeGP
2390 | FileType::Heif
2391 | FileType::Avif
2392 | FileType::Cr3
2393 | FileType::Crm
2394 | FileType::F4v
2395 | FileType::Mqv
2396 | FileType::Lrv => {
2397 formats::quicktime::read_quicktime_with_ee(data, self.options.extract_embedded)
2398 }
2399 FileType::Mkv | FileType::WebM => formats::matroska::read_matroska(data),
2400 FileType::Asf | FileType::Wmv | FileType::Wma => formats::asf::read_asf(data),
2401 FileType::Wtv => formats::wtv::read_wtv(data),
2402 FileType::Crw => formats::canon_raw::read_crw(data),
2404 FileType::Raf => formats::raf::read_raf(data),
2405 FileType::Mrw => formats::mrw::read_mrw(data),
2406 FileType::Mrc => formats::mrc::read_mrc(data, self.options.extract_embedded),
2407 FileType::Jp2 => formats::jp2::read_jp2(data),
2409 FileType::J2c => formats::jp2::read_j2c(data),
2410 FileType::Jxl => formats::jp2::read_jxl(data),
2411 FileType::Ico => formats::ico::read_ico(data),
2412 FileType::Icc => formats::icc::read_icc(data),
2413 FileType::Pdf => formats::pdf::read_pdf(data, self.options.extract_embedded),
2415 FileType::PostScript => {
2416 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
2418 formats::font::read_pfa(data).or_else(|_| {
2419 formats::postscript::read_postscript(data, self.options.extract_embedded)
2420 })
2421 } else {
2422 formats::postscript::read_postscript(data, self.options.extract_embedded)
2423 }
2424 }
2425 FileType::Eip => formats::capture_one::read_eip(data, self.options.extract_embedded),
2426 FileType::Zip
2427 | FileType::Docx
2428 | FileType::Xlsx
2429 | FileType::Pptx
2430 | FileType::Doc
2431 | FileType::Xls
2432 | FileType::Ppt
2433 | FileType::Numbers
2434 | FileType::Pages
2435 | FileType::Key => formats::zip::read_zip(data, self.options.extract_embedded),
2436 FileType::Rtf => formats::rtf::read_rtf(data),
2437 FileType::InDesign => formats::indesign::read_indesign(data),
2438 FileType::Pcap => formats::pcap::read_pcap(data),
2439 FileType::Pcapng => formats::pcap::read_pcapng(data),
2440 FileType::Vrd => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
2442 FileType::Dr4 => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
2443 FileType::Xmp => formats::xmp_file::read_xmp(data),
2445 FileType::Svg => formats::svg::read_svg(data),
2446 FileType::Html => {
2447 let is_svg = data.windows(4).take(512).any(|w| w == b"<svg");
2449 if is_svg {
2450 formats::svg::read_svg(data)
2451 } else {
2452 formats::html::read_html(data)
2453 }
2454 }
2455 FileType::Exe => formats::exe::read_exe(data),
2456 FileType::Font => {
2457 if data.starts_with(b"StartFontMetrics") {
2459 return formats::font::read_afm(data);
2460 }
2461 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
2463 return formats::font::read_pfa(data).or_else(|_| Ok(Vec::new()));
2464 }
2465 if data.len() >= 2 && data[0] == 0x80 && (data[1] == 0x01 || data[1] == 0x02) {
2467 return formats::font::read_pfb(data).or_else(|_| Ok(Vec::new()));
2468 }
2469 formats::font::read_font(data)
2470 }
2471 FileType::WavPack | FileType::Dsf => formats::id3::read_mp3(data),
2473 FileType::Ape => formats::ape::read_ape(data),
2474 FileType::Mpc => formats::ape::read_mpc(data),
2475 FileType::Aac => formats::aac::read_aac(data),
2476 FileType::RealAudio => {
2477 formats::real_audio::read_real_audio(data).or_else(|_| Ok(Vec::new()))
2478 }
2479 FileType::RealMedia => {
2480 formats::real_media::read_real_media(data).or_else(|_| Ok(Vec::new()))
2481 }
2482 FileType::Czi => formats::czi::read_czi(data).or_else(|_| Ok(Vec::new())),
2484 FileType::PhotoCd => formats::photo_cd::read_photo_cd(data).or_else(|_| Ok(Vec::new())),
2485 FileType::Dicom => formats::dicom::read_dicom(data),
2486 FileType::Fits => formats::fits::read_fits(data),
2487 FileType::Fit => formats::fit::read_fit_with_ee(data, self.options.extract_embedded),
2488 FileType::Flv => formats::flv::read_flv(data),
2489 FileType::Mxf => formats::mxf::read_mxf(data, self.options.extract_embedded)
2490 .or_else(|_| Ok(Vec::new())),
2491 FileType::Swf => formats::swf::read_swf(data),
2492 FileType::Hdr => formats::hdr::read_hdr(data),
2493 FileType::DjVu => formats::djvu::read_djvu(data),
2494 FileType::Xcf => formats::gimp::read_xcf(data),
2495 FileType::Mie => formats::mie::read_mie(data),
2496 FileType::Lfp => formats::lytro::read_lfp(data),
2497 FileType::Fpf => formats::flir_fpf::read_fpf(data),
2499 FileType::Flif => formats::flif::read_flif(data),
2500 FileType::Bpg => formats::bpg::read_bpg(data),
2501 FileType::Pcx => formats::pcx::read_pcx(data),
2502 FileType::Pict => formats::pict::read_pict(data),
2503 FileType::Mpeg => formats::mpeg::read_mpeg(data),
2504 FileType::M2ts => formats::m2ts::read_m2ts(data, self.options.extract_embedded),
2505 FileType::Gzip => formats::gzip::read_gzip(data),
2506 FileType::Rar => formats::rar::read_rar(data),
2507 FileType::SevenZ => formats::sevenz::read_7z(data),
2508 FileType::Dss => formats::dss::read_dss(data),
2509 FileType::Moi => formats::moi::read_moi(data),
2510 FileType::MacOs => formats::macos::read_macos(data),
2511 FileType::Json => formats::json_format::read_json(data),
2512 FileType::Pgf => formats::pgf::read_pgf(data),
2514 FileType::Xisf => formats::xisf::read_xisf(data),
2515 FileType::Torrent => formats::torrent::read_torrent(data),
2516 FileType::Mobi => formats::palm::read_palm(data),
2517 FileType::Psp => formats::psp::read_psp(data),
2518 FileType::SonyPmp => formats::sony_pmp::read_sony_pmp(data),
2519 FileType::Audible => formats::audible::read_audible(data),
2520 FileType::Exr => formats::openexr::read_openexr(data),
2521 FileType::Plist => {
2523 if data.starts_with(b"bplist") {
2524 formats::plist::read_binary_plist_tags(data)
2525 } else {
2526 formats::plist::read_xml_plist(data)
2527 }
2528 }
2529 FileType::Aae => {
2530 if data.starts_with(b"bplist") {
2531 formats::plist::read_binary_plist_tags(data)
2532 } else {
2533 formats::plist::read_aae_plist(data)
2534 }
2535 }
2536 FileType::KyoceraRaw => formats::kyocera_raw::read_kyocera_raw(data),
2537 FileType::PortableFloatMap => formats::pfm::read_pfm(data),
2538 FileType::Ods
2539 | FileType::Odt
2540 | FileType::Odp
2541 | FileType::Odg
2542 | FileType::Odf
2543 | FileType::Odb
2544 | FileType::Odi
2545 | FileType::Odc => formats::zip::read_zip(data, self.options.extract_embedded),
2546 FileType::Lif => formats::lif::read_lif(data),
2547 FileType::Rwz => formats::rawzor::read_rawzor(data),
2548 FileType::Jxr => formats::jxr::read_jxr(data),
2549 FileType::Miff => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
2550 FileType::Tnef => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
2551 FileType::Wpg => formats::wpg::read_wpg(data).or_else(|_| Ok(Vec::new())),
2552 FileType::Dv => {
2553 formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new()))
2554 }
2555 FileType::Itc => formats::itc::read_itc(data).or_else(|_| Ok(Vec::new())),
2556 FileType::Iso => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
2557 FileType::Afm => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
2558 FileType::Pfa => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
2559 FileType::Pfb => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
2560 FileType::Dfont => formats::font::read_font(data).or_else(|_| Ok(Vec::new())),
2561 FileType::Xml | FileType::Inx => {
2562 formats::xmp_file::read_xmp(data).or_else(|_| Ok(Vec::new()))
2563 }
2564 FileType::Eps => {
2565 formats::postscript::read_postscript(data, self.options.extract_embedded)
2566 }
2567 _ => Err(Error::UnsupportedFileType(format!("{}", file_type))),
2568 }
2569 }
2570
2571 fn process_by_extension(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
2573 let ext = path
2574 .extension()
2575 .and_then(|e| e.to_str())
2576 .unwrap_or("")
2577 .to_ascii_lowercase();
2578
2579 match ext.as_str() {
2580 "ppm" | "pgm" | "pbm" => formats::ppm::read_ppm(data),
2581 "pfm" => {
2582 if data.len() >= 3 && data[0] == b'P' && (data[1] == b'f' || data[1] == b'F') {
2584 formats::ppm::read_ppm(data)
2585 } else {
2586 Ok(Vec::new()) }
2588 }
2589 "json" => formats::json_format::read_json(data),
2590 "svg" => formats::svg::read_svg(data),
2591 "ram" => formats::ram::read_ram(data).or_else(|_| Ok(Vec::new())),
2592 "txt" | "log" | "igc" => Ok(compute_text_tags(data, false)),
2593 "csv" => Ok(compute_text_tags(data, true)),
2594 "url" => formats::lnk::read_url(data).or_else(|_| Ok(Vec::new())),
2595 "lnk" => formats::lnk::read_lnk(data).or_else(|_| Ok(Vec::new())),
2596 "gpx" | "kml" | "xml" | "inx" => formats::xmp_file::read_xmp(data),
2597 "plist" => {
2598 if data.starts_with(b"bplist") {
2599 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
2600 } else {
2601 formats::plist::read_xml_plist(data).or_else(|_| Ok(Vec::new()))
2602 }
2603 }
2604 "aae" => {
2605 if data.starts_with(b"bplist") {
2606 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
2607 } else {
2608 formats::plist::read_aae_plist(data).or_else(|_| Ok(Vec::new()))
2609 }
2610 }
2611 "vcf" | "ics" | "vcard" => {
2612 let s = crate::encoding::decode_utf8_or_latin1(&data[..data.len().min(100)]);
2613 if s.contains("BEGIN:VCALENDAR") {
2614 formats::vcard::read_ics(data).or_else(|_| Ok(Vec::new()))
2615 } else {
2616 formats::vcard::read_vcf(data).or_else(|_| Ok(Vec::new()))
2617 }
2618 }
2619 "xcf" => Ok(Vec::new()), "vrd" => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
2621 "dr4" => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
2622 "indd" | "indt" => Ok(Vec::new()), "x3f" => formats::sigma_raw::read_x3f(data).or_else(|_| Ok(Vec::new())),
2624 "mie" => Ok(Vec::new()), "exr" => Ok(Vec::new()), "wpg" => formats::wpg::read_wpg(data).or_else(|_| Ok(Vec::new())),
2627 "moi" => formats::moi::read_moi(data).or_else(|_| Ok(Vec::new())),
2628 "macos" => formats::macos::read_macos(data).or_else(|_| Ok(Vec::new())),
2629 "dpx" => formats::dpx::read_dpx(data).or_else(|_| Ok(Vec::new())),
2630 "r3d" => formats::red::read_r3d(data).or_else(|_| Ok(Vec::new())),
2631 "tnef" => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
2632 "ppt" | "fpx" => formats::flashpix::read_fpx(data).or_else(|_| Ok(Vec::new())),
2633 "fpf" => formats::flir_fpf::read_fpf(data).or_else(|_| Ok(Vec::new())),
2634 "itc" => formats::itc::read_itc(data).or_else(|_| Ok(Vec::new())),
2635 "mpg" | "mpeg" | "m1v" | "m2v" | "mpv" => {
2636 formats::mpeg::read_mpeg(data).or_else(|_| Ok(Vec::new()))
2637 }
2638 "dv" => formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new())),
2639 "czi" => formats::czi::read_czi(data).or_else(|_| Ok(Vec::new())),
2640 "miff" => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
2641 "lfp" | "mrc" | "dss" | "mobi" | "psp" | "pgf" | "raw" | "pmp" | "torrent" | "xisf"
2642 | "mxf" | "dfont" => Ok(Vec::new()),
2643 "iso" => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
2644 "afm" => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
2645 "pfa" => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
2646 "pfb" => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
2647 _ => Err(Error::UnsupportedFileType(ext)),
2648 }
2649 }
2650}
2651
2652impl Default for ExifTool {
2653 fn default() -> Self {
2654 Self::new()
2655 }
2656}
2657
2658fn exe_subtype(d: &[u8]) -> Option<(&'static str, &'static str, &'static str)> {
2663 const MIME: &str = "application/octet-stream";
2664 if d.len() < 8 {
2665 return None;
2666 }
2667 if &d[0..4] == b"\x7fELF" && d.len() >= 18 {
2669 let le = d[5] == 1;
2670 let e_type = if le {
2671 u16::from_le_bytes([d[16], d[17]])
2672 } else {
2673 u16::from_be_bytes([d[16], d[17]])
2674 };
2675 return Some(match e_type {
2676 1 => ("ELF relocatable", MIME, "o"),
2677 2 => ("ELF executable", MIME, ""),
2678 3 => ("ELF shared library", MIME, "so"),
2679 4 => ("ELF core file", MIME, ""),
2680 _ => ("ELF", MIME, ""),
2681 });
2682 }
2683 let magic_be = u32::from_be_bytes([d[0], d[1], d[2], d[3]]);
2685 let macho = matches!(magic_be, 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE);
2686 if macho && d.len() >= 16 {
2687 let le = matches!(magic_be, 0xCEFAEDFE | 0xCFFAEDFE);
2688 let filetype = if le {
2689 u32::from_le_bytes([d[12], d[13], d[14], d[15]])
2690 } else {
2691 u32::from_be_bytes([d[12], d[13], d[14], d[15]])
2692 };
2693 return Some(match filetype {
2694 1 => ("Mach-O object file", MIME, "o"),
2695 6 => ("Mach-O dynamic link library", MIME, "dylib"),
2696 8 => ("Mach-O dynamic bound bundle", MIME, "dylib"),
2697 9 => ("Mach-O dynamic link library stub", MIME, "dylib"),
2698 _ => ("Mach-O executable", MIME, ""),
2699 });
2700 }
2701 if matches!(magic_be, 0xCAFEBABE | 0xBEBAFECA) {
2703 return Some(("Mach-O fat binary executable", MIME, ""));
2704 }
2705 if d.starts_with(b"!<arch>\n") {
2707 let is_macho = d.windows(4).take(4096).any(|w| {
2708 let m = u32::from_be_bytes([w[0], w[1], w[2], w[3]]);
2709 matches!(
2710 m,
2711 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE | 0xCAFEBABE
2712 )
2713 });
2714 return Some(if is_macho {
2715 ("Mach-O static library", MIME, "a")
2716 } else {
2717 ("Static library", MIME, "a")
2718 });
2719 }
2720 if &d[0..2] == b"MZ" && d.len() >= 0x40 {
2722 let pe_off = u32::from_le_bytes([d[0x3c], d[0x3d], d[0x3e], d[0x3f]]) as usize;
2723 if pe_off + 6 <= d.len() && &d[pe_off..pe_off + 4] == b"PE\0\0" {
2724 let machine = u16::from_le_bytes([d[pe_off + 4], d[pe_off + 5]]);
2725 return Some(match machine {
2726 0x8664 | 0xAA64 => ("Win64 EXE", MIME, "exe"),
2727 _ => ("Win32 EXE", MIME, "exe"),
2728 });
2729 }
2730 }
2731 None
2732}
2733
2734fn is_tiff_based(ft: FileType) -> bool {
2736 matches!(
2737 ft,
2738 FileType::Dng
2739 | FileType::Cr2
2740 | FileType::Nef
2741 | FileType::Arw
2742 | FileType::Sr2
2743 | FileType::Orf
2744 | FileType::Pef
2745 | FileType::Erf
2746 | FileType::Rwl
2747 | FileType::Mef
2748 | FileType::Srw
2749 | FileType::Gpr
2750 | FileType::Arq
2751 | FileType::ThreeFR
2752 | FileType::Dcr
2753 | FileType::Rw2
2754 | FileType::Srf
2755 | FileType::Iiq
2756 | FileType::Btf
2757 )
2758}
2759
2760fn detect_ole2_type(data: &[u8]) -> Option<FileType> {
2763 fn has_utf16(data: &[u8], name: &str) -> bool {
2764 let needle: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
2765 data.windows(needle.len()).any(|w| w == needle.as_slice())
2766 }
2767 if has_utf16(data, "PowerPoint Document") {
2768 Some(FileType::Ppt)
2769 } else if has_utf16(data, "Workbook") || has_utf16(data, "Book") {
2770 Some(FileType::Xls)
2771 } else {
2772 None
2773 }
2774}
2775
2776fn detect_iwork_type(data: &[u8], path: &Path) -> Option<FileType> {
2780 const MARKERS: &[&[u8]] = &[
2781 b"index.xml",
2782 b"index.apxl",
2783 b"QuickLook/Thumbnail.jpg",
2784 b"Index/Document.iwa",
2785 b"Index/Slide.iwa",
2786 b"Index/Tables/DataList.iwa",
2787 ];
2788 let has_marker = MARKERS
2789 .iter()
2790 .any(|m| data.windows(m.len()).any(|w| w == *m));
2791 if !has_marker {
2792 return None;
2793 }
2794 let ext = path
2795 .extension()
2796 .and_then(|e| e.to_str())
2797 .unwrap_or("")
2798 .to_ascii_lowercase();
2799 match ext.as_str() {
2800 "numbers" | "nmbtemplate" => Some(FileType::Numbers),
2801 "pages" => Some(FileType::Pages),
2802 "key" | "kth" => Some(FileType::Key),
2803 _ => None,
2804 }
2805}
2806
2807fn refine_filetype_by_content(file_type: FileType, data: &[u8]) -> Option<(String, String)> {
2810 match file_type {
2811 FileType::PortableFloatMap if data.len() >= 2 && data[0] == 0x00 && data[1] <= 0x02 => {
2813 Some(("PFM".into(), "application/x-font-type1".into()))
2814 }
2815 FileType::Plist if !data.starts_with(b"bplist") => {
2817 Some(("PLIST".into(), "application/xml".into()))
2818 }
2819 FileType::Jxl if data.starts_with(&[0xFF, 0x0A]) => {
2821 Some(("JXL Codestream".into(), file_type.mime_type().to_string()))
2822 }
2823 FileType::WebP if data.len() >= 16 && &data[12..16] == b"VP8X" => {
2825 Some(("Extended WEBP".into(), file_type.mime_type().to_string()))
2826 }
2827 FileType::DjVu if data.len() >= 16 && &data[12..16] == b"DJVM" => Some((
2829 "DJVU (multi-page)".into(),
2830 file_type.mime_type().to_string(),
2831 )),
2832 _ => None,
2833 }
2834}
2835
2836fn detect_opendocument_type(data: &[u8]) -> Option<FileType> {
2837 if data.len() < 30 || data[0..4] != [0x50, 0x4B, 0x03, 0x04] {
2839 return None;
2840 }
2841 let compression = u16::from_le_bytes([data[8], data[9]]);
2842 let compressed_size = u32::from_le_bytes([data[18], data[19], data[20], data[21]]) as usize;
2843 let name_len = u16::from_le_bytes([data[26], data[27]]) as usize;
2844 let extra_len = u16::from_le_bytes([data[28], data[29]]) as usize;
2845 let name_start = 30;
2846 if name_start + name_len > data.len() {
2847 return None;
2848 }
2849 let filename = std::str::from_utf8(&data[name_start..name_start + name_len]).unwrap_or("");
2850 if filename != "mimetype" || compression != 0 {
2851 return None;
2852 }
2853 let content_start = name_start + name_len + extra_len;
2854 let content_end = (content_start + compressed_size).min(data.len());
2855 if content_start >= content_end {
2856 return None;
2857 }
2858 let mime = std::str::from_utf8(&data[content_start..content_end])
2859 .unwrap_or("")
2860 .trim();
2861 match mime {
2862 "application/vnd.oasis.opendocument.spreadsheet" => Some(FileType::Ods),
2863 "application/vnd.oasis.opendocument.text" => Some(FileType::Odt),
2864 "application/vnd.oasis.opendocument.presentation" => Some(FileType::Odp),
2865 "application/vnd.oasis.opendocument.graphics" => Some(FileType::Odg),
2866 "application/vnd.oasis.opendocument.formula" => Some(FileType::Odf),
2867 "application/vnd.oasis.opendocument.database" => Some(FileType::Odb),
2868 "application/vnd.oasis.opendocument.image" => Some(FileType::Odi),
2869 "application/vnd.oasis.opendocument.chart" => Some(FileType::Odc),
2870 _ => None,
2871 }
2872}
2873
2874pub fn get_file_type<P: AsRef<Path>>(path: P) -> Result<FileType> {
2876 let path = path.as_ref();
2877 let mut file = fs::File::open(path).map_err(Error::Io)?;
2878 let mut header = [0u8; 256];
2879 use std::io::Read;
2880 let n = file.read(&mut header).map_err(Error::Io)?;
2881
2882 if let Some(ft) = file_type::detect_from_magic(&header[..n]) {
2883 return Ok(ft);
2884 }
2885
2886 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2887 if let Some(ft) = file_type::detect_from_extension(ext) {
2888 return Ok(ft);
2889 }
2890 }
2891
2892 Err(Error::UnsupportedFileType("unknown".into()))
2893}
2894
2895enum ExifIfdGroup {
2897 Ifd0,
2898 ExifIfd,
2899 Gps,
2900}
2901
2902fn classify_exif_tag(tag_id: u16) -> ExifIfdGroup {
2904 match tag_id {
2905 0x829A..=0x829D | 0x8822..=0x8827 | 0x8830 | 0x9000..=0x9292 | 0xA000..=0xA435 => {
2907 ExifIfdGroup::ExifIfd
2908 }
2909 0x0000..=0x001F if tag_id <= 0x001F => ExifIfdGroup::Gps,
2911 _ => ExifIfdGroup::Ifd0,
2913 }
2914}
2915
2916fn extract_existing_exif_entries(
2918 jpeg_data: &[u8],
2919 target_bo: ByteOrderMark,
2920) -> Vec<exif_writer::IfdEntry> {
2921 let mut entries = Vec::new();
2922
2923 let mut pos = 2; while pos + 4 <= jpeg_data.len() {
2926 if jpeg_data[pos] != 0xFF {
2927 pos += 1;
2928 continue;
2929 }
2930 let marker = jpeg_data[pos + 1];
2931 pos += 2;
2932
2933 if marker == 0xDA || marker == 0xD9 {
2934 break; }
2936 if marker == 0xFF || marker == 0x00 || marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
2937 continue;
2938 }
2939
2940 if pos + 2 > jpeg_data.len() {
2941 break;
2942 }
2943 let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
2944 if seg_len < 2 || pos + seg_len > jpeg_data.len() {
2945 break;
2946 }
2947
2948 let seg_data = &jpeg_data[pos + 2..pos + seg_len];
2949
2950 if marker == 0xE1 && seg_data.len() > 14 && seg_data.starts_with(b"Exif\0\0") {
2952 let tiff_data = &seg_data[6..];
2953 extract_ifd_entries(tiff_data, target_bo, &mut entries);
2954 break;
2955 }
2956
2957 pos += seg_len;
2958 }
2959
2960 entries
2961}
2962
2963fn extract_ifd_entries(
2965 tiff_data: &[u8],
2966 target_bo: ByteOrderMark,
2967 entries: &mut Vec<exif_writer::IfdEntry>,
2968) {
2969 use crate::metadata::exif::parse_tiff_header;
2970
2971 let header = match parse_tiff_header(tiff_data) {
2972 Ok(h) => h,
2973 Err(_) => return,
2974 };
2975
2976 let src_bo = header.byte_order;
2977
2978 read_ifd_for_merge(
2980 tiff_data,
2981 header.ifd0_offset as usize,
2982 src_bo,
2983 target_bo,
2984 entries,
2985 );
2986
2987 let ifd0_offset = header.ifd0_offset as usize;
2989 if ifd0_offset + 2 > tiff_data.len() {
2990 return;
2991 }
2992 let count = read_u16_bo(tiff_data, ifd0_offset, src_bo) as usize;
2993 for i in 0..count {
2994 let eoff = ifd0_offset + 2 + i * 12;
2995 if eoff + 12 > tiff_data.len() {
2996 break;
2997 }
2998 let tag = read_u16_bo(tiff_data, eoff, src_bo);
2999 let value_off = read_u32_bo(tiff_data, eoff + 8, src_bo) as usize;
3000
3001 match tag {
3002 0x8769 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
3003 0x8825 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
3004 _ => {}
3005 }
3006 }
3007}
3008
3009fn read_ifd_for_merge(
3011 data: &[u8],
3012 offset: usize,
3013 src_bo: ByteOrderMark,
3014 target_bo: ByteOrderMark,
3015 entries: &mut Vec<exif_writer::IfdEntry>,
3016) {
3017 if offset + 2 > data.len() {
3018 return;
3019 }
3020 let count = read_u16_bo(data, offset, src_bo) as usize;
3021
3022 for i in 0..count {
3023 let eoff = offset + 2 + i * 12;
3024 if eoff + 12 > data.len() {
3025 break;
3026 }
3027
3028 let tag = read_u16_bo(data, eoff, src_bo);
3029 let dtype = read_u16_bo(data, eoff + 2, src_bo);
3030 let count_val = read_u32_bo(data, eoff + 4, src_bo);
3031
3032 if tag == 0x8769 || tag == 0x8825 || tag == 0xA005 || tag == 0x927C {
3034 continue;
3035 }
3036
3037 let type_size = match dtype {
3038 1 | 2 | 6 | 7 => 1usize,
3039 3 | 8 => 2,
3040 4 | 9 | 11 | 13 => 4,
3041 5 | 10 | 12 => 8,
3042 _ => continue,
3043 };
3044
3045 let total_size = type_size * count_val as usize;
3046 let raw_data = if total_size <= 4 {
3047 data[eoff + 8..eoff + 12].to_vec()
3048 } else {
3049 let voff = read_u32_bo(data, eoff + 8, src_bo) as usize;
3050 if voff + total_size > data.len() {
3051 continue;
3052 }
3053 data[voff..voff + total_size].to_vec()
3054 };
3055
3056 let final_data = if src_bo != target_bo && type_size > 1 {
3058 reencode_bytes(&raw_data, dtype, count_val as usize, src_bo, target_bo)
3059 } else {
3060 raw_data[..total_size].to_vec()
3061 };
3062
3063 let format = match dtype {
3064 1 => exif_writer::ExifFormat::Byte,
3065 2 => exif_writer::ExifFormat::Ascii,
3066 3 => exif_writer::ExifFormat::Short,
3067 4 => exif_writer::ExifFormat::Long,
3068 5 => exif_writer::ExifFormat::Rational,
3069 6 => exif_writer::ExifFormat::SByte,
3070 7 => exif_writer::ExifFormat::Undefined,
3071 8 => exif_writer::ExifFormat::SShort,
3072 9 => exif_writer::ExifFormat::SLong,
3073 10 => exif_writer::ExifFormat::SRational,
3074 11 => exif_writer::ExifFormat::Float,
3075 12 => exif_writer::ExifFormat::Double,
3076 _ => continue,
3077 };
3078
3079 entries.push(exif_writer::IfdEntry {
3080 tag,
3081 format,
3082 data: final_data,
3083 });
3084 }
3085}
3086
3087fn reencode_bytes(
3089 data: &[u8],
3090 dtype: u16,
3091 count: usize,
3092 src_bo: ByteOrderMark,
3093 dst_bo: ByteOrderMark,
3094) -> Vec<u8> {
3095 let mut out = Vec::with_capacity(data.len());
3096 match dtype {
3097 3 | 8 => {
3098 for i in 0..count {
3100 let v = read_u16_bo(data, i * 2, src_bo);
3101 match dst_bo {
3102 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
3103 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
3104 }
3105 }
3106 }
3107 4 | 9 | 11 | 13 => {
3108 for i in 0..count {
3110 let v = read_u32_bo(data, i * 4, src_bo);
3111 match dst_bo {
3112 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
3113 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
3114 }
3115 }
3116 }
3117 5 | 10 => {
3118 for i in 0..count {
3120 let n = read_u32_bo(data, i * 8, src_bo);
3121 let d = read_u32_bo(data, i * 8 + 4, src_bo);
3122 match dst_bo {
3123 ByteOrderMark::LittleEndian => {
3124 out.extend_from_slice(&n.to_le_bytes());
3125 out.extend_from_slice(&d.to_le_bytes());
3126 }
3127 ByteOrderMark::BigEndian => {
3128 out.extend_from_slice(&n.to_be_bytes());
3129 out.extend_from_slice(&d.to_be_bytes());
3130 }
3131 }
3132 }
3133 }
3134 12 => {
3135 for i in 0..count {
3137 let mut bytes = [0u8; 8];
3138 bytes.copy_from_slice(&data[i * 8..i * 8 + 8]);
3139 if src_bo != dst_bo {
3140 bytes.reverse();
3141 }
3142 out.extend_from_slice(&bytes);
3143 }
3144 }
3145 _ => out.extend_from_slice(data),
3146 }
3147 out
3148}
3149
3150fn read_u16_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
3151 if offset + 2 > data.len() {
3152 return 0;
3153 }
3154 match bo {
3155 ByteOrderMark::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
3156 ByteOrderMark::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
3157 }
3158}
3159
3160fn read_u32_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
3161 if offset + 4 > data.len() {
3162 return 0;
3163 }
3164 match bo {
3165 ByteOrderMark::LittleEndian => u32::from_le_bytes([
3166 data[offset],
3167 data[offset + 1],
3168 data[offset + 2],
3169 data[offset + 3],
3170 ]),
3171 ByteOrderMark::BigEndian => u32::from_be_bytes([
3172 data[offset],
3173 data[offset + 1],
3174 data[offset + 2],
3175 data[offset + 3],
3176 ]),
3177 }
3178}
3179
3180fn tag_name_to_id(name: &str) -> Option<u16> {
3182 encode_exif_tag(name, "", "", ByteOrderMark::BigEndian).map(|(id, _, _)| id)
3183}
3184
3185fn value_to_filename(value: &str) -> String {
3187 value
3188 .chars()
3189 .map(|c| match c {
3190 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
3191 c if c.is_control() => '_',
3192 c => c,
3193 })
3194 .collect::<String>()
3195 .trim()
3196 .to_string()
3197}
3198
3199pub fn parse_date_shift(shift: &str) -> Option<(i32, u32, u32, u32)> {
3202 let (sign, rest) = if let Some(stripped) = shift.strip_prefix('-') {
3203 (-1, stripped)
3204 } else if let Some(stripped) = shift.strip_prefix('+') {
3205 (1, stripped)
3206 } else {
3207 (1, shift)
3208 };
3209
3210 let parts: Vec<&str> = rest.split(':').collect();
3211 match parts.len() {
3212 1 => {
3213 let h: u32 = parts[0].parse().ok()?;
3214 Some((sign, h, 0, 0))
3215 }
3216 2 => {
3217 let h: u32 = parts[0].parse().ok()?;
3218 let m: u32 = parts[1].parse().ok()?;
3219 Some((sign, h, m, 0))
3220 }
3221 3 => {
3222 let h: u32 = parts[0].parse().ok()?;
3223 let m: u32 = parts[1].parse().ok()?;
3224 let s: u32 = parts[2].parse().ok()?;
3225 Some((sign, h, m, s))
3226 }
3227 _ => None,
3228 }
3229}
3230
3231pub fn shift_datetime(datetime: &str, shift: &str) -> Option<String> {
3234 let (sign, hours, minutes, seconds) = parse_date_shift(shift)?;
3235
3236 if datetime.len() < 19 {
3238 return None;
3239 }
3240 let year: i32 = datetime[0..4].parse().ok()?;
3241 let month: u32 = datetime[5..7].parse().ok()?;
3242 let day: u32 = datetime[8..10].parse().ok()?;
3243 let hour: u32 = datetime[11..13].parse().ok()?;
3244 let min: u32 = datetime[14..16].parse().ok()?;
3245 let sec: u32 = datetime[17..19].parse().ok()?;
3246
3247 let total_secs = (hour * 3600 + min * 60 + sec) as i64
3249 + sign as i64 * (hours * 3600 + minutes * 60 + seconds) as i64;
3250
3251 let days_shift = if total_secs < 0 {
3252 -1 - (-total_secs - 1) / 86400
3253 } else {
3254 total_secs / 86400
3255 };
3256
3257 let time_secs = ((total_secs % 86400) + 86400) % 86400;
3258 let new_hour = (time_secs / 3600) as u32;
3259 let new_min = ((time_secs % 3600) / 60) as u32;
3260 let new_sec = (time_secs % 60) as u32;
3261
3262 let mut new_day = day as i32 + days_shift as i32;
3264 let mut new_month = month;
3265 let mut new_year = year;
3266
3267 let days_in_month = |m: u32, y: i32| -> i32 {
3268 match m {
3269 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3270 4 | 6 | 9 | 11 => 30,
3271 2 => {
3272 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
3273 29
3274 } else {
3275 28
3276 }
3277 }
3278 _ => 30,
3279 }
3280 };
3281
3282 while new_day > days_in_month(new_month, new_year) {
3283 new_day -= days_in_month(new_month, new_year);
3284 new_month += 1;
3285 if new_month > 12 {
3286 new_month = 1;
3287 new_year += 1;
3288 }
3289 }
3290 while new_day < 1 {
3291 new_month = if new_month == 1 { 12 } else { new_month - 1 };
3292 if new_month == 12 {
3293 new_year -= 1;
3294 }
3295 new_day += days_in_month(new_month, new_year);
3296 }
3297
3298 Some(format!(
3299 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
3300 new_year, new_month, new_day, new_hour, new_min, new_sec
3301 ))
3302}
3303
3304const FILE_LEVEL_GROUPS: &[(&str, &str, &str, &str)] = &[
3317 ("CurrentIPTCDigest", "File", "File", "Image"),
3321 ("Directory", "File", "System", "Other"),
3322 ("Error", "ExifTool", "ExifTool", "ExifTool"),
3323 ("ExifToolVersion", "ExifTool", "ExifTool", "ExifTool"),
3324 ("FileAccessDate", "File", "System", "Time"),
3325 ("FileCreateDate", "File", "System", "Time"),
3326 ("FileInodeChangeDate", "File", "System", "Time"),
3327 ("FileModifyDate", "File", "System", "Time"),
3328 ("FileName", "File", "System", "Other"),
3329 ("FilePermissions", "File", "System", "Other"),
3330 ("FileSize", "File", "System", "Other"),
3331 ("Warning", "ExifTool", "ExifTool", "ExifTool"),
3332];
3333
3334fn file_level_group(name: &str) -> Option<(&'static str, &'static str, &'static str)> {
3337 FILE_LEVEL_GROUPS
3338 .iter()
3339 .find(|(n, ..)| *n == name)
3340 .map(|&(_, f0, f1, f2)| (f0, f1, f2))
3341}
3342
3343#[cfg(unix)]
3350fn format_file_permissions(mode: u32) -> String {
3351 let type_char = match mode & 0o170000 {
3352 0o010000 => 'p', 0o020000 => 'c', 0o040000 => 'd', 0o060000 => 'b', 0o120000 => 'l', 0o140000 => 's', _ => '-',
3359 };
3360 let mut s = String::with_capacity(10);
3361 s.push(type_char);
3362 let mut mask = 0o400u32;
3363 while mask > 0 {
3364 for ch in ['r', 'w', 'x'] {
3365 s.push(if mode & mask != 0 { ch } else { '-' });
3366 mask >>= 1;
3367 }
3368 }
3369 s
3370}
3371
3372enum FileData {
3376 Mapped(memmap2::Mmap),
3377 Owned(Vec<u8>),
3378}
3379
3380impl std::ops::Deref for FileData {
3381 type Target = [u8];
3382 fn deref(&self) -> &[u8] {
3383 match self {
3384 FileData::Mapped(m) => m,
3385 FileData::Owned(v) => v,
3386 }
3387 }
3388}
3389
3390fn map_file_for_read(path: &Path) -> Result<FileData> {
3393 let file = fs::File::open(path).map_err(Error::Io)?;
3394 let len = file.metadata().map_err(Error::Io)?.len();
3395 if len == 0 {
3396 return Ok(FileData::Owned(Vec::new()));
3397 }
3398 match unsafe { memmap2::Mmap::map(&file) } {
3403 Ok(m) => Ok(FileData::Mapped(m)),
3404 Err(_) => Ok(FileData::Owned(fs::read(path).map_err(Error::Io)?)),
3405 }
3406}
3407
3408fn format_file_size(bytes: u64) -> String {
3410 let v = bytes as f64;
3411 if bytes < 2000 {
3412 format!("{} bytes", bytes)
3413 } else if bytes < 10_000 {
3414 format!("{:.1} kB", v / 1000.0)
3415 } else if bytes < 2_000_000 {
3416 format!("{:.0} kB", v / 1000.0)
3417 } else if bytes < 10_000_000 {
3418 format!("{:.1} MB", v / 1_000_000.0)
3419 } else if bytes < 2_000_000_000 {
3420 format!("{:.0} MB", v / 1_000_000.0)
3421 } else if bytes < 10_000_000_000 {
3422 format!("{:.1} GB", v / 1_000_000_000.0)
3423 } else {
3424 format!("{:.0} GB", v / 1_000_000_000.0)
3425 }
3426}
3427
3428fn is_xmp_tag(tag: &str) -> bool {
3430 matches!(
3431 tag.to_lowercase().as_str(),
3432 "title"
3433 | "description"
3434 | "subject"
3435 | "creator"
3436 | "rights"
3437 | "keywords"
3438 | "rating"
3439 | "label"
3440 | "hierarchicalsubject"
3441 )
3442}
3443
3444fn encode_exif_tag(
3447 tag_name: &str,
3448 value: &str,
3449 _group: &str,
3450 bo: ByteOrderMark,
3451) -> Option<(u16, exif_writer::ExifFormat, Vec<u8>)> {
3452 let tag_lower = tag_name.to_lowercase();
3453
3454 let (tag_id, format): (u16, exif_writer::ExifFormat) = match tag_lower.as_str() {
3456 "imagedescription" => (0x010E, exif_writer::ExifFormat::Ascii),
3458 "make" => (0x010F, exif_writer::ExifFormat::Ascii),
3459 "model" => (0x0110, exif_writer::ExifFormat::Ascii),
3460 "software" => (0x0131, exif_writer::ExifFormat::Ascii),
3461 "modifydate" | "datetime" => (0x0132, exif_writer::ExifFormat::Ascii),
3462 "artist" => (0x013B, exif_writer::ExifFormat::Ascii),
3463 "copyright" => (0x8298, exif_writer::ExifFormat::Ascii),
3464 "orientation" => (0x0112, exif_writer::ExifFormat::Short),
3466 "xresolution" => (0x011A, exif_writer::ExifFormat::Rational),
3467 "yresolution" => (0x011B, exif_writer::ExifFormat::Rational),
3468 "resolutionunit" => (0x0128, exif_writer::ExifFormat::Short),
3469 "datetimeoriginal" => (0x9003, exif_writer::ExifFormat::Ascii),
3471 "createdate" | "datetimedigitized" => (0x9004, exif_writer::ExifFormat::Ascii),
3472 "usercomment" => (0x9286, exif_writer::ExifFormat::Undefined),
3473 "imageuniqueid" => (0xA420, exif_writer::ExifFormat::Ascii),
3474 "ownername" | "cameraownername" => (0xA430, exif_writer::ExifFormat::Ascii),
3475 "serialnumber" | "bodyserialnumber" => (0xA431, exif_writer::ExifFormat::Ascii),
3476 "lensmake" => (0xA433, exif_writer::ExifFormat::Ascii),
3477 "lensmodel" => (0xA434, exif_writer::ExifFormat::Ascii),
3478 "lensserialnumber" => (0xA435, exif_writer::ExifFormat::Ascii),
3479 _ => return None,
3480 };
3481
3482 let encoded = match format {
3483 exif_writer::ExifFormat::Ascii => exif_writer::encode_ascii(value),
3484 exif_writer::ExifFormat::Short => {
3485 let v: u16 = value.parse().ok()?;
3486 exif_writer::encode_u16(v, bo)
3487 }
3488 exif_writer::ExifFormat::Long => {
3489 let v: u32 = value.parse().ok()?;
3490 exif_writer::encode_u32(v, bo)
3491 }
3492 exif_writer::ExifFormat::Rational => {
3493 if let Some(slash) = value.find('/') {
3495 let num: u32 = value[..slash].trim().parse().ok()?;
3496 let den: u32 = value[slash + 1..].trim().parse().ok()?;
3497 exif_writer::encode_urational(num, den, bo)
3498 } else if let Ok(v) = value.parse::<f64>() {
3499 let den = 10000u32;
3501 let num = (v * den as f64).round() as u32;
3502 exif_writer::encode_urational(num, den, bo)
3503 } else {
3504 return None;
3505 }
3506 }
3507 exif_writer::ExifFormat::Undefined => {
3508 let mut data = vec![0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00]; data.extend_from_slice(value.as_bytes());
3511 data
3512 }
3513 _ => return None,
3514 };
3515
3516 Some((tag_id, format, encoded))
3517}
3518
3519fn compute_text_tags(data: &[u8], is_csv: bool) -> Vec<Tag> {
3521 let mut tags = Vec::new();
3522 let mk = |name: &str, val: String| Tag {
3523 id: crate::tag::TagId::Text(name.into()),
3524 name: name.into(),
3525 description: name.into(),
3526 group: crate::tag::TagGroup {
3527 family0: "File".into(),
3528 family1: "File".into(),
3529 family2: "Other".into(),
3530 family3: "Main".into(),
3531 },
3532 raw_value: Value::String(val.clone()),
3533 print_value: val,
3534 priority: 0,
3535 };
3536
3537 let is_ascii = data.iter().all(|&b| b < 128);
3539 let has_utf8_bom = data.starts_with(&[0xEF, 0xBB, 0xBF]);
3540 let has_utf16le_bom =
3541 data.starts_with(&[0xFF, 0xFE]) && !data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
3542 let has_utf16be_bom = data.starts_with(&[0xFE, 0xFF]);
3543 let has_utf32le_bom = data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
3544 let has_utf32be_bom = data.starts_with(&[0x00, 0x00, 0xFE, 0xFF]);
3545
3546 let has_weird_ctrl = data.iter().any(|&b| {
3548 (b <= 0x06) || (0x0e..=0x1a).contains(&b) || (0x1c..=0x1f).contains(&b) || b == 0x7f
3549 });
3550
3551 let (encoding, is_bom, is_utf16) = if has_utf32le_bom {
3552 ("utf-32le", true, false)
3553 } else if has_utf32be_bom {
3554 ("utf-32be", true, false)
3555 } else if has_utf16le_bom {
3556 ("utf-16le", true, true)
3557 } else if has_utf16be_bom {
3558 ("utf-16be", true, true)
3559 } else if has_weird_ctrl {
3560 return tags;
3562 } else if is_ascii {
3563 ("us-ascii", false, false)
3564 } else {
3565 let is_valid_utf8 = std::str::from_utf8(data).is_ok();
3567 if is_valid_utf8 {
3568 if has_utf8_bom {
3569 ("utf-8", true, false)
3570 } else {
3571 ("utf-8", false, false)
3575 }
3576 } else if !data.iter().any(|&b| (0x80..=0x9f).contains(&b)) {
3577 ("iso-8859-1", false, false)
3578 } else {
3579 ("unknown-8bit", false, false)
3580 }
3581 };
3582
3583 tags.push(mk("MIMEEncoding", encoding.into()));
3584
3585 if is_bom {
3586 tags.push(mk("ByteOrderMark", "Yes".into()));
3587 }
3588
3589 let has_cr = data.contains(&b'\r');
3591 let has_lf = data.contains(&b'\n');
3592 let newline_type = if has_cr && has_lf {
3593 "Windows CRLF"
3594 } else if has_lf {
3595 "Unix LF"
3596 } else if has_cr {
3597 "Macintosh CR"
3598 } else {
3599 "(none)"
3600 };
3601 tags.push(mk("Newlines", newline_type.into()));
3602
3603 if is_csv {
3604 let text = crate::encoding::decode_utf8_or_latin1(data);
3606 let mut delim = "";
3607 let mut quot = "";
3608 let mut ncols = 1usize;
3609 let mut nrows = 0usize;
3610
3611 for line in text.lines() {
3612 if nrows == 0 {
3613 let comma_count = line.matches(',').count();
3615 let semi_count = line.matches(';').count();
3616 let tab_count = line.matches('\t').count();
3617 if comma_count > semi_count && comma_count > tab_count {
3618 delim = ",";
3619 ncols = comma_count + 1;
3620 } else if semi_count > tab_count {
3621 delim = ";";
3622 ncols = semi_count + 1;
3623 } else if tab_count > 0 {
3624 delim = "\t";
3625 ncols = tab_count + 1;
3626 } else {
3627 delim = "";
3628 ncols = 1;
3629 }
3630 if line.contains('"') {
3632 quot = "\"";
3633 } else if line.contains('\'') {
3634 quot = "'";
3635 }
3636 }
3637 nrows += 1;
3638 if nrows >= 1000 {
3639 break;
3640 }
3641 }
3642
3643 let delim_display = match delim {
3644 "," => "Comma",
3645 ";" => "Semicolon",
3646 "\t" => "Tab",
3647 _ => "(none)",
3648 };
3649 let quot_display = match quot {
3650 "\"" => "Double quotes",
3651 "'" => "Single quotes",
3652 _ => "(none)",
3653 };
3654
3655 tags.push(mk("Delimiter", delim_display.into()));
3656 tags.push(mk("Quoting", quot_display.into()));
3657 tags.push(mk("ColumnCount", ncols.to_string()));
3658 if nrows > 0 {
3659 tags.push(mk("RowCount", nrows.to_string()));
3660 }
3661 } else if !is_utf16 {
3662 let nl_count = data.iter().filter(|&&b| b == b'\n').count();
3666 let line_count = if !data.is_empty() && data.last() != Some(&b'\n') {
3667 nl_count + 1
3668 } else {
3669 nl_count
3670 };
3671 tags.push(mk("LineCount", line_count.to_string()));
3672
3673 let text = crate::encoding::decode_utf8_or_latin1(data);
3674 let word_count = text.split_whitespace().count();
3675 tags.push(mk("WordCount", word_count.to_string()));
3676 }
3677
3678 tags
3679}
3680
3681#[cfg(test)]
3682mod tests {
3683 use super::*;
3684
3685 #[test]
3686 fn new_has_default_options() {
3687 let et = ExifTool::new();
3688 assert!(!et.options().duplicates);
3689 assert!(et.options().print_conv);
3690 assert_eq!(et.options().fast_scan, 0);
3691 assert!(et.options().requested_tags.is_empty());
3692 assert_eq!(et.options().extract_embedded, 0);
3693 assert_eq!(et.options().show_unknown, 0);
3694 assert!(!et.options().process_compressed);
3695 assert!(!et.options().use_mwg);
3696 }
3697
3698 #[test]
3699 fn tag_matches_request_group_qualified() {
3700 let tag = Tag {
3701 id: crate::tag::TagId::Text("By-line".into()),
3702 name: "By-line".into(),
3703 description: "By-line".into(),
3704 group: crate::tag::TagGroup {
3705 family0: "IPTC".into(),
3706 family1: "IPTC".into(),
3707 family2: "Author".into(),
3708 family3: "Main".into(),
3709 },
3710 raw_value: Value::String("Martín".into()),
3711 print_value: "Martín".into(),
3712 priority: 1,
3713 };
3714 assert!(ExifTool::tag_matches_request(&tag, "By-line"));
3716 assert!(ExifTool::tag_matches_request(&tag, "by-line"));
3717 assert!(ExifTool::tag_matches_request(&tag, "IPTC:By-line"));
3719 assert!(ExifTool::tag_matches_request(&tag, "Author:By-line"));
3720 assert!(ExifTool::tag_matches_request(&tag, "IPTC:*"));
3722 assert!(ExifTool::tag_matches_request(&tag, "*"));
3723 assert!(!ExifTool::tag_matches_request(&tag, "EXIF:By-line"));
3725 assert!(!ExifTool::tag_matches_request(&tag, "IPTC:Make"));
3726 assert!(!ExifTool::tag_matches_request(&tag, "Headline"));
3727 }
3728
3729 #[test]
3730 fn with_options_preserves_custom() {
3731 let opts = Options {
3732 duplicates: true,
3733 print_conv: false,
3734 fast_scan: 2,
3735 requested_tags: vec!["Artist".to_string()],
3736 extract_embedded: 1,
3737 show_unknown: 1,
3738 process_compressed: true,
3739 use_mwg: true,
3740 geolocation: true,
3741 };
3742 let et = ExifTool::with_options(opts.clone());
3743 assert!(et.options().duplicates);
3744 assert!(!et.options().print_conv);
3745 assert_eq!(et.options().fast_scan, 2);
3746 assert_eq!(et.options().requested_tags, vec!["Artist".to_string()]);
3747 assert_eq!(et.options().extract_embedded, 1);
3748 assert_eq!(et.options().show_unknown, 1);
3749 assert!(et.options().process_compressed);
3750 assert!(et.options().use_mwg);
3751 }
3752
3753 #[test]
3754 fn set_new_value_simple_tag() {
3755 let mut et = ExifTool::new();
3756 et.set_new_value("Artist", Some("John"));
3757 assert_eq!(et.new_values.len(), 1);
3758 assert_eq!(et.new_values[0].tag, "Artist");
3759 assert_eq!(et.new_values[0].group, None);
3760 assert_eq!(et.new_values[0].value, Some("John".to_string()));
3761 }
3762
3763 #[test]
3764 fn set_new_value_with_group_prefix() {
3765 let mut et = ExifTool::new();
3766 et.set_new_value("XMP:Title", Some("Test"));
3767 assert_eq!(et.new_values.len(), 1);
3768 assert_eq!(et.new_values[0].tag, "Title");
3769 assert_eq!(et.new_values[0].group, Some("XMP".to_string()));
3770 assert_eq!(et.new_values[0].value, Some("Test".to_string()));
3771 }
3772
3773 #[test]
3774 fn set_new_value_delete() {
3775 let mut et = ExifTool::new();
3776 et.set_new_value("Comment", None);
3777 assert_eq!(et.new_values.len(), 1);
3778 assert_eq!(et.new_values[0].tag, "Comment");
3779 assert_eq!(et.new_values[0].value, None);
3780 }
3781
3782 #[test]
3783 fn clear_new_values_empties_queue() {
3784 let mut et = ExifTool::new();
3785 et.set_new_value("Artist", Some("A"));
3786 et.set_new_value("Copyright", Some("B"));
3787 assert_eq!(et.new_values.len(), 2);
3788 et.clear_new_values();
3789 assert!(et.new_values.is_empty());
3790 }
3791
3792 #[test]
3793 fn set_new_value_multiple() {
3794 let mut et = ExifTool::new();
3795 et.set_new_value("Artist", Some("John"));
3796 et.set_new_value("IPTC:Keywords", Some("test"));
3797 et.set_new_value("XMP:Subject", None);
3798 assert_eq!(et.new_values.len(), 3);
3799 assert_eq!(et.new_values[1].group, Some("IPTC".to_string()));
3800 assert_eq!(et.new_values[1].tag, "Keywords");
3801 assert_eq!(et.new_values[2].value, None);
3802 }
3803
3804 #[test]
3805 fn options_mut_modifies() {
3806 let mut et = ExifTool::new();
3807 et.options_mut().duplicates = true;
3808 et.options_mut().fast_scan = 3;
3809 assert!(et.options().duplicates);
3810 assert_eq!(et.options().fast_scan, 3);
3811 }
3812
3813 #[test]
3814 fn default_options() {
3815 let opts = Options::default();
3816 assert!(!opts.duplicates);
3817 assert!(opts.print_conv);
3818 assert_eq!(opts.fast_scan, 0);
3819 }
3820}