use gamut_core::{Error, Result};
use crate::clip_pixel8;
const FIX: i32 = 16;
const HALF: i32 = 1 << (FIX - 1);
const CHROMA_BIAS: i32 = (128 << FIX) + HALF;
const LUMA_BIAS_LIMITED: i32 = 16 << FIX;
const FIX2: i32 = 6;
const MASK2: i32 = (256 << FIX2) - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bt601Range {
Full,
Limited,
}
fn mult_hi(v: i32, coeff: i32) -> i32 {
(v * coeff) >> 8
}
fn vp8_clip8(v: i32) -> u8 {
if v & !MASK2 == 0 {
(v >> FIX2) as u8
} else if v < 0 {
0
} else {
255
}
}
#[must_use]
pub fn rgb_to_ycbcr(r: u8, g: u8, b: u8, range: Bt601Range) -> (u8, u8, u8) {
let (r, g, b) = (i32::from(r), i32::from(g), i32::from(b));
match range {
Bt601Range::Full => {
let y = (19595 * r + 38470 * g + 7471 * b + HALF) >> FIX;
let cb = (-11059 * r - 21709 * g + 32768 * b + CHROMA_BIAS) >> FIX;
let cr = (32768 * r - 27439 * g - 5329 * b + CHROMA_BIAS) >> FIX;
(clip_pixel8(y), clip_pixel8(cb), clip_pixel8(cr))
}
Bt601Range::Limited => {
let y = (16839 * r + 33059 * g + 6420 * b + LUMA_BIAS_LIMITED + HALF) >> FIX;
let cb = (-9719 * r - 19081 * g + 28800 * b + CHROMA_BIAS) >> FIX;
let cr = (28800 * r - 24116 * g - 4684 * b + CHROMA_BIAS) >> FIX;
(clip_pixel8(y), clip_pixel8(cb), clip_pixel8(cr))
}
}
}
#[must_use]
pub fn ycbcr_to_rgb(y: u8, cb: u8, cr: u8, range: Bt601Range) -> (u8, u8, u8) {
match range {
Bt601Range::Full => {
let y = i32::from(y);
let cb = i32::from(cb) - 128;
let cr = i32::from(cr) - 128;
let r = y + ((91881 * cr + HALF) >> FIX);
let g = y + ((-22554 * cb - 46802 * cr + HALF) >> FIX);
let b = y + ((116130 * cb + HALF) >> FIX);
(clip_pixel8(r), clip_pixel8(g), clip_pixel8(b))
}
Bt601Range::Limited => {
let (y, cb, cr) = (i32::from(y), i32::from(cb), i32::from(cr));
let yy = mult_hi(y, 19077);
let r = vp8_clip8(yy + mult_hi(cr, 26149) - 14234);
let g = vp8_clip8(yy - mult_hi(cb, 6419) - mult_hi(cr, 13320) + 8708);
let b = vp8_clip8(yy + mult_hi(cb, 33050) - 17685);
(r, g, b)
}
}
}
#[derive(Debug, Clone)]
pub struct Yuv420 {
width: u32,
height: u32,
y: Vec<u8>,
u: Vec<u8>,
v: Vec<u8>,
}
impl Yuv420 {
#[must_use]
pub fn chroma_width(width: u32) -> u32 {
width.div_ceil(2)
}
#[must_use]
pub fn chroma_height(height: u32) -> u32 {
height.div_ceil(2)
}
pub fn new(width: u32, height: u32, y: Vec<u8>, u: Vec<u8>, v: Vec<u8>) -> Result<Self> {
let luma = width as usize * height as usize;
let chroma = Self::chroma_width(width) as usize * Self::chroma_height(height) as usize;
if y.len() != luma || u.len() != chroma || v.len() != chroma {
return Err(Error::InvalidInput(
"YUV plane length does not match dimensions",
));
}
Ok(Self {
width,
height,
y,
u,
v,
})
}
pub fn from_rgb8(rgb: &[u8], width: u32, height: u32, range: Bt601Range) -> Result<Self> {
let (w, h) = (width as usize, height as usize);
if width == 0 || height == 0 || rgb.len() != w * h * 3 {
return Err(Error::InvalidInput(
"rgb buffer length does not match dimensions",
));
}
let mut y = vec![0u8; w * h];
let mut cb_full = vec![0u8; w * h];
let mut cr_full = vec![0u8; w * h];
for i in 0..w * h {
let (yy, cb, cr) = rgb_to_ycbcr(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2], range);
y[i] = yy;
cb_full[i] = cb;
cr_full[i] = cr;
}
let cw = Self::chroma_width(width) as usize;
let ch = Self::chroma_height(height) as usize;
let mut u = vec![0u8; cw * ch];
let mut v = vec![0u8; cw * ch];
for cy in 0..ch {
for cx in 0..cw {
let (mut su, mut sv, mut count) = (0u32, 0u32, 0u32);
for dy in 0..2 {
for dx in 0..2 {
let (px, py) = (cx * 2 + dx, cy * 2 + dy);
if px < w && py < h {
su += u32::from(cb_full[py * w + px]);
sv += u32::from(cr_full[py * w + px]);
count += 1;
}
}
}
u[cy * cw + cx] = ((su + count / 2) / count) as u8;
v[cy * cw + cx] = ((sv + count / 2) / count) as u8;
}
}
Ok(Self {
width,
height,
y,
u,
v,
})
}
#[must_use]
pub fn to_rgb8(&self, range: Bt601Range) -> Vec<u8> {
let (w, h) = (self.width as usize, self.height as usize);
let cw = Self::chroma_width(self.width) as usize;
let mut out = vec![0u8; w * h * 3];
for py in 0..h {
for px in 0..w {
let ci = (py / 2) * cw + (px / 2);
let (r, g, b) = ycbcr_to_rgb(self.y[py * w + px], self.u[ci], self.v[ci], range);
let o = (py * w + px) * 3;
out[o] = r;
out[o + 1] = g;
out[o + 2] = b;
}
}
out
}
#[must_use]
pub fn width(&self) -> u32 {
self.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.height
}
#[must_use]
pub fn y(&self) -> &[u8] {
&self.y
}
#[must_use]
pub fn u(&self) -> &[u8] {
&self.u
}
#[must_use]
pub fn v(&self) -> &[u8] {
&self.v
}
}
#[cfg(test)]
mod tests {
use super::*;
use Bt601Range::{Full, Limited};
#[test]
fn full_range_color_anchors() {
assert_eq!(rgb_to_ycbcr(0, 0, 0, Full), (0, 128, 128));
assert_eq!(rgb_to_ycbcr(255, 255, 255, Full), (255, 128, 128));
assert_eq!(rgb_to_ycbcr(255, 0, 0, Full), (76, 85, 255));
let (y, cb, cr) = rgb_to_ycbcr(128, 128, 128, Full);
assert_eq!((cb, cr), (128, 128));
assert!((i32::from(y) - 128).abs() <= 1);
}
#[test]
fn limited_range_matches_libwebp_anchors() {
assert_eq!(rgb_to_ycbcr(0, 0, 0, Limited), (16, 128, 128));
assert_eq!(rgb_to_ycbcr(255, 255, 255, Limited), (235, 128, 128));
assert_eq!(rgb_to_ycbcr(255, 0, 0, Limited), (82, 90, 240));
let (r, g, b) = ycbcr_to_rgb(82, 90, 240, Limited);
assert!(
r >= 254 && g <= 2 && b <= 2,
"limited red inverse = ({r},{g},{b})"
);
}
#[test]
fn limited_luma_stays_in_studio_range() {
for r in (0..=255).step_by(17) {
for g in (0..=255).step_by(17) {
let (yl, ..) = rgb_to_ycbcr(r, g, 128, Limited);
assert!(
(16..=235).contains(&yl),
"limited Y {yl} out of studio range"
);
}
}
assert_eq!(rgb_to_ycbcr(0, 0, 0, Full).0, 0);
assert_eq!(rgb_to_ycbcr(255, 255, 255, Full).0, 255);
}
#[test]
fn pixel_roundtrip_within_tolerance() {
let colors = [
(0, 0, 0),
(255, 255, 255),
(255, 0, 0),
(0, 255, 0),
(0, 0, 255),
(10, 200, 90),
(123, 45, 67),
(200, 200, 50),
(17, 17, 200),
];
for range in [Full, Limited] {
for (r, g, b) in colors {
let (y, cb, cr) = rgb_to_ycbcr(r, g, b, range);
let (r2, g2, b2) = ycbcr_to_rgb(y, cb, cr, range);
let err = (i32::from(r) - i32::from(r2)).abs().max(
(i32::from(g) - i32::from(g2))
.abs()
.max((i32::from(b) - i32::from(b2)).abs()),
);
assert!(
err <= 4,
"{range:?} color ({r},{g},{b}) round-trip error {err}"
);
}
}
}
#[test]
fn flat_image_roundtrips_through_420() {
let rgb: Vec<u8> = [90u8, 140, 200].repeat(7 * 5);
for range in [Full, Limited] {
let yuv = Yuv420::from_rgb8(&rgb, 7, 5, range).unwrap();
let back = yuv.to_rgb8(range);
for (a, b) in rgb.iter().zip(&back) {
assert!(
(i32::from(*a) - i32::from(*b)).abs() <= 4,
"{range:?} round-trip"
);
}
}
}
#[test]
fn chroma_dimensions_round_up_for_odd_sizes() {
let yuv = Yuv420::from_rgb8(&[0u8; 5 * 3 * 3], 5, 3, Limited).unwrap();
assert_eq!(yuv.y().len(), 15);
assert_eq!((yuv.u().len(), yuv.v().len()), (6, 6));
assert_eq!(Yuv420::chroma_width(5), 3);
assert_eq!(Yuv420::chroma_height(3), 2);
}
#[test]
fn box_subsample_averages_the_block() {
let rgb = [
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, ];
let yuv = Yuv420::from_rgb8(&rgb, 2, 2, Full).unwrap();
assert_eq!((yuv.u().len(), yuv.v().len()), (1, 1));
let mut su = 0u32;
let mut sv = 0u32;
for &(r, g, b) in &[(255u8, 0u8, 0u8), (0, 255, 0), (0, 0, 255), (255, 255, 255)] {
let (_, cb, cr) = rgb_to_ycbcr(r, g, b, Full);
su += u32::from(cb);
sv += u32::from(cr);
}
assert_eq!(yuv.u()[0], ((su + 2) / 4) as u8);
assert_eq!(yuv.v()[0], ((sv + 2) / 4) as u8);
}
#[test]
fn new_validates_plane_lengths() {
assert!(Yuv420::new(4, 4, vec![0; 16], vec![0; 4], vec![0; 4]).is_ok());
assert!(Yuv420::new(4, 4, vec![0; 16], vec![0; 3], vec![0; 4]).is_err());
assert!(Yuv420::new(4, 4, vec![0; 15], vec![0; 4], vec![0; 4]).is_err());
}
#[test]
fn rejects_bad_rgb_length() {
assert!(Yuv420::from_rgb8(&[0, 1, 2, 3], 1, 1, Limited).is_err());
assert!(Yuv420::from_rgb8(&[], 0, 1, Limited).is_err());
}
}