use otf_pixels_core::{PixelsError, Result};
const SM_WEIGHTS_4: [i32; 4] = [255, 149, 85, 64];
const SM_WEIGHTS_8: [i32; 8] = [255, 197, 146, 105, 73, 50, 37, 32];
const SM_WEIGHTS_16: [i32; 16] = [
255, 225, 196, 170, 145, 123, 102, 84, 68, 54, 43, 33, 26, 20, 17, 16,
];
const SM_WEIGHTS_32: [i32; 32] = [
255, 240, 225, 210, 196, 182, 169, 157, 145, 133, 122, 111, 101, 92, 83, 74, 66, 59, 52, 45,
39, 34, 29, 25, 21, 17, 14, 12, 10, 9, 8, 8,
];
const SM_WEIGHTS_64: [i32; 64] = [
255, 248, 240, 233, 225, 218, 210, 203, 196, 189, 182, 176, 169, 163, 156, 150, 144, 138, 133,
127, 121, 116, 111, 106, 101, 96, 91, 86, 82, 77, 73, 69, 65, 61, 57, 54, 50, 47, 44, 41, 38,
35, 32, 29, 27, 25, 22, 20, 18, 16, 15, 13, 12, 10, 9, 8, 7, 6, 6, 5, 5, 4, 4, 4,
];
fn sm_weights(dim: usize) -> &'static [i32] {
match dim {
8 => &SM_WEIGHTS_8,
16 => &SM_WEIGHTS_16,
32 => &SM_WEIGHTS_32,
64 => &SM_WEIGHTS_64,
_ => &SM_WEIGHTS_4,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntraMode {
Dc,
V,
H,
D45,
D135,
D113,
D157,
D203,
D67,
Smooth,
SmoothV,
SmoothH,
Paeth,
}
impl IntraMode {
#[must_use]
pub fn from_index(index: u8) -> Option<Self> {
Some(match index {
0 => Self::Dc,
1 => Self::V,
2 => Self::H,
3 => Self::D45,
4 => Self::D135,
5 => Self::D113,
6 => Self::D157,
7 => Self::D203,
8 => Self::D67,
9 => Self::Smooth,
10 => Self::SmoothV,
11 => Self::SmoothH,
12 => Self::Paeth,
_ => return None,
})
}
#[must_use]
pub fn is_directional(self) -> bool {
matches!(
self,
Self::V
| Self::H
| Self::D45
| Self::D135
| Self::D113
| Self::D157
| Self::D203
| Self::D67
)
}
}
fn round2(x: i32, n: u32) -> i32 {
if n == 0 { x } else { (x + (1 << (n - 1))) >> n }
}
#[derive(Debug, Clone, Copy)]
pub struct Neighbours {
pub above: [i32; 8],
pub left: [i32; 8],
pub corner: i32,
pub have_above: bool,
pub have_left: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct PredBlock<'a> {
pub above: &'a [i32],
pub left: &'a [i32],
pub corner: i32,
pub have_above: bool,
pub have_left: bool,
pub w: usize,
pub h: usize,
}
pub fn predict_intra_block(mode: IntraMode, b: &PredBlock<'_>, bit_depth: u8) -> Result<Vec<u16>> {
let (w, h) = (b.w, b.h);
let max = (1_i32 << bit_depth) - 1;
let clip1 = |v: i32| v.clamp(0, max) as u16;
let a = |j: usize| b.above.get(j).copied().unwrap_or(0);
let l = |i: usize| b.left.get(i).copied().unwrap_or(0);
let mut pred = vec![0_u16; w * h];
let put = |pred: &mut Vec<u16>, i: usize, j: usize, v: u16| {
if let Some(cell) = pred.get_mut(i * w + j) {
*cell = v;
}
};
match mode {
IntraMode::Dc => {
let value = dc_value(b, bit_depth);
pred.fill(value);
}
IntraMode::V => {
for i in 0..h {
for j in 0..w {
put(&mut pred, i, j, clip1(a(j)));
}
}
}
IntraMode::H => {
for i in 0..h {
for j in 0..w {
put(&mut pred, i, j, clip1(l(i)));
}
}
}
IntraMode::Paeth => {
for i in 0..h {
for j in 0..w {
let base = a(j) + l(i) - b.corner;
let p_left = (base - l(i)).abs();
let p_top = (base - a(j)).abs();
let p_corner = (base - b.corner).abs();
let v = if p_left <= p_top && p_left <= p_corner {
l(i)
} else if p_top <= p_corner {
a(j)
} else {
b.corner
};
put(&mut pred, i, j, clip1(v));
}
}
}
IntraMode::Smooth => {
let wx = sm_weights(w);
let wy = sm_weights(h);
let below_left = l(h - 1);
let above_right = a(w - 1);
for i in 0..h {
let wyi = wy.get(i).copied().unwrap_or(0);
for j in 0..w {
let wxj = wx.get(j).copied().unwrap_or(0);
let smooth = wyi * a(j)
+ (256 - wyi) * below_left
+ wxj * l(i)
+ (256 - wxj) * above_right;
put(&mut pred, i, j, clip1(round2(smooth, 9)));
}
}
}
IntraMode::SmoothV => {
let wy = sm_weights(h);
let below_left = l(h - 1);
for i in 0..h {
let wyi = wy.get(i).copied().unwrap_or(0);
for j in 0..w {
let smooth = wyi * a(j) + (256 - wyi) * below_left;
put(&mut pred, i, j, clip1(round2(smooth, 8)));
}
}
}
IntraMode::SmoothH => {
let wx = sm_weights(w);
let above_right = a(w - 1);
for i in 0..h {
for j in 0..w {
let wxj = wx.get(j).copied().unwrap_or(0);
let smooth = wxj * l(i) + (256 - wxj) * above_right;
put(&mut pred, i, j, clip1(round2(smooth, 8)));
}
}
}
IntraMode::D45
| IntraMode::D135
| IntraMode::D113
| IntraMode::D157
| IntraMode::D203
| IntraMode::D67 => {
return Err(PixelsError::unsupported(
"avif: slanted directional intra prediction is not implemented yet",
));
}
}
Ok(pred)
}
pub fn predict_intra_4x4(mode: IntraMode, n: &Neighbours, bit_depth: u8) -> Result<[[u16; 4]; 4]> {
let block = PredBlock {
above: &n.above,
left: &n.left,
corner: n.corner,
have_above: n.have_above,
have_left: n.have_left,
w: 4,
h: 4,
};
let flat = predict_intra_block(mode, &block, bit_depth)?;
let mut pred = [[0_u16; 4]; 4];
for (i, row) in pred.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
*cell = flat.get(i * 4 + j).copied().unwrap_or(0);
}
}
Ok(pred)
}
fn dc_value(b: &PredBlock<'_>, bit_depth: u8) -> u16 {
let max = (1_i32 << bit_depth) - 1;
let clip1 = |v: i32| v.clamp(0, max) as u16;
let (w, h) = (b.w, b.h);
let left_sum: i32 = b.left.iter().take(h).sum();
let above_sum: i32 = b.above.iter().take(w).sum();
match (b.have_left, b.have_above) {
(true, true) => {
let sum = left_sum + above_sum + ((w + h) >> 1) as i32;
(sum / (w + h) as i32) as u16
}
(true, false) => clip1((left_sum + (h >> 1) as i32) >> h.trailing_zeros()),
(false, true) => clip1((above_sum + (w >> 1) as i32) >> w.trailing_zeros()),
(false, false) => 1_u16 << (bit_depth - 1),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::panic,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
fn neighbours(above: [i32; 8], left: [i32; 8], corner: i32) -> Neighbours {
Neighbours {
above,
left,
corner,
have_above: true,
have_left: true,
}
}
#[test]
fn mode_indexing_and_directional_classification() {
assert_eq!(IntraMode::from_index(0), Some(IntraMode::Dc));
assert_eq!(IntraMode::from_index(12), Some(IntraMode::Paeth));
assert_eq!(IntraMode::from_index(13), None);
assert!(IntraMode::V.is_directional());
assert!(!IntraMode::Dc.is_directional());
assert!(!IntraMode::Smooth.is_directional());
assert!(IntraMode::D45.is_directional());
}
#[test]
fn dc_with_no_neighbours_is_the_midpoint() {
let n = Neighbours {
above: [0; 8],
left: [0; 8],
corner: 0,
have_above: false,
have_left: false,
};
let pred = predict_intra_4x4(IntraMode::Dc, &n, 8).unwrap();
assert_eq!(pred, [[128; 4]; 4]);
}
#[test]
fn dc_averages_both_edges() {
let n = neighbours([100; 8], [60; 8], 100);
let pred = predict_intra_4x4(IntraMode::Dc, &n, 8).unwrap();
assert_eq!(pred, [[80; 4]; 4]);
}
#[test]
fn v_copies_the_above_row_down_each_column() {
let n = neighbours([10, 20, 30, 40, 0, 0, 0, 0], [99; 8], 5);
let pred = predict_intra_4x4(IntraMode::V, &n, 8).unwrap();
for row in &pred {
assert_eq!(row, &[10, 20, 30, 40]);
}
}
#[test]
fn h_copies_the_left_column_across_each_row() {
let n = neighbours([99; 8], [10, 20, 30, 40, 0, 0, 0, 0], 5);
let pred = predict_intra_4x4(IntraMode::H, &n, 8).unwrap();
for (i, row) in pred.iter().enumerate() {
assert!(row.iter().all(|&v| v == [10, 20, 30, 40][i]));
}
}
#[test]
fn paeth_picks_the_closest_predictor() {
let n = neighbours([50; 8], [50; 8], 50);
let pred = predict_intra_4x4(IntraMode::Paeth, &n, 8).unwrap();
assert_eq!(pred, [[50; 4]; 4]);
}
#[test]
fn smooth_of_a_flat_edge_is_that_value() {
let n = neighbours([128; 8], [128; 8], 128);
for mode in [IntraMode::Smooth, IntraMode::SmoothV, IntraMode::SmoothH] {
let pred = predict_intra_4x4(mode, &n, 8).unwrap();
assert_eq!(pred, [[128; 4]; 4], "mode {mode:?}");
}
}
#[test]
fn slanted_directional_modes_are_unsupported_for_now() {
let n = neighbours([100; 8], [100; 8], 100);
assert!(predict_intra_4x4(IntraMode::D45, &n, 8).is_err());
}
#[test]
fn dc_averages_both_edges_at_a_rectangular_size() {
let above = [100; 8];
let left = [60; 8];
let b = PredBlock {
above: &above,
left: &left,
corner: 100,
have_above: true,
have_left: true,
w: 8,
h: 4,
};
let pred = predict_intra_block(IntraMode::Dc, &b, 8).unwrap();
assert_eq!(pred.len(), 32);
assert!(pred.iter().all(|&v| v == 87));
}
#[test]
fn smooth_of_a_flat_edge_is_that_value_at_8x8() {
let above = [128; 8];
let left = [128; 8];
for mode in [IntraMode::Smooth, IntraMode::SmoothV, IntraMode::SmoothH] {
let b = PredBlock {
above: &above,
left: &left,
corner: 128,
have_above: true,
have_left: true,
w: 8,
h: 8,
};
let pred = predict_intra_block(mode, &b, 8).unwrap();
assert_eq!(pred.len(), 64);
assert!(pred.iter().all(|&v| v == 128), "mode {mode:?}");
}
}
#[test]
fn v_copies_the_above_row_at_8x8() {
let above = [10, 20, 30, 40, 50, 60, 70, 80];
let left = [0; 8];
let b = PredBlock {
above: &above,
left: &left,
corner: 0,
have_above: true,
have_left: true,
w: 8,
h: 8,
};
let pred = predict_intra_block(IntraMode::V, &b, 8).unwrap();
let expected: [u16; 8] = [10, 20, 30, 40, 50, 60, 70, 80];
for row in pred.chunks(8) {
assert_eq!(row, &expected[..]);
}
}
}