Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ brotli = "8.0"
flate2 = "1.1"
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
libc = "0.2"
moxcms = "0.8.1"
png = "0.18"
rav1d = { git = "https://github.com/ente-io/rav1d.git", branch = "arm32_stable", default-features = false, features = [
"bitdepth_8",
Expand Down
147 changes: 131 additions & 16 deletions src/isobmff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4587,31 +4587,40 @@ fn resolve_grid_tile_hvcc_colr_and_transforms(
Ok((hvcc, colr, transforms))
}

/// ICC colour profile for the primary HEIC item, mirroring libheif: the
/// primary item's own `colr` ICC wins; a `grid` primary without one inherits
/// the first tile's ICC (libheif/libheif/context.cc).
pub fn primary_heic_icc_profile(input: &[u8]) -> Option<Vec<u8>> {
/// Colour properties for the primary HEIC item, mirroring libheif: the primary
/// item's own `colr` properties win; a `grid` primary without one inherits the
/// first tile's colour properties (libheif/libheif/context.cc).
pub fn primary_heic_color_properties(input: &[u8]) -> Option<PrimaryItemColorProperties> {
let (_meta, resolved) = resolve_primary_heic_item_graph(input).ok()?;

let mut icc = None;
let mut primary_colr = PrimaryItemColorProperties::default();
for property in &resolved.primary_item.properties {
if property.property.header.box_type.as_bytes() != COLR_BOX_TYPE {
continue;
}
if let Ok(parsed_colr) = property.property.parse_colr()
&& let ColorInformation::Icc(profile) = parsed_colr.information
{
icc = Some(profile.profile);
if let Ok(parsed_colr) = property.property.parse_colr() {
match parsed_colr.information {
ColorInformation::Nclx(profile) => {
primary_colr.nclx = Some(profile);
}
ColorInformation::Icc(profile) => {
primary_colr.icc = Some(profile);
}
}
}
}
if icc.is_some() {
return icc;
if primary_colr.icc.is_some() {
return Some(primary_colr);
}

// Grid primary without its own ICC: inherit the first tile's.
let item_type = resolved.primary_item.item_info.item_type?;
if item_type.as_bytes() != GRID_ITEM_TYPE {
return None;
let is_grid_primary = resolved
.primary_item
.item_info
.item_type
.map(|item_type| item_type.as_bytes() == GRID_ITEM_TYPE)
.unwrap_or(false);
if !is_grid_primary {
return primary_colr.nclx.is_some().then_some(primary_colr);
}
let tile_item_id = resolved
.primary_item
Expand All @@ -4632,7 +4641,40 @@ pub fn primary_heic_icc_profile(input: &[u8]) -> Option<Vec<u8>> {
&flattened_properties,
)
.ok()?;
tile_colr.icc.map(|profile| profile.profile)
merge_primary_and_grid_tile_color_properties(primary_colr, Some(tile_colr))
}

fn merge_primary_and_grid_tile_color_properties(
primary_colr: PrimaryItemColorProperties,
tile_colr: Option<PrimaryItemColorProperties>,
) -> Option<PrimaryItemColorProperties> {
if primary_colr.icc.is_some() {
return Some(primary_colr);
}

let Some(tile_colr) = tile_colr else {
return primary_colr.nclx.is_some().then_some(primary_colr);
};

if tile_colr.icc.is_some() {
return Some(PrimaryItemColorProperties {
nclx: primary_colr.nclx.or(tile_colr.nclx),
icc: tile_colr.icc,
});
}

if primary_colr.nclx.is_some() {
return Some(primary_colr);
}

tile_colr.nclx.is_some().then_some(tile_colr)
}

/// ICC colour profile for the primary HEIC item, mirroring libheif: the
/// primary item's own `colr` ICC wins; a `grid` primary without one inherits
/// the first tile's ICC (libheif/libheif/context.cc).
pub fn primary_heic_icc_profile(input: &[u8]) -> Option<Vec<u8>> {
primary_heic_color_properties(input).and_then(|colr| colr.icc.map(|profile| profile.profile))
}

fn resolve_primary_heic_item_graph<'a>(
Expand Down Expand Up @@ -7021,3 +7063,76 @@ fn read_u64_be(input: &[u8]) -> u64 {
input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7],
])
}

#[cfg(test)]
mod tests {
use super::{
FourCc, IccColorProfile, NclxColorProfile, PrimaryItemColorProperties,
merge_primary_and_grid_tile_color_properties,
};

#[test]
fn grid_primary_nclx_preserves_first_tile_icc_fallback() {
let primary = PrimaryItemColorProperties {
nclx: Some(nclx_profile(12)),
icc: None,
};
let tile = PrimaryItemColorProperties {
nclx: None,
icc: Some(icc_profile(vec![1, 2, 3])),
};

let merged = merge_primary_and_grid_tile_color_properties(primary, Some(tile))
.expect("expected merged color properties");

assert_eq!(merged.icc.unwrap().profile, vec![1, 2, 3]);
assert_eq!(merged.nclx.unwrap().colour_primaries, 12);
}

#[test]
fn grid_primary_icc_wins_over_first_tile_icc() {
let primary = PrimaryItemColorProperties {
nclx: None,
icc: Some(icc_profile(vec![4, 5, 6])),
};
let tile = PrimaryItemColorProperties {
nclx: None,
icc: Some(icc_profile(vec![1, 2, 3])),
};

let merged = merge_primary_and_grid_tile_color_properties(primary, Some(tile))
.expect("expected merged color properties");

assert_eq!(merged.icc.unwrap().profile, vec![4, 5, 6]);
}

#[test]
fn grid_primary_nclx_survives_without_first_tile_icc() {
let primary = PrimaryItemColorProperties {
nclx: Some(nclx_profile(12)),
icc: None,
};

let merged = merge_primary_and_grid_tile_color_properties(primary, None)
.expect("expected primary nclx color properties");

assert_eq!(merged.nclx.unwrap().colour_primaries, 12);
assert!(merged.icc.is_none());
}

fn nclx_profile(colour_primaries: u16) -> NclxColorProfile {
NclxColorProfile {
colour_primaries,
transfer_characteristics: 13,
matrix_coefficients: 1,
full_range_flag: true,
}
}

fn icc_profile(profile: Vec<u8>) -> IccColorProfile {
IccColorProfile {
profile_type: FourCc::new(*b"prof"),
profile,
}
}
}
132 changes: 110 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ extern crate alloc;
use brotli::Decompressor as BrotliDecompressor;
use flate2::read::{DeflateDecoder, ZlibDecoder};
use heic_decoder::DecodedFrame as HeicFrame;
use moxcms::{
CicpColorPrimaries, CicpProfile, ColorProfile, MatrixCoefficients, TransferCharacteristics,
};
use rav1d::include::dav1d::data::Dav1dData;
use rav1d::include::dav1d::dav1d::{Dav1dContext, Dav1dSettings};
use rav1d::include::dav1d::headers::{
Expand Down Expand Up @@ -2406,12 +2409,14 @@ fn decode_primary_uncompressed_to_image_internal(
});
}

let icc_profile = icc_profile_from_color_properties(&properties.colr);

Ok(DecodedUncompressedImage {
width,
height,
bit_depth: output_bit_depth,
rgba,
icc_profile: properties.colr.icc.map(|profile| profile.profile),
icc_profile,
})
}

Expand Down Expand Up @@ -6426,28 +6431,72 @@ fn primary_icc_profile_from_resolved_avif_graph(
// Provenance: primary-item colr extraction follows libheif item-property
// traversal in libheif/libheif/context.cc, with colr payload parsing from
// libheif/libheif/nclx.cc:Box_colr::parse.
let mut icc_profile = None;
let mut colr = isobmff::PrimaryItemColorProperties::default();
for property in &resolved.primary_item.properties {
if property.property.header.box_type.as_bytes() != *b"colr" {
continue;
}
// Tolerate individually malformed colr boxes (libheif parses each
// independently) instead of discarding an already-found profile.
if let Ok(parsed_colr) = property.property.parse_colr()
&& let isobmff::ColorInformation::Icc(profile) = parsed_colr.information
{
icc_profile = Some(profile.profile);
if let Ok(parsed_colr) = property.property.parse_colr() {
match parsed_colr.information {
isobmff::ColorInformation::Nclx(profile) => {
colr.nclx = Some(profile);
}
isobmff::ColorInformation::Icc(profile) => {
colr.icc = Some(profile);
}
}
}
}
icc_profile

icc_profile_from_color_properties(&colr)
}

fn primary_icc_profile_from_heic(input: &[u8]) -> Option<Vec<u8>> {
// Provenance: primary-item colr extraction follows libheif item-property
// traversal in libheif/libheif/context.cc (including the grid item's
// first-tile ICC inheritance), with colr payload parsing from
// libheif/libheif/nclx.cc:Box_colr::parse.
isobmff::primary_heic_icc_profile(input)
isobmff::primary_heic_color_properties(input)
.and_then(|colr| icc_profile_from_color_properties(&colr))
}

fn icc_profile_from_color_properties(
colr: &isobmff::PrimaryItemColorProperties,
) -> Option<Vec<u8>> {
if let Some(profile) = &colr.icc {
return Some(profile.profile.clone());
}

colr.nclx.as_ref().and_then(nclx_to_icc_profile)
}

fn nclx_to_icc_profile(nclx: &isobmff::NclxColorProfile) -> Option<Vec<u8>> {
if nclx_is_undefined(nclx) {
return None;
}

let cicp = CicpProfile {
color_primaries: CicpColorPrimaries::try_from(u8::try_from(nclx.colour_primaries).ok()?)
.ok()?,
transfer_characteristics: TransferCharacteristics::try_from(
u8::try_from(nclx.transfer_characteristics).ok()?,
)
.ok()?,
matrix_coefficients: MatrixCoefficients::try_from(
u8::try_from(nclx.matrix_coefficients).ok()?,
)
.ok()?,
full_range: nclx.full_range_flag,
};

let profile = ColorProfile::new_from_cicp(cicp);
if !profile.is_matrix_shaper() {
return None;
}

profile.encode().ok()
}

fn ycbcr_range_from_primary_colr(colr: &isobmff::PrimaryItemColorProperties) -> YCbCrRange {
Expand All @@ -6472,13 +6521,16 @@ fn ycbcr_range_override_from_primary_colr(
// stream metadata remains in effect (libheif/libheif/color-conversion/
// yuv2rgb.cc:Op_YCbCr_to_RGB::convert_colorspace and
// libheif/libheif/plugins/decoder_libde265.cc color-profile population).
colr.nclx.as_ref().filter(|nclx| !nclx_is_undefined(nclx)).map(|nclx| {
if nclx.full_range_flag {
YCbCrRange::Full
} else {
YCbCrRange::Limited
}
})
colr.nclx
.as_ref()
.filter(|nclx| !nclx_is_undefined(nclx))
.map(|nclx| {
if nclx.full_range_flag {
YCbCrRange::Full
} else {
YCbCrRange::Limited
}
})
}

fn ycbcr_matrix_from_primary_colr(
Expand All @@ -6496,9 +6548,9 @@ fn ycbcr_matrix_override_from_primary_colr(
.as_ref()
.filter(|nclx| !nclx_is_undefined(nclx))
.map(|nclx| YCbCrMatrixCoefficients {
matrix_coefficients: nclx.matrix_coefficients,
colour_primaries: nclx.colour_primaries,
})
matrix_coefficients: nclx.matrix_coefficients,
colour_primaries: nclx.colour_primaries,
})
}

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -7745,8 +7797,7 @@ fn convert_avif_to_rgba8(decoded: &DecodedAvifImage) -> Result<Vec<u8>, DecodeAv
decoded.layout == AvifPixelLayout::Yuv420,
);
// 8-bit grayscale: libheif's Op_mono_to_RGB24_32 copies Y verbatim.
let mono_verbatim =
matches!(chroma, ChromaPlanesU8::Monochrome) && decoded.bit_depth == 8;
let mono_verbatim = matches!(chroma, ChromaPlanesU8::Monochrome) && decoded.bit_depth == 8;

for y in 0..height {
let row_start = y * width;
Expand Down Expand Up @@ -8751,8 +8802,8 @@ fn scale_sample_to_u16(sample: u16, bit_depth: u8) -> u16 {
// the low bits only and vanishes in the harness's 8-bit pixel comparison.
let shift = 16 - u32::from(bit_depth);
let v = u32::from(sample);
((v << shift) | (v >> (u32::from(bit_depth).saturating_sub(shift))))
.min(u32::from(u16::MAX)) as u16
((v << shift) | (v >> (u32::from(bit_depth).saturating_sub(shift)))).min(u32::from(u16::MAX))
as u16
}

#[derive(Default)]
Expand Down Expand Up @@ -9085,3 +9136,40 @@ fn copy_plane_samples(

Ok(AvifPlaneSamples::U16(out))
}

#[cfg(test)]
mod tests {
use moxcms::{CicpColorPrimaries, ColorProfile, DataColorSpace, TransferCharacteristics};

use super::{isobmff, nclx_to_icc_profile};

#[test]
fn synthesizes_icc_profile_from_display_p3_nclx() {
let nclx = isobmff::NclxColorProfile {
colour_primaries: 12,
transfer_characteristics: 13,
matrix_coefficients: 1,
full_range_flag: true,
};

let icc = nclx_to_icc_profile(&nclx).expect("expected synthesized ICC");
let parsed = ColorProfile::new_from_slice(&icc).expect("synthesized ICC should parse");

assert_eq!(parsed.color_space, DataColorSpace::Rgb);
let cicp = parsed.cicp.expect("synthesized ICC should carry CICP");
assert_eq!(cicp.color_primaries, CicpColorPrimaries::Smpte432);
assert_eq!(cicp.transfer_characteristics, TransferCharacteristics::Srgb);
}

#[test]
fn skips_undefined_nclx_profile() {
let nclx = isobmff::NclxColorProfile {
colour_primaries: 2,
transfer_characteristics: 2,
matrix_coefficients: 2,
full_range_flag: true,
};

assert!(nclx_to_icc_profile(&nclx).is_none());
}
}