use ultrahdr_rs::Decoder;
fn build_jpeg_with_segments(segments: &[Vec<u8>]) -> Vec<u8> {
let mut data = vec![0xFF, 0xD8]; for seg in segments {
data.extend_from_slice(seg);
}
data.extend_from_slice(&[0xFF, 0xD9]); data
}
fn xmp_segment(xml: &str) -> Vec<u8> {
let namespace = b"http://ns.adobe.com/xap/1.0/\0";
let payload_len = namespace.len() + xml.len();
let total_len = 2 + payload_len; let mut seg = Vec::with_capacity(4 + payload_len);
seg.push(0xFF);
seg.push(0xE1); seg.push(((total_len >> 8) & 0xFF) as u8);
seg.push((total_len & 0xFF) as u8);
seg.extend_from_slice(namespace);
seg.extend_from_slice(xml.as_bytes());
seg
}
fn jfif_segment() -> Vec<u8> {
vec![
0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F', 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, ]
}
#[test]
fn test_jpeg_xmp_without_hdrgm() {
let xml = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmp:CreatorTool="TestCamera"
xmp:CreateDate="2024-01-01T00:00:00"/>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>"#;
let jpeg = build_jpeg_with_segments(&[jfif_segment(), xmp_segment(xml)]);
let decoder = Decoder::new(&jpeg).unwrap();
assert!(!decoder.is_ultrahdr(), "Camera XMP should not be UltraHDR");
assert!(decoder.metadata().is_none(), "Should have no metadata");
assert!(
decoder.gainmap_jpeg().is_none(),
"Should have no gain map JPEG"
);
}
#[test]
fn test_plain_jpeg_with_jfif() {
let jpeg = build_jpeg_with_segments(&[jfif_segment()]);
let decoder = Decoder::new(&jpeg).unwrap();
assert!(!decoder.is_ultrahdr());
assert!(decoder.metadata().is_none());
assert!(decoder.gainmap_jpeg().is_none());
assert!(
decoder.primary_jpeg().is_some(),
"Primary should be whole file"
);
}
#[test]
fn test_plain_jpeg_no_gainmap() {
let jpeg = vec![0xFF, 0xD8, 0xFF, 0xD9];
let decoder = Decoder::new(&jpeg).unwrap();
assert!(!decoder.is_ultrahdr());
assert!(decoder.gainmap_jpeg().is_none());
assert!(decoder.metadata().is_none());
}
#[test]
fn test_xmp_malformed_xml() {
let xml = r#"<rdf:Description hdrgm:Version="1.0" hdrgm:GainMapMax="2.0">"#;
let jpeg = build_jpeg_with_segments(&[xmp_segment(xml)]);
let decoder = Decoder::new(&jpeg).unwrap();
let _ = decoder.is_ultrahdr();
let _ = decoder.metadata();
}
#[test]
fn test_xmp_hdrgm_but_no_gainmap() {
let xml = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
hdrgm:Version="1.0"
hdrgm:GainMapMax="2.0"
hdrgm:GainMapMin="0.0"
hdrgm:Gamma="1.0"
hdrgm:OffsetSDR="0.015625"
hdrgm:OffsetHDR="0.015625"
hdrgm:HDRCapacityMax="2.0"
hdrgm:HDRCapacityMin="0.0"/>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>"#;
let jpeg = build_jpeg_with_segments(&[xmp_segment(xml)]);
let decoder = Decoder::new(&jpeg).unwrap();
assert!(
decoder.metadata().is_some(),
"XMP metadata should parse successfully"
);
}
#[test]
fn test_empty_input() {
assert!(Decoder::new(&[]).is_err());
}
#[test]
fn test_just_soi() {
assert!(Decoder::new(&[0xFF, 0xD8]).is_err());
}
#[test]
fn test_truncated_mid_segment() {
let data = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, ];
let _ = Decoder::new(&data);
}
#[test]
fn test_no_eoi() {
let data = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, ];
let result = Decoder::new(&data);
if let Ok(decoder) = result {
assert!(!decoder.is_ultrahdr());
}
}
#[test]
fn test_garbage_bytes() {
let data = vec![0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49];
assert!(Decoder::new(&data).is_err());
}
#[test]
fn test_truncated_mpf_segment() {
let data = vec![
0xFF, 0xD8, 0xFF, 0xE2, 0x00, 0x20, b'M', b'P', b'F', 0x00, b'M', b'M', 0x00, 0x2A, ];
let result = Decoder::new(&data);
if let Ok(decoder) = result {
assert!(!decoder.is_ultrahdr());
}
}
#[test]
fn test_mpf_single_image() {
let mut mpf_data = Vec::new();
mpf_data.extend_from_slice(b"MPF\0"); mpf_data.extend_from_slice(b"MM"); mpf_data.extend_from_slice(&0x002Au16.to_be_bytes()); mpf_data.extend_from_slice(&8u32.to_be_bytes()); mpf_data.extend_from_slice(&2u16.to_be_bytes());
mpf_data.extend_from_slice(&0xB001u16.to_be_bytes()); mpf_data.extend_from_slice(&4u16.to_be_bytes()); mpf_data.extend_from_slice(&1u32.to_be_bytes()); mpf_data.extend_from_slice(&1u32.to_be_bytes()); let mp_entry_offset = (8 + 2 + 24 + 4) as u32; mpf_data.extend_from_slice(&0xB002u16.to_be_bytes()); mpf_data.extend_from_slice(&7u16.to_be_bytes()); mpf_data.extend_from_slice(&16u32.to_be_bytes()); mpf_data.extend_from_slice(&mp_entry_offset.to_be_bytes());
mpf_data.extend_from_slice(&0u32.to_be_bytes());
mpf_data.extend_from_slice(&0x03_0000u32.to_be_bytes()); mpf_data.extend_from_slice(&1000u32.to_be_bytes()); mpf_data.extend_from_slice(&0u32.to_be_bytes()); mpf_data.extend_from_slice(&0u32.to_be_bytes());
let total_len = 2 + mpf_data.len();
let mut app2 = vec![
0xFF,
0xE2,
((total_len >> 8) & 0xFF) as u8,
(total_len & 0xFF) as u8,
];
app2.extend_from_slice(&mpf_data);
let jpeg = build_jpeg_with_segments(&[app2]);
let decoder = Decoder::new(&jpeg).unwrap();
assert!(
!decoder.is_ultrahdr(),
"Single-image MPF should not be UltraHDR"
);
assert!(decoder.gainmap_jpeg().is_none());
}
#[test]
fn test_two_jpegs_no_mpf() {
let data = vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, 0xFF, 0xD8, 0xFF, 0xD9, ];
let decoder = Decoder::new(&data).unwrap();
assert!(decoder.primary_jpeg().is_some());
assert!(decoder.gainmap_jpeg().is_some());
}
#[test]
fn test_icc_profile_none_on_plain_jpeg() {
let jpeg = build_jpeg_with_segments(&[jfif_segment()]);
let decoder = Decoder::new(&jpeg).unwrap();
assert!(decoder.icc_profile().is_none());
}
#[test]
fn test_icc_profile_detected() {
let mut icc_seg = Vec::new();
let icc_data = vec![0u8; 100]; let payload = {
let mut p = Vec::new();
p.extend_from_slice(b"ICC_PROFILE\0");
p.push(1); p.push(1); p.extend_from_slice(&icc_data);
p
};
let total_len = 2 + payload.len();
icc_seg.push(0xFF);
icc_seg.push(0xE2);
icc_seg.push(((total_len >> 8) & 0xFF) as u8);
icc_seg.push((total_len & 0xFF) as u8);
icc_seg.extend_from_slice(&payload);
let jpeg = build_jpeg_with_segments(&[icc_seg]);
let decoder = Decoder::new(&jpeg).unwrap();
let icc = decoder.icc_profile();
assert!(icc.is_some(), "Should find ICC profile");
assert_eq!(icc.unwrap(), icc_data, "ICC data should match");
}