use crate::vp8l_decode::{decode_argb, decode_entropy_coded_image, DecodeError, DecodedImage};
use crate::vp8l_stream::{BitReader, TransformType};
#[inline]
fn div_round_up(num: u32, den: u32) -> u32 {
num.div_ceil(den)
}
#[inline]
fn alpha(argb: u32) -> u8 {
(argb >> 24) as u8
}
#[inline]
fn red(argb: u32) -> u8 {
(argb >> 16) as u8
}
#[inline]
fn green(argb: u32) -> u8 {
(argb >> 8) as u8
}
#[inline]
fn blue(argb: u32) -> u8 {
argb as u8
}
#[inline]
fn pack_argb(a: u8, r: u8, g: u8, b: u8) -> u32 {
((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}
#[inline]
fn average2(a: u32, b: u32) -> u32 {
(a & b).wrapping_add((a ^ b) >> 1 & 0x7f7f_7f7f)
}
#[inline]
fn clamp(a: i32) -> i32 {
a.clamp(0, 255)
}
#[inline]
fn clamp_add_subtract_full(a: u32, b: u32, c: u32) -> u32 {
let f = |sh: u32| -> u32 {
let ca = ((a >> sh) & 0xff) as i32;
let cb = ((b >> sh) & 0xff) as i32;
let cc = ((c >> sh) & 0xff) as i32;
clamp(ca + cb - cc) as u32
};
(f(24) << 24) | (f(16) << 16) | (f(8) << 8) | f(0)
}
#[inline]
fn clamp_add_subtract_half(a: u32, b: u32) -> u32 {
let f = |sh: u32| -> u32 {
let ca = ((a >> sh) & 0xff) as i32;
let cb = ((b >> sh) & 0xff) as i32;
clamp(ca + (ca - cb) / 2) as u32
};
(f(24) << 24) | (f(16) << 16) | (f(8) << 8) | f(0)
}
#[inline]
fn select(l: u32, t: u32, tl: u32) -> u32 {
let p_alpha = alpha(l) as i32 + alpha(t) as i32 - alpha(tl) as i32;
let p_red = red(l) as i32 + red(t) as i32 - red(tl) as i32;
let p_green = green(l) as i32 + green(t) as i32 - green(tl) as i32;
let p_blue = blue(l) as i32 + blue(t) as i32 - blue(tl) as i32;
let p_l = (p_alpha - alpha(l) as i32).abs()
+ (p_red - red(l) as i32).abs()
+ (p_green - green(l) as i32).abs()
+ (p_blue - blue(l) as i32).abs();
let p_t = (p_alpha - alpha(t) as i32).abs()
+ (p_red - red(t) as i32).abs()
+ (p_green - green(t) as i32).abs()
+ (p_blue - blue(t) as i32).abs();
if p_l < p_t {
l
} else {
t
}
}
fn predict(mode: u8, l: u32, t: u32, tr: u32, tl: u32) -> u32 {
match mode {
0 => 0xff00_0000,
1 => l,
2 => t,
3 => tr,
4 => tl,
5 => average2(average2(l, tr), t),
6 => average2(l, tl),
7 => average2(l, t),
8 => average2(tl, t),
9 => average2(t, tr),
10 => average2(average2(l, tl), average2(t, tr)),
11 => select(l, t, tl),
12 => clamp_add_subtract_full(l, t, tl),
13 => clamp_add_subtract_half(average2(l, t), tl),
_ => 0xff00_0000,
}
}
#[inline]
fn add_pred(residual: u32, pred: u32) -> u32 {
let lo = (residual & 0x00ff_00ff).wrapping_add(pred & 0x00ff_00ff) & 0x00ff_00ff;
let hi = (residual & 0xff00_ff00).wrapping_add(pred & 0xff00_ff00) & 0xff00_ff00;
lo | hi
}
pub fn inverse_predictor(
pixels: &mut [u32],
width: u32,
height: u32,
predictor_image: &[u32],
transform_width: u32,
size_bits: u8,
) {
if width == 0 || height == 0 {
return;
}
let w = width as usize;
let h = height as usize;
pixels[0] = add_pred(pixels[0], 0xff00_0000);
for x in 1..w {
let pred = pixels[x - 1];
pixels[x] = add_pred(pixels[x], pred);
}
if h == 1 {
return;
}
for y in 1..h {
let idx = y * w;
let pred = pixels[idx - w];
pixels[idx] = add_pred(pixels[idx], pred);
}
if w == 1 {
return;
}
let tw = transform_width as usize;
for y in 1..h {
let row = y * w;
let block_row = (y >> size_bits) * tw;
for x in 1..w - 1 {
let idx = row + x;
let block_index = block_row + (x >> size_bits);
let mode = green(predictor_image[block_index]);
let l = pixels[idx - 1];
let t = pixels[idx - w];
let tl = pixels[idx - w - 1];
let tr = pixels[idx - w + 1];
let pred = predict(mode, l, t, tr, tl);
pixels[idx] = add_pred(pixels[idx], pred);
}
let x = w - 1;
let idx = row + x;
let block_index = block_row + (x >> size_bits);
let mode = green(predictor_image[block_index]);
let l = pixels[idx - 1];
let t = pixels[idx - w];
let tl = pixels[idx - w - 1];
let tr = pixels[row - w];
let pred = predict(mode, l, t, tr, tl);
pixels[idx] = add_pred(pixels[idx], pred);
}
}
#[inline]
fn color_transform_delta(t: u8, c: u8) -> i32 {
let ts = t as i8 as i32;
let cs = c as i8 as i32;
(ts * cs) >> 5
}
#[inline]
fn inverse_color_pixel(
r: u8,
g: u8,
b: u8,
green_to_red: u8,
green_to_blue: u8,
red_to_blue: u8,
) -> (u8, u8) {
let mut tmp_red = r as i32;
let mut tmp_blue = b as i32;
tmp_red += color_transform_delta(green_to_red, g);
tmp_blue += color_transform_delta(green_to_blue, g);
tmp_blue += color_transform_delta(red_to_blue, (tmp_red & 0xff) as u8);
((tmp_red & 0xff) as u8, (tmp_blue & 0xff) as u8)
}
pub fn inverse_color(
pixels: &mut [u32],
width: u32,
height: u32,
color_image: &[u32],
transform_width: u32,
size_bits: u8,
) {
if width == 0 || height == 0 {
return;
}
let w = width as usize;
let h = height as usize;
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
let block_index = (y as u32 >> size_bits) * transform_width + (x as u32 >> size_bits);
let cte = color_image[block_index as usize];
let red_to_blue = red(cte);
let green_to_blue = green(cte);
let green_to_red = blue(cte);
let px = pixels[idx];
let (new_red, new_blue) = inverse_color_pixel(
red(px),
green(px),
blue(px),
green_to_red,
green_to_blue,
red_to_blue,
);
pixels[idx] = pack_argb(alpha(px), new_red, green(px), new_blue);
}
}
}
pub fn inverse_subtract_green(pixels: &mut [u32]) {
for px in pixels.iter_mut() {
let p = *px;
let g = (p >> 8) & 0xff;
let mask = (g << 16) | g; let lo = (p & 0x00ff_00ff).wrapping_add(mask) & 0x00ff_00ff;
let hi = p & 0xff00_ff00; *px = lo | hi;
}
}
pub fn inverse_color_table(color_table: &mut [u32]) {
for i in 1..color_table.len() {
let prev = color_table[i - 1];
let cur = color_table[i];
let a = alpha(cur).wrapping_add(alpha(prev));
let r = red(cur).wrapping_add(red(prev));
let g = green(cur).wrapping_add(green(prev));
let b = blue(cur).wrapping_add(blue(prev));
color_table[i] = pack_argb(a, r, g, b);
}
}
fn color_indexing_width_bits(color_table_size: usize) -> u8 {
if color_table_size <= 2 {
3
} else if color_table_size <= 4 {
2
} else if color_table_size <= 16 {
1
} else {
0
}
}
pub fn inverse_color_indexing(
packed: &[u32],
orig_width: u32,
height: u32,
color_table: &[u32],
) -> Vec<u32> {
let width_bits = color_indexing_width_bits(color_table.len());
let ow = orig_width as usize;
let h = height as usize;
let mut out = vec![0u32; ow * h];
if width_bits == 0 {
for (o, &p) in out.iter_mut().zip(packed.iter()) {
let index = green(p) as usize;
*o = color_table.get(index).copied().unwrap_or(0);
}
return out;
}
let count = 1usize << width_bits;
let bits = 8 / count; let mask = (1u32 << bits) - 1;
let packed_w = div_round_up(orig_width, count as u32) as usize;
for y in 0..h {
for x in 0..ow {
let px = packed[y * packed_w + (x / count)];
let shift = (x % count) * bits;
let index = ((green(px) as u32 >> shift) & mask) as usize;
out[y * ow + x] = color_table.get(index).copied().unwrap_or(0);
}
}
out
}
#[derive(Debug, Clone)]
enum ParsedTransform {
Predictor {
size_bits: u8,
transform_width: u32,
image: Vec<u32>,
},
Color {
size_bits: u8,
transform_width: u32,
image: Vec<u32>,
},
SubtractGreen,
ColorIndexing {
color_table: Vec<u32>,
},
}
pub fn decode_lossless(
payload: &[u8],
width: u32,
height: u32,
) -> Result<DecodedImage, DecodeError> {
let mut reader = BitReader::new_after_image_header(payload);
decode_lossless_with_reader(&mut reader, width, height)
}
pub fn decode_lossless_headerless(
payload: &[u8],
width: u32,
height: u32,
) -> Result<DecodedImage, DecodeError> {
let mut reader = BitReader::new(payload);
decode_lossless_with_reader(&mut reader, width, height)
}
fn decode_lossless_with_reader(
reader: &mut BitReader<'_>,
width: u32,
height: u32,
) -> Result<DecodedImage, DecodeError> {
let mut parsed: Vec<ParsedTransform> = Vec::new();
let mut seen = [false; 4];
let mut current_width = width;
while reader.read_bit()? {
let ttype = TransformType::from_bits(reader.read_bits(2)?);
let idx = ttype as usize;
if seen[idx] {
return Err(DecodeError::DuplicateTransform);
}
seen[idx] = true;
match ttype {
TransformType::Predictor => {
let size_bits = (reader.read_bits(3)? + 2) as u8;
let block = 1u32 << size_bits;
let tw = div_round_up(current_width, block);
let th = div_round_up(height, block);
let image = decode_entropy_coded_image(reader, tw, th)?;
parsed.push(ParsedTransform::Predictor {
size_bits,
transform_width: tw,
image: image.pixels().to_vec(),
});
}
TransformType::Color => {
let size_bits = (reader.read_bits(3)? + 2) as u8;
let block = 1u32 << size_bits;
let tw = div_round_up(current_width, block);
let th = div_round_up(height, block);
let image = decode_entropy_coded_image(reader, tw, th)?;
parsed.push(ParsedTransform::Color {
size_bits,
transform_width: tw,
image: image.pixels().to_vec(),
});
}
TransformType::SubtractGreen => {
parsed.push(ParsedTransform::SubtractGreen);
}
TransformType::ColorIndexing => {
let color_table_size = reader.read_bits(8)? + 1;
let table_img = decode_entropy_coded_image(reader, color_table_size, 1)?;
let mut color_table = table_img.pixels().to_vec();
inverse_color_table(&mut color_table);
let width_bits = color_indexing_width_bits(color_table.len());
current_width = div_round_up(current_width, 1u32 << width_bits);
parsed.push(ParsedTransform::ColorIndexing { color_table });
}
}
}
let mut image = decode_argb(reader, current_width, height)?;
let mut cur_w = current_width;
for transform in parsed.iter().rev() {
match transform {
ParsedTransform::Predictor {
size_bits,
transform_width,
image: predictor_image,
} => {
inverse_predictor(
image.pixels_mut(),
cur_w,
height,
predictor_image,
*transform_width,
*size_bits,
);
}
ParsedTransform::Color {
size_bits,
transform_width,
image: color_image,
} => {
inverse_color(
image.pixels_mut(),
cur_w,
height,
color_image,
*transform_width,
*size_bits,
);
}
ParsedTransform::SubtractGreen => {
inverse_subtract_green(image.pixels_mut());
}
ParsedTransform::ColorIndexing { color_table } => {
let out = inverse_color_indexing(image.pixels(), width, height, color_table);
cur_w = width;
image = DecodedImage::from_parts(width, height, out);
}
}
}
Ok(image)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn average2_per_channel_floor() {
let a = pack_argb(10, 200, 0, 254);
let b = pack_argb(20, 201, 1, 255);
let r = average2(a, b);
assert_eq!(alpha(r), 15);
assert_eq!(red(r), 200);
assert_eq!(green(r), 0);
assert_eq!(blue(r), 254);
}
#[test]
fn clamp_add_subtract_full_clamps() {
let a = pack_argb(200, 10, 0, 0);
let b = pack_argb(200, 10, 0, 0);
let c = pack_argb(10, 200, 0, 0);
let r = clamp_add_subtract_full(a, b, c);
assert_eq!(alpha(r), 255); assert_eq!(red(r), 0); }
#[test]
fn clamp_add_subtract_half_matches_spec_formula() {
let a = pack_argb(0, 100, 0, 0);
let b = pack_argb(0, 40, 0, 0);
let r = clamp_add_subtract_half(a, b);
assert_eq!(red(r), 130);
let a2 = pack_argb(0, 0, 10, 0);
let b2 = pack_argb(0, 0, 200, 0);
let r2 = clamp_add_subtract_half(a2, b2);
assert_eq!(green(r2), 0);
}
#[test]
fn select_picks_closer_neighbour() {
let l = pack_argb(255, 100, 50, 25);
let t = pack_argb(255, 10, 10, 10);
let tl = pack_argb(255, 10, 10, 10);
assert_eq!(select(l, t, tl), l);
let l2 = pack_argb(255, 10, 10, 10);
let t2 = pack_argb(255, 100, 50, 25);
let tl2 = pack_argb(255, 10, 10, 10);
assert_eq!(select(l2, t2, tl2), t2);
}
#[test]
fn predict_modes_pick_expected_neighbour() {
let l = pack_argb(1, 2, 3, 4);
let t = pack_argb(5, 6, 7, 8);
let tr = pack_argb(9, 10, 11, 12);
let tl = pack_argb(13, 14, 15, 16);
assert_eq!(predict(0, l, t, tr, tl), 0xff00_0000);
assert_eq!(predict(1, l, t, tr, tl), l);
assert_eq!(predict(2, l, t, tr, tl), t);
assert_eq!(predict(3, l, t, tr, tl), tr);
assert_eq!(predict(4, l, t, tr, tl), tl);
}
#[test]
fn inverse_predictor_top_left_is_black_residual() {
let mut px = vec![0u32];
let pred_img = vec![0u32]; inverse_predictor(&mut px, 1, 1, &pred_img, 1, 2);
assert_eq!(px[0], 0xff00_0000);
}
#[test]
fn inverse_predictor_top_row_uses_left() {
let p0 = pack_argb(0, 0, 10, 0);
let d = pack_argb(0, 0, 5, 0);
let mut px = vec![p0, d, d];
let pred_img = vec![pack_argb(0, 0, 1, 0)];
inverse_predictor(&mut px, 3, 1, &pred_img, 1, 9);
assert_eq!(green(px[0]), 10);
assert_eq!(green(px[1]), 15);
assert_eq!(green(px[2]), 20);
assert_eq!(alpha(px[0]), 255);
assert_eq!(alpha(px[1]), 255);
assert_eq!(alpha(px[2]), 255);
}
#[test]
fn inverse_predictor_left_column_uses_top() {
let p0 = pack_argb(255, 0, 10, 0);
let d = pack_argb(0, 0, 5, 0);
let mut px = vec![p0, d, d];
let pred_img = vec![pack_argb(0, 0, 2, 0)];
inverse_predictor(&mut px, 1, 3, &pred_img, 1, 9);
assert_eq!(green(px[0]), 10);
assert_eq!(green(px[1]), 15);
assert_eq!(green(px[2]), 20);
}
#[test]
fn inverse_predictor_right_column_uses_row_leftmost_as_tr() {
let a_after = pack_argb(255, 100, 50, 25); let res_d = pack_argb(0, 1, 2, 3); let pred_img = vec![pack_argb(0, 0, 3, 0)];
let res_a = pack_argb(0, 100, 50, 25);
let mut px = vec![res_a, 0u32, 0u32, res_d];
inverse_predictor(&mut px, 2, 2, &pred_img, 1, 9);
assert_eq!(px[0], a_after);
assert_eq!(px[1], a_after);
assert_eq!(px[2], a_after);
let expected_d = add_pred(res_d, a_after);
assert_eq!(px[3], expected_d);
}
#[test]
fn inverse_predictor_matches_unsplit_reference_random() {
fn reference(
pixels: &mut [u32],
width: u32,
height: u32,
predictor_image: &[u32],
transform_width: u32,
size_bits: u8,
) {
if width == 0 || height == 0 {
return;
}
let w = width as usize;
let h = height as usize;
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
let pred = if x == 0 && y == 0 {
0xff00_0000
} else if y == 0 {
pixels[idx - 1]
} else if x == 0 {
pixels[idx - w]
} else {
let bi =
(y as u32 >> size_bits) * transform_width + (x as u32 >> size_bits);
let mode = green(predictor_image[bi as usize]);
let l = pixels[idx - 1];
let t = pixels[idx - w];
let tl = pixels[idx - w - 1];
let tr = if x == w - 1 {
pixels[idx - w - (w - 1)]
} else {
pixels[idx - w + 1]
};
predict(mode, l, t, tr, tl)
};
pixels[idx] = add_pred(pixels[idx], pred);
}
}
}
let mut seed: u32 = 0x1234_5678;
let mut rng = || {
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
seed
};
for &(w, h, size_bits) in &[
(1u32, 1u32, 3u8),
(1, 7, 2),
(7, 1, 2),
(2, 2, 1),
(5, 5, 1),
(8, 6, 0), (13, 9, 2),
] {
let n = (w as usize) * (h as usize);
let pixels: Vec<u32> = (0..n).map(|_| rng()).collect();
let tw = div_round_up(w, 1u32 << size_bits);
let th = div_round_up(h, 1u32 << size_bits);
let pred_n = (tw as usize) * (th as usize);
let pred_img: Vec<u32> = (0..pred_n)
.map(|_| pack_argb(0, 0, (rng() % 14) as u8, 0))
.collect();
let mut new_path = pixels.clone();
inverse_predictor(&mut new_path, w, h, &pred_img, tw, size_bits);
let mut ref_path = pixels.clone();
reference(&mut ref_path, w, h, &pred_img, tw, size_bits);
assert_eq!(
new_path, ref_path,
"split-loop diverges from per-pixel reference at \
(w={w}, h={h}, size_bits={size_bits})"
);
}
}
#[test]
fn color_transform_delta_signed() {
assert_eq!(color_transform_delta(0xFF, 0x40), -2);
assert_eq!(color_transform_delta(2, 0x40), 4);
assert_eq!(color_transform_delta(0, 0x7F), 0);
}
#[test]
fn inverse_color_is_inverse_of_forward() {
let green_to_red = 0x12u8;
let green_to_blue = 0xF0u8; let red_to_blue = 0x05u8;
let (r0, g0, b0) = (120u8, 80u8, 200u8);
let mut tr = r0 as i32;
let mut tb = b0 as i32;
tr -= color_transform_delta(green_to_red, g0);
tb -= color_transform_delta(green_to_blue, g0);
tb -= color_transform_delta(red_to_blue, r0);
let enc_r = (tr & 0xff) as u8;
let enc_b = (tb & 0xff) as u8;
let (dec_r, dec_b) =
inverse_color_pixel(enc_r, g0, enc_b, green_to_red, green_to_blue, red_to_blue);
assert_eq!(dec_r, r0);
assert_eq!(dec_b, b0);
}
#[test]
fn inverse_color_in_place_uses_block_element() {
let mut px = vec![pack_argb(255, 100, 50, 200)];
let color_img = vec![pack_argb(255, 0, 0, 0)];
inverse_color(&mut px, 1, 1, &color_img, 1, 9);
assert_eq!(px[0], pack_argb(255, 100, 50, 200));
}
#[test]
fn inverse_subtract_green_adds_green() {
let mut px = vec![pack_argb(255, 5, 10, 250)];
inverse_subtract_green(&mut px);
assert_eq!(red(px[0]), 15);
assert_eq!(green(px[0]), 10);
assert_eq!(blue(px[0]), 4); assert_eq!(alpha(px[0]), 255);
}
#[test]
fn color_table_subtraction_decode() {
let mut t = vec![
pack_argb(255, 10, 20, 30),
pack_argb(0, 5, 5, 5),
pack_argb(0, 1, 2, 3),
];
inverse_color_table(&mut t);
assert_eq!(t[0], pack_argb(255, 10, 20, 30));
assert_eq!(t[1], pack_argb(255, 15, 25, 35));
assert_eq!(t[2], pack_argb(255, 16, 27, 38));
}
#[test]
fn color_indexing_no_bundling_lookup() {
let mut table = vec![0u32; 17];
for (i, c) in table.iter_mut().enumerate() {
*c = pack_argb(255, i as u8, 0, 0);
}
let packed = vec![
pack_argb(0, 0, 0, 0), pack_argb(0, 0, 3, 0), pack_argb(0, 0, 16, 0), ];
let out = inverse_color_indexing(&packed, 3, 1, &table);
assert_eq!(out, vec![table[0], table[3], table[16]]);
}
#[test]
fn color_indexing_out_of_range_is_transparent_black() {
let table = vec![pack_argb(255, 1, 2, 3), pack_argb(255, 4, 5, 6)];
let packed = vec![pack_argb(0, 0, 0x05, 0)]; let out = inverse_color_indexing(&packed, 1, 1, &table);
assert_eq!(out, vec![table[1]]);
}
#[test]
fn color_indexing_bundling_width_bits_1() {
let mut table = vec![0u32; 8];
for (i, c) in table.iter_mut().enumerate() {
*c = pack_argb(255, i as u8, 0, 0);
}
let packed = vec![pack_argb(0, 0, 0x21, 0), pack_argb(0, 0, 0x43, 0)];
let out = inverse_color_indexing(&packed, 4, 1, &table);
assert_eq!(out, vec![table[1], table[2], table[3], table[4]]);
}
#[test]
fn color_indexing_bundling_width_bits_3() {
let table = vec![pack_argb(255, 0, 0, 0), pack_argb(255, 255, 255, 255)];
let packed = vec![pack_argb(0, 0, 0x4D, 0)];
let out = inverse_color_indexing(&packed, 8, 1, &table);
let expect: Vec<u32> = [1, 0, 1, 1, 0, 0, 1, 0].iter().map(|&i| table[i]).collect();
assert_eq!(out, expect);
}
#[test]
fn width_bits_thresholds() {
assert_eq!(color_indexing_width_bits(1), 3);
assert_eq!(color_indexing_width_bits(2), 3);
assert_eq!(color_indexing_width_bits(3), 2);
assert_eq!(color_indexing_width_bits(4), 2);
assert_eq!(color_indexing_width_bits(5), 1);
assert_eq!(color_indexing_width_bits(16), 1);
assert_eq!(color_indexing_width_bits(17), 0);
assert_eq!(color_indexing_width_bits(256), 0);
}
}