use gamut_color::clip_pixel8;
use crate::vp8l::div_round_up;
pub const PREDICTOR_TRANSFORM: u8 = 0;
pub const COLOR_TRANSFORM: u8 = 1;
pub const SUBTRACT_GREEN_TRANSFORM: u8 = 2;
pub const COLOR_INDEXING_TRANSFORM: u8 = 3;
pub const NUM_PREDICTOR_MODES: u8 = 14;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Vp8lTransform {
Predictor {
size_bits: u8,
blocks: Vec<u32>,
},
Color {
size_bits: u8,
blocks: Vec<u32>,
},
SubtractGreen,
ColorIndexing {
table: Vec<u32>,
expanded_width: u32,
},
}
impl Vp8lTransform {
pub fn apply_inverse(&self, pixels: &mut Vec<u32>, width: u32, height: u32) -> u32 {
match self {
Self::SubtractGreen => {
inverse_subtract_green(pixels);
width
}
Self::Predictor { size_bits, blocks } => {
inverse_predictor(pixels, width, height, *size_bits, blocks);
width
}
Self::Color { size_bits, blocks } => {
inverse_color(pixels, width, height, *size_bits, blocks);
width
}
Self::ColorIndexing {
table,
expanded_width,
} => {
*pixels = inverse_color_indexing(pixels, width, height, *expanded_width, table);
*expanded_width
}
}
}
}
#[inline]
#[must_use]
pub const fn alpha(p: u32) -> u8 {
(p >> 24) as u8
}
#[inline]
#[must_use]
pub const fn red(p: u32) -> u8 {
(p >> 16) as u8
}
#[inline]
#[must_use]
pub const fn green(p: u32) -> u8 {
(p >> 8) as u8
}
#[inline]
#[must_use]
pub const fn blue(p: u32) -> u8 {
p as u8
}
#[inline]
#[must_use]
pub const fn make_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]
#[must_use]
pub const fn add_pixels(a: u32, b: u32) -> u32 {
make_argb(
alpha(a).wrapping_add(alpha(b)),
red(a).wrapping_add(red(b)),
green(a).wrapping_add(green(b)),
blue(a).wrapping_add(blue(b)),
)
}
#[inline]
#[must_use]
pub const fn subtract_pixels(a: u32, b: u32) -> u32 {
make_argb(
alpha(a).wrapping_sub(alpha(b)),
red(a).wrapping_sub(red(b)),
green(a).wrapping_sub(green(b)),
blue(a).wrapping_sub(blue(b)),
)
}
pub fn inverse_subtract_green(pixels: &mut [u32]) {
for p in pixels.iter_mut() {
let g = green(*p);
let r = red(*p).wrapping_add(g);
let b = blue(*p).wrapping_add(g);
*p = make_argb(alpha(*p), r, g, b);
}
}
pub fn forward_subtract_green(pixels: &mut [u32]) {
for p in pixels.iter_mut() {
let g = green(*p);
let r = red(*p).wrapping_sub(g);
let b = blue(*p).wrapping_sub(g);
*p = make_argb(alpha(*p), r, g, b);
}
}
#[inline]
fn average2(a: u32, b: u32) -> u32 {
make_argb(
((u16::from(alpha(a)) + u16::from(alpha(b))) / 2) as u8,
((u16::from(red(a)) + u16::from(red(b))) / 2) as u8,
((u16::from(green(a)) + u16::from(green(b))) / 2) as u8,
((u16::from(blue(a)) + u16::from(blue(b))) / 2) as u8,
)
}
fn select(l: u32, t: u32, tl: u32) -> u32 {
let pred = |cl: u8, ct: u8, ctl: u8| i32::from(cl) + i32::from(ct) - i32::from(ctl);
let pa = pred(alpha(l), alpha(t), alpha(tl));
let pr = pred(red(l), red(t), red(tl));
let pg = pred(green(l), green(t), green(tl));
let pb = pred(blue(l), blue(t), blue(tl));
let dist = |p: u32, a: i32, r: i32, g: i32, b: i32| {
(a - i32::from(alpha(p))).abs()
+ (r - i32::from(red(p))).abs()
+ (g - i32::from(green(p))).abs()
+ (b - i32::from(blue(p))).abs()
};
if dist(l, pa, pr, pg, pb) < dist(t, pa, pr, pg, pb) {
l
} else {
t
}
}
#[inline]
fn clamp_add_subtract_full(a: u32, b: u32, c: u32) -> u32 {
let f = |ca: u8, cb: u8, cc: u8| clip_pixel8(i32::from(ca) + i32::from(cb) - i32::from(cc));
make_argb(
f(alpha(a), alpha(b), alpha(c)),
f(red(a), red(b), red(c)),
f(green(a), green(b), green(c)),
f(blue(a), blue(b), blue(c)),
)
}
#[inline]
fn clamp_add_subtract_half(a: u32, b: u32) -> u32 {
let f = |ca: u8, cb: u8| {
let ca = i32::from(ca);
clip_pixel8(ca + (ca - i32::from(cb)) / 2)
};
make_argb(
f(alpha(a), alpha(b)),
f(red(a), red(b)),
f(green(a), green(b)),
f(blue(a), blue(b)),
)
}
#[must_use]
pub fn predict(mode: u8, l: u32, t: u32, tl: u32, tr: 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_residual(res: u32, pred: u32) -> u32 {
make_argb(
alpha(res).wrapping_add(alpha(pred)),
red(res).wrapping_add(red(pred)),
green(res).wrapping_add(green(pred)),
blue(res).wrapping_add(blue(pred)),
)
}
#[inline]
fn sub_residual(value: u32, pred: u32) -> u32 {
make_argb(
alpha(value).wrapping_sub(alpha(pred)),
red(value).wrapping_sub(red(pred)),
green(value).wrapping_sub(green(pred)),
blue(value).wrapping_sub(blue(pred)),
)
}
#[inline]
fn predicted_value(img: &[u32], width: usize, x: usize, y: usize, mode: u8) -> u32 {
let idx = y * width + x;
if x == 0 && y == 0 {
0xff00_0000
} else if y == 0 {
img[idx - 1] } else if x == 0 {
img[idx - width] } else {
let l = img[idx - 1];
let t = img[idx - width];
let tl = img[idx - width - 1];
let tr = if x + 1 < width {
img[idx - width + 1]
} else {
img[y * width]
};
predict(mode, l, t, tl, tr)
}
}
#[inline]
fn block_mode(blocks: &[u32], transform_width: usize, size_bits: u8, x: usize, y: usize) -> u8 {
let block = (y >> size_bits) * transform_width + (x >> size_bits);
blocks.get(block).map_or(0, |&b| green(b))
}
pub fn inverse_predictor(
pixels: &mut [u32],
width: u32,
height: u32,
size_bits: u8,
blocks: &[u32],
) {
if width == 0 || height == 0 {
return;
}
let w = width as usize;
let transform_width = div_round_up(width, 1 << size_bits) as usize;
for y in 0..height as usize {
for x in 0..w {
let mode = block_mode(blocks, transform_width, size_bits, x, y);
let pred = predicted_value(pixels, w, x, y, mode);
let idx = y * w + x;
pixels[idx] = add_residual(pixels[idx], pred);
}
}
}
#[must_use]
pub fn forward_predictor(
pixels: &[u32],
width: u32,
height: u32,
size_bits: u8,
) -> (Vec<u32>, Vec<u32>) {
let w = width as usize;
let h = height as usize;
let transform_width = div_round_up(width, 1 << size_bits) as usize;
let transform_height = div_round_up(height, 1 << size_bits) as usize;
let mut sub_image = vec![0xff00_0000u32; transform_width * transform_height];
for by in 0..transform_height {
for bx in 0..transform_width {
let mode = best_predictor_mode(pixels, w, h, size_bits, bx, by);
sub_image[by * transform_width + bx] = make_argb(0xff, 0, mode, 0);
}
}
let mut residuals = vec![0u32; pixels.len()];
for y in 0..h {
for x in 0..w {
let mode = block_mode(&sub_image, transform_width, size_bits, x, y);
let pred = predicted_value(pixels, w, x, y, mode);
let idx = y * w + x;
residuals[idx] = sub_residual(pixels[idx], pred);
}
}
(residuals, sub_image)
}
#[inline]
fn residual_cost(res: u32) -> u64 {
let c = |v: u8| u64::from(v.min(v.wrapping_neg()));
c(alpha(res)) + c(red(res)) + c(green(res)) + c(blue(res))
}
fn best_predictor_mode(
pixels: &[u32],
width: usize,
height: usize,
size_bits: u8,
bx: usize,
by: usize,
) -> u8 {
let block_size = 1usize << size_bits;
let x0 = bx * block_size;
let y0 = by * block_size;
let mut best_mode = 0u8;
let mut best_cost = u64::MAX;
for mode in 0..NUM_PREDICTOR_MODES {
let mut cost = 0u64;
for y in y0..(y0 + block_size).min(height) {
for x in x0..(x0 + block_size).min(width) {
let pred = predicted_value(pixels, width, x, y, mode);
cost += residual_cost(sub_residual(pixels[y * width + x], pred));
}
}
if cost < best_cost {
best_cost = cost;
best_mode = mode;
}
}
best_mode
}
#[inline]
#[must_use]
pub fn color_transform_delta(t: i8, c: i8) -> i32 {
(i32::from(t) * i32::from(c)) >> 5
}
pub fn inverse_color(pixels: &mut [u32], width: u32, height: u32, size_bits: u8, blocks: &[u32]) {
if width == 0 || height == 0 {
return;
}
let w = width as usize;
let transform_width = div_round_up(width, 1 << size_bits) as usize;
for y in 0..height as usize {
for x in 0..w {
let idx = y * w + x;
let p = pixels[idx];
let block = (y >> size_bits) * transform_width + (x >> size_bits);
let cte = blocks.get(block).copied().unwrap_or(0xff00_0000);
let green_to_red = blue(cte) as i8;
let green_to_blue = green(cte) as i8;
let red_to_blue = red(cte) as i8;
let g = green(p);
let mut tmp_red = i32::from(red(p));
let mut tmp_blue = i32::from(blue(p));
tmp_red += color_transform_delta(green_to_red, g as i8);
tmp_blue += color_transform_delta(green_to_blue, g as i8);
tmp_blue += color_transform_delta(red_to_blue, (tmp_red & 0xff) as i8);
pixels[idx] = make_argb(alpha(p), (tmp_red & 0xff) as u8, g, (tmp_blue & 0xff) as u8);
}
}
}
#[inline]
#[must_use]
fn pack_color_element(green_to_red: i8, green_to_blue: i8, red_to_blue: i8) -> u32 {
make_argb(
0xff,
red_to_blue as u8,
green_to_blue as u8,
green_to_red as u8,
)
}
#[must_use]
pub fn forward_color(pixels: &mut [u32], width: u32, height: u32, size_bits: u8) -> Vec<u32> {
let w = width as usize;
let h = height as usize;
let transform_width = div_round_up(width, 1 << size_bits) as usize;
let transform_height = div_round_up(height, 1 << size_bits) as usize;
let block_size = 1usize << size_bits;
let mut sub_image = vec![pack_color_element(0, 0, 0); transform_width * transform_height];
for by in 0..transform_height {
for bx in 0..transform_width {
let (g2r, g2b, r2b) = estimate_color_element(pixels, w, h, block_size, bx, by);
sub_image[by * transform_width + bx] = pack_color_element(g2r, g2b, r2b);
for y in (by * block_size)..((by + 1) * block_size).min(h) {
for x in (bx * block_size)..((bx + 1) * block_size).min(w) {
let p = pixels[y * w + x];
let g = green(p) as i8;
let orig_red = red(p);
let new_red = (i32::from(orig_red) - color_transform_delta(g2r, g)) & 0xff;
let new_blue = (i32::from(blue(p))
- color_transform_delta(g2b, g)
- color_transform_delta(r2b, orig_red as i8))
& 0xff;
pixels[y * w + x] =
make_argb(alpha(p), new_red as u8, green(p), new_blue as u8);
}
}
}
}
sub_image
}
fn estimate_color_element(
pixels: &[u32],
width: usize,
height: usize,
block_size: usize,
bx: usize,
by: usize,
) -> (i8, i8, i8) {
let mut rg = 0i64; let mut gg = 0i64; let mut bg = 0i64; let mut br = 0i64; let mut rr = 0i64; for y in (by * block_size)..((by + 1) * block_size).min(height) {
for x in (bx * block_size)..((bx + 1) * block_size).min(width) {
let p = pixels[y * width + x];
let r = i64::from(red(p) as i8);
let g = i64::from(green(p) as i8);
let b = i64::from(blue(p) as i8);
rg += r * g;
gg += g * g;
bg += b * g;
br += b * r;
rr += r * r;
}
}
let coeff = |num: i64, den: i64| -> i8 {
if den == 0 {
0
} else {
((num * 32) / den).clamp(-128, 127) as i8
}
};
(coeff(rg, gg), coeff(bg, gg), coeff(br, rr))
}
#[must_use]
pub fn color_index_width_bits(table_size: usize) -> u8 {
if table_size <= 2 {
3
} else if table_size <= 4 {
2
} else if table_size <= 16 {
1
} else {
0
}
}
#[must_use]
pub fn forward_color_indexing(
pixels: &[u32],
width: u32,
height: u32,
palette: &[u32],
) -> (Vec<u32>, u32) {
use std::collections::HashMap;
let width_bits = color_index_width_bits(palette.len());
let pixels_per = 1usize << width_bits;
let bits_per = 8 / pixels_per as u32;
let bundled_width = div_round_up(width, pixels_per as u32);
let index_of: HashMap<u32, u32> = palette
.iter()
.enumerate()
.map(|(i, &c)| (c, i as u32))
.collect();
let w = width as usize;
let bw = bundled_width as usize;
let mut bundled = vec![0xff00_0000u32; bw * height as usize];
for y in 0..height as usize {
for x in 0..w {
let index = index_of.get(&pixels[y * w + x]).copied().unwrap_or(0);
let pos = y * bw + x / pixels_per;
let shift = (x % pixels_per) as u32 * bits_per;
let packed = u32::from(green(bundled[pos])) | (index << shift);
bundled[pos] = make_argb(0xff, 0, packed as u8, 0);
}
}
(bundled, bundled_width)
}
#[must_use]
pub fn inverse_color_indexing(
bundled: &[u32],
bundled_width: u32,
height: u32,
expanded_width: u32,
table: &[u32],
) -> Vec<u32> {
let width_bits = color_index_width_bits(table.len());
let bw = bundled_width as usize;
let ew = expanded_width as usize;
let mut out = vec![0u32; ew * height as usize];
let pixels_per = 1usize << width_bits; let bits_per = 8 / pixels_per; let mask = (1u32 << bits_per) - 1;
for y in 0..height as usize {
for x in 0..ew {
let index = if width_bits == 0 {
u32::from(green(bundled[y * bw + x]))
} else {
let packed = u32::from(green(bundled[y * bw + x / pixels_per]));
(packed >> ((x % pixels_per) * bits_per)) & mask
};
out[y * ew + x] = table.get(index as usize).copied().unwrap_or(0);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subtract_green_inverse_adds_green() {
let mut px = [make_argb(0xff, 40, 10, 190)];
inverse_subtract_green(&mut px);
assert_eq!(px[0], make_argb(0xff, 50, 10, 200));
let mut wrap = [make_argb(0, 250, 10, 0)];
inverse_subtract_green(&mut wrap);
assert_eq!(red(wrap[0]), 250u8.wrapping_add(10));
}
#[test]
fn predict_basic_modes() {
let l = make_argb(1, 2, 3, 4);
let t = make_argb(5, 6, 7, 8);
let tl = make_argb(9, 10, 11, 12);
let tr = make_argb(13, 14, 15, 16);
assert_eq!(predict(0, l, t, tl, tr), 0xff00_0000);
assert_eq!(predict(1, l, t, tl, tr), l);
assert_eq!(predict(2, l, t, tl, tr), t);
assert_eq!(predict(3, l, t, tl, tr), tr);
assert_eq!(predict(4, l, t, tl, tr), tl);
assert_eq!(predict(7, l, t, tl, tr), average2(l, t));
assert_eq!(predict(99, l, t, tl, tr), 0xff00_0000);
}
#[test]
fn average2_rounds_down_per_channel() {
let a = make_argb(10, 20, 30, 41);
let b = make_argb(11, 21, 31, 40);
assert_eq!(average2(a, b), make_argb(10, 20, 30, 40));
}
#[test]
fn select_picks_closer_neighbor() {
let l = make_argb(100, 100, 100, 100);
let t = make_argb(50, 50, 50, 50);
let tl = make_argb(50, 50, 50, 50);
assert_eq!(select(l, t, tl), l);
}
#[test]
fn clamp_predictors_saturate() {
let a = make_argb(200, 200, 200, 200);
let b = make_argb(200, 200, 200, 200);
let c = make_argb(0, 0, 0, 0);
assert_eq!(
clamp_add_subtract_full(a, b, c),
make_argb(255, 255, 255, 255)
);
assert_eq!(clamp_add_subtract_half(a, c), make_argb(255, 255, 255, 255));
}
#[test]
fn color_transform_delta_sign_extends() {
assert_eq!(color_transform_delta(-1, 64), -2);
assert_eq!(color_transform_delta(0, 100), 0);
assert_eq!(color_transform_delta(32, 64), (32 * 64) >> 5);
}
fn forward_predictor_fixed_mode(
orig: &[u32],
width: usize,
height: usize,
mode: u8,
) -> Vec<u32> {
let mut res = vec![0u32; orig.len()];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
let pred = if x == 0 && y == 0 {
0xff00_0000
} else if y == 0 {
orig[idx - 1]
} else if x == 0 {
orig[idx - width]
} else {
let tr = if x + 1 < width {
orig[idx - width + 1]
} else {
orig[y * width]
};
predict(
mode,
orig[idx - 1],
orig[idx - width],
orig[idx - width - 1],
tr,
)
};
res[idx] = make_argb(
alpha(orig[idx]).wrapping_sub(alpha(pred)),
red(orig[idx]).wrapping_sub(red(pred)),
green(orig[idx]).wrapping_sub(green(pred)),
blue(orig[idx]).wrapping_sub(blue(pred)),
);
}
}
res
}
#[test]
fn predictor_round_trips_every_mode_with_borders() {
let (w, h) = (5usize, 4usize);
let orig: Vec<u32> = (0..(w * h))
.map(|i| {
make_argb(
(i * 3) as u8,
(i * 7 + 1) as u8,
(i ^ 0x5a) as u8,
(i * 13) as u8,
)
})
.collect();
for mode in 0..NUM_PREDICTOR_MODES {
let blocks = vec![make_argb(0xff, 0, mode, 0)];
let mut res = forward_predictor_fixed_mode(&orig, w, h, mode);
inverse_predictor(&mut res, w as u32, h as u32, 3, &blocks);
assert_eq!(res, orig, "mode {mode} failed to round-trip");
}
}
#[test]
fn color_transform_round_trips() {
let (w, h) = (3usize, 2usize);
let orig: Vec<u32> = (0..(w * h))
.map(|i| make_argb(0xff, (i * 17) as u8, (i * 5 + 3) as u8, (i * 29) as u8))
.collect();
let elem = make_argb(0xff, 7, 200, 30); let blocks = vec![elem];
let mut transformed = orig.clone();
for p in transformed.iter_mut() {
let g = green(*p);
let green_to_red = blue(elem) as i8;
let green_to_blue = green(elem) as i8;
let red_to_blue = red(elem) as i8;
let orig_red = red(*p);
let new_red =
(i32::from(orig_red) - color_transform_delta(green_to_red, g as i8)) & 0xff;
let new_blue = (i32::from(blue(*p))
- color_transform_delta(green_to_blue, g as i8)
- color_transform_delta(red_to_blue, orig_red as i8))
& 0xff;
*p = make_argb(alpha(*p), new_red as u8, g, new_blue as u8);
}
inverse_color(&mut transformed, w as u32, h as u32, 3, &blocks);
assert_eq!(transformed, orig);
}
#[test]
fn color_indexing_unbundles() {
assert_eq!(color_index_width_bits(32), 0);
let table: Vec<u32> = (0..32)
.map(|i| make_argb(0xff, i as u8, i as u8, 0))
.collect();
let bundled: Vec<u32> = [0u8, 31, 1, 2]
.iter()
.map(|&i| make_argb(0xff, 0, i, 0))
.collect();
let out = inverse_color_indexing(&bundled, 4, 1, 4, &table);
assert_eq!(out, vec![table[0], table[31], table[1], table[2]]);
let oob = vec![make_argb(0xff, 0, 200, 0)];
let out2 = inverse_color_indexing(&oob, 1, 1, 1, &table);
assert_eq!(out2, vec![0]);
}
#[test]
fn color_indexing_bundled_width_bits() {
assert_eq!(color_index_width_bits(2), 3);
let table = vec![0xff00_0000, 0xffff_ffff];
let bundled = vec![make_argb(0xff, 0, 0b1010_1010, 0)];
let out = inverse_color_indexing(&bundled, 1, 1, 8, &table);
let expected: Vec<u32> = (0..8)
.map(|x| if x % 2 == 1 { table[1] } else { table[0] })
.collect();
assert_eq!(out, expected);
assert_eq!(color_index_width_bits(16), 1);
let table16: Vec<u32> = (0..16).map(|i| make_argb(0xff, 0, 0, i as u8)).collect();
let bundled16 = vec![make_argb(0xff, 0, 0x53, 0)];
let out16 = inverse_color_indexing(&bundled16, 1, 1, 2, &table16);
assert_eq!(out16, vec![table16[3], table16[5]]);
}
#[test]
fn forward_subtract_green_inverts() {
let mut px: Vec<u32> = (0..20)
.map(|i| make_argb(i as u8, (i * 3) as u8, (i * 7 + 1) as u8, (i * 11) as u8))
.collect();
let orig = px.clone();
forward_subtract_green(&mut px);
inverse_subtract_green(&mut px);
assert_eq!(px, orig);
}
#[test]
fn forward_predictor_inverts() {
let (w, h) = (6u32, 4u32);
let orig: Vec<u32> = (0..(w * h))
.map(|i| make_argb(0xff, (i * 3) as u8, (i ^ 0x2a) as u8, (i * 5) as u8))
.collect();
let (residual, sub) = forward_predictor(&orig, w, h, 2);
let mut recon = residual;
inverse_predictor(&mut recon, w, h, 2, &sub);
assert_eq!(recon, orig);
}
#[test]
fn forward_color_inverts() {
let (w, h) = (5u32, 3u32);
let orig: Vec<u32> = (0..(w * h))
.map(|i| make_argb(0xff, (i * 9) as u8, (i * 5 + 3) as u8, (i * 29) as u8))
.collect();
let mut px = orig.clone();
let sub = forward_color(&mut px, w, h, 2);
inverse_color(&mut px, w, h, 2, &sub);
assert_eq!(px, orig);
}
#[test]
fn forward_color_indexing_inverts() {
let palette = vec![
make_argb(0xff, 10, 20, 30),
make_argb(0x80, 40, 50, 60),
make_argb(0xff, 0, 0, 0),
];
let (w, h) = (5u32, 2u32);
let orig: Vec<u32> = (0..(w * h)).map(|i| palette[(i % 3) as usize]).collect();
let (bundled, bundled_width) = forward_color_indexing(&orig, w, h, &palette);
assert_eq!(bundled_width, 2); let restored = inverse_color_indexing(&bundled, bundled_width, h, w, &palette);
assert_eq!(restored, orig);
}
#[test]
fn forward_color_indexing_two_color_bundles_eight() {
let palette = vec![make_argb(0xff, 1, 2, 3), make_argb(0xff, 9, 9, 9)];
let (w, h) = (10u32, 1u32);
let orig: Vec<u32> = (0..w).map(|i| palette[(i % 2) as usize]).collect();
let (bundled, bundled_width) = forward_color_indexing(&orig, w, h, &palette);
assert_eq!(bundled_width, 2); let restored = inverse_color_indexing(&bundled, bundled_width, h, w, &palette);
assert_eq!(restored, orig);
}
#[test]
fn apply_inverse_dispatches_and_expands_width() {
let table = vec![0xff00_0000, 0xffaa_bbcc];
let mut pixels = vec![make_argb(0xff, 0, 0b0000_0001, 0)]; let t = Vp8lTransform::ColorIndexing {
table: table.clone(),
expanded_width: 8,
};
let new_width = t.apply_inverse(&mut pixels, 1, 1);
assert_eq!(new_width, 8);
assert_eq!(pixels.len(), 8);
assert_eq!(pixels[0], table[1]);
assert_eq!(pixels[1], table[0]);
}
}