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