1use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use crate::error::{Error, Result};
11use crate::file_type::{self, FileType};
12use crate::formats;
13use crate::metadata::exif::ByteOrderMark;
14use crate::tag::Tag;
15use crate::value::Value;
16use crate::writer::{
17 exif_writer, iptc_writer, jpeg_writer, matroska_writer, mp4_writer, pdf_writer, png_writer,
18 psd_writer, tiff_writer, webp_writer, xmp_writer,
19};
20
21#[derive(Debug, Clone)]
23pub struct Options {
24 pub duplicates: bool,
26 pub print_conv: bool,
28 pub fast_scan: u8,
30 pub requested_tags: Vec<String>,
32 pub extract_embedded: u8,
34 pub show_unknown: u8,
36 pub process_compressed: bool,
38 pub use_mwg: bool,
40 pub geolocation: bool,
43}
44
45impl Default for Options {
46 fn default() -> Self {
47 Self {
48 duplicates: false,
49 print_conv: true,
50 fast_scan: 0,
51 requested_tags: Vec::new(),
52 extract_embedded: 0,
53 show_unknown: 0,
54 process_compressed: false,
55 use_mwg: false,
56 geolocation: false,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
75pub struct NewValue {
76 pub tag: String,
78 pub group: Option<String>,
80 pub value: Option<String>,
82}
83
84pub struct ExifTool {
113 options: Options,
114 new_values: Vec<NewValue>,
115}
116
117pub type ImageInfo = HashMap<String, String>;
119
120impl ExifTool {
121 pub fn new() -> Self {
123 Self {
124 options: Options::default(),
125 new_values: Vec::new(),
126 }
127 }
128
129 pub fn with_options(options: Options) -> Self {
131 Self {
132 options,
133 new_values: Vec::new(),
134 }
135 }
136
137 pub fn options_mut(&mut self) -> &mut Options {
139 &mut self.options
140 }
141
142 pub fn options(&self) -> &Options {
144 &self.options
145 }
146
147 pub fn set_new_value(&mut self, tag: &str, value: Option<&str>) {
169 let (group, tag_name) = if let Some(colon_pos) = tag.find(':') {
170 (
171 Some(tag[..colon_pos].to_string()),
172 tag[colon_pos + 1..].to_string(),
173 )
174 } else {
175 (None, tag.to_string())
176 };
177
178 self.new_values.push(NewValue {
179 tag: tag_name,
180 group,
181 value: value.map(|v| v.to_string()),
182 });
183 }
184
185 pub fn clear_new_values(&mut self) {
187 self.new_values.clear();
188 }
189
190 pub fn set_new_values_from_file<P: AsRef<Path>>(
195 &mut self,
196 src_path: P,
197 tags_to_copy: Option<&[&str]>,
198 ) -> Result<u32> {
199 let src_tags = self.extract_info(src_path)?;
200 let mut count = 0u32;
201
202 for tag in &src_tags {
203 if tag.group.family0 == "File" || tag.group.family0 == "Composite" {
205 continue;
206 }
207 if tag.print_value.starts_with("(Binary") || tag.print_value.starts_with("(Undefined") {
209 continue;
210 }
211 if tag.print_value.is_empty() {
212 continue;
213 }
214
215 if let Some(filter) = tags_to_copy {
217 let name_lower = tag.name.to_lowercase();
218 if !filter.iter().any(|f| f.to_lowercase() == name_lower) {
219 continue;
220 }
221 }
222
223 let _full_tag = format!("{}:{}", tag.group.family0, tag.name);
224 self.new_values.push(NewValue {
225 tag: tag.name.clone(),
226 group: Some(tag.group.family0.clone()),
227 value: Some(tag.print_value.clone()),
228 });
229 count += 1;
230 }
231
232 Ok(count)
233 }
234
235 pub fn set_file_name_from_tag<P: AsRef<Path>>(
237 &self,
238 path: P,
239 tag_name: &str,
240 template: &str,
241 ) -> Result<String> {
242 let path = path.as_ref();
243 let tags = self.extract_info(path)?;
244
245 let tag_value = tags
246 .iter()
247 .find(|t| t.name.to_lowercase() == tag_name.to_lowercase())
248 .map(|t| &t.print_value)
249 .ok_or_else(|| Error::TagNotFound(tag_name.to_string()))?;
250
251 let new_name = if template.contains('%') {
254 template.replace("%v", value_to_filename(tag_value).as_str())
255 } else {
256 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
258 let clean = value_to_filename(tag_value);
259 if ext.is_empty() {
260 clean
261 } else {
262 format!("{}.{}", clean, ext)
263 }
264 };
265
266 let parent = path.parent().unwrap_or(Path::new(""));
267 let new_path = parent.join(&new_name);
268
269 fs::rename(path, &new_path).map_err(Error::Io)?;
270 Ok(new_path.to_string_lossy().to_string())
271 }
272
273 pub fn write_info<P: AsRef<Path>, Q: AsRef<Path>>(
278 &self,
279 src_path: P,
280 dst_path: Q,
281 ) -> Result<u32> {
282 let src_path = src_path.as_ref();
283 let dst_path = dst_path.as_ref();
284 let data = fs::read(src_path).map_err(Error::Io)?;
285
286 let file_type = self.detect_file_type(&data, src_path)?;
287 let output = self.apply_changes(&data, file_type)?;
288
289 let temp_path = dst_path.with_extension("exiftool_tmp");
291 fs::write(&temp_path, &output).map_err(Error::Io)?;
292 fs::rename(&temp_path, dst_path).map_err(Error::Io)?;
293
294 Ok(self.new_values.len() as u32)
295 }
296
297 fn apply_changes(&self, data: &[u8], file_type: FileType) -> Result<Vec<u8>> {
299 match file_type {
300 FileType::Jpeg => self.write_jpeg(data),
301 FileType::Png => self.write_png(data),
302 FileType::Tiff
303 | FileType::Dng
304 | FileType::Cr2
305 | FileType::Nef
306 | FileType::Arw
307 | FileType::Orf
308 | FileType::Pef => self.write_tiff(data),
309 FileType::WebP => self.write_webp(data),
310 FileType::Mp4
311 | FileType::QuickTime
312 | FileType::M4a
313 | FileType::ThreeGP
314 | FileType::F4v => self.write_mp4(data),
315 FileType::Psd => self.write_psd(data),
316 FileType::Pdf => self.write_pdf(data),
317 FileType::Heif | FileType::Avif => self.write_mp4(data),
318 FileType::Mkv | FileType::WebM => self.write_matroska(data),
319 FileType::Gif => {
320 let comment = self
321 .new_values
322 .iter()
323 .find(|nv| nv.tag.to_lowercase() == "comment")
324 .and_then(|nv| nv.value.clone());
325 crate::writer::gif_writer::write_gif(data, comment.as_deref())
326 }
327 FileType::Flac => {
328 let changes: Vec<(&str, &str)> = self
329 .new_values
330 .iter()
331 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
332 .collect();
333 crate::writer::flac_writer::write_flac(data, &changes)
334 }
335 FileType::Mp3 | FileType::Aiff => {
336 let changes: Vec<(&str, &str)> = self
337 .new_values
338 .iter()
339 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
340 .collect();
341 crate::writer::id3_writer::write_id3(data, &changes)
342 }
343 FileType::Jp2 | FileType::Jxl => {
344 let new_xmp = if self
345 .new_values
346 .iter()
347 .any(|nv| nv.group.as_deref() == Some("XMP"))
348 {
349 let refs: Vec<&NewValue> = self
350 .new_values
351 .iter()
352 .filter(|nv| nv.group.as_deref() == Some("XMP"))
353 .collect();
354 Some(self.build_new_xmp(&refs))
355 } else {
356 None
357 };
358 crate::writer::jp2_writer::write_jp2(data, new_xmp.as_deref(), None)
359 }
360 FileType::PostScript => {
361 let changes: Vec<(&str, &str)> = self
362 .new_values
363 .iter()
364 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
365 .collect();
366 crate::writer::ps_writer::write_postscript(data, &changes)
367 }
368 FileType::Ogg | FileType::Opus => {
369 let changes: Vec<(&str, &str)> = self
370 .new_values
371 .iter()
372 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
373 .collect();
374 crate::writer::ogg_writer::write_ogg(data, &changes)
375 }
376 FileType::Xmp => {
377 let props: Vec<xmp_writer::XmpProperty> = self
378 .new_values
379 .iter()
380 .filter_map(|nv| {
381 let val = nv.value.as_deref()?;
382 Some(xmp_writer::XmpProperty {
383 namespace: nv.group.clone().unwrap_or_else(|| "dc".into()),
384 property: nv.tag.clone(),
385 values: vec![val.to_string()],
386 prop_type: xmp_writer::XmpPropertyType::Simple,
387 })
388 })
389 .collect();
390 Ok(crate::writer::xmp_sidecar_writer::write_xmp_sidecar(&props))
391 }
392 _ => Err(Error::UnsupportedFileType(format!(
393 "writing not yet supported for {}",
394 file_type
395 ))),
396 }
397 }
398
399 pub fn writable_tags(file_type: FileType) -> Option<std::collections::HashSet<&'static str>> {
403 use std::collections::HashSet;
404
405 const EXIF_TAGS: &[&str] = &[
407 "imagedescription",
408 "make",
409 "model",
410 "orientation",
411 "xresolution",
412 "yresolution",
413 "resolutionunit",
414 "software",
415 "modifydate",
416 "datetime",
417 "artist",
418 "copyright",
419 "datetimeoriginal",
420 "createdate",
421 "datetimedigitized",
422 "usercomment",
423 "imageuniqueid",
424 "ownername",
425 "cameraownername",
426 "serialnumber",
427 "bodyserialnumber",
428 "lensmake",
429 "lensmodel",
430 "lensserialnumber",
431 ];
432
433 const IPTC_TAGS: &[&str] = &[
435 "objectname",
436 "title",
437 "urgency",
438 "category",
439 "supplementalcategories",
440 "keywords",
441 "specialinstructions",
442 "datecreated",
443 "timecreated",
444 "by-line",
445 "author",
446 "byline",
447 "by-linetitle",
448 "authorsposition",
449 "bylinetitle",
450 "city",
451 "sub-location",
452 "sublocation",
453 "province-state",
454 "state",
455 "provincestate",
456 "country-primarylocationcode",
457 "countrycode",
458 "country-primarylocationname",
459 "country",
460 "headline",
461 "credit",
462 "source",
463 "copyrightnotice",
464 "contact",
465 "caption-abstract",
466 "caption",
467 "description",
468 "writer-editor",
469 "captionwriter",
470 ];
471
472 const XMP_AUTO_TAGS: &[&str] = &[
474 "title",
475 "description",
476 "subject",
477 "creator",
478 "rights",
479 "keywords",
480 "rating",
481 "label",
482 "hierarchicalsubject",
483 ];
484
485 const ID3_TAGS: &[&str] = &[
487 "title",
488 "artist",
489 "album",
490 "year",
491 "date",
492 "track",
493 "genre",
494 "comment",
495 "composer",
496 "albumartist",
497 "encoder",
498 "encodedby",
499 "publisher",
500 "copyright",
501 "bpm",
502 "lyrics",
503 ];
504
505 const MP4_TAGS: &[&str] = &[
507 "title",
508 "artist",
509 "album",
510 "year",
511 "date",
512 "comment",
513 "genre",
514 "composer",
515 "writer",
516 "encoder",
517 "encodedby",
518 "grouping",
519 "lyrics",
520 "description",
521 "albumartist",
522 "copyright",
523 ];
524
525 const PDF_TAGS: &[&str] = &[
527 "title", "author", "subject", "keywords", "creator", "producer",
528 ];
529
530 const PS_TAGS: &[&str] = &[
532 "title",
533 "creator",
534 "author",
535 "for",
536 "creationdate",
537 "createdate",
538 ];
539
540 match file_type {
541 FileType::Png
543 | FileType::Flac
544 | FileType::Mkv
545 | FileType::WebM
546 | FileType::Ogg
547 | FileType::Opus
548 | FileType::Xmp => None,
549
550 FileType::Jpeg => {
552 let mut set: HashSet<&str> = HashSet::new();
553 set.extend(EXIF_TAGS);
554 set.extend(IPTC_TAGS);
555 set.extend(XMP_AUTO_TAGS);
556 set.insert("comment");
557 Some(set)
558 }
559
560 FileType::Tiff
562 | FileType::Dng
563 | FileType::Cr2
564 | FileType::Nef
565 | FileType::Arw
566 | FileType::Orf
567 | FileType::Pef => {
568 let mut set: HashSet<&str> = HashSet::new();
569 set.extend(EXIF_TAGS);
570 Some(set)
571 }
572
573 FileType::WebP => {
575 let mut set: HashSet<&str> = HashSet::new();
576 set.extend(EXIF_TAGS);
577 set.extend(XMP_AUTO_TAGS);
578 Some(set)
579 }
580
581 FileType::Mp4
583 | FileType::QuickTime
584 | FileType::M4a
585 | FileType::ThreeGP
586 | FileType::F4v
587 | FileType::Heif
588 | FileType::Avif => {
589 let mut set: HashSet<&str> = HashSet::new();
590 set.extend(MP4_TAGS);
591 set.extend(XMP_AUTO_TAGS);
592 Some(set)
593 }
594
595 FileType::Psd => {
597 let mut set: HashSet<&str> = HashSet::new();
598 set.extend(IPTC_TAGS);
599 set.extend(XMP_AUTO_TAGS);
600 Some(set)
601 }
602
603 FileType::Pdf => Some(PDF_TAGS.iter().copied().collect()),
604 FileType::PostScript => Some(PS_TAGS.iter().copied().collect()),
605
606 FileType::Mp3 | FileType::Aiff => Some(ID3_TAGS.iter().copied().collect()),
607
608 FileType::Gif => {
609 let mut set: HashSet<&str> = HashSet::new();
610 set.insert("comment");
611 Some(set)
612 }
613
614 FileType::Jp2 | FileType::Jxl => Some(XMP_AUTO_TAGS.iter().copied().collect()),
616
617 _ => Some(HashSet::new()),
619 }
620 }
621
622 fn write_jpeg(&self, data: &[u8]) -> Result<Vec<u8>> {
624 let mut exif_values: Vec<&NewValue> = Vec::new();
626 let mut xmp_values: Vec<&NewValue> = Vec::new();
627 let mut iptc_values: Vec<&NewValue> = Vec::new();
628 let mut comment_value: Option<&str> = None;
629 let mut remove_exif = false;
630 let mut remove_xmp = false;
631 let mut remove_iptc = false;
632 let mut remove_comment = false;
633
634 for nv in &self.new_values {
635 let group = nv.group.as_deref().unwrap_or("");
636 let group_upper = group.to_uppercase();
637
638 if nv.value.is_none() && nv.tag == "*" {
640 match group_upper.as_str() {
641 "EXIF" => {
642 remove_exif = true;
643 continue;
644 }
645 "XMP" => {
646 remove_xmp = true;
647 continue;
648 }
649 "IPTC" => {
650 remove_iptc = true;
651 continue;
652 }
653 _ => {}
654 }
655 }
656
657 match group_upper.as_str() {
658 "XMP" => xmp_values.push(nv),
659 "IPTC" => iptc_values.push(nv),
660 "EXIF" | "IFD0" | "EXIFIFD" | "GPS" => exif_values.push(nv),
661 "" => {
662 if nv.tag.to_lowercase() == "comment" {
664 if nv.value.is_none() {
665 remove_comment = true;
666 } else {
667 comment_value = nv.value.as_deref();
668 }
669 } else if is_xmp_tag(&nv.tag) {
670 xmp_values.push(nv);
671 } else {
672 exif_values.push(nv);
673 }
674 }
675 _ => exif_values.push(nv), }
677 }
678
679 let new_exif = if !exif_values.is_empty() {
681 Some(self.build_new_exif(data, &exif_values)?)
682 } else {
683 None
684 };
685
686 let new_xmp = if !xmp_values.is_empty() {
688 Some(self.build_new_xmp(&xmp_values))
689 } else {
690 None
691 };
692
693 let new_iptc_data = if !iptc_values.is_empty() {
695 let records: Vec<iptc_writer::IptcRecord> = iptc_values
696 .iter()
697 .filter_map(|nv| {
698 let value = nv.value.as_deref()?;
699 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
700 Some(iptc_writer::IptcRecord {
701 record,
702 dataset,
703 data: value.as_bytes().to_vec(),
704 })
705 })
706 .collect();
707 if records.is_empty() {
708 None
709 } else {
710 Some(iptc_writer::build_iptc(&records))
711 }
712 } else {
713 None
714 };
715
716 jpeg_writer::write_jpeg(
718 data,
719 new_exif.as_deref(),
720 new_xmp.as_deref(),
721 new_iptc_data.as_deref(),
722 comment_value,
723 remove_exif,
724 remove_xmp,
725 remove_iptc,
726 remove_comment,
727 )
728 }
729
730 fn build_new_exif(&self, jpeg_data: &[u8], values: &[&NewValue]) -> Result<Vec<u8>> {
732 let bo = ByteOrderMark::BigEndian;
733 let mut ifd0_entries = Vec::new();
734 let mut exif_entries = Vec::new();
735 let mut gps_entries = Vec::new();
736
737 let existing = extract_existing_exif_entries(jpeg_data, bo);
739 for entry in &existing {
740 match classify_exif_tag(entry.tag) {
741 ExifIfdGroup::Ifd0 => ifd0_entries.push(entry.clone()),
742 ExifIfdGroup::ExifIfd => exif_entries.push(entry.clone()),
743 ExifIfdGroup::Gps => gps_entries.push(entry.clone()),
744 }
745 }
746
747 let deleted_tags: Vec<u16> = values
749 .iter()
750 .filter(|nv| nv.value.is_none())
751 .filter_map(|nv| tag_name_to_id(&nv.tag))
752 .collect();
753
754 ifd0_entries.retain(|e| !deleted_tags.contains(&e.tag));
756 exif_entries.retain(|e| !deleted_tags.contains(&e.tag));
757 gps_entries.retain(|e| !deleted_tags.contains(&e.tag));
758
759 for nv in values {
761 if nv.value.is_none() {
762 continue;
763 }
764 let value_str = nv.value.as_deref().unwrap_or("");
765 let group = nv.group.as_deref().unwrap_or("");
766
767 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, value_str, group, bo)
768 {
769 let entry = exif_writer::IfdEntry {
770 tag: tag_id,
771 format,
772 data: encoded,
773 };
774
775 let target = match group.to_uppercase().as_str() {
776 "GPS" => &mut gps_entries,
777 "EXIFIFD" => &mut exif_entries,
778 _ => match classify_exif_tag(tag_id) {
779 ExifIfdGroup::ExifIfd => &mut exif_entries,
780 ExifIfdGroup::Gps => &mut gps_entries,
781 ExifIfdGroup::Ifd0 => &mut ifd0_entries,
782 },
783 };
784
785 if let Some(existing) = target.iter_mut().find(|e| e.tag == tag_id) {
787 *existing = entry;
788 } else {
789 target.push(entry);
790 }
791 }
792 }
793
794 ifd0_entries.retain(|e| e.tag != 0x8769 && e.tag != 0x8825 && e.tag != 0xA005);
796
797 exif_writer::build_exif(&ifd0_entries, &exif_entries, &gps_entries, bo)
798 }
799
800 fn write_png(&self, data: &[u8]) -> Result<Vec<u8>> {
802 let mut new_text: Vec<(&str, &str)> = Vec::new();
803 let mut remove_text: Vec<&str> = Vec::new();
804
805 let owned_pairs: Vec<(String, String)> = self
808 .new_values
809 .iter()
810 .filter(|nv| nv.value.is_some())
811 .map(|nv| (nv.tag.clone(), nv.value.clone().unwrap()))
812 .collect();
813
814 for (tag, value) in &owned_pairs {
815 new_text.push((tag.as_str(), value.as_str()));
816 }
817
818 for nv in &self.new_values {
819 if nv.value.is_none() {
820 remove_text.push(&nv.tag);
821 }
822 }
823
824 png_writer::write_png(data, &new_text, None, &remove_text)
825 }
826
827 fn write_psd(&self, data: &[u8]) -> Result<Vec<u8>> {
829 let mut iptc_values = Vec::new();
830 let mut xmp_values = Vec::new();
831
832 for nv in &self.new_values {
833 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
834 match group.as_str() {
835 "XMP" => xmp_values.push(nv),
836 "IPTC" => iptc_values.push(nv),
837 _ => {
838 if is_xmp_tag(&nv.tag) {
839 xmp_values.push(nv);
840 } else {
841 iptc_values.push(nv);
842 }
843 }
844 }
845 }
846
847 let new_iptc = if !iptc_values.is_empty() {
848 let records: Vec<_> = iptc_values
849 .iter()
850 .filter_map(|nv| {
851 let value = nv.value.as_deref()?;
852 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
853 Some(iptc_writer::IptcRecord {
854 record,
855 dataset,
856 data: value.as_bytes().to_vec(),
857 })
858 })
859 .collect();
860 if records.is_empty() {
861 None
862 } else {
863 Some(iptc_writer::build_iptc(&records))
864 }
865 } else {
866 None
867 };
868
869 let new_xmp = if !xmp_values.is_empty() {
870 let refs: Vec<&NewValue> = xmp_values.to_vec();
871 Some(self.build_new_xmp(&refs))
872 } else {
873 None
874 };
875
876 psd_writer::write_psd(data, new_iptc.as_deref(), new_xmp.as_deref())
877 }
878
879 fn write_matroska(&self, data: &[u8]) -> Result<Vec<u8>> {
881 let changes: Vec<(&str, &str)> = self
882 .new_values
883 .iter()
884 .filter_map(|nv| {
885 let value = nv.value.as_deref()?;
886 Some((nv.tag.as_str(), value))
887 })
888 .collect();
889
890 matroska_writer::write_matroska(data, &changes)
891 }
892
893 fn write_pdf(&self, data: &[u8]) -> Result<Vec<u8>> {
895 let changes: Vec<(&str, &str)> = self
896 .new_values
897 .iter()
898 .filter_map(|nv| {
899 let value = nv.value.as_deref()?;
900 Some((nv.tag.as_str(), value))
901 })
902 .collect();
903
904 pdf_writer::write_pdf(data, &changes)
905 }
906
907 fn write_mp4(&self, data: &[u8]) -> Result<Vec<u8>> {
909 let mut ilst_tags: Vec<([u8; 4], String)> = Vec::new();
910 let mut xmp_values: Vec<&NewValue> = Vec::new();
911
912 for nv in &self.new_values {
913 if nv.value.is_none() {
914 continue;
915 }
916 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
917 if group == "XMP" {
918 xmp_values.push(nv);
919 } else if let Some(key) = mp4_writer::tag_to_ilst_key(&nv.tag) {
920 ilst_tags.push((key, nv.value.clone().unwrap()));
921 }
922 }
923
924 let tag_refs: Vec<(&[u8; 4], &str)> =
925 ilst_tags.iter().map(|(k, v)| (k, v.as_str())).collect();
926
927 let new_xmp = if !xmp_values.is_empty() {
928 let refs: Vec<&NewValue> = xmp_values.to_vec();
929 Some(self.build_new_xmp(&refs))
930 } else {
931 None
932 };
933
934 mp4_writer::write_mp4(data, &tag_refs, new_xmp.as_deref())
935 }
936
937 fn write_webp(&self, data: &[u8]) -> Result<Vec<u8>> {
939 let mut exif_values: Vec<&NewValue> = Vec::new();
940 let mut xmp_values: Vec<&NewValue> = Vec::new();
941 let mut remove_exif = false;
942 let mut remove_xmp = false;
943
944 for nv in &self.new_values {
945 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
946 if nv.value.is_none() && nv.tag == "*" {
947 if group == "EXIF" {
948 remove_exif = true;
949 }
950 if group == "XMP" {
951 remove_xmp = true;
952 }
953 continue;
954 }
955 match group.as_str() {
956 "XMP" => xmp_values.push(nv),
957 _ => exif_values.push(nv),
958 }
959 }
960
961 let new_exif = if !exif_values.is_empty() {
962 let bo = ByteOrderMark::BigEndian;
963 let mut entries = Vec::new();
964 for nv in &exif_values {
965 if let Some(ref v) = nv.value {
966 let group = nv.group.as_deref().unwrap_or("");
967 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, v, group, bo)
968 {
969 entries.push(exif_writer::IfdEntry {
970 tag: tag_id,
971 format,
972 data: encoded,
973 });
974 }
975 }
976 }
977 if !entries.is_empty() {
978 Some(exif_writer::build_exif(&entries, &[], &[], bo)?)
979 } else {
980 None
981 }
982 } else {
983 None
984 };
985
986 let new_xmp = if !xmp_values.is_empty() {
987 Some(self.build_new_xmp(&xmp_values.to_vec()))
988 } else {
989 None
990 };
991
992 webp_writer::write_webp(
993 data,
994 new_exif.as_deref(),
995 new_xmp.as_deref(),
996 remove_exif,
997 remove_xmp,
998 )
999 }
1000
1001 fn write_tiff(&self, data: &[u8]) -> Result<Vec<u8>> {
1003 let bo = if data.starts_with(b"II") {
1004 ByteOrderMark::LittleEndian
1005 } else {
1006 ByteOrderMark::BigEndian
1007 };
1008
1009 let mut changes: Vec<(u16, Vec<u8>)> = Vec::new();
1010 for nv in &self.new_values {
1011 if let Some(ref value) = nv.value {
1012 let group = nv.group.as_deref().unwrap_or("");
1013 if let Some((tag_id, _format, encoded)) = encode_exif_tag(&nv.tag, value, group, bo)
1014 {
1015 changes.push((tag_id, encoded));
1016 }
1017 }
1018 }
1019
1020 tiff_writer::write_tiff(data, &changes)
1021 }
1022
1023 fn build_new_xmp(&self, values: &[&NewValue]) -> Vec<u8> {
1025 let mut properties = Vec::new();
1026
1027 for nv in values {
1028 let value_str = match &nv.value {
1029 Some(v) => v.clone(),
1030 None => continue,
1031 };
1032
1033 let ns = nv.group.as_deref().unwrap_or("dc").to_lowercase();
1034 let ns = if ns == "xmp" { "xmp".to_string() } else { ns };
1035
1036 let prop_type = match nv.tag.to_lowercase().as_str() {
1037 "title" | "description" | "rights" => xmp_writer::XmpPropertyType::LangAlt,
1038 "subject" | "keywords" => xmp_writer::XmpPropertyType::Bag,
1039 "creator" => xmp_writer::XmpPropertyType::Seq,
1040 _ => xmp_writer::XmpPropertyType::Simple,
1041 };
1042
1043 let values = if matches!(
1044 prop_type,
1045 xmp_writer::XmpPropertyType::Bag | xmp_writer::XmpPropertyType::Seq
1046 ) {
1047 value_str.split(',').map(|s| s.trim().to_string()).collect()
1048 } else {
1049 vec![value_str]
1050 };
1051
1052 properties.push(xmp_writer::XmpProperty {
1053 namespace: ns,
1054 property: nv.tag.clone(),
1055 values,
1056 prop_type,
1057 });
1058 }
1059
1060 xmp_writer::build_xmp(&properties).into_bytes()
1061 }
1062
1063 pub fn image_info<P: AsRef<Path>>(&self, path: P) -> Result<ImageInfo> {
1071 let tags = self.extract_info(path)?;
1072 Ok(self.get_info(&tags))
1073 }
1074
1075 pub fn extract_info<P: AsRef<Path>>(&self, path: P) -> Result<Vec<Tag>> {
1079 let path = path.as_ref();
1080 let data = map_file_for_read(path)?;
1086 self.extract_info_from_bytes(&data, path)
1087 }
1088
1089 pub fn extract_info_from_bytes(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
1091 crate::metadata::exif::set_show_unknown(self.options.show_unknown);
1093 crate::formats::pdf::set_process_compressed(self.options.process_compressed);
1095
1096 let file_type_result = self.detect_file_type(data, path);
1097 let (file_type, mut tags) = match file_type_result {
1098 Ok(ft) => {
1099 let t = self
1100 .process_file(data, ft)
1101 .or_else(|_| self.process_by_extension(data, path))?;
1102 (Some(ft), t)
1103 }
1104 Err(_) => {
1105 let t = self.process_by_extension(data, path)?;
1107 (None, t)
1108 }
1109 };
1110 let file_type = file_type.unwrap_or(FileType::Zip); let default_tags = || {
1115 (
1116 file_type.code().to_string(),
1117 file_type.mime_type().to_string(),
1118 file_type
1119 .extensions()
1120 .first()
1121 .copied()
1122 .unwrap_or("")
1123 .to_string(),
1124 )
1125 };
1126 let (ft_code, mime_str, ext_str): (String, String, String) = if file_type == FileType::Exe {
1127 exe_subtype(data)
1128 .map(|(ft, mime, ext)| (ft.to_string(), mime.to_string(), ext.to_string()))
1129 .unwrap_or_else(default_tags)
1130 } else if let Some((code, mime)) = refine_filetype_by_content(file_type, data) {
1131 let (_, _, ext) = default_tags();
1132 (code, mime, ext)
1133 } else {
1134 default_tags()
1135 };
1136
1137 tags.push(Tag {
1139 id: crate::tag::TagId::Text("FileType".into()),
1140 name: "FileType".into(),
1141 description: "File Type".into(),
1142 group: crate::tag::TagGroup {
1143 family0: "File".into(),
1144 family1: "File".into(),
1145 family2: "Other".into(),
1146 },
1147 raw_value: Value::String(format!("{:?}", file_type)),
1148 print_value: ft_code.clone(),
1151 priority: 1,
1152 });
1153
1154 tags.push(Tag {
1155 id: crate::tag::TagId::Text("MIMEType".into()),
1156 name: "MIMEType".into(),
1157 description: "MIME Type".into(),
1158 group: crate::tag::TagGroup {
1159 family0: "File".into(),
1160 family1: "File".into(),
1161 family2: "Other".into(),
1162 },
1163 raw_value: Value::String(mime_str.clone()),
1164 print_value: mime_str.clone(),
1165 priority: 1,
1166 });
1167
1168 if let Ok(metadata) = fs::metadata(path) {
1169 tags.push(Tag {
1170 id: crate::tag::TagId::Text("FileSize".into()),
1171 name: "FileSize".into(),
1172 description: "File Size".into(),
1173 group: crate::tag::TagGroup {
1174 family0: "File".into(),
1175 family1: "File".into(),
1176 family2: "Other".into(),
1177 },
1178 raw_value: Value::String(metadata.len().to_string()),
1181 print_value: format_file_size(metadata.len()),
1182 priority: 0,
1183 });
1184 }
1185
1186 let file_tag = |name: &str, val: Value| -> Tag {
1188 Tag {
1189 id: crate::tag::TagId::Text(name.to_string()),
1190 name: name.to_string(),
1191 description: name.to_string(),
1192 group: crate::tag::TagGroup {
1193 family0: "File".into(),
1194 family1: "File".into(),
1195 family2: "Other".into(),
1196 },
1197 raw_value: val.clone(),
1198 print_value: val.to_display_string(),
1199 priority: 1,
1200 }
1201 };
1202
1203 if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
1204 tags.push(file_tag("FileName", Value::String(fname.to_string())));
1205 }
1206 if let Some(dir) = path.parent().and_then(|p| p.to_str()) {
1207 tags.push(file_tag("Directory", Value::String(dir.to_string())));
1208 }
1209 if !ext_str.is_empty() || file_type == FileType::Exe {
1212 tags.push(file_tag(
1213 "FileTypeExtension",
1214 Value::String(ext_str.clone()),
1215 ));
1216 }
1217
1218 #[cfg(unix)]
1219 if let Ok(metadata) = fs::metadata(path) {
1220 use std::os::unix::fs::MetadataExt;
1221 let mode = metadata.mode();
1222 tags.push(Tag {
1226 id: crate::tag::TagId::Text("FilePermissions".into()),
1227 name: "FilePermissions".into(),
1228 description: "FilePermissions".into(),
1229 group: crate::tag::TagGroup {
1230 family0: "File".into(),
1231 family1: "File".into(),
1232 family2: "Other".into(),
1233 },
1234 raw_value: Value::String(format!("{:o}", mode)),
1235 print_value: format_file_permissions(mode),
1236 priority: 1,
1237 });
1238
1239 use crate::formats::gzip::gzip_unix_to_datetime;
1242 if let Ok(modified) = metadata.modified() {
1244 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
1245 let secs = dur.as_secs() as i64;
1246 tags.push(file_tag(
1247 "FileModifyDate",
1248 Value::String(gzip_unix_to_datetime(secs)),
1249 ));
1250 }
1251 }
1252 if let Ok(accessed) = metadata.accessed() {
1254 if let Ok(dur) = accessed.duration_since(std::time::UNIX_EPOCH) {
1255 let secs = dur.as_secs() as i64;
1256 tags.push(file_tag(
1257 "FileAccessDate",
1258 Value::String(gzip_unix_to_datetime(secs)),
1259 ));
1260 }
1261 }
1262 let ctime = metadata.ctime();
1264 if ctime > 0 {
1265 tags.push(file_tag(
1266 "FileInodeChangeDate",
1267 Value::String(gzip_unix_to_datetime(ctime)),
1268 ));
1269 }
1270 }
1271
1272 {
1274 let bo_str = if data.len() > 8 {
1275 let check: Option<&[u8]> = if data.starts_with(&[0xFF, 0xD8]) {
1277 data.windows(6)
1279 .position(|w| w == b"Exif\0\0")
1280 .map(|p| &data[p + 6..])
1281 } else if data.starts_with(b"FUJIFILMCCD-RAW") && data.len() >= 0x60 {
1282 let jpeg_offset =
1284 u32::from_be_bytes([data[0x54], data[0x55], data[0x56], data[0x57]])
1285 as usize;
1286 let jpeg_length =
1287 u32::from_be_bytes([data[0x58], data[0x59], data[0x5A], data[0x5B]])
1288 as usize;
1289 if jpeg_offset > 0 && jpeg_offset + jpeg_length <= data.len() {
1290 let jpeg = &data[jpeg_offset..jpeg_offset + jpeg_length];
1291 jpeg.windows(6)
1292 .position(|w| w == b"Exif\0\0")
1293 .map(|p| &jpeg[p + 6..])
1294 } else {
1295 None
1296 }
1297 } else if data.starts_with(b"RIFF") && data.len() >= 12 {
1298 let mut riff_bo: Option<&[u8]> = None;
1300 let mut pos = 12usize;
1301 while pos + 8 <= data.len() {
1302 let cid = &data[pos..pos + 4];
1303 let csz = u32::from_le_bytes([
1304 data[pos + 4],
1305 data[pos + 5],
1306 data[pos + 6],
1307 data[pos + 7],
1308 ]) as usize;
1309 let cstart = pos + 8;
1310 let cend = (cstart + csz).min(data.len());
1311 if cid == b"EXIF" && cend > cstart {
1312 let exif_data = &data[cstart..cend];
1313 let tiff = if exif_data.starts_with(b"Exif\0\0") {
1314 &exif_data[6..]
1315 } else {
1316 exif_data
1317 };
1318 riff_bo = Some(tiff);
1319 break;
1320 }
1321 if cid == b"LIST" && cend >= cstart + 4 {
1323 }
1325 pos = cend + (csz & 1);
1326 }
1327 riff_bo
1328 } else if data.starts_with(&[0x00, 0x00, 0x00, 0x0C, b'J', b'X', b'L', b' ']) {
1329 let mut jxl_bo: Option<String> = None;
1331 let mut jpos = 12usize; while jpos + 8 <= data.len() {
1333 let bsize = u32::from_be_bytes([
1334 data[jpos],
1335 data[jpos + 1],
1336 data[jpos + 2],
1337 data[jpos + 3],
1338 ]) as usize;
1339 let btype = &data[jpos + 4..jpos + 8];
1340 if bsize < 8 || jpos + bsize > data.len() {
1341 break;
1342 }
1343 if btype == b"brob" && jpos + bsize > 12 {
1344 let inner_type = &data[jpos + 8..jpos + 12];
1345 if inner_type == b"Exif" || inner_type == b"exif" {
1346 let brotli_payload = &data[jpos + 12..jpos + bsize];
1347 use std::io::Cursor;
1348 let mut inp = Cursor::new(brotli_payload);
1349 let mut out: Vec<u8> = Vec::new();
1350 if brotli::BrotliDecompress(&mut inp, &mut out).is_ok() {
1351 let exif_start = if out.len() > 4 { 4 } else { 0 };
1352 if exif_start < out.len() {
1353 if out[exif_start..].starts_with(b"MM") {
1354 jxl_bo = Some("Big-endian (Motorola, MM)".to_string());
1355 } else if out[exif_start..].starts_with(b"II") {
1356 jxl_bo = Some("Little-endian (Intel, II)".to_string());
1357 }
1358 }
1359 }
1360 break;
1361 }
1362 }
1363 jpos += bsize;
1364 }
1365 if let Some(bo) = jxl_bo {
1366 if !bo.is_empty() && file_type != FileType::Btf {
1367 tags.push(file_tag("ExifByteOrder", Value::String(bo)));
1368 }
1369 }
1370 None
1372 } else if data.starts_with(&[0x00, b'M', b'R', b'M']) {
1373 let mrw_data_offset = if data.len() >= 8 {
1375 u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize + 8
1376 } else {
1377 0
1378 };
1379 let mut mrw_bo: Option<&[u8]> = None;
1380 let mut mpos = 8usize;
1381 while mpos + 8 <= mrw_data_offset.min(data.len()) {
1382 let seg_tag = &data[mpos..mpos + 4];
1383 let seg_len = u32::from_be_bytes([
1384 data[mpos + 4],
1385 data[mpos + 5],
1386 data[mpos + 6],
1387 data[mpos + 7],
1388 ]) as usize;
1389 if seg_tag == b"\x00TTW" && mpos + 8 + seg_len <= data.len() {
1390 mrw_bo = Some(&data[mpos + 8..mpos + 8 + seg_len]);
1391 break;
1392 }
1393 mpos += 8 + seg_len;
1394 }
1395 mrw_bo
1396 } else {
1397 Some(data)
1398 };
1399 if let Some(tiff) = check {
1400 if tiff.starts_with(b"II") {
1401 "Little-endian (Intel, II)"
1402 } else if tiff.starts_with(b"MM") {
1403 "Big-endian (Motorola, MM)"
1404 } else {
1405 ""
1406 }
1407 } else {
1408 ""
1409 }
1410 } else {
1411 ""
1412 };
1413 let already_has_exifbyteorder = tags.iter().any(|t| t.name == "ExifByteOrder");
1416 if !bo_str.is_empty()
1417 && !already_has_exifbyteorder
1418 && file_type != FileType::Btf
1419 && file_type != FileType::Dr4
1420 && file_type != FileType::Vrd
1421 && file_type != FileType::Crw
1422 {
1423 tags.push(file_tag("ExifByteOrder", Value::String(bo_str.to_string())));
1424 }
1425 }
1426
1427 tags.push(file_tag(
1428 "ExifToolVersion",
1429 Value::String(crate::VERSION.to_string()),
1430 ));
1431
1432 {
1435 const SPECIAL_WINS: &[(&str, &str)] = &[
1436 ("Kodak", "FNumber"),
1437 ("Kodak", "ExposureTime"),
1438 ("MinoltaRaw", "Contrast"),
1439 ("MinoltaRaw", "Saturation"),
1440 ("MinoltaRaw", "Sharpness"),
1441 ("MinoltaRaw", "ISOSetting"),
1442 ("Lytro", "FocalLength"),
1445 ];
1446 for (grp, name) in SPECIAL_WINS {
1447 if tags
1448 .iter()
1449 .any(|t| t.name == *name && t.group.family1 == *grp)
1450 {
1451 tags.retain(|t| t.name != *name || t.group.family1 == *grp);
1452 }
1453 }
1454 }
1455
1456 let composite = crate::composite::compute_composite_tags(&tags);
1458 tags.extend(composite);
1459
1460 for bal in ["RedBalance", "BlueBalance"] {
1464 let has_composite = tags
1465 .iter()
1466 .any(|t| t.name == bal && t.group.family0 == "Composite");
1467 if has_composite {
1468 tags.retain(|t| t.name != bal || t.group.family0 == "Composite");
1469 }
1470 }
1471
1472 if self.options.geolocation {
1474 if let Some(geo) = crate::composite::compute_geolocation(&tags) {
1475 tags.extend(geo);
1476 }
1477 }
1478
1479 if self.options.use_mwg {
1481 let mwg = crate::composite::compute_mwg_composites(&tags);
1482 tags.extend(mwg);
1483 }
1484
1485 {
1491 let is_flir_fff = tags
1492 .iter()
1493 .any(|t| t.group.family0 == "APP1" && t.group.family1 == "FLIR");
1494 if is_flir_fff {
1495 tags.retain(|t| !(t.name == "LensID" && t.group.family0 == "Composite"));
1496 }
1497 }
1498
1499 {
1504 let make = tags
1505 .iter()
1506 .find(|t| t.name == "Make")
1507 .map(|t| t.print_value.clone())
1508 .unwrap_or_default();
1509 if !make.to_uppercase().contains("CANON") {
1510 tags.retain(|t| t.name != "Lens" || t.group.family0 != "Composite");
1511 }
1512 }
1513
1514 {
1518 let riff_priority_zero_tags = ["Quality", "SampleSize", "StreamType"];
1519 for tag_name in &riff_priority_zero_tags {
1520 let has_makernotes = tags
1521 .iter()
1522 .any(|t| t.name == *tag_name && t.group.family0 != "RIFF");
1523 if has_makernotes {
1524 tags.retain(|t| !(t.name == *tag_name && t.group.family0 == "RIFF"));
1525 }
1526 }
1527 }
1528
1529 if !self.options.duplicates {
1532 {
1537 const SPECIAL_WINS: &[(&str, &str)] = &[
1538 ("GoPro", "WhiteBalance"),
1539 ("GoPro", "Sharpness"),
1540 ("GoPro", "ExposureCompensation"),
1541 ("ID3", "Comment"),
1543 ("MinoltaRaw", "Contrast"),
1546 ("MinoltaRaw", "Saturation"),
1547 ("MinoltaRaw", "Sharpness"),
1548 ("MinoltaRaw", "ISOSetting"),
1549 ("Kodak", "FNumber"),
1551 ("Kodak", "ExposureTime"),
1552 ("Sigma", "X3FillLight"),
1554 ];
1555 for (grp, name) in SPECIAL_WINS {
1556 if tags
1557 .iter()
1558 .any(|t| t.name == *name && t.group.family1 == *grp)
1559 {
1560 tags.retain(|t| t.name != *name || t.group.family1 == *grp);
1561 }
1562 }
1563
1564 const QT_LAST_WINS: &[&str] = &[
1568 "HandlerType",
1569 "HandlerClass",
1570 "HandlerVendorID",
1571 "MediaTimeScale",
1572 "SourceImageWidth",
1573 "SourceImageHeight",
1574 "MediaDataOffset",
1577 "MediaDataSize",
1578 ];
1579 for name in QT_LAST_WINS {
1580 let last = tags
1581 .iter()
1582 .rposition(|t| t.name == *name && t.group.family1 == "QuickTime");
1583 if let Some(li) = last {
1584 let mut i = 0usize;
1585 tags.retain(|t| {
1586 let keep =
1587 !(t.name == *name && t.group.family1 == "QuickTime" && i != li);
1588 i += 1;
1589 keep
1590 });
1591 }
1592 }
1593 }
1594
1595 let mut best_priority: HashMap<String, i32> = HashMap::new();
1596 for tag in &tags {
1597 let entry = best_priority
1598 .entry(tag.name.clone())
1599 .or_insert(tag.priority);
1600 if tag.priority > *entry {
1601 *entry = tag.priority;
1602 }
1603 }
1604 tags.retain(|t| t.priority >= *best_priority.get(&t.name).unwrap_or(&0));
1605
1606 {
1610 let is_native_doc =
1611 |g1: &str| matches!(g1, "PDF" | "PostScript" | "DjVu" | "DjVu-Meta");
1612 let other_names: std::collections::HashSet<String> = tags
1613 .iter()
1614 .filter(|t| !is_native_doc(&t.group.family1) && !t.print_value.is_empty())
1615 .map(|t| t.name.clone())
1616 .collect();
1617 tags.retain(|t| {
1618 t.name == "Trapped"
1620 || !is_native_doc(&t.group.family1)
1621 || !other_names.contains(&t.name)
1622 });
1623 }
1624
1625 {
1628 const MWG_XMP_WINS: &[&str] = &["City", "DateCreated"];
1629 let xmp_has: std::collections::HashSet<String> = tags
1630 .iter()
1631 .filter(|t| t.group.family0 == "XMP" && !t.print_value.is_empty())
1632 .map(|t| t.name.clone())
1633 .collect();
1634 tags.retain(|t| {
1635 !(MWG_XMP_WINS.contains(&t.name.as_str())
1636 && t.group.family0 == "IPTC"
1637 && xmp_has.contains(&t.name))
1638 });
1639 }
1640
1641 let has_non_xmp: std::collections::HashSet<String> = tags
1645 .iter()
1646 .filter(|t| t.group.family0 != "XMP" && !t.print_value.is_empty())
1647 .map(|t| t.name.clone())
1648 .collect();
1649 tags.retain(|t| t.group.family0 != "XMP" || !has_non_xmp.contains(&t.name));
1650
1651 {
1657 let full_res_ifds: std::collections::HashSet<String> = tags
1658 .iter()
1659 .filter(|t| t.name == "SubfileType" && t.print_value == "Full-resolution image")
1660 .map(|t| t.group.family1.clone())
1661 .collect();
1662 if !full_res_ifds.is_empty() {
1663 const STRUCTURAL: &[&str] = &[
1666 "ImageWidth",
1667 "ImageHeight",
1668 "BitsPerSample",
1669 "Compression",
1670 "PhotometricInterpretation",
1671 "SamplesPerPixel",
1672 "StripOffsets",
1673 "StripByteCounts",
1674 "RowsPerStrip",
1675 "PlanarConfiguration",
1676 ];
1677 let has_full: std::collections::HashSet<String> = tags
1680 .iter()
1681 .filter(|t| {
1682 STRUCTURAL.contains(&t.name.as_str())
1683 && full_res_ifds.contains(&t.group.family1)
1684 })
1685 .map(|t| t.name.clone())
1686 .collect();
1687 tags.retain(|t| {
1688 !STRUCTURAL.contains(&t.name.as_str())
1689 || !has_full.contains(&t.name)
1690 || full_res_ifds.contains(&t.group.family1)
1691 });
1692 }
1693 }
1694
1695 {
1700 let last = tags.iter().rposition(|t| {
1701 t.name == "SubfileType"
1702 && (t.group.family1 == "IFD0" || t.group.family1.starts_with("SubIFD"))
1703 });
1704 if let Some(li) = last {
1705 let mut i = 0usize;
1706 tags.retain(|t| {
1707 let drop = t.name == "SubfileType"
1708 && (t.group.family1 == "IFD0" || t.group.family1.starts_with("SubIFD"))
1709 && i != li;
1710 i += 1;
1711 !drop
1712 });
1713 }
1714 }
1715
1716 {
1719 for dname in ["CreateDate", "ModifyDate"] {
1720 let has_qt = tags
1721 .iter()
1722 .any(|t| t.name == dname && t.group.family1 == "QuickTime");
1723 if has_qt {
1724 tags.retain(|t| t.name != dname || t.group.family1 == "QuickTime");
1725 }
1726 }
1727 }
1728
1729 {
1735 fn is_sub_document(g1: &str) -> bool {
1736 g1 == "IFD1"
1737 || g1.starts_with("SubIFD")
1738 || g1 == "PreviewIFD"
1739 || g1.starts_with("Doc")
1740 || (g1.starts_with("Track")
1741 && g1 != "Track1"
1742 && g1.len() > 5
1743 && g1.as_bytes()[5].is_ascii_digit())
1744 }
1747 fn is_first_wins_group(g1: &str) -> bool {
1752 g1 == "QuickTime"
1753 || g1 == "Track1"
1754 || g1 == "JP2"
1755 || g1 == "PhotoMechanic"
1756 || g1 == "DjVu"
1757 }
1758 use std::collections::HashMap as HM;
1759 let mut by_name: HM<&str, Vec<usize>> = HM::new();
1761 for (i, t) in tags.iter().enumerate() {
1762 by_name.entry(t.name.as_str()).or_default().push(i);
1763 }
1764 let mut drop: std::collections::HashSet<usize> = std::collections::HashSet::new();
1765 for idxs in by_name.values() {
1766 if idxs.len() < 2 {
1767 continue;
1768 }
1769 let g1 = &tags[idxs[0]].group.family1;
1770 let priority = tags[idxs[0]].priority;
1771 let uniform = idxs
1773 .iter()
1774 .all(|&i| &tags[i].group.family1 == g1 && tags[i].priority == priority);
1775 if !uniform || is_sub_document(g1) {
1776 continue;
1777 }
1778 if is_first_wins_group(g1) && priority == 0 {
1783 for &i in &idxs[1..] {
1785 drop.insert(i);
1786 }
1787 continue;
1788 }
1789 for &i in &idxs[..idxs.len() - 1] {
1791 drop.insert(i);
1792 }
1793 }
1794 if !drop.is_empty() {
1795 let mut i = 0usize;
1796 tags.retain(|_| {
1797 let keep = !drop.contains(&i);
1798 i += 1;
1799 keep
1800 });
1801 }
1802 }
1803 }
1804
1805 if !self.options.requested_tags.is_empty() {
1807 let requested: Vec<String> = self
1808 .options
1809 .requested_tags
1810 .iter()
1811 .map(|t| t.to_lowercase())
1812 .collect();
1813 tags.retain(|t| requested.contains(&t.name.to_lowercase()));
1814 }
1815
1816 Ok(tags)
1817 }
1818
1819 fn get_info(&self, tags: &[Tag]) -> ImageInfo {
1823 let mut info = ImageInfo::new();
1824 let mut seen: HashMap<String, (usize, i32)> = HashMap::new(); for tag in tags {
1827 let value = if self.options.print_conv {
1828 &tag.print_value
1829 } else {
1830 &tag.raw_value.to_display_string()
1831 };
1832
1833 let entry = seen.entry(tag.name.clone()).or_insert((0, i32::MIN));
1834 entry.0 += 1;
1835
1836 if entry.0 == 1 {
1837 entry.1 = tag.priority;
1838 info.insert(tag.name.clone(), value.clone());
1839 } else if tag.priority > entry.1 {
1840 entry.1 = tag.priority;
1842 info.insert(tag.name.clone(), value.clone());
1843 } else if self.options.duplicates {
1844 let key = format!("{} [{}:{}]", tag.name, tag.group.family0, tag.group.family1);
1845 info.insert(key, value.clone());
1846 }
1847 }
1848
1849 info
1850 }
1851
1852 fn detect_file_type(&self, data: &[u8], path: &Path) -> Result<FileType> {
1854 let header_len = data.len().min(256);
1856 if let Some(ft) = file_type::detect_from_magic(&data[..header_len]) {
1857 if ft == FileType::Ico {
1859 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1860 if ext.eq_ignore_ascii_case("dfont") {
1861 return Ok(FileType::Dfont);
1862 }
1863 }
1864 }
1865 if ft == FileType::Jpeg {
1867 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1868 if ext.eq_ignore_ascii_case("jps") {
1869 return Ok(FileType::Jps);
1870 }
1871 }
1872 }
1873 if ft == FileType::Plist {
1875 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1876 if ext.eq_ignore_ascii_case("aae") {
1877 return Ok(FileType::Aae);
1878 }
1879 }
1880 }
1881 if ft == FileType::Xmp || ft == FileType::Xml {
1883 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1884 if ext.eq_ignore_ascii_case("plist") {
1885 return Ok(FileType::Plist);
1886 }
1887 if ext.eq_ignore_ascii_case("aae") {
1888 return Ok(FileType::Aae);
1889 }
1890 }
1891 }
1892 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1894 if ext.eq_ignore_ascii_case("pcd")
1895 && data.len() >= 2056
1896 && &data[2048..2055] == b"PCD_IPI"
1897 {
1898 return Ok(FileType::PhotoCd);
1899 }
1900 }
1901 if ft == FileType::Mp3 {
1903 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1904 if ext.eq_ignore_ascii_case("mpc") {
1905 return Ok(FileType::Mpc);
1906 }
1907 if ext.eq_ignore_ascii_case("ape") {
1908 return Ok(FileType::Ape);
1909 }
1910 if ext.eq_ignore_ascii_case("wv") {
1911 return Ok(FileType::WavPack);
1912 }
1913 }
1914 }
1915 if ft == FileType::Asf {
1917 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1918 if ext.eq_ignore_ascii_case("wmv") {
1919 return Ok(FileType::Wmv);
1920 }
1921 if ext.eq_ignore_ascii_case("wma") {
1922 return Ok(FileType::Wma);
1923 }
1924 }
1925 }
1926 if ft == FileType::Ogg {
1928 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1929 if ext.eq_ignore_ascii_case("opus") {
1930 return Ok(FileType::Opus);
1931 }
1932 }
1933 }
1934 if ft == FileType::Tiff {
1937 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1938 if let Some(ext_ft) = file_type::detect_from_extension(ext) {
1939 if ext_ft != FileType::Tiff && is_tiff_based(ext_ft) {
1940 return Ok(ext_ft);
1941 }
1942 }
1943 }
1944 }
1945 if ft == FileType::Zip {
1947 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1949 if ext.eq_ignore_ascii_case("eip") {
1950 return Ok(FileType::Eip);
1951 }
1952 }
1953 if let Some(iw) = detect_iwork_type(data, path) {
1956 return Ok(iw);
1957 }
1958 if let Some(od_type) = detect_opendocument_type(data) {
1959 return Ok(od_type);
1960 }
1961 }
1962 if ft == FileType::Doc {
1965 if let Some(ole) = detect_ole2_type(data) {
1966 return Ok(ole);
1967 }
1968 }
1969 return Ok(ft);
1970 }
1971
1972 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1974 if let Some(ft) = file_type::detect_from_extension(ext) {
1975 return Ok(ft);
1976 }
1977 }
1978
1979 let ext_str = path
1980 .extension()
1981 .and_then(|e| e.to_str())
1982 .unwrap_or("unknown");
1983 Err(Error::UnsupportedFileType(ext_str.to_string()))
1984 }
1985
1986 fn process_file(&self, data: &[u8], file_type: FileType) -> Result<Vec<Tag>> {
1988 match file_type {
1989 FileType::Jpeg | FileType::Jps => formats::jpeg::read_jpeg(data),
1990 FileType::Png | FileType::Mng => formats::png::read_png(data),
1991 FileType::Tiff
1993 | FileType::Btf
1994 | FileType::Dng
1995 | FileType::Cr2
1996 | FileType::Nef
1997 | FileType::Arw
1998 | FileType::Sr2
1999 | FileType::Orf
2000 | FileType::Pef
2001 | FileType::Erf
2002 | FileType::Fff
2003 | FileType::Rwl
2004 | FileType::Mef
2005 | FileType::Srw
2006 | FileType::Gpr
2007 | FileType::Arq
2008 | FileType::ThreeFR
2009 | FileType::Dcr
2010 | FileType::Rw2
2011 | FileType::Srf => formats::tiff::read_tiff(data),
2012 FileType::Iiq => formats::iiq::read_iiq(data),
2014 FileType::Gif => formats::gif::read_gif(data),
2016 FileType::Bmp => formats::bmp::read_bmp(data),
2017 FileType::WebP | FileType::Avi | FileType::Wav => formats::riff::read_riff(data),
2018 FileType::Psd => formats::psd::read_psd(data),
2019 FileType::Mp3 => formats::id3::read_mp3(data),
2021 FileType::Flac => formats::flac::read_flac(data),
2022 FileType::Ogg | FileType::Opus => formats::ogg::read_ogg(data),
2023 FileType::Aiff => formats::aiff::read_aiff(data),
2024 FileType::Mp4
2026 | FileType::QuickTime
2027 | FileType::M4a
2028 | FileType::ThreeGP
2029 | FileType::Heif
2030 | FileType::Avif
2031 | FileType::Cr3
2032 | FileType::Crm
2033 | FileType::F4v
2034 | FileType::Mqv
2035 | FileType::Lrv => {
2036 formats::quicktime::read_quicktime_with_ee(data, self.options.extract_embedded)
2037 }
2038 FileType::Mkv | FileType::WebM => formats::matroska::read_matroska(data),
2039 FileType::Asf | FileType::Wmv | FileType::Wma => formats::asf::read_asf(data),
2040 FileType::Wtv => formats::wtv::read_wtv(data),
2041 FileType::Crw => formats::canon_raw::read_crw(data),
2043 FileType::Raf => formats::raf::read_raf(data),
2044 FileType::Mrw => formats::mrw::read_mrw(data),
2045 FileType::Mrc => formats::mrc::read_mrc(data),
2046 FileType::Jp2 => formats::jp2::read_jp2(data),
2048 FileType::J2c => formats::jp2::read_j2c(data),
2049 FileType::Jxl => formats::jp2::read_jxl(data),
2050 FileType::Ico => formats::ico::read_ico(data),
2051 FileType::Icc => formats::icc::read_icc(data),
2052 FileType::Pdf => formats::pdf::read_pdf(data),
2054 FileType::PostScript => {
2055 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
2057 formats::font::read_pfa(data)
2058 .or_else(|_| formats::postscript::read_postscript(data))
2059 } else {
2060 formats::postscript::read_postscript(data)
2061 }
2062 }
2063 FileType::Eip => formats::capture_one::read_eip(data),
2064 FileType::Zip
2065 | FileType::Docx
2066 | FileType::Xlsx
2067 | FileType::Pptx
2068 | FileType::Doc
2069 | FileType::Xls
2070 | FileType::Ppt
2071 | FileType::Numbers
2072 | FileType::Pages
2073 | FileType::Key => formats::zip::read_zip(data),
2074 FileType::Rtf => formats::rtf::read_rtf(data),
2075 FileType::InDesign => formats::indesign::read_indesign(data),
2076 FileType::Pcap => formats::pcap::read_pcap(data),
2077 FileType::Pcapng => formats::pcap::read_pcapng(data),
2078 FileType::Vrd => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
2080 FileType::Dr4 => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
2081 FileType::Xmp => formats::xmp_file::read_xmp(data),
2083 FileType::Svg => formats::svg::read_svg(data),
2084 FileType::Html => {
2085 let is_svg = data.windows(4).take(512).any(|w| w == b"<svg");
2087 if is_svg {
2088 formats::svg::read_svg(data)
2089 } else {
2090 formats::html::read_html(data)
2091 }
2092 }
2093 FileType::Exe => formats::exe::read_exe(data),
2094 FileType::Font => {
2095 if data.starts_with(b"StartFontMetrics") {
2097 return formats::font::read_afm(data);
2098 }
2099 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
2101 return formats::font::read_pfa(data).or_else(|_| Ok(Vec::new()));
2102 }
2103 if data.len() >= 2 && data[0] == 0x80 && (data[1] == 0x01 || data[1] == 0x02) {
2105 return formats::font::read_pfb(data).or_else(|_| Ok(Vec::new()));
2106 }
2107 formats::font::read_font(data)
2108 }
2109 FileType::WavPack | FileType::Dsf => formats::id3::read_mp3(data),
2111 FileType::Ape => formats::ape::read_ape(data),
2112 FileType::Mpc => formats::ape::read_mpc(data),
2113 FileType::Aac => formats::aac::read_aac(data),
2114 FileType::RealAudio => {
2115 formats::real_audio::read_real_audio(data).or_else(|_| Ok(Vec::new()))
2116 }
2117 FileType::RealMedia => {
2118 formats::real_media::read_real_media(data).or_else(|_| Ok(Vec::new()))
2119 }
2120 FileType::Czi => formats::czi::read_czi(data).or_else(|_| Ok(Vec::new())),
2122 FileType::PhotoCd => formats::photo_cd::read_photo_cd(data).or_else(|_| Ok(Vec::new())),
2123 FileType::Dicom => formats::dicom::read_dicom(data),
2124 FileType::Fits => formats::fits::read_fits(data),
2125 FileType::Flv => formats::flv::read_flv(data),
2126 FileType::Mxf => formats::mxf::read_mxf(data).or_else(|_| Ok(Vec::new())),
2127 FileType::Swf => formats::swf::read_swf(data),
2128 FileType::Hdr => formats::hdr::read_hdr(data),
2129 FileType::DjVu => formats::djvu::read_djvu(data),
2130 FileType::Xcf => formats::gimp::read_xcf(data),
2131 FileType::Mie => formats::mie::read_mie(data),
2132 FileType::Lfp => formats::lytro::read_lfp(data),
2133 FileType::Fpf => formats::flir_fpf::read_fpf(data),
2135 FileType::Flif => formats::flif::read_flif(data),
2136 FileType::Bpg => formats::bpg::read_bpg(data),
2137 FileType::Pcx => formats::pcx::read_pcx(data),
2138 FileType::Pict => formats::pict::read_pict(data),
2139 FileType::Mpeg => formats::mpeg::read_mpeg(data),
2140 FileType::M2ts => formats::m2ts::read_m2ts(data, self.options.extract_embedded),
2141 FileType::Gzip => formats::gzip::read_gzip(data),
2142 FileType::Rar => formats::rar::read_rar(data),
2143 FileType::SevenZ => formats::sevenz::read_7z(data),
2144 FileType::Dss => formats::dss::read_dss(data),
2145 FileType::Moi => formats::moi::read_moi(data),
2146 FileType::MacOs => formats::macos::read_macos(data),
2147 FileType::Json => formats::json_format::read_json(data),
2148 FileType::Pgf => formats::pgf::read_pgf(data),
2150 FileType::Xisf => formats::xisf::read_xisf(data),
2151 FileType::Torrent => formats::torrent::read_torrent(data),
2152 FileType::Mobi => formats::palm::read_palm(data),
2153 FileType::Psp => formats::psp::read_psp(data),
2154 FileType::SonyPmp => formats::sony_pmp::read_sony_pmp(data),
2155 FileType::Audible => formats::audible::read_audible(data),
2156 FileType::Exr => formats::openexr::read_openexr(data),
2157 FileType::Plist => {
2159 if data.starts_with(b"bplist") {
2160 formats::plist::read_binary_plist_tags(data)
2161 } else {
2162 formats::plist::read_xml_plist(data)
2163 }
2164 }
2165 FileType::Aae => {
2166 if data.starts_with(b"bplist") {
2167 formats::plist::read_binary_plist_tags(data)
2168 } else {
2169 formats::plist::read_aae_plist(data)
2170 }
2171 }
2172 FileType::KyoceraRaw => formats::kyocera_raw::read_kyocera_raw(data),
2173 FileType::PortableFloatMap => formats::pfm::read_pfm(data),
2174 FileType::Ods
2175 | FileType::Odt
2176 | FileType::Odp
2177 | FileType::Odg
2178 | FileType::Odf
2179 | FileType::Odb
2180 | FileType::Odi
2181 | FileType::Odc => formats::zip::read_zip(data),
2182 FileType::Lif => formats::lif::read_lif(data),
2183 FileType::Rwz => formats::rawzor::read_rawzor(data),
2184 FileType::Jxr => formats::jxr::read_jxr(data),
2185 FileType::Miff => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
2186 FileType::Tnef => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
2187 FileType::Wpg => formats::wpg::read_wpg(data).or_else(|_| Ok(Vec::new())),
2188 FileType::Dv => {
2189 formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new()))
2190 }
2191 FileType::Itc => formats::itc::read_itc(data).or_else(|_| Ok(Vec::new())),
2192 FileType::Iso => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
2193 FileType::Afm => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
2194 FileType::Pfa => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
2195 FileType::Pfb => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
2196 FileType::Dfont => formats::font::read_font(data).or_else(|_| Ok(Vec::new())),
2197 FileType::Xml | FileType::Inx => {
2198 formats::xmp_file::read_xmp(data).or_else(|_| Ok(Vec::new()))
2199 }
2200 FileType::Eps => formats::postscript::read_postscript(data),
2201 _ => Err(Error::UnsupportedFileType(format!("{}", file_type))),
2202 }
2203 }
2204
2205 fn process_by_extension(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
2207 let ext = path
2208 .extension()
2209 .and_then(|e| e.to_str())
2210 .unwrap_or("")
2211 .to_ascii_lowercase();
2212
2213 match ext.as_str() {
2214 "ppm" | "pgm" | "pbm" => formats::ppm::read_ppm(data),
2215 "pfm" => {
2216 if data.len() >= 3 && data[0] == b'P' && (data[1] == b'f' || data[1] == b'F') {
2218 formats::ppm::read_ppm(data)
2219 } else {
2220 Ok(Vec::new()) }
2222 }
2223 "json" => formats::json_format::read_json(data),
2224 "svg" => formats::svg::read_svg(data),
2225 "ram" => formats::ram::read_ram(data).or_else(|_| Ok(Vec::new())),
2226 "txt" | "log" | "igc" => Ok(compute_text_tags(data, false)),
2227 "csv" => Ok(compute_text_tags(data, true)),
2228 "url" => formats::lnk::read_url(data).or_else(|_| Ok(Vec::new())),
2229 "lnk" => formats::lnk::read_lnk(data).or_else(|_| Ok(Vec::new())),
2230 "gpx" | "kml" | "xml" | "inx" => formats::xmp_file::read_xmp(data),
2231 "plist" => {
2232 if data.starts_with(b"bplist") {
2233 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
2234 } else {
2235 formats::plist::read_xml_plist(data).or_else(|_| Ok(Vec::new()))
2236 }
2237 }
2238 "aae" => {
2239 if data.starts_with(b"bplist") {
2240 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
2241 } else {
2242 formats::plist::read_aae_plist(data).or_else(|_| Ok(Vec::new()))
2243 }
2244 }
2245 "vcf" | "ics" | "vcard" => {
2246 let s = crate::encoding::decode_utf8_or_latin1(&data[..data.len().min(100)]);
2247 if s.contains("BEGIN:VCALENDAR") {
2248 formats::vcard::read_ics(data).or_else(|_| Ok(Vec::new()))
2249 } else {
2250 formats::vcard::read_vcf(data).or_else(|_| Ok(Vec::new()))
2251 }
2252 }
2253 "xcf" => Ok(Vec::new()), "vrd" => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
2255 "dr4" => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
2256 "indd" | "indt" => Ok(Vec::new()), "x3f" => formats::sigma_raw::read_x3f(data).or_else(|_| Ok(Vec::new())),
2258 "mie" => Ok(Vec::new()), "exr" => Ok(Vec::new()), "wpg" => formats::wpg::read_wpg(data).or_else(|_| Ok(Vec::new())),
2261 "moi" => formats::moi::read_moi(data).or_else(|_| Ok(Vec::new())),
2262 "macos" => formats::macos::read_macos(data).or_else(|_| Ok(Vec::new())),
2263 "dpx" => formats::dpx::read_dpx(data).or_else(|_| Ok(Vec::new())),
2264 "r3d" => formats::red::read_r3d(data).or_else(|_| Ok(Vec::new())),
2265 "tnef" => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
2266 "ppt" | "fpx" => formats::flashpix::read_fpx(data).or_else(|_| Ok(Vec::new())),
2267 "fpf" => formats::flir_fpf::read_fpf(data).or_else(|_| Ok(Vec::new())),
2268 "itc" => formats::itc::read_itc(data).or_else(|_| Ok(Vec::new())),
2269 "mpg" | "mpeg" | "m1v" | "m2v" | "mpv" => {
2270 formats::mpeg::read_mpeg(data).or_else(|_| Ok(Vec::new()))
2271 }
2272 "dv" => formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new())),
2273 "czi" => formats::czi::read_czi(data).or_else(|_| Ok(Vec::new())),
2274 "miff" => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
2275 "lfp" | "mrc" | "dss" | "mobi" | "psp" | "pgf" | "raw" | "pmp" | "torrent" | "xisf"
2276 | "mxf" | "dfont" => Ok(Vec::new()),
2277 "iso" => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
2278 "afm" => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
2279 "pfa" => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
2280 "pfb" => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
2281 _ => Err(Error::UnsupportedFileType(ext)),
2282 }
2283 }
2284}
2285
2286impl Default for ExifTool {
2287 fn default() -> Self {
2288 Self::new()
2289 }
2290}
2291
2292fn exe_subtype(d: &[u8]) -> Option<(&'static str, &'static str, &'static str)> {
2297 const MIME: &str = "application/octet-stream";
2298 if d.len() < 8 {
2299 return None;
2300 }
2301 if &d[0..4] == b"\x7fELF" && d.len() >= 18 {
2303 let le = d[5] == 1;
2304 let e_type = if le {
2305 u16::from_le_bytes([d[16], d[17]])
2306 } else {
2307 u16::from_be_bytes([d[16], d[17]])
2308 };
2309 return Some(match e_type {
2310 1 => ("ELF relocatable", MIME, "o"),
2311 2 => ("ELF executable", MIME, ""),
2312 3 => ("ELF shared library", MIME, "so"),
2313 4 => ("ELF core file", MIME, ""),
2314 _ => ("ELF", MIME, ""),
2315 });
2316 }
2317 let magic_be = u32::from_be_bytes([d[0], d[1], d[2], d[3]]);
2319 let macho = matches!(magic_be, 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE);
2320 if macho && d.len() >= 16 {
2321 let le = matches!(magic_be, 0xCEFAEDFE | 0xCFFAEDFE);
2322 let filetype = if le {
2323 u32::from_le_bytes([d[12], d[13], d[14], d[15]])
2324 } else {
2325 u32::from_be_bytes([d[12], d[13], d[14], d[15]])
2326 };
2327 return Some(match filetype {
2328 1 => ("Mach-O object file", MIME, "o"),
2329 6 => ("Mach-O dynamic link library", MIME, "dylib"),
2330 8 => ("Mach-O dynamic bound bundle", MIME, "dylib"),
2331 9 => ("Mach-O dynamic link library stub", MIME, "dylib"),
2332 _ => ("Mach-O executable", MIME, ""),
2333 });
2334 }
2335 if matches!(magic_be, 0xCAFEBABE | 0xBEBAFECA) {
2337 return Some(("Mach-O fat binary executable", MIME, ""));
2338 }
2339 if d.starts_with(b"!<arch>\n") {
2341 let is_macho = d.windows(4).take(4096).any(|w| {
2342 let m = u32::from_be_bytes([w[0], w[1], w[2], w[3]]);
2343 matches!(
2344 m,
2345 0xFEEDFACE | 0xFEEDFACF | 0xCEFAEDFE | 0xCFFAEDFE | 0xCAFEBABE
2346 )
2347 });
2348 return Some(if is_macho {
2349 ("Mach-O static library", MIME, "a")
2350 } else {
2351 ("Static library", MIME, "a")
2352 });
2353 }
2354 if &d[0..2] == b"MZ" && d.len() >= 0x40 {
2356 let pe_off = u32::from_le_bytes([d[0x3c], d[0x3d], d[0x3e], d[0x3f]]) as usize;
2357 if pe_off + 6 <= d.len() && &d[pe_off..pe_off + 4] == b"PE\0\0" {
2358 let machine = u16::from_le_bytes([d[pe_off + 4], d[pe_off + 5]]);
2359 return Some(match machine {
2360 0x8664 | 0xAA64 => ("Win64 EXE", MIME, "exe"),
2361 _ => ("Win32 EXE", MIME, "exe"),
2362 });
2363 }
2364 }
2365 None
2366}
2367
2368fn is_tiff_based(ft: FileType) -> bool {
2370 matches!(
2371 ft,
2372 FileType::Dng
2373 | FileType::Cr2
2374 | FileType::Nef
2375 | FileType::Arw
2376 | FileType::Sr2
2377 | FileType::Orf
2378 | FileType::Pef
2379 | FileType::Erf
2380 | FileType::Rwl
2381 | FileType::Mef
2382 | FileType::Srw
2383 | FileType::Gpr
2384 | FileType::Arq
2385 | FileType::ThreeFR
2386 | FileType::Dcr
2387 | FileType::Rw2
2388 | FileType::Srf
2389 | FileType::Iiq
2390 | FileType::Btf
2391 )
2392}
2393
2394fn detect_ole2_type(data: &[u8]) -> Option<FileType> {
2397 fn has_utf16(data: &[u8], name: &str) -> bool {
2398 let needle: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
2399 data.windows(needle.len()).any(|w| w == needle.as_slice())
2400 }
2401 if has_utf16(data, "PowerPoint Document") {
2402 Some(FileType::Ppt)
2403 } else if has_utf16(data, "Workbook") || has_utf16(data, "Book") {
2404 Some(FileType::Xls)
2405 } else {
2406 None
2407 }
2408}
2409
2410fn detect_iwork_type(data: &[u8], path: &Path) -> Option<FileType> {
2414 const MARKERS: &[&[u8]] = &[
2415 b"index.xml",
2416 b"index.apxl",
2417 b"QuickLook/Thumbnail.jpg",
2418 b"Index/Document.iwa",
2419 b"Index/Slide.iwa",
2420 b"Index/Tables/DataList.iwa",
2421 ];
2422 let has_marker = MARKERS
2423 .iter()
2424 .any(|m| data.windows(m.len()).any(|w| w == *m));
2425 if !has_marker {
2426 return None;
2427 }
2428 let ext = path
2429 .extension()
2430 .and_then(|e| e.to_str())
2431 .unwrap_or("")
2432 .to_ascii_lowercase();
2433 match ext.as_str() {
2434 "numbers" | "nmbtemplate" => Some(FileType::Numbers),
2435 "pages" => Some(FileType::Pages),
2436 "key" | "kth" => Some(FileType::Key),
2437 _ => None,
2438 }
2439}
2440
2441fn refine_filetype_by_content(file_type: FileType, data: &[u8]) -> Option<(String, String)> {
2444 match file_type {
2445 FileType::PortableFloatMap if data.len() >= 2 && data[0] == 0x00 && data[1] <= 0x02 => {
2447 Some(("PFM".into(), "application/x-font-type1".into()))
2448 }
2449 FileType::Plist if !data.starts_with(b"bplist") => {
2451 Some(("PLIST".into(), "application/xml".into()))
2452 }
2453 FileType::Jxl if data.starts_with(&[0xFF, 0x0A]) => {
2455 Some(("JXL Codestream".into(), file_type.mime_type().to_string()))
2456 }
2457 FileType::WebP if data.len() >= 16 && &data[12..16] == b"VP8X" => {
2459 Some(("Extended WEBP".into(), file_type.mime_type().to_string()))
2460 }
2461 FileType::DjVu if data.len() >= 16 && &data[12..16] == b"DJVM" => Some((
2463 "DJVU (multi-page)".into(),
2464 file_type.mime_type().to_string(),
2465 )),
2466 _ => None,
2467 }
2468}
2469
2470fn detect_opendocument_type(data: &[u8]) -> Option<FileType> {
2471 if data.len() < 30 || data[0..4] != [0x50, 0x4B, 0x03, 0x04] {
2473 return None;
2474 }
2475 let compression = u16::from_le_bytes([data[8], data[9]]);
2476 let compressed_size = u32::from_le_bytes([data[18], data[19], data[20], data[21]]) as usize;
2477 let name_len = u16::from_le_bytes([data[26], data[27]]) as usize;
2478 let extra_len = u16::from_le_bytes([data[28], data[29]]) as usize;
2479 let name_start = 30;
2480 if name_start + name_len > data.len() {
2481 return None;
2482 }
2483 let filename = std::str::from_utf8(&data[name_start..name_start + name_len]).unwrap_or("");
2484 if filename != "mimetype" || compression != 0 {
2485 return None;
2486 }
2487 let content_start = name_start + name_len + extra_len;
2488 let content_end = (content_start + compressed_size).min(data.len());
2489 if content_start >= content_end {
2490 return None;
2491 }
2492 let mime = std::str::from_utf8(&data[content_start..content_end])
2493 .unwrap_or("")
2494 .trim();
2495 match mime {
2496 "application/vnd.oasis.opendocument.spreadsheet" => Some(FileType::Ods),
2497 "application/vnd.oasis.opendocument.text" => Some(FileType::Odt),
2498 "application/vnd.oasis.opendocument.presentation" => Some(FileType::Odp),
2499 "application/vnd.oasis.opendocument.graphics" => Some(FileType::Odg),
2500 "application/vnd.oasis.opendocument.formula" => Some(FileType::Odf),
2501 "application/vnd.oasis.opendocument.database" => Some(FileType::Odb),
2502 "application/vnd.oasis.opendocument.image" => Some(FileType::Odi),
2503 "application/vnd.oasis.opendocument.chart" => Some(FileType::Odc),
2504 _ => None,
2505 }
2506}
2507
2508pub fn get_file_type<P: AsRef<Path>>(path: P) -> Result<FileType> {
2510 let path = path.as_ref();
2511 let mut file = fs::File::open(path).map_err(Error::Io)?;
2512 let mut header = [0u8; 256];
2513 use std::io::Read;
2514 let n = file.read(&mut header).map_err(Error::Io)?;
2515
2516 if let Some(ft) = file_type::detect_from_magic(&header[..n]) {
2517 return Ok(ft);
2518 }
2519
2520 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2521 if let Some(ft) = file_type::detect_from_extension(ext) {
2522 return Ok(ft);
2523 }
2524 }
2525
2526 Err(Error::UnsupportedFileType("unknown".into()))
2527}
2528
2529enum ExifIfdGroup {
2531 Ifd0,
2532 ExifIfd,
2533 Gps,
2534}
2535
2536fn classify_exif_tag(tag_id: u16) -> ExifIfdGroup {
2538 match tag_id {
2539 0x829A..=0x829D | 0x8822..=0x8827 | 0x8830 | 0x9000..=0x9292 | 0xA000..=0xA435 => {
2541 ExifIfdGroup::ExifIfd
2542 }
2543 0x0000..=0x001F if tag_id <= 0x001F => ExifIfdGroup::Gps,
2545 _ => ExifIfdGroup::Ifd0,
2547 }
2548}
2549
2550fn extract_existing_exif_entries(
2552 jpeg_data: &[u8],
2553 target_bo: ByteOrderMark,
2554) -> Vec<exif_writer::IfdEntry> {
2555 let mut entries = Vec::new();
2556
2557 let mut pos = 2; while pos + 4 <= jpeg_data.len() {
2560 if jpeg_data[pos] != 0xFF {
2561 pos += 1;
2562 continue;
2563 }
2564 let marker = jpeg_data[pos + 1];
2565 pos += 2;
2566
2567 if marker == 0xDA || marker == 0xD9 {
2568 break; }
2570 if marker == 0xFF || marker == 0x00 || marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
2571 continue;
2572 }
2573
2574 if pos + 2 > jpeg_data.len() {
2575 break;
2576 }
2577 let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
2578 if seg_len < 2 || pos + seg_len > jpeg_data.len() {
2579 break;
2580 }
2581
2582 let seg_data = &jpeg_data[pos + 2..pos + seg_len];
2583
2584 if marker == 0xE1 && seg_data.len() > 14 && seg_data.starts_with(b"Exif\0\0") {
2586 let tiff_data = &seg_data[6..];
2587 extract_ifd_entries(tiff_data, target_bo, &mut entries);
2588 break;
2589 }
2590
2591 pos += seg_len;
2592 }
2593
2594 entries
2595}
2596
2597fn extract_ifd_entries(
2599 tiff_data: &[u8],
2600 target_bo: ByteOrderMark,
2601 entries: &mut Vec<exif_writer::IfdEntry>,
2602) {
2603 use crate::metadata::exif::parse_tiff_header;
2604
2605 let header = match parse_tiff_header(tiff_data) {
2606 Ok(h) => h,
2607 Err(_) => return,
2608 };
2609
2610 let src_bo = header.byte_order;
2611
2612 read_ifd_for_merge(
2614 tiff_data,
2615 header.ifd0_offset as usize,
2616 src_bo,
2617 target_bo,
2618 entries,
2619 );
2620
2621 let ifd0_offset = header.ifd0_offset as usize;
2623 if ifd0_offset + 2 > tiff_data.len() {
2624 return;
2625 }
2626 let count = read_u16_bo(tiff_data, ifd0_offset, src_bo) as usize;
2627 for i in 0..count {
2628 let eoff = ifd0_offset + 2 + i * 12;
2629 if eoff + 12 > tiff_data.len() {
2630 break;
2631 }
2632 let tag = read_u16_bo(tiff_data, eoff, src_bo);
2633 let value_off = read_u32_bo(tiff_data, eoff + 8, src_bo) as usize;
2634
2635 match tag {
2636 0x8769 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
2637 0x8825 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
2638 _ => {}
2639 }
2640 }
2641}
2642
2643fn read_ifd_for_merge(
2645 data: &[u8],
2646 offset: usize,
2647 src_bo: ByteOrderMark,
2648 target_bo: ByteOrderMark,
2649 entries: &mut Vec<exif_writer::IfdEntry>,
2650) {
2651 if offset + 2 > data.len() {
2652 return;
2653 }
2654 let count = read_u16_bo(data, offset, src_bo) as usize;
2655
2656 for i in 0..count {
2657 let eoff = offset + 2 + i * 12;
2658 if eoff + 12 > data.len() {
2659 break;
2660 }
2661
2662 let tag = read_u16_bo(data, eoff, src_bo);
2663 let dtype = read_u16_bo(data, eoff + 2, src_bo);
2664 let count_val = read_u32_bo(data, eoff + 4, src_bo);
2665
2666 if tag == 0x8769 || tag == 0x8825 || tag == 0xA005 || tag == 0x927C {
2668 continue;
2669 }
2670
2671 let type_size = match dtype {
2672 1 | 2 | 6 | 7 => 1usize,
2673 3 | 8 => 2,
2674 4 | 9 | 11 | 13 => 4,
2675 5 | 10 | 12 => 8,
2676 _ => continue,
2677 };
2678
2679 let total_size = type_size * count_val as usize;
2680 let raw_data = if total_size <= 4 {
2681 data[eoff + 8..eoff + 12].to_vec()
2682 } else {
2683 let voff = read_u32_bo(data, eoff + 8, src_bo) as usize;
2684 if voff + total_size > data.len() {
2685 continue;
2686 }
2687 data[voff..voff + total_size].to_vec()
2688 };
2689
2690 let final_data = if src_bo != target_bo && type_size > 1 {
2692 reencode_bytes(&raw_data, dtype, count_val as usize, src_bo, target_bo)
2693 } else {
2694 raw_data[..total_size].to_vec()
2695 };
2696
2697 let format = match dtype {
2698 1 => exif_writer::ExifFormat::Byte,
2699 2 => exif_writer::ExifFormat::Ascii,
2700 3 => exif_writer::ExifFormat::Short,
2701 4 => exif_writer::ExifFormat::Long,
2702 5 => exif_writer::ExifFormat::Rational,
2703 6 => exif_writer::ExifFormat::SByte,
2704 7 => exif_writer::ExifFormat::Undefined,
2705 8 => exif_writer::ExifFormat::SShort,
2706 9 => exif_writer::ExifFormat::SLong,
2707 10 => exif_writer::ExifFormat::SRational,
2708 11 => exif_writer::ExifFormat::Float,
2709 12 => exif_writer::ExifFormat::Double,
2710 _ => continue,
2711 };
2712
2713 entries.push(exif_writer::IfdEntry {
2714 tag,
2715 format,
2716 data: final_data,
2717 });
2718 }
2719}
2720
2721fn reencode_bytes(
2723 data: &[u8],
2724 dtype: u16,
2725 count: usize,
2726 src_bo: ByteOrderMark,
2727 dst_bo: ByteOrderMark,
2728) -> Vec<u8> {
2729 let mut out = Vec::with_capacity(data.len());
2730 match dtype {
2731 3 | 8 => {
2732 for i in 0..count {
2734 let v = read_u16_bo(data, i * 2, src_bo);
2735 match dst_bo {
2736 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
2737 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
2738 }
2739 }
2740 }
2741 4 | 9 | 11 | 13 => {
2742 for i in 0..count {
2744 let v = read_u32_bo(data, i * 4, src_bo);
2745 match dst_bo {
2746 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
2747 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
2748 }
2749 }
2750 }
2751 5 | 10 => {
2752 for i in 0..count {
2754 let n = read_u32_bo(data, i * 8, src_bo);
2755 let d = read_u32_bo(data, i * 8 + 4, src_bo);
2756 match dst_bo {
2757 ByteOrderMark::LittleEndian => {
2758 out.extend_from_slice(&n.to_le_bytes());
2759 out.extend_from_slice(&d.to_le_bytes());
2760 }
2761 ByteOrderMark::BigEndian => {
2762 out.extend_from_slice(&n.to_be_bytes());
2763 out.extend_from_slice(&d.to_be_bytes());
2764 }
2765 }
2766 }
2767 }
2768 12 => {
2769 for i in 0..count {
2771 let mut bytes = [0u8; 8];
2772 bytes.copy_from_slice(&data[i * 8..i * 8 + 8]);
2773 if src_bo != dst_bo {
2774 bytes.reverse();
2775 }
2776 out.extend_from_slice(&bytes);
2777 }
2778 }
2779 _ => out.extend_from_slice(data),
2780 }
2781 out
2782}
2783
2784fn read_u16_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
2785 if offset + 2 > data.len() {
2786 return 0;
2787 }
2788 match bo {
2789 ByteOrderMark::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
2790 ByteOrderMark::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
2791 }
2792}
2793
2794fn read_u32_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
2795 if offset + 4 > data.len() {
2796 return 0;
2797 }
2798 match bo {
2799 ByteOrderMark::LittleEndian => u32::from_le_bytes([
2800 data[offset],
2801 data[offset + 1],
2802 data[offset + 2],
2803 data[offset + 3],
2804 ]),
2805 ByteOrderMark::BigEndian => u32::from_be_bytes([
2806 data[offset],
2807 data[offset + 1],
2808 data[offset + 2],
2809 data[offset + 3],
2810 ]),
2811 }
2812}
2813
2814fn tag_name_to_id(name: &str) -> Option<u16> {
2816 encode_exif_tag(name, "", "", ByteOrderMark::BigEndian).map(|(id, _, _)| id)
2817}
2818
2819fn value_to_filename(value: &str) -> String {
2821 value
2822 .chars()
2823 .map(|c| match c {
2824 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
2825 c if c.is_control() => '_',
2826 c => c,
2827 })
2828 .collect::<String>()
2829 .trim()
2830 .to_string()
2831}
2832
2833pub fn parse_date_shift(shift: &str) -> Option<(i32, u32, u32, u32)> {
2836 let (sign, rest) = if let Some(stripped) = shift.strip_prefix('-') {
2837 (-1, stripped)
2838 } else if let Some(stripped) = shift.strip_prefix('+') {
2839 (1, stripped)
2840 } else {
2841 (1, shift)
2842 };
2843
2844 let parts: Vec<&str> = rest.split(':').collect();
2845 match parts.len() {
2846 1 => {
2847 let h: u32 = parts[0].parse().ok()?;
2848 Some((sign, h, 0, 0))
2849 }
2850 2 => {
2851 let h: u32 = parts[0].parse().ok()?;
2852 let m: u32 = parts[1].parse().ok()?;
2853 Some((sign, h, m, 0))
2854 }
2855 3 => {
2856 let h: u32 = parts[0].parse().ok()?;
2857 let m: u32 = parts[1].parse().ok()?;
2858 let s: u32 = parts[2].parse().ok()?;
2859 Some((sign, h, m, s))
2860 }
2861 _ => None,
2862 }
2863}
2864
2865pub fn shift_datetime(datetime: &str, shift: &str) -> Option<String> {
2868 let (sign, hours, minutes, seconds) = parse_date_shift(shift)?;
2869
2870 if datetime.len() < 19 {
2872 return None;
2873 }
2874 let year: i32 = datetime[0..4].parse().ok()?;
2875 let month: u32 = datetime[5..7].parse().ok()?;
2876 let day: u32 = datetime[8..10].parse().ok()?;
2877 let hour: u32 = datetime[11..13].parse().ok()?;
2878 let min: u32 = datetime[14..16].parse().ok()?;
2879 let sec: u32 = datetime[17..19].parse().ok()?;
2880
2881 let total_secs = (hour * 3600 + min * 60 + sec) as i64
2883 + sign as i64 * (hours * 3600 + minutes * 60 + seconds) as i64;
2884
2885 let days_shift = if total_secs < 0 {
2886 -1 - (-total_secs - 1) / 86400
2887 } else {
2888 total_secs / 86400
2889 };
2890
2891 let time_secs = ((total_secs % 86400) + 86400) % 86400;
2892 let new_hour = (time_secs / 3600) as u32;
2893 let new_min = ((time_secs % 3600) / 60) as u32;
2894 let new_sec = (time_secs % 60) as u32;
2895
2896 let mut new_day = day as i32 + days_shift as i32;
2898 let mut new_month = month;
2899 let mut new_year = year;
2900
2901 let days_in_month = |m: u32, y: i32| -> i32 {
2902 match m {
2903 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2904 4 | 6 | 9 | 11 => 30,
2905 2 => {
2906 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
2907 29
2908 } else {
2909 28
2910 }
2911 }
2912 _ => 30,
2913 }
2914 };
2915
2916 while new_day > days_in_month(new_month, new_year) {
2917 new_day -= days_in_month(new_month, new_year);
2918 new_month += 1;
2919 if new_month > 12 {
2920 new_month = 1;
2921 new_year += 1;
2922 }
2923 }
2924 while new_day < 1 {
2925 new_month = if new_month == 1 { 12 } else { new_month - 1 };
2926 if new_month == 12 {
2927 new_year -= 1;
2928 }
2929 new_day += days_in_month(new_month, new_year);
2930 }
2931
2932 Some(format!(
2933 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
2934 new_year, new_month, new_day, new_hour, new_min, new_sec
2935 ))
2936}
2937
2938#[cfg(unix)]
2945fn format_file_permissions(mode: u32) -> String {
2946 let type_char = match mode & 0o170000 {
2947 0o010000 => 'p', 0o020000 => 'c', 0o040000 => 'd', 0o060000 => 'b', 0o120000 => 'l', 0o140000 => 's', _ => '-',
2954 };
2955 let mut s = String::with_capacity(10);
2956 s.push(type_char);
2957 let mut mask = 0o400u32;
2958 while mask > 0 {
2959 for ch in ['r', 'w', 'x'] {
2960 s.push(if mode & mask != 0 { ch } else { '-' });
2961 mask >>= 1;
2962 }
2963 }
2964 s
2965}
2966
2967enum FileData {
2971 Mapped(memmap2::Mmap),
2972 Owned(Vec<u8>),
2973}
2974
2975impl std::ops::Deref for FileData {
2976 type Target = [u8];
2977 fn deref(&self) -> &[u8] {
2978 match self {
2979 FileData::Mapped(m) => m,
2980 FileData::Owned(v) => v,
2981 }
2982 }
2983}
2984
2985fn map_file_for_read(path: &Path) -> Result<FileData> {
2988 let file = fs::File::open(path).map_err(Error::Io)?;
2989 let len = file.metadata().map_err(Error::Io)?.len();
2990 if len == 0 {
2991 return Ok(FileData::Owned(Vec::new()));
2992 }
2993 match unsafe { memmap2::Mmap::map(&file) } {
2998 Ok(m) => Ok(FileData::Mapped(m)),
2999 Err(_) => Ok(FileData::Owned(fs::read(path).map_err(Error::Io)?)),
3000 }
3001}
3002
3003fn format_file_size(bytes: u64) -> String {
3005 let v = bytes as f64;
3006 if bytes < 2000 {
3007 format!("{} bytes", bytes)
3008 } else if bytes < 10_000 {
3009 format!("{:.1} kB", v / 1000.0)
3010 } else if bytes < 2_000_000 {
3011 format!("{:.0} kB", v / 1000.0)
3012 } else if bytes < 10_000_000 {
3013 format!("{:.1} MB", v / 1_000_000.0)
3014 } else if bytes < 2_000_000_000 {
3015 format!("{:.0} MB", v / 1_000_000.0)
3016 } else if bytes < 10_000_000_000 {
3017 format!("{:.1} GB", v / 1_000_000_000.0)
3018 } else {
3019 format!("{:.0} GB", v / 1_000_000_000.0)
3020 }
3021}
3022
3023fn is_xmp_tag(tag: &str) -> bool {
3025 matches!(
3026 tag.to_lowercase().as_str(),
3027 "title"
3028 | "description"
3029 | "subject"
3030 | "creator"
3031 | "rights"
3032 | "keywords"
3033 | "rating"
3034 | "label"
3035 | "hierarchicalsubject"
3036 )
3037}
3038
3039fn encode_exif_tag(
3042 tag_name: &str,
3043 value: &str,
3044 _group: &str,
3045 bo: ByteOrderMark,
3046) -> Option<(u16, exif_writer::ExifFormat, Vec<u8>)> {
3047 let tag_lower = tag_name.to_lowercase();
3048
3049 let (tag_id, format): (u16, exif_writer::ExifFormat) = match tag_lower.as_str() {
3051 "imagedescription" => (0x010E, exif_writer::ExifFormat::Ascii),
3053 "make" => (0x010F, exif_writer::ExifFormat::Ascii),
3054 "model" => (0x0110, exif_writer::ExifFormat::Ascii),
3055 "software" => (0x0131, exif_writer::ExifFormat::Ascii),
3056 "modifydate" | "datetime" => (0x0132, exif_writer::ExifFormat::Ascii),
3057 "artist" => (0x013B, exif_writer::ExifFormat::Ascii),
3058 "copyright" => (0x8298, exif_writer::ExifFormat::Ascii),
3059 "orientation" => (0x0112, exif_writer::ExifFormat::Short),
3061 "xresolution" => (0x011A, exif_writer::ExifFormat::Rational),
3062 "yresolution" => (0x011B, exif_writer::ExifFormat::Rational),
3063 "resolutionunit" => (0x0128, exif_writer::ExifFormat::Short),
3064 "datetimeoriginal" => (0x9003, exif_writer::ExifFormat::Ascii),
3066 "createdate" | "datetimedigitized" => (0x9004, exif_writer::ExifFormat::Ascii),
3067 "usercomment" => (0x9286, exif_writer::ExifFormat::Undefined),
3068 "imageuniqueid" => (0xA420, exif_writer::ExifFormat::Ascii),
3069 "ownername" | "cameraownername" => (0xA430, exif_writer::ExifFormat::Ascii),
3070 "serialnumber" | "bodyserialnumber" => (0xA431, exif_writer::ExifFormat::Ascii),
3071 "lensmake" => (0xA433, exif_writer::ExifFormat::Ascii),
3072 "lensmodel" => (0xA434, exif_writer::ExifFormat::Ascii),
3073 "lensserialnumber" => (0xA435, exif_writer::ExifFormat::Ascii),
3074 _ => return None,
3075 };
3076
3077 let encoded = match format {
3078 exif_writer::ExifFormat::Ascii => exif_writer::encode_ascii(value),
3079 exif_writer::ExifFormat::Short => {
3080 let v: u16 = value.parse().ok()?;
3081 exif_writer::encode_u16(v, bo)
3082 }
3083 exif_writer::ExifFormat::Long => {
3084 let v: u32 = value.parse().ok()?;
3085 exif_writer::encode_u32(v, bo)
3086 }
3087 exif_writer::ExifFormat::Rational => {
3088 if let Some(slash) = value.find('/') {
3090 let num: u32 = value[..slash].trim().parse().ok()?;
3091 let den: u32 = value[slash + 1..].trim().parse().ok()?;
3092 exif_writer::encode_urational(num, den, bo)
3093 } else if let Ok(v) = value.parse::<f64>() {
3094 let den = 10000u32;
3096 let num = (v * den as f64).round() as u32;
3097 exif_writer::encode_urational(num, den, bo)
3098 } else {
3099 return None;
3100 }
3101 }
3102 exif_writer::ExifFormat::Undefined => {
3103 let mut data = vec![0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00]; data.extend_from_slice(value.as_bytes());
3106 data
3107 }
3108 _ => return None,
3109 };
3110
3111 Some((tag_id, format, encoded))
3112}
3113
3114fn compute_text_tags(data: &[u8], is_csv: bool) -> Vec<Tag> {
3116 let mut tags = Vec::new();
3117 let mk = |name: &str, val: String| Tag {
3118 id: crate::tag::TagId::Text(name.into()),
3119 name: name.into(),
3120 description: name.into(),
3121 group: crate::tag::TagGroup {
3122 family0: "File".into(),
3123 family1: "File".into(),
3124 family2: "Other".into(),
3125 },
3126 raw_value: Value::String(val.clone()),
3127 print_value: val,
3128 priority: 0,
3129 };
3130
3131 let is_ascii = data.iter().all(|&b| b < 128);
3133 let has_utf8_bom = data.starts_with(&[0xEF, 0xBB, 0xBF]);
3134 let has_utf16le_bom =
3135 data.starts_with(&[0xFF, 0xFE]) && !data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
3136 let has_utf16be_bom = data.starts_with(&[0xFE, 0xFF]);
3137 let has_utf32le_bom = data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
3138 let has_utf32be_bom = data.starts_with(&[0x00, 0x00, 0xFE, 0xFF]);
3139
3140 let has_weird_ctrl = data.iter().any(|&b| {
3142 (b <= 0x06) || (0x0e..=0x1a).contains(&b) || (0x1c..=0x1f).contains(&b) || b == 0x7f
3143 });
3144
3145 let (encoding, is_bom, is_utf16) = if has_utf32le_bom {
3146 ("utf-32le", true, false)
3147 } else if has_utf32be_bom {
3148 ("utf-32be", true, false)
3149 } else if has_utf16le_bom {
3150 ("utf-16le", true, true)
3151 } else if has_utf16be_bom {
3152 ("utf-16be", true, true)
3153 } else if has_weird_ctrl {
3154 return tags;
3156 } else if is_ascii {
3157 ("us-ascii", false, false)
3158 } else {
3159 let is_valid_utf8 = std::str::from_utf8(data).is_ok();
3161 if is_valid_utf8 {
3162 if has_utf8_bom {
3163 ("utf-8", true, false)
3164 } else {
3165 ("utf-8", false, false)
3169 }
3170 } else if !data.iter().any(|&b| (0x80..=0x9f).contains(&b)) {
3171 ("iso-8859-1", false, false)
3172 } else {
3173 ("unknown-8bit", false, false)
3174 }
3175 };
3176
3177 tags.push(mk("MIMEEncoding", encoding.into()));
3178
3179 if is_bom {
3180 tags.push(mk("ByteOrderMark", "Yes".into()));
3181 }
3182
3183 let has_cr = data.contains(&b'\r');
3185 let has_lf = data.contains(&b'\n');
3186 let newline_type = if has_cr && has_lf {
3187 "Windows CRLF"
3188 } else if has_lf {
3189 "Unix LF"
3190 } else if has_cr {
3191 "Macintosh CR"
3192 } else {
3193 "(none)"
3194 };
3195 tags.push(mk("Newlines", newline_type.into()));
3196
3197 if is_csv {
3198 let text = crate::encoding::decode_utf8_or_latin1(data);
3200 let mut delim = "";
3201 let mut quot = "";
3202 let mut ncols = 1usize;
3203 let mut nrows = 0usize;
3204
3205 for line in text.lines() {
3206 if nrows == 0 {
3207 let comma_count = line.matches(',').count();
3209 let semi_count = line.matches(';').count();
3210 let tab_count = line.matches('\t').count();
3211 if comma_count > semi_count && comma_count > tab_count {
3212 delim = ",";
3213 ncols = comma_count + 1;
3214 } else if semi_count > tab_count {
3215 delim = ";";
3216 ncols = semi_count + 1;
3217 } else if tab_count > 0 {
3218 delim = "\t";
3219 ncols = tab_count + 1;
3220 } else {
3221 delim = "";
3222 ncols = 1;
3223 }
3224 if line.contains('"') {
3226 quot = "\"";
3227 } else if line.contains('\'') {
3228 quot = "'";
3229 }
3230 }
3231 nrows += 1;
3232 if nrows >= 1000 {
3233 break;
3234 }
3235 }
3236
3237 let delim_display = match delim {
3238 "," => "Comma",
3239 ";" => "Semicolon",
3240 "\t" => "Tab",
3241 _ => "(none)",
3242 };
3243 let quot_display = match quot {
3244 "\"" => "Double quotes",
3245 "'" => "Single quotes",
3246 _ => "(none)",
3247 };
3248
3249 tags.push(mk("Delimiter", delim_display.into()));
3250 tags.push(mk("Quoting", quot_display.into()));
3251 tags.push(mk("ColumnCount", ncols.to_string()));
3252 if nrows > 0 {
3253 tags.push(mk("RowCount", nrows.to_string()));
3254 }
3255 } else if !is_utf16 {
3256 let nl_count = data.iter().filter(|&&b| b == b'\n').count();
3260 let line_count = if !data.is_empty() && data.last() != Some(&b'\n') {
3261 nl_count + 1
3262 } else {
3263 nl_count
3264 };
3265 tags.push(mk("LineCount", line_count.to_string()));
3266
3267 let text = crate::encoding::decode_utf8_or_latin1(data);
3268 let word_count = text.split_whitespace().count();
3269 tags.push(mk("WordCount", word_count.to_string()));
3270 }
3271
3272 tags
3273}
3274
3275#[cfg(test)]
3276mod tests {
3277 use super::*;
3278
3279 #[test]
3280 fn new_has_default_options() {
3281 let et = ExifTool::new();
3282 assert!(!et.options().duplicates);
3283 assert!(et.options().print_conv);
3284 assert_eq!(et.options().fast_scan, 0);
3285 assert!(et.options().requested_tags.is_empty());
3286 assert_eq!(et.options().extract_embedded, 0);
3287 assert_eq!(et.options().show_unknown, 0);
3288 assert!(!et.options().process_compressed);
3289 assert!(!et.options().use_mwg);
3290 }
3291
3292 #[test]
3293 fn with_options_preserves_custom() {
3294 let opts = Options {
3295 duplicates: true,
3296 print_conv: false,
3297 fast_scan: 2,
3298 requested_tags: vec!["Artist".to_string()],
3299 extract_embedded: 1,
3300 show_unknown: 1,
3301 process_compressed: true,
3302 use_mwg: true,
3303 geolocation: true,
3304 };
3305 let et = ExifTool::with_options(opts.clone());
3306 assert!(et.options().duplicates);
3307 assert!(!et.options().print_conv);
3308 assert_eq!(et.options().fast_scan, 2);
3309 assert_eq!(et.options().requested_tags, vec!["Artist".to_string()]);
3310 assert_eq!(et.options().extract_embedded, 1);
3311 assert_eq!(et.options().show_unknown, 1);
3312 assert!(et.options().process_compressed);
3313 assert!(et.options().use_mwg);
3314 }
3315
3316 #[test]
3317 fn set_new_value_simple_tag() {
3318 let mut et = ExifTool::new();
3319 et.set_new_value("Artist", Some("John"));
3320 assert_eq!(et.new_values.len(), 1);
3321 assert_eq!(et.new_values[0].tag, "Artist");
3322 assert_eq!(et.new_values[0].group, None);
3323 assert_eq!(et.new_values[0].value, Some("John".to_string()));
3324 }
3325
3326 #[test]
3327 fn set_new_value_with_group_prefix() {
3328 let mut et = ExifTool::new();
3329 et.set_new_value("XMP:Title", Some("Test"));
3330 assert_eq!(et.new_values.len(), 1);
3331 assert_eq!(et.new_values[0].tag, "Title");
3332 assert_eq!(et.new_values[0].group, Some("XMP".to_string()));
3333 assert_eq!(et.new_values[0].value, Some("Test".to_string()));
3334 }
3335
3336 #[test]
3337 fn set_new_value_delete() {
3338 let mut et = ExifTool::new();
3339 et.set_new_value("Comment", None);
3340 assert_eq!(et.new_values.len(), 1);
3341 assert_eq!(et.new_values[0].tag, "Comment");
3342 assert_eq!(et.new_values[0].value, None);
3343 }
3344
3345 #[test]
3346 fn clear_new_values_empties_queue() {
3347 let mut et = ExifTool::new();
3348 et.set_new_value("Artist", Some("A"));
3349 et.set_new_value("Copyright", Some("B"));
3350 assert_eq!(et.new_values.len(), 2);
3351 et.clear_new_values();
3352 assert!(et.new_values.is_empty());
3353 }
3354
3355 #[test]
3356 fn set_new_value_multiple() {
3357 let mut et = ExifTool::new();
3358 et.set_new_value("Artist", Some("John"));
3359 et.set_new_value("IPTC:Keywords", Some("test"));
3360 et.set_new_value("XMP:Subject", None);
3361 assert_eq!(et.new_values.len(), 3);
3362 assert_eq!(et.new_values[1].group, Some("IPTC".to_string()));
3363 assert_eq!(et.new_values[1].tag, "Keywords");
3364 assert_eq!(et.new_values[2].value, None);
3365 }
3366
3367 #[test]
3368 fn options_mut_modifies() {
3369 let mut et = ExifTool::new();
3370 et.options_mut().duplicates = true;
3371 et.options_mut().fast_scan = 3;
3372 assert!(et.options().duplicates);
3373 assert_eq!(et.options().fast_scan, 3);
3374 }
3375
3376 #[test]
3377 fn default_options() {
3378 let opts = Options::default();
3379 assert!(!opts.duplicates);
3380 assert!(opts.print_conv);
3381 assert_eq!(opts.fast_scan, 0);
3382 }
3383}