use std::ops::Range;
use ultrahdr_core::{Error, Result};
#[derive(Debug, Clone)]
pub struct MpfDirectory {
pub entries: Vec<MpfEntry>,
pub mpf_marker_offset: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct MpfEntry {
pub image_type: MpfImageType,
pub size: u32,
pub offset: u32,
pub index: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MpfImageType {
Primary,
LargeThumbnail,
MultiFramePanorama,
MultiFrameDisparity,
MultiFrameMultiAngle,
GainMap,
DepthMap,
Unknown(u32),
}
impl MpfImageType {
pub fn from_attribute(attr: u32) -> Self {
match attr {
0x03_0000 => MpfImageType::Primary,
0x00_0000 => MpfImageType::GainMap, _ => {
let type_code = (attr >> 24) & 0x07;
match type_code {
0 => {
if attr == 0 {
MpfImageType::GainMap
} else {
MpfImageType::Primary
}
}
1 => MpfImageType::LargeThumbnail,
2 => MpfImageType::MultiFramePanorama,
3 => MpfImageType::MultiFrameDisparity,
4 => MpfImageType::MultiFrameMultiAngle,
_ => MpfImageType::Unknown(attr),
}
}
}
}
pub fn to_attribute(self) -> u32 {
match self {
MpfImageType::Primary => 0x03_0000,
MpfImageType::GainMap => 0x00_0000,
MpfImageType::DepthMap => 0x00_0000,
MpfImageType::LargeThumbnail => 0x01_0001,
MpfImageType::MultiFramePanorama => 0x02_0002,
MpfImageType::MultiFrameDisparity => 0x03_0003,
MpfImageType::MultiFrameMultiAngle => 0x04_0004,
MpfImageType::Unknown(attr) => attr,
}
}
}
#[derive(Debug, Clone)]
pub struct AppSegment {
pub marker_num: u8,
pub data: Vec<u8>,
pub offset: usize,
}
impl AppSegment {
pub fn is_mpf(&self) -> bool {
self.marker_num == 2 && self.data.starts_with(b"MPF\0")
}
pub fn is_xmp(&self) -> bool {
self.marker_num == 1 && self.data.starts_with(b"http://ns.adobe.com/xap/1.0/\0")
}
pub fn is_exif(&self) -> bool {
self.marker_num == 1 && self.data.starts_with(b"Exif\0\0")
}
pub fn is_icc(&self) -> bool {
self.marker_num == 2 && self.data.starts_with(b"ICC_PROFILE\0")
}
pub fn is_jfif(&self) -> bool {
self.marker_num == 0 && self.data.starts_with(b"JFIF\0")
}
}
pub fn primary_bounds(data: &[u8]) -> Option<Range<usize>> {
if data.len() < 4 || data[0] != 0xFF || data[1] != 0xD8 {
return None;
}
let mut pos = 2;
while pos < data.len() - 1 {
if data[pos] == 0xFF && data[pos + 1] == 0xD9 {
return Some(0..pos + 2);
}
if data[pos] == 0xFF {
let marker = data[pos + 1];
if marker == 0x00 || marker == 0x01 || (0xD0..=0xD9).contains(&marker) || marker == 0xFF
{
pos += 2;
continue;
}
if pos + 4 <= data.len() {
let len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
if len >= 2 {
pos += 2 + len;
continue;
}
}
}
pos += 1;
}
None
}
pub fn scan_segments(data: &[u8]) -> Vec<AppSegment> {
let mut segments = Vec::new();
if data.len() < 4 || data[0] != 0xFF || data[1] != 0xD8 {
return segments;
}
let mut pos = 2;
while pos < data.len() - 3 {
if data[pos] != 0xFF {
pos += 1;
continue;
}
while pos < data.len() - 1 && data[pos + 1] == 0xFF {
pos += 1;
}
if pos >= data.len() - 1 {
break;
}
let marker = data[pos + 1];
let offset = pos;
if marker == 0xDA {
break;
}
if marker == 0xD8
|| marker == 0xD9
|| (0xD0..=0xD7).contains(&marker)
|| marker == 0x01
|| marker == 0x00
{
pos += 2;
continue;
}
if pos + 4 > data.len() {
break;
}
let length = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
if length < 2 || pos + 2 + length > data.len() {
break;
}
if (0xE0..=0xEF).contains(&marker) {
let marker_num = marker - 0xE0;
let segment_data = data[pos + 4..pos + 2 + length].to_vec();
segments.push(AppSegment {
marker_num,
data: segment_data,
offset,
});
}
pos += 2 + length;
}
segments
}
pub fn parse_mpf_segment(data: &[u8], mpf_marker_offset: usize) -> Result<MpfDirectory> {
let mpf_data = if data.starts_with(b"MPF\0") {
&data[4..]
} else {
data
};
if mpf_data.len() < 8 {
return Err(Error::MpfParse("MPF data too short".into()));
}
let big_endian = &mpf_data[0..2] == b"MM";
if !big_endian && &mpf_data[0..2] != b"II" {
return Err(Error::MpfParse("Invalid MPF endianness marker".into()));
}
let ifd_offset = read_u32(mpf_data, 4, big_endian) as usize;
if ifd_offset + 2 > mpf_data.len() {
return Err(Error::MpfParse("Invalid IFD offset".into()));
}
let num_entries = read_u16(mpf_data, ifd_offset, big_endian) as usize;
let mut mp_entry_offset = 0usize;
let mut mp_entry_count = 0u32;
let entry_start = ifd_offset + 2;
for i in 0..num_entries {
let offset = entry_start + i * 12;
if offset + 12 > mpf_data.len() {
break;
}
let tag = read_u16(mpf_data, offset, big_endian);
let value_offset = read_u32(mpf_data, offset + 8, big_endian);
match tag {
0xB001 => {
mp_entry_count = value_offset;
}
0xB002 => {
mp_entry_offset = value_offset as usize;
}
_ => {}
}
}
let max_possible = if mp_entry_offset > 0 && mp_entry_offset < mpf_data.len() {
(mpf_data.len() - mp_entry_offset) / 16
} else {
0
};
let mp_entry_count = (mp_entry_count as usize).min(max_possible).min(1000);
let mut entries = Vec::with_capacity(mp_entry_count);
if mp_entry_offset > 0 && mp_entry_count > 0 {
for i in 0..mp_entry_count {
let entry_pos = mp_entry_offset + i * 16;
if entry_pos + 16 > mpf_data.len() {
break;
}
let attr = read_u32(mpf_data, entry_pos, big_endian);
let size = read_u32(mpf_data, entry_pos + 4, big_endian);
let offset = read_u32(mpf_data, entry_pos + 8, big_endian);
entries.push(MpfEntry {
image_type: MpfImageType::from_attribute(attr),
size,
offset,
index: i as u32,
});
}
}
if entries.is_empty() {
return Err(Error::MpfParse("No images found in MPF".into()));
}
Ok(MpfDirectory {
entries,
mpf_marker_offset,
})
}
pub fn extract_secondary_images<'a>(data: &'a [u8], mpf: &MpfDirectory) -> Vec<&'a [u8]> {
let mut images = Vec::new();
for entry in &mpf.entries {
if entry.index == 0 {
continue;
}
let tiff_header_offset = mpf.mpf_marker_offset + 8;
let actual_offset = tiff_header_offset + entry.offset as usize;
let end = actual_offset + entry.size as usize;
if actual_offset < data.len() && end <= data.len() {
images.push(&data[actual_offset..end]);
}
}
images
}
pub fn assemble(primary: &[u8], secondaries: &[&[u8]], types: &[MpfImageType]) -> Result<Vec<u8>> {
if secondaries.len() != types.len() {
return Err(Error::MpfParse(
"Mismatched secondaries and types count".into(),
));
}
if secondaries.is_empty() {
return Ok(primary.to_vec());
}
let insert_pos = find_mpf_insert_position(primary)?;
let mpf_header = create_mpf_header_with_placeholder();
let primary_with_mpf_size = primary.len() + mpf_header.len();
let mut entries = Vec::with_capacity(1 + secondaries.len());
entries.push((MpfImageType::Primary, primary_with_mpf_size as u32, 0u32));
let tiff_header_pos = insert_pos + 8;
let mut offset = primary_with_mpf_size as u32;
for (i, secondary) in secondaries.iter().enumerate() {
let img_type = types.get(i).copied().unwrap_or(MpfImageType::GainMap);
let relative_offset = offset - tiff_header_pos as u32;
entries.push((img_type, secondary.len() as u32, relative_offset));
offset += secondary.len() as u32;
}
let mpf_header = create_mpf_header(&entries, insert_pos);
let total_size =
primary.len() + mpf_header.len() + secondaries.iter().map(|s| s.len()).sum::<usize>();
let mut output = Vec::with_capacity(total_size);
output.extend_from_slice(&primary[..insert_pos]);
output.extend_from_slice(&mpf_header);
output.extend_from_slice(&primary[insert_pos..]);
for secondary in secondaries {
output.extend_from_slice(secondary);
}
Ok(output)
}
pub fn generate_mpf(
primary_size: usize,
secondary_sizes: &[usize],
types: &[MpfImageType],
mpf_offset: usize,
) -> Vec<u8> {
let mut entries = Vec::with_capacity(1 + secondary_sizes.len());
entries.push((MpfImageType::Primary, primary_size as u32, 0u32));
let tiff_header_pos = mpf_offset + 8;
let mut offset = primary_size as u32;
for (i, &size) in secondary_sizes.iter().enumerate() {
let img_type = types.get(i).copied().unwrap_or(MpfImageType::GainMap);
let relative_offset = offset - tiff_header_pos as u32;
entries.push((img_type, size as u32, relative_offset));
offset += size as u32;
}
create_mpf_header(&entries, mpf_offset)
}
fn read_u16(data: &[u8], offset: usize, big_endian: bool) -> u16 {
if big_endian {
u16::from_be_bytes([data[offset], data[offset + 1]])
} else {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
}
fn read_u32(data: &[u8], offset: usize, big_endian: bool) -> u32 {
if big_endian {
u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
} else {
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
}
fn find_mpf_insert_position(data: &[u8]) -> Result<usize> {
if data.len() < 4 || data[0] != 0xFF || data[1] != 0xD8 {
return Err(Error::JpegDecode("Not a valid JPEG".into()));
}
let mut pos = 2;
while pos < data.len() - 3 {
if data[pos] != 0xFF {
break;
}
let marker = data[pos + 1];
if !(0xE0..=0xE1).contains(&marker) {
break;
}
let length = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
pos += 2 + length;
}
Ok(pos)
}
fn create_mpf_header_with_placeholder() -> Vec<u8> {
vec![0u8; 82] }
fn create_mpf_header(entries: &[(MpfImageType, u32, u32)], _mpf_offset: usize) -> Vec<u8> {
let mut mpf = Vec::with_capacity(128);
mpf.extend_from_slice(b"MM");
mpf.push(0x00);
mpf.push(0x2A);
mpf.extend_from_slice(&8u32.to_be_bytes());
mpf.extend_from_slice(&3u16.to_be_bytes());
mpf.extend_from_slice(&0xB000u16.to_be_bytes()); mpf.extend_from_slice(&7u16.to_be_bytes()); mpf.extend_from_slice(&4u32.to_be_bytes()); mpf.extend_from_slice(b"0100");
mpf.extend_from_slice(&0xB001u16.to_be_bytes()); mpf.extend_from_slice(&4u16.to_be_bytes()); mpf.extend_from_slice(&1u32.to_be_bytes()); mpf.extend_from_slice(&(entries.len() as u32).to_be_bytes());
let mp_entry_size = (entries.len() * 16) as u32;
let mp_entry_offset: u32 = 8 + 2 + 36 + 4; mpf.extend_from_slice(&0xB002u16.to_be_bytes()); mpf.extend_from_slice(&7u16.to_be_bytes()); mpf.extend_from_slice(&mp_entry_size.to_be_bytes()); mpf.extend_from_slice(&mp_entry_offset.to_be_bytes());
mpf.extend_from_slice(&0u32.to_be_bytes());
for (i, (img_type, size, offset)) in entries.iter().enumerate() {
let attr = if i == 0 {
MpfImageType::Primary.to_attribute()
} else {
img_type.to_attribute()
};
mpf.extend_from_slice(&attr.to_be_bytes());
mpf.extend_from_slice(&size.to_be_bytes());
mpf.extend_from_slice(&offset.to_be_bytes());
mpf.extend_from_slice(&0u32.to_be_bytes());
}
let mut marker = Vec::with_capacity(4 + 4 + mpf.len());
marker.push(0xFF);
marker.push(0xE2);
let length = 2 + 4 + mpf.len(); marker.push(((length >> 8) & 0xFF) as u8);
marker.push((length & 0xFF) as u8);
marker.extend_from_slice(b"MPF\0");
marker.extend_from_slice(&mpf);
marker
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_primary_bounds() {
let jpeg = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xD9, ];
let bounds = primary_bounds(&jpeg).unwrap();
assert_eq!(bounds.start, 0);
assert_eq!(bounds.end, jpeg.len());
}
#[test]
fn test_primary_bounds_multi_image() {
let jpeg1 = vec![
0xFF, 0xD8, 0xFF, 0xD9, ];
let jpeg2 = vec![
0xFF, 0xD8, 0xFF, 0xD9, ];
let mut data = jpeg1.clone();
data.extend_from_slice(&jpeg2);
let bounds = primary_bounds(&data).unwrap();
assert_eq!(bounds, 0..4); }
#[test]
fn test_scan_segments() {
let jpeg = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xE1, 0x00, 0x06, b'T', b'E', b'S', b'T', 0xFF, 0xDA, 0x00, 0x00, ];
let segments = scan_segments(&jpeg);
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].marker_num, 0); assert!(segments[0].is_jfif());
assert_eq!(segments[1].marker_num, 1); }
#[test]
fn test_mpf_image_type_roundtrip() {
let attr = MpfImageType::Primary.to_attribute();
assert_eq!(attr, 0x03_0000);
let back = MpfImageType::from_attribute(attr);
assert!(matches!(back, MpfImageType::Primary));
let attr = MpfImageType::GainMap.to_attribute();
assert_eq!(attr, 0x00_0000);
let back = MpfImageType::from_attribute(attr);
assert!(matches!(back, MpfImageType::GainMap));
}
#[test]
fn test_assemble_basic() {
let primary = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, ];
let secondary = vec![
0xFF, 0xD8, 0xFF, 0xD9, ];
let result = assemble(&primary, &[&secondary], &[MpfImageType::GainMap]).unwrap();
assert_eq!(result[0], 0xFF);
assert_eq!(result[1], 0xD8);
let has_mpf = result
.windows(6)
.any(|w| w[0] == 0xFF && w[1] == 0xE2 && &w[4..] == b"MP");
assert!(has_mpf);
assert_eq!(result[result.len() - 2], 0xFF);
assert_eq!(result[result.len() - 1], 0xD9);
}
#[test]
fn test_generate_mpf() {
let mpf_data = generate_mpf(50000, &[10000], &[MpfImageType::GainMap], 100);
assert_eq!(mpf_data[0], 0xFF);
assert_eq!(mpf_data[1], 0xE2);
assert!(mpf_data.windows(4).any(|w| w == b"MPF\0"));
}
#[test]
fn test_scan_segments_empty() {
let segments = scan_segments(&[]);
assert!(segments.is_empty());
}
#[test]
fn test_scan_segments_not_jpeg() {
let png_header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
let segments = scan_segments(&png_header);
assert!(segments.is_empty());
let random = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
let segments = scan_segments(&random);
assert!(segments.is_empty());
}
#[test]
fn test_scan_segments_only_soi() {
let jpeg = vec![
0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x05, 0x00, 0x01, 0x02, 0xFF, 0xD9, ];
let segments = scan_segments(&jpeg);
assert!(segments.is_empty());
}
#[test]
fn test_primary_bounds_not_jpeg() {
assert!(primary_bounds(&[]).is_none());
assert!(primary_bounds(&[0x00, 0x01, 0x02, 0x03]).is_none());
assert!(primary_bounds(&[0x89, 0x50, 0x4E, 0x47]).is_none()); }
#[test]
fn test_primary_bounds_no_eoi() {
let data = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, ];
assert!(primary_bounds(&data).is_none());
}
#[test]
fn test_app_segment_type_checks() {
let mpf = AppSegment {
marker_num: 2,
data: b"MPF\0some_data".to_vec(),
offset: 0,
};
assert!(mpf.is_mpf());
assert!(!mpf.is_xmp());
assert!(!mpf.is_exif());
assert!(!mpf.is_icc());
assert!(!mpf.is_jfif());
let xmp = AppSegment {
marker_num: 1,
data: b"http://ns.adobe.com/xap/1.0/\0<xmp>test</xmp>".to_vec(),
offset: 0,
};
assert!(!xmp.is_mpf());
assert!(xmp.is_xmp());
assert!(!xmp.is_exif());
assert!(!xmp.is_icc());
assert!(!xmp.is_jfif());
let exif = AppSegment {
marker_num: 1,
data: b"Exif\0\0some_exif".to_vec(),
offset: 0,
};
assert!(!exif.is_mpf());
assert!(!exif.is_xmp());
assert!(exif.is_exif());
assert!(!exif.is_icc());
assert!(!exif.is_jfif());
let icc = AppSegment {
marker_num: 2,
data: b"ICC_PROFILE\0chunk_data".to_vec(),
offset: 0,
};
assert!(!icc.is_mpf());
assert!(!icc.is_xmp());
assert!(!icc.is_exif());
assert!(icc.is_icc());
assert!(!icc.is_jfif());
let jfif = AppSegment {
marker_num: 0,
data: b"JFIF\0\x01\x01".to_vec(),
offset: 0,
};
assert!(!jfif.is_mpf());
assert!(!jfif.is_xmp());
assert!(!jfif.is_exif());
assert!(!jfif.is_icc());
assert!(jfif.is_jfif());
let wrong_marker = AppSegment {
marker_num: 1,
data: b"MPF\0data".to_vec(),
offset: 0,
};
assert!(!wrong_marker.is_mpf());
let app2_not_mpf = AppSegment {
marker_num: 2,
data: b"SOMETHING_ELSE".to_vec(),
offset: 0,
};
assert!(!app2_not_mpf.is_mpf());
assert!(!app2_not_mpf.is_icc());
}
#[test]
fn test_parse_mpf_segment_too_short() {
let short_data = b"MPF\0MM\x00";
let result = parse_mpf_segment(short_data, 0);
assert!(result.is_err());
let result = parse_mpf_segment(&[], 0);
assert!(result.is_err());
let result = parse_mpf_segment(b"MPF\0ABCD", 0);
assert!(result.is_err());
}
#[test]
fn test_parse_mpf_segment_invalid_endian() {
let mut data = b"MPF\0".to_vec();
data.extend_from_slice(b"XX"); data.extend_from_slice(&[0x00, 0x2A]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x08]); let result = parse_mpf_segment(&data, 0);
assert!(result.is_err());
}
#[test]
fn test_extract_secondary_images_out_of_bounds() {
let data = vec![0xFF, 0xD8, 0xFF, 0xD9];
let mpf = MpfDirectory {
entries: vec![
MpfEntry {
image_type: MpfImageType::Primary,
size: 4,
offset: 0,
index: 0,
},
MpfEntry {
image_type: MpfImageType::GainMap,
size: 1000, offset: 100, index: 1,
},
],
mpf_marker_offset: 0,
};
let secondaries = extract_secondary_images(&data, &mpf);
assert!(secondaries.is_empty());
}
#[test]
fn test_assemble_no_secondaries() {
let primary = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, ];
let result = assemble(&primary, &[], &[]).unwrap();
assert_eq!(result, primary);
}
#[test]
fn test_assemble_mismatched_counts() {
let primary = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, ];
let secondary = [0xFF, 0xD8, 0xFF, 0xD9];
let result = assemble(
&primary,
&[&secondary[..]],
&[MpfImageType::GainMap, MpfImageType::DepthMap],
);
assert!(result.is_err());
let result = assemble(
&primary,
&[&secondary[..], &secondary[..]],
&[MpfImageType::GainMap],
);
assert!(result.is_err());
}
#[test]
fn test_mpf_image_type_unknown() {
let attr = 0xDEAD_BEEFu32;
let img_type = MpfImageType::from_attribute(attr);
assert!(matches!(img_type, MpfImageType::Unknown(_)));
if let MpfImageType::Unknown(v) = img_type {
assert_eq!(v, 0xDEAD_BEEF);
}
assert_eq!(img_type.to_attribute(), 0xDEAD_BEEF);
}
#[test]
fn test_mpf_image_type_all_variants() {
assert_eq!(MpfImageType::LargeThumbnail.to_attribute(), 0x01_0001);
assert_eq!(MpfImageType::MultiFramePanorama.to_attribute(), 0x02_0002);
assert_eq!(MpfImageType::MultiFrameDisparity.to_attribute(), 0x03_0003);
assert_eq!(MpfImageType::MultiFrameMultiAngle.to_attribute(), 0x04_0004);
let lt = MpfImageType::from_attribute(0x0100_0000);
assert!(matches!(lt, MpfImageType::LargeThumbnail));
let mfp = MpfImageType::from_attribute(0x0200_0000);
assert!(matches!(mfp, MpfImageType::MultiFramePanorama));
let mfd = MpfImageType::from_attribute(0x0300_0000);
assert!(matches!(mfd, MpfImageType::MultiFrameDisparity));
let mfma = MpfImageType::from_attribute(0x0400_0000);
assert!(matches!(mfma, MpfImageType::MultiFrameMultiAngle));
let unknown = MpfImageType::from_attribute(0x0500_0000);
assert!(matches!(unknown, MpfImageType::Unknown(_)));
}
#[test]
fn test_generate_mpf_basic() {
let mpf_data = generate_mpf(
10000,
&[5000, 3000],
&[MpfImageType::GainMap, MpfImageType::DepthMap],
50,
);
assert_eq!(mpf_data[0], 0xFF);
assert_eq!(mpf_data[1], 0xE2);
let length = u16::from_be_bytes([mpf_data[2], mpf_data[3]]) as usize;
assert_eq!(length + 2, mpf_data.len());
assert_eq!(&mpf_data[4..8], b"MPF\0");
assert_eq!(&mpf_data[8..10], b"MM");
assert_eq!(mpf_data[10], 0x00);
assert_eq!(mpf_data[11], 0x2A);
let ifd_offset = 8 + 8; let num_ifd_entries = u16::from_be_bytes([mpf_data[ifd_offset], mpf_data[ifd_offset + 1]]);
assert_eq!(num_ifd_entries, 3); }
}