use alloc::vec;
use super::types::make_argb;
const MIN_DIM_FOR_NEAR_LOSSLESS: usize = 64;
#[inline]
pub fn max_quantization_from_quality(quality: u8) -> u32 {
1u32 << near_lossless_bits(quality)
}
#[inline]
fn near_lossless_bits(quality: u8) -> u8 {
5 - quality / 20
}
#[inline]
fn max_diff_between_pixels(p1: u32, p2: u32) -> i32 {
let diff_a = ((p1 >> 24) as i32 - (p2 >> 24) as i32).abs();
let diff_r = (((p1 >> 16) & 0xff) as i32 - ((p2 >> 16) & 0xff) as i32).abs();
let diff_g = (((p1 >> 8) & 0xff) as i32 - ((p2 >> 8) & 0xff) as i32).abs();
let diff_b = ((p1 & 0xff) as i32 - (p2 & 0xff) as i32).abs();
diff_a.max(diff_r).max(diff_g.max(diff_b))
}
#[inline]
fn max_diff_around_pixel(current: u32, up: u32, down: u32, left: u32, right: u32) -> u8 {
let d = max_diff_between_pixels(current, up)
.max(max_diff_between_pixels(current, down))
.max(max_diff_between_pixels(current, left).max(max_diff_between_pixels(current, right)));
d.min(255) as u8
}
#[inline]
fn add_green_to_blue_and_red(argb: u32) -> u32 {
let green = (argb >> 8) & 0xff;
let mut red_blue = argb & 0x00ff00ff;
red_blue = red_blue.wrapping_add((green << 16) | green);
red_blue &= 0x00ff00ff;
(argb & 0xff00ff00) | red_blue
}
pub fn max_diffs_for_row(
pixels: &[u32],
width: usize,
y: usize,
max_diffs: &mut [u8],
used_subtract_green: bool,
) {
if width <= 2 {
return;
}
let row = y * width;
let above = (y - 1) * width;
let below = (y + 1) * width;
let transform = |p: u32| -> u32 {
if used_subtract_green {
add_green_to_blue_and_red(p)
} else {
p
}
};
let mut current = transform(pixels[row]);
let mut right = transform(pixels[row + 1]);
for x in 1..width - 1 {
let up = transform(pixels[above + x]);
let down = transform(pixels[below + x]);
let left = current;
current = right;
right = transform(pixels[row + x + 1]);
max_diffs[x] = max_diff_around_pixel(current, up, down, left, right);
}
}
#[inline]
fn near_lossless_component(value: u8, predict: u8, boundary: u8, quantization: i32) -> u8 {
let residual = (value as i32 - predict as i32) & 0xff;
let boundary_residual = (boundary as i32 - predict as i32) & 0xff;
let lower = residual & !(quantization - 1);
let upper = lower + quantization;
let bias = (((boundary as i32 - value as i32) & 0xff) < boundary_residual) as i32;
if residual - lower < upper - residual + bias {
if residual > boundary_residual && lower <= boundary_residual {
(lower + (quantization >> 1)) as u8
} else {
lower as u8
}
} else {
if residual <= boundary_residual && upper > boundary_residual {
(lower + (quantization >> 1)) as u8
} else {
(upper & 0xff) as u8
}
}
}
#[inline]
pub fn near_lossless_residual(
value: u32,
predict: u32,
max_quantization: u32,
max_diff: u8,
used_subtract_green: bool,
) -> (u32, u32) {
if max_diff as i32 <= 2 {
let res = sub_pixels(value, predict);
return (res, value);
}
let mut quantization = max_quantization as i32;
while quantization >= max_diff as i32 {
quantization >>= 1;
}
let a = if (value >> 24) == 0 || (value >> 24) == 0xff {
((value >> 24) as u8).wrapping_sub((predict >> 24) as u8)
} else {
near_lossless_component(
(value >> 24) as u8,
(predict >> 24) as u8,
0xff,
quantization,
)
};
let g = near_lossless_component(
((value >> 8) & 0xff) as u8,
((predict >> 8) & 0xff) as u8,
0xff,
quantization,
);
let (new_green, green_diff) = if used_subtract_green {
let new_g = ((predict >> 8) as u8).wrapping_add(g);
let g_diff = new_g.wrapping_sub(((value >> 8) & 0xff) as u8);
(new_g, g_diff)
} else {
(0, 0)
};
let r_value = ((value >> 16) & 0xff) as u8;
let r = near_lossless_component(
r_value.wrapping_sub(green_diff),
((predict >> 16) & 0xff) as u8,
0xffu8.wrapping_sub(new_green),
quantization,
);
let b_value = (value & 0xff) as u8;
let b = near_lossless_component(
b_value.wrapping_sub(green_diff),
(predict & 0xff) as u8,
0xffu8.wrapping_sub(new_green),
quantization,
);
let residual = make_argb(a, r, g, b);
let reconstructed = add_pixels(predict, residual);
(residual, reconstructed)
}
#[inline]
fn sub_pixels(a: u32, b: u32) -> u32 {
let pa = ((a >> 24) as u8).wrapping_sub((b >> 24) as u8);
let pr = (((a >> 16) & 0xff) as u8).wrapping_sub(((b >> 16) & 0xff) as u8);
let pg = (((a >> 8) & 0xff) as u8).wrapping_sub(((b >> 8) & 0xff) as u8);
let pb = ((a & 0xff) as u8).wrapping_sub((b & 0xff) as u8);
make_argb(pa, pr, pg, pb)
}
#[inline]
fn add_pixels(a: u32, b: u32) -> u32 {
let pa = ((a >> 24) as u8).wrapping_add((b >> 24) as u8);
let pr = (((a >> 16) & 0xff) as u8).wrapping_add(((b >> 16) & 0xff) as u8);
let pg = (((a >> 8) & 0xff) as u8).wrapping_add(((b >> 8) & 0xff) as u8);
let pb = ((a & 0xff) as u8).wrapping_add((b & 0xff) as u8);
make_argb(pa, pr, pg, pb)
}
#[inline]
fn find_closest_discretized(a: u8, bits: u8) -> u8 {
let a = a as u32;
let mask = (1u32 << bits) - 1;
let biased = a + (mask >> 1) + ((a >> bits) & 1);
if biased > 0xff {
0xff
} else {
(biased & !mask) as u8
}
}
#[inline]
fn closest_discretized_argb(argb: u32, bits: u8) -> u32 {
(find_closest_discretized((argb >> 24) as u8, bits) as u32) << 24
| (find_closest_discretized((argb >> 16) as u8, bits) as u32) << 16
| (find_closest_discretized((argb >> 8) as u8, bits) as u32) << 8
| find_closest_discretized(argb as u8, bits) as u32
}
#[inline]
fn is_near(a: u32, b: u32, limit: i32) -> bool {
for k in 0..4 {
let delta = ((a >> (k * 8)) & 0xff) as i32 - ((b >> (k * 8)) & 0xff) as i32;
if delta >= limit || delta <= -limit {
return false;
}
}
true
}
#[inline]
fn is_smooth(prev_row: &[u32], curr_row: &[u32], next_row: &[u32], x: usize, limit: i32) -> bool {
is_near(curr_row[x], curr_row[x - 1], limit)
&& is_near(curr_row[x], curr_row[x + 1], limit)
&& is_near(curr_row[x], prev_row[x], limit)
&& is_near(curr_row[x], next_row[x], limit)
}
fn near_lossless_pass(src: &[u32], w: usize, h: usize, bits: u8, dst: &mut [u32]) {
let limit = 1i32 << bits;
let mut prev_row = vec![0u32; w];
let mut curr_row = vec![0u32; w];
let mut next_row = vec![0u32; w];
curr_row.copy_from_slice(&src[..w]);
if h > 1 {
next_row.copy_from_slice(&src[w..2 * w]);
}
for y in 0..h {
if y == 0 || y == h - 1 {
dst[y * w..(y + 1) * w].copy_from_slice(&src[y * w..(y + 1) * w]);
} else {
if y + 1 < h {
next_row.copy_from_slice(&src[(y + 1) * w..(y + 2) * w]);
}
dst[y * w] = src[y * w];
dst[y * w + w - 1] = src[y * w + w - 1];
for x in 1..w - 1 {
if is_smooth(&prev_row, &curr_row, &next_row, x, limit) {
dst[y * w + x] = curr_row[x];
} else {
dst[y * w + x] = closest_discretized_argb(curr_row[x], bits);
}
}
}
let temp = core::mem::replace(&mut prev_row, curr_row);
curr_row = core::mem::replace(&mut next_row, temp);
}
}
pub fn apply_near_lossless(argb: &mut [u32], w: usize, h: usize, quality: u8) {
if quality >= 100 {
return;
}
let limit_bits = near_lossless_bits(quality);
if limit_bits == 0 {
return;
}
if (w < MIN_DIM_FOR_NEAR_LOSSLESS && h < MIN_DIM_FOR_NEAR_LOSSLESS) || h < 3 {
return;
}
let mut copy_buffer = argb.to_vec();
near_lossless_pass(©_buffer, w, h, limit_bits, argb);
for bits in (1..limit_bits).rev() {
copy_buffer.copy_from_slice(argb);
near_lossless_pass(©_buffer, w, h, bits, argb);
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn test_max_quantization_from_quality() {
assert_eq!(max_quantization_from_quality(100), 1);
assert_eq!(max_quantization_from_quality(99), 2);
assert_eq!(max_quantization_from_quality(80), 2);
assert_eq!(max_quantization_from_quality(60), 4);
assert_eq!(max_quantization_from_quality(40), 8);
assert_eq!(max_quantization_from_quality(20), 16);
assert_eq!(max_quantization_from_quality(0), 32);
}
#[test]
fn test_max_diff_between_pixels() {
let a = 0xFF_80_40_20u32;
let b = 0xFF_90_40_20u32;
assert_eq!(max_diff_between_pixels(a, b), 16); }
#[test]
fn test_near_lossless_component_basic() {
let r = near_lossless_component(130, 128, 0xff, 4);
assert_eq!(r, 0);
}
#[test]
fn test_near_lossless_residual_smooth() {
let value = 0xFF_80_40_20u32;
let predict = 0xFF_80_40_1Eu32;
let (res, _recon) = near_lossless_residual(value, predict, 4, 2, false);
assert_eq!(res, sub_pixels(value, predict));
}
#[test]
fn test_near_lossless_residual_reconstruct() {
let value = 0xFF_80_60_40u32;
let predict = 0xFF_70_50_30u32;
let (res, recon) = near_lossless_residual(value, predict, 8, 20, false);
assert_eq!(add_pixels(predict, res), recon);
}
#[test]
fn test_add_green_to_blue_and_red() {
let argb = 0xFF_10_20_30u32;
let result = add_green_to_blue_and_red(argb);
assert_eq!(result, 0xFF_30_20_50u32);
}
#[test]
fn test_near_lossless_bits() {
assert_eq!(near_lossless_bits(100), 0);
assert_eq!(near_lossless_bits(99), 1);
assert_eq!(near_lossless_bits(80), 1);
assert_eq!(near_lossless_bits(79), 2);
assert_eq!(near_lossless_bits(60), 2);
assert_eq!(near_lossless_bits(59), 3);
assert_eq!(near_lossless_bits(40), 3);
assert_eq!(near_lossless_bits(39), 4);
assert_eq!(near_lossless_bits(20), 4);
assert_eq!(near_lossless_bits(19), 5);
assert_eq!(near_lossless_bits(0), 5);
}
#[test]
fn test_find_closest_discretized() {
assert_eq!(find_closest_discretized(0, 1), 0); assert_eq!(find_closest_discretized(1, 1), 0); assert_eq!(find_closest_discretized(2, 1), 2); assert_eq!(find_closest_discretized(3, 1), 4); assert_eq!(find_closest_discretized(4, 1), 4); assert_eq!(find_closest_discretized(254, 1), 254);
assert_eq!(find_closest_discretized(255, 1), 255);
assert_eq!(find_closest_discretized(0, 2), 0); assert_eq!(find_closest_discretized(1, 2), 0); assert_eq!(find_closest_discretized(2, 2), 0); assert_eq!(find_closest_discretized(3, 2), 4); assert_eq!(find_closest_discretized(4, 2), 4); assert_eq!(find_closest_discretized(253, 2), 252);
assert_eq!(find_closest_discretized(255, 2), 255); }
#[test]
fn test_find_closest_discretized_matches_libwebp() {
for bits in 1u8..=5 {
let mask = (1u32 << bits) - 1;
for a in 0u8..=255 {
let a32 = a as u32;
let biased = a32 + (mask >> 1) + ((a32 >> bits) & 1);
let expected = if biased > 0xff {
0xff
} else {
(biased & !mask) as u8
};
assert_eq!(
find_closest_discretized(a, bits),
expected,
"mismatch at a={a}, bits={bits}"
);
}
}
}
#[test]
fn test_is_near() {
let a = 0xFF_80_40_20u32; let b = 0xFF_82_3E_21u32;
assert!(is_near(a, b, 4));
assert!(!is_near(a, b, 2));
}
#[test]
fn test_quality_100_is_noop() {
let mut argb = vec![0xFF_00_00_00u32; 100 * 100];
let original = argb.clone();
apply_near_lossless(&mut argb, 100, 100, 100);
assert_eq!(argb, original);
}
#[test]
fn test_small_image_skipped() {
let mut argb = vec![0xFF_80_80_80u32; 32 * 32];
let original = argb.clone();
apply_near_lossless(&mut argb, 32, 32, 50);
assert_eq!(argb, original);
}
#[test]
fn test_short_image_skipped() {
let mut argb = vec![0xFF_80_80_80u32; 100 * 2];
let original = argb.clone();
apply_near_lossless(&mut argb, 100, 2, 50);
assert_eq!(argb, original);
}
#[test]
fn test_near_lossless_modifies_pixels() {
let w = 64;
let h = 64;
let mut argb = Vec::with_capacity(w * h);
for y in 0..h {
for x in 0..w {
let r = ((x * 37 + y * 13) % 256) as u8;
let g = ((x * 53 + y * 7) % 256) as u8;
let b = ((x * 11 + y * 29) % 256) as u8;
argb.push(0xFF000000 | (r as u32) << 16 | (g as u32) << 8 | b as u32);
}
}
let original = argb.clone();
apply_near_lossless(&mut argb, w, h, 60);
for x in 0..w {
assert_eq!(argb[x], original[x], "top row modified at x={x}");
assert_eq!(
argb[(h - 1) * w + x],
original[(h - 1) * w + x],
"bottom row modified at x={x}"
);
}
for y in 0..h {
assert_eq!(argb[y * w], original[y * w], "left col modified at y={y}");
assert_eq!(
argb[y * w + w - 1],
original[y * w + w - 1],
"right col modified at y={y}"
);
}
let mut changed = 0;
for y in 1..h - 1 {
for x in 1..w - 1 {
if argb[y * w + x] != original[y * w + x] {
changed += 1;
}
}
}
assert!(changed > 0, "no interior pixels were modified");
}
#[test]
fn test_near_lossless_bounded_error() {
let w = 64;
let h = 64;
let mut argb = Vec::with_capacity(w * h);
for y in 0..h {
for x in 0..w {
let r = ((x * 37 + y * 13) % 256) as u8;
let g = ((x * 53 + y * 7) % 256) as u8;
let b = ((x * 11 + y * 29) % 256) as u8;
argb.push(0xFF000000 | (r as u32) << 16 | (g as u32) << 8 | b as u32);
}
}
let original = argb.clone();
for quality in [0u8, 20, 40, 60, 80, 99] {
let mut test = original.clone();
apply_near_lossless(&mut test, w, h, quality);
let limit_bits = near_lossless_bits(quality);
for i in 0..w * h {
for ch in 0..4 {
let orig_val = ((original[i] >> (ch * 8)) & 0xff) as i32;
let new_val = ((test[i] >> (ch * 8)) & 0xff) as i32;
let max_error = (1i32 << limit_bits) - 1;
assert!(
(orig_val - new_val).abs() <= max_error,
"quality={quality}, pixel={i}, channel={ch}: orig={orig_val}, new={new_val}, max_error={max_error}"
);
}
}
}
}
}