Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DisplayP3 and BT.2020 + PQ #96

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

hannes-vernooij
Copy link
Contributor

@hannes-vernooij hannes-vernooij commented Dec 23, 2024

⚠️ Depends on #94
This PR adds support for storing Display P3 and Rec.2020 + PQ aside from sRGB. Functionality to specify which color space to use has also been added to cavif-rs. Be mindful that exported HDR images are not well supported on the web.

Isolated changes: a8e10d4...e01354c

In the current implementation, 10-bit signals can be stored, but they are derived from 8-bit signals, offering no visual improvement. All incoming data has been assumed to be 8-bit sRGB. This PR takes the first step in removing these assumptions by enabling the storage of signals with higher bit depth. It lays the groundwork for full support of writing HDR images.
src/main.rs Outdated Show resolved Hide resolved
src/main.rs Outdated Show resolved Hide resolved
@hannes-vernooij hannes-vernooij marked this pull request as ready for review December 24, 2024 11:27
Ten,
/// Pick 8 or 10 depending on image format and decoder compatibility
Auto,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove the Auto option. I plan to keep it around for at least a year longer.

@@ -84,7 +191,8 @@ pub struct Encoder {
impl Encoder {
/// Start here
#[must_use]
pub fn new() -> Self {
// Assumptions about color spaces shouldn't be made since it can't reliably be deduced automatically.
pub fn new(color_space: ColorSpace) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the public API of ravif backwards-compatible. This crate's biggest user is the image crate, and I'd like to stay compatible with it. Users of the image crate are slow to upgrade it, so if I made a semver-major release of ravif, it would take quite a while before majority of its users got it. I prefer to be able to release improvements to all existing users quickly.

This unfortunately means that the old use of ColorSpace needs to stay for now. You could rename this one to InputColorSpace, or maybe ColorGamut?.

If it makes anything simpler, I suggest not supporting P3/2020 in 8-bit. There isn't enough precision in 8 bits for this to make sense, and I plan to remove 8-bit support eventually. You could just add dedicated non-generic 10-bit encoding functions that support wide gamut, and leave 8-bit code to rot.

@@ -110,11 +219,12 @@ impl Encoder {
#[doc(hidden)]
#[deprecated(note = "Renamed to with_bit_depth")]
pub fn with_depth(self, depth: Option<u8>) -> Self {
self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Auto))
self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Ten))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto is important for me.

let buffer = new_alpha.as_ref().map(|b| b.as_ref()).unwrap_or(in_buffer);
let use_alpha = buffer.pixels().any(|px| px.a != 255);
let buffer = new_alpha.as_ref().map(|b: &Img<Vec<Rgba<P>>>| b.as_ref()).unwrap_or(in_buffer);
let use_alpha = buffer.pixels().any(|px| px.a != P::cast_from(255));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks buggy. You've removed comment that this function is for 8-bit inputs only, but check against 255 looks like a bug for 10-bit depths. Please write a unit test to prove that this is not buggy.

@@ -284,83 +370,45 @@ impl Encoder {
///
/// returns AVIF file, size of color metadata
#[inline]
pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result<EncodedImage, Error> {
pub fn encode_rgb<P: Pixel + Default>(&self, buffer: Img<&[Rgb<P>]>) -> Result<EncodedImage, Error> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to expose the messy rav1e::Pixel in the public API.

Please keep encode_rgb unchanged for API backwards-compatibility, and add a separate method like encode_rgb_10bit that takes Rgb<u16>.

let mut out = Vec::with_capacity(img.width() * img.height());
loop9::loop9_img(img, |_, _, top, mid, bot| {
out.push(if mid.curr.a == 255 {
out.push(if mid.curr.a == P::cast_from(255) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug for 10-bit depths

let mut avg = sum.map(|c| (c / 9) as u8);
if mid.curr.a == 0 {
avg.with_alpha(0)
let sum: Rgb<P> = chain(&top, &mid, &bot).map(|px| px.rgb()).sum();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug for 8-bit depths. It's a sum of 9 pixels, so it will overflow u8.

T: Pixel + Default,
{
let alpha = Into::<u32>::into(alpha);
let rounded = Into::<u32>::into(px) * alpha / 255 * 255;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug for 10-bit depths

let low = ((rounded + 16) / alpha) as u8;
let hi = ((rounded + 239) / alpha) as u8;
let low = (rounded + 16) / alpha;
let hi = (rounded + 239) / alpha;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug for 10-bit depths too

.arg(Arg::new("color-space")
.short('C')
.long("color-space")
.value_name("srgb/displayp3/rec2020pq")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not work for the cavif tool. This command-line flag should not exist.

Input images for this program are not read with naive byte-by-byte copying of pixel values. All input images are color-managed and explicitly converted to sRGB, including destroying gamut of images in Rec.2020 and DisplayP3.

If you then take the always-sRGB color data and label it as P3 in AVIF metadata, then decoders will be fooled into interpreting values as in P3, and incorrectly stretch them over a wide gamut, giving a falsely boosted saturation with shifted primaries. Or when reading such AVIF for an sRGB screen, the P3 to sRGB conversion will crush the values and make them clearly different from the actual sRGB input.

To make such hacky relabelling work, users would need to generate P3 files, and then incorrectly relabel pixels as being in sRGB, to create an encoding error in the opposite way of the encoding error caused by this option.
If this was text, it would be like requiring users to type ’ mojibake to get printed in the output.

That's not a good practice, and that's not a good application interface.


For the ravif crate, it's fine to take user-supplied label, since it takes raw pixel data, so it's up to the caller to generate correct colors or decode input images properly themselves.

But cavif does reading of images itself, so support for wide-gamut inputs unfortunately requires changing how image loading works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants