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.requested_tags.is_empty() {
1160 let requested: Vec<String> = self
1161 .options
1162 .requested_tags
1163 .iter()
1164 .map(|t| t.to_lowercase())
1165 .collect();
1166 tags.retain(|t| requested.contains(&t.name.to_lowercase()));
1167 }
1168
1169 Ok(tags)
1170 }
1171
1172 fn get_info(&self, tags: &[Tag]) -> ImageInfo {
1176 let mut info = ImageInfo::new();
1177 let mut seen: HashMap<String, usize> = HashMap::new();
1178
1179 for tag in tags {
1180 let value = if self.options.print_conv {
1181 &tag.print_value
1182 } else {
1183 &tag.raw_value.to_display_string()
1184 };
1185
1186 let count = seen.entry(tag.name.clone()).or_insert(0);
1187 *count += 1;
1188
1189 if *count == 1 {
1190 info.insert(tag.name.clone(), value.clone());
1191 } else if self.options.duplicates {
1192 let key = format!("{} [{}:{}]", tag.name, tag.group.family0, tag.group.family1);
1193 info.insert(key, value.clone());
1194 }
1195 }
1196
1197 info
1198 }
1199
1200 fn detect_file_type(&self, data: &[u8], path: &Path) -> Result<FileType> {
1202 let header_len = data.len().min(256);
1204 if let Some(ft) = file_type::detect_from_magic(&data[..header_len]) {
1205 if ft == FileType::Ico {
1207 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1208 if ext.eq_ignore_ascii_case("dfont") {
1209 return Ok(FileType::Font);
1210 }
1211 }
1212 }
1213 if ft == FileType::Jpeg {
1215 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1216 if ext.eq_ignore_ascii_case("jps") {
1217 return Ok(FileType::Jps);
1218 }
1219 }
1220 }
1221 if ft == FileType::Plist {
1223 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1224 if ext.eq_ignore_ascii_case("aae") {
1225 return Ok(FileType::Aae);
1226 }
1227 }
1228 }
1229 if ft == FileType::Xmp {
1231 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1232 if ext.eq_ignore_ascii_case("plist") {
1233 return Ok(FileType::Plist);
1234 }
1235 if ext.eq_ignore_ascii_case("aae") {
1236 return Ok(FileType::Aae);
1237 }
1238 }
1239 }
1240 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1242 if ext.eq_ignore_ascii_case("pcd") && data.len() >= 2056
1243 && &data[2048..2055] == b"PCD_IPI"
1244 {
1245 return Ok(FileType::PhotoCd);
1246 }
1247 }
1248 if ft == FileType::Mp3 {
1250 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1251 if ext.eq_ignore_ascii_case("mpc") {
1252 return Ok(FileType::Mpc);
1253 }
1254 if ext.eq_ignore_ascii_case("ape") {
1255 return Ok(FileType::Ape);
1256 }
1257 if ext.eq_ignore_ascii_case("wv") {
1258 return Ok(FileType::WavPack);
1259 }
1260 }
1261 }
1262 if ft == FileType::Zip {
1264 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1266 if ext.eq_ignore_ascii_case("eip") {
1267 return Ok(FileType::Eip);
1268 }
1269 }
1270 if let Some(od_type) = detect_opendocument_type(data) {
1271 return Ok(od_type);
1272 }
1273 }
1274 return Ok(ft);
1275 }
1276
1277 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1279 if let Some(ft) = file_type::detect_from_extension(ext) {
1280 return Ok(ft);
1281 }
1282 }
1283
1284 let ext_str = path
1285 .extension()
1286 .and_then(|e| e.to_str())
1287 .unwrap_or("unknown");
1288 Err(Error::UnsupportedFileType(ext_str.to_string()))
1289 }
1290
1291 fn process_file(&self, data: &[u8], file_type: FileType) -> Result<Vec<Tag>> {
1294 match file_type {
1295 FileType::Jpeg | FileType::Jps => formats::jpeg::read_jpeg(data),
1296 FileType::Png | FileType::Mng => formats::png::read_png(data),
1297 FileType::Tiff
1299 | FileType::Btf
1300 | FileType::Dng
1301 | FileType::Cr2
1302 | FileType::Nef
1303 | FileType::Arw
1304 | FileType::Sr2
1305 | FileType::Orf
1306 | FileType::Pef
1307 | FileType::Erf
1308 | FileType::Fff
1309 | FileType::Rwl
1310 | FileType::Mef
1311 | FileType::Srw
1312 | FileType::Gpr
1313 | FileType::Arq
1314 | FileType::ThreeFR
1315 | FileType::Dcr
1316 | FileType::Rw2
1317 | FileType::Srf => formats::tiff::read_tiff(data),
1318 FileType::Iiq => formats::misc::read_iiq(data),
1320 FileType::Gif => formats::gif::read_gif(data),
1322 FileType::Bmp => formats::bmp::read_bmp(data),
1323 FileType::WebP | FileType::Avi | FileType::Wav => formats::riff::read_riff(data),
1324 FileType::Psd => formats::psd::read_psd(data),
1325 FileType::Mp3 => formats::id3::read_mp3(data),
1327 FileType::Flac => formats::flac::read_flac(data),
1328 FileType::Ogg | FileType::Opus => formats::ogg::read_ogg(data),
1329 FileType::Aiff => formats::aiff::read_aiff(data),
1330 FileType::Mp4
1332 | FileType::QuickTime
1333 | FileType::M4a
1334 | FileType::ThreeGP
1335 | FileType::Heif
1336 | FileType::Avif
1337 | FileType::Cr3
1338 | FileType::Crm
1339 | FileType::F4v
1340 | FileType::Mqv
1341 | FileType::Lrv => formats::quicktime::read_quicktime_with_ee(data, self.options.extract_embedded),
1342 FileType::Mkv | FileType::WebM => formats::matroska::read_matroska(data),
1343 FileType::Asf | FileType::Wmv | FileType::Wma => formats::asf::read_asf(data),
1344 FileType::Wtv => formats::wtv::read_wtv(data),
1345 FileType::Crw => formats::canon_raw::read_crw(data),
1347 FileType::Raf => formats::raf::read_raf(data),
1348 FileType::Mrw => formats::mrw::read_mrw(data),
1349 FileType::Mrc => formats::mrc::read_mrc(data),
1350 FileType::Jp2 => formats::jp2::read_jp2(data),
1352 FileType::J2c => formats::jp2::read_j2c(data),
1353 FileType::Jxl => formats::jp2::read_jxl(data),
1354 FileType::Ico => formats::ico::read_ico(data),
1355 FileType::Icc => formats::icc::read_icc(data),
1356 FileType::Pdf => formats::pdf::read_pdf(data),
1358 FileType::PostScript => {
1359 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
1361 formats::font::read_pfa(data).or_else(|_| formats::postscript::read_postscript(data))
1362 } else {
1363 formats::postscript::read_postscript(data)
1364 }
1365 }
1366 FileType::Eip => formats::capture_one::read_eip(data),
1367 FileType::Zip | FileType::Docx | FileType::Xlsx | FileType::Pptx
1368 | FileType::Doc | FileType::Xls | FileType::Ppt => formats::zip::read_zip(data),
1369 FileType::Rtf => formats::rtf::read_rtf(data),
1370 FileType::InDesign => formats::misc::read_indesign(data),
1371 FileType::Pcap => formats::misc::read_pcap(data),
1372 FileType::Pcapng => formats::misc::read_pcapng(data),
1373 FileType::Vrd => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
1375 FileType::Dr4 => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
1376 FileType::Xmp => formats::xmp_file::read_xmp(data),
1378 FileType::Svg => formats::misc::read_svg(data),
1379 FileType::Html => {
1380 let is_svg = data.windows(4).take(512).any(|w| w == b"<svg");
1382 if is_svg {
1383 formats::misc::read_svg(data)
1384 } else {
1385 formats::html::read_html(data)
1386 }
1387 }
1388 FileType::Exe => formats::exe::read_exe(data),
1389 FileType::Font => {
1390 if data.starts_with(b"StartFontMetrics") {
1392 return formats::font::read_afm(data);
1393 }
1394 if data.starts_with(b"%!PS-AdobeFont") || data.starts_with(b"%!FontType1") {
1396 return formats::font::read_pfa(data).or_else(|_| Ok(Vec::new()));
1397 }
1398 if data.len() >= 2 && data[0] == 0x80 && (data[1] == 0x01 || data[1] == 0x02) {
1400 return formats::font::read_pfb(data).or_else(|_| Ok(Vec::new()));
1401 }
1402 formats::font::read_font(data)
1403 }
1404 FileType::WavPack | FileType::Dsf => formats::id3::read_mp3(data),
1406 FileType::Ape => formats::ape::read_ape(data),
1407 FileType::Mpc => formats::ape::read_mpc(data),
1408 FileType::Aac => formats::misc::read_aac(data),
1409 FileType::RealAudio => {
1410 formats::misc::read_real_audio(data).or_else(|_| Ok(Vec::new()))
1411 }
1412 FileType::RealMedia => {
1413 formats::misc::read_real_media(data).or_else(|_| Ok(Vec::new()))
1414 }
1415 FileType::Czi => formats::misc::read_czi(data).or_else(|_| Ok(Vec::new())),
1417 FileType::PhotoCd => formats::misc::read_photo_cd(data).or_else(|_| Ok(Vec::new())),
1418 FileType::Dicom => formats::dicom::read_dicom(data),
1419 FileType::Fits => formats::misc::read_fits(data),
1420 FileType::Flv => formats::misc::read_flv(data),
1421 FileType::Mxf => formats::misc::read_mxf(data).or_else(|_| Ok(Vec::new())),
1422 FileType::Swf => formats::misc::read_swf(data),
1423 FileType::Hdr => formats::misc::read_hdr(data),
1424 FileType::DjVu => formats::djvu::read_djvu(data),
1425 FileType::Xcf => formats::gimp::read_xcf(data),
1426 FileType::Mie => formats::mie::read_mie(data),
1427 FileType::Lfp => formats::lytro::read_lfp(data),
1428 FileType::Fpf => formats::flir_fpf::read_fpf(data),
1430 FileType::Flif => formats::misc::read_flif(data),
1431 FileType::Bpg => formats::misc::read_bpg(data),
1432 FileType::Pcx => formats::misc::read_pcx(data),
1433 FileType::Pict => formats::misc::read_pict(data),
1434 FileType::Mpeg => formats::mpeg::read_mpeg(data),
1435 FileType::M2ts => formats::misc::read_m2ts(data, self.options.extract_embedded),
1436 FileType::Gzip => formats::misc::read_gzip(data),
1437 FileType::Rar => formats::misc::read_rar(data),
1438 FileType::SevenZ => formats::misc::read_7z(data),
1439 FileType::Dss => formats::misc::read_dss(data),
1440 FileType::Moi => formats::misc::read_moi(data),
1441 FileType::MacOs => formats::misc::read_macos(data),
1442 FileType::Json => formats::misc::read_json(data),
1443 FileType::Pgf => formats::pgf::read_pgf(data),
1445 FileType::Xisf => formats::xisf::read_xisf(data),
1446 FileType::Torrent => formats::torrent::read_torrent(data),
1447 FileType::Mobi => formats::palm::read_palm(data),
1448 FileType::Psp => formats::psp::read_psp(data),
1449 FileType::SonyPmp => formats::sony_pmp::read_sony_pmp(data),
1450 FileType::Audible => formats::audible::read_audible(data),
1451 FileType::Exr => formats::openexr::read_openexr(data),
1452 FileType::Plist => {
1454 if data.starts_with(b"bplist") {
1455 formats::plist::read_binary_plist_tags(data)
1456 } else {
1457 formats::plist::read_xml_plist(data)
1458 }
1459 }
1460 FileType::Aae => {
1461 if data.starts_with(b"bplist") {
1462 formats::plist::read_binary_plist_tags(data)
1463 } else {
1464 formats::plist::read_aae_plist(data)
1465 }
1466 }
1467 FileType::KyoceraRaw => formats::misc::read_kyocera_raw(data),
1468 FileType::PortableFloatMap => formats::misc::read_pfm(data),
1469 FileType::Ods | FileType::Odt | FileType::Odp | FileType::Odg |
1470 FileType::Odf | FileType::Odb | FileType::Odi | FileType::Odc => formats::zip::read_zip(data),
1471 FileType::Lif => formats::misc::read_lif(data),
1472 FileType::Rwz => formats::misc::read_rawzor(data),
1473 FileType::Jxr => formats::misc::read_jxr(data),
1474 _ => Err(Error::UnsupportedFileType(format!("{}", file_type))),
1475 }
1476 }
1477
1478 fn process_by_extension(&self, data: &[u8], path: &Path) -> Result<Vec<Tag>> {
1480 let ext = path
1481 .extension()
1482 .and_then(|e| e.to_str())
1483 .unwrap_or("")
1484 .to_ascii_lowercase();
1485
1486 match ext.as_str() {
1487 "ppm" | "pgm" | "pbm" => formats::misc::read_ppm(data),
1488 "pfm" => {
1489 if data.len() >= 3 && data[0] == b'P' && (data[1] == b'f' || data[1] == b'F') {
1491 formats::misc::read_ppm(data)
1492 } else {
1493 Ok(Vec::new()) }
1495 }
1496 "json" => formats::misc::read_json(data),
1497 "svg" => formats::misc::read_svg(data),
1498 "ram" => formats::misc::read_ram(data).or_else(|_| Ok(Vec::new())),
1499 "txt" | "log" | "igc" => {
1500 Ok(compute_text_tags(data, false))
1501 }
1502 "csv" => {
1503 Ok(compute_text_tags(data, true))
1504 }
1505 "url" => formats::lnk::read_url(data).or_else(|_| Ok(Vec::new())),
1506 "lnk" => formats::lnk::read_lnk(data).or_else(|_| Ok(Vec::new())),
1507 "gpx" | "kml" | "xml" | "inx" => formats::xmp_file::read_xmp(data),
1508 "plist" => {
1509 if data.starts_with(b"bplist") {
1510 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
1511 } else {
1512 formats::plist::read_xml_plist(data).or_else(|_| Ok(Vec::new()))
1513 }
1514 }
1515 "aae" => {
1516 if data.starts_with(b"bplist") {
1517 formats::plist::read_binary_plist_tags(data).or_else(|_| Ok(Vec::new()))
1518 } else {
1519 formats::plist::read_aae_plist(data).or_else(|_| Ok(Vec::new()))
1520 }
1521 }
1522 "vcf" | "ics" | "vcard" => {
1523 let s = String::from_utf8_lossy(&data[..data.len().min(100)]);
1524 if s.contains("BEGIN:VCALENDAR") {
1525 formats::vcard::read_ics(data).or_else(|_| Ok(Vec::new()))
1526 } else {
1527 formats::vcard::read_vcf(data).or_else(|_| Ok(Vec::new()))
1528 }
1529 }
1530 "xcf" => Ok(Vec::new()), "vrd" => formats::canon_vrd::read_vrd(data).or_else(|_| Ok(Vec::new())),
1532 "dr4" => formats::canon_vrd::read_dr4(data).or_else(|_| Ok(Vec::new())),
1533 "indd" | "indt" => Ok(Vec::new()), "x3f" => formats::sigma_raw::read_x3f(data).or_else(|_| Ok(Vec::new())),
1535 "mie" => Ok(Vec::new()), "exr" => Ok(Vec::new()), "wpg" => formats::misc::read_wpg(data).or_else(|_| Ok(Vec::new())),
1538 "moi" => formats::misc::read_moi(data).or_else(|_| Ok(Vec::new())),
1539 "macos" => formats::misc::read_macos(data).or_else(|_| Ok(Vec::new())),
1540 "dpx" => formats::dpx::read_dpx(data).or_else(|_| Ok(Vec::new())),
1541 "r3d" => formats::red::read_r3d(data).or_else(|_| Ok(Vec::new())),
1542 "tnef" => formats::tnef::read_tnef(data).or_else(|_| Ok(Vec::new())),
1543 "ppt" | "fpx" => formats::flashpix::read_fpx(data).or_else(|_| Ok(Vec::new())),
1544 "fpf" => formats::flir_fpf::read_fpf(data).or_else(|_| Ok(Vec::new())),
1545 "itc" => formats::misc::read_itc(data).or_else(|_| Ok(Vec::new())),
1546 "mpg" | "mpeg" | "m1v" | "m2v" | "mpv" => formats::mpeg::read_mpeg(data).or_else(|_| Ok(Vec::new())),
1547 "dv" => formats::dv::read_dv(data, data.len() as u64).or_else(|_| Ok(Vec::new())),
1548 "czi" => formats::misc::read_czi(data).or_else(|_| Ok(Vec::new())),
1549 "miff" => formats::miff::read_miff(data).or_else(|_| Ok(Vec::new())),
1550 "lfp" | "mrc"
1551 | "dss" | "mobi" | "psp" | "pgf" | "raw"
1552 | "pmp" | "torrent"
1553 | "xisf" | "mxf"
1554 | "dfont" => Ok(Vec::new()),
1555 "iso" => formats::iso::read_iso(data).or_else(|_| Ok(Vec::new())),
1556 "afm" => formats::font::read_afm(data).or_else(|_| Ok(Vec::new())),
1557 "pfa" => formats::font::read_pfa(data).or_else(|_| Ok(Vec::new())),
1558 "pfb" => formats::font::read_pfb(data).or_else(|_| Ok(Vec::new())),
1559 _ => Err(Error::UnsupportedFileType(ext)),
1560 }
1561 }
1562}
1563
1564impl Default for ExifTool {
1565 fn default() -> Self {
1566 Self::new()
1567 }
1568}
1569
1570fn detect_opendocument_type(data: &[u8]) -> Option<FileType> {
1573 if data.len() < 30 || data[0..4] != [0x50, 0x4B, 0x03, 0x04] {
1575 return None;
1576 }
1577 let compression = u16::from_le_bytes([data[8], data[9]]);
1578 let compressed_size = u32::from_le_bytes([data[18], data[19], data[20], data[21]]) as usize;
1579 let name_len = u16::from_le_bytes([data[26], data[27]]) as usize;
1580 let extra_len = u16::from_le_bytes([data[28], data[29]]) as usize;
1581 let name_start = 30;
1582 if name_start + name_len > data.len() {
1583 return None;
1584 }
1585 let filename = std::str::from_utf8(&data[name_start..name_start + name_len]).unwrap_or("");
1586 if filename != "mimetype" || compression != 0 {
1587 return None;
1588 }
1589 let content_start = name_start + name_len + extra_len;
1590 let content_end = (content_start + compressed_size).min(data.len());
1591 if content_start >= content_end {
1592 return None;
1593 }
1594 let mime = std::str::from_utf8(&data[content_start..content_end]).unwrap_or("").trim();
1595 match mime {
1596 "application/vnd.oasis.opendocument.spreadsheet" => Some(FileType::Ods),
1597 "application/vnd.oasis.opendocument.text" => Some(FileType::Odt),
1598 "application/vnd.oasis.opendocument.presentation" => Some(FileType::Odp),
1599 "application/vnd.oasis.opendocument.graphics" => Some(FileType::Odg),
1600 "application/vnd.oasis.opendocument.formula" => Some(FileType::Odf),
1601 "application/vnd.oasis.opendocument.database" => Some(FileType::Odb),
1602 "application/vnd.oasis.opendocument.image" => Some(FileType::Odi),
1603 "application/vnd.oasis.opendocument.chart" => Some(FileType::Odc),
1604 _ => None,
1605 }
1606}
1607
1608pub fn get_file_type<P: AsRef<Path>>(path: P) -> Result<FileType> {
1610 let path = path.as_ref();
1611 let mut file = fs::File::open(path).map_err(Error::Io)?;
1612 let mut header = [0u8; 256];
1613 use std::io::Read;
1614 let n = file.read(&mut header).map_err(Error::Io)?;
1615
1616 if let Some(ft) = file_type::detect_from_magic(&header[..n]) {
1617 return Ok(ft);
1618 }
1619
1620 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1621 if let Some(ft) = file_type::detect_from_extension(ext) {
1622 return Ok(ft);
1623 }
1624 }
1625
1626 Err(Error::UnsupportedFileType("unknown".into()))
1627}
1628
1629enum ExifIfdGroup {
1631 Ifd0,
1632 ExifIfd,
1633 Gps,
1634}
1635
1636fn classify_exif_tag(tag_id: u16) -> ExifIfdGroup {
1638 match tag_id {
1639 0x829A..=0x829D | 0x8822..=0x8827 | 0x8830 | 0x9000..=0x9292
1641 | 0xA000..=0xA435 => ExifIfdGroup::ExifIfd,
1642 0x0000..=0x001F if tag_id <= 0x001F => ExifIfdGroup::Gps,
1644 _ => ExifIfdGroup::Ifd0,
1646 }
1647}
1648
1649fn extract_existing_exif_entries(jpeg_data: &[u8], target_bo: ByteOrderMark) -> Vec<exif_writer::IfdEntry> {
1651 let mut entries = Vec::new();
1652
1653 let mut pos = 2; while pos + 4 <= jpeg_data.len() {
1656 if jpeg_data[pos] != 0xFF {
1657 pos += 1;
1658 continue;
1659 }
1660 let marker = jpeg_data[pos + 1];
1661 pos += 2;
1662
1663 if marker == 0xDA || marker == 0xD9 {
1664 break; }
1666 if marker == 0xFF || marker == 0x00 || marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
1667 continue;
1668 }
1669
1670 if pos + 2 > jpeg_data.len() {
1671 break;
1672 }
1673 let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
1674 if seg_len < 2 || pos + seg_len > jpeg_data.len() {
1675 break;
1676 }
1677
1678 let seg_data = &jpeg_data[pos + 2..pos + seg_len];
1679
1680 if marker == 0xE1 && seg_data.len() > 14 && seg_data.starts_with(b"Exif\0\0") {
1682 let tiff_data = &seg_data[6..];
1683 extract_ifd_entries(tiff_data, target_bo, &mut entries);
1684 break;
1685 }
1686
1687 pos += seg_len;
1688 }
1689
1690 entries
1691}
1692
1693fn extract_ifd_entries(
1695 tiff_data: &[u8],
1696 target_bo: ByteOrderMark,
1697 entries: &mut Vec<exif_writer::IfdEntry>,
1698) {
1699 use crate::metadata::exif::parse_tiff_header;
1700
1701 let header = match parse_tiff_header(tiff_data) {
1702 Ok(h) => h,
1703 Err(_) => return,
1704 };
1705
1706 let src_bo = header.byte_order;
1707
1708 read_ifd_for_merge(tiff_data, header.ifd0_offset as usize, src_bo, target_bo, entries);
1710
1711 let ifd0_offset = header.ifd0_offset as usize;
1713 if ifd0_offset + 2 > tiff_data.len() {
1714 return;
1715 }
1716 let count = read_u16_bo(tiff_data, ifd0_offset, src_bo) as usize;
1717 for i in 0..count {
1718 let eoff = ifd0_offset + 2 + i * 12;
1719 if eoff + 12 > tiff_data.len() {
1720 break;
1721 }
1722 let tag = read_u16_bo(tiff_data, eoff, src_bo);
1723 let value_off = read_u32_bo(tiff_data, eoff + 8, src_bo) as usize;
1724
1725 match tag {
1726 0x8769 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
1727 0x8825 => read_ifd_for_merge(tiff_data, value_off, src_bo, target_bo, entries),
1728 _ => {}
1729 }
1730 }
1731}
1732
1733fn read_ifd_for_merge(
1735 data: &[u8],
1736 offset: usize,
1737 src_bo: ByteOrderMark,
1738 target_bo: ByteOrderMark,
1739 entries: &mut Vec<exif_writer::IfdEntry>,
1740) {
1741 if offset + 2 > data.len() {
1742 return;
1743 }
1744 let count = read_u16_bo(data, offset, src_bo) as usize;
1745
1746 for i in 0..count {
1747 let eoff = offset + 2 + i * 12;
1748 if eoff + 12 > data.len() {
1749 break;
1750 }
1751
1752 let tag = read_u16_bo(data, eoff, src_bo);
1753 let dtype = read_u16_bo(data, eoff + 2, src_bo);
1754 let count_val = read_u32_bo(data, eoff + 4, src_bo);
1755
1756 if tag == 0x8769 || tag == 0x8825 || tag == 0xA005 || tag == 0x927C {
1758 continue;
1759 }
1760
1761 let type_size = match dtype {
1762 1 | 2 | 6 | 7 => 1usize,
1763 3 | 8 => 2,
1764 4 | 9 | 11 | 13 => 4,
1765 5 | 10 | 12 => 8,
1766 _ => continue,
1767 };
1768
1769 let total_size = type_size * count_val as usize;
1770 let raw_data = if total_size <= 4 {
1771 data[eoff + 8..eoff + 12].to_vec()
1772 } else {
1773 let voff = read_u32_bo(data, eoff + 8, src_bo) as usize;
1774 if voff + total_size > data.len() {
1775 continue;
1776 }
1777 data[voff..voff + total_size].to_vec()
1778 };
1779
1780 let final_data = if src_bo != target_bo && type_size > 1 {
1782 reencode_bytes(&raw_data, dtype, count_val as usize, src_bo, target_bo)
1783 } else {
1784 raw_data[..total_size].to_vec()
1785 };
1786
1787 let format = match dtype {
1788 1 => exif_writer::ExifFormat::Byte,
1789 2 => exif_writer::ExifFormat::Ascii,
1790 3 => exif_writer::ExifFormat::Short,
1791 4 => exif_writer::ExifFormat::Long,
1792 5 => exif_writer::ExifFormat::Rational,
1793 6 => exif_writer::ExifFormat::SByte,
1794 7 => exif_writer::ExifFormat::Undefined,
1795 8 => exif_writer::ExifFormat::SShort,
1796 9 => exif_writer::ExifFormat::SLong,
1797 10 => exif_writer::ExifFormat::SRational,
1798 11 => exif_writer::ExifFormat::Float,
1799 12 => exif_writer::ExifFormat::Double,
1800 _ => continue,
1801 };
1802
1803 entries.push(exif_writer::IfdEntry {
1804 tag,
1805 format,
1806 data: final_data,
1807 });
1808 }
1809}
1810
1811fn reencode_bytes(
1813 data: &[u8],
1814 dtype: u16,
1815 count: usize,
1816 src_bo: ByteOrderMark,
1817 dst_bo: ByteOrderMark,
1818) -> Vec<u8> {
1819 let mut out = Vec::with_capacity(data.len());
1820 match dtype {
1821 3 | 8 => {
1822 for i in 0..count {
1824 let v = read_u16_bo(data, i * 2, src_bo);
1825 match dst_bo {
1826 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
1827 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
1828 }
1829 }
1830 }
1831 4 | 9 | 11 | 13 => {
1832 for i in 0..count {
1834 let v = read_u32_bo(data, i * 4, src_bo);
1835 match dst_bo {
1836 ByteOrderMark::LittleEndian => out.extend_from_slice(&v.to_le_bytes()),
1837 ByteOrderMark::BigEndian => out.extend_from_slice(&v.to_be_bytes()),
1838 }
1839 }
1840 }
1841 5 | 10 => {
1842 for i in 0..count {
1844 let n = read_u32_bo(data, i * 8, src_bo);
1845 let d = read_u32_bo(data, i * 8 + 4, src_bo);
1846 match dst_bo {
1847 ByteOrderMark::LittleEndian => {
1848 out.extend_from_slice(&n.to_le_bytes());
1849 out.extend_from_slice(&d.to_le_bytes());
1850 }
1851 ByteOrderMark::BigEndian => {
1852 out.extend_from_slice(&n.to_be_bytes());
1853 out.extend_from_slice(&d.to_be_bytes());
1854 }
1855 }
1856 }
1857 }
1858 12 => {
1859 for i in 0..count {
1861 let mut bytes = [0u8; 8];
1862 bytes.copy_from_slice(&data[i * 8..i * 8 + 8]);
1863 if src_bo != dst_bo {
1864 bytes.reverse();
1865 }
1866 out.extend_from_slice(&bytes);
1867 }
1868 }
1869 _ => out.extend_from_slice(data),
1870 }
1871 out
1872}
1873
1874fn read_u16_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1875 if offset + 2 > data.len() { return 0; }
1876 match bo {
1877 ByteOrderMark::LittleEndian => u16::from_le_bytes([data[offset], data[offset + 1]]),
1878 ByteOrderMark::BigEndian => u16::from_be_bytes([data[offset], data[offset + 1]]),
1879 }
1880}
1881
1882fn read_u32_bo(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1883 if offset + 4 > data.len() { return 0; }
1884 match bo {
1885 ByteOrderMark::LittleEndian => u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]),
1886 ByteOrderMark::BigEndian => u32::from_be_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]),
1887 }
1888}
1889
1890fn tag_name_to_id(name: &str) -> Option<u16> {
1892 encode_exif_tag(name, "", "", ByteOrderMark::BigEndian).map(|(id, _, _)| id)
1893}
1894
1895fn value_to_filename(value: &str) -> String {
1897 value
1898 .chars()
1899 .map(|c| match c {
1900 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1901 c if c.is_control() => '_',
1902 c => c,
1903 })
1904 .collect::<String>()
1905 .trim()
1906 .to_string()
1907}
1908
1909pub fn parse_date_shift(shift: &str) -> Option<(i32, u32, u32, u32)> {
1912 let (sign, rest) = if shift.starts_with('-') {
1913 (-1, &shift[1..])
1914 } else if shift.starts_with('+') {
1915 (1, &shift[1..])
1916 } else {
1917 (1, shift)
1918 };
1919
1920 let parts: Vec<&str> = rest.split(':').collect();
1921 match parts.len() {
1922 1 => {
1923 let h: u32 = parts[0].parse().ok()?;
1924 Some((sign, h, 0, 0))
1925 }
1926 2 => {
1927 let h: u32 = parts[0].parse().ok()?;
1928 let m: u32 = parts[1].parse().ok()?;
1929 Some((sign, h, m, 0))
1930 }
1931 3 => {
1932 let h: u32 = parts[0].parse().ok()?;
1933 let m: u32 = parts[1].parse().ok()?;
1934 let s: u32 = parts[2].parse().ok()?;
1935 Some((sign, h, m, s))
1936 }
1937 _ => None,
1938 }
1939}
1940
1941pub fn shift_datetime(datetime: &str, shift: &str) -> Option<String> {
1944 let (sign, hours, minutes, seconds) = parse_date_shift(shift)?;
1945
1946 if datetime.len() < 19 {
1948 return None;
1949 }
1950 let year: i32 = datetime[0..4].parse().ok()?;
1951 let month: u32 = datetime[5..7].parse().ok()?;
1952 let day: u32 = datetime[8..10].parse().ok()?;
1953 let hour: u32 = datetime[11..13].parse().ok()?;
1954 let min: u32 = datetime[14..16].parse().ok()?;
1955 let sec: u32 = datetime[17..19].parse().ok()?;
1956
1957 let total_secs = (hour * 3600 + min * 60 + sec) as i64
1959 + sign as i64 * (hours * 3600 + minutes * 60 + seconds) as i64;
1960
1961 let days_shift = if total_secs < 0 {
1962 -1 - (-total_secs - 1) as i64 / 86400
1963 } else {
1964 total_secs / 86400
1965 };
1966
1967 let time_secs = ((total_secs % 86400) + 86400) % 86400;
1968 let new_hour = (time_secs / 3600) as u32;
1969 let new_min = ((time_secs % 3600) / 60) as u32;
1970 let new_sec = (time_secs % 60) as u32;
1971
1972 let mut new_day = day as i32 + days_shift as i32;
1974 let mut new_month = month;
1975 let mut new_year = year;
1976
1977 let days_in_month = |m: u32, y: i32| -> i32 {
1978 match m {
1979 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1980 4 | 6 | 9 | 11 => 30,
1981 2 => if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 29 } else { 28 },
1982 _ => 30,
1983 }
1984 };
1985
1986 while new_day > days_in_month(new_month, new_year) {
1987 new_day -= days_in_month(new_month, new_year);
1988 new_month += 1;
1989 if new_month > 12 {
1990 new_month = 1;
1991 new_year += 1;
1992 }
1993 }
1994 while new_day < 1 {
1995 new_month = if new_month == 1 { 12 } else { new_month - 1 };
1996 if new_month == 12 {
1997 new_year -= 1;
1998 }
1999 new_day += days_in_month(new_month, new_year);
2000 }
2001
2002 Some(format!(
2003 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
2004 new_year, new_month, new_day, new_hour, new_min, new_sec
2005 ))
2006}
2007
2008fn unix_to_datetime(secs: i64) -> String {
2009 let days = secs / 86400;
2010 let time = secs % 86400;
2011 let h = time / 3600;
2012 let m = (time % 3600) / 60;
2013 let s = time % 60;
2014 let mut y = 1970i32;
2015 let mut rem = days;
2016 loop {
2017 let dy = if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 366 } else { 365 };
2018 if rem < dy { break; }
2019 rem -= dy;
2020 y += 1;
2021 }
2022 let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
2023 let months = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
2024 let mut mo = 1;
2025 for &dm in &months {
2026 if rem < dm { break; }
2027 rem -= dm;
2028 mo += 1;
2029 }
2030 format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", y, mo, rem + 1, h, m, s)
2031}
2032
2033fn format_file_size(bytes: u64) -> String {
2034 if bytes < 1024 {
2035 format!("{} bytes", bytes)
2036 } else if bytes < 1024 * 1024 {
2037 format!("{:.1} kB", bytes as f64 / 1024.0)
2038 } else if bytes < 1024 * 1024 * 1024 {
2039 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
2040 } else {
2041 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
2042 }
2043}
2044
2045fn is_xmp_tag(tag: &str) -> bool {
2047 matches!(
2048 tag.to_lowercase().as_str(),
2049 "title" | "description" | "subject" | "creator" | "rights"
2050 | "keywords" | "rating" | "label" | "hierarchicalsubject"
2051 )
2052}
2053
2054fn encode_exif_tag(
2057 tag_name: &str,
2058 value: &str,
2059 _group: &str,
2060 bo: ByteOrderMark,
2061) -> Option<(u16, exif_writer::ExifFormat, Vec<u8>)> {
2062 let tag_lower = tag_name.to_lowercase();
2063
2064 let (tag_id, format): (u16, exif_writer::ExifFormat) = match tag_lower.as_str() {
2066 "imagedescription" => (0x010E, exif_writer::ExifFormat::Ascii),
2068 "make" => (0x010F, exif_writer::ExifFormat::Ascii),
2069 "model" => (0x0110, exif_writer::ExifFormat::Ascii),
2070 "software" => (0x0131, exif_writer::ExifFormat::Ascii),
2071 "modifydate" | "datetime" => (0x0132, exif_writer::ExifFormat::Ascii),
2072 "artist" => (0x013B, exif_writer::ExifFormat::Ascii),
2073 "copyright" => (0x8298, exif_writer::ExifFormat::Ascii),
2074 "orientation" => (0x0112, exif_writer::ExifFormat::Short),
2076 "xresolution" => (0x011A, exif_writer::ExifFormat::Rational),
2077 "yresolution" => (0x011B, exif_writer::ExifFormat::Rational),
2078 "resolutionunit" => (0x0128, exif_writer::ExifFormat::Short),
2079 "datetimeoriginal" => (0x9003, exif_writer::ExifFormat::Ascii),
2081 "createdate" | "datetimedigitized" => (0x9004, exif_writer::ExifFormat::Ascii),
2082 "usercomment" => (0x9286, exif_writer::ExifFormat::Undefined),
2083 "imageuniqueid" => (0xA420, exif_writer::ExifFormat::Ascii),
2084 "ownername" | "cameraownername" => (0xA430, exif_writer::ExifFormat::Ascii),
2085 "serialnumber" | "bodyserialnumber" => (0xA431, exif_writer::ExifFormat::Ascii),
2086 "lensmake" => (0xA433, exif_writer::ExifFormat::Ascii),
2087 "lensmodel" => (0xA434, exif_writer::ExifFormat::Ascii),
2088 "lensserialnumber" => (0xA435, exif_writer::ExifFormat::Ascii),
2089 _ => return None,
2090 };
2091
2092 let encoded = match format {
2093 exif_writer::ExifFormat::Ascii => exif_writer::encode_ascii(value),
2094 exif_writer::ExifFormat::Short => {
2095 let v: u16 = value.parse().ok()?;
2096 exif_writer::encode_u16(v, bo)
2097 }
2098 exif_writer::ExifFormat::Long => {
2099 let v: u32 = value.parse().ok()?;
2100 exif_writer::encode_u32(v, bo)
2101 }
2102 exif_writer::ExifFormat::Rational => {
2103 if let Some(slash) = value.find('/') {
2105 let num: u32 = value[..slash].trim().parse().ok()?;
2106 let den: u32 = value[slash + 1..].trim().parse().ok()?;
2107 exif_writer::encode_urational(num, den, bo)
2108 } else if let Ok(v) = value.parse::<f64>() {
2109 let den = 10000u32;
2111 let num = (v * den as f64).round() as u32;
2112 exif_writer::encode_urational(num, den, bo)
2113 } else {
2114 return None;
2115 }
2116 }
2117 exif_writer::ExifFormat::Undefined => {
2118 let mut data = vec![0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00]; data.extend_from_slice(value.as_bytes());
2121 data
2122 }
2123 _ => return None,
2124 };
2125
2126 Some((tag_id, format, encoded))
2127}
2128
2129fn compute_text_tags(data: &[u8], is_csv: bool) -> Vec<Tag> {
2131 let mut tags = Vec::new();
2132 let mk = |name: &str, val: String| Tag {
2133 id: crate::tag::TagId::Text(name.into()),
2134 name: name.into(), description: name.into(),
2135 group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Other".into() },
2136 raw_value: Value::String(val.clone()), print_value: val, priority: 0,
2137 };
2138
2139 let is_ascii = data.iter().all(|&b| b < 128);
2141 let has_utf8_bom = data.starts_with(&[0xEF, 0xBB, 0xBF]);
2142 let has_utf16le_bom = data.starts_with(&[0xFF, 0xFE]) && !data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
2143 let has_utf16be_bom = data.starts_with(&[0xFE, 0xFF]);
2144 let has_utf32le_bom = data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]);
2145 let has_utf32be_bom = data.starts_with(&[0x00, 0x00, 0xFE, 0xFF]);
2146
2147 let has_weird_ctrl = data.iter().any(|&b| (b <= 0x06) || (b >= 0x0e && b <= 0x1a) || (b >= 0x1c && b <= 0x1f) || b == 0x7f);
2149
2150 let (encoding, is_bom, is_utf16) = if has_utf32le_bom {
2151 ("utf-32le", true, false)
2152 } else if has_utf32be_bom {
2153 ("utf-32be", true, false)
2154 } else if has_utf16le_bom {
2155 ("utf-16le", true, true)
2156 } else if has_utf16be_bom {
2157 ("utf-16be", true, true)
2158 } else if has_weird_ctrl {
2159 return tags;
2161 } else if is_ascii {
2162 ("us-ascii", false, false)
2163 } else {
2164 let is_valid_utf8 = std::str::from_utf8(data).is_ok();
2166 if is_valid_utf8 {
2167 if has_utf8_bom {
2168 ("utf-8", true, false)
2169 } else {
2170 ("utf-8", false, false)
2174 }
2175 } else if !data.iter().any(|&b| b >= 0x80 && b <= 0x9f) {
2176 ("iso-8859-1", false, false)
2177 } else {
2178 ("unknown-8bit", false, false)
2179 }
2180 };
2181
2182 tags.push(mk("MIMEEncoding", encoding.into()));
2183
2184 if is_bom {
2185 tags.push(mk("ByteOrderMark", "Yes".into()));
2186 }
2187
2188 let has_cr = data.contains(&b'\r');
2190 let has_lf = data.contains(&b'\n');
2191 let newline_type = if has_cr && has_lf { "Windows CRLF" }
2192 else if has_lf { "Unix LF" }
2193 else if has_cr { "Macintosh CR" }
2194 else { "(none)" };
2195 tags.push(mk("Newlines", newline_type.into()));
2196
2197 if is_csv {
2198 let text = String::from_utf8_lossy(data);
2200 let mut delim = "";
2201 let mut quot = "";
2202 let mut ncols = 1usize;
2203 let mut nrows = 0usize;
2204
2205 for line in text.lines() {
2206 if nrows == 0 {
2207 let comma_count = line.matches(',').count();
2209 let semi_count = line.matches(';').count();
2210 let tab_count = line.matches('\t').count();
2211 if comma_count > semi_count && comma_count > tab_count {
2212 delim = ",";
2213 ncols = comma_count + 1;
2214 } else if semi_count > tab_count {
2215 delim = ";";
2216 ncols = semi_count + 1;
2217 } else if tab_count > 0 {
2218 delim = "\t";
2219 ncols = tab_count + 1;
2220 } else {
2221 delim = "";
2222 ncols = 1;
2223 }
2224 if line.contains('"') { quot = "\""; }
2226 else if line.contains('\'') { quot = "'"; }
2227 }
2228 nrows += 1;
2229 if nrows >= 1000 { break; }
2230 }
2231
2232 let delim_display = match delim {
2233 "," => "Comma",
2234 ";" => "Semicolon",
2235 "\t" => "Tab",
2236 _ => "(none)",
2237 };
2238 let quot_display = match quot {
2239 "\"" => "Double quotes",
2240 "'" => "Single quotes",
2241 _ => "(none)",
2242 };
2243
2244 tags.push(mk("Delimiter", delim_display.into()));
2245 tags.push(mk("Quoting", quot_display.into()));
2246 tags.push(mk("ColumnCount", ncols.to_string()));
2247 if nrows > 0 {
2248 tags.push(mk("RowCount", nrows.to_string()));
2249 }
2250 } else if !is_utf16 {
2251 let line_count = data.iter().filter(|&&b| b == b'\n').count();
2253 let line_count = if line_count == 0 && !data.is_empty() { 1 } else { line_count };
2254 tags.push(mk("LineCount", line_count.to_string()));
2255
2256 let text = String::from_utf8_lossy(data);
2257 let word_count = text.split_whitespace().count();
2258 tags.push(mk("WordCount", word_count.to_string()));
2259 }
2260
2261 tags
2262}