diff --git a/ravif/src/av1encoder.rs b/ravif/src/av1encoder.rs index 1a3fabf..b9cc8fd 100644 --- a/ravif/src/av1encoder.rs +++ b/ravif/src/av1encoder.rs @@ -1,11 +1,104 @@ #![allow(deprecated)] + use crate::dirtyalpha::blurred_dirty_alpha; use crate::error::Error; #[cfg(not(feature = "threading"))] use crate::rayoff as rayon; use imgref::{Img, ImgVec}; use rav1e::prelude::*; -use rgb::{RGB8, RGBA8}; +use rgb::{Rgb, Rgba}; +use rav1e::color::{TransferCharacteristics, MatrixCoefficients}; + +#[derive(Debug,Clone, Copy)] +pub enum ColorSpace { + Srgb, + DisplayP3, + Rec2020Pq, +} + +fn to_avif_serialize(coefficients: MatrixCoefficients) -> avif_serialize::constants::MatrixCoefficients { + match coefficients { + MatrixCoefficients::Identity => avif_serialize::constants::MatrixCoefficients::Rgb, + MatrixCoefficients::BT709 => avif_serialize::constants::MatrixCoefficients::Bt709, + MatrixCoefficients::BT601 => avif_serialize::constants::MatrixCoefficients::Bt601, + MatrixCoefficients::YCgCo => avif_serialize::constants::MatrixCoefficients::Ycgco, + MatrixCoefficients::BT2020NCL => avif_serialize::constants::MatrixCoefficients::Bt2020Ncl, + MatrixCoefficients::BT2020CL => avif_serialize::constants::MatrixCoefficients::Bt2020Cl, + _ => avif_serialize::constants::MatrixCoefficients::Unspecified, + } +} + +// From ITU-T H.273, Table 4 + Equations 45 to 47 +#[inline] +fn enum_to_values(matrix_coefficients: MatrixCoefficients) -> [f32; 3] { + let r_g = match matrix_coefficients { + MatrixCoefficients::Identity => [0.0; 2], + MatrixCoefficients::BT709 => [0.2126, 0.0722], + MatrixCoefficients::BT470BG => [0.3, 0.11], + MatrixCoefficients::BT601 => [0.299, 0.114], + MatrixCoefficients::SMPTE240 => [ 0.212, 0.087 ], + MatrixCoefficients::BT2020NCL => [ 0.2627, 0.0593 ], + MatrixCoefficients::Unspecified => unreachable!("This is unspecified."), + MatrixCoefficients::FCC => unimplemented!("For future use by ITU-T | ISO/IEC"), + _ => unimplemented!(), + }; + + [r_g[0], 1.0 - r_g[0] - r_g[1], r_g[1]] +} + +/// Converts RGB to YCbCr based on MatrixCoefficients. Doesn't apply any transformation when [`MatrixCoefficients::Unspecified`] is set. +fn apply_matrix_transformations(coefficients: MatrixCoefficients, bit_depth: BitDepth, px: Rgb

) -> [P; 3] { + let depth = bit_depth.to_usize(); + let max_value = ((1 << depth) - 1) as f32; + let coefficients = enum_to_values(coefficients); + + let rf = (u32::cast_from(px.r) as f32) / max_value; + let gf = (u32::cast_from(px.g) as f32) / max_value; + let bf = (u32::cast_from(px.b) as f32) / max_value; + + let y = coefficients[0] * rf + coefficients[1]* gf + coefficients[2]* bf; + let cb = (bf - y) / (2.0*(1.0 - coefficients[2])); + let cr = (rf - y) / (2.0*(1.0 - coefficients[0])); + + let y_int = (y * max_value).round() as u16; + let cb_int = ((cb + 0.5)*max_value).round() as u16; + let cr_int = ((cr + 0.5)*max_value).round() as u16; + + [P::cast_from(y_int), P::cast_from(cb_int), P::cast_from(cr_int)] +} + +impl ColorSpace { + const fn transfer_characteristics(self) -> TransferCharacteristics { + match self { + ColorSpace::Srgb => TransferCharacteristics::SRGB, + // For Rec.2020 there are a few valid transfer functions, two are currently supported here. + ColorSpace::Rec2020Pq => TransferCharacteristics::SMPTE2084, + // Display P3 uses the sRGB transfer function + ColorSpace::DisplayP3 => TransferCharacteristics::SRGB, + } + } + + const fn color_primaries(self) -> ColorPrimaries { + match self { + ColorSpace::Srgb => ColorPrimaries::BT709, + ColorSpace::Rec2020Pq => ColorPrimaries::BT2020, + ColorSpace::DisplayP3 => ColorPrimaries::SMPTE432 + } + } + + const fn matrix_coefficients(self, color_model: ColorModel) -> MatrixCoefficients { + match color_model{ + ColorModel::YCbCr => match self { + ColorSpace::Srgb => MatrixCoefficients::BT709, + ColorSpace::Rec2020Pq => MatrixCoefficients::BT2020NCL, + // Since there's no matrix for Display P3 implemented, the one of BT709 is used. + // Although this isn't perfectly accurate, it should be acceptable as long as the decoder uses the same approach. + ColorSpace::DisplayP3 => MatrixCoefficients::BT709, + }, + ColorModel::RGB => MatrixCoefficients::Identity, + } + } +} /// For [`Encoder::with_internal_color_model`] #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -38,13 +131,25 @@ pub enum AlphaColorMode { Premultiplied, } +/// The 8-bit mode only exists as a historical curiosity caused by lack of interoperability with old Safari versions. +/// There's no other reason to use it. 8 bits internally isn't precise enough for a complex codec like AV1, and 10 bits always compresses much better (even if the input and output are 8-bit sRGB). +/// The workaround for Safari is no longer needed, and the 8-bit encoding is planned to be deleted in a few months when usage of the oldest Safari versions becomes negligible. +/// https://github.com/kornelski/cavif-rs/pull/94#discussion_r1883073823 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] pub enum BitDepth { - #[default] Eight, + #[default] Ten, - /// Pick 8 or 10 depending on image format and decoder compatibility - Auto, +} + +impl BitDepth { + /// Returns the bit depth in usize, this can currently be either `8` or `10`. + fn to_usize(self) -> usize { + match self { + BitDepth::Eight => 8, + BitDepth::Ten => 10, + } + } } /// The newly-created image file + extra info FYI @@ -72,6 +177,8 @@ pub struct Encoder { premultiplied_alpha: bool, /// Which pixel format to use in AVIF file. RGB tends to give larger files. color_model: ColorModel, + /// Which color space is processed and stored in the AVIF file. + color_space: ColorSpace, /// How many threads should be used (0 = match core count), None - use global rayon thread pool threads: Option, /// [`AlphaColorMode`] @@ -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 { Self { quantizer: quality_to_quantizer(80.), alpha_quantizer: quality_to_quantizer(80.), @@ -94,6 +202,7 @@ impl Encoder { color_model: ColorModel::YCbCr, threads: None, alpha_color_mode: AlphaColorMode::UnassociatedClean, + color_space, } } @@ -110,11 +219,12 @@ impl Encoder { #[doc(hidden)] #[deprecated(note = "Renamed to with_bit_depth")] pub fn with_depth(self, depth: Option) -> 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)) } - /// Depth 8 or 10. + /// Depth 8 or 10-bit, default is 10-bit, even when 8 bit input data is provided. #[inline(always)] + #[track_caller] #[must_use] pub fn with_bit_depth(mut self, depth: BitDepth) -> Self { self.depth = depth; @@ -154,7 +264,6 @@ impl Encoder { } #[doc(hidden)] - #[deprecated = "Renamed to `with_internal_color_model()`"] pub fn with_internal_color_space(self, color_model: ColorModel) -> Self { self.with_internal_color_model(color_model) } @@ -199,66 +308,43 @@ impl Encoder { /// /// If all pixels are opaque, the alpha channel will be left out automatically. /// - /// This function takes 8-bit inputs, but will generate an AVIF file using 10-bit depth. - /// /// returns AVIF file with info about sizes about AV1 payload. - pub fn encode_rgba(&self, in_buffer: Img<&[rgb::RGBA]>) -> Result { + pub fn encode_rgba(&self, in_buffer: Img<&[Rgba

]>) -> Result { let new_alpha = self.convert_alpha(in_buffer); - 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>>| b.as_ref()).unwrap_or(in_buffer); + let use_alpha = buffer.pixels().any(|px| px.a != P::cast_from(255)); if !use_alpha { return self.encode_rgb_internal(buffer.width(), buffer.height(), buffer.pixels().map(|px| px.rgb())); } let width = buffer.width(); let height = buffer.height(); - let matrix_coefficients = match self.color_model { - ColorModel::YCbCr => MatrixCoefficients::BT601, - ColorModel::RGB => MatrixCoefficients::Identity, - }; - match self.depth { - BitDepth::Eight | BitDepth::Auto => { - let planes = buffer.pixels().map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px.rgb(), BT601), - ColorModel::RGB => rgb_to_8_bit_gbr(px.rgb()), - }; - [y, u, v] - }); - let alpha = buffer.pixels().map(|px| px.a); - self.encode_raw_planes_8_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) - }, - BitDepth::Ten => { - let planes = buffer.pixels().map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px.rgb(), BT601), - ColorModel::RGB => rgb_to_10_bit_gbr(px.rgb()), - }; - [y, u, v] - }); - let alpha = buffer.pixels().map(|px| to_ten(px.a)); - self.encode_raw_planes_10_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) - }, - } + let mc = self.color_space.matrix_coefficients(self.color_model); + let planes = buffer.pixels().map(|px| match self.color_model { + ColorModel::YCbCr => apply_matrix_transformations(mc, self.depth, px.rgb()), + ColorModel::RGB => [px.g, px.b, px.r], + }); + let alpha = buffer.pixels().map(|px| px.a); + self.encode_raw_planes(width, height, planes, Some(alpha), PixelRange::Full) } - fn convert_alpha(&self, in_buffer: Img<&[RGBA8]>) -> Option> { + fn convert_alpha(&self, in_buffer: Img<&[Rgba

]>) -> Option>> { + let max_value = (1 << self.depth.to_usize()) -1; match self.alpha_color_mode { AlphaColorMode::UnassociatedDirty => None, AlphaColorMode::UnassociatedClean => blurred_dirty_alpha(in_buffer), AlphaColorMode::Premultiplied => { - let prem = in_buffer.pixels() - .filter(|px| px.a != 255) + let prem = in_buffer + .pixels() + .filter(|px| px.a != P::cast_from(max_value)) .map(|px| { - if px.a == 0 { - RGBA8::default() + if Into::::into(px.a) == 0 { + Rgba::new(px.a, px.a, px.a, px.a) } else { - RGBA8::new( - (u16::from(px.r) * 255 / u16::from(px.a)) as u8, - (u16::from(px.g) * 255 / u16::from(px.a)) as u8, - (u16::from(px.b) * 255 / u16::from(px.a)) as u8, - px.a, - ) + let r = px.r * P::cast_from(max_value) / px.a; + let g = px.g * P::cast_from(max_value) / px.a; + let b = px.b * P::cast_from(max_value) / px.a; + Rgba::new(r, g, b, px.a) } }) .collect(); @@ -284,83 +370,45 @@ impl Encoder { /// /// returns AVIF file, size of color metadata #[inline] - pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result { + pub fn encode_rgb(&self, buffer: Img<&[Rgb

]>) -> Result { self.encode_rgb_internal(buffer.width(), buffer.height(), buffer.pixels()) } - fn encode_rgb_internal(&self, width: usize, height: usize, pixels: impl Iterator + Send + Sync) -> Result { - let matrix_coefficients = match self.color_model { - ColorModel::YCbCr => MatrixCoefficients::BT601, - ColorModel::RGB => MatrixCoefficients::Identity, - }; + fn encode_rgb_internal( + &self, width: usize, height: usize, pixels: impl Iterator> + Send + Sync, + ) -> Result { + let is_eight_bit = std::mem::size_of::

() == 1; - match self.depth { - BitDepth::Eight => { - let planes = pixels.map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px, BT601), - ColorModel::RGB => rgb_to_8_bit_gbr(px), - }; - [y, u, v] - }); - self.encode_raw_planes_8_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) - }, - BitDepth::Ten | BitDepth::Auto => { - let planes = pixels.map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px, BT601), - ColorModel::RGB => rgb_to_10_bit_gbr(px), - }; - [y, u, v] - }); - self.encode_raw_planes_10_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) - }, - } - } + // First convert from RGB to GBR or YCbCr + let mc = self.color_space.matrix_coefficients(self.color_model); + let planes = pixels.map(|px| match self.color_model { + ColorModel::YCbCr => apply_matrix_transformations(mc, self.depth, px), + ColorModel::RGB => [px.g, px.b, px.r], + }); - /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, - /// with sRGB transfer characteristics and color primaries. - /// - /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway. - /// If there's no alpha, use `None::<[_; 0]>`. - /// - /// returns AVIF file, size of color metadata, size of alpha metadata overhead - #[inline] - pub fn encode_raw_planes_8_bit( - &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, - ) -> Result { - self.encode_raw_planes(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 8) + // Then convert the bit depth when needed. + if self.depth != BitDepth::Eight && is_eight_bit { + let planes_u16 = planes.map(|px| [to_ten(px[0]), to_ten(px[1]), to_ten(px[2])]); + self.encode_raw_planes(width, height, planes_u16, None::<[_; 0]>, PixelRange::Full) + } else { + self.encode_raw_planes(width, height, planes, None::<[_; 0]>, PixelRange::Full) + } } /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, /// with sRGB transfer characteristics and color primaries. /// - /// The pixels are 10-bit (values `0.=1023`). + /// If pixels are 10-bit values range from `0.=1023`. /// /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway. /// If there's no alpha, use `None::<[_; 0]>`. /// /// returns AVIF file, size of color metadata, size of alpha metadata overhead - #[inline] - pub fn encode_raw_planes_10_bit( - &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, - ) -> Result { - self.encode_raw_planes(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 10) - } - #[inline(never)] - fn encode_raw_planes( + fn encode_raw_planes( &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, bit_depth: u8, + color_pixel_range: PixelRange, ) -> Result { - let color_description = Some(ColorDescription { - transfer_characteristics: TransferCharacteristics::SRGB, - color_primaries: ColorPrimaries::BT709, // sRGB-compatible - matrix_coefficients, - }); - let threads = self.threads.map(|threads| { if threads > 0 { threads } else { rayon::current_num_threads() } }); @@ -370,13 +418,14 @@ impl Encoder { &Av1EncodeConfig { width, height, - bit_depth: bit_depth.into(), + bit_depth: self.depth.to_usize(), quantizer: self.quantizer.into(), speed: SpeedTweaks::from_my_preset(self.speed, self.quantizer), threads, pixel_range: color_pixel_range, chroma_sampling: ChromaSampling::Cs444, - color_description, + color_space: self.color_space, + color_model: self.color_model, }, move |frame| init_frame_3(width, height, planes, frame), ) @@ -387,13 +436,14 @@ impl Encoder { &Av1EncodeConfig { width, height, - bit_depth: bit_depth.into(), + bit_depth: self.depth.to_usize(), quantizer: self.alpha_quantizer.into(), speed: SpeedTweaks::from_my_preset(self.speed, self.alpha_quantizer), threads, pixel_range: PixelRange::Full, chroma_sampling: ChromaSampling::Cs400, - color_description: None, + color_space: self.color_space, + color_model: self.color_model, }, |frame| init_frame_1(width, height, alpha, frame), ) @@ -406,18 +456,9 @@ impl Encoder { let (color, alpha) = (color?, alpha.transpose()?); let avif_file = avif_serialize::Aviffy::new() - .matrix_coefficients(match matrix_coefficients { - MatrixCoefficients::Identity => avif_serialize::constants::MatrixCoefficients::Rgb, - MatrixCoefficients::BT709 => avif_serialize::constants::MatrixCoefficients::Bt709, - MatrixCoefficients::Unspecified => avif_serialize::constants::MatrixCoefficients::Unspecified, - MatrixCoefficients::BT601 => avif_serialize::constants::MatrixCoefficients::Bt601, - MatrixCoefficients::YCgCo => avif_serialize::constants::MatrixCoefficients::Ycgco, - MatrixCoefficients::BT2020NCL => avif_serialize::constants::MatrixCoefficients::Bt2020Ncl, - MatrixCoefficients::BT2020CL => avif_serialize::constants::MatrixCoefficients::Bt2020Cl, - _ => return Err(Error::Unsupported("matrix coefficients")), - }) + .matrix_coefficients(to_avif_serialize(self.color_space.matrix_coefficients(self.color_model))) .premultiplied_alpha(self.premultiplied_alpha) - .to_vec(&color, alpha.as_deref(), width as u32, height as u32, bit_depth); + .to_vec(&color, alpha.as_deref(), width as u32, height as u32, self.depth.to_usize() as u8); let color_byte_size = color.len(); let alpha_byte_size = alpha.as_ref().map_or(0, |a| a.len()); @@ -428,44 +469,8 @@ impl Encoder { } #[inline(always)] -fn to_ten(x: u8) -> u16 { - (u16::from(x) << 2) | (u16::from(x) >> 6) -} - -#[inline(always)] -fn rgb_to_10_bit_gbr(px: rgb::RGB) -> (u16, u16, u16) { - (to_ten(px.g), to_ten(px.b), to_ten(px.r)) -} - -#[inline(always)] -fn rgb_to_8_bit_gbr(px: rgb::RGB) -> (u8, u8, u8) { - (px.g, px.b, px.r) -} - -// const REC709: [f32; 3] = [0.2126, 0.7152, 0.0722]; -const BT601: [f32; 3] = [0.2990, 0.5870, 0.1140]; - -#[inline(always)] -fn rgb_to_ycbcr(px: rgb::RGB, depth: u8, matrix: [f32; 3]) -> (f32, f32, f32) { - let max_value = ((1 << depth) - 1) as f32; - let scale = max_value / 255.; - let shift = (max_value * 0.5).round(); - let y = scale * matrix[0] * f32::from(px.r) + scale * matrix[1] * f32::from(px.g) + scale * matrix[2] * f32::from(px.b); - let cb = (f32::from(px.b) * scale - y).mul_add(0.5 / (1. - matrix[2]), shift); - let cr = (f32::from(px.r) * scale - y).mul_add(0.5 / (1. - matrix[0]), shift); - (y.round(), cb.round(), cr.round()) -} - -#[inline(always)] -fn rgb_to_10_bit_ycbcr(px: rgb::RGB, matrix: [f32; 3]) -> (u16, u16, u16) { - let (y, u, v) = rgb_to_ycbcr(px, 10, matrix); - (y as u16, u as u16, v as u16) -} - -#[inline(always)] -fn rgb_to_8_bit_ycbcr(px: rgb::RGB, matrix: [f32; 3]) -> (u8, u8, u8) { - let (y, u, v) = rgb_to_ycbcr(px, 8, matrix); - (y as u8, u as u8, v as u8) +fn to_ten(x: P) -> u16 { + (u16::cast_from(x) << 2) | (u16::cast_from(x) >> 6) } fn quality_to_quantizer(quality: f32) -> u8 { @@ -601,7 +606,8 @@ struct Av1EncodeConfig { pub threads: Option, pub pixel_range: PixelRange, pub chroma_sampling: ChromaSampling, - pub color_description: Option, + pub color_space: ColorSpace, + pub color_model: ColorModel, } fn rav1e_config(p: &Av1EncodeConfig) -> Config { @@ -611,6 +617,14 @@ fn rav1e_config(p: &Av1EncodeConfig) -> Config { let threads = p.threads.unwrap_or_else(rayon::current_num_threads); threads.min((p.width * p.height) / (p.speed.min_tile_size as usize).pow(2)) }; + + let color_description = ColorDescription{ + color_primaries: p.color_space.color_primaries(), + transfer_characteristics: p.color_space.transfer_characteristics(), + matrix_coefficients: p.color_space.matrix_coefficients(p.color_model), + }; + + let speed_settings = p.speed.speed_settings(); let cfg = Config::new() .with_encoder_config(EncoderConfig { @@ -622,7 +636,7 @@ fn rav1e_config(p: &Av1EncodeConfig) -> Config { chroma_sampling: p.chroma_sampling, chroma_sample_position: ChromaSamplePosition::Unknown, pixel_range: p.pixel_range, - color_description: p.color_description, + color_description: Some(color_description), mastering_display: None, content_light: None, enable_timing_info: false, @@ -652,9 +666,7 @@ fn rav1e_config(p: &Av1EncodeConfig) -> Config { } } -fn init_frame_3( - width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

, -) -> Result<(), Error> { +fn init_frame_3(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { let mut f = frame.planes.iter_mut(); let mut planes = planes.into_iter(); @@ -677,7 +689,7 @@ fn init_frame_3( Ok(()) } -fn init_frame_1(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { +fn init_frame_1(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { let mut y = frame.planes[0].mut_slice(Default::default()); let mut planes = planes.into_iter(); @@ -691,7 +703,7 @@ fn init_frame_1(width: usize, height: usize, planes: } #[inline(never)] -fn encode_to_av1(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame

) -> Result<(), Error>) -> Result, Error> { +fn encode_to_av1(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame

) -> Result<(), Error>) -> Result, Error> { let mut ctx: Context

= rav1e_config(p).new_context()?; let mut frame = ctx.new_frame(); diff --git a/ravif/src/dirtyalpha.rs b/ravif/src/dirtyalpha.rs index b07b4a1..89c1b8a 100644 --- a/ravif/src/dirtyalpha.rs +++ b/ravif/src/dirtyalpha.rs @@ -1,66 +1,85 @@ -use imgref::{Img, ImgRef}; -use rgb::{ComponentMap, RGB, RGBA8}; +use imgref::Img; +use imgref::ImgRef; +use rav1e::Pixel; +use rgb::ComponentMap; +use rgb::Rgb; +use rgb::Rgba; #[inline] -fn weighed_pixel(px: RGBA8) -> (u16, RGB) { - if px.a == 0 { - return (0, RGB::new(0, 0, 0)); +fn weighed_pixel(px: Rgba

) -> (P, Rgb

) { + if px.a == P::cast_from(0) { + return (px.a, Rgb::new(px.a, px.a, px.a)); } - let weight = 256 - u16::from(px.a); - (weight, RGB::new( - u32::from(px.r) * u32::from(weight), - u32::from(px.g) * u32::from(weight), - u32::from(px.b) * u32::from(weight))) + let weight = P::cast_from(256) - px.a; + ( + weight, + Rgb::new(px.r * weight, px.g * weight, px.b * weight), + ) } /// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1 -pub(crate) fn blurred_dirty_alpha(img: ImgRef) -> Option>> { +pub(crate) fn blurred_dirty_alpha( + img: ImgRef>, +) -> Option>>> { // get dominant visible transparent color (excluding opaque pixels) - let mut sum = RGB::new(0, 0, 0); + let mut sum = Rgb::new(0, 0, 0); let mut weights = 0; // Only consider colors around transparent images // (e.g. solid semitransparent area doesn't need to contribute) loop9::loop9_img(img, |_, _, top, mid, bot| { - if mid.curr.a == 255 || mid.curr.a == 0 { + if mid.curr.a == P::cast_from(255) || mid.curr.a == P::cast_from(0) { return; } - if chain(&top, &mid, &bot).any(|px| px.a == 0) { + if chain(&top, &mid, &bot).any(|px| px.a == P::cast_from(0)) { let (w, px) = weighed_pixel(mid.curr); - weights += u64::from(w); - sum += px.map(u64::from); + weights += Into::::into(w) as u64; + sum += Rgb::new( + Into::::into(px.r) as u64, + Into::::into(px.g) as u64, + Into::::into(px.b) as u64, + ); } }); if weights == 0 { return None; // opaque image } - let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0); + let neutral_alpha = Rgba::new( + P::cast_from((sum.r / weights) as u8), + P::cast_from((sum.g / weights) as u8), + P::cast_from((sum.b / weights) as u8), + P::cast_from(0), + ); let img2 = bleed_opaque_color(img, neutral_alpha); Some(blur_transparent_pixels(img2.as_ref())) } /// copy color from opaque pixels to transparent pixels /// (so that when edges get crushed by compression, the distortion will be away from visible edge) -fn bleed_opaque_color(img: ImgRef, bg: RGBA8) -> Img> { +fn bleed_opaque_color(img: ImgRef>, bg: Rgba

) -> Img>> { 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) { mid.curr } else { - let (weights, sum) = chain(&top, &mid, &bot) - .map(|c| weighed_pixel(*c)) - .fold((0u32, RGB::new(0,0,0)), |mut sum, item| { - sum.0 += u32::from(item.0); + let (weights, sum) = chain(&top, &mid, &bot).map(|c| weighed_pixel(*c)).fold( + ( + 0u32, + Rgb::new(P::cast_from(0), P::cast_from(0), P::cast_from(0)), + ), + |mut sum, item| { + sum.0 += Into::::into(item.0); sum.1 += item.1; sum - }); + }, + ); if weights == 0 { bg } else { - let mut avg = sum.map(|c| (c / weights) as u8); - if mid.curr.a == 0 { - avg.with_alpha(0) + let mut avg = sum.map(|c| P::cast_from(Into::::into(c) / weights)); + if mid.curr.a == P::cast_from(0) { + avg.with_alpha(mid.curr.a) } else { // also change non-transparent colors, but only within range where // rounding caused by premultiplied alpha would land on the same color @@ -76,16 +95,16 @@ fn bleed_opaque_color(img: ImgRef, bg: RGBA8) -> Img> { } /// ensure there are no sharp edges created by the cleared alpha -fn blur_transparent_pixels(img: ImgRef) -> Img> { +fn blur_transparent_pixels(img: ImgRef>) -> Img>> { 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) { mid.curr } else { - let sum: RGB = chain(&top, &mid, &bot).map(|px| px.rgb().map(u16::from)).sum(); - let mut avg = sum.map(|c| (c / 9) as u8); - if mid.curr.a == 0 { - avg.with_alpha(0) + let sum: Rgb

= chain(&top, &mid, &bot).map(|px| px.rgb()).sum(); + let mut avg = sum.map(|c| (c / P::cast_from(9))); + if mid.curr.a == P::cast_from(0) { + avg.with_alpha(mid.curr.a) } else { // also change non-transparent colors, but only within range where // rounding caused by premultiplied alpha would land on the same color @@ -100,27 +119,42 @@ fn blur_transparent_pixels(img: ImgRef) -> Img> { } #[inline(always)] -fn chain<'a, T>(top: &'a loop9::Triple, mid: &'a loop9::Triple, bot: &'a loop9::Triple) -> impl Iterator + 'a { +fn chain<'a, T>( + top: &'a loop9::Triple, + mid: &'a loop9::Triple, + bot: &'a loop9::Triple, +) -> impl Iterator + 'a { top.iter().chain(mid.iter()).chain(bot.iter()) } #[inline] -fn clamp(px: u8, (min, max): (u8, u8)) -> u8 { - px.max(min).min(max) +fn clamp(px: P, (min, max): (P, P)) -> P { + P::cast_from( + Into::::into(px) + .max(Into::::into(min)) + .min(Into::::into(max)), + ) } /// safe range to change px color given its alpha /// (mostly-transparent colors tolerate more variation) #[inline] -fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) { - let alpha = u16::from(alpha); - let rounded = u16::from(px) * alpha / 255 * 255; +fn premultiplied_minmax(px: P, alpha: T) -> (P, T) +where + P: Pixel + Default, + T: Pixel + Default, +{ + let alpha = Into::::into(alpha); + let rounded = Into::::into(px) * alpha / 255 * 255; // leave some spare room for rounding - let low = ((rounded + 16) / alpha) as u8; - let hi = ((rounded + 239) / alpha) as u8; + let low = (rounded + 16) / alpha; + let hi = (rounded + 239) / alpha; - (low.min(px), hi.max(px)) + ( + P::cast_from(low).min(px), + T::cast_from(hi).max(T::cast_from(Into::::into(px))), + ) } #[test] diff --git a/ravif/src/lib.rs b/ravif/src/lib.rs index 3dee2b5..a57d24f 100644 --- a/ravif/src/lib.rs +++ b/ravif/src/lib.rs @@ -14,11 +14,7 @@ mod error; pub use av1encoder::ColorModel; pub use error::Error; -#[doc(hidden)] -#[deprecated = "Renamed to `ColorModel`"] -pub use ColorModel as ColorSpace; - -pub use av1encoder::{AlphaColorMode, BitDepth, EncodedImage, Encoder}; +pub use av1encoder::{AlphaColorMode, BitDepth, EncodedImage, Encoder, ColorSpace}; #[doc(inline)] pub use rav1e::prelude::MatrixCoefficients; @@ -27,7 +23,7 @@ mod dirtyalpha; #[doc(no_inline)] pub use imgref::Img; #[doc(no_inline)] -pub use rgb::{RGB8, RGBA8}; +pub use rgb::{RGB16, RGB8, RGBA16, RGBA8}; #[cfg(not(feature = "threading"))] mod rayoff { diff --git a/src/main.rs b/src/main.rs index 51a50f3..8b87468 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use clap::{value_parser, Arg, ArgAction, Command}; use imgref::ImgVec; -use ravif::{AlphaColorMode, BitDepth, ColorModel, EncodedImage, Encoder, RGBA8}; +use ravif::{AlphaColorMode, BitDepth, ColorModel, ColorSpace, EncodedImage, Encoder, RGBA8}; use rayon::prelude::*; use std::fs; use std::io::{Read, Write}; @@ -41,11 +41,27 @@ fn parse_speed(arg: &str) -> Result { Ok(s) } +fn parse_color_space(s: &str) -> Result { + match s.to_lowercase().as_str() { + "srgb" => Ok(ColorSpace::Srgb), + "displayp3" => Ok(ColorSpace::DisplayP3), + "rec2020pq" => Ok(ColorSpace::Rec2020Pq), + _ => Err(format!("invalid color-space: {}", s)), + } +} + fn run() -> Result<(), BoxError> { let args = Command::new("cavif-rs") .version(clap::crate_version!()) .author("Kornel LesiƄski ") .about("Convert JPEG/PNG images to AVIF image format (based on AV1/rav1e)") + .arg(Arg::new("color-space") + .short('C') + .long("color-space") + .value_name("srgb/displayp3/rec2020pq") + .value_parser(parse_color_space) + .required(true) + .help("The color space passed values are in [possible values: srgb, displayp3, rec2020pq]")) .arg(Arg::new("quality") .short('Q') .long("quality") @@ -131,9 +147,11 @@ fn run() -> Result<(), BoxError> { let depth = match args.get_one::("depth").expect("default").as_str() { "8" => BitDepth::Eight, "10" => BitDepth::Ten, - _ => BitDepth::Auto, + _ => BitDepth::Ten, }; + let color_space = args.get_one::("color-space").expect("default"); + let files = args.get_many::("IMAGES").ok_or("Please specify image paths to convert")?; let files: Vec<_> = files .filter(|pathstr| { @@ -200,7 +218,7 @@ fn run() -> Result<(), BoxError> { }, _ => {}, } - let enc = Encoder::new() + let enc = Encoder::new(*color_space) .with_quality(quality) .with_bit_depth(depth) .with_speed(speed)