use alloc::vec::Vec;
use crate::GainMapMetadata;
pub mod tags {
pub const HDR_IMAGE_TYPE: u16 = 0x000A;
pub const HDR_HEADROOM: u16 = 0x0021;
pub const HDR_GAIN: u16 = 0x0030;
}
const TIFF_TAG_EXIF_IFD: u16 = 0x8769;
const TIFF_TAG_MAKERNOTE: u16 = 0x927C;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct AppleHdrInfo {
pub hdr_headroom: Option<f64>,
pub hdr_gain: f64,
pub hdr_image_type: Option<i32>,
}
impl AppleHdrInfo {
pub fn headroom_stops(&self) -> Option<f64> {
let maker33 = self.hdr_headroom?;
let maker48 = self.hdr_gain;
let stops = if maker33 < 1.0 {
if maker48 <= 0.01 {
-20.0 * maker48 + 1.8
} else {
-0.101 * maker48 + 1.601
}
} else if maker48 <= 0.01 {
-70.0 * maker48 + 3.0
} else {
-0.44 * maker48 + 2.86
};
Some(if stops > 0.0 { stops } else { 0.0 })
}
pub fn is_hdr(&self) -> bool {
self.hdr_image_type == Some(3)
}
pub fn has_gain_map(&self) -> bool {
self.hdr_headroom.is_some()
}
}
pub fn from_apple_headroom(info: &AppleHdrInfo) -> Option<GainMapMetadata> {
let stops = info.headroom_stops()?;
let mut params = GainMapMetadata::default();
params.base_hdr_headroom = 0.0;
params.alternate_hdr_headroom = stops;
for ch in &mut params.channels {
ch.min = 0.0;
ch.max = stops;
ch.gamma = 1.0;
}
Some(params)
}
pub fn parse_exif_for_apple_hdr(exif: &[u8]) -> Option<AppleHdrInfo> {
let (tiff, endian) = tiff_start(exif)?;
let ifd0_off = endian.u32(tiff, 4)? as usize;
let (_, _, exif_ptr) = ifd_find(tiff, endian, ifd0_off, TIFF_TAG_EXIF_IFD)?;
let exif_ifd_off = endian.u32(&exif_ptr, 0)? as usize;
let (_, _, maker) = ifd_find(tiff, endian, exif_ifd_off, TIFF_TAG_MAKERNOTE)?;
parse_apple_makernote(&maker)
}
pub fn parse_apple_makernote(maker: &[u8]) -> Option<AppleHdrInfo> {
if maker.len() < 20 || &maker[..10] != b"Apple iOS\0" {
return None;
}
let bo = 12;
let endian = match (maker[bo], maker[bo + 1]) {
(b'M', b'M') => Endian::Big,
(b'I', b'I') => Endian::Little,
_ => return None,
};
let tiff = &maker[bo..];
let ifd_off = if endian.u16(tiff, 2)? == 42 {
endian.u32(tiff, 4)? as usize
} else {
2
};
let mut info = AppleHdrInfo::default();
let count = endian.u16(tiff, ifd_off)? as usize;
let entries = ifd_off + 2;
for i in 0..count {
let e = entries + i * 12;
if e + 12 > tiff.len() {
break;
}
let Some(tag) = endian.u16(tiff, e) else {
break;
};
if tag != tags::HDR_HEADROOM && tag != tags::HDR_GAIN && tag != tags::HDR_IMAGE_TYPE {
continue;
}
let (Some(dtype), Some(n)) = (endian.u16(tiff, e + 2), endian.u32(tiff, e + 4)) else {
continue;
};
let total = type_size(dtype).saturating_mul(n as usize);
let value = if total <= 4 {
match tiff.get(e + 8..e + 8 + total.min(4)) {
Some(v) => v.to_vec(),
None => continue,
}
} else {
let Some(off) = endian.u32(tiff, e + 8) else {
continue;
};
match maker.get(off as usize..(off as usize).saturating_add(total)) {
Some(v) => v.to_vec(),
None => continue,
}
};
match tag {
tags::HDR_HEADROOM => info.hdr_headroom = read_rational(&value, dtype, endian),
tags::HDR_GAIN => {
if let Some(g) = read_rational(&value, dtype, endian) {
info.hdr_gain = g;
}
}
tags::HDR_IMAGE_TYPE => info.hdr_image_type = read_int(&value, dtype, endian),
_ => {}
}
}
Some(info)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Endian {
Big,
Little,
}
impl Endian {
fn u16(self, b: &[u8], o: usize) -> Option<u16> {
let s = b.get(o..o + 2)?;
let a = [s[0], s[1]];
Some(match self {
Endian::Big => u16::from_be_bytes(a),
Endian::Little => u16::from_le_bytes(a),
})
}
fn u32(self, b: &[u8], o: usize) -> Option<u32> {
let s = b.get(o..o + 4)?;
let a = [s[0], s[1], s[2], s[3]];
Some(match self {
Endian::Big => u32::from_be_bytes(a),
Endian::Little => u32::from_le_bytes(a),
})
}
fn i32(self, b: &[u8], o: usize) -> Option<i32> {
self.u32(b, o).map(|v| v as i32)
}
}
fn type_size(dtype: u16) -> usize {
match dtype {
1 | 2 | 6 | 7 => 1, 3 | 8 => 2, 4 | 9 | 11 => 4, 5 | 10 | 12 => 8, _ => 1,
}
}
fn tiff_start(exif: &[u8]) -> Option<(&[u8], Endian)> {
let window = exif.len().min(16);
for base in 0..window {
match exif.get(base..base + 4) {
Some(b"II\x2a\x00") => return Some((&exif[base..], Endian::Little)),
Some(b"MM\x00\x2a") => return Some((&exif[base..], Endian::Big)),
_ => {}
}
}
None
}
fn ifd_find(tiff: &[u8], endian: Endian, ifd_off: usize, want: u16) -> Option<(u16, u32, Vec<u8>)> {
let count = endian.u16(tiff, ifd_off)? as usize;
let entries = ifd_off + 2;
for i in 0..count {
let e = entries + i * 12;
if e + 12 > tiff.len() {
break;
}
if endian.u16(tiff, e)? != want {
continue;
}
let dtype = endian.u16(tiff, e + 2)?;
let n = endian.u32(tiff, e + 4)?;
let total = type_size(dtype) * n as usize;
let value = if total <= 4 {
tiff.get(e + 8..e + 8 + total.min(4))?.to_vec()
} else {
let off = endian.u32(tiff, e + 8)? as usize;
tiff.get(off..off + total)?.to_vec()
};
return Some((dtype, n, value));
}
None
}
fn read_rational(value: &[u8], dtype: u16, endian: Endian) -> Option<f64> {
if value.len() < 8 {
return None;
}
match dtype {
10 => {
let num = endian.i32(value, 0)?;
let den = endian.i32(value, 4)?;
if den == 0 {
None
} else {
Some(num as f64 / den as f64)
}
}
5 => {
let num = endian.u32(value, 0)?;
let den = endian.u32(value, 4)?;
if den == 0 {
None
} else {
Some(num as f64 / den as f64)
}
}
_ => None,
}
}
fn read_int(value: &[u8], dtype: u16, endian: Endian) -> Option<i32> {
match dtype {
3 | 8 => endian.u16(value, 0).map(|v| v as i32),
4 | 9 => endian.i32(value, 0),
1 | 6 => value.first().map(|&b| b as i32),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn headroom_formula_all_branches() {
let hi = AppleHdrInfo {
hdr_headroom: Some(1.686),
hdr_gain: 0.0,
hdr_image_type: Some(3),
};
assert!((hi.headroom_stops().unwrap() - 3.0).abs() < 1e-9);
let hi = AppleHdrInfo {
hdr_headroom: Some(2.0),
hdr_gain: 0.5,
hdr_image_type: None,
};
assert!((hi.headroom_stops().unwrap() - 2.64).abs() < 1e-9);
let hi = AppleHdrInfo {
hdr_headroom: Some(0.5),
hdr_gain: 0.0,
hdr_image_type: None,
};
assert!((hi.headroom_stops().unwrap() - 1.8).abs() < 1e-9);
let hi = AppleHdrInfo {
hdr_headroom: Some(0.5),
hdr_gain: 0.05,
hdr_image_type: None,
};
assert!((hi.headroom_stops().unwrap() - (-0.101 * 0.05 + 1.601)).abs() < 1e-9);
}
#[test]
fn no_headroom_tag_means_no_signal() {
let hi = AppleHdrInfo::default();
assert_eq!(hi.headroom_stops(), None);
assert!(!hi.has_gain_map());
assert_eq!(from_apple_headroom(&hi), None);
}
#[test]
fn maps_to_gain_map_params() {
let hi = AppleHdrInfo {
hdr_headroom: Some(1.686),
hdr_gain: 0.0,
hdr_image_type: Some(3),
};
let p = from_apple_headroom(&hi).unwrap();
assert!((p.alternate_hdr_headroom - 3.0).abs() < 1e-9);
assert_eq!(p.base_hdr_headroom, 0.0);
for ch in &p.channels {
assert_eq!(ch.min, 0.0);
assert!((ch.max - 3.0).abs() < 1e-9);
assert_eq!(ch.gamma, 1.0);
}
assert!(p.validate().is_ok());
}
#[test]
fn parse_apple_makernote_extracts_hdr_tags() {
let maker = build_apple_makernote_be(1686, 1000, 0, 1, 3);
let hi = parse_apple_makernote(&maker).unwrap();
assert_eq!(hi.hdr_headroom, Some(1.686));
assert_eq!(hi.hdr_gain, 0.0);
assert_eq!(hi.hdr_image_type, Some(3));
assert!(hi.is_hdr());
assert!((hi.headroom_stops().unwrap() - 3.0).abs() < 1e-9);
}
#[test]
fn parse_full_exif_walk() {
let exif = build_exif_with_apple_makernote();
let hi = parse_exif_for_apple_hdr(&exif).unwrap();
assert_eq!(hi.hdr_headroom, Some(1.686));
assert_eq!(hi.hdr_image_type, Some(3));
}
#[test]
fn rejects_non_apple_makernote() {
assert_eq!(parse_apple_makernote(b"Nikon\0\0\0not apple here!!"), None);
assert_eq!(parse_apple_makernote(b"short"), None);
}
fn be16(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
fn be32(v: u32) -> [u8; 4] {
v.to_be_bytes()
}
fn ifd_entry(tag: u16, dtype: u16, count: u32, inline_or_off: [u8; 4]) -> Vec<u8> {
let mut e = Vec::new();
e.extend_from_slice(&be16(tag));
e.extend_from_slice(&be16(dtype));
e.extend_from_slice(&be32(count));
e.extend_from_slice(&inline_or_off);
e
}
fn build_apple_makernote_be(
hr_num: i32,
hr_den: i32,
g_num: i32,
g_den: i32,
img_type: u16,
) -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(b"Apple iOS\0");
blob.extend_from_slice(&be16(14));
let n_entries: u16 = 3;
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();
tiff.extend_from_slice(b"MM");
tiff.extend_from_slice(&be16(n_entries));
let mut img_inline = [0u8; 4];
img_inline[..2].copy_from_slice(&be16(img_type));
tiff.extend_from_slice(&ifd_entry(tags::HDR_IMAGE_TYPE, 3, 1, img_inline));
tiff.extend_from_slice(&ifd_entry(tags::HDR_HEADROOM, 10, 1, be32(hr_off)));
tiff.extend_from_slice(&ifd_entry(tags::HDR_GAIN, 10, 1, be32(g_off)));
tiff.extend_from_slice(&be32(hr_num as u32));
tiff.extend_from_slice(&be32(hr_den as u32));
tiff.extend_from_slice(&be32(g_num as u32));
tiff.extend_from_slice(&be32(g_den as u32));
blob.extend_from_slice(&tiff);
blob
}
fn build_exif_with_apple_makernote() -> Vec<u8> {
let maker = build_apple_makernote_be(1686, 1000, 0, 1, 3);
let exif_ifd_off: u32 = 26;
let maker_off: u32 = 44;
let mut t = Vec::new();
t.extend_from_slice(b"MM\x00\x2a"); t.extend_from_slice(&be32(8));
t.extend_from_slice(&be16(1));
t.extend_from_slice(&ifd_entry(TIFF_TAG_EXIF_IFD, 4, 1, be32(exif_ifd_off)));
t.extend_from_slice(&be32(0));
t.extend_from_slice(&be16(1));
t.extend_from_slice(&ifd_entry(
TIFF_TAG_MAKERNOTE,
7,
maker.len() as u32,
be32(maker_off),
));
t.extend_from_slice(&be32(0));
debug_assert_eq!(t.len() as u32, maker_off);
t.extend_from_slice(&maker);
t
}
#[test]
fn tolerates_leading_tiff_offset_prefix() {
let mut exif = vec![0u8, 0, 0, 0];
exif.extend_from_slice(&build_exif_with_apple_makernote());
let hi = parse_exif_for_apple_hdr(&exif).unwrap();
assert_eq!(hi.hdr_headroom, Some(1.686));
}
}