use crate::decode::DecodedExtras;
use crate::decoder::Decoder;
use crate::error::{Error, Result};
use ultrahdr_core::{
ColorGamut, GainMap, GainMapMetadata, color::tonemap::AdaptiveTonemapper, gainmap::RowDecoder,
metadata::xmp::parse_xmp,
};
pub trait UltraHdrExtras {
fn is_ultrahdr(&self) -> bool;
fn ultrahdr_metadata(&self) -> Option<Result<(GainMapMetadata, Option<usize>)>>;
fn decode_gainmap(&self) -> Option<Result<GainMap>>;
}
impl UltraHdrExtras for DecodedExtras {
fn is_ultrahdr(&self) -> bool {
self.xmp()
.map(|xmp: &str| xmp.contains("hdrgm:Version") || xmp.contains("hdrgm:GainMapMax"))
.unwrap_or(false)
}
fn ultrahdr_metadata(&self) -> Option<Result<(GainMapMetadata, Option<usize>)>> {
if let Some(xmp) = self.xmp()
&& let Ok((metadata, len)) = parse_xmp(xmp)
&& (metadata.gain_map_max != [0.0; 3] || metadata.alternate_hdr_headroom != 0.0)
{
return Some(Ok((metadata, len)));
}
if let Some(gainmap_jpeg) = self.gainmap()
&& let Some(gm_xmp) = extract_xmp_from_jpeg(gainmap_jpeg)
{
return Some(parse_xmp(&gm_xmp).map_err(ultrahdr_to_jpegli_error));
}
let xmp = self.xmp()?;
Some(parse_xmp(xmp).map_err(ultrahdr_to_jpegli_error))
}
fn decode_gainmap(&self) -> Option<Result<GainMap>> {
let gainmap_jpeg = self.gainmap()?;
Some(decode_gainmap_jpeg(gainmap_jpeg))
}
}
pub fn create_hdr_reconstructor(
width: u32,
height: u32,
extras: &DecodedExtras,
display_boost: f32,
) -> Result<RowDecoder> {
let (metadata, _) = extras
.ultrahdr_metadata()
.ok_or_else(|| Error::decode_error("Not an UltraHDR image".to_string()))??;
let gainmap = extras
.decode_gainmap()
.ok_or_else(|| Error::decode_error("No gain map found".to_string()))??;
RowDecoder::new(
gainmap,
metadata,
width,
height,
display_boost,
ColorGamut::Bt709,
)
.map_err(ultrahdr_to_jpegli_error)
}
pub fn tonemapper_from_ultrahdr(extras: &DecodedExtras) -> Result<AdaptiveTonemapper> {
let (metadata, _) = extras
.ultrahdr_metadata()
.ok_or_else(|| Error::decode_error("Not an UltraHDR image".to_string()))??;
Ok(AdaptiveTonemapper::from_gainmap(&metadata))
}
fn decode_gainmap_jpeg(jpeg_data: &[u8]) -> Result<GainMap> {
let decoded = Decoder::new().decode(jpeg_data, enough::Unstoppable)?;
let width = decoded.width();
let height = decoded.height();
let pixels = decoded.pixels_u8().unwrap().to_vec();
let channels = if is_grayscale_content(&pixels) { 1 } else { 3 };
let data = if channels == 1 {
pixels.chunks_exact(3).map(|p| p[0]).collect()
} else {
pixels
};
Ok(GainMap {
width,
height,
channels,
data,
})
}
fn is_grayscale_content(pixels: &[u8]) -> bool {
pixels
.chunks_exact(3)
.take(100) .all(|p| p[0] == p[1] && p[1] == p[2])
}
fn extract_xmp_from_jpeg(jpeg: &[u8]) -> Option<String> {
let xmp_ns = b"http://ns.adobe.com/xap/1.0/\0";
let idx = jpeg.windows(xmp_ns.len()).position(|w| w == xmp_ns)?;
let xmp_start = idx + xmp_ns.len();
let marker_pos = idx.checked_sub(4)?;
if jpeg.get(marker_pos)? != &0xFF || jpeg.get(marker_pos + 1)? != &0xE1 {
return None;
}
let length = u16::from_be_bytes([jpeg[marker_pos + 2], jpeg[marker_pos + 3]]) as usize;
let xmp_end = marker_pos + 2 + length;
let xmp_bytes = jpeg.get(xmp_start..xmp_end)?;
String::from_utf8(xmp_bytes.to_vec()).ok()
}
fn ultrahdr_to_jpegli_error(e: ultrahdr_core::Error) -> Error {
Error::decode_error(e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode::SegmentType;
fn make_test_extras_with_xmp(xmp: &str) -> DecodedExtras {
let mut extras = DecodedExtras::new();
let xmp_data = format!("http://ns.adobe.com/xap/1.0/\0{}", xmp);
extras.add_segment(0xE1, xmp_data.into_bytes(), SegmentType::Xmp);
extras
}
#[test]
fn test_is_ultrahdr_positive() {
let extras = make_test_extras_with_xmp(
r#"<x:xmpmeta><rdf:RDF><rdf:Description hdrgm:Version="1.0"/></rdf:RDF></x:xmpmeta>"#,
);
assert!(extras.is_ultrahdr());
}
#[test]
fn test_is_ultrahdr_negative() {
let extras = make_test_extras_with_xmp(
r#"<x:xmpmeta><rdf:RDF><rdf:Description dc:creator="Test"/></rdf:RDF></x:xmpmeta>"#,
);
assert!(!extras.is_ultrahdr());
}
#[test]
fn test_is_ultrahdr_no_xmp() {
let extras = DecodedExtras::new();
assert!(!extras.is_ultrahdr());
}
#[test]
fn test_is_grayscale_content() {
let gray = vec![128, 128, 128, 64, 64, 64, 200, 200, 200];
assert!(is_grayscale_content(&gray));
let color = vec![255, 0, 0, 0, 255, 0, 0, 0, 255];
assert!(!is_grayscale_content(&color));
}
}