#![doc = include_str!("../docs/user/overview.md")]
#![doc = include_str!("../docs/user/scenarios.md")]
#![doc = include_str!("../docs/user/color.md")]
#![doc = include_str!("../docs/user/ultra-hdr.md")]
#![doc = include_str!("../docs/user/limitations.md")]
#![doc = include_str!("../docs/user/migration-0.5.md")]
#![deny(missing_docs)]
mod codec;
mod container;
mod error;
mod gainmap;
pub mod icc;
mod metadata;
mod reconstruct;
mod types;
pub use error::{Error, Result};
pub use types::ChromaSubsampling;
pub use types::Chromaticity;
pub use types::CodestreamLayout;
pub use types::ColorMetadata;
pub use types::CompressionEffort;
pub use types::ComputeGainMapOptions;
pub use types::ComputedGainMap;
pub use types::ContainerKind;
pub use types::ContainerLayout;
pub use types::DecodeOptions;
pub use types::DecodedGainMap;
pub use types::DecodedImage;
pub use types::EncodeOptions;
pub use types::Encoder;
pub use types::GainMapBundle;
pub use types::GainMapChannels;
pub use types::GainMapMetadataSource;
pub use types::GainMapScale;
pub use types::GamutInfo;
pub use types::Inspection;
pub use types::MetadataLocation;
pub use types::ParsedGainMapXmp;
pub use types::PreparePrimaryOptions;
pub use types::PreparedPrimary;
pub use types::PrimaryMetadata;
pub use types::UltraHdrEncodeOptions;
pub use types::UltraHdrMetadata;
pub use types::UltraHdrMetadataEmission;
pub use ultrahdr_core::ColorGamut;
pub use ultrahdr_core::ColorTransfer;
pub use ultrahdr_core::GainMap;
pub use ultrahdr_core::GainMapMetadata;
pub use ultrahdr_core::PixelFormat;
pub use ultrahdr_core::RawImage as Image;
pub use ultrahdr_core::gainmap::HdrOutputFormat;
use codec::{decode_gain_map, decode_primary_image, encode_image};
use container::{
assemble_container_owned, inspect_container, inspect_container_layout as inspect_layout_impl,
parse_container,
};
use gainmap::{compute_gain_map_impl, prepare_primary_impl, ultra_hdr_encode_options};
use metadata::{
build_ultra_hdr_metadata, parse_gain_map_xmp_raw, parse_iso_21496_1_raw,
parse_ultra_hdr_metadata,
};
use rayon::join;
use ultrahdr_core::{ColorGamut as CoreColorGamut, ColorTransfer as CoreColorTransfer};
const PARALLEL_DECODE_THRESHOLD_BYTES: usize = 256 * 1024;
pub fn decode(bytes: &[u8]) -> Result<DecodedImage> {
decode_internal(bytes, DecodeOptions::default())
}
pub fn decode_with_options(bytes: &[u8], options: DecodeOptions) -> Result<DecodedImage> {
decode_internal(bytes, options)
}
pub fn inspect(bytes: &[u8]) -> Result<Inspection> {
let parsed = inspect_container(bytes)?;
Ok(Inspection {
primary_jpeg_len: parsed.primary_jpeg_len,
gain_map_jpeg_len: parsed.gain_map_jpeg_len,
primary_metadata: parsed.primary_metadata,
ultra_hdr: parse_ultra_hdr_metadata(
parsed.xmp.as_deref(),
parsed.xmp_location,
parsed.iso.as_deref(),
parsed.iso_location,
)?,
})
}
pub fn inspect_container_layout(bytes: &[u8]) -> Result<ContainerLayout> {
inspect_layout_impl(bytes)
}
pub fn encode(image: &Image, options: &EncodeOptions) -> Result<Vec<u8>> {
Encoder::new(options.clone()).encode(image)
}
pub fn compute_gain_map(
hdr_image: &Image,
primary_image: &Image,
options: &ComputeGainMapOptions,
) -> Result<ComputedGainMap> {
compute_gain_map_impl(hdr_image, primary_image, options)
}
pub fn parse_gain_map_xmp(xmp: &str) -> Result<ParsedGainMapXmp> {
parse_gain_map_xmp_raw(xmp)
}
pub fn parse_iso_21496_1(iso_21496_1: &[u8]) -> Result<GainMapMetadata> {
parse_iso_21496_1_raw(iso_21496_1)
}
pub fn prepare_sdr_primary(
image: &Image,
options: &PreparePrimaryOptions,
) -> Result<PreparedPrimary> {
prepare_primary_impl(image, options)
}
pub fn encode_ultra_hdr(
hdr_image: &Image,
primary_image: &Image,
options: &UltraHdrEncodeOptions,
) -> Result<Vec<u8>> {
if options.primary.gain_map.is_some() {
return Err(Error::InvalidInput(
"UltraHdrEncodeOptions::primary.gain_map must be None".into(),
));
}
let computed = compute_gain_map(hdr_image, primary_image, &options.gain_map)?;
let encode_options = ultra_hdr_encode_options(&options.primary, computed, options);
encode(primary_image, &encode_options)
}
impl Encoder {
#[must_use]
pub fn new(options: EncodeOptions) -> Self {
Self { options }
}
pub fn encode(&self, image: &Image) -> Result<Vec<u8>> {
let primary_metadata = resolved_primary_metadata(
image,
&self.options.primary_metadata,
self.options.gain_map.is_some(),
)?;
let primary_jpeg = encode_image(
image,
self.options.quality,
self.options.progressive,
self.options.compression,
self.options.chroma_subsampling,
&primary_metadata,
)?;
let (gain_map_jpeg, ultra_hdr_metadata) = match self.options.gain_map.as_ref() {
Some(gain_map) => {
gain_map.metadata.validate()?;
let jpeg = encode_image(
&gain_map.image,
gain_map.quality,
gain_map.progressive,
gain_map.compression,
ChromaSubsampling::Yuv444,
&PrimaryMetadata::default(),
)?;
let metadata = build_ultra_hdr_metadata(
&gain_map.metadata,
self.options.ultra_hdr_metadata_emission,
)?;
(Some(jpeg), Some(metadata))
}
None => (None, None),
};
assemble_container_owned(
primary_jpeg,
gain_map_jpeg.as_deref(),
&primary_metadata,
ultra_hdr_metadata.as_ref(),
)
}
}
impl DecodedImage {
pub fn reconstruct_hdr(
&self,
display_boost: f32,
output_format: HdrOutputFormat,
) -> Result<Image> {
self.reconstruct_hdr_with(display_boost, output_format)
}
}
fn decode_internal(bytes: &[u8], options: DecodeOptions) -> Result<DecodedImage> {
let parsed = parse_container(bytes, &options)?;
let ultra_hdr = parse_ultra_hdr_metadata(
parsed.xmp.as_deref(),
parsed.xmp_location,
parsed.iso.as_deref(),
parsed.iso_location,
)?;
let gain_map_metadata = ultra_hdr
.as_ref()
.and_then(|metadata| metadata.gain_map_metadata.clone());
let (mut image, gain_map) = match parsed.gain_map_jpeg {
Some(gain_map_jpeg) if options.decode_gain_map => {
let decode_gain_map_fn = || decode_gain_map(gain_map_jpeg, gain_map_metadata.as_ref());
let decode_primary_fn = || decode_primary_image(parsed.primary_jpeg);
let (primary_result, gain_map_result) =
if should_parallel_decode(parsed.primary_jpeg, gain_map_jpeg) {
join(decode_primary_fn, decode_gain_map_fn)
} else {
(decode_primary_fn(), decode_gain_map_fn())
};
let mut decoded_gain_map = gain_map_result?;
decoded_gain_map.metadata = gain_map_metadata;
if options.retain_gain_map_jpeg {
decoded_gain_map.jpeg_bytes = Some(gain_map_jpeg.to_vec());
}
(primary_result?, Some(decoded_gain_map))
}
_ => (decode_primary_image(parsed.primary_jpeg)?, None),
};
if let Some(gamut) = named_gamut(&parsed.primary_metadata.color) {
image.gamut = gamut;
}
if let Some(transfer) = parsed.primary_metadata.color.transfer {
image.transfer = transfer;
}
Ok(DecodedImage {
image,
primary_jpeg: options
.retain_primary_jpeg
.then(|| parsed.primary_jpeg.to_vec()),
primary_metadata: parsed.primary_metadata,
ultra_hdr,
gain_map,
})
}
fn should_parallel_decode(primary_jpeg: &[u8], gain_map_jpeg: &[u8]) -> bool {
primary_jpeg.len() + gain_map_jpeg.len() >= PARALLEL_DECODE_THRESHOLD_BYTES
}
fn resolved_primary_metadata(
image: &Image,
primary_metadata: &PrimaryMetadata,
has_gain_map: bool,
) -> Result<PrimaryMetadata> {
if !has_gain_map || primary_metadata.color.icc_profile.is_some() {
return Ok(primary_metadata.clone());
}
let gamut = named_gamut(&primary_metadata.color).unwrap_or(image.gamut);
let transfer = primary_metadata.color.transfer.unwrap_or(image.transfer);
if gamut == CoreColorGamut::DisplayP3 && transfer == CoreColorTransfer::Srgb {
let mut resolved = primary_metadata.clone();
resolved.color.icc_profile = Some(icc::display_p3().to_vec());
resolved.color.gamut = Some(CoreColorGamut::DisplayP3);
resolved.color.gamut_info = Some(GamutInfo::from_standard(CoreColorGamut::DisplayP3));
resolved.color.transfer = Some(CoreColorTransfer::Srgb);
return Ok(resolved);
}
Ok(primary_metadata.clone())
}
fn named_gamut(color_metadata: &ColorMetadata) -> Option<ColorGamut> {
color_metadata.gamut.or_else(|| {
color_metadata
.gamut_info
.as_ref()
.and_then(|info| info.standard)
})
}