use crate::decode::DecodedExtras;
use crate::decoder::Decoder;
use crate::error::{Error, Result};
use enough::Stop;
use ultrahdr_core::{
color::tonemap::AdaptiveTonemapper,
gainmap::{apply_gainmap, DecodeInput, HdrOutputFormat, RowDecoder},
metadata::xmp::parse_xmp,
ColorGamut, GainMap, GainMapMetadata, RawImage,
};
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>)>> {
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 reconstruct_hdr(
sdr_pixels: &[u8],
width: u32,
height: u32,
extras: &DecodedExtras,
display_boost: f32,
output_format: HdrOutputFormat,
stop: impl Stop,
) -> Result<RawImage> {
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()))??;
let sdr = create_sdr_rawimage(sdr_pixels, width, height)?;
apply_gainmap(
&sdr,
&gainmap,
&metadata,
display_boost,
output_format,
stop,
)
.map_err(ultrahdr_to_jpegli_error)
}
pub fn create_hdr_reconstructor(
width: u32,
height: u32,
extras: &DecodedExtras,
display_boost: f32,
output_format: HdrOutputFormat,
) -> 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::with_input_config(
gainmap,
metadata,
width,
height,
display_boost,
output_format,
ColorGamut::Bt709,
DecodeInput::rgb8(width),
)
.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))
}
pub fn reencode_ultrahdr<F>(
jpeg_data: &[u8],
display_boost: f32,
transform: Option<F>,
gainmap_config: &ultrahdr_core::gainmap::GainMapConfig,
encoder_config: &crate::encoder::EncoderConfig,
gainmap_quality: f32,
stop: impl Stop,
) -> Result<Vec<u8>>
where
F: FnOnce(&mut RawImage),
{
use crate::ultrahdr::encode::encode_ultrahdr_with_tonemapper;
let decoded = Decoder::new().decode(jpeg_data)?;
let extras = decoded
.extras()
.ok_or_else(|| Error::decode_error("No extras preserved during decode".to_string()))?;
let tonemapper = tonemapper_from_ultrahdr(extras)?;
let mut hdr = reconstruct_hdr(
decoded.pixels(),
decoded.width(),
decoded.height(),
extras,
display_boost,
HdrOutputFormat::LinearFloat,
&stop,
)?;
stop.check()?;
if let Some(f) = transform {
f(&mut hdr);
}
encode_ultrahdr_with_tonemapper(
&hdr,
&tonemapper,
gainmap_config,
encoder_config,
gainmap_quality,
stop,
)
}
fn decode_gainmap_jpeg(jpeg_data: &[u8]) -> Result<GainMap> {
let decoded = Decoder::new().decode(jpeg_data)?;
let width = decoded.width();
let height = decoded.height();
let pixels = decoded.pixels().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 create_sdr_rawimage(pixels: &[u8], width: u32, height: u32) -> Result<RawImage> {
let expected_rgb = (width * height * 3) as usize;
let expected_rgba = (width * height * 4) as usize;
let (format, data) = if pixels.len() == expected_rgba {
(ultrahdr_core::PixelFormat::Rgba8, pixels.to_vec())
} else if pixels.len() == expected_rgb {
let mut rgba = Vec::with_capacity(expected_rgba);
for chunk in pixels.chunks_exact(3) {
rgba.push(chunk[0]);
rgba.push(chunk[1]);
rgba.push(chunk[2]);
rgba.push(255);
}
(ultrahdr_core::PixelFormat::Rgba8, rgba)
} else {
return Err(Error::invalid_buffer_size(expected_rgb, pixels.len()));
};
RawImage::from_data(
width,
height,
format,
ultrahdr_core::ColorGamut::Bt709,
ultrahdr_core::ColorTransfer::Srgb,
data,
)
.map_err(ultrahdr_to_jpegli_error)
}
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));
}
}