use crate::lossless::constants::{ARGB_BLACK, subsample_size};
use crate::lossless::histogram::shannon_bits;
use crate::lossless::prelude::*;
use crate::lossless::transform::{add_pixels, sub_pixels};
use crate::lossless::work::work;
pub(crate) fn inverse(argb: &mut [u32], width: u32, bits: u32, tile_data: &[u32]) {
if width == 0 || argb.is_empty() {
return;
}
let tiles_per_row = subsample_size(width, bits) as usize;
let width = width as usize;
let height = argb.len() / width;
reconstruct_first_row(argb, width);
for y in 1..height {
reconstruct_row(argb, width, bits, tile_data, tiles_per_row, y);
}
}
fn reconstruct_first_row(argb: &mut [u32], width: usize) {
argb[0] = add_pixels(argb[0], ARGB_BLACK);
for x in 1..width {
argb[x] = add_pixels(argb[x], argb[x - 1]);
}
}
fn reconstruct_row(
argb: &mut [u32],
width: usize,
bits: u32,
tile_data: &[u32],
tiles_per_row: usize,
y: usize,
) {
let row = y * width;
argb[row] = add_pixels(argb[row], argb[row - width]);
let tile_row = (y >> bits) * tiles_per_row;
for x in 1..width {
let i = row + x;
let mode = (tile_data[tile_row + (x >> bits)] >> 8) & 0x0f;
let left = argb[i - 1];
let top = argb[i - width];
let top_left = argb[i - width - 1];
let top_right = argb[i - width + 1];
argb[i] = add_pixels(argb[i], predict(mode, left, top_left, top, top_right));
}
}
pub(crate) fn reconstruct_row_into(
out: &mut [u32],
residual: &[u32],
prev_out: &[u32],
y: usize,
bits: u32,
tile_data: &[u32],
) {
let width = out.len();
if width == 0 {
return;
}
if y == 0 {
out[0] = add_pixels(residual[0], ARGB_BLACK);
for x in 1..width {
out[x] = add_pixels(residual[x], out[x - 1]);
}
return;
}
let tiles_per_row = subsample_size(u32::try_from(width).unwrap_or(0), bits) as usize;
out[0] = add_pixels(residual[0], prev_out[0]);
let tile_row = (y >> bits) * tiles_per_row;
for x in 1..width {
let mode = (tile_data[tile_row + (x >> bits)] >> 8) & 0x0f;
let left = out[x - 1];
let top = prev_out[x];
let top_left = prev_out[x - 1];
let top_right = if x + 1 < width {
prev_out[x + 1]
} else {
out[0]
};
out[x] = add_pixels(residual[x], predict(mode, left, top_left, top, top_right));
}
}
struct EntropyScratch {
counts: [[u32; 256]; 4],
touched: [Vec<u8>; 4],
used_counts: Vec<u32>,
neighbors: Vec<(u32, u32, u32, u32, u32)>,
}
impl EntropyScratch {
fn new() -> Self {
Self {
counts: [[0u32; 256]; 4],
touched: core::array::from_fn(|_| Vec::new()),
used_counts: Vec::new(),
neighbors: Vec::new(),
}
}
}
fn best_mode(
argb: &[u32],
width: usize,
height: usize,
bits: u32,
tx: usize,
ty: usize,
scratch: &mut EntropyScratch,
) -> u32 {
let tile = 1usize << bits;
let x0 = (tx << bits).max(1);
let y0 = (ty << bits).max(1);
let x1 = ((tx << bits) + tile).min(width);
let y1 = ((ty << bits) + tile).min(height);
let total = (y1.saturating_sub(y0) * x1.saturating_sub(x0)) as u64;
scratch.neighbors.clear();
for y in y0..y1 {
let row = y * width;
for x in x0..x1 {
let i = row + x;
scratch.neighbors.push((
argb[i],
argb[i - 1],
argb[i - width - 1],
argb[i - width],
argb[i - width + 1],
));
}
}
let mut best = 0u32;
let mut best_cost = u64::MAX;
for mode in 0..=13u32 {
work!(PredictorModeEval);
for &(cur, left, top_left, top, top_right) in &scratch.neighbors {
let residual = sub_pixels(cur, predict(mode, left, top_left, top, top_right));
for (c, &byte) in residual.to_le_bytes().iter().enumerate() {
let idx = usize::from(byte);
if scratch.counts[c][idx] == 0 {
scratch.touched[c].push(byte);
}
scratch.counts[c][idx] += 1;
}
}
let mut cost = 0u64;
for c in 0..4 {
scratch.used_counts.clear();
let n = scratch.touched[c].len();
for k in 0..n {
let byte = scratch.touched[c][k];
scratch
.used_counts
.push(scratch.counts[c][usize::from(byte)]);
}
cost += shannon_bits(total, &scratch.used_counts);
for k in 0..n {
let byte = scratch.touched[c][k];
scratch.counts[c][usize::from(byte)] = 0;
}
scratch.touched[c].clear();
}
if cost < best_cost {
best_cost = cost;
best = mode;
}
}
best
}
#[must_use]
pub(crate) fn forward(argb: &[u32], width: u32, height: u32, bits: u32) -> (Vec<u32>, Vec<u32>) {
let tiles_per_row = subsample_size(width, bits) as usize;
let tiles_per_col = subsample_size(height, bits) as usize;
let mut tile_data = vec![0u32; tiles_per_row * tiles_per_col];
let mut residual = vec![0u32; argb.len()];
if width == 0 || argb.is_empty() {
return (residual, tile_data);
}
let width = width as usize;
let height = height as usize;
let mut scratch = EntropyScratch::new();
for ty in 0..tiles_per_col {
for tx in 0..tiles_per_row {
let mode = best_mode(argb, width, height, bits, tx, ty, &mut scratch);
tile_data[ty * tiles_per_row + tx] = (mode & 0x0f) << 8;
}
}
residual[0] = sub_pixels(argb[0], ARGB_BLACK);
for x in 1..width {
residual[x] = sub_pixels(argb[x], argb[x - 1]);
}
for y in 1..height {
let row = y * width;
residual[row] = sub_pixels(argb[row], argb[row - width]);
let tile_row = (y >> bits) * tiles_per_row;
for x in 1..width {
let i = row + x;
let mode = (tile_data[tile_row + (x >> bits)] >> 8) & 0x0f;
let left = argb[i - 1];
let top = argb[i - width];
let top_left = argb[i - width - 1];
let top_right = argb[i - width + 1];
residual[i] = sub_pixels(argb[i], predict(mode, left, top_left, top, top_right));
}
}
(residual, tile_data)
}
#[must_use]
fn predict(mode: u32, left: u32, tl: u32, t: u32, tr: u32) -> u32 {
match mode {
1 => left,
2 => t,
3 => tr,
4 => tl,
5 => average3(left, t, tr),
6 => average2(left, tl),
7 => average2(left, t),
8 => average2(tl, t),
9 => average2(t, tr),
10 => average4(left, tl, t, tr),
11 => select(t, left, tl),
12 => clamped_add_subtract_full(left, t, tl),
13 => clamped_add_subtract_half(left, t, tl),
_ => ARGB_BLACK,
}
}
#[must_use]
const fn average2(a: u32, b: u32) -> u32 {
(((a ^ b) & 0xfefe_fefe) >> 1).wrapping_add(a & b)
}
#[must_use]
const fn average3(a: u32, b: u32, c: u32) -> u32 {
average2(average2(a, c), b)
}
#[must_use]
const fn average4(a: u32, b: u32, c: u32, d: u32) -> u32 {
average2(average2(a, b), average2(c, d))
}
#[must_use]
const fn clip255(v: i32) -> u8 {
if v < 0 {
0
} else if v > 255 {
255
} else {
v.to_le_bytes()[0]
}
}
#[must_use]
fn add_sub_full(a: i32, b: i32, c: i32) -> i32 {
i32::from(clip255(a + b - c))
}
#[must_use]
fn add_sub_half(a: i32, b: i32) -> i32 {
i32::from(clip255(a + (a - b) / 2))
}
#[must_use]
fn clamped_add_subtract_full(c0: u32, c1: u32, c2: u32) -> u32 {
let a = c0.to_le_bytes();
let b = c1.to_le_bytes();
let c = c2.to_le_bytes();
let out: [u8; 4] = core::array::from_fn(|k| {
add_sub_full(i32::from(a[k]), i32::from(b[k]), i32::from(c[k])).to_le_bytes()[0]
});
u32::from_le_bytes(out)
}
#[must_use]
fn clamped_add_subtract_half(c0: u32, c1: u32, c2: u32) -> u32 {
let ave = average2(c0, c1).to_le_bytes();
let c = c2.to_le_bytes();
let out: [u8; 4] =
core::array::from_fn(|k| add_sub_half(i32::from(ave[k]), i32::from(c[k])).to_le_bytes()[0]);
u32::from_le_bytes(out)
}
#[must_use]
fn select(a: u32, b: u32, c: u32) -> u32 {
let ba = a.to_le_bytes();
let bb = b.to_le_bytes();
let bc = c.to_le_bytes();
let sum: i32 = ba
.iter()
.zip(&bb)
.zip(&bc)
.map(|((&av, &bv), &cv)| {
let cc = i32::from(cv);
(i32::from(bv) - cc).abs() - (i32::from(av) - cc).abs()
})
.sum();
if sum <= 0 { a } else { b }
}
#[cfg(test)]
mod tests {
#![allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "test fixtures build ARGB pixels from small in-range signed offsets; the \
casts fit their targets by construction"
)]
use super::{
EntropyScratch, add_sub_full, add_sub_half, average2, average3, average4, best_mode,
clamped_add_subtract_full, clamped_add_subtract_half, clip255, forward, inverse, predict,
reconstruct_row_into, select, shannon_bits, sub_pixels,
};
use crate::lossless::constants::subsample_size;
use proptest::prelude::*;
fn best_mode_reference(
argb: &[u32],
width: usize,
height: usize,
bits: u32,
tx: usize,
ty: usize,
scratch: &mut EntropyScratch,
) -> u32 {
let tile = 1usize << bits;
let x0 = (tx << bits).max(1);
let y0 = (ty << bits).max(1);
let x1 = ((tx << bits) + tile).min(width);
let y1 = ((ty << bits) + tile).min(height);
let total = (y1.saturating_sub(y0) * x1.saturating_sub(x0)) as u64;
let mut best = 0u32;
let mut best_cost = u64::MAX;
for mode in 0..=13u32 {
for y in y0..y1 {
let row = y * width;
for x in x0..x1 {
let i = row + x;
let left = argb[i - 1];
let top = argb[i - width];
let top_left = argb[i - width - 1];
let top_right = argb[i - width + 1];
let residual =
sub_pixels(argb[i], predict(mode, left, top_left, top, top_right));
for (c, &byte) in residual.to_le_bytes().iter().enumerate() {
let idx = usize::from(byte);
if scratch.counts[c][idx] == 0 {
scratch.touched[c].push(byte);
}
scratch.counts[c][idx] += 1;
}
}
}
let mut cost = 0u64;
for c in 0..4 {
scratch.used_counts.clear();
let n = scratch.touched[c].len();
for k in 0..n {
let byte = scratch.touched[c][k];
scratch
.used_counts
.push(scratch.counts[c][usize::from(byte)]);
}
cost += shannon_bits(total, &scratch.used_counts);
for k in 0..n {
let byte = scratch.touched[c][k];
scratch.counts[c][usize::from(byte)] = 0;
}
scratch.touched[c].clear();
}
if cost < best_cost {
best_cost = cost;
best = mode;
}
}
best
}
fn assert_rows_equal_batch(
coded: &[u32],
width: u32,
height: u32,
bits: u32,
tile_data: &[u32],
) {
let mut batch = coded.to_vec();
inverse(&mut batch, width, bits, tile_data);
let w = width as usize;
let mut rows = Vec::with_capacity(coded.len());
let mut prev: Vec<u32> = vec![0u32; w];
for y in 0..height as usize {
let mut out = vec![0u32; w];
reconstruct_row_into(
&mut out,
&coded[y * w..(y + 1) * w],
&prev,
y,
bits,
tile_data,
);
rows.extend_from_slice(&out);
prev = out;
}
assert_eq!(batch, rows);
}
#[test]
fn average2_floors_each_lane_independently() {
assert_eq!(average2(0x1020_3040, 0x3040_5060), 0x2030_4050);
assert_eq!(average2(0x0000_00ff, 0x0000_0000), 0x0000_007f);
}
#[test]
fn average3_averages_a_and_c_first() {
assert_eq!(average3(0x0000_0000, 0x0000_00ff, 0x0000_0002), 0x0000_0080);
}
#[test]
fn average4_averages_pairs_then_combines() {
assert_eq!(
average4(0x0000_0000, 0x0000_0004, 0x0000_0008, 0x0000_000c),
0x0000_0006
);
}
#[test]
#[allow(
clippy::cast_sign_loss,
reason = "test reproduces libwebp's raw i32->u32 !a>>24 bit trick to prove equivalence"
)]
fn clip255_matches_libwebp_bit_trick_over_full_domain() {
for v in -255..=510 {
let got = u32::from(clip255(v));
let vu = v as u32; let trick = if vu < 256 { vu } else { !vu >> 24 };
assert_eq!(got, trick, "clip255 diverged from trick at v={v}");
}
}
#[test]
fn add_sub_full_clamps_both_ends() {
assert_eq!(add_sub_full(10, 20, 5), 25);
assert_eq!(add_sub_full(0, 0, 255), 0); assert_eq!(add_sub_full(255, 255, 0), 255); }
#[test]
fn add_sub_half_truncates_toward_zero_not_arithmetic_shift() {
assert_eq!(add_sub_half(10, 13), 9);
assert_eq!(add_sub_half(0, 255), 0); assert_eq!(add_sub_half(255, 0), 255); }
#[test]
fn select_returns_a_on_tie_and_b_when_positive() {
assert_eq!(select(0x0000_005a, 0x0000_006e, 0x0000_0064), 0x0000_005a);
assert_eq!(select(0x0000_005a, 0x0000_0082, 0x0000_0064), 0x0000_0082);
}
#[test]
fn clamped_add_subtract_full_and_half_known_values() {
assert_eq!(
clamped_add_subtract_full(0x1020_3040, 0x0101_0101, 0x0202_0202),
0x0f1f_2f3f
);
assert_eq!(
clamped_add_subtract_full(0x0000_00ff, 0x0000_00ff, 0x0000_0000),
0x0000_00ff );
assert_eq!(
clamped_add_subtract_half(0x0000_0014, 0x0000_0014, 0x0000_000a),
0x0000_0019
);
}
#[test]
fn inverse_reconstructs_3x2_with_mode7_average() {
let tile_data = [0x0000_0700_u32];
let mut argb = [
0x0010_2030, 0x0001_0203, 0x0001_0101, 0x0002_0202, 0x0000_0000, 0x0000_0000, ];
inverse(&mut argb, 3, 2, &tile_data);
assert_eq!(
argb,
[
0xff10_2030, 0xff11_2233, 0xff12_2334, 0xff12_2232, 0xff11_2232, 0xff11_2233, ]
);
}
#[test]
fn inverse_last_column_top_right_wraps_to_current_row_col0() {
let tile_data = [0x0000_0300_u32];
let mut argb = [0x0000_0005, 0x0000_0003, 0x0000_0002, 0x0000_0001];
inverse(&mut argb, 2, 1, &tile_data);
assert_eq!(argb, [0xff00_0005, 0xff00_0008, 0xff00_0007, 0xff00_0008]);
}
#[test]
fn forward_reverses_the_mode7_inverse_fixture() {
let original = [
0xff10_2030u32,
0xff11_2233,
0xff12_2334,
0xff12_2232,
0xff11_2232,
0xff11_2233,
];
let (residual, tile_data) = forward(&original, 3, 2, 2);
let mut round = residual; inverse(&mut round, 3, 2, &tile_data);
assert_eq!(round, original);
}
#[test]
fn forward_reverses_the_last_column_wrap_fixture() {
let original = [0xff00_0005u32, 0xff00_0008, 0xff00_0007, 0xff00_0008];
let (residual, tile_data) = forward(&original, 2, 2, 1);
let mut round = residual; inverse(&mut round, 2, 1, &tile_data);
assert_eq!(round, original);
}
#[test]
fn best_mode_prefers_lz77_friendly_mode_on_wrapping_gradient() {
let ramp = |wide: u32, tall: u32| -> Vec<u32> {
(0..tall)
.flat_map(|row| {
(0..wide).map(move |col| {
let red = (4 * col) & 0xff;
let green = (4 * row) & 0xff;
let blue = (2 * (col + row)) & 0xff;
(255u32 << 24) | (red << 16) | (green << 8) | blue
})
})
.collect()
};
let (wide, tall) = (96u32, 8u32);
let argb = ramp(wide, tall);
let (_residual, tile_data) = forward(&argb, wide, tall, 2);
let modes: Vec<u32> = tile_data.iter().map(|&td| (td >> 8) & 0x0f).collect();
assert!(
modes.iter().all(|&m| m != 12),
"entropy metric must not pick clip255-spiky mode 12: {modes:?}"
);
let ones = modes.iter().filter(|&&m| m == 1).count();
assert!(
ones * 2 > modes.len(),
"mode 1 should dominate the interior tiles, got {ones}/{} : {modes:?}",
modes.len()
);
let wrap_tile: Vec<u32> = (0..8u32)
.flat_map(|row| {
(0..8u32).map(move |col| {
let red = (40 * col) & 0xff;
let green = (40 * row) & 0xff;
let blue = (40 * (col + row)) & 0xff;
(255u32 << 24) | (red << 16) | (green << 8) | blue
})
})
.collect();
let (_r2, td2) = forward(&wrap_tile, 8, 8, 3); assert_eq!(td2.len(), 1);
assert_eq!(
(td2[0] >> 8) & 0x0f,
1,
"single wrap tile must resolve to mode 1"
);
}
#[test]
fn inverse_tile_row_offset_is_multiplied_not_divided() {
let tile_data = [
0x0000_0100_u32, 0x0000_0100, 0x0000_0200, 0x0000_0200, ];
let mut argb = [
0x0000_000a_u32,
0x0000_0003,
0x0000_0005,
0x0000_0002, 0x0000_0005,
0x0000_0002,
0x0000_0002,
0x0000_0002, 0x0000_0004,
0x0000_0003,
0x0000_0001,
0x0000_0001, ];
inverse(&mut argb, 4, 1, &tile_data);
assert_eq!(
argb,
[
0xff00_000a,
0xff00_000d,
0xff00_0012,
0xff00_0014, 0xff00_000f,
0xff00_0011,
0xff00_0013,
0xff00_0015, 0xff00_0013,
0xff00_0014,
0xff00_0014,
0xff00_0016, ]
);
}
#[test]
fn reconstruct_row_into_equals_whole_buffer_inverse_on_fixtures() {
assert_rows_equal_batch(
&[0x0010_2030, 0x0001_0203, 0x0001_0101, 0x0002_0202, 0, 0],
3,
2,
2,
&[0x0000_0700],
);
assert_rows_equal_batch(&[5, 3, 2, 1], 2, 2, 1, &[0x0000_0300]);
}
#[test]
fn inverse_returns_untouched_on_zero_width() {
let mut argb = [0x1122_3344u32, 0x5566_7788];
inverse(&mut argb, 0, 2, &[]);
assert_eq!(argb, [0x1122_3344u32, 0x5566_7788]);
}
#[test]
fn forward_returns_zero_residual_on_zero_width() {
let (residual, tile_data) = forward(&[0x1122_3344u32], 0, 1, 2);
assert_eq!(residual, vec![0u32]);
assert!(tile_data.is_empty());
}
#[test]
fn best_mode_uses_shifted_tile_origin_for_interior_row() {
let argb = [0u32, 0, 15, 20, 10, 15, 10, 15];
let mut scratch = EntropyScratch::new();
assert_eq!(best_mode(&argb, 2, 4, 1, 0, 1, &mut scratch), 0);
}
#[test]
fn best_mode_reads_top_right_by_subtraction_not_division() {
let argb = [
0xff00_0044_u32,
0xff00_0020,
0xff00_0082,
0xff00_003c, 0xff00_00fd,
0xff00_0082,
0xff00_003c,
0xff00_00fd, 0xff00_00e6,
0xff00_003c,
0xff00_00fd,
0xff00_00e6, ];
let mut scratch = EntropyScratch::new();
assert_eq!(best_mode(&argb, 4, 3, 2, 0, 0, &mut scratch), 3);
}
#[test]
fn predict_dispatches_each_mode_to_its_distinct_combo() {
let left = 0xff10_2030u32;
let tl = 0xff40_5060u32;
let t = 0xff70_8090u32;
let tr = 0xffa0_b0c0u32;
assert_eq!(predict(2, left, tl, t, tr), t);
assert_eq!(predict(4, left, tl, t, tr), tl);
assert_eq!(predict(5, left, tl, t, tr), average3(left, t, tr));
assert_eq!(predict(6, left, tl, t, tr), average2(left, tl));
assert_eq!(predict(8, left, tl, t, tr), average2(tl, t));
assert_eq!(predict(9, left, tl, t, tr), average2(t, tr));
assert_eq!(predict(10, left, tl, t, tr), average4(left, tl, t, tr));
assert_eq!(predict(11, left, tl, t, tr), select(t, left, tl));
assert_eq!(
predict(12, left, tl, t, tr),
clamped_add_subtract_full(left, t, tl)
);
assert_eq!(
predict(13, left, tl, t, tr),
clamped_add_subtract_half(left, t, tl)
);
for &v in &[
t,
tl,
average3(left, t, tr),
average2(left, tl),
average2(tl, t),
average2(t, tr),
average4(left, tl, t, tr),
select(t, left, tl),
clamped_add_subtract_full(left, t, tl),
clamped_add_subtract_half(left, t, tl),
] {
assert_ne!(v, super::ARGB_BLACK);
}
}
proptest! {
#[test]
fn forward_then_inverse_is_identity(
(width, height, bits, argb) in (1u32..=8, 1u32..=8, 2u32..=5u32).prop_flat_map(
|(w, h, bits)| {
prop::collection::vec(any::<u32>(), (w as usize) * (h as usize))
.prop_map(move |argb| (w, h, bits, argb))
},
)
) {
let (residual, tile_data) = forward(&argb, width, height, bits);
let mut round = residual;
inverse(&mut round, width, bits, &tile_data);
prop_assert_eq!(round, argb);
}
#[test]
fn reconstruct_row_into_matches_whole_buffer_inverse(
(width, height, bits, coded, modes) in (1u32..=8, 1u32..=8, 2u32..=5u32)
.prop_flat_map(|(w, h, bits)| {
let n = (w as usize) * (h as usize);
let tiles =
(subsample_size(w, bits) as usize) * (subsample_size(h, bits) as usize);
(
Just(w),
Just(h),
Just(bits),
prop::collection::vec(any::<u32>(), n),
prop::collection::vec(0u32..=13, tiles),
)
})
) {
let tile_data: Vec<u32> = modes.iter().map(|&m| (m & 0x0f) << 8).collect();
assert_rows_equal_batch(&coded, width, height, bits, &tile_data);
}
#[test]
fn best_mode_matches_reference(
(width, height, bits, argb) in (1u32..=8, 1u32..=8, 2u32..=5u32).prop_flat_map(
|(w, h, bits)| {
let n = (w as usize) * (h as usize);
prop_oneof![
prop::collection::vec(any::<u32>(), n),
prop::collection::vec(
prop::sample::select(vec![
0u32, 0xffff_ffff, 0xff00_0000, 0x00ff_ffff, 0xff80_8080,
]),
n,
),
]
.prop_map(move |argb| (w, h, bits, argb))
},
)
) {
let w = width as usize;
let h = height as usize;
let tiles_per_row = subsample_size(width, bits) as usize;
let tiles_per_col = subsample_size(height, bits) as usize;
let mut fast = EntropyScratch::new();
let mut slow = EntropyScratch::new();
for ty in 0..tiles_per_col {
for tx in 0..tiles_per_row {
let got = best_mode(&argb, w, h, bits, tx, ty, &mut fast);
let want = best_mode_reference(&argb, w, h, bits, tx, ty, &mut slow);
prop_assert_eq!(got, want);
}
}
}
}
fn img_left_winner() -> Vec<u32> {
(0..4)
.flat_map(|_y| (0..4u32).map(|x| 0xff00_0000 | (20 * x)))
.collect()
}
fn img_top_winner() -> Vec<u32> {
let h = [0u32, 7, 15, 26];
(0..4u32)
.flat_map(|y| (0..4usize).map(move |x| 0xff00_0000 | (10 * y + h[x])))
.collect()
}
fn img_top_left_winner() -> Vec<u32> {
let h = [0i32, 5, 9, 20, 26, 35, 41]; (0..4i32)
.flat_map(|y| {
(0..4i32).map(move |x| 0xff00_0000 | ((10 * y + h[(x - y + 3) as usize]) as u32))
})
.collect()
}
#[test]
fn best_mode_tile_x_origin_shifts_left_not_right() {
let blue = [0u32, 100, 110, 120];
let argb: Vec<u32> = (0..4)
.flat_map(|_y| (0..4usize).map(move |x| 0xff00_0000 | blue[x]))
.collect();
let mut scratch = EntropyScratch::new();
assert_eq!(best_mode(&argb, 4, 4, 1, 1, 1, &mut scratch), 1);
}
#[test]
fn best_mode_top_left_neighbor_is_load_bearing() {
let argb = img_top_left_winner();
let mut scratch = EntropyScratch::new();
assert_eq!(best_mode(&argb, 4, 4, 2, 0, 0, &mut scratch), 4);
}
#[test]
fn best_mode_top_neighbor_is_load_bearing() {
let argb = img_top_winner();
let mut scratch = EntropyScratch::new();
assert_eq!(best_mode(&argb, 4, 4, 2, 0, 0, &mut scratch), 2);
}
#[test]
fn best_mode_left_ramp_resolves_to_mode_one() {
let argb = img_left_winner();
let mut scratch = EntropyScratch::new();
assert_eq!(best_mode(&argb, 4, 4, 2, 0, 0, &mut scratch), 1);
}
#[test]
fn reconstruct_row_into_interior_modes_and_wrap_are_pinned() {
let prev_out = [0x0000_000a_u32, 0x0000_0014, 0x0000_001e, 0x0000_0028];
let residual = [0x0000_0002_u32, 0x0000_0001, 0x0000_0003, 0x0000_0005];
let tile_data = [0x0000_0200_u32, 0x0000_0100, 0x0000_0400, 0x0000_0300];
let mut out = [0u32; 4];
reconstruct_row_into(&mut out, &residual, &prev_out, 2, 1, &tile_data);
assert_eq!(out, [0x0000_000c, 0x0000_000b, 0x0000_002b, 0x0000_0011]);
}
#[test]
fn inverse_mode4_reads_top_left_neighbor() {
let tile_data = [0x0000_0400_u32]; let mut argb = [
0x0000_0005_u32,
0x0000_0003,
0x0000_0002, 0x0000_0001,
0x0000_0000,
0x0000_0000, ];
inverse(&mut argb, 3, 2, &tile_data);
assert_eq!(
argb,
[
0xff00_0005,
0xff00_0008,
0xff00_000a, 0xff00_0006,
0xff00_0005,
0xff00_0008, ]
);
}
#[test]
fn forward_roundtrips_each_single_predictor_mode() {
let mode3_img = [
0xff00_0044_u32,
0xff00_0020,
0xff00_0082,
0xff00_003c, 0xff00_00fd,
0xff00_0082,
0xff00_003c,
0xff00_00fd, 0xff00_00e6,
0xff00_003c,
0xff00_00fd,
0xff00_00e6, ];
for (img, w, h, bits) in [
(img_left_winner(), 4u32, 4u32, 2u32),
(img_top_winner(), 4, 4, 2),
(img_top_left_winner(), 4, 4, 2),
(mode3_img.to_vec(), 4, 3, 2),
] {
let (residual, tile_data) = forward(&img, w, h, bits);
let mut round = residual;
inverse(&mut round, w, bits, &tile_data);
assert_eq!(round, img, "round-trip failed for {w}x{h}");
}
}
#[test]
fn forward_roundtrips_multi_tile_with_mixed_modes() {
let h = [0i32, 5, 9, 20, 26, 35, 41];
let img: Vec<u32> = (0..8i32)
.flat_map(|y| {
(0..8i32).map(move |x| {
let b = if y < 4 {
(13 * x) & 0xff
} else {
(7 * y + h[(x % 7) as usize]) & 0xff
};
0xff00_0000 | (b as u32)
})
})
.collect();
let (residual, tile_data) = forward(&img, 8, 8, 2);
let modes: Vec<u32> = tile_data.iter().map(|&d| (d >> 8) & 0x0f).collect();
assert_eq!(modes, vec![1, 1, 2, 12]);
let mut round = residual;
inverse(&mut round, 8, 2, &tile_data);
assert_eq!(round, img);
}
}