1use byteorder::{BigEndian, ByteOrder, LittleEndian};
7
8use crate::error::{Error, Result};
9use crate::tag::{Tag, TagGroup, TagId};
10use crate::tags::exif as exif_tags;
11use crate::value::Value;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ByteOrderMark {
16 LittleEndian,
17 BigEndian,
18}
19
20#[derive(Debug)]
22pub struct TiffHeader {
23 pub byte_order: ByteOrderMark,
24 pub ifd0_offset: u32,
25}
26
27#[derive(Debug)]
29struct IfdEntry {
30 tag: u16,
31 data_type: u16,
32 count: u32,
33 value_offset: u32,
34 inline_data: [u8; 4],
36}
37
38fn type_size(data_type: u16) -> Option<usize> {
40 match data_type {
41 1 => Some(1), 2 => Some(1), 3 => Some(2), 4 => Some(4), 5 => Some(8), 6 => Some(1), 7 => Some(1), 8 => Some(2), 9 => Some(4), 10 => Some(8), 11 => Some(4), 12 => Some(8), 13 => Some(4), _ => None,
55 }
56}
57
58pub fn parse_tiff_header(data: &[u8]) -> Result<TiffHeader> {
60 if data.len() < 8 {
61 return Err(Error::InvalidTiffHeader);
62 }
63
64 let byte_order = match (data[0], data[1]) {
65 (b'I', b'I') => ByteOrderMark::LittleEndian,
66 (b'M', b'M') => ByteOrderMark::BigEndian,
67 _ => return Err(Error::InvalidTiffHeader),
68 };
69
70 let magic = match byte_order {
71 ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[2..4]),
72 ByteOrderMark::BigEndian => BigEndian::read_u16(&data[2..4]),
73 };
74
75 if magic != 42 {
76 return Err(Error::InvalidTiffHeader);
77 }
78
79 let ifd0_offset = match byte_order {
80 ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[4..8]),
81 ByteOrderMark::BigEndian => BigEndian::read_u32(&data[4..8]),
82 };
83
84 Ok(TiffHeader {
85 byte_order,
86 ifd0_offset,
87 })
88}
89
90pub struct ExifReader;
92
93impl ExifReader {
94 pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
96 let header = parse_tiff_header(data)?;
97 let mut tags = Vec::new();
98
99 let bo_str = match header.byte_order {
101 ByteOrderMark::LittleEndian => "Little-endian (Intel, II)",
102 ByteOrderMark::BigEndian => "Big-endian (Motorola, MM)",
103 };
104 tags.push(Tag {
105 id: TagId::Text("ExifByteOrder".to_string()),
106 name: "ExifByteOrder".to_string(),
107 description: "Exif Byte Order".to_string(),
108 group: TagGroup {
109 family0: "EXIF".to_string(),
110 family1: "IFD0".to_string(),
111 family2: "ExifTool".to_string(),
112 },
113 raw_value: Value::String(bo_str.to_string()),
114 print_value: bo_str.to_string(),
115 priority: 0,
116 });
117
118 let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
120
121 Self::read_ifd(data, &header, header.ifd0_offset, "IFD0", &mut tags)?;
123
124 if is_cr2 {
127 for tag in tags.iter_mut() {
129 if tag.group.family1 == "IFD0" {
130 if tag.name == "StripOffsets" {
131 tag.name = "PreviewImageStart".to_string();
132 tag.description = "Preview Image Start".to_string();
133 tag.id = TagId::Text("PreviewImageStart".to_string());
134 } else if tag.name == "StripByteCounts" {
135 tag.name = "PreviewImageLength".to_string();
136 tag.description = "Preview Image Length".to_string();
137 tag.id = TagId::Text("PreviewImageLength".to_string());
138 }
139 }
140 }
141 let preview_start = tags.iter()
143 .find(|t| t.name == "PreviewImageStart" && t.group.family1 == "IFD0")
144 .and_then(|t| t.raw_value.as_u64())
145 .map(|v| v as usize);
146 let preview_len = tags.iter()
147 .find(|t| t.name == "PreviewImageLength" && t.group.family1 == "IFD0")
148 .and_then(|t| t.raw_value.as_u64())
149 .map(|v| v as usize);
150 if let (Some(start), Some(len)) = (preview_start, preview_len) {
151 if len > 0 && start + len <= data.len() {
152 let img_data = data[start..start+len].to_vec();
153 let pv = format!("(Binary data {} bytes, use -b option to extract)", len);
154 tags.push(Tag {
155 id: TagId::Text("PreviewImage".to_string()),
156 name: "PreviewImage".to_string(),
157 description: "Preview Image".to_string(),
158 group: TagGroup {
159 family0: "EXIF".to_string(),
160 family1: "IFD0".to_string(),
161 family2: "Preview".to_string(),
162 },
163 raw_value: Value::Binary(img_data),
164 print_value: pv,
165 priority: 0,
166 });
167 }
168 }
169 }
170
171 let make = tags
173 .iter()
174 .find(|t| t.name == "Make")
175 .map(|t| t.print_value.clone())
176 .unwrap_or_default();
177
178 let model = tags
179 .iter()
180 .find(|t| t.name == "Model")
181 .map(|t| t.print_value.clone())
182 .unwrap_or_default();
183
184 let make_and_model = if model.is_empty() { make.clone() } else { model };
186
187 let mn_info: Option<(usize, usize)> = {
190 let mut result = None;
192 Self::find_makernote(data, &header, &mut result);
193 result
194 };
195
196 if let Some((mn_offset, mn_size)) = mn_info {
197 let mn_tags = crate::metadata::makernotes::parse_makernotes(
198 data, mn_offset, mn_size, &make, &make_and_model, header.byte_order,
199 );
200 tags.retain(|t| t.name != "MakerNote");
202 {
206 let exif_primary: &[&str] = &[
208 "ThumbnailOffset", "ThumbnailLength", "ThumbnailImage",
209 "StripOffsets", "StripByteCounts",
210 "PreviewImageStart", "PreviewImageLength", "PreviewImage",
211 "ImageWidth", "ImageHeight", "BitsPerSample", "Compression",
212 "PhotometricInterpretation", "SamplesPerPixel", "RowsPerStrip",
213 "PlanarConfiguration", "XResolution", "YResolution", "ResolutionUnit",
214 "Orientation", "Make", "Model", "Software",
215 "ExifByteOrder", "CR2CFAPattern", "RawImageSegmentation",
216 "ColorSpace", "ExifVersion", "FlashpixVersion",
217 "ExifImageWidth", "ExifImageHeight", "InteropIndex", "InteropVersion",
218 "DateTimeOriginal", "CreateDate", "ModifyDate", "DateTime",
219 "FocalPlaneXResolution", "FocalPlaneYResolution", "FocalPlaneResolutionUnit",
220 "CustomRendered", "ExposureMode", "SceneCaptureType",
221 "Flash", "FocalLength", "ISO", "ExposureTime", "ExposureProgram",
222 "FNumber", "ShutterSpeedValue", "ApertureValue", "ComponentsConfiguration",
223 "UserComment",
224 ];
225 let mn_name_set: std::collections::HashSet<String> = mn_tags.iter()
226 .map(|t| t.name.clone())
227 .collect();
228 let exif_has: std::collections::HashSet<String> = tags.iter()
229 .map(|t| t.name.clone())
230 .collect();
231 tags.retain(|t| {
233 !mn_name_set.contains(&t.name)
234 || exif_primary.contains(&t.name.as_str())
235 });
236 for mn_tag in mn_tags {
238 if exif_primary.contains(&mn_tag.name.as_str())
239 && exif_has.contains(&mn_tag.name) {
240 continue;
242 }
243 tags.push(mn_tag);
244 }
245 }
246 }
247
248 if mn_info.is_none() {
250 Self::parse_dng_private_data(data, &header, &make, &make_and_model, &mut tags);
253 }
254
255 {
258 let iptc_data: Option<Vec<u8>> = tags.iter()
259 .find(|t| t.name == "IPTC-NAA")
260 .and_then(|t| {
261 match &t.raw_value {
262 Value::Undefined(bytes) => Some(bytes.clone()),
263 Value::Binary(bytes) => Some(bytes.clone()),
264 Value::List(items) => {
265 let mut bytes = Vec::with_capacity(items.len() * 4);
267 for item in items {
268 match item {
269 Value::U32(v) => bytes.extend_from_slice(&v.to_be_bytes()),
270 _ => {}
271 }
272 }
273 if bytes.is_empty() { None } else { Some(bytes) }
274 }
275 _ => None,
276 }
277 });
278
279 if let Some(iptc_bytes) = iptc_data {
280 let md5_hex = crate::md5::md5_hex(&iptc_bytes);
282
283 if let Ok(iptc_tags) = crate::metadata::IptcReader::read(&iptc_bytes) {
284 tags.retain(|t| t.name != "IPTC-NAA");
286 tags.extend(iptc_tags);
287 }
288
289 tags.push(crate::tag::Tag {
291 id: crate::tag::TagId::Text("CurrentIPTCDigest".into()),
292 name: "CurrentIPTCDigest".into(),
293 description: "Current IPTC Digest".into(),
294 group: crate::tag::TagGroup {
295 family0: "IPTC".into(),
296 family1: "IPTC".into(),
297 family2: "Other".into(),
298 },
299 raw_value: Value::String(md5_hex.clone()),
300 print_value: md5_hex,
301 priority: 0,
302 });
303 }
304 }
305
306 {
308 let icc_data: Option<Vec<u8>> = tags.iter()
309 .find(|t| t.name == "ICC_Profile")
310 .and_then(|t| {
311 match &t.raw_value {
312 Value::Undefined(bytes) => Some(bytes.clone()),
313 Value::Binary(bytes) => Some(bytes.clone()),
314 _ => None,
315 }
316 });
317
318 if let Some(icc_bytes) = icc_data {
319 if let Ok(icc_tags) = crate::formats::icc::read_icc(&icc_bytes) {
320 tags.retain(|t| t.name != "ICC_Profile");
322 tags.extend(icc_tags);
323 }
324 }
325 }
326
327 process_geotiff_keys(&mut tags);
329
330 {
335 let mn_tags_start = tags.iter().position(|t| t.group.family0 == "MakerNotes")
337 .unwrap_or(tags.len());
338 if mn_tags_start < tags.len() {
339 let mut last_idx: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
341 for (i, t) in tags[mn_tags_start..].iter().enumerate() {
342 last_idx.insert(t.name.as_str(), i + mn_tags_start);
343 }
344 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
346 let mut keep = vec![false; tags.len()];
348 for (i, t) in tags.iter().enumerate().rev() {
349 if t.group.family0 != "MakerNotes" {
350 keep[i] = true;
351 continue;
352 }
353 if seen.insert(t.name.as_str()) {
354 keep[i] = true; }
356 }
357 let mut iter = keep.iter();
358 tags.retain(|_| *iter.next().unwrap_or(&true));
359 }
360 }
361
362 Ok(tags)
363 }
364
365 fn parse_dng_private_data(data: &[u8], header: &TiffHeader, make: &str, model: &str, tags: &mut Vec<Tag>) {
368 let ifd0_offset = header.ifd0_offset as usize;
370 if ifd0_offset + 2 > data.len() { return; }
371 let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
372 let entries_start = ifd0_offset + 2;
373 for i in 0..entry_count {
374 let eoff = entries_start + i * 12;
375 if eoff + 12 > data.len() { break; }
376 let tag = read_u16(data, eoff, header.byte_order);
377 if tag == 0xC634 {
378 let dtype = read_u16(data, eoff + 2, header.byte_order);
379 let count = read_u32(data, eoff + 4, header.byte_order) as usize;
380 let elem_size = match dtype { 1 | 7 => 1, _ => 0 };
381 let total = elem_size * count;
382 if total < 14 { continue; }
383 let off = read_u32(data, eoff + 8, header.byte_order) as usize;
384 if off + total > data.len() { continue; }
385 let pdata = &data[off..off+total];
386 if !pdata.starts_with(b"Adobe\0") { continue; }
388 let mut bpos = 6;
389 while bpos + 8 <= pdata.len() {
390 let btag = &pdata[bpos..bpos+4];
391 let bsize = u32::from_be_bytes([pdata[bpos+4], pdata[bpos+5], pdata[bpos+6], pdata[bpos+7]]) as usize;
392 bpos += 8;
393 if bpos + bsize > pdata.len() { break; }
394 if btag == b"MakN" && bsize > 6 {
395 let mn_block = &pdata[bpos..bpos+bsize];
396 let mn_bo = if &mn_block[0..2] == b"II" {
397 ByteOrderMark::LittleEndian
398 } else {
399 ByteOrderMark::BigEndian
400 };
401 let mut mn_start = 6; if bsize >= 18 && &mn_block[6..10] == b"\0\0\0\x01" {
404 mn_start += 12;
405 }
406 if mn_start < bsize {
407 let mn_bo_str = if mn_bo == ByteOrderMark::LittleEndian {
409 "Little-endian (Intel, II)"
410 } else {
411 "Big-endian (Motorola, MM)"
412 };
413 tags.push(Tag {
414 id: TagId::Text("MakerNoteByteOrder".into()),
415 name: "MakerNoteByteOrder".into(),
416 description: "Maker Note Byte Order".into(),
417 group: TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
418 raw_value: Value::String(mn_bo_str.into()),
419 print_value: mn_bo_str.into(),
420 priority: 0,
421 });
422 let mn_data_in_block = &mn_block[mn_start..];
426 let mn_abs_offset = off + (bpos - 8 + 8) + mn_start; let fix_base = if mn_data_in_block.len() > 8 {
429 let footer = &mn_data_in_block[mn_data_in_block.len()-8..];
430 if (footer[0..2] == *b"II" || footer[0..2] == *b"MM")
431 && (footer[2..4] == *b"\x2a\x00" || footer[2..4] == *b"\x00\x2a")
432 {
433 let old_off = if footer[0] == b'I' {
434 u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]])
435 } else {
436 u32::from_be_bytes([footer[4], footer[5], footer[6], footer[7]])
437 } as usize;
438 if old_off > 0 && mn_abs_offset > old_off {
439 mn_abs_offset as isize - old_off as isize
440 } else { 0 }
441 } else { 0 }
442 } else { 0 };
443
444 let mn_tags = if fix_base != 0 {
445 crate::metadata::makernotes::parse_makernotes_with_base(
447 data, mn_abs_offset, mn_data_in_block.len(),
448 make, model, mn_bo, fix_base,
449 )
450 } else {
451 crate::metadata::makernotes::parse_makernotes(
452 mn_data_in_block, 0, mn_data_in_block.len(),
453 make, model, mn_bo,
454 )
455 };
456 let dng_suppress = ["AESetting", "CameraISO", "ImageStabilization",
458 "SpotMeteringMode", "RawJpgSize", "Warning"];
459 for mn_tag in mn_tags {
460 if tags.iter().any(|t| t.name == mn_tag.name) { continue; }
462 if dng_suppress.contains(&mn_tag.name.as_str()) { continue; }
463 tags.push(mn_tag);
464 }
465 }
466 }
467 bpos += bsize;
468 }
469 break;
470 }
471 }
472 }
473
474 fn find_makernote(data: &[u8], header: &TiffHeader, result: &mut Option<(usize, usize)>) {
475 let ifd0_offset = header.ifd0_offset as usize;
477 if ifd0_offset + 2 > data.len() {
478 return;
479 }
480 let entry_count = read_u16(data, ifd0_offset, header.byte_order) as usize;
481 let entries_start = ifd0_offset + 2;
482
483 for i in 0..entry_count {
484 let eoff = entries_start + i * 12;
485 if eoff + 12 > data.len() { break; }
486 let tag = read_u16(data, eoff, header.byte_order);
487 if tag == 0x8769 {
488 let exif_offset = read_u32(data, eoff + 8, header.byte_order) as usize;
490 Self::find_makernote_in_ifd(data, header, exif_offset, result);
491 break;
492 }
493 }
494 }
495
496 fn find_makernote_in_ifd(data: &[u8], header: &TiffHeader, ifd_offset: usize, result: &mut Option<(usize, usize)>) {
497 if ifd_offset + 2 > data.len() {
498 return;
499 }
500 let entry_count = read_u16(data, ifd_offset, header.byte_order) as usize;
501 let entries_start = ifd_offset + 2;
502
503 for i in 0..entry_count {
504 let eoff = entries_start + i * 12;
505 if eoff + 12 > data.len() { break; }
506 let tag = read_u16(data, eoff, header.byte_order);
507 if tag == 0x927C {
508 let data_type = read_u16(data, eoff + 2, header.byte_order);
509 let count = read_u32(data, eoff + 4, header.byte_order) as usize;
510 let type_size = match data_type { 1 | 2 | 6 | 7 => 1, 3 | 8 => 2, 4 | 9 | 11 | 13 => 4, 5 | 10 | 12 => 8, _ => 1 };
511 let total_size = type_size * count;
512
513 if total_size <= 4 {
514 break;
516 }
517 let offset = read_u32(data, eoff + 8, header.byte_order) as usize;
518 if offset + total_size <= data.len() {
519 *result = Some((offset, total_size));
520 }
521 break;
522 }
523 }
524 }
525
526 fn read_ifd(
528 data: &[u8],
529 header: &TiffHeader,
530 offset: u32,
531 ifd_name: &str,
532 tags: &mut Vec<Tag>,
533 ) -> Result<Option<u32>> {
534 let offset = offset as usize;
535 if offset + 2 > data.len() {
536 return Err(Error::InvalidExif(format!(
537 "{} offset {} beyond data length {}",
538 ifd_name,
539 offset,
540 data.len()
541 )));
542 }
543
544 let entry_count = read_u16(data, offset, header.byte_order) as usize;
545 let entries_start = offset + 2;
546 let _entries_end = entries_start + entry_count * 12;
547
548 if entries_start + 12 > data.len() && entry_count > 0 {
550 return Err(Error::InvalidExif(format!(
551 "{} entries extend beyond data (need {}, have {})",
552 ifd_name,
553 entries_start + 12,
554 data.len()
555 )));
556 }
557 let entry_count = entry_count.min((data.len().saturating_sub(entries_start)) / 12);
559 let entries_end = entries_start + entry_count * 12;
560
561 for i in 0..entry_count {
562 let entry_offset = entries_start + i * 12;
563 let entry = parse_ifd_entry(data, entry_offset, header.byte_order);
564
565 match entry.tag {
567 0x8769 => {
568 let sub_offset = entry.value_offset;
570 if (sub_offset as usize) < data.len() {
571 let _ = Self::read_ifd(data, header, sub_offset, "ExifIFD", tags);
572 }
573 continue;
574 }
575 0x8825 => {
576 let sub_offset = entry.value_offset;
578 if (sub_offset as usize) < data.len() {
579 let _ = Self::read_ifd(data, header, sub_offset, "GPS", tags);
580 }
581 continue;
582 }
583 0xA005 => {
584 let sub_offset = entry.value_offset;
586 if (sub_offset as usize) < data.len() {
587 let _ = Self::read_ifd(data, header, sub_offset, "InteropIFD", tags);
588 }
589 continue;
590 }
591 0xC4A5 => {
593 let total_size = match entry.data_type {
594 1 | 2 | 6 | 7 => entry.count as usize,
595 _ => 0,
596 };
597 if total_size > 11 {
598 let off = entry.value_offset as usize;
599 if off + 11 <= data.len() && &data[off..off+7] == b"PrintIM" {
600 let ver = String::from_utf8_lossy(&data[off+7..off+11]).to_string();
601 tags.push(Tag {
602 id: TagId::Text("PrintIMVersion".into()),
603 name: "PrintIMVersion".into(),
604 description: "PrintIM Version".into(),
605 group: TagGroup { family0: "PrintIM".into(), family1: "PrintIM".into(), family2: "Printing".into() },
606 raw_value: Value::String(ver.clone()),
607 print_value: ver,
608 priority: 0,
609 });
610 }
611 }
612 continue; }
614 0x0006 if ifd_name == "GPS" => {
616 if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
617 if let Value::URational(0, 0) = val {
618 continue;
619 }
620 }
621 }
622 0x0201 if ifd_name.starts_with("SubIFD") => {
624 if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
625 let pv = val.to_display_string();
626 tags.push(Tag {
627 id: TagId::Numeric(entry.tag),
628 name: "JpgFromRawStart".into(),
629 description: "Jpg From Raw Start".into(),
630 group: TagGroup {
631 family0: "EXIF".into(),
632 family1: ifd_name.to_string(),
633 family2: "Image".into(),
634 },
635 raw_value: val,
636 print_value: pv,
637 priority: 0,
638 });
639 }
640 continue;
641 }
642 0x0202 if ifd_name.starts_with("SubIFD") => {
644 if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
645 let pv = val.to_display_string();
646 tags.push(Tag {
647 id: TagId::Numeric(entry.tag),
648 name: "JpgFromRawLength".into(),
649 description: "Jpg From Raw Length".into(),
650 group: TagGroup {
651 family0: "EXIF".into(),
652 family1: ifd_name.to_string(),
653 family2: "Image".into(),
654 },
655 raw_value: val,
656 print_value: pv,
657 priority: 0,
658 });
659 }
660 continue;
661 }
662 0x014A if ifd_name == "IFD0" => {
664 if let Some(val) = read_ifd_value(data, &entry, header.byte_order) {
666 let offsets: Vec<u32> = match &val {
667 Value::U32(v) => vec![*v],
668 Value::List(items) => items.iter().filter_map(|v| {
669 if let Value::U32(o) = v { Some(*o) } else { None }
670 }).collect(),
671 _ => vec![],
672 };
673 for (idx, &off) in offsets.iter().enumerate() {
674 if (off as usize) < data.len() {
675 let sub_name = format!("SubIFD{}", idx);
676 let before_idx = tags.len();
677 let _ = Self::read_ifd(data, header, off, &sub_name, tags);
678
679 let is_jpeg = tags[before_idx..].iter()
681 .any(|t| t.name == "Compression" && (t.print_value.contains("JPEG") || t.raw_value.as_u64() == Some(6)));
682
683 if is_jpeg {
684 let (start_name, len_name, img_name) = if idx == 2 {
687 ("JpgFromRawStart", "JpgFromRawLength", "JpgFromRaw")
688 } else {
689 ("PreviewImageStart", "PreviewImageLength", "PreviewImage")
690 };
691 let strip_off = tags[before_idx..].iter()
693 .find(|t| t.name == "StripOffsets")
694 .and_then(|t| t.raw_value.as_u64());
695 let strip_len = tags[before_idx..].iter()
696 .find(|t| t.name == "StripByteCounts")
697 .and_then(|t| t.raw_value.as_u64());
698 if let (Some(s), Some(l)) = (strip_off, strip_len) {
699 tags.push(Tag {
700 id: TagId::Text(start_name.into()), name: start_name.into(),
701 description: start_name.into(),
702 group: TagGroup { family0: "EXIF".into(), family1: sub_name.clone(), family2: "Preview".into() },
703 raw_value: Value::U32(s as u32), print_value: s.to_string(), priority: 0,
704 });
705 tags.push(Tag {
706 id: TagId::Text(len_name.into()), name: len_name.into(),
707 description: len_name.into(),
708 group: TagGroup { family0: "EXIF".into(), family1: sub_name.clone(), family2: "Preview".into() },
709 raw_value: Value::U32(l as u32), print_value: l.to_string(), priority: 0,
710 });
711 let s = s as usize;
713 let l = l as usize;
714 if l > 0 && s + l <= data.len() {
715 let pv = format!("(Binary data {} bytes, use -b option to extract)", l);
716 tags.push(Tag {
717 id: TagId::Text(img_name.into()), name: img_name.into(),
718 description: img_name.into(),
719 group: TagGroup { family0: "EXIF".into(), family1: sub_name.clone(), family2: "Preview".into() },
720 raw_value: Value::Binary(data[s..s+l].to_vec()),
721 print_value: pv, priority: 0,
722 });
723 }
724 }
725 }
726
727 let jpg_start = tags[before_idx..].iter()
729 .find(|t| t.name == "JpgFromRawStart")
730 .and_then(|t| t.raw_value.as_u64());
731 let jpg_len = tags[before_idx..].iter()
732 .find(|t| t.name == "JpgFromRawLength")
733 .and_then(|t| t.raw_value.as_u64());
734 if let (Some(start), Some(len)) = (jpg_start, jpg_len) {
735 let start = start as usize;
736 let len = len as usize;
737 if len > 0 && start + len <= data.len() {
738 let pv = format!(
739 "(Binary data {} bytes, use -b option to extract)",
740 len
741 );
742 tags.push(Tag {
743 id: TagId::Text("JpgFromRaw".into()),
744 name: "JpgFromRaw".into(),
745 description: "Jpg From Raw".into(),
746 group: TagGroup {
747 family0: "EXIF".into(),
748 family1: sub_name,
749 family2: "Preview".into(),
750 },
751 raw_value: Value::Binary(
752 data[start..start + len].to_vec(),
753 ),
754 print_value: pv,
755 priority: 0,
756 });
757 }
758 }
759 }
760 }
761 }
762 continue;
763 }
764 0x0100 | 0x0101 | 0x0102 | 0x0103 | 0x0111 | 0x0117
769 if ifd_name == "IFD2" => {
770 continue;
771 }
772 0x0103 if ifd_name == "IFD3" => {
774 continue;
775 }
776 _ => {}
777 }
778
779 if let Some(mut value) = read_ifd_value(data, &entry, header.byte_order) {
780 if ifd_name == "GPS" && entry.tag == 0x0007 {
783 if let Value::List(ref mut items) = value {
784 for item in items.iter_mut() {
785 if matches!(item, Value::URational(0, 0)) {
786 *item = Value::URational(0, 1);
787 }
788 }
789 }
790 }
791 let tag_info = exif_tags::lookup(ifd_name, entry.tag);
792 let (name, description, family2) = match tag_info {
793 Some(info) => (
794 info.name.to_string(),
795 info.description.to_string(),
796 info.family2.to_string(),
797 ),
798 None => {
799 if matches!(entry.tag,
801 0xC634 ) {
805 continue;
806 }
807 match exif_tags::lookup_generated(entry.tag) {
809 Some((n, d)) => (n.to_string(), d.to_string(), "Other".to_string()),
810 None => {
811 continue;
813 },
814 }
815 }
816 };
817
818 if name == "ApplicationNotes" {
820 if let Value::Binary(ref xmp_bytes) = value {
821 if let Ok(xmp_tags) = crate::metadata::XmpReader::read(xmp_bytes) {
822 tags.extend(xmp_tags);
823 }
824 }
825 continue;
826 }
827 if matches!(name.as_str(),
829 "MinSampleValue" | "MaxSampleValue" | "ProcessingSoftware" | "PanasonicTitle" | "PanasonicTitle2" ) {
833 continue;
834 }
835
836 let print_value =
837 exif_tags::print_conv(ifd_name, entry.tag, &value)
838 .or_else(|| {
839 value.as_u64()
841 .and_then(|v| crate::tags::print_conv_generated::print_conv_by_name(&name, v as i64))
842 .map(|s| s.to_string())
843 })
844 .unwrap_or_else(|| value.to_display_string());
845
846 tags.push(Tag {
847 id: TagId::Numeric(entry.tag),
848 name,
849 description,
850 group: TagGroup {
851 family0: "EXIF".to_string(),
852 family1: ifd_name.to_string(),
853 family2,
854 },
855 raw_value: value,
856 print_value,
857 priority: 0,
858 });
859 }
860 }
861
862 let next_ifd_offset = if entries_end + 4 <= data.len() {
864 read_u32(data, entries_end, header.byte_order)
865 } else { 0 };
866 if next_ifd_offset != 0 && ifd_name == "IFD0" {
867 let ifd1_start_idx = tags.len();
869 let ifd1_next = Self::read_ifd(data, header, next_ifd_offset, "IFD1", tags)
870 .ok().flatten();
871 {
874 let ifd0_names: std::collections::HashSet<String> = tags[..ifd1_start_idx].iter()
875 .map(|t| t.name.clone())
876 .collect();
877 let thumbnail_tags = ["ThumbnailOffset", "ThumbnailLength", "ThumbnailImage",
878 "Compression", "PhotometricInterpretation", "JPEGInterchangeFormat",
879 "JPEGInterchangeFormatLength",
880 "SubfileType", "StripOffsets", "StripByteCounts"];
881 tags.retain(|t| {
882 if t.group.family1 != "IFD1" { return true; }
883 if thumbnail_tags.contains(&t.name.as_str()) { return true; }
885 !ifd0_names.contains(&t.name)
887 });
888 }
889
890 let thumb_offset = tags.iter()
892 .find(|t| t.name == "ThumbnailOffset" && t.group.family1 == "IFD1")
893 .and_then(|t| t.raw_value.as_u64());
894 let thumb_length = tags.iter()
895 .find(|t| t.name == "ThumbnailLength" && t.group.family1 == "IFD1")
896 .and_then(|t| t.raw_value.as_u64());
897
898 if let (Some(off), Some(len)) = (thumb_offset, thumb_length) {
899 let off = off as usize;
900 let len = len as usize;
901 if off + len <= data.len() && len > 0 {
902 tags.push(Tag {
903 id: TagId::Text("ThumbnailImage".into()),
904 name: "ThumbnailImage".into(),
905 description: "Thumbnail Image".into(),
906 group: TagGroup { family0: "EXIF".into(), family1: "IFD1".into(), family2: "Image".into() },
907 raw_value: Value::Binary(data[off..off+len].to_vec()),
908 print_value: format!("(Binary data {} bytes)", len),
909 priority: 0,
910 });
911 }
912 }
913
914 let is_cr2 = data.len() > 10 && &data[8..10] == b"CR";
917 if is_cr2 {
918 if let Some(ifd2_offset) = ifd1_next {
919 let ifd2_next = Self::read_ifd(data, header, ifd2_offset, "IFD2", tags)
921 .ok().flatten();
922 if let Some(ifd3_offset) = ifd2_next {
924 let _ = Self::read_ifd(data, header, ifd3_offset, "IFD3", tags);
925 }
926 }
927 }
928 }
929
930 Ok(if next_ifd_offset != 0 {
931 Some(next_ifd_offset)
932 } else {
933 None
934 })
935 }
936
937 pub fn read_as_named_ifd(data: &[u8], ifd_name: &str) -> Vec<Tag> {
941 let header = match parse_tiff_header(data) {
942 Ok(h) => h,
943 Err(_) => return Vec::new(),
944 };
945 let mut tags = Vec::new();
946 let _ = Self::read_ifd(data, &header, header.ifd0_offset, ifd_name, &mut tags);
947 tags
948 }
949}
950
951fn parse_ifd_entry(data: &[u8], offset: usize, byte_order: ByteOrderMark) -> IfdEntry {
952 let tag = read_u16(data, offset, byte_order);
953 let data_type = read_u16(data, offset + 2, byte_order);
954 let count = read_u32(data, offset + 4, byte_order);
955 let value_offset = read_u32(data, offset + 8, byte_order);
956 let mut inline_data = [0u8; 4];
957 inline_data.copy_from_slice(&data[offset + 8..offset + 12]);
958
959 IfdEntry {
960 tag,
961 data_type,
962 count,
963 value_offset,
964 inline_data,
965 }
966}
967
968fn read_ifd_value(data: &[u8], entry: &IfdEntry, byte_order: ByteOrderMark) -> Option<Value> {
969 let elem_size = type_size(entry.data_type)?;
970 let total_size = elem_size * entry.count as usize;
971
972 let value_data = if total_size <= 4 {
973 &entry.inline_data[..total_size]
974 } else {
975 let offset = entry.value_offset as usize;
976 if offset + total_size > data.len() {
977 return None;
978 }
979 &data[offset..offset + total_size]
980 };
981
982 if entry.tag == 0x83BB {
984 return Some(Value::Binary(value_data.to_vec()));
985 }
986
987 if entry.tag == 0x02BC {
989 return Some(Value::Binary(value_data.to_vec()));
990 }
991
992 match entry.data_type {
993 1 => {
995 if entry.count == 1 {
996 Some(Value::U8(value_data[0]))
997 } else {
998 Some(Value::List(value_data.iter().map(|&b| Value::U8(b)).collect()))
999 }
1000 }
1001 2 => {
1003 let s = String::from_utf8_lossy(value_data);
1004 Some(Value::String(s.trim_end_matches('\0').to_string()))
1005 }
1006 3 => {
1008 if entry.count == 1 {
1009 Some(Value::U16(read_u16(value_data, 0, byte_order)))
1010 } else {
1011 let vals: Vec<Value> = (0..entry.count as usize)
1012 .map(|i| Value::U16(read_u16(value_data, i * 2, byte_order)))
1013 .collect();
1014 Some(Value::List(vals))
1015 }
1016 }
1017 4 | 13 => {
1019 if entry.count == 1 {
1020 Some(Value::U32(read_u32(value_data, 0, byte_order)))
1021 } else {
1022 let vals: Vec<Value> = (0..entry.count as usize)
1023 .map(|i| Value::U32(read_u32(value_data, i * 4, byte_order)))
1024 .collect();
1025 Some(Value::List(vals))
1026 }
1027 }
1028 5 => {
1030 if entry.count == 1 {
1031 let n = read_u32(value_data, 0, byte_order);
1032 let d = read_u32(value_data, 4, byte_order);
1033 Some(Value::URational(n, d))
1034 } else {
1035 let vals: Vec<Value> = (0..entry.count as usize)
1036 .map(|i| {
1037 let n = read_u32(value_data, i * 8, byte_order);
1038 let d = read_u32(value_data, i * 8 + 4, byte_order);
1039 Value::URational(n, d)
1040 })
1041 .collect();
1042 Some(Value::List(vals))
1043 }
1044 }
1045 6 => {
1047 if entry.count == 1 {
1048 Some(Value::I16(value_data[0] as i8 as i16))
1049 } else {
1050 let vals: Vec<Value> = value_data
1051 .iter()
1052 .map(|&b| Value::I16(b as i8 as i16))
1053 .collect();
1054 Some(Value::List(vals))
1055 }
1056 }
1057 7 => Some(Value::Undefined(value_data.to_vec())),
1059 8 => {
1061 if entry.count == 1 {
1062 Some(Value::I16(read_i16(value_data, 0, byte_order)))
1063 } else {
1064 let vals: Vec<Value> = (0..entry.count as usize)
1065 .map(|i| Value::I16(read_i16(value_data, i * 2, byte_order)))
1066 .collect();
1067 Some(Value::List(vals))
1068 }
1069 }
1070 9 => {
1072 if entry.count == 1 {
1073 Some(Value::I32(read_i32(value_data, 0, byte_order)))
1074 } else {
1075 let vals: Vec<Value> = (0..entry.count as usize)
1076 .map(|i| Value::I32(read_i32(value_data, i * 4, byte_order)))
1077 .collect();
1078 Some(Value::List(vals))
1079 }
1080 }
1081 10 => {
1083 if entry.count == 1 {
1084 let n = read_i32(value_data, 0, byte_order);
1085 let d = read_i32(value_data, 4, byte_order);
1086 Some(Value::IRational(n, d))
1087 } else {
1088 let vals: Vec<Value> = (0..entry.count as usize)
1089 .map(|i| {
1090 let n = read_i32(value_data, i * 8, byte_order);
1091 let d = read_i32(value_data, i * 8 + 4, byte_order);
1092 Value::IRational(n, d)
1093 })
1094 .collect();
1095 Some(Value::List(vals))
1096 }
1097 }
1098 11 => {
1100 if entry.count == 1 {
1101 let bits = read_u32(value_data, 0, byte_order);
1102 Some(Value::F32(f32::from_bits(bits)))
1103 } else {
1104 let vals: Vec<Value> = (0..entry.count as usize)
1105 .map(|i| {
1106 let bits = read_u32(value_data, i * 4, byte_order);
1107 Value::F32(f32::from_bits(bits))
1108 })
1109 .collect();
1110 Some(Value::List(vals))
1111 }
1112 }
1113 12 => {
1115 if entry.count == 1 {
1116 let bits = read_u64(value_data, 0, byte_order);
1117 Some(Value::F64(f64::from_bits(bits)))
1118 } else {
1119 let vals: Vec<Value> = (0..entry.count as usize)
1120 .map(|i| {
1121 let bits = read_u64(value_data, i * 8, byte_order);
1122 Value::F64(f64::from_bits(bits))
1123 })
1124 .collect();
1125 Some(Value::List(vals))
1126 }
1127 }
1128 _ => None,
1129 }
1130}
1131
1132fn read_u16(data: &[u8], offset: usize, bo: ByteOrderMark) -> u16 {
1134 match bo {
1135 ByteOrderMark::LittleEndian => LittleEndian::read_u16(&data[offset..]),
1136 ByteOrderMark::BigEndian => BigEndian::read_u16(&data[offset..]),
1137 }
1138}
1139
1140fn read_u32(data: &[u8], offset: usize, bo: ByteOrderMark) -> u32 {
1141 match bo {
1142 ByteOrderMark::LittleEndian => LittleEndian::read_u32(&data[offset..]),
1143 ByteOrderMark::BigEndian => BigEndian::read_u32(&data[offset..]),
1144 }
1145}
1146
1147fn read_u64(data: &[u8], offset: usize, bo: ByteOrderMark) -> u64 {
1148 match bo {
1149 ByteOrderMark::LittleEndian => LittleEndian::read_u64(&data[offset..]),
1150 ByteOrderMark::BigEndian => BigEndian::read_u64(&data[offset..]),
1151 }
1152}
1153
1154fn read_i16(data: &[u8], offset: usize, bo: ByteOrderMark) -> i16 {
1155 match bo {
1156 ByteOrderMark::LittleEndian => LittleEndian::read_i16(&data[offset..]),
1157 ByteOrderMark::BigEndian => BigEndian::read_i16(&data[offset..]),
1158 }
1159}
1160
1161fn read_i32(data: &[u8], offset: usize, bo: ByteOrderMark) -> i32 {
1162 match bo {
1163 ByteOrderMark::LittleEndian => LittleEndian::read_i32(&data[offset..]),
1164 ByteOrderMark::BigEndian => BigEndian::read_i32(&data[offset..]),
1165 }
1166}
1167
1168fn process_geotiff_keys(tags: &mut Vec<Tag>) {
1171 let dir_vals: Option<Vec<u16>> = tags.iter()
1173 .find(|t| t.name == "GeoTiffDirectory")
1174 .and_then(|t| {
1175 match &t.raw_value {
1176 Value::List(items) => {
1177 let vals: Vec<u16> = items.iter().filter_map(|v| {
1178 match v {
1179 Value::U16(x) => Some(*x),
1180 Value::U32(x) => Some(*x as u16),
1181 _ => None,
1182 }
1183 }).collect();
1184 if vals.is_empty() { None } else { Some(vals) }
1185 }
1186 _ => None,
1187 }
1188 });
1189
1190 let dir_vals = match dir_vals {
1191 Some(v) => v,
1192 None => return,
1193 };
1194
1195 if dir_vals.len() < 4 {
1196 return;
1197 }
1198
1199 let version = dir_vals[0];
1200 let revision = dir_vals[1];
1201 let minor_rev = dir_vals[2];
1202 let num_entries = dir_vals[3] as usize;
1203
1204 if dir_vals.len() < 4 + num_entries * 4 {
1205 return;
1206 }
1207
1208 let ascii_params: Option<String> = tags.iter()
1210 .find(|t| t.name == "GeoTiffAsciiParams")
1211 .map(|t| t.print_value.clone());
1212
1213 let double_params: Option<Vec<f64>> = tags.iter()
1215 .find(|t| t.name == "GeoTiffDoubleParams")
1216 .and_then(|t| {
1217 match &t.raw_value {
1218 Value::List(items) => {
1219 let vals: Vec<f64> = items.iter().filter_map(|v| {
1220 match v {
1221 Value::F64(x) => Some(*x),
1222 Value::F32(x) => Some(*x as f64),
1223 _ => None,
1224 }
1225 }).collect();
1226 if vals.is_empty() { None } else { Some(vals) }
1227 }
1228 _ => None,
1229 }
1230 });
1231
1232 let mut new_tags = Vec::new();
1233
1234 new_tags.push(Tag {
1236 id: TagId::Text("GeoTiffVersion".to_string()),
1237 name: "GeoTiffVersion".to_string(),
1238 description: "GeoTiff Version".to_string(),
1239 group: TagGroup { family0: "EXIF".into(), family1: "IFD0".into(), family2: "Location".into() },
1240 raw_value: Value::String(format!("{}.{}.{}", version, revision, minor_rev)),
1241 print_value: format!("{}.{}.{}", version, revision, minor_rev),
1242 priority: 0,
1243 });
1244
1245 for i in 0..num_entries {
1247 let base = 4 + i * 4;
1248 let key_id = dir_vals[base];
1249 let location = dir_vals[base + 1];
1250 let count = dir_vals[base + 2] as usize;
1251 let value_or_offset = dir_vals[base + 3];
1252
1253 let raw_val: Option<String> = match location {
1254 0 => {
1255 Some(format!("{}", value_or_offset))
1257 }
1258 34737 => {
1259 if let Some(ref ascii) = ascii_params {
1261 let off = value_or_offset as usize;
1262 let end = (off + count).min(ascii.len());
1263 if off <= end {
1264 let s = &ascii[off..end];
1265 let s = s.trim_end_matches('|').trim().to_string();
1267 Some(s)
1268 } else {
1269 None
1270 }
1271 } else {
1272 None
1273 }
1274 }
1275 34736 => {
1276 if let Some(ref doubles) = double_params {
1278 let off = value_or_offset as usize;
1279 if count == 1 && off < doubles.len() {
1280 Some(format!("{}", doubles[off]))
1281 } else if count > 1 {
1282 let vals: Vec<String> = doubles.iter().skip(off).take(count)
1283 .map(|v| format!("{}", v)).collect();
1284 Some(vals.join(" "))
1285 } else {
1286 None
1287 }
1288 } else {
1289 None
1290 }
1291 }
1292 _ => None,
1293 };
1294
1295 let val_str = match raw_val {
1296 Some(v) => v,
1297 None => continue,
1298 };
1299
1300 let (tag_name, print_val) = geotiff_key_to_tag(key_id, &val_str);
1302 if tag_name.is_empty() { continue; }
1303
1304 new_tags.push(Tag {
1305 id: TagId::Text(tag_name.clone()),
1306 name: tag_name.clone(),
1307 description: tag_name.clone(),
1308 group: TagGroup { family0: "EXIF".into(), family1: "IFD0".into(), family2: "Location".into() },
1309 raw_value: Value::String(val_str),
1310 print_value: print_val,
1311 priority: 0,
1312 });
1313 }
1314
1315 if !new_tags.is_empty() {
1316 tags.retain(|t| t.name != "GeoTiffDirectory" && t.name != "GeoTiffAsciiParams" && t.name != "GeoTiffDoubleParams");
1318 tags.extend(new_tags);
1319 }
1320}
1321
1322fn geotiff_key_to_tag(key_id: u16, value: &str) -> (String, String) {
1324 let val_u16: Option<u16> = value.parse().ok();
1325
1326 match key_id {
1327 0x0001 => return ("GeoTiffVersion".to_string(), value.to_string()), 0x0400 => { let print = match val_u16 {
1331 Some(1) => "Projected".to_string(),
1332 Some(2) => "Geographic".to_string(),
1333 Some(3) => "Geocentric".to_string(),
1334 Some(32767) => "User Defined".to_string(),
1335 _ => value.to_string(),
1336 };
1337 return ("GTModelType".to_string(), print);
1338 }
1339 0x0401 => { let print = match val_u16 {
1341 Some(1) => "Pixel Is Area".to_string(),
1342 Some(2) => "Pixel Is Point".to_string(),
1343 Some(32767) => "User Defined".to_string(),
1344 _ => value.to_string(),
1345 };
1346 return ("GTRasterType".to_string(), print);
1347 }
1348 0x0402 => return ("GTCitation".to_string(), value.to_string()),
1349
1350 0x0800 => return ("GeographicType".to_string(), geotiff_pcs_name(val_u16.unwrap_or(0), value)),
1352 0x0801 => return ("GeogCitation".to_string(), value.to_string()),
1353 0x0802 => return ("GeogGeodeticDatum".to_string(), value.to_string()),
1354 0x0803 => return ("GeogPrimeMeridian".to_string(), value.to_string()),
1355 0x0804 => return ("GeogLinearUnits".to_string(), geotiff_linear_unit_name(val_u16.unwrap_or(0), value)),
1356 0x0805 => return ("GeogLinearUnitSize".to_string(), value.to_string()),
1357 0x0806 => return ("GeogAngularUnits".to_string(), value.to_string()),
1358 0x0807 => return ("GeogAngularUnitSize".to_string(), value.to_string()),
1359 0x0808 => return ("GeogEllipsoid".to_string(), value.to_string()),
1360 0x0809 => return ("GeogSemiMajorAxis".to_string(), value.to_string()),
1361 0x080a => return ("GeogSemiMinorAxis".to_string(), value.to_string()),
1362 0x080b => return ("GeogInvFlattening".to_string(), value.to_string()),
1363 0x080c => return ("GeogAzimuthUnits".to_string(), value.to_string()),
1364 0x080d => return ("GeogPrimeMeridianLong".to_string(), value.to_string()),
1365
1366 0x0C00 => { return ("ProjectedCSType".to_string(), geotiff_pcs_name(val_u16.unwrap_or(0), value));
1369 }
1370 0x0C01 => return ("PCSCitation".to_string(), value.to_string()),
1371 0x0C02 => return ("Projection".to_string(), value.to_string()),
1372 0x0C03 => return ("ProjCoordTrans".to_string(), value.to_string()),
1373 0x0C04 => return ("ProjLinearUnits".to_string(), geotiff_linear_unit_name(val_u16.unwrap_or(0), value)),
1374 0x0C05 => return ("ProjLinearUnitSize".to_string(), value.to_string()),
1375 0x0C06 => return ("ProjStdParallel1".to_string(), value.to_string()),
1376 0x0C07 => return ("ProjStdParallel2".to_string(), value.to_string()),
1377 0x0C08 => return ("ProjNatOriginLong".to_string(), value.to_string()),
1378 0x0C09 => return ("ProjNatOriginLat".to_string(), value.to_string()),
1379 0x0C0a => return ("ProjFalseEasting".to_string(), value.to_string()),
1380 0x0C0b => return ("ProjFalseNorthing".to_string(), value.to_string()),
1381 0x0C0c => return ("ProjFalseOriginLong".to_string(), value.to_string()),
1382 0x0C0d => return ("ProjFalseOriginLat".to_string(), value.to_string()),
1383 0x0C0e => return ("ProjFalseOriginEasting".to_string(), value.to_string()),
1384 0x0C0f => return ("ProjFalseOriginNorthing".to_string(), value.to_string()),
1385 0x0C10 => return ("ProjCenterLong".to_string(), value.to_string()),
1386 0x0C11 => return ("ProjCenterLat".to_string(), value.to_string()),
1387 0x0C12 => return ("ProjCenterEasting".to_string(), value.to_string()),
1388 0x0C13 => return ("ProjCenterNorthing".to_string(), value.to_string()),
1389 0x0C14 => return ("ProjScaleAtNatOrigin".to_string(), value.to_string()),
1390 0x0C15 => return ("ProjScaleAtCenter".to_string(), value.to_string()),
1391 0x0C16 => return ("ProjAzimuthAngle".to_string(), value.to_string()),
1392 0x0C17 => return ("ProjStraightVertPoleLong".to_string(), value.to_string()),
1393
1394 0x1000 => return ("VerticalCSType".to_string(), value.to_string()),
1396 0x1001 => return ("VerticalCitation".to_string(), value.to_string()),
1397 0x1002 => return ("VerticalDatum".to_string(), value.to_string()),
1398 0x1003 => return ("VerticalUnits".to_string(), geotiff_linear_unit_name(val_u16.unwrap_or(0), value)),
1399
1400 _ => {}
1401 }
1402 (String::new(), String::new())
1403}
1404
1405fn geotiff_linear_unit_name(val: u16, fallback: &str) -> String {
1406 match val {
1407 9001 => "Linear Meter".to_string(),
1408 9002 => "Linear Foot".to_string(),
1409 9003 => "Linear Foot US Survey".to_string(),
1410 9004 => "Linear Foot Modified American".to_string(),
1411 9005 => "Linear Foot Clarke".to_string(),
1412 9006 => "Linear Foot Indian".to_string(),
1413 9007 => "Linear Link".to_string(),
1414 9008 => "Linear Link Benoit".to_string(),
1415 9009 => "Linear Link Sears".to_string(),
1416 9010 => "Linear Chain Benoit".to_string(),
1417 9011 => "Linear Chain Sears".to_string(),
1418 9012 => "Linear Yard Sears".to_string(),
1419 9013 => "Linear Yard Indian".to_string(),
1420 9014 => "Linear Fathom".to_string(),
1421 9015 => "Linear Mile International Nautical".to_string(),
1422 _ => fallback.to_string(),
1423 }
1424}
1425
1426fn geotiff_pcs_name(val: u16, fallback: &str) -> String {
1427 match val {
1429 26918 => "NAD83 UTM zone 18N".to_string(),
1430 26919 => "NAD83 UTM zone 19N".to_string(),
1431 32618 => "WGS84 UTM zone 18N".to_string(),
1432 32619 => "WGS84 UTM zone 19N".to_string(),
1433 4326 => "WGS 84".to_string(),
1434 4269 => "NAD83".to_string(),
1435 4267 => "NAD27".to_string(),
1436 32767 => "User Defined".to_string(),
1437 _ => fallback.to_string(),
1438 }
1439}