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() {
695 let records: Vec<iptc_writer::IptcRecord> = iptc_values
696 .iter()
697 .filter_map(|nv| {
698 let value = nv.value.as_deref()?;
699 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
700 Some(iptc_writer::IptcRecord {
701 record,
702 dataset,
703 data: crate::encoding::encode_latin1(value),
707 })
708 })
709 .collect();
710 if records.is_empty() {
711 None
712 } else {
713 Some(iptc_writer::build_iptc(&records))
714 }
715 } else {
716 None
717 };
718
719 jpeg_writer::write_jpeg(
721 data,
722 new_exif.as_deref(),
723 new_xmp.as_deref(),
724 new_iptc_data.as_deref(),
725 comment_value,
726 remove_exif,
727 remove_xmp,
728 remove_iptc,
729 remove_comment,
730 )
731 }
732
733 fn build_new_exif(&self, jpeg_data: &[u8], values: &[&NewValue]) -> Result<Vec<u8>> {
735 let bo = ByteOrderMark::BigEndian;
736 let mut ifd0_entries = Vec::new();
737 let mut exif_entries = Vec::new();
738 let mut gps_entries = Vec::new();
739
740 let existing = extract_existing_exif_entries(jpeg_data, bo);
742 for entry in &existing {
743 match classify_exif_tag(entry.tag) {
744 ExifIfdGroup::Ifd0 => ifd0_entries.push(entry.clone()),
745 ExifIfdGroup::ExifIfd => exif_entries.push(entry.clone()),
746 ExifIfdGroup::Gps => gps_entries.push(entry.clone()),
747 }
748 }
749
750 let deleted_tags: Vec<u16> = values
752 .iter()
753 .filter(|nv| nv.value.is_none())
754 .filter_map(|nv| tag_name_to_id(&nv.tag))
755 .collect();
756
757 ifd0_entries.retain(|e| !deleted_tags.contains(&e.tag));
759 exif_entries.retain(|e| !deleted_tags.contains(&e.tag));
760 gps_entries.retain(|e| !deleted_tags.contains(&e.tag));
761
762 for nv in values {
764 if nv.value.is_none() {
765 continue;
766 }
767 let value_str = nv.value.as_deref().unwrap_or("");
768 let group = nv.group.as_deref().unwrap_or("");
769
770 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, value_str, group, bo)
771 {
772 let entry = exif_writer::IfdEntry {
773 tag: tag_id,
774 format,
775 data: encoded,
776 };
777
778 let target = match group.to_uppercase().as_str() {
779 "GPS" => &mut gps_entries,
780 "EXIFIFD" => &mut exif_entries,
781 _ => match classify_exif_tag(tag_id) {
782 ExifIfdGroup::ExifIfd => &mut exif_entries,
783 ExifIfdGroup::Gps => &mut gps_entries,
784 ExifIfdGroup::Ifd0 => &mut ifd0_entries,
785 },
786 };
787
788 if let Some(existing) = target.iter_mut().find(|e| e.tag == tag_id) {
790 *existing = entry;
791 } else {
792 target.push(entry);
793 }
794 }
795 }
796
797 ifd0_entries.retain(|e| e.tag != 0x8769 && e.tag != 0x8825 && e.tag != 0xA005);
799
800 exif_writer::build_exif(&ifd0_entries, &exif_entries, &gps_entries, bo)
801 }
802
803 fn write_png(&self, data: &[u8]) -> Result<Vec<u8>> {
805 let mut new_text: Vec<(&str, &str)> = Vec::new();
806 let mut remove_text: Vec<&str> = Vec::new();
807
808 let owned_pairs: Vec<(String, String)> = self
811 .new_values
812 .iter()
813 .filter(|nv| nv.value.is_some())
814 .map(|nv| (nv.tag.clone(), nv.value.clone().unwrap()))
815 .collect();
816
817 for (tag, value) in &owned_pairs {
818 new_text.push((tag.as_str(), value.as_str()));
819 }
820
821 for nv in &self.new_values {
822 if nv.value.is_none() {
823 remove_text.push(&nv.tag);
824 }
825 }
826
827 png_writer::write_png(data, &new_text, None, &remove_text)
828 }
829
830 fn write_psd(&self, data: &[u8]) -> Result<Vec<u8>> {
832 let mut iptc_values = Vec::new();
833 let mut xmp_values = Vec::new();
834
835 for nv in &self.new_values {
836 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
837 match group.as_str() {
838 "XMP" => xmp_values.push(nv),
839 "IPTC" => iptc_values.push(nv),
840 _ => {
841 if is_xmp_tag(&nv.tag) {
842 xmp_values.push(nv);
843 } else {
844 iptc_values.push(nv);
845 }
846 }
847 }
848 }
849
850 let new_iptc = if !iptc_values.is_empty() {
851 let records: Vec<_> = iptc_values
852 .iter()
853 .filter_map(|nv| {
854 let value = nv.value.as_deref()?;
855 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
856 Some(iptc_writer::IptcRecord {
857 record,
858 dataset,
859 data: crate::encoding::encode_latin1(value),
863 })
864 })
865 .collect();
866 if records.is_empty() {
867 None
868 } else {
869 Some(iptc_writer::build_iptc(&records))
870 }
871 } else {
872 None
873 };
874
875 let new_xmp = if !xmp_values.is_empty() {
876 let refs: Vec<&NewValue> = xmp_values.to_vec();
877 Some(self.build_new_xmp(&refs))
878 } else {
879 None
880 };
881
882 psd_writer::write_psd(data, new_iptc.as_deref(), new_xmp.as_deref())
883 }
884
885 fn write_matroska(&self, data: &[u8]) -> Result<Vec<u8>> {
887 let changes: Vec<(&str, &str)> = self
888 .new_values
889 .iter()
890 .filter_map(|nv| {
891 let value = nv.value.as_deref()?;
892 Some((nv.tag.as_str(), value))
893 })
894 .collect();
895
896 matroska_writer::write_matroska(data, &changes)
897 }
898
899 fn write_pdf(&self, data: &[u8]) -> Result<Vec<u8>> {
901 let changes: Vec<(&str, &str)> = self
902 .new_values
903 .iter()
904 .filter_map(|nv| {
905 let value = nv.value.as_deref()?;
906 Some((nv.tag.as_str(), value))
907 })
908 .collect();
909
910 pdf_writer::write_pdf(data, &changes)
911 }
912
913 fn write_mp4(&self, data: &[u8]) -> Result<Vec<u8>> {
915 let mut ilst_tags: Vec<([u8; 4], String)> = Vec::new();
916 let mut xmp_values: Vec<&NewValue> = Vec::new();
917
918 for nv in &self.new_values {
919 if nv.value.is_none() {
920 continue;
921 }
922 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
923 if group == "XMP" {
924 xmp_values.push(nv);
925 } else if let Some(key) = mp4_writer::tag_to_ilst_key(&nv.tag) {
926 ilst_tags.push((key, nv.value.clone().unwrap()));
927 }
928 }
929
930 let tag_refs: Vec<(&[u8; 4], &str)> =
931 ilst_tags.iter().map(|(k, v)| (k, v.as_str())).collect();
932
933 let new_xmp = if !xmp_values.is_empty() {
934 let refs: Vec<&NewValue> = xmp_values.to_vec();
935 Some(self.build_new_xmp(&refs))
936 } else {
937 None
938 };
939
940 mp4_writer::write_mp4(data, &tag_refs, new_xmp.as_deref())
941 }
942
943 fn write_webp(&self, data: &[u8]) -> Result<Vec<u8>> {
945 let mut exif_values: Vec<&NewValue> = Vec::new();
946 let mut xmp_values: Vec<&NewValue> = Vec::new();
947 let mut remove_exif = false;
948 let mut remove_xmp = false;
949
950 for nv in &self.new_values {
951 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
952 if nv.value.is_none() && nv.tag == "*" {
953 if group == "EXIF" {
954 remove_exif = true;
955 }
956 if group == "XMP" {
957 remove_xmp = true;
958 }
959 continue;
960 }
961 match group.as_str() {
962 "XMP" => xmp_values.push(nv),
963 _ => exif_values.push(nv),
964 }
965 }
966
967 let new_exif = if !exif_values.is_empty() {
968 let bo = ByteOrderMark::BigEndian;
969 let mut entries = Vec::new();
970 for nv in &exif_values {
971 if let Some(ref v) = nv.value {
972 let group = nv.group.as_deref().unwrap_or("");
973 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, v, group, bo)
974 {
975 entries.push(exif_writer::IfdEntry {
976 tag: tag_id,
977 format,
978 data: encoded,
979 });
980 }
981 }
982 }
983 if !entries.is_empty() {
984 Some(exif_writer::build_exif(&entries, &[], &[], bo)?)
985 } else {
986 None
987 }
988 } else {
989 None
990 };
991
992 let new_xmp = if !xmp_values.is_empty() {
993 Some(self.build_new_xmp(&xmp_values.to_vec()))
994 } else {
995 None
996 };
997
998 webp_writer::write_webp(
999 data,
1000 new_exif.as_deref(),
1001 new_xmp.as_deref(),
1002 remove_exif,
1003 remove_xmp,
1004 )
1005 }
1006
1007 fn write_tiff(&self, data: &[u8]) -> Result<Vec<u8>> {
1009 let bo = if data.starts_with(b"II") {
1010 ByteOrderMark::LittleEndian
1011 } else {
1012 ByteOrderMark::BigEndian
1013 };
1014
1015 let mut changes: Vec<(u16, Vec<u8>)> = Vec::new();
1016 for nv in &self.new_values {
1017 if let Some(ref value) = nv.value {
1018 let group = nv.group.as_deref().unwrap_or("");
1019 if let Some((tag_id, _format, encoded)) = encode_exif_tag(&nv.tag, value, group, bo)
1020 {
1021 changes.push((tag_id, encoded));
1022 }
1023 }
1024 }
1025
1026 tiff_writer::write_tiff(data, &changes)
1027 }
1028
1029 fn build_new_xmp(&self, values: &[&NewValue]) -> Vec<u8> {
1031 let mut properties = Vec::new();
1032
1033 for nv in values {
1034 let value_str = match &nv.value {
1035 Some(v) => v.clone(),
1036 None => continue,
1037 };
1038
1039 let ns = nv.group.as_deref().unwrap_or("dc").to_lowercase();
1040 let ns = if ns == "xmp" { "xmp".to_string() } else { ns };
1041
1042 let prop_type = match nv.tag.to_lowercase().as_str() {
1043 "title" | "description" | "rights" => xmp_writer::XmpPropertyType::LangAlt,
1044 "subject" | "keywords" => xmp_writer::XmpPropertyType::Bag,
1045 "creator" => xmp_writer::XmpPropertyType::Seq,
1046 _ => xmp_writer::XmpPropertyType::Simple,
1047 };
1048
1049 let values = if matches!(
1050 prop_type,
1051 xmp_writer::XmpPropertyType::Bag | xmp_writer::XmpPropertyType::Seq
1052 ) {
1053 value_str.split(',').map(|s| s.trim().to_string()).collect()
1054 } else {
1055 vec![value_str]
1056 };
1057
1058 properties.push(xmp_writer::XmpProperty {
1059 namespace: ns,
1060 property: nv.tag.clone(),
1061 values,
1062 prop_type,
1063 });
1064 }
1065
1066 xmp_writer::build_xmp(&properties).into_bytes()
1067 }
1068
1069 pub fn image_info<P: AsRef<Path>>(&self, path: P) -> Result<ImageInfo> {
1077 let tags = self.extract_info(path)?;
1078 Ok(self.get_info(&tags))
1079 }
1080
1081 pub fn extract_info<P: AsRef<Path>>(&self, path: P) -> Result<Vec<Tag>> {
1085 let path = path.as_ref();
1086 let data = map_file_for_read(path)?;
1092 self.extract_info_from_bytes(&data, path)
1093 }
1094
1095 pub fn extract_info_from_bytes(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
1097 crate::metadata::exif::set_show_unknown(self.options.show_unknown);
1099 crate::metadata::exif::set_keep_duplicates(
1103 self.options.duplicates || self.options.extract_embedded > 0,
1104 );
1105 crate::formats::pdf::set_process_compressed(self.options.process_compressed);
1107
1108 let file_type_result = self.detect_file_type(data, path);
1109 let (file_type, mut tags) = match file_type_result {
1110 Ok(ft) => {
1111 let t = self
1112 .process_file(data, ft)
1113 .or_else(|_| self.process_by_extension(data, path))?;
1114 (Some(ft), t)
1115 }
1116 Err(_) => {
1117 let t = self.process_by_extension(data, path)?;
1119 (None, t)
1120 }
1121 };
1122 let file_type = file_type.unwrap_or(FileType::Zip); let default_tags = || {
1127 (
1128 file_type.code().to_string(),
1129 file_type.mime_type().to_string(),
1130 file_type
1131 .extensions()
1132 .first()
1133 .copied()
1134 .unwrap_or("")
1135 .to_string(),
1136 )
1137 };
1138 let ooxml = if file_type == FileType::Zip {
1143 crate::formats::zip::detect_ooxml_type(data, path.extension().and_then(|e| e.to_str()))
1144 } else {
1145 None
1146 };
1147 let (ft_code, mime_str, ext_str): (String, String, String) = if file_type == FileType::Exe {
1148 exe_subtype(data)
1149 .map(|(ft, mime, ext)| (ft.to_string(), mime.to_string(), ext.to_string()))
1150 .unwrap_or_else(default_tags)
1151 } else if let Some(triple) = ooxml {
1152 triple
1153 } else if let Some((code, mime)) = refine_filetype_by_content(file_type, data) {
1154 let (_, _, ext) = default_tags();
1155 (code, mime, ext)
1156 } else {
1157 default_tags()
1158 };
1159
1160 let mut pre: Vec<Tag> = Vec::new();
1175
1176 let file_tag = |name: &str, val: Value| -> Tag {
1183 Tag {
1184 id: crate::tag::TagId::Text(name.to_string()),
1185 name: name.to_string(),
1186 description: name.to_string(),
1187 group: crate::tag::TagGroup {
1188 family0: "File".into(),
1189 family1: "File".into(),
1190 family2: "Other".into(),
1191 family3: "Main".into(),
1192 },
1193 raw_value: val.clone(),
1194 print_value: val.to_display_string(),
1195 priority: 1,
1196 }
1197 };
1198
1199 pre.push(file_tag(
1200 "ExifToolVersion",
1201 Value::String(crate::VERSION.to_string()),
1202 ));
1203
1204 if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
1205 pre.push(file_tag("FileName", Value::String(fname.to_string())));
1206 }
1207 if let Some(dir) = path.parent().and_then(|p| p.to_str()) {
1208 pre.push(file_tag("Directory", Value::String(dir.to_string())));
1209 }
1210
1211 if let Ok(metadata) = fs::metadata(path) {
1212 pre.push(Tag {
1213 id: crate::tag::TagId::Text("FileSize".into()),
1214 name: "FileSize".into(),
1215 description: "File Size".into(),
1216 group: crate::tag::TagGroup {
1217 family0: "File".into(),
1218 family1: "File".into(),
1219 family2: "Other".into(),
1220 family3: "Main".into(),
1221 },
1222 raw_value: Value::String(metadata.len().to_string()),
1225 print_value: format_file_size(metadata.len()),
1226 priority: 0,
1227 });
1228 }
1229
1230 #[cfg(unix)]
1231 if let Ok(metadata) = fs::metadata(path) {
1232 use std::os::unix::fs::MetadataExt;
1233 let mode = metadata.mode();
1234 use crate::formats::gzip::gzip_unix_to_datetime;
1237 if let Ok(modified) = metadata.modified() {
1239 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
1240 let secs = dur.as_secs() as i64;
1241 pre.push(file_tag(
1242 "FileModifyDate",
1243 Value::String(gzip_unix_to_datetime(secs)),
1244 ));
1245 }
1246 }
1247 if let Ok(accessed) = metadata.accessed() {
1249 if let Ok(dur) = accessed.duration_since(std::time::UNIX_EPOCH) {
1250 let secs = dur.as_secs() as i64;
1251 pre.push(file_tag(
1252 "FileAccessDate",
1253 Value::String(gzip_unix_to_datetime(secs)),
1254 ));
1255 }
1256 }
1257 let ctime = metadata.ctime();
1259 if ctime > 0 {
1260 pre.push(file_tag(
1261 "FileInodeChangeDate",
1262 Value::String(gzip_unix_to_datetime(ctime)),
1263 ));
1264 }
1265
1266 pre.push(Tag {
1270 id: crate::tag::TagId::Text("FilePermissions".into()),
1271 name: "FilePermissions".into(),
1272 description: "FilePermissions".into(),
1273 group: crate::tag::TagGroup {
1274 family0: "File".into(),
1275 family1: "File".into(),
1276 family2: "Other".into(),
1277 family3: "Main".into(),
1278 },
1279 raw_value: Value::String(format!("{:o}", mode)),
1280 print_value: format_file_permissions(mode),
1281 priority: 1,
1282 });
1283 }
1284
1285 pre.push(Tag {
1286 id: crate::tag::TagId::Text("FileType".into()),
1287 name: "FileType".into(),
1288 description: "File Type".into(),
1289 group: crate::tag::TagGroup {
1290 family0: "File".into(),
1291 family1: "File".into(),
1292 family2: "Other".into(),
1293 family3: "Main".into(),
1294 },
1295 raw_value: Value::String(format!("{:?}", file_type)),
1296 print_value: ft_code.clone(),
1299 priority: 1,
1300 });
1301
1302 if !ext_str.is_empty() || file_type == FileType::Exe {
1305 pre.push(file_tag(
1306 "FileTypeExtension",
1307 Value::String(ext_str.clone()),
1308 ));
1309 }
1310
1311 pre.push(Tag {
1312 id: crate::tag::TagId::Text("MIMEType".into()),
1313 name: "MIMEType".into(),
1314 description: "MIME Type".into(),
1315 group: crate::tag::TagGroup {
1316 family0: "File".into(),
1317 family1: "File".into(),
1318 family2: "Other".into(),
1319 family3: "Main".into(),
1320 },
1321 raw_value: Value::String(mime_str.clone()),
1322 print_value: mime_str.clone(),
1323 priority: 1,
1324 });
1325
1326 {
1328 let bo_str = if data.len() > 8 {
1329 let check: Option<&[u8]> = if data.starts_with(&[0xFF, 0xD8]) {
1331 data.windows(6)
1333 .position(|w| w == b"Exif\0\0")
1334 .map(|p| &data[p + 6..])
1335 } else if data.starts_with(b"FUJIFILMCCD-RAW") && data.len() >= 0x60 {
1336 let jpeg_offset =
1338 u32::from_be_bytes([data[0x54], data[0x55], data[0x56], data[0x57]])
1339 as usize;
1340 let jpeg_length =
1341 u32::from_be_bytes([data[0x58], data[0x59], data[0x5A], data[0x5B]])
1342 as usize;
1343 if jpeg_offset > 0 && jpeg_offset + jpeg_length <= data.len() {
1344 let jpeg = &data[jpeg_offset..jpeg_offset + jpeg_length];
1345 jpeg.windows(6)
1346 .position(|w| w == b"Exif\0\0")
1347 .map(|p| &jpeg[p + 6..])
1348 } else {
1349 None
1350 }
1351 } else if data.starts_with(b"RIFF") && data.len() >= 12 {
1352 let mut riff_bo: Option<&[u8]> = None;
1354 let mut pos = 12usize;
1355 while pos + 8 <= data.len() {
1356 let cid = &data[pos..pos + 4];
1357 let csz = u32::from_le_bytes([
1358 data[pos + 4],
1359 data[pos + 5],
1360 data[pos + 6],
1361 data[pos + 7],
1362 ]) as usize;
1363 let cstart = pos + 8;
1364 let cend = (cstart + csz).min(data.len());
1365 if cid == b"EXIF" && cend > cstart {
1366 let exif_data = &data[cstart..cend];
1367 let tiff = if exif_data.starts_with(b"Exif\0\0") {
1368 &exif_data[6..]
1369 } else {
1370 exif_data
1371 };
1372 riff_bo = Some(tiff);
1373 break;
1374 }
1375 if cid == b"LIST" && cend >= cstart + 4 {
1377 }
1379 pos = cend + (csz & 1);
1380 }
1381 riff_bo
1382 } else if data.starts_with(&[0x00, 0x00, 0x00, 0x0C, b'J', b'X', b'L', b' ']) {
1383 None
1389 } else if data.starts_with(&[0x00, b'M', b'R', b'M']) {
1390 let mrw_data_offset = if data.len() >= 8 {
1392 u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize + 8
1393 } else {
1394 0
1395 };
1396 let mut mrw_bo: Option<&[u8]> = None;
1397 let mut mpos = 8usize;
1398 while mpos + 8 <= mrw_data_offset.min(data.len()) {
1399 let seg_tag = &data[mpos..mpos + 4];
1400 let seg_len = u32::from_be_bytes([
1401 data[mpos + 4],
1402 data[mpos + 5],
1403 data[mpos + 6],
1404 data[mpos + 7],
1405 ]) as usize;
1406 if seg_tag == b"\x00TTW" && mpos + 8 + seg_len <= data.len() {
1407 mrw_bo = Some(&data[mpos + 8..mpos + 8 + seg_len]);
1408 break;
1409 }
1410 mpos += 8 + seg_len;
1411 }
1412 mrw_bo
1413 } else {
1414 Some(data)
1415 };
1416 if let Some(tiff) = check {
1417 if tiff.starts_with(b"II") {
1418 "Little-endian (Intel, II)"
1419 } else if tiff.starts_with(b"MM") {
1420 "Big-endian (Motorola, MM)"
1421 } else {
1422 ""
1423 }
1424 } else {
1425 ""
1426 }
1427 } else {
1428 ""
1429 };
1430 let already_has_exifbyteorder = tags.iter().any(|t| t.name == "ExifByteOrder");
1433 if !bo_str.is_empty()
1434 && !already_has_exifbyteorder
1435 && file_type != FileType::Btf
1436 && file_type != FileType::Dr4
1437 && file_type != FileType::Vrd
1438 && file_type != FileType::Crw
1439 {
1440 pre.push(file_tag("ExifByteOrder", Value::String(bo_str.to_string())));
1441 }
1442 }
1443
1444 tags.splice(0..0, pre);
1446
1447 {
1453 let is_mime = |t: &Tag| {
1456 t.name == "MIMEType"
1457 && t.group.family0 == "File"
1458 && t.group.family3 == crate::tag::MAIN_DOCUMENT
1459 };
1460 if tags.iter().filter(|t| is_mime(t)).count() > 1 {
1461 let last = tags.iter().rposition(is_mime).unwrap();
1462 let (value, print) = (tags[last].raw_value.clone(), tags[last].print_value.clone());
1463 let first = tags.iter().position(is_mime).unwrap();
1464 tags[first].raw_value = value;
1465 tags[first].print_value = print;
1466 let mut seen = false;
1467 tags.retain(|t| {
1468 !is_mime(t) || {
1469 let keep = !seen;
1470 seen = true;
1471 keep
1472 }
1473 });
1474 }
1475 }
1476
1477 {
1488 const SPECIAL_WINS: &[(&str, &str)] =
1489 &[("Kodak", "FNumber"), ("Kodak", "ExposureTime")];
1490 let keep_dups = self.options.duplicates || self.options.extract_embedded > 0;
1499 for (grp, name) in SPECIAL_WINS {
1500 if !tags
1501 .iter()
1502 .any(|t| t.name == *name && t.group.family1 == *grp)
1503 {
1504 continue;
1505 }
1506 if keep_dups {
1507 for t in tags.iter_mut() {
1508 if t.name == *name && t.group.family1 == *grp {
1509 t.priority = t.priority_rank() + 1;
1510 }
1511 }
1512 } else {
1513 tags.retain(|t| t.name != *name || t.group.family1 == *grp);
1514 }
1515 }
1516 }
1517
1518 let gps = crate::composite::gps_coordinates(&tags);
1523 tags.extend(gps);
1524
1525 let composite = crate::composite::compute_composite_tags(&tags);
1527 tags.extend(composite);
1528
1529 if !(self.options.duplicates || self.options.extract_embedded > 0) {
1537 let has_composite_alt = tags
1538 .iter()
1539 .any(|t| t.name == "GPSAltitude" && t.group.family0 == "Composite");
1540 let has_alt_ref = tags.iter().any(|t| t.name == "GPSAltitudeRef");
1541 if !has_composite_alt && has_alt_ref {
1542 tags.retain(|t| {
1543 !(t.name == "GPSAltitude"
1544 && t.group.family0 == "EXIF"
1545 && t.print_value == "undef")
1546 });
1547 }
1548 }
1549
1550 if self.options.geolocation {
1558 if let Some(geo) = crate::composite::compute_geolocation(&tags) {
1559 tags.extend(geo);
1560 }
1561 }
1562
1563 if self.options.use_mwg {
1565 let mwg = crate::composite::compute_mwg_composites(&tags);
1566 tags.extend(mwg);
1567 }
1568
1569 {
1575 let is_flir_fff = tags
1576 .iter()
1577 .any(|t| t.group.family0 == "APP1" && t.group.family1 == "FLIR");
1578 if is_flir_fff {
1579 tags.retain(|t| !(t.name == "LensID" && t.group.family0 == "Composite"));
1580 }
1581 }
1582
1583 {
1588 let make = tags
1589 .iter()
1590 .find(|t| t.name == "Make")
1591 .map(|t| t.print_value.clone())
1592 .unwrap_or_default();
1593 if !make.to_uppercase().contains("CANON") {
1594 tags.retain(|t| t.name != "Lens" || t.group.family0 != "Composite");
1595 }
1596 }
1597
1598 let collapse_duplicates = !self.options.duplicates && self.options.extract_embedded == 0;
1607 if collapse_duplicates {
1608 {
1617 let mut seen: std::collections::HashSet<&str> = tags
1618 .iter()
1619 .filter(|t| t.group.family3 == MAIN_DOCUMENT)
1620 .map(|t| t.name.as_str())
1621 .collect();
1622 let mut keep = Vec::with_capacity(tags.len());
1623 for t in &tags {
1624 keep.push(t.group.family3 == MAIN_DOCUMENT || seen.insert(t.name.as_str()));
1625 }
1626 let mut it = keep.into_iter();
1627 tags.retain(|_| it.next().unwrap_or(true));
1628 }
1629
1630 {
1635 const SPECIAL_WINS: &[(&str, &str)] = &[
1636 ("GoPro", "WhiteBalance"),
1637 ("GoPro", "Sharpness"),
1638 ("GoPro", "ExposureCompensation"),
1639 ("ID3v2_4", "Comment"),
1644 ("ID3v2_3", "Comment"),
1645 ("ID3v2_2", "Comment"),
1646 ("MinoltaRaw", "Contrast"),
1649 ("MinoltaRaw", "Saturation"),
1650 ("MinoltaRaw", "Sharpness"),
1651 ("MinoltaRaw", "ISOSetting"),
1652 ("Kodak", "FNumber"),
1654 ("Kodak", "ExposureTime"),
1655 ("Sigma", "X3FillLight"),
1657 ];
1658 for (grp, name) in SPECIAL_WINS {
1659 if tags
1660 .iter()
1661 .any(|t| t.name == *name && t.group.family1 == *grp)
1662 {
1663 tags.retain(|t| t.name != *name || t.group.family1 == *grp);
1664 }
1665 }
1666 }
1667
1668 let mut best_priority: HashMap<String, i32> = HashMap::new();
1669 for tag in &tags {
1670 let entry = best_priority
1671 .entry(tag.name.clone())
1672 .or_insert_with(|| tag.priority_rank());
1673 if tag.priority_rank() > *entry {
1674 *entry = tag.priority_rank();
1675 }
1676 }
1677 tags.retain(|t| t.priority_rank() >= *best_priority.get(&t.name).unwrap_or(&0));
1678
1679 {
1683 let is_native_doc = |g1: &str| matches!(g1, "PDF" | "PostScript" | "DjVu");
1689 let other_names: std::collections::HashSet<String> = tags
1690 .iter()
1691 .filter(|t| !is_native_doc(&t.group.family1) && !t.print_value.is_empty())
1692 .map(|t| t.name.clone())
1693 .collect();
1694 tags.retain(|t| {
1695 t.name == "Trapped"
1697 || !is_native_doc(&t.group.family1)
1698 || !other_names.contains(&t.name)
1699 });
1700 }
1701
1702 {
1740 #[rustfmt::skip]
1766 const LOW_PRIORITY_TAGS: &[(&str, &str)] = &[
1767 ("Canon", "BaseISO"), ("Canon", "FNumber"),
1774 ("Canon", "ExposureTime"),
1775 ("Canon", "FocalLength"),
1779 ("CIFF", "FocalLength"),
1780 ("Sigma", "Contrast"),
1784 ("Sigma", "Shadow"),
1785 ("Sigma", "Highlight"),
1786 ("Sigma", "Saturation"),
1787 ("Sigma", "Sharpness"),
1788 ];
1789 const LOW_PRIORITY_GROUPS1: &[&str] = &["PictureInfo", "XML"];
1802 #[rustfmt::skip]
1806 const SIGMARAW_PROPERTIES: &[&str] = &[
1807 "AFArea", "AFInFocus", "ApertureDisplayed", "BracketShot",
1808 "BurstShot", "CameraName", "ColorSpace", "DateTimeOriginal",
1809 "DriveMode", "EvalState", "ExposureCompensation",
1810 "ExposureProgram", "ExposureTime", "FNumber", "FirmwareVersion",
1811 "FlashExpComp", "FlashMode", "FlashPower", "FlashTTLMode",
1812 "FlashType", "FocalLength", "FocalLengthIn35mmFormat", "Focus",
1813 "FocusMode", "ISO", "ImageBoardID", "ImagerBoardID",
1814 "IntegrationTime", "LensApertureRange", "LensFocalRange",
1815 "LensType", "Make", "MeteringMode", "Model",
1816 "NetExposureCompensation", "Quality", "SceneCaptureType",
1817 "SensorID", "SensorTemperature", "SerialNumber",
1818 "ShutterSpeedDisplayed", "VersionBF", "WhiteBalance",
1819 ];
1820 let ifd1_low = matches!(ft_code.as_str(), "JPEG" | "JPS" | "MPO" | "ARW");
1833 let is_low_priority_source = |g: &TagGroup, name: &str| -> bool {
1834 let g1 = g.family1.as_str();
1835 if g.family2 == "Unknown" {
1839 return true;
1840 }
1841 if g.family3 != MAIN_DOCUMENT {
1845 return true;
1846 }
1847 if LOW_PRIORITY_TAGS.contains(&(g1, name))
1848 || LOW_PRIORITY_GROUPS1.contains(&g1)
1849 || (g1 == "SigmaRaw" && SIGMARAW_PROPERTIES.contains(&name))
1850 {
1851 return true;
1852 }
1853 match g.family0.as_str() {
1854 "XMP" => {
1859 crate::tags::priority0_generated::xmp_is_priority0(g1, name)
1860 || crate::tags::group2::xmp_property_is_unknown(g1, name)
1861 }
1862 "QuickTime" => {
1871 g1 == "QuickTime"
1872 && matches!(name, "AverageBitrate" | "BufferSize" | "MaxBitrate")
1873 }
1874 "EXIF" | "MakerNotes" => g1 == "PreviewIFD" || (ifd1_low && g1 == "IFD1"),
1878 "RAF" => true,
1885 "IPTC" => g1 != "IPTC",
1891 _ => matches!(g1, "Jpeg2000" | "PhotoMechanic" | "DjVu"),
1894 }
1895 };
1896 let priority_dir: Option<String> = tags
1904 .iter()
1905 .find(|t| {
1906 t.group.family0 == "EXIF"
1907 && matches!(t.name.as_str(), "SubfileType" | "OldSubfileType")
1908 && t.print_value == "Full-resolution image"
1909 })
1910 .map(|t| t.group.family1.clone());
1911 let xmp_is_priority_dir = matches!(
1919 file_type,
1920 FileType::Mp4
1921 | FileType::QuickTime
1922 | FileType::M4a
1923 | FileType::ThreeGP
1924 | FileType::Avif
1925 | FileType::Cr3
1926 | FileType::Crm
1927 | FileType::F4v
1928 | FileType::Mqv
1929 | FileType::Lrv
1930 ) || (file_type == FileType::Heif && ft_code != "HEIC");
1931 use std::collections::HashMap as HM;
1932 let mut by_name: HM<&str, Vec<usize>> = HM::new();
1939 for (i, t) in tags.iter().enumerate() {
1940 by_name.entry(t.name.as_str()).or_default().push(i);
1941 }
1942 let mut drop: std::collections::HashSet<usize> = std::collections::HashSet::new();
1943 for idxs in by_name.values() {
1944 if idxs.len() < 2 {
1945 continue;
1946 }
1947 let eff = |i: usize| -> i32 {
1953 let t = &tags[i];
1954 let in_priority_dir = priority_dir.as_deref()
1958 == Some(t.group.family1.as_str())
1959 || (xmp_is_priority_dir && t.group.family0 == "XMP");
1960 if t.group.family0 == "XMP"
1973 && crate::tags::priority0_generated::xmp_is_below_priority0(
1974 &t.group.family1,
1975 &t.name,
1976 )
1977 {
1978 return -1;
1979 }
1980 if t.priority == crate::tag::PRIORITY_EXPLICIT_ZERO {
1981 if t.group.family3 != MAIN_DOCUMENT {
1982 return 0;
1983 }
1984 return i32::from(in_priority_dir);
1985 }
1986 if t.priority == 0 && is_low_priority_source(&t.group, &t.name) {
1987 i32::from(
1994 in_priority_dir
1995 && t.group.family0 == "XMP"
1996 && t.group.family3 == MAIN_DOCUMENT,
1997 )
1998 } else {
1999 t.priority.max(1)
2000 }
2001 };
2002 let promoted = |p: i32| if p == 0 { 1 } else { p };
2006 let mut winner = idxs[0];
2007 for &i in &idxs[1..] {
2008 if eff(i) >= promoted(eff(winner)) {
2009 winner = i;
2010 }
2011 }
2012 for &i in idxs {
2013 if i != winner {
2014 drop.insert(i);
2015 }
2016 }
2017 }
2018 if !drop.is_empty() {
2019 let mut i = 0usize;
2020 tags.retain(|_| {
2021 let keep = !drop.contains(&i);
2022 i += 1;
2023 keep
2024 });
2025 }
2026 }
2027 }
2028
2029 for tag in &mut tags {
2035 if let Some((f0, f1, f2)) = file_level_group(&tag.name) {
2036 tag.group.family0 = f0.to_string();
2037 tag.group.family1 = f1.to_string();
2038 tag.group.family2 = f2.to_string();
2039 }
2040 }
2041
2042 for tag in &mut tags {
2050 if file_level_group(&tag.name).is_some() {
2051 continue;
2052 }
2053 if let Some(f2) = crate::tags::group2::family2_for(
2054 &tag.group.family0,
2055 &tag.group.family1,
2056 &tag.name,
2057 &tag.group.family2,
2058 ) {
2059 if f2 != tag.group.family2 {
2060 tag.group.family2 = f2.to_string();
2061 }
2062 }
2063 }
2064
2065 if !self.options.requested_tags.is_empty() {
2069 tags.retain(|t| {
2070 self.options
2071 .requested_tags
2072 .iter()
2073 .any(|req| Self::tag_matches_request(t, req))
2074 });
2075 }
2076
2077 Ok(tags)
2078 }
2079
2080 fn tag_matches_request(tag: &Tag, request: &str) -> bool {
2084 let req = request.to_lowercase();
2085 let (group, name) = match req.split_once(':') {
2086 Some((g, n)) => (Some(g), n),
2087 None => (None, req.as_str()),
2088 };
2089 if name != "*" && tag.name.to_lowercase() != name {
2090 return false;
2091 }
2092 match group {
2093 None => true,
2094 Some(g) => {
2095 let grp = &tag.group;
2096 grp.family0.to_lowercase() == g
2097 || grp.family1.to_lowercase() == g
2098 || grp.family2.to_lowercase() == g
2099 }
2100 }
2101 }
2102
2103 fn get_info(&self, tags: &[Tag]) -> ImageInfo {
2107 let mut info = ImageInfo::new();
2108 let mut seen: HashMap<String, (usize, i32)> = HashMap::new(); for tag in tags {
2111 let value = if self.options.print_conv {
2112 &tag.print_value
2113 } else {
2114 &tag.raw_value.to_display_string()
2115 };
2116
2117 let entry = seen.entry(tag.name.clone()).or_insert((0, i32::MIN));
2118 entry.0 += 1;
2119
2120 if entry.0 == 1 {
2121 entry.1 = tag.priority_rank();
2122 info.insert(tag.name.clone(), value.clone());
2123 } else if tag.priority_rank() > entry.1 {
2124 entry.1 = tag.priority_rank();
2126 info.insert(tag.name.clone(), value.clone());
2127 } else if self.options.duplicates {
2128 let key = format!("{} [{}:{}]", tag.name, tag.group.family0, tag.group.family1);
2129 info.insert(key, value.clone());
2130 }
2131 }
2132
2133 info
2134 }
2135
2136 fn detect_file_type(&self, data: &[u8], path: &Path) -> Result<FileType> {
2138 let header_len = data.len().min(256);
2140 if let Some(ft) = file_type::detect_from_magic(&data[..header_len]) {
2141 if ft == FileType::Ico {
2143 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2144 if ext.eq_ignore_ascii_case("dfont") {
2145 return Ok(FileType::Dfont);
2146 }
2147 }
2148 }
2149 if ft == FileType::Jpeg {
2151 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2152 if ext.eq_ignore_ascii_case("jps") {
2153 return Ok(FileType::Jps);
2154 }
2155 }
2156 }
2157 if ft == FileType::Plist {
2159 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2160 if ext.eq_ignore_ascii_case("aae") {
2161 return Ok(FileType::Aae);
2162 }
2163 }
2164 }
2165 if ft == FileType::Xmp || ft == FileType::Xml {
2167 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2168 if ext.eq_ignore_ascii_case("plist") {
2169 return Ok(FileType::Plist);
2170 }
2171 if ext.eq_ignore_ascii_case("aae") {
2172 return Ok(FileType::Aae);
2173 }
2174 }
2175 }
2176 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2178 if ext.eq_ignore_ascii_case("pcd")
2179 && data.len() >= 2056
2180 && &data[2048..2055] == b"PCD_IPI"
2181 {
2182 return Ok(FileType::PhotoCd);
2183 }
2184 }
2185 if ft == FileType::Mp3 {
2187 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2188 if ext.eq_ignore_ascii_case("mpc") {
2189 return Ok(FileType::Mpc);
2190 }
2191 if ext.eq_ignore_ascii_case("ape") {
2192 return Ok(FileType::Ape);
2193 }
2194 if ext.eq_ignore_ascii_case("wv") {
2195 return Ok(FileType::WavPack);
2196 }
2197 }
2198 }
2199 if ft == FileType::Asf {
2201 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2202 if ext.eq_ignore_ascii_case("wmv") {
2203 return Ok(FileType::Wmv);
2204 }
2205 if ext.eq_ignore_ascii_case("wma") {
2206 return Ok(FileType::Wma);
2207 }
2208 }
2209 }
2210 if ft == FileType::Ogg {
2212 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2213 if ext.eq_ignore_ascii_case("opus") {
2214 return Ok(FileType::Opus);
2215 }
2216 }
2217 }
2218 if ft == FileType::Tiff {
2221 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2222 if let Some(ext_ft) = file_type::detect_from_extension(ext) {
2223 if ext_ft != FileType::Tiff && is_tiff_based(ext_ft) {
2224 return Ok(ext_ft);
2225 }
2226 }
2227 }
2228 }
2229 if ft == FileType::Zip {
2231 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2233 if ext.eq_ignore_ascii_case("eip") {
2234 return Ok(FileType::Eip);
2235 }
2236 }
2237 if let Some(iw) = detect_iwork_type(data, path) {
2240 return Ok(iw);
2241 }
2242 if let Some(od_type) = detect_opendocument_type(data) {
2243 return Ok(od_type);
2244 }
2245 }
2246 if ft == FileType::Doc {
2249 if let Some(ole) = detect_ole2_type(data) {
2250 return Ok(ole);
2251 }
2252 }
2253 return Ok(ft);
2254 }
2255
2256 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2258 if let Some(ft) = file_type::detect_from_extension(ext) {
2259 return Ok(ft);
2260 }
2261 }
2262
2263 let ext_str = path
2264 .extension()
2265 .and_then(|e| e.to_str())
2266 .unwrap_or("unknown");
2267 Err(Error::UnsupportedFileType(ext_str.to_string()))
2268 }
2269
2270 fn process_file(&self, data: &[u8], file_type: FileType) -> Result<Vec<Tag>> {
2272 match file_type {
2273 FileType::Jpeg | FileType::Jps => {
2274 formats::jpeg::read_jpeg_with_ee(data, self.options.extract_embedded)
2275 }
2276 FileType::Png | FileType::Mng => formats::png::read_png(data),
2277 FileType::Tiff
2279 | FileType::Btf
2280 | FileType::Dng
2281 | FileType::Cr2
2282 | FileType::Nef
2283 | FileType::Arw
2284 | FileType::Sr2
2285 | FileType::Orf
2286 | FileType::Pef
2287 | FileType::Erf
2288 | FileType::Fff
2289 | FileType::Rwl
2290 | FileType::Mef
2291 | FileType::Srw
2292 | FileType::Gpr
2293 | FileType::Arq
2294 | FileType::ThreeFR
2295 | FileType::Dcr
2296 | FileType::Rw2
2297 | FileType::Srf => formats::tiff::read_tiff(data),
2298 FileType::Iiq => formats::iiq::read_iiq(
2300 data,
2301 !self.options.duplicates && self.options.extract_embedded == 0,
2302 ),
2303 FileType::Gif => formats::gif::read_gif(data),
2305 FileType::Bmp => formats::bmp::read_bmp(data),
2306 FileType::WebP | FileType::Avi | FileType::Wav => formats::riff::read_riff(data),
2307 FileType::Psd => formats::psd::read_psd(data),
2308 FileType::Mp3 => formats::id3::read_mp3(data),
2310 FileType::Flac => formats::flac::read_flac(data),
2311 FileType::Ogg | FileType::Opus => formats::ogg::read_ogg(data),
2312 FileType::Aiff => formats::aiff::read_aiff(data),
2313 FileType::Mp4
2315 | FileType::QuickTime
2316 | FileType::M4a
2317 | FileType::ThreeGP
2318 | FileType::Heif
2319 | FileType::Avif
2320 | FileType::Cr3
2321 | FileType::Crm
2322 | FileType::F4v
2323 | FileType::Mqv
2324 | FileType::Lrv => {
2325 formats::quicktime::read_quicktime_with_ee(data, self.options.extract_embedded)
2326 }
2327 FileType::Mkv | FileType::WebM => formats::matroska::read_matroska(data),
2328 FileType::Asf | FileType::Wmv | FileType::Wma => formats::asf::read_asf(data),
2329 FileType::Wtv => formats::wtv::read_wtv(data),
2330 FileType::Crw => formats::canon_raw::read_crw(data),
2332 FileType::Raf => formats::raf::read_raf(data),
2333 FileType::Mrw => formats::mrw::read_mrw(data),
2334 FileType::Mrc => formats::mrc::read_mrc(data, self.options.extract_embedded),
2335 FileType::Jp2 => formats::jp2::read_jp2(data),
2337 FileType::J2c => formats::jp2::read_j2c(data),
2338 FileType::Jxl => formats::jp2::read_jxl(data),
2339 FileType::Ico => formats::ico::read_ico(data),
2340 FileType::Icc => formats::icc::read_icc(data),
2341 FileType::Pdf => formats::pdf::read_pdf(data, self.options.extract_embedded),
2343 FileType::PostScript => {
2344 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
2346 formats::font::read_pfa(data).or_else(|_| {
2347 formats::postscript::read_postscript(data, self.options.extract_embedded)
2348 })
2349 } else {
2350 formats::postscript::read_postscript(data, self.options.extract_embedded)
2351 }
2352 }
2353 FileType::Eip => formats::capture_one::read_eip(data, self.options.extract_embedded),
2354 FileType::Zip
2355 | FileType::Docx
2356 | FileType::Xlsx
2357 | FileType::Pptx
2358 | FileType::Doc
2359 | FileType::Xls
2360 | FileType::Ppt
2361 | FileType::Numbers
2362 | FileType::Pages
2363 | FileType::Key => formats::zip::read_zip(data, self.options.extract_embedded),
2364 FileType::Rtf => formats::rtf::read_rtf(data),
2365 FileType::InDesign => formats::indesign::read_indesign(data),
2366 FileType::Pcap => formats::pcap::read_pcap(data),
2367 FileType::Pcapng => formats::pcap::read_pcapng(data),
2368 FileType::Vrd => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
2370 FileType::Dr4 => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
2371 FileType::Xmp => formats::xmp_file::read_xmp(data),
2373 FileType::Svg => formats::svg::read_svg(data),
2374 FileType::Html => {
2375 let is_svg = data.windows(4).take(512).any(|w| w == b"<svg");
2377 if is_svg {
2378 formats::svg::read_svg(data)
2379 } else {
2380 formats::html::read_html(data)
2381 }
2382 }
2383 FileType::Exe => formats::exe::read_exe(data),
2384 FileType::Font => {
2385 if data.starts_with(b"StartFontMetrics") {
2387 return formats::font::read_afm(data);
2388 }
2389 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
2391 return formats::font::read_pfa(data).or_else(|_| Ok(Vec::new()));
2392 }
2393 if data.len() >= 2 && data[0] == 0x80 && (data[1] == 0x01 || data[1] == 0x02) {
2395 return formats::font::read_pfb(data).or_else(|_| Ok(Vec::new()));
2396 }
2397 formats::font::read_font(data)
2398 }
2399 FileType::WavPack | FileType::Dsf => formats::id3::read_mp3(data),
2401 FileType::Ape => formats::ape::read_ape(data),
2402 FileType::Mpc => formats::ape::read_mpc(data),
2403 FileType::Aac => formats::aac::read_aac(data),
2404 FileType::RealAudio => {
2405 formats::real_audio::read_real_audio(data).or_else(|_| Ok(Vec::new()))
2406 }
2407 FileType::RealMedia => {
2408 formats::real_media::read_real_media(data).or_else(|_| Ok(Vec::new()))
2409 }
2410 FileType::Czi => formats::czi::read_czi(data).or_else(|_| Ok(Vec::new())),
2412 FileType::PhotoCd => formats::photo_cd::read_photo_cd(data).or_else(|_| Ok(Vec::new())),
2413 FileType::Dicom => formats::dicom::read_dicom(data),
2414 FileType::Fits => formats::fits::read_fits(data),
2415 FileType::Fit => formats::fit::read_fit_with_ee(data, self.options.extract_embedded),
2416 FileType::Flv => formats::flv::read_flv(data),
2417 FileType::Mxf => formats::mxf::read_mxf(data, self.options.extract_embedded)
2418 .or_else(|_| Ok(Vec::new())),
2419 FileType::Swf => formats::swf::read_swf(data),
2420 FileType::Hdr => formats::hdr::read_hdr(data),
2421 FileType::DjVu => formats::djvu::read_djvu(data),
2422 FileType::Xcf => formats::gimp::read_xcf(data),
2423 FileType::Mie => formats::mie::read_mie(data),
2424 FileType::Lfp => formats::lytro::read_lfp(data),
2425 FileType::Fpf => formats::flir_fpf::read_fpf(data),
2427 FileType::Flif => formats::flif::read_flif(data),
2428 FileType::Bpg => formats::bpg::read_bpg(data),
2429 FileType::Pcx => formats::pcx::read_pcx(data),
2430 FileType::Pict => formats::pict::read_pict(data),
2431 FileType::Mpeg => formats::mpeg::read_mpeg(data),
2432 FileType::M2ts => formats::m2ts::read_m2ts(data, self.options.extract_embedded),
2433 FileType::Gzip => formats::gzip::read_gzip(data),
2434 FileType::Rar => formats::rar::read_rar(data),
2435 FileType::SevenZ => formats::sevenz::read_7z(data),
2436 FileType::Dss => formats::dss::read_dss(data),
2437 FileType::Moi => formats::moi::read_moi(data),
2438 FileType::MacOs => formats::macos::read_macos(data),
2439 FileType::Json => formats::json_format::read_json(data),
2440 FileType::Pgf => formats::pgf::read_pgf(data),
2442 FileType::Xisf => formats::xisf::read_xisf(data),
2443 FileType::Torrent => formats::torrent::read_torrent(data),
2444 FileType::Mobi => formats::palm::read_palm(data),
2445 FileType::Psp => formats::psp::read_psp(data),
2446 FileType::SonyPmp => formats::sony_pmp::read_sony_pmp(data),
2447 FileType::Audible => formats::audible::read_audible(data),
2448 FileType::Exr => formats::openexr::read_openexr(data),
2449 FileType::Plist => {
2451 if data.starts_with(b"bplist") {
2452 formats::plist::read_binary_plist_tags(data)
2453 } else {
2454 formats::plist::read_xml_plist(data)
2455 }
2456 }
2457 FileType::Aae => {
2458 if data.starts_with(b"bplist") {
2459 formats::plist::read_binary_plist_tags(data)
2460 } else {
2461 formats::plist::read_aae_plist(data)
2462 }
2463 }
2464 FileType::KyoceraRaw => formats::kyocera_raw::read_kyocera_raw(data),
2465 FileType::PortableFloatMap => formats::pfm::read_pfm(data),
2466 FileType::Ods
2467 | FileType::Odt
2468 | FileType::Odp
2469 | FileType::Odg
2470 | FileType::Odf
2471 | FileType::Odb
2472 | FileType::Odi
2473 | FileType::Odc => formats::zip::read_zip(data, self.options.extract_embedded),
2474 FileType::Lif => formats::lif::read_lif(data),
2475 FileType::Rwz => formats::rawzor::read_rawzor(data),
2476 FileType::Jxr => formats::jxr::read_jxr(data),
2477 FileType::Miff => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
2478 FileType::Tnef => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
2479 FileType::Wpg => formats::wpg::read_wpg(data).or_else(|_| Ok(Vec::new())),
2480 FileType::Dv => {
2481 formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new()))
2482 }
2483 FileType::Itc => formats::itc::read_itc(data).or_else(|_| Ok(Vec::new())),
2484 FileType::Iso => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
2485 FileType::Afm => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
2486 FileType::Pfa => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
2487 FileType::Pfb => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
2488 FileType::Dfont => formats::font::read_font(data).or_else(|_| Ok(Vec::new())),
2489 FileType::Xml | FileType::Inx => {
2490 formats::xmp_file::read_xmp(data).or_else(|_| Ok(Vec::new()))
2491 }
2492 FileType::Eps => {
2493 formats::postscript::read_postscript(data, self.options.extract_embedded)
2494 }
2495 _ => Err(Error::UnsupportedFileType(format!("{}", file_type))),
2496 }
2497 }
2498
2499 fn process_by_extension(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
2501 let ext = path
2502 .extension()
2503 .and_then(|e| e.to_str())
2504 .unwrap_or("")
2505 .to_ascii_lowercase();
2506
2507 match ext.as_str() {
2508 "ppm" | "pgm" | "pbm" => formats::ppm::read_ppm(data),
2509 "pfm" => {
2510 if data.len() >= 3 && data[0] == b'P' && (data[1] == b'f' || data[1] == b'F') {
2512 formats::ppm::read_ppm(data)
2513 } else {
2514 Ok(Vec::new()) }
2516 }
2517 "json" => formats::json_format::read_json(data),
2518 "svg" => formats::svg::read_svg(data),
2519 "ram" => formats::ram::read_ram(data).or_else(|_| Ok(Vec::new())),
2520 "txt" | "log" | "igc" => Ok(compute_text_tags(data, false)),
2521 "csv" => Ok(compute_text_tags(data, true)),
2522 "url" => formats::lnk::read_url(data).or_else(|_| Ok(Vec::new())),
2523 "lnk" => formats::lnk::read_lnk(data).or_else(|_| Ok(Vec::new())),
2524 "gpx" | "kml" | "xml" | "inx" => formats::xmp_file::read_xmp(data),
2525 "plist" => {
2526 if data.starts_with(b"bplist") {
2527 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
2528 } else {
2529 formats::plist::read_xml_plist(data).or_else(|_| Ok(Vec::new()))
2530 }
2531 }
2532 "aae" => {
2533 if data.starts_with(b"bplist") {
2534 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
2535 } else {
2536 formats::plist::read_aae_plist(data).or_else(|_| Ok(Vec::new()))
2537 }
2538 }
2539 "vcf" | "ics" | "vcard" => {
2540 let s = crate::encoding::decode_utf8_or_latin1(&data[..data.len().min(100)]);
2541 if s.contains("BEGIN:VCALENDAR") {
2542 formats::vcard::read_ics(data).or_else(|_| Ok(Vec::new()))
2543 } else {
2544 formats::vcard::read_vcf(data).or_else(|_| Ok(Vec::new()))
2545 }
2546 }
2547 "xcf" => Ok(Vec::new()), "vrd" => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
2549 "dr4" => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
2550 "indd" | "indt" => Ok(Vec::new()), "x3f" => formats::sigma_raw::read_x3f(data).or_else(|_| Ok(Vec::new())),
2552 "mie" => Ok(Vec::new()), "exr" => Ok(Vec::new()), "wpg" => formats::wpg::read_wpg(data).or_else(|_| Ok(Vec::new())),
2555 "moi" => formats::moi::read_moi(data).or_else(|_| Ok(Vec::new())),
2556 "macos" => formats::macos::read_macos(data).or_else(|_| Ok(Vec::new())),
2557 "dpx" => formats::dpx::read_dpx(data).or_else(|_| Ok(Vec::new())),
2558 "r3d" => formats::red::read_r3d(data).or_else(|_| Ok(Vec::new())),
2559 "tnef" => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
2560 "ppt" | "fpx" => formats::flashpix::read_fpx(data).or_else(|_| Ok(Vec::new())),
2561 "fpf" => formats::flir_fpf::read_fpf(data).or_else(|_| Ok(Vec::new())),
2562 "itc" => formats::itc::read_itc(data).or_else(|_| Ok(Vec::new())),
2563 "mpg" | "mpeg" | "m1v" | "m2v" | "mpv" => {
2564 formats::mpeg::read_mpeg(data).or_else(|_| Ok(Vec::new()))
2565 }
2566 "dv" => formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new())),
2567 "czi" => formats::czi::read_czi(data).or_else(|_| Ok(Vec::new())),
2568 "miff" => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
2569 "lfp" | "mrc" | "dss" | "mobi" | "psp" | "pgf" | "raw" | "pmp" | "torrent" | "xisf"
2570 | "mxf" | "dfont" => Ok(Vec::new()),
2571 "iso" => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
2572 "afm" => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
2573 "pfa" => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
2574 "pfb" => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
2575 _ => Err(Error::UnsupportedFileType(ext)),
2576 }
2577 }
2578}
2579
2580impl Default for ExifTool {
2581 fn default() -> Self {
2582 Self::new()
2583 }
2584}
2585
2586fn exe_subtype(d: &[u8]) -> Option<(&'static str, &'static str, &'static str)> {
2591 const MIME: &str = "application/octet-stream";
2592 if d.len() < 8 {
2593 return None;
2594 }
2595 if &d[0..4] == b"\x7fELF" && d.len() >= 18 {
2597 let le = d[5] == 1;
2598 let e_type = if le {
2599 u16::from_le_bytes([d[16], d[17]])
2600 } else {
2601 u16::from_be_bytes([d[16], d[17]])
2602 };
2603 return Some(match e_type {
2604 1 => ("ELF relocatable", MIME, "o"),
2605 2 => ("ELF executable", MIME, ""),
2606 3 => ("ELF shared library", MIME, "so"),
2607 4 => ("ELF core file", MIME, ""),
2608 _ => ("ELF", MIME, ""),
2609 });
2610 }
2611 let magic_be = u32::from_be_bytes([d[0], d[1], d[2], d[3]]);
2613 let macho = matches!(magic_be, 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE);
2614 if macho && d.len() >= 16 {
2615 let le = matches!(magic_be, 0xCEFAEDFE | 0xCFFAEDFE);
2616 let filetype = if le {
2617 u32::from_le_bytes([d[12], d[13], d[14], d[15]])
2618 } else {
2619 u32::from_be_bytes([d[12], d[13], d[14], d[15]])
2620 };
2621 return Some(match filetype {
2622 1 => ("Mach-O object file", MIME, "o"),
2623 6 => ("Mach-O dynamic link library", MIME, "dylib"),
2624 8 => ("Mach-O dynamic bound bundle", MIME, "dylib"),
2625 9 => ("Mach-O dynamic link library stub", MIME, "dylib"),
2626 _ => ("Mach-O executable", MIME, ""),
2627 });
2628 }
2629 if matches!(magic_be, 0xCAFEBABE | 0xBEBAFECA) {
2631 return Some(("Mach-O fat binary executable", MIME, ""));
2632 }
2633 if d.starts_with(b"!<arch>\n") {
2635 let is_macho = d.windows(4).take(4096).any(|w| {
2636 let m = u32::from_be_bytes([w[0], w[1], w[2], w[3]]);
2637 matches!(
2638 m,
2639 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE | 0xCAFEBABE
2640 )
2641 });
2642 return Some(if is_macho {
2643 ("Mach-O static library", MIME, "a")
2644 } else {
2645 ("Static library", MIME, "a")
2646 });
2647 }
2648 if &d[0..2] == b"MZ" && d.len() >= 0x40 {
2650 let pe_off = u32::from_le_bytes([d[0x3c], d[0x3d], d[0x3e], d[0x3f]]) as usize;
2651 if pe_off + 6 <= d.len() && &d[pe_off..pe_off + 4] == b"PE\0\0" {
2652 let machine = u16::from_le_bytes([d[pe_off + 4], d[pe_off + 5]]);
2653 return Some(match machine {
2654 0x8664 | 0xAA64 => ("Win64 EXE", MIME, "exe"),
2655 _ => ("Win32 EXE", MIME, "exe"),
2656 });
2657 }
2658 }
2659 None
2660}
2661
2662fn is_tiff_based(ft: FileType) -> bool {
2664 matches!(
2665 ft,
2666 FileType::Dng
2667 | FileType::Cr2
2668 | FileType::Nef
2669 | FileType::Arw
2670 | FileType::Sr2
2671 | FileType::Orf
2672 | FileType::Pef
2673 | FileType::Erf
2674 | FileType::Rwl
2675 | FileType::Mef
2676 | FileType::Srw
2677 | FileType::Gpr
2678 | FileType::Arq
2679 | FileType::ThreeFR
2680 | FileType::Dcr
2681 | FileType::Rw2
2682 | FileType::Srf
2683 | FileType::Iiq
2684 | FileType::Btf
2685 )
2686}
2687
2688fn detect_ole2_type(data: &[u8]) -> Option<FileType> {
2691 fn has_utf16(data: &[u8], name: &str) -> bool {
2692 let needle: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
2693 data.windows(needle.len()).any(|w| w == needle.as_slice())
2694 }
2695 if has_utf16(data, "PowerPoint Document") {
2696 Some(FileType::Ppt)
2697 } else if has_utf16(data, "Workbook") || has_utf16(data, "Book") {
2698 Some(FileType::Xls)
2699 } else {
2700 None
2701 }
2702}
2703
2704fn detect_iwork_type(data: &[u8], path: &Path) -> Option<FileType> {
2708 const MARKERS: &[&[u8]] = &[
2709 b"index.xml",
2710 b"index.apxl",
2711 b"QuickLook/Thumbnail.jpg",
2712 b"Index/Document.iwa",
2713 b"Index/Slide.iwa",
2714 b"Index/Tables/DataList.iwa",
2715 ];
2716 let has_marker = MARKERS
2717 .iter()
2718 .any(|m| data.windows(m.len()).any(|w| w == *m));
2719 if !has_marker {
2720 return None;
2721 }
2722 let ext = path
2723 .extension()
2724 .and_then(|e| e.to_str())
2725 .unwrap_or("")
2726 .to_ascii_lowercase();
2727 match ext.as_str() {
2728 "numbers" | "nmbtemplate" => Some(FileType::Numbers),
2729 "pages" => Some(FileType::Pages),
2730 "key" | "kth" => Some(FileType::Key),
2731 _ => None,
2732 }
2733}
2734
2735fn refine_filetype_by_content(file_type: FileType, data: &[u8]) -> Option<(String, String)> {
2738 match file_type {
2739 FileType::PortableFloatMap if data.len() >= 2 && data[0] == 0x00 && data[1] <= 0x02 => {
2741 Some(("PFM".into(), "application/x-font-type1".into()))
2742 }
2743 FileType::Plist if !data.starts_with(b"bplist") => {
2745 Some(("PLIST".into(), "application/xml".into()))
2746 }
2747 FileType::Jxl if data.starts_with(&[0xFF, 0x0A]) => {
2749 Some(("JXL Codestream".into(), file_type.mime_type().to_string()))
2750 }
2751 FileType::WebP if data.len() >= 16 && &data[12..16] == b"VP8X" => {
2753 Some(("Extended WEBP".into(), file_type.mime_type().to_string()))
2754 }
2755 FileType::DjVu if data.len() >= 16 && &data[12..16] == b"DJVM" => Some((
2757 "DJVU (multi-page)".into(),
2758 file_type.mime_type().to_string(),
2759 )),
2760 _ => None,
2761 }
2762}
2763
2764fn detect_opendocument_type(data: &[u8]) -> Option<FileType> {
2765 if data.len() < 30 || data[0..4] != [0x50, 0x4B, 0x03, 0x04] {
2767 return None;
2768 }
2769 let compression = u16::from_le_bytes([data[8], data[9]]);
2770 let compressed_size = u32::from_le_bytes([data[18], data[19], data[20], data[21]]) as usize;
2771 let name_len = u16::from_le_bytes([data[26], data[27]]) as usize;
2772 let extra_len = u16::from_le_bytes([data[28], data[29]]) as usize;
2773 let name_start = 30;
2774 if name_start + name_len > data.len() {
2775 return None;
2776 }
2777 let filename = std::str::from_utf8(&data[name_start..name_start + name_len]).unwrap_or("");
2778 if filename != "mimetype" || compression != 0 {
2779 return None;
2780 }
2781 let content_start = name_start + name_len + extra_len;
2782 let content_end = (content_start + compressed_size).min(data.len());
2783 if content_start >= content_end {
2784 return None;
2785 }
2786 let mime = std::str::from_utf8(&data[content_start..content_end])
2787 .unwrap_or("")
2788 .trim();
2789 match mime {
2790 "application/vnd.oasis.opendocument.spreadsheet" => Some(FileType::Ods),
2791 "application/vnd.oasis.opendocument.text" => Some(FileType::Odt),
2792 "application/vnd.oasis.opendocument.presentation" => Some(FileType::Odp),
2793 "application/vnd.oasis.opendocument.graphics" => Some(FileType::Odg),
2794 "application/vnd.oasis.opendocument.formula" => Some(FileType::Odf),
2795 "application/vnd.oasis.opendocument.database" => Some(FileType::Odb),
2796 "application/vnd.oasis.opendocument.image" => Some(FileType::Odi),
2797 "application/vnd.oasis.opendocument.chart" => Some(FileType::Odc),
2798 _ => None,
2799 }
2800}
2801
2802pub fn get_file_type<P: AsRef<Path>>(path: P) -> Result<FileType> {
2804 let path = path.as_ref();
2805 let mut file = fs::File::open(path).map_err(Error::Io)?;
2806 let mut header = [0u8; 256];
2807 use std::io::Read;
2808 let n = file.read(&mut header).map_err(Error::Io)?;
2809
2810 if let Some(ft) = file_type::detect_from_magic(&header[..n]) {
2811 return Ok(ft);
2812 }
2813
2814 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2815 if let Some(ft) = file_type::detect_from_extension(ext) {
2816 return Ok(ft);
2817 }
2818 }
2819
2820 Err(Error::UnsupportedFileType("unknown".into()))
2821}
2822
2823enum ExifIfdGroup {
2825 Ifd0,
2826 ExifIfd,
2827 Gps,
2828}
2829
2830fn classify_exif_tag(tag_id: u16) -> ExifIfdGroup {
2832 match tag_id {
2833 0x829A..=0x829D | 0x8822..=0x8827 | 0x8830 | 0x9000..=0x9292 | 0xA000..=0xA435 => {
2835 ExifIfdGroup::ExifIfd
2836 }
2837 0x0000..=0x001F if tag_id <= 0x001F => ExifIfdGroup::Gps,
2839 _ => ExifIfdGroup::Ifd0,
2841 }
2842}
2843
2844fn extract_existing_exif_entries(
2846 jpeg_data: &[u8],
2847 target_bo: ByteOrderMark,
2848) -> Vec<exif_writer::IfdEntry> {
2849 let mut entries = Vec::new();
2850
2851 let mut pos = 2; while pos + 4 <= jpeg_data.len() {
2854 if jpeg_data[pos] != 0xFF {
2855 pos += 1;
2856 continue;
2857 }
2858 let marker = jpeg_data[pos + 1];
2859 pos += 2;
2860
2861 if marker == 0xDA || marker == 0xD9 {
2862 break; }
2864 if marker == 0xFF || marker == 0x00 || marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
2865 continue;
2866 }
2867
2868 if pos + 2 > jpeg_data.len() {
2869 break;
2870 }
2871 let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
2872 if seg_len < 2 || pos + seg_len > jpeg_data.len() {
2873 break;
2874 }
2875
2876 let seg_data = &jpeg_data[pos + 2..pos + seg_len];
2877
2878 if marker == 0xE1 && seg_data.len() > 14 && seg_data.starts_with(b"Exif\0\0") {
2880 let tiff_data = &seg_data[6..];
2881 extract_ifd_entries(tiff_data, target_bo, &mut entries);
2882 break;
2883 }
2884
2885 pos += seg_len;
2886 }
2887
2888 entries
2889}
2890
2891fn extract_ifd_entries(
2893 tiff_data: &[u8],
2894 target_bo: ByteOrderMark,
2895 entries: &mut Vec<exif_writer::IfdEntry>,
2896) {
2897 use crate::metadata::exif::parse_tiff_header;
2898
2899 let header = match parse_tiff_header(tiff_data) {
2900 Ok(h) => h,
2901 Err(_) => return,
2902 };
2903
2904 let src_bo = header.byte_order;
2905
2906 read_ifd_for_merge(
2908 tiff_data,
2909 header.ifd0_offset as usize,
2910 src_bo,
2911 target_bo,
2912 entries,
2913 );
2914
2915 let ifd0_offset = header.ifd0_offset as usize;
2917 if ifd0_offset + 2 > tiff_data.len() {
2918 return;
2919 }
2920 let count = read_u16_bo(tiff_data, ifd0_offset, src_bo) as usize;
2921 for i in 0..count {
2922 let eoff = ifd0_offset + 2 + i * 12;
2923 if eoff + 12 > tiff_data.len() {
2924 break;
2925 }
2926 let tag = read_u16_bo(tiff_data, eoff, src_bo);
2927 let value_off = read_u32_bo(tiff_data, eoff + 8, src_bo) as usize;
2928
2929 match tag {
2930 0x8769 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
2931 0x8825 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
2932 _ => {}
2933 }
2934 }
2935}
2936
2937fn read_ifd_for_merge(
2939 data: &[u8],
2940 offset: usize,
2941 src_bo: ByteOrderMark,
2942 target_bo: ByteOrderMark,
2943 entries: &mut Vec<exif_writer::IfdEntry>,
2944) {
2945 if offset + 2 > data.len() {
2946 return;
2947 }
2948 let count = read_u16_bo(data, offset, src_bo) as usize;
2949
2950 for i in 0..count {
2951 let eoff = offset + 2 + i * 12;
2952 if eoff + 12 > data.len() {
2953 break;
2954 }
2955
2956 let tag = read_u16_bo(data, eoff, src_bo);
2957 let dtype = read_u16_bo(data, eoff + 2, src_bo);
2958 let count_val = read_u32_bo(data, eoff + 4, src_bo);
2959
2960 if tag == 0x8769 || tag == 0x8825 || tag == 0xA005 || tag == 0x927C {
2962 continue;
2963 }
2964
2965 let type_size = match dtype {
2966 1 | 2 | 6 | 7 => 1usize,
2967 3 | 8 => 2,
2968 4 | 9 | 11 | 13 => 4,
2969 5 | 10 | 12 => 8,
2970 _ => continue,
2971 };
2972
2973 let total_size = type_size * count_val as usize;
2974 let raw_data = if total_size <= 4 {
2975 data[eoff + 8..eoff + 12].to_vec()
2976 } else {
2977 let voff = read_u32_bo(data, eoff + 8, src_bo) as usize;
2978 if voff + total_size > data.len() {
2979 continue;
2980 }
2981 data[voff..voff + total_size].to_vec()
2982 };
2983
2984 let final_data = if src_bo != target_bo && type_size > 1 {
2986 reencode_bytes(&raw_data, dtype, count_val as usize, src_bo, target_bo)
2987 } else {
2988 raw_data[..total_size].to_vec()
2989 };
2990
2991 let format = match dtype {
2992 1 => exif_writer::ExifFormat::Byte,
2993 2 => exif_writer::ExifFormat::Ascii,
2994 3 => exif_writer::ExifFormat::Short,
2995 4 => exif_writer::ExifFormat::Long,
2996 5 => exif_writer::ExifFormat::Rational,
2997 6 => exif_writer::ExifFormat::SByte,
2998 7 => exif_writer::ExifFormat::Undefined,
2999 8 => exif_writer::ExifFormat::SShort,
3000 9 => exif_writer::ExifFormat::SLong,
3001 10 => exif_writer::ExifFormat::SRational,
3002 11 => exif_writer::ExifFormat::Float,
3003 12 => exif_writer::ExifFormat::Double,
3004 _ => continue,
3005 };
3006
3007 entries.push(exif_writer::IfdEntry {
3008 tag,
3009 format,
3010 data: final_data,
3011 });
3012 }
3013}
3014
3015fn reencode_bytes(
3017 data: &[u8],
3018 dtype: u16,
3019 count: usize,
3020 src_bo: ByteOrderMark,
3021 dst_bo: ByteOrderMark,
3022) -> Vec<u8> {
3023 let mut out = Vec::with_capacity(data.len());
3024 match dtype {
3025 3 | 8 => {
3026 for i in 0..count {
3028 let v = read_u16_bo(data, i * 2, src_bo);
3029 match dst_bo {
3030 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
3031 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
3032 }
3033 }
3034 }
3035 4 | 9 | 11 | 13 => {
3036 for i in 0..count {
3038 let v = read_u32_bo(data, i * 4, src_bo);
3039 match dst_bo {
3040 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
3041 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
3042 }
3043 }
3044 }
3045 5 | 10 => {
3046 for i in 0..count {
3048 let n = read_u32_bo(data, i * 8, src_bo);
3049 let d = read_u32_bo(data, i * 8 + 4, src_bo);
3050 match dst_bo {
3051 ByteOrderMark::LittleEndian => {
3052 out.extend_from_slice(&n.to_le_bytes());
3053 out.extend_from_slice(&d.to_le_bytes());
3054 }
3055 ByteOrderMark::BigEndian => {
3056 out.extend_from_slice(&n.to_be_bytes());
3057 out.extend_from_slice(&d.to_be_bytes());
3058 }
3059 }
3060 }
3061 }
3062 12 => {
3063 for i in 0..count {
3065 let mut bytes = [0u8; 8];
3066 bytes.copy_from_slice(&data[i * 8..i * 8 + 8]);
3067 if src_bo != dst_bo {
3068 bytes.reverse();
3069 }
3070 out.extend_from_slice(&bytes);
3071 }
3072 }
3073 _ => out.extend_from_slice(data),
3074 }
3075 out
3076}
3077
3078fn read_u16_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
3079 if offset + 2 > data.len() {
3080 return 0;
3081 }
3082 match bo {
3083 ByteOrderMark::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
3084 ByteOrderMark::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
3085 }
3086}
3087
3088fn read_u32_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
3089 if offset + 4 > data.len() {
3090 return 0;
3091 }
3092 match bo {
3093 ByteOrderMark::LittleEndian => u32::from_le_bytes([
3094 data[offset],
3095 data[offset + 1],
3096 data[offset + 2],
3097 data[offset + 3],
3098 ]),
3099 ByteOrderMark::BigEndian => u32::from_be_bytes([
3100 data[offset],
3101 data[offset + 1],
3102 data[offset + 2],
3103 data[offset + 3],
3104 ]),
3105 }
3106}
3107
3108fn tag_name_to_id(name: &str) -> Option<u16> {
3110 encode_exif_tag(name, "", "", ByteOrderMark::BigEndian).map(|(id, _, _)| id)
3111}
3112
3113fn value_to_filename(value: &str) -> String {
3115 value
3116 .chars()
3117 .map(|c| match c {
3118 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
3119 c if c.is_control() => '_',
3120 c => c,
3121 })
3122 .collect::<String>()
3123 .trim()
3124 .to_string()
3125}
3126
3127pub fn parse_date_shift(shift: &str) -> Option<(i32, u32, u32, u32)> {
3130 let (sign, rest) = if let Some(stripped) = shift.strip_prefix('-') {
3131 (-1, stripped)
3132 } else if let Some(stripped) = shift.strip_prefix('+') {
3133 (1, stripped)
3134 } else {
3135 (1, shift)
3136 };
3137
3138 let parts: Vec<&str> = rest.split(':').collect();
3139 match parts.len() {
3140 1 => {
3141 let h: u32 = parts[0].parse().ok()?;
3142 Some((sign, h, 0, 0))
3143 }
3144 2 => {
3145 let h: u32 = parts[0].parse().ok()?;
3146 let m: u32 = parts[1].parse().ok()?;
3147 Some((sign, h, m, 0))
3148 }
3149 3 => {
3150 let h: u32 = parts[0].parse().ok()?;
3151 let m: u32 = parts[1].parse().ok()?;
3152 let s: u32 = parts[2].parse().ok()?;
3153 Some((sign, h, m, s))
3154 }
3155 _ => None,
3156 }
3157}
3158
3159pub fn shift_datetime(datetime: &str, shift: &str) -> Option<String> {
3162 let (sign, hours, minutes, seconds) = parse_date_shift(shift)?;
3163
3164 if datetime.len() < 19 {
3166 return None;
3167 }
3168 let year: i32 = datetime[0..4].parse().ok()?;
3169 let month: u32 = datetime[5..7].parse().ok()?;
3170 let day: u32 = datetime[8..10].parse().ok()?;
3171 let hour: u32 = datetime[11..13].parse().ok()?;
3172 let min: u32 = datetime[14..16].parse().ok()?;
3173 let sec: u32 = datetime[17..19].parse().ok()?;
3174
3175 let total_secs = (hour * 3600 + min * 60 + sec) as i64
3177 + sign as i64 * (hours * 3600 + minutes * 60 + seconds) as i64;
3178
3179 let days_shift = if total_secs < 0 {
3180 -1 - (-total_secs - 1) / 86400
3181 } else {
3182 total_secs / 86400
3183 };
3184
3185 let time_secs = ((total_secs % 86400) + 86400) % 86400;
3186 let new_hour = (time_secs / 3600) as u32;
3187 let new_min = ((time_secs % 3600) / 60) as u32;
3188 let new_sec = (time_secs % 60) as u32;
3189
3190 let mut new_day = day as i32 + days_shift as i32;
3192 let mut new_month = month;
3193 let mut new_year = year;
3194
3195 let days_in_month = |m: u32, y: i32| -> i32 {
3196 match m {
3197 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3198 4 | 6 | 9 | 11 => 30,
3199 2 => {
3200 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
3201 29
3202 } else {
3203 28
3204 }
3205 }
3206 _ => 30,
3207 }
3208 };
3209
3210 while new_day > days_in_month(new_month, new_year) {
3211 new_day -= days_in_month(new_month, new_year);
3212 new_month += 1;
3213 if new_month > 12 {
3214 new_month = 1;
3215 new_year += 1;
3216 }
3217 }
3218 while new_day < 1 {
3219 new_month = if new_month == 1 { 12 } else { new_month - 1 };
3220 if new_month == 12 {
3221 new_year -= 1;
3222 }
3223 new_day += days_in_month(new_month, new_year);
3224 }
3225
3226 Some(format!(
3227 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
3228 new_year, new_month, new_day, new_hour, new_min, new_sec
3229 ))
3230}
3231
3232const FILE_LEVEL_GROUPS: &[(&str, &str, &str, &str)] = &[
3245 ("CurrentIPTCDigest", "File", "File", "Image"),
3249 ("Directory", "File", "System", "Other"),
3250 ("Error", "ExifTool", "ExifTool", "ExifTool"),
3251 ("ExifToolVersion", "ExifTool", "ExifTool", "ExifTool"),
3252 ("FileAccessDate", "File", "System", "Time"),
3253 ("FileCreateDate", "File", "System", "Time"),
3254 ("FileInodeChangeDate", "File", "System", "Time"),
3255 ("FileModifyDate", "File", "System", "Time"),
3256 ("FileName", "File", "System", "Other"),
3257 ("FilePermissions", "File", "System", "Other"),
3258 ("FileSize", "File", "System", "Other"),
3259 ("Warning", "ExifTool", "ExifTool", "ExifTool"),
3260];
3261
3262fn file_level_group(name: &str) -> Option<(&'static str, &'static str, &'static str)> {
3265 FILE_LEVEL_GROUPS
3266 .iter()
3267 .find(|(n, ..)| *n == name)
3268 .map(|&(_, f0, f1, f2)| (f0, f1, f2))
3269}
3270
3271#[cfg(unix)]
3278fn format_file_permissions(mode: u32) -> String {
3279 let type_char = match mode & 0o170000 {
3280 0o010000 => 'p', 0o020000 => 'c', 0o040000 => 'd', 0o060000 => 'b', 0o120000 => 'l', 0o140000 => 's', _ => '-',
3287 };
3288 let mut s = String::with_capacity(10);
3289 s.push(type_char);
3290 let mut mask = 0o400u32;
3291 while mask > 0 {
3292 for ch in ['r', 'w', 'x'] {
3293 s.push(if mode & mask != 0 { ch } else { '-' });
3294 mask >>= 1;
3295 }
3296 }
3297 s
3298}
3299
3300enum FileData {
3304 Mapped(memmap2::Mmap),
3305 Owned(Vec<u8>),
3306}
3307
3308impl std::ops::Deref for FileData {
3309 type Target = [u8];
3310 fn deref(&self) -> &[u8] {
3311 match self {
3312 FileData::Mapped(m) => m,
3313 FileData::Owned(v) => v,
3314 }
3315 }
3316}
3317
3318fn map_file_for_read(path: &Path) -> Result<FileData> {
3321 let file = fs::File::open(path).map_err(Error::Io)?;
3322 let len = file.metadata().map_err(Error::Io)?.len();
3323 if len == 0 {
3324 return Ok(FileData::Owned(Vec::new()));
3325 }
3326 match unsafe { memmap2::Mmap::map(&file) } {
3331 Ok(m) => Ok(FileData::Mapped(m)),
3332 Err(_) => Ok(FileData::Owned(fs::read(path).map_err(Error::Io)?)),
3333 }
3334}
3335
3336fn format_file_size(bytes: u64) -> String {
3338 let v = bytes as f64;
3339 if bytes < 2000 {
3340 format!("{} bytes", bytes)
3341 } else if bytes < 10_000 {
3342 format!("{:.1} kB", v / 1000.0)
3343 } else if bytes < 2_000_000 {
3344 format!("{:.0} kB", v / 1000.0)
3345 } else if bytes < 10_000_000 {
3346 format!("{:.1} MB", v / 1_000_000.0)
3347 } else if bytes < 2_000_000_000 {
3348 format!("{:.0} MB", v / 1_000_000.0)
3349 } else if bytes < 10_000_000_000 {
3350 format!("{:.1} GB", v / 1_000_000_000.0)
3351 } else {
3352 format!("{:.0} GB", v / 1_000_000_000.0)
3353 }
3354}
3355
3356fn is_xmp_tag(tag: &str) -> bool {
3358 matches!(
3359 tag.to_lowercase().as_str(),
3360 "title"
3361 | "description"
3362 | "subject"
3363 | "creator"
3364 | "rights"
3365 | "keywords"
3366 | "rating"
3367 | "label"
3368 | "hierarchicalsubject"
3369 )
3370}
3371
3372fn encode_exif_tag(
3375 tag_name: &str,
3376 value: &str,
3377 _group: &str,
3378 bo: ByteOrderMark,
3379) -> Option<(u16, exif_writer::ExifFormat, Vec<u8>)> {
3380 let tag_lower = tag_name.to_lowercase();
3381
3382 let (tag_id, format): (u16, exif_writer::ExifFormat) = match tag_lower.as_str() {
3384 "imagedescription" => (0x010E, exif_writer::ExifFormat::Ascii),
3386 "make" => (0x010F, exif_writer::ExifFormat::Ascii),
3387 "model" => (0x0110, exif_writer::ExifFormat::Ascii),
3388 "software" => (0x0131, exif_writer::ExifFormat::Ascii),
3389 "modifydate" | "datetime" => (0x0132, exif_writer::ExifFormat::Ascii),
3390 "artist" => (0x013B, exif_writer::ExifFormat::Ascii),
3391 "copyright" => (0x8298, exif_writer::ExifFormat::Ascii),
3392 "orientation" => (0x0112, exif_writer::ExifFormat::Short),
3394 "xresolution" => (0x011A, exif_writer::ExifFormat::Rational),
3395 "yresolution" => (0x011B, exif_writer::ExifFormat::Rational),
3396 "resolutionunit" => (0x0128, exif_writer::ExifFormat::Short),
3397 "datetimeoriginal" => (0x9003, exif_writer::ExifFormat::Ascii),
3399 "createdate" | "datetimedigitized" => (0x9004, exif_writer::ExifFormat::Ascii),
3400 "usercomment" => (0x9286, exif_writer::ExifFormat::Undefined),
3401 "imageuniqueid" => (0xA420, exif_writer::ExifFormat::Ascii),
3402 "ownername" | "cameraownername" => (0xA430, exif_writer::ExifFormat::Ascii),
3403 "serialnumber" | "bodyserialnumber" => (0xA431, exif_writer::ExifFormat::Ascii),
3404 "lensmake" => (0xA433, exif_writer::ExifFormat::Ascii),
3405 "lensmodel" => (0xA434, exif_writer::ExifFormat::Ascii),
3406 "lensserialnumber" => (0xA435, exif_writer::ExifFormat::Ascii),
3407 _ => return None,
3408 };
3409
3410 let encoded = match format {
3411 exif_writer::ExifFormat::Ascii => exif_writer::encode_ascii(value),
3412 exif_writer::ExifFormat::Short => {
3413 let v: u16 = value.parse().ok()?;
3414 exif_writer::encode_u16(v, bo)
3415 }
3416 exif_writer::ExifFormat::Long => {
3417 let v: u32 = value.parse().ok()?;
3418 exif_writer::encode_u32(v, bo)
3419 }
3420 exif_writer::ExifFormat::Rational => {
3421 if let Some(slash) = value.find('/') {
3423 let num: u32 = value[..slash].trim().parse().ok()?;
3424 let den: u32 = value[slash + 1..].trim().parse().ok()?;
3425 exif_writer::encode_urational(num, den, bo)
3426 } else if let Ok(v) = value.parse::<f64>() {
3427 let den = 10000u32;
3429 let num = (v * den as f64).round() as u32;
3430 exif_writer::encode_urational(num, den, bo)
3431 } else {
3432 return None;
3433 }
3434 }
3435 exif_writer::ExifFormat::Undefined => {
3436 let mut data = vec![0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00]; data.extend_from_slice(value.as_bytes());
3439 data
3440 }
3441 _ => return None,
3442 };
3443
3444 Some((tag_id, format, encoded))
3445}
3446
3447fn compute_text_tags(data: &[u8], is_csv: bool) -> Vec<Tag> {
3449 let mut tags = Vec::new();
3450 let mk = |name: &str, val: String| Tag {
3451 id: crate::tag::TagId::Text(name.into()),
3452 name: name.into(),
3453 description: name.into(),
3454 group: crate::tag::TagGroup {
3455 family0: "File".into(),
3456 family1: "File".into(),
3457 family2: "Other".into(),
3458 family3: "Main".into(),
3459 },
3460 raw_value: Value::String(val.clone()),
3461 print_value: val,
3462 priority: 0,
3463 };
3464
3465 let is_ascii = data.iter().all(|&b| b < 128);
3467 let has_utf8_bom = data.starts_with(&[0xEF, 0xBB, 0xBF]);
3468 let has_utf16le_bom =
3469 data.starts_with(&[0xFF, 0xFE]) && !data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
3470 let has_utf16be_bom = data.starts_with(&[0xFE, 0xFF]);
3471 let has_utf32le_bom = data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
3472 let has_utf32be_bom = data.starts_with(&[0x00, 0x00, 0xFE, 0xFF]);
3473
3474 let has_weird_ctrl = data.iter().any(|&b| {
3476 (b <= 0x06) || (0x0e..=0x1a).contains(&b) || (0x1c..=0x1f).contains(&b) || b == 0x7f
3477 });
3478
3479 let (encoding, is_bom, is_utf16) = if has_utf32le_bom {
3480 ("utf-32le", true, false)
3481 } else if has_utf32be_bom {
3482 ("utf-32be", true, false)
3483 } else if has_utf16le_bom {
3484 ("utf-16le", true, true)
3485 } else if has_utf16be_bom {
3486 ("utf-16be", true, true)
3487 } else if has_weird_ctrl {
3488 return tags;
3490 } else if is_ascii {
3491 ("us-ascii", false, false)
3492 } else {
3493 let is_valid_utf8 = std::str::from_utf8(data).is_ok();
3495 if is_valid_utf8 {
3496 if has_utf8_bom {
3497 ("utf-8", true, false)
3498 } else {
3499 ("utf-8", false, false)
3503 }
3504 } else if !data.iter().any(|&b| (0x80..=0x9f).contains(&b)) {
3505 ("iso-8859-1", false, false)
3506 } else {
3507 ("unknown-8bit", false, false)
3508 }
3509 };
3510
3511 tags.push(mk("MIMEEncoding", encoding.into()));
3512
3513 if is_bom {
3514 tags.push(mk("ByteOrderMark", "Yes".into()));
3515 }
3516
3517 let has_cr = data.contains(&b'\r');
3519 let has_lf = data.contains(&b'\n');
3520 let newline_type = if has_cr && has_lf {
3521 "Windows CRLF"
3522 } else if has_lf {
3523 "Unix LF"
3524 } else if has_cr {
3525 "Macintosh CR"
3526 } else {
3527 "(none)"
3528 };
3529 tags.push(mk("Newlines", newline_type.into()));
3530
3531 if is_csv {
3532 let text = crate::encoding::decode_utf8_or_latin1(data);
3534 let mut delim = "";
3535 let mut quot = "";
3536 let mut ncols = 1usize;
3537 let mut nrows = 0usize;
3538
3539 for line in text.lines() {
3540 if nrows == 0 {
3541 let comma_count = line.matches(',').count();
3543 let semi_count = line.matches(';').count();
3544 let tab_count = line.matches('\t').count();
3545 if comma_count > semi_count && comma_count > tab_count {
3546 delim = ",";
3547 ncols = comma_count + 1;
3548 } else if semi_count > tab_count {
3549 delim = ";";
3550 ncols = semi_count + 1;
3551 } else if tab_count > 0 {
3552 delim = "\t";
3553 ncols = tab_count + 1;
3554 } else {
3555 delim = "";
3556 ncols = 1;
3557 }
3558 if line.contains('"') {
3560 quot = "\"";
3561 } else if line.contains('\'') {
3562 quot = "'";
3563 }
3564 }
3565 nrows += 1;
3566 if nrows >= 1000 {
3567 break;
3568 }
3569 }
3570
3571 let delim_display = match delim {
3572 "," => "Comma",
3573 ";" => "Semicolon",
3574 "\t" => "Tab",
3575 _ => "(none)",
3576 };
3577 let quot_display = match quot {
3578 "\"" => "Double quotes",
3579 "'" => "Single quotes",
3580 _ => "(none)",
3581 };
3582
3583 tags.push(mk("Delimiter", delim_display.into()));
3584 tags.push(mk("Quoting", quot_display.into()));
3585 tags.push(mk("ColumnCount", ncols.to_string()));
3586 if nrows > 0 {
3587 tags.push(mk("RowCount", nrows.to_string()));
3588 }
3589 } else if !is_utf16 {
3590 let nl_count = data.iter().filter(|&&b| b == b'\n').count();
3594 let line_count = if !data.is_empty() && data.last() != Some(&b'\n') {
3595 nl_count + 1
3596 } else {
3597 nl_count
3598 };
3599 tags.push(mk("LineCount", line_count.to_string()));
3600
3601 let text = crate::encoding::decode_utf8_or_latin1(data);
3602 let word_count = text.split_whitespace().count();
3603 tags.push(mk("WordCount", word_count.to_string()));
3604 }
3605
3606 tags
3607}
3608
3609#[cfg(test)]
3610mod tests {
3611 use super::*;
3612
3613 #[test]
3614 fn new_has_default_options() {
3615 let et = ExifTool::new();
3616 assert!(!et.options().duplicates);
3617 assert!(et.options().print_conv);
3618 assert_eq!(et.options().fast_scan, 0);
3619 assert!(et.options().requested_tags.is_empty());
3620 assert_eq!(et.options().extract_embedded, 0);
3621 assert_eq!(et.options().show_unknown, 0);
3622 assert!(!et.options().process_compressed);
3623 assert!(!et.options().use_mwg);
3624 }
3625
3626 #[test]
3627 fn tag_matches_request_group_qualified() {
3628 let tag = Tag {
3629 id: crate::tag::TagId::Text("By-line".into()),
3630 name: "By-line".into(),
3631 description: "By-line".into(),
3632 group: crate::tag::TagGroup {
3633 family0: "IPTC".into(),
3634 family1: "IPTC".into(),
3635 family2: "Author".into(),
3636 family3: "Main".into(),
3637 },
3638 raw_value: Value::String("Martín".into()),
3639 print_value: "Martín".into(),
3640 priority: 1,
3641 };
3642 assert!(ExifTool::tag_matches_request(&tag, "By-line"));
3644 assert!(ExifTool::tag_matches_request(&tag, "by-line"));
3645 assert!(ExifTool::tag_matches_request(&tag, "IPTC:By-line"));
3647 assert!(ExifTool::tag_matches_request(&tag, "Author:By-line"));
3648 assert!(ExifTool::tag_matches_request(&tag, "IPTC:*"));
3650 assert!(ExifTool::tag_matches_request(&tag, "*"));
3651 assert!(!ExifTool::tag_matches_request(&tag, "EXIF:By-line"));
3653 assert!(!ExifTool::tag_matches_request(&tag, "IPTC:Make"));
3654 assert!(!ExifTool::tag_matches_request(&tag, "Headline"));
3655 }
3656
3657 #[test]
3658 fn with_options_preserves_custom() {
3659 let opts = Options {
3660 duplicates: true,
3661 print_conv: false,
3662 fast_scan: 2,
3663 requested_tags: vec!["Artist".to_string()],
3664 extract_embedded: 1,
3665 show_unknown: 1,
3666 process_compressed: true,
3667 use_mwg: true,
3668 geolocation: true,
3669 };
3670 let et = ExifTool::with_options(opts.clone());
3671 assert!(et.options().duplicates);
3672 assert!(!et.options().print_conv);
3673 assert_eq!(et.options().fast_scan, 2);
3674 assert_eq!(et.options().requested_tags, vec!["Artist".to_string()]);
3675 assert_eq!(et.options().extract_embedded, 1);
3676 assert_eq!(et.options().show_unknown, 1);
3677 assert!(et.options().process_compressed);
3678 assert!(et.options().use_mwg);
3679 }
3680
3681 #[test]
3682 fn set_new_value_simple_tag() {
3683 let mut et = ExifTool::new();
3684 et.set_new_value("Artist", Some("John"));
3685 assert_eq!(et.new_values.len(), 1);
3686 assert_eq!(et.new_values[0].tag, "Artist");
3687 assert_eq!(et.new_values[0].group, None);
3688 assert_eq!(et.new_values[0].value, Some("John".to_string()));
3689 }
3690
3691 #[test]
3692 fn set_new_value_with_group_prefix() {
3693 let mut et = ExifTool::new();
3694 et.set_new_value("XMP:Title", Some("Test"));
3695 assert_eq!(et.new_values.len(), 1);
3696 assert_eq!(et.new_values[0].tag, "Title");
3697 assert_eq!(et.new_values[0].group, Some("XMP".to_string()));
3698 assert_eq!(et.new_values[0].value, Some("Test".to_string()));
3699 }
3700
3701 #[test]
3702 fn set_new_value_delete() {
3703 let mut et = ExifTool::new();
3704 et.set_new_value("Comment", None);
3705 assert_eq!(et.new_values.len(), 1);
3706 assert_eq!(et.new_values[0].tag, "Comment");
3707 assert_eq!(et.new_values[0].value, None);
3708 }
3709
3710 #[test]
3711 fn clear_new_values_empties_queue() {
3712 let mut et = ExifTool::new();
3713 et.set_new_value("Artist", Some("A"));
3714 et.set_new_value("Copyright", Some("B"));
3715 assert_eq!(et.new_values.len(), 2);
3716 et.clear_new_values();
3717 assert!(et.new_values.is_empty());
3718 }
3719
3720 #[test]
3721 fn set_new_value_multiple() {
3722 let mut et = ExifTool::new();
3723 et.set_new_value("Artist", Some("John"));
3724 et.set_new_value("IPTC:Keywords", Some("test"));
3725 et.set_new_value("XMP:Subject", None);
3726 assert_eq!(et.new_values.len(), 3);
3727 assert_eq!(et.new_values[1].group, Some("IPTC".to_string()));
3728 assert_eq!(et.new_values[1].tag, "Keywords");
3729 assert_eq!(et.new_values[2].value, None);
3730 }
3731
3732 #[test]
3733 fn options_mut_modifies() {
3734 let mut et = ExifTool::new();
3735 et.options_mut().duplicates = true;
3736 et.options_mut().fast_scan = 3;
3737 assert!(et.options().duplicates);
3738 assert_eq!(et.options().fast_scan, 3);
3739 }
3740
3741 #[test]
3742 fn default_options() {
3743 let opts = Options::default();
3744 assert!(!opts.duplicates);
3745 assert!(opts.print_conv);
3746 assert_eq!(opts.fast_scan, 0);
3747 }
3748}