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