1#[cfg(test)]
30use alloc::vec::Vec;
31
32use zencodec::GainMapParams as GainMapMetadata;
33
34pub mod tags {
36 pub const HDR_IMAGE_TYPE: u16 = 0x000A;
38 pub const HDR_HEADROOM: u16 = 0x0021;
40 pub const HDR_GAIN: u16 = 0x0030;
42}
43
44const TIFF_TAG_EXIF_IFD: u16 = 0x8769;
46const TIFF_TAG_MAKERNOTE: u16 = 0x927C;
48
49#[derive(Clone, Copy, Debug, PartialEq, Default)]
51pub struct AppleHdrInfo {
52 pub hdr_headroom: Option<f64>,
55 pub hdr_gain: f64,
58 pub hdr_image_type: Option<i32>,
60}
61
62impl AppleHdrInfo {
63 pub fn headroom_stops(&self) -> Option<f64> {
75 let maker33 = self.hdr_headroom?;
76 let maker48 = self.hdr_gain;
77 if !maker33.is_finite() || !maker48.is_finite() {
78 return None;
79 }
80 self.headroom_stops_compatibility()
81 }
82
83 fn headroom_stops_compatibility(&self) -> Option<f64> {
84 let maker33 = self.hdr_headroom?;
85 let maker48 = self.hdr_gain;
86 let stops = if maker33 < 1.0 {
87 if maker48 <= 0.01 {
88 -20.0 * maker48 + 1.8
89 } else {
90 -0.101 * maker48 + 1.601
91 }
92 } else if maker48 <= 0.01 {
93 -70.0 * maker48 + 3.0
94 } else {
95 -0.44 * maker48 + 2.86
96 };
97 Some(if stops > 0.0 { stops } else { 0.0 })
99 }
100
101 pub fn is_hdr(&self) -> bool {
103 self.hdr_image_type == Some(3)
104 }
105
106 pub fn has_gain_map(&self) -> bool {
108 self.hdr_headroom.is_some()
109 }
110}
111
112pub fn from_apple_headroom(info: &AppleHdrInfo) -> Option<GainMapMetadata> {
125 let stops = info.headroom_stops()?;
126 let mut params = GainMapMetadata::default();
128 params.base_hdr_headroom = 0.0;
129 params.alternate_hdr_headroom = stops;
130 for ch in &mut params.channels {
131 ch.min = 0.0;
132 ch.max = stops;
133 ch.gamma = 1.0;
134 }
135 Some(params)
136}
137
138pub fn parse_exif_for_apple_hdr(exif: &[u8]) -> Option<AppleHdrInfo> {
146 parse_exif_with_mode(exif, Interpretation::Strict)
147}
148
149fn parse_exif_with_mode(exif: &[u8], mode: Interpretation) -> Option<AppleHdrInfo> {
150 let (tiff, endian) = tiff_start(exif)?;
151 let ifd0_off = endian.u32(tiff, 4)? as usize;
153 let (kind, count, exif_ptr) = ifd_find(tiff, endian, ifd0_off, TIFF_TAG_EXIF_IFD, mode)?;
155 if mode == Interpretation::Strict && (kind != 4 || count != 1) {
156 return None;
157 }
158 let exif_ifd_off = endian.u32(exif_ptr, 0)? as usize;
159 let (kind, _, maker) = ifd_find(tiff, endian, exif_ifd_off, TIFF_TAG_MAKERNOTE, mode)?;
161 if mode == Interpretation::Strict && kind != 7 {
162 return None;
163 }
164 parse_makernote_with_mode(maker, mode)
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169pub struct MakerNoteEntry<'a> {
170 tag: u16,
171 kind: u16,
172 count: u32,
173 value: &'a [u8],
174}
175impl<'a> MakerNoteEntry<'a> {
176 pub fn tag(&self) -> u16 {
178 self.tag
179 }
180 pub fn kind(&self) -> u16 {
182 self.kind
183 }
184 pub fn count(&self) -> u32 {
186 self.count
187 }
188 pub fn value(&self) -> &'a [u8] {
190 self.value
191 }
192}
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195#[non_exhaustive]
196pub struct MakerNoteEntryError {
197 pub index: usize,
199 pub tag: Option<u16>,
201}
202#[derive(Debug)]
205pub struct AppleMakerNote<'a> {
206 data: &'a [u8],
207 endian: Endian,
208 entries: usize,
209 count: usize,
210}
211impl<'a> AppleMakerNote<'a> {
212 pub fn parse(data: &'a [u8]) -> Option<Self> {
214 Self::parse_with_mode(data, Interpretation::Strict)
215 }
216
217 fn parse_with_mode(data: &'a [u8], mode: Interpretation) -> Option<Self> {
218 if (mode == Interpretation::Strict && data.len() > 16 * 1024 * 1024)
219 || (mode == Interpretation::Compatibility && data.len() < 20)
220 || data.get(..10)? != b"Apple iOS\0"
221 {
222 return None;
223 }
224 let endian = match data.get(12..14)? {
225 b"MM" => Endian::Big,
226 b"II" => Endian::Little,
227 _ => return None,
228 };
229 let ifd = if endian.u16(data, 14)? == 42 {
230 12usize.checked_add(endian.u32(data, 16)? as usize)?
231 } else {
232 14
233 };
234 let count = endian.u16(data, ifd)? as usize;
235 if mode == Interpretation::Strict && count > 4096 {
236 return None;
237 }
238 Some(Self {
239 data,
240 endian,
241 entries: ifd.checked_add(2)?,
242 count,
243 })
244 }
245 pub fn byte_order(&self) -> zencodec::exif::ByteOrder {
247 match self.endian {
248 Endian::Big => zencodec::exif::ByteOrder::Big,
249 Endian::Little => zencodec::exif::ByteOrder::Little,
250 }
251 }
252 pub fn entries(
254 &self,
255 ) -> impl Iterator<Item = core::result::Result<MakerNoteEntry<'a>, MakerNoteEntryError>> + '_
256 {
257 self.entries_with_mode(Interpretation::Strict)
258 }
259
260 fn entries_with_mode(
261 &self,
262 mode: Interpretation,
263 ) -> impl Iterator<Item = core::result::Result<MakerNoteEntry<'a>, MakerNoteEntryError>> + '_
264 {
265 (0..self.count).map(move |index| {
266 let offset = self.entries + index * 12;
267 let tag = self.endian.u16(self.data, offset);
268 let error = MakerNoteEntryError { index, tag };
269 if mode == Interpretation::Compatibility {
270 self.data.get(offset..offset + 12).ok_or(error)?;
271 }
272 let tag = tag.ok_or(error)?;
273 let kind = self.endian.u16(self.data, offset + 2).ok_or(error)?;
274 let count = self.endian.u32(self.data, offset + 4).ok_or(error)?;
275 if mode == Interpretation::Strict
276 && !(1..=13).contains(&kind)
277 && !(16..=18).contains(&kind)
278 {
279 return Err(error);
280 }
281 let size = mode
282 .type_size(kind)
283 .checked_mul(count as usize)
284 .ok_or(error)?;
285 let start = if size <= 4 {
286 offset + 8
287 } else {
288 self.endian.u32(self.data, offset + 8).ok_or(error)? as usize
289 };
290 let value = self
291 .data
292 .get(start..start.checked_add(size).ok_or(error)?)
293 .ok_or(error)?;
294 Ok(MakerNoteEntry {
295 tag,
296 kind,
297 count,
298 value,
299 })
300 })
301 }
302}
303
304pub fn parse_apple_makernote(maker: &[u8]) -> Option<AppleHdrInfo> {
313 parse_makernote_with_mode(maker, Interpretation::Strict)
314}
315
316fn parse_makernote_with_mode(maker: &[u8], mode: Interpretation) -> Option<AppleHdrInfo> {
317 let view = AppleMakerNote::parse_with_mode(maker, mode)?;
318 let mut info = AppleHdrInfo::default();
319 let mut seen = 0u8;
320 for entry in view.entries_with_mode(mode) {
321 let entry = match entry {
322 Ok(entry) => entry,
323 Err(_) if mode == Interpretation::Compatibility => continue,
324 Err(_) => return None,
325 };
326 let bit = match entry.tag {
327 tags::HDR_HEADROOM => 1,
328 tags::HDR_GAIN => 2,
329 tags::HDR_IMAGE_TYPE => 4,
330 _ => continue,
331 };
332 if mode == Interpretation::Compatibility {
333 match entry.tag {
334 tags::HDR_HEADROOM => {
335 info.hdr_headroom = read_rational(entry.value, entry.kind, view.endian)
336 }
337 tags::HDR_GAIN => {
338 if let Some(gain) = read_rational(entry.value, entry.kind, view.endian) {
339 info.hdr_gain = gain;
340 }
341 }
342 tags::HDR_IMAGE_TYPE => {
343 info.hdr_image_type = read_int(
344 entry.value,
345 match entry.kind {
346 6 => 1,
347 8 => 3,
348 kind => kind,
349 },
350 view.endian,
351 )
352 }
353 _ => unreachable!(),
354 }
355 continue;
356 }
357 if seen & bit != 0 || entry.count != 1 {
358 return None;
359 }
360 seen |= bit;
361 match entry.tag {
362 tags::HDR_HEADROOM => {
363 info.hdr_headroom = Some(read_rational(entry.value, entry.kind, view.endian)?)
364 }
365 tags::HDR_GAIN => info.hdr_gain = read_rational(entry.value, entry.kind, view.endian)?,
366 tags::HDR_IMAGE_TYPE => {
367 info.hdr_image_type = Some(read_int(entry.value, entry.kind, view.endian)?)
368 }
369 _ => unreachable!(),
370 }
371 }
372 Some(info)
373}
374
375#[derive(Clone, Copy, PartialEq, Eq)]
376enum Interpretation {
377 Strict,
378 Compatibility,
379}
380
381impl Interpretation {
382 fn type_size(self, kind: u16) -> usize {
383 if self == Self::Compatibility && matches!(kind, 13 | 16..=18) {
384 1 } else {
386 type_size(kind)
387 }
388 }
389}
390
391pub mod compatibility {
398 use super::{AppleHdrInfo, Interpretation};
399
400 pub fn parse_exif_for_apple_hdr(exif: &[u8]) -> Option<AppleHdrInfo> {
402 super::parse_exif_with_mode(exif, Interpretation::Compatibility)
403 }
404
405 pub fn parse_apple_makernote(maker: &[u8]) -> Option<AppleHdrInfo> {
407 super::parse_makernote_with_mode(maker, Interpretation::Compatibility)
408 }
409
410 pub fn headroom_stops(info: &AppleHdrInfo) -> Option<f64> {
412 info.headroom_stops_compatibility()
413 }
414}
415
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420enum Endian {
421 Big,
422 Little,
423}
424
425impl Endian {
426 fn u16(self, b: &[u8], o: usize) -> Option<u16> {
427 let s = b.get(o..o.checked_add(2)?)?;
428 let a = [s[0], s[1]];
429 Some(match self {
430 Endian::Big => u16::from_be_bytes(a),
431 Endian::Little => u16::from_le_bytes(a),
432 })
433 }
434
435 fn u32(self, b: &[u8], o: usize) -> Option<u32> {
436 let s = b.get(o..o.checked_add(4)?)?;
437 let a = [s[0], s[1], s[2], s[3]];
438 Some(match self {
439 Endian::Big => u32::from_be_bytes(a),
440 Endian::Little => u32::from_le_bytes(a),
441 })
442 }
443
444 fn i32(self, b: &[u8], o: usize) -> Option<i32> {
445 self.u32(b, o).map(|v| v as i32)
446 }
447}
448
449fn type_size(dtype: u16) -> usize {
452 match dtype {
453 1 | 2 | 6 | 7 => 1, 3 | 8 => 2, 4 | 9 | 11 | 13 => 4, 5 | 10 | 12 | 16..=18 => 8, _ => 1,
458 }
459}
460
461fn tiff_start(exif: &[u8]) -> Option<(&[u8], Endian)> {
465 let window = exif.len().min(16);
466 for base in 0..window {
467 match exif.get(base..base + 4) {
468 Some(b"II\x2a\x00") => return Some((&exif[base..], Endian::Little)),
469 Some(b"MM\x00\x2a") => return Some((&exif[base..], Endian::Big)),
470 _ => {}
471 }
472 }
473 None
474}
475
476fn ifd_find(
480 tiff: &[u8],
481 endian: Endian,
482 ifd_off: usize,
483 want: u16,
484 mode: Interpretation,
485) -> Option<(u16, u32, &[u8])> {
486 let count = endian.u16(tiff, ifd_off)? as usize;
487 let entries = ifd_off.checked_add(2)?;
488 let mut result = None;
489 for i in 0..count {
490 let e = entries + i * 12;
491 if e + 12 > tiff.len() {
492 return None;
493 }
494 if endian.u16(tiff, e)? != want {
495 continue;
496 }
497 let dtype = endian.u16(tiff, e + 2)?;
498 let n = endian.u32(tiff, e + 4)?;
499 let total = mode.type_size(dtype).checked_mul(n as usize)?;
500 let value = if total <= 4 {
501 tiff.get(e + 8..e + 8 + total.min(4))?
502 } else {
503 let off = endian.u32(tiff, e + 8)? as usize;
504 tiff.get(off..off.checked_add(total)?)?
505 };
506 if mode == Interpretation::Compatibility {
507 return Some((dtype, n, value));
508 }
509 if result.replace((dtype, n, value)).is_some() {
510 return None;
511 }
512 }
513 result
514}
515
516fn read_rational(value: &[u8], dtype: u16, endian: Endian) -> Option<f64> {
520 if value.len() < 8 {
521 return None;
522 }
523 match dtype {
524 10 => {
525 let num = endian.i32(value, 0)?;
526 let den = endian.i32(value, 4)?;
527 if den == 0 {
528 None
529 } else {
530 Some(num as f64 / den as f64)
531 }
532 }
533 5 => {
534 let num = endian.u32(value, 0)?;
535 let den = endian.u32(value, 4)?;
536 if den == 0 {
537 None
538 } else {
539 Some(num as f64 / den as f64)
540 }
541 }
542 _ => None,
543 }
544}
545
546fn read_int(value: &[u8], dtype: u16, endian: Endian) -> Option<i32> {
548 match dtype {
549 3 => endian.u16(value, 0).map(|v| v as i32),
550 8 => endian.u16(value, 0).map(|v| v as i16 as i32),
551 4 | 9 => endian.i32(value, 0),
552 1 => value.first().map(|&b| b as i32),
553 6 => value.first().map(|&b| b as i8 as i32),
554 _ => None,
555 }
556}
557
558#[cfg(test)]
560#[path = "../tests/support/apple_0_6_0.rs"]
561mod released;
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use alloc::vec;
567
568 fn assert_released_parity(data: &[u8], exif: bool) {
569 let old = if exif {
570 released::parse_exif_for_apple_hdr(data)
571 } else {
572 released::parse_apple_makernote(data)
573 }
574 .map(|info| (info.hdr_headroom, info.hdr_gain, info.hdr_image_type));
575 let new = if exif {
576 compatibility::parse_exif_for_apple_hdr(data)
577 } else {
578 compatibility::parse_apple_makernote(data)
579 }
580 .map(|info| (info.hdr_headroom, info.hdr_gain, info.hdr_image_type));
581 assert_eq!(new, old, "exif={exif}, bytes={data:?}");
582 }
583
584 #[test]
585 fn compatibility_matches_released_parser_on_mutations_and_truncations() {
586 for (data, exif) in [
587 (build_apple_makernote_be(1686, 1000, 1, 20, 3), false),
588 (build_exif_with_apple_makernote(), true),
589 ] {
590 for end in 0..=data.len() {
591 assert_released_parity(&data[..end], exif);
592 }
593 for offset in 0..data.len() {
594 for value in [
595 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 16, 17, 18, 42, 127, 128, 255,
596 ] {
597 let mut changed = data.clone();
598 changed[offset] = value;
599 assert_released_parity(&changed, exif);
600 }
601 }
602 }
603 }
604
605 #[test]
606 fn oversized_exif_value_count_is_rejected_on_every_pointer_width() {
607 let mut exif = build_exif_with_apple_makernote();
608 exif[14..18].copy_from_slice(&u32::MAX.to_be_bytes());
611 assert_eq!(compatibility::parse_exif_for_apple_hdr(&exif), None);
612 assert_eq!(parse_exif_for_apple_hdr(&exif), None);
613 }
614
615 #[test]
616 fn compatibility_preserves_nonfinite_formula_behavior() {
617 for maker33 in [
618 f64::NEG_INFINITY,
619 -1.0,
620 0.0,
621 0.5,
622 1.0,
623 2.0,
624 f64::INFINITY,
625 f64::NAN,
626 ] {
627 for maker48 in [
628 f64::NEG_INFINITY,
629 -1.0,
630 0.0,
631 0.01,
632 0.5,
633 f64::INFINITY,
634 f64::NAN,
635 ] {
636 let old = released::AppleHdrInfo {
637 hdr_headroom: Some(maker33),
638 hdr_gain: maker48,
639 hdr_image_type: None,
640 };
641 let new = AppleHdrInfo {
642 hdr_headroom: Some(maker33),
643 hdr_gain: maker48,
644 hdr_image_type: None,
645 };
646 assert_eq!(compatibility::headroom_stops(&new), old.headroom_stops());
647 }
648 }
649 }
650
651 #[test]
652 fn strict_and_compatibility_rules_remain_distinct() {
653 let mut note = build_apple_makernote_be(1686, 1000, 1, 20, 3);
654 note[40..42].copy_from_slice(&tags::HDR_HEADROOM.to_be_bytes());
656 assert!(parse_apple_makernote(¬e).is_none());
657 assert_released_parity(¬e, false);
658 assert!(compatibility::parse_apple_makernote(¬e).is_some());
659 }
660
661 #[test]
662 fn headroom_formula_all_branches() {
663 let hi = AppleHdrInfo {
665 hdr_headroom: Some(1.686),
666 hdr_gain: 0.0,
667 hdr_image_type: Some(3),
668 };
669 assert!((hi.headroom_stops().unwrap() - 3.0).abs() < 1e-9);
670
671 let hi = AppleHdrInfo {
673 hdr_headroom: Some(2.0),
674 hdr_gain: 0.5,
675 hdr_image_type: None,
676 };
677 assert!((hi.headroom_stops().unwrap() - 2.64).abs() < 1e-9);
678
679 let hi = AppleHdrInfo {
681 hdr_headroom: Some(0.5),
682 hdr_gain: 0.0,
683 hdr_image_type: None,
684 };
685 assert!((hi.headroom_stops().unwrap() - 1.8).abs() < 1e-9);
686
687 let hi = AppleHdrInfo {
689 hdr_headroom: Some(0.5),
690 hdr_gain: 0.05,
691 hdr_image_type: None,
692 };
693 assert!((hi.headroom_stops().unwrap() - (-0.101 * 0.05 + 1.601)).abs() < 1e-9);
694 }
695
696 #[test]
697 fn no_headroom_tag_means_no_signal() {
698 let hi = AppleHdrInfo::default();
699 assert_eq!(hi.headroom_stops(), None);
700 assert!(!hi.has_gain_map());
701 assert_eq!(from_apple_headroom(&hi), None);
702 }
703
704 #[test]
705 fn maps_to_gain_map_params() {
706 let hi = AppleHdrInfo {
707 hdr_headroom: Some(1.686),
708 hdr_gain: 0.0,
709 hdr_image_type: Some(3),
710 };
711 let p = from_apple_headroom(&hi).unwrap();
712 assert!((p.alternate_hdr_headroom - 3.0).abs() < 1e-9);
713 assert_eq!(p.base_hdr_headroom, 0.0);
714 for ch in &p.channels {
715 assert_eq!(ch.min, 0.0);
716 assert!((ch.max - 3.0).abs() < 1e-9);
717 assert_eq!(ch.gamma, 1.0);
718 }
719 assert!(p.validate().is_ok());
720 }
721
722 #[test]
725 fn parse_apple_makernote_extracts_hdr_tags() {
726 let maker = build_apple_makernote_be(1686, 1000, 0, 1, 3);
727 let hi = parse_apple_makernote(&maker).unwrap();
728 assert_eq!(hi.hdr_headroom, Some(1.686));
729 assert_eq!(hi.hdr_gain, 0.0);
730 assert_eq!(hi.hdr_image_type, Some(3));
731 assert!(hi.is_hdr());
732 assert!((hi.headroom_stops().unwrap() - 3.0).abs() < 1e-9);
733 }
734
735 #[test]
738 fn parse_full_exif_walk() {
739 let exif = build_exif_with_apple_makernote();
740 let hi = parse_exif_for_apple_hdr(&exif).unwrap();
741 assert_eq!(hi.hdr_headroom, Some(1.686));
742 assert_eq!(hi.hdr_image_type, Some(3));
743 }
744
745 #[test]
746 fn rejects_non_apple_makernote() {
747 assert_eq!(parse_apple_makernote(b"Nikon\0\0\0not apple here!!"), None);
748 assert_eq!(parse_apple_makernote(b"short"), None);
749 }
750
751 #[test]
752 fn inspect_unknown_entries_and_report_truncation() {
753 let mut bytes = build_apple_makernote_be(1686, 1000, 0, 1, 3);
754 let view = AppleMakerNote::parse(&bytes).unwrap();
755 assert_eq!(view.entries().count(), 3);
756 assert!(view.entries().all(|e| e.is_ok()));
757 bytes[16..18].copy_from_slice(&0xf123u16.to_be_bytes());
759 let view = AppleMakerNote::parse(&bytes).unwrap();
760 assert_eq!(view.entries().next().unwrap().unwrap().tag(), 0xf123);
761 let truncated = &bytes[..24];
762 let view = AppleMakerNote::parse(truncated).unwrap();
763 assert!(view.entries().any(|e| e.is_err()));
764 }
765 #[test]
766 fn invalid_critical_rational_refuses_instead_of_guessing() {
767 let bytes = build_apple_makernote_be(1686, 0, 0, 1, 3);
768 assert!(parse_apple_makernote(&bytes).is_none());
769 assert!(
770 AppleHdrInfo {
771 hdr_headroom: Some(f64::NAN),
772 ..Default::default()
773 }
774 .headroom_stops()
775 .is_none()
776 );
777 }
778
779 fn be16(v: u16) -> [u8; 2] {
782 v.to_be_bytes()
783 }
784 fn be32(v: u32) -> [u8; 4] {
785 v.to_be_bytes()
786 }
787
788 fn ifd_entry(tag: u16, dtype: u16, count: u32, inline_or_off: [u8; 4]) -> Vec<u8> {
791 let mut e = Vec::new();
792 e.extend_from_slice(&be16(tag));
793 e.extend_from_slice(&be16(dtype));
794 e.extend_from_slice(&be32(count));
795 e.extend_from_slice(&inline_or_off);
796 e
797 }
798
799 fn build_apple_makernote_be(
803 hr_num: i32,
804 hr_den: i32,
805 g_num: i32,
806 g_den: i32,
807 img_type: u16,
808 ) -> Vec<u8> {
809 let mut blob = Vec::new();
812 blob.extend_from_slice(b"Apple iOS\0");
813 blob.extend_from_slice(&be16(14)); let n_entries: u16 = 3;
820 let ool_tiff = 2 + 2 + (n_entries as usize) * 12; let hr_off = (12 + ool_tiff) as u32; let g_off = (12 + ool_tiff + 8) as u32; let mut tiff = Vec::new();
828 tiff.extend_from_slice(b"MM");
829 tiff.extend_from_slice(&be16(n_entries));
830 let mut img_inline = [0u8; 4];
832 img_inline[..2].copy_from_slice(&be16(img_type));
833 tiff.extend_from_slice(&ifd_entry(tags::HDR_IMAGE_TYPE, 3, 1, img_inline));
834 tiff.extend_from_slice(&ifd_entry(tags::HDR_HEADROOM, 10, 1, be32(hr_off)));
836 tiff.extend_from_slice(&ifd_entry(tags::HDR_GAIN, 10, 1, be32(g_off)));
838 tiff.extend_from_slice(&be32(hr_num as u32));
840 tiff.extend_from_slice(&be32(hr_den as u32));
841 tiff.extend_from_slice(&be32(g_num as u32));
842 tiff.extend_from_slice(&be32(g_den as u32));
843
844 blob.extend_from_slice(&tiff);
845 blob
846 }
847
848 fn build_exif_with_apple_makernote() -> Vec<u8> {
851 let maker = build_apple_makernote_be(1686, 1000, 0, 1, 3);
852
853 let exif_ifd_off: u32 = 26;
860 let maker_off: u32 = 44;
861
862 let mut t = Vec::new();
863 t.extend_from_slice(b"MM\x00\x2a"); t.extend_from_slice(&be32(8)); t.extend_from_slice(&be16(1));
868 t.extend_from_slice(&ifd_entry(TIFF_TAG_EXIF_IFD, 4, 1, be32(exif_ifd_off)));
869 t.extend_from_slice(&be32(0)); t.extend_from_slice(&be16(1));
873 t.extend_from_slice(&ifd_entry(
874 TIFF_TAG_MAKERNOTE,
875 7,
876 maker.len() as u32,
877 be32(maker_off),
878 ));
879 t.extend_from_slice(&be32(0)); debug_assert_eq!(t.len() as u32, maker_off);
882 t.extend_from_slice(&maker);
883 t
884 }
885
886 #[test]
887 fn tolerates_leading_tiff_offset_prefix() {
888 let mut exif = vec![0u8, 0, 0, 0];
890 exif.extend_from_slice(&build_exif_with_apple_makernote());
891 let hi = parse_exif_for_apple_hdr(&exif).unwrap();
892 assert_eq!(hi.hdr_headroom, Some(1.686));
893 }
894}