use crate::av1::Plane;
use otf_pixels_core::{PixelsError, Result};
const COEFF_BITS: u32 = 20;
const CHROMA_BITS: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Depths {
pub(crate) input: u8,
pub(crate) output: u8,
}
impl Depths {
pub(crate) fn for_input(input: u8) -> Self {
Self {
input,
output: if input > 8 { 16 } else { 8 },
}
}
pub(crate) fn output_max(self) -> i64 {
(1_i64 << self.output) - 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct YuvMatrix {
depths: Depths,
y_scale: i64,
y_offset: i64,
c_mid: i64,
weights: [(i64, i64); 3],
}
impl YuvMatrix {
pub(crate) fn new(matrix_coefficients: u16, full_range: bool, depths: Depths) -> Result<Self> {
let (kr, kb) = match matrix_coefficients {
1 => (0.2126, 0.0722),
2 | 5 | 6 => (0.299, 0.114),
9 => (0.2627, 0.0593),
8 => (0.0, 0.0),
other => {
return Err(PixelsError::unsupported(format!(
"avif: YUV to RGB conversion for colour matrix {other} is not implemented"
)));
}
};
let ycgco = matrix_coefficients == 8;
let kg = 1.0 - kr - kb;
let step = f64::from(1_u32 << (depths.input - 8));
let input_max = f64::from((1_u32 << depths.input) - 1);
let (y_span, y_offset, c_span) = if full_range {
(input_max, 0, input_max)
} else {
(219.0 * step, 16 << (depths.input - 8), 224.0 * step)
};
let out = depths.output_max() as f64;
let (y_scale, c_scale) = (out / y_span, out / c_span);
let fixed = |x: f64| (x * f64::from(1_u32 << COEFF_BITS)).round() as i64;
Ok(Self {
depths,
y_scale: fixed(y_scale),
y_offset,
c_mid: 1 << (depths.input - 1),
weights: if ycgco {
let c = fixed(c_scale);
[(-c, c), (c, 0), (-c, -c)]
} else {
[
(0, fixed(2.0 * (1.0 - kr) * c_scale)),
(
-fixed(2.0 * kb * (1.0 - kb) / kg * c_scale),
-fixed(2.0 * kr * (1.0 - kr) / kg * c_scale),
),
(fixed(2.0 * (1.0 - kb) * c_scale), 0),
]
},
})
}
fn rgb(&self, y: u16, u: i64, v: i64) -> [u16; 3] {
const SHIFT: u32 = COEFF_BITS + CHROMA_BITS;
const HALF: i64 = 1 << (SHIFT - 1);
let mid = self.c_mid << CHROMA_BITS;
let (u, v) = (u - mid, v - mid);
let luma = ((i64::from(y) - self.y_offset) * self.y_scale) << CHROMA_BITS;
let max = self.depths.output_max();
let channel = |sum: i64| ((sum + HALF) >> SHIFT).clamp(0, max) as u16;
self.weights.map(|(wu, wv)| channel(luma + wu * u + wv * v))
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Layout {
pub(crate) width: usize,
pub(crate) height: usize,
pub(crate) subsampling_x: usize,
pub(crate) subsampling_y: usize,
}
pub(crate) fn yuv_to_rgb(planes: &[Plane], layout: Layout, matrix: &YuvMatrix) -> Result<Vec<u16>> {
let [y_plane, u_plane, v_plane] = planes else {
return Err(PixelsError::malformed("avif", "a colour plane is missing"));
};
let chroma = Chroma {
luma_width: layout.width,
width: (layout.width + layout.subsampling_x) >> layout.subsampling_x,
height: (layout.height + layout.subsampling_y) >> layout.subsampling_y,
sub_x: layout.subsampling_x,
sub_y: layout.subsampling_y,
};
Ok(samples(layout, |x, y| {
let luma = y_plane.get(x, y).unwrap_or(0);
let u = i64::from(chroma.upsample(u_plane, x, y));
let v = i64::from(chroma.upsample(v_plane, x, y));
matrix.rgb(luma, u, v)
}))
}
pub(crate) fn identity_to_rgb(
planes: &[Plane],
layout: Layout,
depths: Depths,
) -> Result<Vec<u16>> {
let [g, b, r] = planes else {
return Err(PixelsError::malformed("avif", "a colour plane is missing"));
};
let scale = Scale::new(depths, true);
Ok(samples(layout, |x, y| {
let at = |plane: &Plane| scale.apply(plane.get(x, y).unwrap_or(0));
[at(r), at(g), at(b)]
}))
}
pub(crate) fn plane_to_grey(
plane: &Plane,
layout: Layout,
depths: Depths,
full_range: bool,
) -> Vec<u16> {
let scale = Scale::new(depths, full_range);
samples(layout, |x, y| [scale.apply(plane.get(x, y).unwrap_or(0))])
}
#[derive(Debug, Clone, Copy)]
struct Scale {
offset: i64,
span: i64,
output_max: i64,
}
impl Scale {
fn new(depths: Depths, full_range: bool) -> Self {
let step = 1_i64 << (depths.input - 8);
let (offset, span) = if full_range {
(0, (1_i64 << depths.input) - 1)
} else {
(16 * step, 219 * step)
};
Self {
offset,
span,
output_max: depths.output_max(),
}
}
fn apply(self, v: u16) -> u16 {
let num = (i64::from(v) - self.offset) * self.output_max;
(2 * num + self.span)
.div_euclid(2 * self.span)
.clamp(0, self.output_max) as u16
}
}
fn samples<const N: usize>(layout: Layout, pixel: impl Fn(usize, usize) -> [u16; N]) -> Vec<u16> {
let mut out = Vec::with_capacity(layout.width * layout.height * N);
for y in 0..layout.height {
for x in 0..layout.width {
out.extend_from_slice(&pixel(x, y));
}
}
out
}
struct Chroma {
luma_width: usize,
width: usize,
height: usize,
sub_x: usize,
sub_y: usize,
}
impl Chroma {
fn upsample(&self, plane: &Plane, x: usize, y: usize) -> i32 {
let taps = |pos: usize, sub: usize, len: usize| -> [(usize, i32); 2] {
if sub == 0 {
return [(pos, 4), (pos, 0)];
}
let near = pos >> 1;
let far = if pos & 1 == 0 {
near.saturating_sub(1)
} else {
(near + 1).min(len - 1)
};
[(near.min(len - 1), 3), (far, 1)]
};
let sample = |cx: usize, cy: usize| i32::from(plane.get(cx, cy).unwrap_or(0));
let x_taps = if self.sub_x == 1 && x + 1 == self.luma_width && x & 1 == 0 {
[(x >> 1, 4), (x >> 1, 0)]
} else {
taps(x, self.sub_x, self.width)
};
let mut sum = 0;
for (cy, wy) in taps(y, self.sub_y, self.height) {
for (cx, wx) in x_taps {
sum += wy * wx * sample(cx, cy);
}
}
sum
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
const DEPTHS: [Depths; 3] = [
Depths {
input: 8,
output: 8,
},
Depths {
input: 10,
output: 16,
},
Depths {
input: 12,
output: 16,
},
];
fn exact(matrix: u16, full_range: bool, depths: Depths, y: f64, u: f64, v: f64) -> [u16; 3] {
let (kr, kb) = match matrix {
1 => (0.2126, 0.0722),
9 => (0.2627, 0.0593),
_ => (0.299, 0.114),
};
let step = f64::from(1_u32 << (depths.input - 8));
let max = f64::from((1_u32 << depths.input) - 1);
let mid = f64::from(1_u32 << (depths.input - 1));
let (y, u, v) = if full_range {
(y / max, (u - mid) / max, (v - mid) / max)
} else {
(
(y - 16.0 * step) / (219.0 * step),
(u - mid) / (224.0 * step),
(v - mid) / (224.0 * step),
)
};
let (r, g, b) = if matrix == 8 {
(y - u + v, y + u, y - u - v)
} else {
let r = y + 2.0 * (1.0 - kr) * v;
let b = y + 2.0 * (1.0 - kb) * u;
(r, (y - kr * r - kb * b) / (1.0 - kr - kb), b)
};
let out = depths.output_max() as f64;
[r, g, b].map(|c| (c * out).round().clamp(0.0, out) as u16)
}
#[test]
fn fixed_point_agrees_with_exact_arithmetic() {
for depths in DEPTHS {
let max = (1_u16 << depths.input) - 1;
for matrix in [1, 6, 8, 9] {
for full_range in [true, false] {
let m = YuvMatrix::new(matrix, full_range, depths).unwrap();
let mut off_by_one = 0;
let mut total = 0;
for y in (0..=max).step_by(usize::from(max / 51)) {
for u in (0..=max).step_by(usize::from(max / 36)) {
for v in (0..=max).step_by(usize::from(max / 36)) {
let (cu, cv) = (i64::from(u), i64::from(v));
let ours = m.rgb(y, cu << CHROMA_BITS, cv << CHROMA_BITS);
let want =
exact(matrix, full_range, depths, y.into(), u.into(), v.into());
for (a, b) in ours.iter().zip(&want) {
let d = a.abs_diff(*b);
assert!(
d <= 1,
"{depths:?} matrix {matrix} full {full_range} yuv {y},{u},{v}"
);
off_by_one += usize::from(d == 1);
total += 1;
}
}
}
}
assert!(
off_by_one * 1000 < total,
"{depths:?} matrix {matrix}: {off_by_one} of {total} off by one"
);
}
}
}
}
#[test]
fn grey_stays_grey_and_the_range_ends_map_to_black_and_white() {
for depths in DEPTHS {
let step = 1_u16 << (depths.input - 8);
let (max_in, max_out) = ((1_u16 << depths.input) - 1, depths.output_max() as u16);
let mid = (1_i64 << (depths.input - 1)) << CHROMA_BITS;
for matrix in [1, 6, 8, 9] {
let full = YuvMatrix::new(matrix, true, depths).unwrap();
assert_eq!(full.rgb(0, mid, mid), [0; 3]);
assert_eq!(full.rgb(max_in, mid, mid), [max_out; 3]);
let studio = YuvMatrix::new(matrix, false, depths).unwrap();
assert_eq!(studio.rgb(16 * step, mid, mid), [0; 3]);
assert_eq!(studio.rgb(235 * step, mid, mid), [max_out; 3]);
}
}
let eight = YuvMatrix::new(6, true, DEPTHS[0]).unwrap();
assert_eq!(
eight.rgb(77, 128 << CHROMA_BITS, 128 << CHROMA_BITS),
[77; 3]
);
}
#[test]
fn unimplemented_matrices_are_refused() {
for matrix in [0, 3, 4, 7, 10, 12, 13, 14, 16] {
assert!(
YuvMatrix::new(matrix, true, DEPTHS[0]).is_err(),
"matrix {matrix}"
);
}
}
#[test]
fn wide_output_is_full_range_sixteen_bit() {
let mut planes = [Plane::new(1, 1), Plane::new(1, 1), Plane::new(1, 1)];
planes[0].set(0, 0, 1023); planes[1].set(0, 0, 0); planes[2].set(0, 0, 512); let layout = Layout {
width: 1,
height: 1,
subsampling_x: 0,
subsampling_y: 0,
};
let rgb = identity_to_rgb(&planes, layout, Depths::for_input(10)).unwrap();
assert_eq!(rgb, [32800, 65535, 0]);
}
#[test]
fn grey_expands_studio_range_and_rescales_depth() {
let layout = Layout {
width: 4,
height: 1,
subsampling_x: 0,
subsampling_y: 0,
};
let mut plane = Plane::new(4, 1);
for (x, v) in [0, 16, 126, 235].into_iter().enumerate() {
plane.set(x, 0, v);
}
let eight = Depths::for_input(8);
assert_eq!(
plane_to_grey(&plane, layout, eight, true),
[0, 16, 126, 235]
);
assert_eq!(
plane_to_grey(&plane, layout, eight, false),
[0, 0, 128, 255]
);
let mut wide = Plane::new(4, 1);
wide.set(0, 0, 4095);
wide.set(1, 0, 2048);
let grey = plane_to_grey(&wide, layout, Depths::for_input(12), true);
assert_eq!(&grey[..2], [65535, 32776]);
}
#[test]
fn upsampling_weighs_the_nearer_chroma_sample_three_to_one() {
let mut plane = Plane::new(2, 1);
plane.set(1, 0, 64);
let chroma = Chroma {
luma_width: 4,
width: 2,
height: 1,
sub_x: 1,
sub_y: 1,
};
let at = |x| chroma.upsample(&plane, x, 0) >> CHROMA_BITS;
assert_eq!([at(0), at(1), at(2), at(3)], [0, 16, 48, 64]);
}
#[test]
fn an_odd_widths_last_column_copies_its_chroma_as_libyuv_does() {
let mut plane = Plane::new(2, 2);
plane.set(1, 0, 64);
plane.set(0, 1, 64);
plane.set(1, 1, 64);
let chroma = Chroma {
luma_width: 3,
width: 2,
height: 2,
sub_x: 1,
sub_y: 1,
};
let at = |x, y| chroma.upsample(&plane, x, y) >> CHROMA_BITS;
assert_eq!([at(0, 0), at(1, 0), at(2, 0)], [0, 16, 64]);
assert_eq!([at(0, 2), at(2, 2)], [48, 64]);
}
}