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::{exif_writer, iptc_writer, jpeg_writer, matroska_writer, mp4_writer, pdf_writer, png_writer, psd_writer, tiff_writer, webp_writer, xmp_writer};
17
18#[derive(Debug, Clone)]
20pub struct Options {
21 pub duplicates: bool,
23 pub print_conv: bool,
25 pub fast_scan: u8,
27 pub requested_tags: Vec<String>,
29 pub extract_embedded: u8,
31}
32
33impl Default for Options {
34 fn default() -> Self {
35 Self {
36 duplicates: false,
37 print_conv: true,
38 fast_scan: 0,
39 requested_tags: Vec::new(),
40 extract_embedded: 0,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
59pub struct NewValue {
60 pub tag: String,
62 pub group: Option<String>,
64 pub value: Option<String>,
66}
67
68pub struct ExifTool {
97 options: Options,
98 new_values: Vec<NewValue>,
99}
100
101pub type ImageInfo = HashMap<String, String>;
103
104impl ExifTool {
105 pub fn new() -> Self {
107 Self {
108 options: Options::default(),
109 new_values: Vec::new(),
110 }
111 }
112
113 pub fn with_options(options: Options) -> Self {
115 Self {
116 options,
117 new_values: Vec::new(),
118 }
119 }
120
121 pub fn options_mut(&mut self) -> &mut Options {
123 &mut self.options
124 }
125
126 pub fn options(&self) -> &Options {
128 &self.options
129 }
130
131 pub fn set_new_value(&mut self, tag: &str, value: Option<&str>) {
153 let (group, tag_name) = if let Some(colon_pos) = tag.find(':') {
154 (Some(tag[..colon_pos].to_string()), tag[colon_pos + 1..].to_string())
155 } else {
156 (None, tag.to_string())
157 };
158
159 self.new_values.push(NewValue {
160 tag: tag_name,
161 group,
162 value: value.map(|v| v.to_string()),
163 });
164 }
165
166 pub fn clear_new_values(&mut self) {
168 self.new_values.clear();
169 }
170
171 pub fn set_new_values_from_file<P: AsRef<Path>>(
176 &mut self,
177 src_path: P,
178 tags_to_copy: Option<&[&str]>,
179 ) -> Result<u32> {
180 let src_tags = self.extract_info(src_path)?;
181 let mut count = 0u32;
182
183 for tag in &src_tags {
184 if tag.group.family0 == "File" || tag.group.family0 == "Composite" {
186 continue;
187 }
188 if tag.print_value.starts_with("(Binary") || tag.print_value.starts_with("(Undefined") {
190 continue;
191 }
192 if tag.print_value.is_empty() {
193 continue;
194 }
195
196 if let Some(filter) = tags_to_copy {
198 let name_lower = tag.name.to_lowercase();
199 if !filter.iter().any(|f| f.to_lowercase() == name_lower) {
200 continue;
201 }
202 }
203
204 let _full_tag = format!("{}:{}", tag.group.family0, tag.name);
205 self.new_values.push(NewValue {
206 tag: tag.name.clone(),
207 group: Some(tag.group.family0.clone()),
208 value: Some(tag.print_value.clone()),
209 });
210 count += 1;
211 }
212
213 Ok(count)
214 }
215
216 pub fn set_file_name_from_tag<P: AsRef<Path>>(
218 &self,
219 path: P,
220 tag_name: &str,
221 template: &str,
222 ) -> Result<String> {
223 let path = path.as_ref();
224 let tags = self.extract_info(path)?;
225
226 let tag_value = tags
227 .iter()
228 .find(|t| t.name.to_lowercase() == tag_name.to_lowercase())
229 .map(|t| &t.print_value)
230 .ok_or_else(|| Error::TagNotFound(tag_name.to_string()))?;
231
232 let new_name = if template.contains('%') {
235 template.replace("%v", value_to_filename(tag_value).as_str())
236 } else {
237 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
239 let clean = value_to_filename(tag_value);
240 if ext.is_empty() {
241 clean
242 } else {
243 format!("{}.{}", clean, ext)
244 }
245 };
246
247 let parent = path.parent().unwrap_or(Path::new(""));
248 let new_path = parent.join(&new_name);
249
250 fs::rename(path, &new_path).map_err(Error::Io)?;
251 Ok(new_path.to_string_lossy().to_string())
252 }
253
254 pub fn write_info<P: AsRef<Path>, Q: AsRef<Path>>(&self, src_path: P, dst_path: Q) -> Result<u32> {
259 let src_path = src_path.as_ref();
260 let dst_path = dst_path.as_ref();
261 let data = fs::read(src_path).map_err(Error::Io)?;
262
263 let file_type = self.detect_file_type(&data, src_path)?;
264 let output = self.apply_changes(&data, file_type)?;
265
266 let temp_path = dst_path.with_extension("exiftool_tmp");
268 fs::write(&temp_path, &output).map_err(Error::Io)?;
269 fs::rename(&temp_path, dst_path).map_err(Error::Io)?;
270
271 Ok(self.new_values.len() as u32)
272 }
273
274 fn apply_changes(&self, data: &[u8], file_type: FileType) -> Result<Vec<u8>> {
276 match file_type {
277 FileType::Jpeg => self.write_jpeg(data),
278 FileType::Png => self.write_png(data),
279 FileType::Tiff | FileType::Dng | FileType::Cr2 | FileType::Nef
280 | FileType::Arw | FileType::Orf | FileType::Pef => self.write_tiff(data),
281 FileType::WebP => self.write_webp(data),
282 FileType::Mp4 | FileType::QuickTime | FileType::M4a
283 | FileType::ThreeGP | FileType::F4v => self.write_mp4(data),
284 FileType::Psd => self.write_psd(data),
285 FileType::Pdf => self.write_pdf(data),
286 FileType::Heif | FileType::Avif => self.write_mp4(data),
287 FileType::Mkv | FileType::WebM => self.write_matroska(data),
288 FileType::Gif => {
289 let comment = self.new_values.iter()
290 .find(|nv| nv.tag.to_lowercase() == "comment")
291 .and_then(|nv| nv.value.clone());
292 crate::writer::gif_writer::write_gif(data, comment.as_deref())
293 }
294 FileType::Flac => {
295 let changes: Vec<(&str, &str)> = self.new_values.iter()
296 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
297 .collect();
298 crate::writer::flac_writer::write_flac(data, &changes)
299 }
300 FileType::Mp3 | FileType::Aiff => {
301 let changes: Vec<(&str, &str)> = self.new_values.iter()
302 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
303 .collect();
304 crate::writer::id3_writer::write_id3(data, &changes)
305 }
306 FileType::Jp2 | FileType::Jxl => {
307 let new_xmp = if self.new_values.iter().any(|nv| nv.group.as_deref() == Some("XMP")) {
308 let refs: Vec<&NewValue> = self.new_values.iter()
309 .filter(|nv| nv.group.as_deref() == Some("XMP"))
310 .collect();
311 Some(self.build_new_xmp(&refs))
312 } else { None };
313 crate::writer::jp2_writer::write_jp2(data, new_xmp.as_deref(), None)
314 }
315 FileType::PostScript => {
316 let changes: Vec<(&str, &str)> = self.new_values.iter()
317 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
318 .collect();
319 crate::writer::ps_writer::write_postscript(data, &changes)
320 }
321 FileType::Ogg | FileType::Opus => {
322 let changes: Vec<(&str, &str)> = self.new_values.iter()
323 .filter_map(|nv| Some((nv.tag.as_str(), nv.value.as_deref()?)))
324 .collect();
325 crate::writer::ogg_writer::write_ogg(data, &changes)
326 }
327 FileType::Xmp => {
328 let props: Vec<xmp_writer::XmpProperty> = self.new_values.iter()
329 .filter_map(|nv| {
330 let val = nv.value.as_deref()?;
331 Some(xmp_writer::XmpProperty {
332 namespace: nv.group.clone().unwrap_or_else(|| "dc".into()),
333 property: nv.tag.clone(),
334 values: vec![val.to_string()],
335 prop_type: xmp_writer::XmpPropertyType::Simple,
336 })
337 })
338 .collect();
339 Ok(crate::writer::xmp_sidecar_writer::write_xmp_sidecar(&props))
340 }
341 _ => Err(Error::UnsupportedFileType(format!("writing not yet supported for {}", file_type))),
342 }
343 }
344
345 pub fn writable_tags(file_type: FileType) -> Option<std::collections::HashSet<&'static str>> {
349 use std::collections::HashSet;
350
351 const EXIF_TAGS: &[&str] = &[
353 "imagedescription", "make", "model", "orientation",
354 "xresolution", "yresolution", "resolutionunit", "software",
355 "modifydate", "datetime", "artist", "copyright",
356 "datetimeoriginal", "createdate", "datetimedigitized",
357 "usercomment", "imageuniqueid", "ownername", "cameraownername",
358 "serialnumber", "bodyserialnumber", "lensmake", "lensmodel", "lensserialnumber",
359 ];
360
361 const IPTC_TAGS: &[&str] = &[
363 "objectname", "title", "urgency", "category", "supplementalcategories",
364 "keywords", "specialinstructions", "datecreated", "timecreated",
365 "by-line", "author", "byline", "by-linetitle", "authorsposition", "bylinetitle",
366 "city", "sub-location", "sublocation", "province-state", "state", "provincestate",
367 "country-primarylocationcode", "countrycode",
368 "country-primarylocationname", "country",
369 "headline", "credit", "source", "copyrightnotice",
370 "contact", "caption-abstract", "caption", "description",
371 "writer-editor", "captionwriter",
372 ];
373
374 const XMP_AUTO_TAGS: &[&str] = &[
376 "title", "description", "subject", "creator", "rights",
377 "keywords", "rating", "label", "hierarchicalsubject",
378 ];
379
380 const ID3_TAGS: &[&str] = &[
382 "title", "artist", "album", "year", "date", "track",
383 "genre", "comment", "composer", "albumartist",
384 "encoder", "encodedby", "publisher", "copyright", "bpm", "lyrics",
385 ];
386
387 const MP4_TAGS: &[&str] = &[
389 "title", "artist", "album", "year", "date", "comment",
390 "genre", "composer", "writer", "encoder", "encodedby",
391 "grouping", "lyrics", "description", "albumartist", "copyright",
392 ];
393
394 const PDF_TAGS: &[&str] = &[
396 "title", "author", "subject", "keywords", "creator", "producer",
397 ];
398
399 const PS_TAGS: &[&str] = &[
401 "title", "creator", "author", "for", "creationdate", "createdate",
402 ];
403
404 match file_type {
405 FileType::Png | FileType::Flac | FileType::Mkv | FileType::WebM
407 | FileType::Ogg | FileType::Opus | FileType::Xmp => None,
408
409 FileType::Jpeg => {
411 let mut set: HashSet<&str> = HashSet::new();
412 set.extend(EXIF_TAGS);
413 set.extend(IPTC_TAGS);
414 set.extend(XMP_AUTO_TAGS);
415 set.insert("comment");
416 Some(set)
417 }
418
419 FileType::Tiff | FileType::Dng | FileType::Cr2 | FileType::Nef
421 | FileType::Arw | FileType::Orf | FileType::Pef => {
422 let mut set: HashSet<&str> = HashSet::new();
423 set.extend(EXIF_TAGS);
424 Some(set)
425 }
426
427 FileType::WebP => {
429 let mut set: HashSet<&str> = HashSet::new();
430 set.extend(EXIF_TAGS);
431 set.extend(XMP_AUTO_TAGS);
432 Some(set)
433 }
434
435 FileType::Mp4 | FileType::QuickTime | FileType::M4a
437 | FileType::ThreeGP | FileType::F4v | FileType::Heif | FileType::Avif => {
438 let mut set: HashSet<&str> = HashSet::new();
439 set.extend(MP4_TAGS);
440 set.extend(XMP_AUTO_TAGS);
441 Some(set)
442 }
443
444 FileType::Psd => {
446 let mut set: HashSet<&str> = HashSet::new();
447 set.extend(IPTC_TAGS);
448 set.extend(XMP_AUTO_TAGS);
449 Some(set)
450 }
451
452 FileType::Pdf => Some(PDF_TAGS.iter().copied().collect()),
453 FileType::PostScript => Some(PS_TAGS.iter().copied().collect()),
454
455 FileType::Mp3 | FileType::Aiff => Some(ID3_TAGS.iter().copied().collect()),
456
457 FileType::Gif => {
458 let mut set: HashSet<&str> = HashSet::new();
459 set.insert("comment");
460 Some(set)
461 }
462
463 FileType::Jp2 | FileType::Jxl => Some(XMP_AUTO_TAGS.iter().copied().collect()),
465
466 _ => Some(HashSet::new()),
468 }
469 }
470
471 fn write_jpeg(&self, data: &[u8]) -> Result<Vec<u8>> {
473 let mut exif_values: Vec<&NewValue> = Vec::new();
475 let mut xmp_values: Vec<&NewValue> = Vec::new();
476 let mut iptc_values: Vec<&NewValue> = Vec::new();
477 let mut comment_value: Option<&str> = None;
478 let mut remove_exif = false;
479 let mut remove_xmp = false;
480 let mut remove_iptc = false;
481 let mut remove_comment = false;
482
483 for nv in &self.new_values {
484 let group = nv.group.as_deref().unwrap_or("");
485 let group_upper = group.to_uppercase();
486
487 if nv.value.is_none() && nv.tag == "*" {
489 match group_upper.as_str() {
490 "EXIF" => { remove_exif = true; continue; }
491 "XMP" => { remove_xmp = true; continue; }
492 "IPTC" => { remove_iptc = true; continue; }
493 _ => {}
494 }
495 }
496
497 match group_upper.as_str() {
498 "XMP" => xmp_values.push(nv),
499 "IPTC" => iptc_values.push(nv),
500 "EXIF" | "IFD0" | "EXIFIFD" | "GPS" => exif_values.push(nv),
501 "" => {
502 if nv.tag.to_lowercase() == "comment" {
504 if nv.value.is_none() {
505 remove_comment = true;
506 } else {
507 comment_value = nv.value.as_deref();
508 }
509 } else if is_xmp_tag(&nv.tag) {
510 xmp_values.push(nv);
511 } else {
512 exif_values.push(nv);
513 }
514 }
515 _ => exif_values.push(nv), }
517 }
518
519 let new_exif = if !exif_values.is_empty() {
521 Some(self.build_new_exif(data, &exif_values)?)
522 } else {
523 None
524 };
525
526 let new_xmp = if !xmp_values.is_empty() {
528 Some(self.build_new_xmp(&xmp_values))
529 } else {
530 None
531 };
532
533 let new_iptc_data = if !iptc_values.is_empty() {
535 let records: Vec<iptc_writer::IptcRecord> = iptc_values
536 .iter()
537 .filter_map(|nv| {
538 let value = nv.value.as_deref()?;
539 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
540 Some(iptc_writer::IptcRecord {
541 record,
542 dataset,
543 data: value.as_bytes().to_vec(),
544 })
545 })
546 .collect();
547 if records.is_empty() {
548 None
549 } else {
550 Some(iptc_writer::build_iptc(&records))
551 }
552 } else {
553 None
554 };
555
556 jpeg_writer::write_jpeg(
558 data,
559 new_exif.as_deref(),
560 new_xmp.as_deref(),
561 new_iptc_data.as_deref(),
562 comment_value,
563 remove_exif,
564 remove_xmp,
565 remove_iptc,
566 remove_comment,
567 )
568 }
569
570 fn build_new_exif(&self, jpeg_data: &[u8], values: &[&NewValue]) -> Result<Vec<u8>> {
572 let bo = ByteOrderMark::BigEndian;
573 let mut ifd0_entries = Vec::new();
574 let mut exif_entries = Vec::new();
575 let mut gps_entries = Vec::new();
576
577 let existing = extract_existing_exif_entries(jpeg_data, bo);
579 for entry in &existing {
580 match classify_exif_tag(entry.tag) {
581 ExifIfdGroup::Ifd0 => ifd0_entries.push(entry.clone()),
582 ExifIfdGroup::ExifIfd => exif_entries.push(entry.clone()),
583 ExifIfdGroup::Gps => gps_entries.push(entry.clone()),
584 }
585 }
586
587 let deleted_tags: Vec<u16> = values
589 .iter()
590 .filter(|nv| nv.value.is_none())
591 .filter_map(|nv| tag_name_to_id(&nv.tag))
592 .collect();
593
594 ifd0_entries.retain(|e| !deleted_tags.contains(&e.tag));
596 exif_entries.retain(|e| !deleted_tags.contains(&e.tag));
597 gps_entries.retain(|e| !deleted_tags.contains(&e.tag));
598
599 for nv in values {
601 if nv.value.is_none() {
602 continue;
603 }
604 let value_str = nv.value.as_deref().unwrap_or("");
605 let group = nv.group.as_deref().unwrap_or("");
606
607 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, value_str, group, bo) {
608 let entry = exif_writer::IfdEntry {
609 tag: tag_id,
610 format,
611 data: encoded,
612 };
613
614 let target = match group.to_uppercase().as_str() {
615 "GPS" => &mut gps_entries,
616 "EXIFIFD" => &mut exif_entries,
617 _ => match classify_exif_tag(tag_id) {
618 ExifIfdGroup::ExifIfd => &mut exif_entries,
619 ExifIfdGroup::Gps => &mut gps_entries,
620 ExifIfdGroup::Ifd0 => &mut ifd0_entries,
621 },
622 };
623
624 if let Some(existing) = target.iter_mut().find(|e| e.tag == tag_id) {
626 *existing = entry;
627 } else {
628 target.push(entry);
629 }
630 }
631 }
632
633 ifd0_entries.retain(|e| e.tag != 0x8769 && e.tag != 0x8825 && e.tag != 0xA005);
635
636 exif_writer::build_exif(&ifd0_entries, &exif_entries, &gps_entries, bo)
637 }
638
639 fn write_png(&self, data: &[u8]) -> Result<Vec<u8>> {
641 let mut new_text: Vec<(&str, &str)> = Vec::new();
642 let mut remove_text: Vec<&str> = Vec::new();
643
644 let owned_pairs: Vec<(String, String)> = self.new_values.iter()
647 .filter(|nv| nv.value.is_some())
648 .map(|nv| (nv.tag.clone(), nv.value.clone().unwrap()))
649 .collect();
650
651 for (tag, value) in &owned_pairs {
652 new_text.push((tag.as_str(), value.as_str()));
653 }
654
655 for nv in &self.new_values {
656 if nv.value.is_none() {
657 remove_text.push(&nv.tag);
658 }
659 }
660
661 png_writer::write_png(data, &new_text, None, &remove_text)
662 }
663
664 fn write_psd(&self, data: &[u8]) -> Result<Vec<u8>> {
666 let mut iptc_values = Vec::new();
667 let mut xmp_values = Vec::new();
668
669 for nv in &self.new_values {
670 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
671 match group.as_str() {
672 "XMP" => xmp_values.push(nv),
673 "IPTC" => iptc_values.push(nv),
674 _ => {
675 if is_xmp_tag(&nv.tag) { xmp_values.push(nv); }
676 else { iptc_values.push(nv); }
677 }
678 }
679 }
680
681 let new_iptc = if !iptc_values.is_empty() {
682 let records: Vec<_> = iptc_values.iter().filter_map(|nv| {
683 let value = nv.value.as_deref()?;
684 let (record, dataset) = iptc_writer::tag_name_to_iptc(&nv.tag)?;
685 Some(iptc_writer::IptcRecord { record, dataset, data: value.as_bytes().to_vec() })
686 }).collect();
687 if records.is_empty() { None } else { Some(iptc_writer::build_iptc(&records)) }
688 } else { None };
689
690 let new_xmp = if !xmp_values.is_empty() {
691 let refs: Vec<&NewValue> = xmp_values.iter().copied().collect();
692 Some(self.build_new_xmp(&refs))
693 } else { None };
694
695 psd_writer::write_psd(data, new_iptc.as_deref(), new_xmp.as_deref())
696 }
697
698 fn write_matroska(&self, data: &[u8]) -> Result<Vec<u8>> {
700 let changes: Vec<(&str, &str)> = self.new_values.iter()
701 .filter_map(|nv| {
702 let value = nv.value.as_deref()?;
703 Some((nv.tag.as_str(), value))
704 })
705 .collect();
706
707 matroska_writer::write_matroska(data, &changes)
708 }
709
710 fn write_pdf(&self, data: &[u8]) -> Result<Vec<u8>> {
712 let changes: Vec<(&str, &str)> = self.new_values.iter()
713 .filter_map(|nv| {
714 let value = nv.value.as_deref()?;
715 Some((nv.tag.as_str(), value))
716 })
717 .collect();
718
719 pdf_writer::write_pdf(data, &changes)
720 }
721
722 fn write_mp4(&self, data: &[u8]) -> Result<Vec<u8>> {
724 let mut ilst_tags: Vec<([u8; 4], String)> = Vec::new();
725 let mut xmp_values: Vec<&NewValue> = Vec::new();
726
727 for nv in &self.new_values {
728 if nv.value.is_none() { continue; }
729 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
730 if group == "XMP" {
731 xmp_values.push(nv);
732 } else if let Some(key) = mp4_writer::tag_to_ilst_key(&nv.tag) {
733 ilst_tags.push((key, nv.value.clone().unwrap()));
734 }
735 }
736
737 let tag_refs: Vec<(&[u8; 4], &str)> = ilst_tags.iter()
738 .map(|(k, v)| (k, v.as_str()))
739 .collect();
740
741 let new_xmp = if !xmp_values.is_empty() {
742 let refs: Vec<&NewValue> = xmp_values.iter().copied().collect();
743 Some(self.build_new_xmp(&refs))
744 } else {
745 None
746 };
747
748 mp4_writer::write_mp4(data, &tag_refs, new_xmp.as_deref())
749 }
750
751 fn write_webp(&self, data: &[u8]) -> Result<Vec<u8>> {
753 let mut exif_values: Vec<&NewValue> = Vec::new();
754 let mut xmp_values: Vec<&NewValue> = Vec::new();
755 let mut remove_exif = false;
756 let mut remove_xmp = false;
757
758 for nv in &self.new_values {
759 let group = nv.group.as_deref().unwrap_or("").to_uppercase();
760 if nv.value.is_none() && nv.tag == "*" {
761 if group == "EXIF" { remove_exif = true; }
762 if group == "XMP" { remove_xmp = true; }
763 continue;
764 }
765 match group.as_str() {
766 "XMP" => xmp_values.push(nv),
767 _ => exif_values.push(nv),
768 }
769 }
770
771 let new_exif = if !exif_values.is_empty() {
772 let bo = ByteOrderMark::BigEndian;
773 let mut entries = Vec::new();
774 for nv in &exif_values {
775 if let Some(ref v) = nv.value {
776 let group = nv.group.as_deref().unwrap_or("");
777 if let Some((tag_id, format, encoded)) = encode_exif_tag(&nv.tag, v, group, bo) {
778 entries.push(exif_writer::IfdEntry { tag: tag_id, format, data: encoded });
779 }
780 }
781 }
782 if !entries.is_empty() {
783 Some(exif_writer::build_exif(&entries, &[], &[], bo)?)
784 } else {
785 None
786 }
787 } else {
788 None
789 };
790
791 let new_xmp = if !xmp_values.is_empty() {
792 Some(self.build_new_xmp(&xmp_values.iter().map(|v| *v).collect::<Vec<_>>()))
793 } else {
794 None
795 };
796
797 webp_writer::write_webp(
798 data,
799 new_exif.as_deref(),
800 new_xmp.as_deref(),
801 remove_exif,
802 remove_xmp,
803 )
804 }
805
806 fn write_tiff(&self, data: &[u8]) -> Result<Vec<u8>> {
808 let bo = if data.starts_with(b"II") {
809 ByteOrderMark::LittleEndian
810 } else {
811 ByteOrderMark::BigEndian
812 };
813
814 let mut changes: Vec<(u16, Vec<u8>)> = Vec::new();
815 for nv in &self.new_values {
816 if let Some(ref value) = nv.value {
817 let group = nv.group.as_deref().unwrap_or("");
818 if let Some((tag_id, _format, encoded)) = encode_exif_tag(&nv.tag, value, group, bo) {
819 changes.push((tag_id, encoded));
820 }
821 }
822 }
823
824 tiff_writer::write_tiff(data, &changes)
825 }
826
827 fn build_new_xmp(&self, values: &[&NewValue]) -> Vec<u8> {
829 let mut properties = Vec::new();
830
831 for nv in values {
832 let value_str = match &nv.value {
833 Some(v) => v.clone(),
834 None => continue,
835 };
836
837 let ns = nv.group.as_deref().unwrap_or("dc").to_lowercase();
838 let ns = if ns == "xmp" { "xmp".to_string() } else { ns };
839
840 let prop_type = match nv.tag.to_lowercase().as_str() {
841 "title" | "description" | "rights" => xmp_writer::XmpPropertyType::LangAlt,
842 "subject" | "keywords" => xmp_writer::XmpPropertyType::Bag,
843 "creator" => xmp_writer::XmpPropertyType::Seq,
844 _ => xmp_writer::XmpPropertyType::Simple,
845 };
846
847 let values = if matches!(prop_type, xmp_writer::XmpPropertyType::Bag | xmp_writer::XmpPropertyType::Seq) {
848 value_str.split(',').map(|s| s.trim().to_string()).collect()
849 } else {
850 vec![value_str]
851 };
852
853 properties.push(xmp_writer::XmpProperty {
854 namespace: ns,
855 property: nv.tag.clone(),
856 values,
857 prop_type,
858 });
859 }
860
861 xmp_writer::build_xmp(&properties).into_bytes()
862 }
863
864 pub fn image_info<P: AsRef<Path>>(&self, path: P) -> Result<ImageInfo> {
872 let tags = self.extract_info(path)?;
873 Ok(self.get_info(&tags))
874 }
875
876 pub fn extract_info<P: AsRef<Path>>(&self, path: P) -> Result<Vec<Tag>> {
880 let path = path.as_ref();
881 let data = fs::read(path).map_err(Error::Io)?;
882
883 self.extract_info_from_bytes(&data, path)
884 }
885
886 pub fn extract_info_from_bytes(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
888 let file_type_result = self.detect_file_type(data, path);
889 let (file_type, mut tags) = match file_type_result {
890 Ok(ft) => {
891 let t = self.process_file(data, ft).or_else(|_| {
892 self.process_by_extension(data, path)
893 })?;
894 (Some(ft), t)
895 }
896 Err(_) => {
897 let t = self.process_by_extension(data, path)?;
899 (None, t)
900 }
901 };
902 let file_type = file_type.unwrap_or(FileType::Zip); tags.push(Tag {
906 id: crate::tag::TagId::Text("FileType".into()),
907 name: "FileType".into(),
908 description: "File Type".into(),
909 group: crate::tag::TagGroup {
910 family0: "File".into(),
911 family1: "File".into(),
912 family2: "Other".into(),
913 },
914 raw_value: Value::String(format!("{:?}", file_type)),
915 print_value: file_type.description().to_string(),
916 priority: 0,
917 });
918
919 tags.push(Tag {
920 id: crate::tag::TagId::Text("MIMEType".into()),
921 name: "MIMEType".into(),
922 description: "MIME Type".into(),
923 group: crate::tag::TagGroup {
924 family0: "File".into(),
925 family1: "File".into(),
926 family2: "Other".into(),
927 },
928 raw_value: Value::String(file_type.mime_type().to_string()),
929 print_value: file_type.mime_type().to_string(),
930 priority: 0,
931 });
932
933 if let Ok(metadata) = fs::metadata(path) {
934 tags.push(Tag {
935 id: crate::tag::TagId::Text("FileSize".into()),
936 name: "FileSize".into(),
937 description: "File Size".into(),
938 group: crate::tag::TagGroup {
939 family0: "File".into(),
940 family1: "File".into(),
941 family2: "Other".into(),
942 },
943 raw_value: Value::U32(metadata.len() as u32),
944 print_value: format_file_size(metadata.len()),
945 priority: 0,
946 });
947 }
948
949 let file_tag = |name: &str, val: Value| -> Tag {
951 Tag {
952 id: crate::tag::TagId::Text(name.to_string()),
953 name: name.to_string(), description: name.to_string(),
954 group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Other".into() },
955 raw_value: val.clone(), print_value: val.to_display_string(), priority: 0,
956 }
957 };
958
959 if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
960 tags.push(file_tag("FileName", Value::String(fname.to_string())));
961 }
962 if let Some(dir) = path.parent().and_then(|p| p.to_str()) {
963 tags.push(file_tag("Directory", Value::String(dir.to_string())));
964 }
965 let canonical_ext = file_type.extensions().first().copied().unwrap_or("");
967 if !canonical_ext.is_empty() {
968 tags.push(file_tag("FileTypeExtension", Value::String(canonical_ext.to_string())));
969 }
970
971 #[cfg(unix)]
972 if let Ok(metadata) = fs::metadata(path) {
973 use std::os::unix::fs::MetadataExt;
974 let mode = metadata.mode();
975 tags.push(file_tag("FilePermissions", Value::String(format!("{:o}", mode & 0o7777))));
976
977 if let Ok(modified) = metadata.modified() {
979 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
980 let secs = dur.as_secs() as i64;
981 tags.push(file_tag("FileModifyDate", Value::String(unix_to_datetime(secs))));
982 }
983 }
984 if let Ok(accessed) = metadata.accessed() {
986 if let Ok(dur) = accessed.duration_since(std::time::UNIX_EPOCH) {
987 let secs = dur.as_secs() as i64;
988 tags.push(file_tag("FileAccessDate", Value::String(unix_to_datetime(secs))));
989 }
990 }
991 let ctime = metadata.ctime();
993 if ctime > 0 {
994 tags.push(file_tag("FileInodeChangeDate", Value::String(unix_to_datetime(ctime))));
995 }
996 }
997
998 {
1000 let bo_str = if data.len() > 8 {
1001 let check: Option<&[u8]> = if data.starts_with(&[0xFF, 0xD8]) {
1003 data.windows(6).position(|w| w == b"Exif\0\0")
1005 .map(|p| &data[p+6..])
1006 } else if data.starts_with(b"FUJIFILMCCD-RAW") && data.len() >= 0x60 {
1007 let jpeg_offset = u32::from_be_bytes([data[0x54], data[0x55], data[0x56], data[0x57]]) as usize;
1009 let jpeg_length = u32::from_be_bytes([data[0x58], data[0x59], data[0x5A], data[0x5B]]) as usize;
1010 if jpeg_offset > 0 && jpeg_offset + jpeg_length <= data.len() {
1011 let jpeg = &data[jpeg_offset..jpeg_offset + jpeg_length];
1012 jpeg.windows(6).position(|w| w == b"Exif\0\0")
1013 .map(|p| &jpeg[p+6..])
1014 } else {
1015 None
1016 }
1017 } else if data.starts_with(b"RIFF") && data.len() >= 12 {
1018 let mut riff_bo: Option<&[u8]> = None;
1020 let mut pos = 12usize;
1021 while pos + 8 <= data.len() {
1022 let cid = &data[pos..pos+4];
1023 let csz = u32::from_le_bytes([data[pos+4],data[pos+5],data[pos+6],data[pos+7]]) as usize;
1024 let cstart = pos + 8;
1025 let cend = (cstart + csz).min(data.len());
1026 if cid == b"EXIF" && cend > cstart {
1027 let exif_data = &data[cstart..cend];
1028 let tiff = if exif_data.starts_with(b"Exif\0\0") { &exif_data[6..] } else { exif_data };
1029 riff_bo = Some(tiff);
1030 break;
1031 }
1032 if cid == b"LIST" && cend >= cstart + 4 {
1034 }
1036 pos = cend + (csz & 1);
1037 }
1038 riff_bo
1039 } else if data.starts_with(&[0x00, 0x00, 0x00, 0x0C, b'J', b'X', b'L', b' ']) {
1040 let mut jxl_bo: Option<String> = None;
1042 let mut jpos = 12usize; while jpos + 8 <= data.len() {
1044 let bsize = u32::from_be_bytes([data[jpos], data[jpos+1], data[jpos+2], data[jpos+3]]) as usize;
1045 let btype = &data[jpos+4..jpos+8];
1046 if bsize < 8 || jpos + bsize > data.len() { break; }
1047 if btype == b"brob" && jpos + bsize > 12 {
1048 let inner_type = &data[jpos+8..jpos+12];
1049 if inner_type == b"Exif" || inner_type == b"exif" {
1050 let brotli_payload = &data[jpos+12..jpos+bsize];
1051 use std::io::Cursor;
1052 let mut inp = Cursor::new(brotli_payload);
1053 let mut out: Vec<u8> = Vec::new();
1054 if brotli::BrotliDecompress(&mut inp, &mut out).is_ok() {
1055 let exif_start = if out.len() > 4 { 4 } else { 0 };
1056 if exif_start < out.len() {
1057 if out[exif_start..].starts_with(b"MM") {
1058 jxl_bo = Some("Big-endian (Motorola, MM)".to_string());
1059 } else if out[exif_start..].starts_with(b"II") {
1060 jxl_bo = Some("Little-endian (Intel, II)".to_string());
1061 }
1062 }
1063 }
1064 break;
1065 }
1066 }
1067 jpos += bsize;
1068 }
1069 if let Some(bo) = jxl_bo {
1070 if !bo.is_empty() && file_type != FileType::Btf {
1071 tags.push(file_tag("ExifByteOrder", Value::String(bo)));
1072 }
1073 }
1074 None
1076 } else if data.starts_with(&[0x00, b'M', b'R', b'M']) {
1077 let mrw_data_offset = if data.len() >= 8 {
1079 u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize + 8
1080 } else { 0 };
1081 let mut mrw_bo: Option<&[u8]> = None;
1082 let mut mpos = 8usize;
1083 while mpos + 8 <= mrw_data_offset.min(data.len()) {
1084 let seg_tag = &data[mpos..mpos+4];
1085 let seg_len = u32::from_be_bytes([data[mpos+4], data[mpos+5], data[mpos+6], data[mpos+7]]) as usize;
1086 if seg_tag == b"\x00TTW" && mpos + 8 + seg_len <= data.len() {
1087 mrw_bo = Some(&data[mpos+8..mpos+8+seg_len]);
1088 break;
1089 }
1090 mpos += 8 + seg_len;
1091 }
1092 mrw_bo
1093 } else {
1094 Some(&data[..])
1095 };
1096 if let Some(tiff) = check {
1097 if tiff.starts_with(b"II") { "Little-endian (Intel, II)" }
1098 else if tiff.starts_with(b"MM") { "Big-endian (Motorola, MM)" }
1099 else { "" }
1100 } else { "" }
1101 } else { "" };
1102 let already_has_exifbyteorder = tags.iter().any(|t| t.name == "ExifByteOrder");
1105 if !bo_str.is_empty() && !already_has_exifbyteorder
1106 && file_type != FileType::Btf
1107 && file_type != FileType::Dr4 && file_type != FileType::Vrd
1108 && file_type != FileType::Crw {
1109 tags.push(file_tag("ExifByteOrder", Value::String(bo_str.to_string())));
1110 }
1111 }
1112
1113 tags.push(file_tag("ExifToolVersion", Value::String(crate::VERSION.to_string())));
1114
1115 let composite = crate::composite::compute_composite_tags(&tags);
1117 tags.extend(composite);
1118
1119 {
1125 let is_flir_fff = tags.iter().any(|t| t.group.family0 == "APP1"
1126 && t.group.family1 == "FLIR");
1127 if is_flir_fff {
1128 tags.retain(|t| !(t.name == "LensID" && t.group.family0 == "Composite"));
1129 }
1130 }
1131
1132 {
1137 let make = tags.iter().find(|t| t.name == "Make")
1138 .map(|t| t.print_value.clone()).unwrap_or_default();
1139 if !make.to_uppercase().contains("CANON") {
1140 tags.retain(|t| t.name != "Lens" || t.group.family0 != "Composite");
1141 }
1142 }
1143
1144 {
1148 let riff_priority_zero_tags = ["Quality", "SampleSize", "StreamType"];
1149 for tag_name in &riff_priority_zero_tags {
1150 let has_makernotes = tags.iter().any(|t| t.name == *tag_name
1151 && t.group.family0 != "RIFF");
1152 if has_makernotes {
1153 tags.retain(|t| !(t.name == *tag_name && t.group.family0 == "RIFF"));
1154 }
1155 }
1156 }
1157
1158 if !self.options.duplicates {
1161 let mut best_priority: HashMap<String, i32> = HashMap::new();
1162 for tag in &tags {
1163 let entry = best_priority.entry(tag.name.clone()).or_insert(tag.priority);
1164 if tag.priority > *entry {
1165 *entry = tag.priority;
1166 }
1167 }
1168 tags.retain(|t| t.priority >= *best_priority.get(&t.name).unwrap_or(&0));
1169 }
1170
1171 if !self.options.requested_tags.is_empty() {
1173 let requested: Vec<String> = self
1174 .options
1175 .requested_tags
1176 .iter()
1177 .map(|t| t.to_lowercase())
1178 .collect();
1179 tags.retain(|t| requested.contains(&t.name.to_lowercase()));
1180 }
1181
1182 Ok(tags)
1183 }
1184
1185 fn get_info(&self, tags: &[Tag]) -> ImageInfo {
1189 let mut info = ImageInfo::new();
1190 let mut seen: HashMap<String, (usize, i32)> = HashMap::new(); for tag in tags {
1193 let value = if self.options.print_conv {
1194 &tag.print_value
1195 } else {
1196 &tag.raw_value.to_display_string()
1197 };
1198
1199 let entry = seen.entry(tag.name.clone()).or_insert((0, i32::MIN));
1200 entry.0 += 1;
1201
1202 if entry.0 == 1 {
1203 entry.1 = tag.priority;
1204 info.insert(tag.name.clone(), value.clone());
1205 } else if tag.priority > entry.1 {
1206 entry.1 = tag.priority;
1208 info.insert(tag.name.clone(), value.clone());
1209 } else if self.options.duplicates {
1210 let key = format!("{} [{}:{}]", tag.name, tag.group.family0, tag.group.family1);
1211 info.insert(key, value.clone());
1212 }
1213 }
1214
1215 info
1216 }
1217
1218 fn detect_file_type(&self, data: &[u8], path: &Path) -> Result<FileType> {
1220 let header_len = data.len().min(256);
1222 if let Some(ft) = file_type::detect_from_magic(&data[..header_len]) {
1223 if ft == FileType::Ico {
1225 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1226 if ext.eq_ignore_ascii_case("dfont") {
1227 return Ok(FileType::Font);
1228 }
1229 }
1230 }
1231 if ft == FileType::Jpeg {
1233 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1234 if ext.eq_ignore_ascii_case("jps") {
1235 return Ok(FileType::Jps);
1236 }
1237 }
1238 }
1239 if ft == FileType::Plist {
1241 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1242 if ext.eq_ignore_ascii_case("aae") {
1243 return Ok(FileType::Aae);
1244 }
1245 }
1246 }
1247 if ft == FileType::Xmp {
1249 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1250 if ext.eq_ignore_ascii_case("plist") {
1251 return Ok(FileType::Plist);
1252 }
1253 if ext.eq_ignore_ascii_case("aae") {
1254 return Ok(FileType::Aae);
1255 }
1256 }
1257 }
1258 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1260 if ext.eq_ignore_ascii_case("pcd") && data.len() >= 2056
1261 && &data[2048..2055] == b"PCD_IPI"
1262 {
1263 return Ok(FileType::PhotoCd);
1264 }
1265 }
1266 if ft == FileType::Mp3 {
1268 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1269 if ext.eq_ignore_ascii_case("mpc") {
1270 return Ok(FileType::Mpc);
1271 }
1272 if ext.eq_ignore_ascii_case("ape") {
1273 return Ok(FileType::Ape);
1274 }
1275 if ext.eq_ignore_ascii_case("wv") {
1276 return Ok(FileType::WavPack);
1277 }
1278 }
1279 }
1280 if ft == FileType::Zip {
1282 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1284 if ext.eq_ignore_ascii_case("eip") {
1285 return Ok(FileType::Eip);
1286 }
1287 }
1288 if let Some(od_type) = detect_opendocument_type(data) {
1289 return Ok(od_type);
1290 }
1291 }
1292 return Ok(ft);
1293 }
1294
1295 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1297 if let Some(ft) = file_type::detect_from_extension(ext) {
1298 return Ok(ft);
1299 }
1300 }
1301
1302 let ext_str = path
1303 .extension()
1304 .and_then(|e| e.to_str())
1305 .unwrap_or("unknown");
1306 Err(Error::UnsupportedFileType(ext_str.to_string()))
1307 }
1308
1309 fn process_file(&self, data: &[u8], file_type: FileType) -> Result<Vec<Tag>> {
1312 match file_type {
1313 FileType::Jpeg | FileType::Jps => formats::jpeg::read_jpeg(data),
1314 FileType::Png | FileType::Mng => formats::png::read_png(data),
1315 FileType::Tiff
1317 | FileType::Btf
1318 | FileType::Dng
1319 | FileType::Cr2
1320 | FileType::Nef
1321 | FileType::Arw
1322 | FileType::Sr2
1323 | FileType::Orf
1324 | FileType::Pef
1325 | FileType::Erf
1326 | FileType::Fff
1327 | FileType::Rwl
1328 | FileType::Mef
1329 | FileType::Srw
1330 | FileType::Gpr
1331 | FileType::Arq
1332 | FileType::ThreeFR
1333 | FileType::Dcr
1334 | FileType::Rw2
1335 | FileType::Srf => formats::tiff::read_tiff(data),
1336 FileType::Iiq => formats::misc::read_iiq(data),
1338 FileType::Gif => formats::gif::read_gif(data),
1340 FileType::Bmp => formats::bmp::read_bmp(data),
1341 FileType::WebP | FileType::Avi | FileType::Wav => formats::riff::read_riff(data),
1342 FileType::Psd => formats::psd::read_psd(data),
1343 FileType::Mp3 => formats::id3::read_mp3(data),
1345 FileType::Flac => formats::flac::read_flac(data),
1346 FileType::Ogg | FileType::Opus => formats::ogg::read_ogg(data),
1347 FileType::Aiff => formats::aiff::read_aiff(data),
1348 FileType::Mp4
1350 | FileType::QuickTime
1351 | FileType::M4a
1352 | FileType::ThreeGP
1353 | FileType::Heif
1354 | FileType::Avif
1355 | FileType::Cr3
1356 | FileType::Crm
1357 | FileType::F4v
1358 | FileType::Mqv
1359 | FileType::Lrv => formats::quicktime::read_quicktime_with_ee(data, self.options.extract_embedded),
1360 FileType::Mkv | FileType::WebM => formats::matroska::read_matroska(data),
1361 FileType::Asf | FileType::Wmv | FileType::Wma => formats::asf::read_asf(data),
1362 FileType::Wtv => formats::wtv::read_wtv(data),
1363 FileType::Crw => formats::canon_raw::read_crw(data),
1365 FileType::Raf => formats::raf::read_raf(data),
1366 FileType::Mrw => formats::mrw::read_mrw(data),
1367 FileType::Mrc => formats::mrc::read_mrc(data),
1368 FileType::Jp2 => formats::jp2::read_jp2(data),
1370 FileType::J2c => formats::jp2::read_j2c(data),
1371 FileType::Jxl => formats::jp2::read_jxl(data),
1372 FileType::Ico => formats::ico::read_ico(data),
1373 FileType::Icc => formats::icc::read_icc(data),
1374 FileType::Pdf => formats::pdf::read_pdf(data),
1376 FileType::PostScript => {
1377 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
1379 formats::font::read_pfa(data).or_else(|_| formats::postscript::read_postscript(data))
1380 } else {
1381 formats::postscript::read_postscript(data)
1382 }
1383 }
1384 FileType::Eip => formats::capture_one::read_eip(data),
1385 FileType::Zip | FileType::Docx | FileType::Xlsx | FileType::Pptx
1386 | FileType::Doc | FileType::Xls | FileType::Ppt => formats::zip::read_zip(data),
1387 FileType::Rtf => formats::rtf::read_rtf(data),
1388 FileType::InDesign => formats::misc::read_indesign(data),
1389 FileType::Pcap => formats::misc::read_pcap(data),
1390 FileType::Pcapng => formats::misc::read_pcapng(data),
1391 FileType::Vrd => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
1393 FileType::Dr4 => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
1394 FileType::Xmp => formats::xmp_file::read_xmp(data),
1396 FileType::Svg => formats::misc::read_svg(data),
1397 FileType::Html => {
1398 let is_svg = data.windows(4).take(512).any(|w| w == b"<svg");
1400 if is_svg {
1401 formats::misc::read_svg(data)
1402 } else {
1403 formats::html::read_html(data)
1404 }
1405 }
1406 FileType::Exe => formats::exe::read_exe(data),
1407 FileType::Font => {
1408 if data.starts_with(b"StartFontMetrics") {
1410 return formats::font::read_afm(data);
1411 }
1412 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
1414 return formats::font::read_pfa(data).or_else(|_| Ok(Vec::new()));
1415 }
1416 if data.len() >= 2 && data[0] == 0x80 && (data[1] == 0x01 || data[1] == 0x02) {
1418 return formats::font::read_pfb(data).or_else(|_| Ok(Vec::new()));
1419 }
1420 formats::font::read_font(data)
1421 }
1422 FileType::WavPack | FileType::Dsf => formats::id3::read_mp3(data),
1424 FileType::Ape => formats::ape::read_ape(data),
1425 FileType::Mpc => formats::ape::read_mpc(data),
1426 FileType::Aac => formats::misc::read_aac(data),
1427 FileType::RealAudio => {
1428 formats::misc::read_real_audio(data).or_else(|_| Ok(Vec::new()))
1429 }
1430 FileType::RealMedia => {
1431 formats::misc::read_real_media(data).or_else(|_| Ok(Vec::new()))
1432 }
1433 FileType::Czi => formats::misc::read_czi(data).or_else(|_| Ok(Vec::new())),
1435 FileType::PhotoCd => formats::misc::read_photo_cd(data).or_else(|_| Ok(Vec::new())),
1436 FileType::Dicom => formats::dicom::read_dicom(data),
1437 FileType::Fits => formats::misc::read_fits(data),
1438 FileType::Flv => formats::misc::read_flv(data),
1439 FileType::Mxf => formats::misc::read_mxf(data).or_else(|_| Ok(Vec::new())),
1440 FileType::Swf => formats::misc::read_swf(data),
1441 FileType::Hdr => formats::misc::read_hdr(data),
1442 FileType::DjVu => formats::djvu::read_djvu(data),
1443 FileType::Xcf => formats::gimp::read_xcf(data),
1444 FileType::Mie => formats::mie::read_mie(data),
1445 FileType::Lfp => formats::lytro::read_lfp(data),
1446 FileType::Fpf => formats::flir_fpf::read_fpf(data),
1448 FileType::Flif => formats::misc::read_flif(data),
1449 FileType::Bpg => formats::misc::read_bpg(data),
1450 FileType::Pcx => formats::misc::read_pcx(data),
1451 FileType::Pict => formats::misc::read_pict(data),
1452 FileType::Mpeg => formats::mpeg::read_mpeg(data),
1453 FileType::M2ts => formats::misc::read_m2ts(data, self.options.extract_embedded),
1454 FileType::Gzip => formats::misc::read_gzip(data),
1455 FileType::Rar => formats::misc::read_rar(data),
1456 FileType::SevenZ => formats::misc::read_7z(data),
1457 FileType::Dss => formats::misc::read_dss(data),
1458 FileType::Moi => formats::misc::read_moi(data),
1459 FileType::MacOs => formats::misc::read_macos(data),
1460 FileType::Json => formats::misc::read_json(data),
1461 FileType::Pgf => formats::pgf::read_pgf(data),
1463 FileType::Xisf => formats::xisf::read_xisf(data),
1464 FileType::Torrent => formats::torrent::read_torrent(data),
1465 FileType::Mobi => formats::palm::read_palm(data),
1466 FileType::Psp => formats::psp::read_psp(data),
1467 FileType::SonyPmp => formats::sony_pmp::read_sony_pmp(data),
1468 FileType::Audible => formats::audible::read_audible(data),
1469 FileType::Exr => formats::openexr::read_openexr(data),
1470 FileType::Plist => {
1472 if data.starts_with(b"bplist") {
1473 formats::plist::read_binary_plist_tags(data)
1474 } else {
1475 formats::plist::read_xml_plist(data)
1476 }
1477 }
1478 FileType::Aae => {
1479 if data.starts_with(b"bplist") {
1480 formats::plist::read_binary_plist_tags(data)
1481 } else {
1482 formats::plist::read_aae_plist(data)
1483 }
1484 }
1485 FileType::KyoceraRaw => formats::misc::read_kyocera_raw(data),
1486 FileType::PortableFloatMap => formats::misc::read_pfm(data),
1487 FileType::Ods | FileType::Odt | FileType::Odp | FileType::Odg |
1488 FileType::Odf | FileType::Odb | FileType::Odi | FileType::Odc => formats::zip::read_zip(data),
1489 FileType::Lif => formats::misc::read_lif(data),
1490 FileType::Rwz => formats::misc::read_rawzor(data),
1491 FileType::Jxr => formats::misc::read_jxr(data),
1492 _ => Err(Error::UnsupportedFileType(format!("{}", file_type))),
1493 }
1494 }
1495
1496 fn process_by_extension(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
1498 let ext = path
1499 .extension()
1500 .and_then(|e| e.to_str())
1501 .unwrap_or("")
1502 .to_ascii_lowercase();
1503
1504 match ext.as_str() {
1505 "ppm" | "pgm" | "pbm" => formats::misc::read_ppm(data),
1506 "pfm" => {
1507 if data.len() >= 3 && data[0] == b'P' && (data[1] == b'f' || data[1] == b'F') {
1509 formats::misc::read_ppm(data)
1510 } else {
1511 Ok(Vec::new()) }
1513 }
1514 "json" => formats::misc::read_json(data),
1515 "svg" => formats::misc::read_svg(data),
1516 "ram" => formats::misc::read_ram(data).or_else(|_| Ok(Vec::new())),
1517 "txt" | "log" | "igc" => {
1518 Ok(compute_text_tags(data, false))
1519 }
1520 "csv" => {
1521 Ok(compute_text_tags(data, true))
1522 }
1523 "url" => formats::lnk::read_url(data).or_else(|_| Ok(Vec::new())),
1524 "lnk" => formats::lnk::read_lnk(data).or_else(|_| Ok(Vec::new())),
1525 "gpx" | "kml" | "xml" | "inx" => formats::xmp_file::read_xmp(data),
1526 "plist" => {
1527 if data.starts_with(b"bplist") {
1528 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
1529 } else {
1530 formats::plist::read_xml_plist(data).or_else(|_| Ok(Vec::new()))
1531 }
1532 }
1533 "aae" => {
1534 if data.starts_with(b"bplist") {
1535 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
1536 } else {
1537 formats::plist::read_aae_plist(data).or_else(|_| Ok(Vec::new()))
1538 }
1539 }
1540 "vcf" | "ics" | "vcard" => {
1541 let s = String::from_utf8_lossy(&data[..data.len().min(100)]);
1542 if s.contains("BEGIN:VCALENDAR") {
1543 formats::vcard::read_ics(data).or_else(|_| Ok(Vec::new()))
1544 } else {
1545 formats::vcard::read_vcf(data).or_else(|_| Ok(Vec::new()))
1546 }
1547 }
1548 "xcf" => Ok(Vec::new()), "vrd" => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
1550 "dr4" => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
1551 "indd" | "indt" => Ok(Vec::new()), "x3f" => formats::sigma_raw::read_x3f(data).or_else(|_| Ok(Vec::new())),
1553 "mie" => Ok(Vec::new()), "exr" => Ok(Vec::new()), "wpg" => formats::misc::read_wpg(data).or_else(|_| Ok(Vec::new())),
1556 "moi" => formats::misc::read_moi(data).or_else(|_| Ok(Vec::new())),
1557 "macos" => formats::misc::read_macos(data).or_else(|_| Ok(Vec::new())),
1558 "dpx" => formats::dpx::read_dpx(data).or_else(|_| Ok(Vec::new())),
1559 "r3d" => formats::red::read_r3d(data).or_else(|_| Ok(Vec::new())),
1560 "tnef" => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
1561 "ppt" | "fpx" => formats::flashpix::read_fpx(data).or_else(|_| Ok(Vec::new())),
1562 "fpf" => formats::flir_fpf::read_fpf(data).or_else(|_| Ok(Vec::new())),
1563 "itc" => formats::misc::read_itc(data).or_else(|_| Ok(Vec::new())),
1564 "mpg" | "mpeg" | "m1v" | "m2v" | "mpv" => formats::mpeg::read_mpeg(data).or_else(|_| Ok(Vec::new())),
1565 "dv" => formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new())),
1566 "czi" => formats::misc::read_czi(data).or_else(|_| Ok(Vec::new())),
1567 "miff" => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
1568 "lfp" | "mrc"
1569 | "dss" | "mobi" | "psp" | "pgf" | "raw"
1570 | "pmp" | "torrent"
1571 | "xisf" | "mxf"
1572 | "dfont" => Ok(Vec::new()),
1573 "iso" => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
1574 "afm" => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
1575 "pfa" => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
1576 "pfb" => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
1577 _ => Err(Error::UnsupportedFileType(ext)),
1578 }
1579 }
1580}
1581
1582impl Default for ExifTool {
1583 fn default() -> Self {
1584 Self::new()
1585 }
1586}
1587
1588fn detect_opendocument_type(data: &[u8]) -> Option<FileType> {
1591 if data.len() < 30 || data[0..4] != [0x50, 0x4B, 0x03, 0x04] {
1593 return None;
1594 }
1595 let compression = u16::from_le_bytes([data[8], data[9]]);
1596 let compressed_size = u32::from_le_bytes([data[18], data[19], data[20], data[21]]) as usize;
1597 let name_len = u16::from_le_bytes([data[26], data[27]]) as usize;
1598 let extra_len = u16::from_le_bytes([data[28], data[29]]) as usize;
1599 let name_start = 30;
1600 if name_start + name_len > data.len() {
1601 return None;
1602 }
1603 let filename = std::str::from_utf8(&data[name_start..name_start + name_len]).unwrap_or("");
1604 if filename != "mimetype" || compression != 0 {
1605 return None;
1606 }
1607 let content_start = name_start + name_len + extra_len;
1608 let content_end = (content_start + compressed_size).min(data.len());
1609 if content_start >= content_end {
1610 return None;
1611 }
1612 let mime = std::str::from_utf8(&data[content_start..content_end]).unwrap_or("").trim();
1613 match mime {
1614 "application/vnd.oasis.opendocument.spreadsheet" => Some(FileType::Ods),
1615 "application/vnd.oasis.opendocument.text" => Some(FileType::Odt),
1616 "application/vnd.oasis.opendocument.presentation" => Some(FileType::Odp),
1617 "application/vnd.oasis.opendocument.graphics" => Some(FileType::Odg),
1618 "application/vnd.oasis.opendocument.formula" => Some(FileType::Odf),
1619 "application/vnd.oasis.opendocument.database" => Some(FileType::Odb),
1620 "application/vnd.oasis.opendocument.image" => Some(FileType::Odi),
1621 "application/vnd.oasis.opendocument.chart" => Some(FileType::Odc),
1622 _ => None,
1623 }
1624}
1625
1626pub fn get_file_type<P: AsRef<Path>>(path: P) -> Result<FileType> {
1628 let path = path.as_ref();
1629 let mut file = fs::File::open(path).map_err(Error::Io)?;
1630 let mut header = [0u8; 256];
1631 use std::io::Read;
1632 let n = file.read(&mut header).map_err(Error::Io)?;
1633
1634 if let Some(ft) = file_type::detect_from_magic(&header[..n]) {
1635 return Ok(ft);
1636 }
1637
1638 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1639 if let Some(ft) = file_type::detect_from_extension(ext) {
1640 return Ok(ft);
1641 }
1642 }
1643
1644 Err(Error::UnsupportedFileType("unknown".into()))
1645}
1646
1647enum ExifIfdGroup {
1649 Ifd0,
1650 ExifIfd,
1651 Gps,
1652}
1653
1654fn classify_exif_tag(tag_id: u16) -> ExifIfdGroup {
1656 match tag_id {
1657 0x829A..=0x829D | 0x8822..=0x8827 | 0x8830 | 0x9000..=0x9292
1659 | 0xA000..=0xA435 => ExifIfdGroup::ExifIfd,
1660 0x0000..=0x001F if tag_id <= 0x001F => ExifIfdGroup::Gps,
1662 _ => ExifIfdGroup::Ifd0,
1664 }
1665}
1666
1667fn extract_existing_exif_entries(jpeg_data: &[u8], target_bo: ByteOrderMark) -> Vec<exif_writer::IfdEntry> {
1669 let mut entries = Vec::new();
1670
1671 let mut pos = 2; while pos + 4 <= jpeg_data.len() {
1674 if jpeg_data[pos] != 0xFF {
1675 pos += 1;
1676 continue;
1677 }
1678 let marker = jpeg_data[pos + 1];
1679 pos += 2;
1680
1681 if marker == 0xDA || marker == 0xD9 {
1682 break; }
1684 if marker == 0xFF || marker == 0x00 || marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
1685 continue;
1686 }
1687
1688 if pos + 2 > jpeg_data.len() {
1689 break;
1690 }
1691 let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
1692 if seg_len < 2 || pos + seg_len > jpeg_data.len() {
1693 break;
1694 }
1695
1696 let seg_data = &jpeg_data[pos + 2..pos + seg_len];
1697
1698 if marker == 0xE1 && seg_data.len() > 14 && seg_data.starts_with(b"Exif\0\0") {
1700 let tiff_data = &seg_data[6..];
1701 extract_ifd_entries(tiff_data, target_bo, &mut entries);
1702 break;
1703 }
1704
1705 pos += seg_len;
1706 }
1707
1708 entries
1709}
1710
1711fn extract_ifd_entries(
1713 tiff_data: &[u8],
1714 target_bo: ByteOrderMark,
1715 entries: &mut Vec<exif_writer::IfdEntry>,
1716) {
1717 use crate::metadata::exif::parse_tiff_header;
1718
1719 let header = match parse_tiff_header(tiff_data) {
1720 Ok(h) => h,
1721 Err(_) => return,
1722 };
1723
1724 let src_bo = header.byte_order;
1725
1726 read_ifd_for_merge(tiff_data, header.ifd0_offset as usize, src_bo, target_bo, entries);
1728
1729 let ifd0_offset = header.ifd0_offset as usize;
1731 if ifd0_offset + 2 > tiff_data.len() {
1732 return;
1733 }
1734 let count = read_u16_bo(tiff_data, ifd0_offset, src_bo) as usize;
1735 for i in 0..count {
1736 let eoff = ifd0_offset + 2 + i * 12;
1737 if eoff + 12 > tiff_data.len() {
1738 break;
1739 }
1740 let tag = read_u16_bo(tiff_data, eoff, src_bo);
1741 let value_off = read_u32_bo(tiff_data, eoff + 8, src_bo) as usize;
1742
1743 match tag {
1744 0x8769 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
1745 0x8825 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
1746 _ => {}
1747 }
1748 }
1749}
1750
1751fn read_ifd_for_merge(
1753 data: &[u8],
1754 offset: usize,
1755 src_bo: ByteOrderMark,
1756 target_bo: ByteOrderMark,
1757 entries: &mut Vec<exif_writer::IfdEntry>,
1758) {
1759 if offset + 2 > data.len() {
1760 return;
1761 }
1762 let count = read_u16_bo(data, offset, src_bo) as usize;
1763
1764 for i in 0..count {
1765 let eoff = offset + 2 + i * 12;
1766 if eoff + 12 > data.len() {
1767 break;
1768 }
1769
1770 let tag = read_u16_bo(data, eoff, src_bo);
1771 let dtype = read_u16_bo(data, eoff + 2, src_bo);
1772 let count_val = read_u32_bo(data, eoff + 4, src_bo);
1773
1774 if tag == 0x8769 || tag == 0x8825 || tag == 0xA005 || tag == 0x927C {
1776 continue;
1777 }
1778
1779 let type_size = match dtype {
1780 1 | 2 | 6 | 7 => 1usize,
1781 3 | 8 => 2,
1782 4 | 9 | 11 | 13 => 4,
1783 5 | 10 | 12 => 8,
1784 _ => continue,
1785 };
1786
1787 let total_size = type_size * count_val as usize;
1788 let raw_data = if total_size <= 4 {
1789 data[eoff + 8..eoff + 12].to_vec()
1790 } else {
1791 let voff = read_u32_bo(data, eoff + 8, src_bo) as usize;
1792 if voff + total_size > data.len() {
1793 continue;
1794 }
1795 data[voff..voff + total_size].to_vec()
1796 };
1797
1798 let final_data = if src_bo != target_bo && type_size > 1 {
1800 reencode_bytes(&raw_data, dtype, count_val as usize, src_bo, target_bo)
1801 } else {
1802 raw_data[..total_size].to_vec()
1803 };
1804
1805 let format = match dtype {
1806 1 => exif_writer::ExifFormat::Byte,
1807 2 => exif_writer::ExifFormat::Ascii,
1808 3 => exif_writer::ExifFormat::Short,
1809 4 => exif_writer::ExifFormat::Long,
1810 5 => exif_writer::ExifFormat::Rational,
1811 6 => exif_writer::ExifFormat::SByte,
1812 7 => exif_writer::ExifFormat::Undefined,
1813 8 => exif_writer::ExifFormat::SShort,
1814 9 => exif_writer::ExifFormat::SLong,
1815 10 => exif_writer::ExifFormat::SRational,
1816 11 => exif_writer::ExifFormat::Float,
1817 12 => exif_writer::ExifFormat::Double,
1818 _ => continue,
1819 };
1820
1821 entries.push(exif_writer::IfdEntry {
1822 tag,
1823 format,
1824 data: final_data,
1825 });
1826 }
1827}
1828
1829fn reencode_bytes(
1831 data: &[u8],
1832 dtype: u16,
1833 count: usize,
1834 src_bo: ByteOrderMark,
1835 dst_bo: ByteOrderMark,
1836) -> Vec<u8> {
1837 let mut out = Vec::with_capacity(data.len());
1838 match dtype {
1839 3 | 8 => {
1840 for i in 0..count {
1842 let v = read_u16_bo(data, i * 2, src_bo);
1843 match dst_bo {
1844 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
1845 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
1846 }
1847 }
1848 }
1849 4 | 9 | 11 | 13 => {
1850 for i in 0..count {
1852 let v = read_u32_bo(data, i * 4, src_bo);
1853 match dst_bo {
1854 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
1855 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
1856 }
1857 }
1858 }
1859 5 | 10 => {
1860 for i in 0..count {
1862 let n = read_u32_bo(data, i * 8, src_bo);
1863 let d = read_u32_bo(data, i * 8 + 4, src_bo);
1864 match dst_bo {
1865 ByteOrderMark::LittleEndian => {
1866 out.extend_from_slice(&n.to_le_bytes());
1867 out.extend_from_slice(&d.to_le_bytes());
1868 }
1869 ByteOrderMark::BigEndian => {
1870 out.extend_from_slice(&n.to_be_bytes());
1871 out.extend_from_slice(&d.to_be_bytes());
1872 }
1873 }
1874 }
1875 }
1876 12 => {
1877 for i in 0..count {
1879 let mut bytes = [0u8; 8];
1880 bytes.copy_from_slice(&data[i * 8..i * 8 + 8]);
1881 if src_bo != dst_bo {
1882 bytes.reverse();
1883 }
1884 out.extend_from_slice(&bytes);
1885 }
1886 }
1887 _ => out.extend_from_slice(data),
1888 }
1889 out
1890}
1891
1892fn read_u16_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1893 if offset + 2 > data.len() { return 0; }
1894 match bo {
1895 ByteOrderMark::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
1896 ByteOrderMark::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
1897 }
1898}
1899
1900fn read_u32_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1901 if offset + 4 > data.len() { return 0; }
1902 match bo {
1903 ByteOrderMark::LittleEndian => u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]),
1904 ByteOrderMark::BigEndian => u32::from_be_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]),
1905 }
1906}
1907
1908fn tag_name_to_id(name: &str) -> Option<u16> {
1910 encode_exif_tag(name, "", "", ByteOrderMark::BigEndian).map(|(id, _, _)| id)
1911}
1912
1913fn value_to_filename(value: &str) -> String {
1915 value
1916 .chars()
1917 .map(|c| match c {
1918 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1919 c if c.is_control() => '_',
1920 c => c,
1921 })
1922 .collect::<String>()
1923 .trim()
1924 .to_string()
1925}
1926
1927pub fn parse_date_shift(shift: &str) -> Option<(i32, u32, u32, u32)> {
1930 let (sign, rest) = if shift.starts_with('-') {
1931 (-1, &shift[1..])
1932 } else if shift.starts_with('+') {
1933 (1, &shift[1..])
1934 } else {
1935 (1, shift)
1936 };
1937
1938 let parts: Vec<&str> = rest.split(':').collect();
1939 match parts.len() {
1940 1 => {
1941 let h: u32 = parts[0].parse().ok()?;
1942 Some((sign, h, 0, 0))
1943 }
1944 2 => {
1945 let h: u32 = parts[0].parse().ok()?;
1946 let m: u32 = parts[1].parse().ok()?;
1947 Some((sign, h, m, 0))
1948 }
1949 3 => {
1950 let h: u32 = parts[0].parse().ok()?;
1951 let m: u32 = parts[1].parse().ok()?;
1952 let s: u32 = parts[2].parse().ok()?;
1953 Some((sign, h, m, s))
1954 }
1955 _ => None,
1956 }
1957}
1958
1959pub fn shift_datetime(datetime: &str, shift: &str) -> Option<String> {
1962 let (sign, hours, minutes, seconds) = parse_date_shift(shift)?;
1963
1964 if datetime.len() < 19 {
1966 return None;
1967 }
1968 let year: i32 = datetime[0..4].parse().ok()?;
1969 let month: u32 = datetime[5..7].parse().ok()?;
1970 let day: u32 = datetime[8..10].parse().ok()?;
1971 let hour: u32 = datetime[11..13].parse().ok()?;
1972 let min: u32 = datetime[14..16].parse().ok()?;
1973 let sec: u32 = datetime[17..19].parse().ok()?;
1974
1975 let total_secs = (hour * 3600 + min * 60 + sec) as i64
1977 + sign as i64 * (hours * 3600 + minutes * 60 + seconds) as i64;
1978
1979 let days_shift = if total_secs < 0 {
1980 -1 - (-total_secs - 1) as i64 / 86400
1981 } else {
1982 total_secs / 86400
1983 };
1984
1985 let time_secs = ((total_secs % 86400) + 86400) % 86400;
1986 let new_hour = (time_secs / 3600) as u32;
1987 let new_min = ((time_secs % 3600) / 60) as u32;
1988 let new_sec = (time_secs % 60) as u32;
1989
1990 let mut new_day = day as i32 + days_shift as i32;
1992 let mut new_month = month;
1993 let mut new_year = year;
1994
1995 let days_in_month = |m: u32, y: i32| -> i32 {
1996 match m {
1997 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1998 4 | 6 | 9 | 11 => 30,
1999 2 => if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 29 } else { 28 },
2000 _ => 30,
2001 }
2002 };
2003
2004 while new_day > days_in_month(new_month, new_year) {
2005 new_day -= days_in_month(new_month, new_year);
2006 new_month += 1;
2007 if new_month > 12 {
2008 new_month = 1;
2009 new_year += 1;
2010 }
2011 }
2012 while new_day < 1 {
2013 new_month = if new_month == 1 { 12 } else { new_month - 1 };
2014 if new_month == 12 {
2015 new_year -= 1;
2016 }
2017 new_day += days_in_month(new_month, new_year);
2018 }
2019
2020 Some(format!(
2021 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
2022 new_year, new_month, new_day, new_hour, new_min, new_sec
2023 ))
2024}
2025
2026fn unix_to_datetime(secs: i64) -> String {
2027 let days = secs / 86400;
2028 let time = secs % 86400;
2029 let h = time / 3600;
2030 let m = (time % 3600) / 60;
2031 let s = time % 60;
2032 let mut y = 1970i32;
2033 let mut rem = days;
2034 loop {
2035 let dy = if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 366 } else { 365 };
2036 if rem < dy { break; }
2037 rem -= dy;
2038 y += 1;
2039 }
2040 let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2041 let months = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
2042 let mut mo = 1;
2043 for &dm in &months {
2044 if rem < dm { break; }
2045 rem -= dm;
2046 mo += 1;
2047 }
2048 format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", y, mo, rem + 1, h, m, s)
2049}
2050
2051fn format_file_size(bytes: u64) -> String {
2052 if bytes < 1024 {
2053 format!("{} bytes", bytes)
2054 } else if bytes < 1024 * 1024 {
2055 format!("{:.1} kB", bytes as f64 / 1024.0)
2056 } else if bytes < 1024 * 1024 * 1024 {
2057 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
2058 } else {
2059 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
2060 }
2061}
2062
2063fn is_xmp_tag(tag: &str) -> bool {
2065 matches!(
2066 tag.to_lowercase().as_str(),
2067 "title" | "description" | "subject" | "creator" | "rights"
2068 | "keywords" | "rating" | "label" | "hierarchicalsubject"
2069 )
2070}
2071
2072fn encode_exif_tag(
2075 tag_name: &str,
2076 value: &str,
2077 _group: &str,
2078 bo: ByteOrderMark,
2079) -> Option<(u16, exif_writer::ExifFormat, Vec<u8>)> {
2080 let tag_lower = tag_name.to_lowercase();
2081
2082 let (tag_id, format): (u16, exif_writer::ExifFormat) = match tag_lower.as_str() {
2084 "imagedescription" => (0x010E, exif_writer::ExifFormat::Ascii),
2086 "make" => (0x010F, exif_writer::ExifFormat::Ascii),
2087 "model" => (0x0110, exif_writer::ExifFormat::Ascii),
2088 "software" => (0x0131, exif_writer::ExifFormat::Ascii),
2089 "modifydate" | "datetime" => (0x0132, exif_writer::ExifFormat::Ascii),
2090 "artist" => (0x013B, exif_writer::ExifFormat::Ascii),
2091 "copyright" => (0x8298, exif_writer::ExifFormat::Ascii),
2092 "orientation" => (0x0112, exif_writer::ExifFormat::Short),
2094 "xresolution" => (0x011A, exif_writer::ExifFormat::Rational),
2095 "yresolution" => (0x011B, exif_writer::ExifFormat::Rational),
2096 "resolutionunit" => (0x0128, exif_writer::ExifFormat::Short),
2097 "datetimeoriginal" => (0x9003, exif_writer::ExifFormat::Ascii),
2099 "createdate" | "datetimedigitized" => (0x9004, exif_writer::ExifFormat::Ascii),
2100 "usercomment" => (0x9286, exif_writer::ExifFormat::Undefined),
2101 "imageuniqueid" => (0xA420, exif_writer::ExifFormat::Ascii),
2102 "ownername" | "cameraownername" => (0xA430, exif_writer::ExifFormat::Ascii),
2103 "serialnumber" | "bodyserialnumber" => (0xA431, exif_writer::ExifFormat::Ascii),
2104 "lensmake" => (0xA433, exif_writer::ExifFormat::Ascii),
2105 "lensmodel" => (0xA434, exif_writer::ExifFormat::Ascii),
2106 "lensserialnumber" => (0xA435, exif_writer::ExifFormat::Ascii),
2107 _ => return None,
2108 };
2109
2110 let encoded = match format {
2111 exif_writer::ExifFormat::Ascii => exif_writer::encode_ascii(value),
2112 exif_writer::ExifFormat::Short => {
2113 let v: u16 = value.parse().ok()?;
2114 exif_writer::encode_u16(v, bo)
2115 }
2116 exif_writer::ExifFormat::Long => {
2117 let v: u32 = value.parse().ok()?;
2118 exif_writer::encode_u32(v, bo)
2119 }
2120 exif_writer::ExifFormat::Rational => {
2121 if let Some(slash) = value.find('/') {
2123 let num: u32 = value[..slash].trim().parse().ok()?;
2124 let den: u32 = value[slash + 1..].trim().parse().ok()?;
2125 exif_writer::encode_urational(num, den, bo)
2126 } else if let Ok(v) = value.parse::<f64>() {
2127 let den = 10000u32;
2129 let num = (v * den as f64).round() as u32;
2130 exif_writer::encode_urational(num, den, bo)
2131 } else {
2132 return None;
2133 }
2134 }
2135 exif_writer::ExifFormat::Undefined => {
2136 let mut data = vec![0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00]; data.extend_from_slice(value.as_bytes());
2139 data
2140 }
2141 _ => return None,
2142 };
2143
2144 Some((tag_id, format, encoded))
2145}
2146
2147fn compute_text_tags(data: &[u8], is_csv: bool) -> Vec<Tag> {
2149 let mut tags = Vec::new();
2150 let mk = |name: &str, val: String| Tag {
2151 id: crate::tag::TagId::Text(name.into()),
2152 name: name.into(), description: name.into(),
2153 group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Other".into() },
2154 raw_value: Value::String(val.clone()), print_value: val, priority: 0,
2155 };
2156
2157 let is_ascii = data.iter().all(|&b| b < 128);
2159 let has_utf8_bom = data.starts_with(&[0xEF, 0xBB, 0xBF]);
2160 let has_utf16le_bom = data.starts_with(&[0xFF, 0xFE]) && !data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
2161 let has_utf16be_bom = data.starts_with(&[0xFE, 0xFF]);
2162 let has_utf32le_bom = data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
2163 let has_utf32be_bom = data.starts_with(&[0x00, 0x00, 0xFE, 0xFF]);
2164
2165 let has_weird_ctrl = data.iter().any(|&b| (b <= 0x06) || (b >= 0x0e && b <= 0x1a) || (b >= 0x1c && b <= 0x1f) || b == 0x7f);
2167
2168 let (encoding, is_bom, is_utf16) = if has_utf32le_bom {
2169 ("utf-32le", true, false)
2170 } else if has_utf32be_bom {
2171 ("utf-32be", true, false)
2172 } else if has_utf16le_bom {
2173 ("utf-16le", true, true)
2174 } else if has_utf16be_bom {
2175 ("utf-16be", true, true)
2176 } else if has_weird_ctrl {
2177 return tags;
2179 } else if is_ascii {
2180 ("us-ascii", false, false)
2181 } else {
2182 let is_valid_utf8 = std::str::from_utf8(data).is_ok();
2184 if is_valid_utf8 {
2185 if has_utf8_bom {
2186 ("utf-8", true, false)
2187 } else {
2188 ("utf-8", false, false)
2192 }
2193 } else if !data.iter().any(|&b| b >= 0x80 && b <= 0x9f) {
2194 ("iso-8859-1", false, false)
2195 } else {
2196 ("unknown-8bit", false, false)
2197 }
2198 };
2199
2200 tags.push(mk("MIMEEncoding", encoding.into()));
2201
2202 if is_bom {
2203 tags.push(mk("ByteOrderMark", "Yes".into()));
2204 }
2205
2206 let has_cr = data.contains(&b'\r');
2208 let has_lf = data.contains(&b'\n');
2209 let newline_type = if has_cr && has_lf { "Windows CRLF" }
2210 else if has_lf { "Unix LF" }
2211 else if has_cr { "Macintosh CR" }
2212 else { "(none)" };
2213 tags.push(mk("Newlines", newline_type.into()));
2214
2215 if is_csv {
2216 let text = String::from_utf8_lossy(data);
2218 let mut delim = "";
2219 let mut quot = "";
2220 let mut ncols = 1usize;
2221 let mut nrows = 0usize;
2222
2223 for line in text.lines() {
2224 if nrows == 0 {
2225 let comma_count = line.matches(',').count();
2227 let semi_count = line.matches(';').count();
2228 let tab_count = line.matches('\t').count();
2229 if comma_count > semi_count && comma_count > tab_count {
2230 delim = ",";
2231 ncols = comma_count + 1;
2232 } else if semi_count > tab_count {
2233 delim = ";";
2234 ncols = semi_count + 1;
2235 } else if tab_count > 0 {
2236 delim = "\t";
2237 ncols = tab_count + 1;
2238 } else {
2239 delim = "";
2240 ncols = 1;
2241 }
2242 if line.contains('"') { quot = "\""; }
2244 else if line.contains('\'') { quot = "'"; }
2245 }
2246 nrows += 1;
2247 if nrows >= 1000 { break; }
2248 }
2249
2250 let delim_display = match delim {
2251 "," => "Comma",
2252 ";" => "Semicolon",
2253 "\t" => "Tab",
2254 _ => "(none)",
2255 };
2256 let quot_display = match quot {
2257 "\"" => "Double quotes",
2258 "'" => "Single quotes",
2259 _ => "(none)",
2260 };
2261
2262 tags.push(mk("Delimiter", delim_display.into()));
2263 tags.push(mk("Quoting", quot_display.into()));
2264 tags.push(mk("ColumnCount", ncols.to_string()));
2265 if nrows > 0 {
2266 tags.push(mk("RowCount", nrows.to_string()));
2267 }
2268 } else if !is_utf16 {
2269 let line_count = data.iter().filter(|&&b| b == b'\n').count();
2271 let line_count = if line_count == 0 && !data.is_empty() { 1 } else { line_count };
2272 tags.push(mk("LineCount", line_count.to_string()));
2273
2274 let text = String::from_utf8_lossy(data);
2275 let word_count = text.split_whitespace().count();
2276 tags.push(mk("WordCount", word_count.to_string()));
2277 }
2278
2279 tags
2280}