use super::{FilterStrategy, PngOptions};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[cfg(feature = "simd")]
use crate::simd;
struct AdaptiveScratch {
none: Vec<u8>,
sub: Vec<u8>,
up: Vec<u8>,
avg: Vec<u8>,
paeth: Vec<u8>,
}
impl AdaptiveScratch {
fn new(row_len: usize) -> Self {
Self {
none: Vec::with_capacity(row_len),
sub: Vec::with_capacity(row_len),
up: Vec::with_capacity(row_len),
avg: Vec::with_capacity(row_len),
paeth: Vec::with_capacity(row_len),
}
}
fn clear(&mut self) {
self.none.clear();
self.sub.clear();
self.up.clear();
self.avg.clear();
self.paeth.clear();
}
}
const FILTER_NONE: u8 = 0;
const FILTER_SUB: u8 = 1;
const FILTER_UP: u8 = 2;
const FILTER_AVERAGE: u8 = 3;
const FILTER_PAETH: u8 = 4;
pub fn apply_filters(
data: &[u8],
width: u32,
height: u32,
bytes_per_pixel: usize,
options: &PngOptions,
) -> Vec<u8> {
let row_bytes = width as usize * bytes_per_pixel;
apply_filters_with_row_bytes(data, width, height, row_bytes, bytes_per_pixel, options)
}
pub fn apply_filters_with_row_bytes(
data: &[u8],
width: u32,
height: u32,
row_bytes: usize,
bytes_per_pixel: usize,
options: &PngOptions,
) -> Vec<u8> {
let filtered_row_size = row_bytes + 1; let zero_row = vec![0u8; row_bytes];
let mut strategy = options.filter_strategy;
let area = (width as usize).saturating_mul(height as usize);
if area <= 4096
&& matches!(
strategy,
FilterStrategy::Adaptive | FilterStrategy::AdaptiveFast | FilterStrategy::Bigrams
)
{
strategy = FilterStrategy::Sub;
}
#[cfg(feature = "parallel")]
{
if height > 32
&& matches!(
strategy,
FilterStrategy::Adaptive | FilterStrategy::AdaptiveFast | FilterStrategy::Bigrams
)
{
return apply_filters_parallel(
data,
height as usize,
row_bytes,
bytes_per_pixel,
filtered_row_size,
strategy,
);
}
}
let height = height as usize;
debug_assert_eq!(
data.len(),
row_bytes.saturating_mul(height),
"filtered rows expect {} bytes, got {}",
row_bytes.saturating_mul(height),
data.len()
);
let mut output = Vec::with_capacity(filtered_row_size * height);
let mut prev_row: &[u8] = &zero_row;
let mut adaptive_scratch = AdaptiveScratch::new(row_bytes);
let mut last_filter: u8 = FILTER_PAETH; let mut last_adaptive_filter: Option<u8> = None;
let mut filter_counts = [0usize; 5];
for y in 0..height {
let row_start = y * row_bytes;
let row = &data[row_start..row_start + row_bytes];
match strategy {
FilterStrategy::MinSum => {
minsum_filter(
row,
if y == 0 { &zero_row[..] } else { prev_row },
bytes_per_pixel,
&mut output,
&mut adaptive_scratch,
);
if let Some(&f) = output.last() {
last_filter = f;
}
}
FilterStrategy::AdaptiveFast => {
let base = output.len();
filter_row(
row,
if y == 0 { &zero_row[..] } else { prev_row },
bytes_per_pixel,
match last_adaptive_filter {
Some(FILTER_SUB) => FilterStrategy::Sub,
Some(FILTER_UP) => FilterStrategy::Up,
Some(FILTER_PAETH) => FilterStrategy::Paeth,
_ => FilterStrategy::AdaptiveFast,
},
&mut output,
&mut adaptive_scratch,
);
if let Some(&f) = output.get(base) {
last_filter = f;
last_adaptive_filter = Some(f);
}
}
_ => {
let base = output.len();
filter_row(
row,
if y == 0 { &zero_row[..] } else { prev_row },
bytes_per_pixel,
strategy,
&mut output,
&mut adaptive_scratch,
);
if let Some(&f) = output.get(base) {
last_filter = f;
}
}
}
prev_row = row;
if options.verbose_filter_log && last_filter <= FILTER_PAETH {
filter_counts[last_filter as usize] += 1;
}
}
if options.verbose_filter_log {
eprintln!(
"PNG filters: strategy={:?}, rows={} counts={{None:{}, Sub:{}, Up:{}, Avg:{}, Paeth:{}}}",
strategy,
height as u32,
filter_counts[0],
filter_counts[1],
filter_counts[2],
filter_counts[3],
filter_counts[4]
);
}
output
}
fn filter_sub(row: &[u8], bpp: usize, output: &mut Vec<u8>) {
#[cfg(feature = "simd")]
{
simd::filter_sub(row, bpp, output);
}
#[cfg(not(feature = "simd"))]
{
for (i, &byte) in row.iter().enumerate() {
let left = if i >= bpp { row[i - bpp] } else { 0 };
output.push(byte.wrapping_sub(left));
}
}
}
fn filter_up(row: &[u8], prev_row: &[u8], output: &mut Vec<u8>) {
#[cfg(feature = "simd")]
{
simd::filter_up(row, prev_row, output);
}
#[cfg(not(feature = "simd"))]
{
for (i, &byte) in row.iter().enumerate() {
output.push(byte.wrapping_sub(prev_row[i]));
}
}
}
fn filter_average(row: &[u8], prev_row: &[u8], bpp: usize, output: &mut Vec<u8>) {
#[cfg(feature = "simd")]
{
simd::filter_average(row, prev_row, bpp, output);
}
#[cfg(not(feature = "simd"))]
{
for (i, &byte) in row.iter().enumerate() {
let left = if i >= bpp { row[i - bpp] as u16 } else { 0 };
let above = prev_row[i] as u16;
let avg = ((left + above) / 2) as u8;
output.push(byte.wrapping_sub(avg));
}
}
}
fn filter_paeth(row: &[u8], prev_row: &[u8], bpp: usize, output: &mut Vec<u8>) {
#[cfg(feature = "simd")]
{
simd::filter_paeth(row, prev_row, bpp, output);
}
#[cfg(not(feature = "simd"))]
{
for (i, &byte) in row.iter().enumerate() {
let left = if i >= bpp { row[i - bpp] } else { 0 };
let above = prev_row[i];
let upper_left = if i >= bpp { prev_row[i - bpp] } else { 0 };
let predicted = paeth_predictor(left, above, upper_left);
output.push(byte.wrapping_sub(predicted));
}
}
}
#[allow(dead_code)]
#[inline]
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
let a_i = a as i16;
let b_i = b as i16;
let c_i = c as i16;
let p = a_i + b_i - c_i;
let pa = (p - a_i).abs();
let pb = (p - b_i).abs();
let pc = (p - c_i).abs();
if pa <= pb && pa <= pc {
a
} else if pb <= pc {
b
} else {
c
}
}
fn adaptive_filter(
row: &[u8],
prev_row: &[u8],
bpp: usize,
output: &mut Vec<u8>,
scratch: &mut AdaptiveScratch,
) {
scratch.clear();
let mut best_filter = FILTER_NONE;
let mut best_score = u64::MAX;
let early_stop = (row.len() as u64 / 4).saturating_add(1);
scratch.none.extend_from_slice(row);
let score = score_filter(&scratch.none);
if score < best_score {
best_score = score;
best_filter = FILTER_NONE;
if best_score <= early_stop {
output.push(best_filter);
output.extend_from_slice(&scratch.none);
return;
}
}
if best_score == 0 {
output.push(best_filter);
output.extend_from_slice(&scratch.none);
return;
}
filter_sub(row, bpp, &mut scratch.sub);
let score = score_filter(&scratch.sub);
if score < best_score {
best_score = score;
best_filter = FILTER_SUB;
if best_score == 0 || best_score <= early_stop {
output.push(best_filter);
output.extend_from_slice(&scratch.sub);
return;
}
}
filter_up(row, prev_row, &mut scratch.up);
let score = score_filter(&scratch.up);
if score < best_score {
best_score = score;
best_filter = FILTER_UP;
if best_score == 0 || best_score <= early_stop {
output.push(best_filter);
output.extend_from_slice(&scratch.up);
return;
}
}
filter_average(row, prev_row, bpp, &mut scratch.avg);
let score = score_filter(&scratch.avg);
if score < best_score {
best_score = score;
best_filter = FILTER_AVERAGE;
if best_score == 0 || best_score <= early_stop {
output.push(best_filter);
output.extend_from_slice(&scratch.avg);
return;
}
}
filter_paeth(row, prev_row, bpp, &mut scratch.paeth);
let score = score_filter(&scratch.paeth);
if score < best_score {
best_filter = FILTER_PAETH;
}
output.push(best_filter);
match best_filter {
FILTER_NONE => output.extend_from_slice(&scratch.none),
FILTER_SUB => output.extend_from_slice(&scratch.sub),
FILTER_UP => output.extend_from_slice(&scratch.up),
FILTER_AVERAGE => output.extend_from_slice(&scratch.avg),
FILTER_PAETH => output.extend_from_slice(&scratch.paeth),
_ => unreachable!(),
}
}
fn minsum_filter(
row: &[u8],
prev_row: &[u8],
bpp: usize,
output: &mut Vec<u8>,
scratch: &mut AdaptiveScratch,
) {
adaptive_filter(row, prev_row, bpp, output, scratch);
}
fn bigrams_filter(
row: &[u8],
prev_row: &[u8],
bpp: usize,
output: &mut Vec<u8>,
scratch: &mut AdaptiveScratch,
) {
scratch.clear();
let mut best_filter = FILTER_NONE;
let mut best_score = usize::MAX;
scratch.none.extend_from_slice(row);
let score = score_bigrams(&scratch.none);
if score < best_score {
best_score = score;
best_filter = FILTER_NONE;
}
filter_sub(row, bpp, &mut scratch.sub);
let score = score_bigrams(&scratch.sub);
if score < best_score {
best_score = score;
best_filter = FILTER_SUB;
}
filter_up(row, prev_row, &mut scratch.up);
let score = score_bigrams(&scratch.up);
if score < best_score {
best_score = score;
best_filter = FILTER_UP;
}
filter_average(row, prev_row, bpp, &mut scratch.avg);
let score = score_bigrams(&scratch.avg);
if score < best_score {
best_score = score;
best_filter = FILTER_AVERAGE;
}
filter_paeth(row, prev_row, bpp, &mut scratch.paeth);
let score = score_bigrams(&scratch.paeth);
if score < best_score {
best_filter = FILTER_PAETH;
}
output.push(best_filter);
match best_filter {
FILTER_NONE => output.extend_from_slice(&scratch.none),
FILTER_SUB => output.extend_from_slice(&scratch.sub),
FILTER_UP => output.extend_from_slice(&scratch.up),
FILTER_AVERAGE => output.extend_from_slice(&scratch.avg),
FILTER_PAETH => output.extend_from_slice(&scratch.paeth),
_ => unreachable!(),
}
}
fn adaptive_filter_fast(
row: &[u8],
prev_row: &[u8],
bpp: usize,
output: &mut Vec<u8>,
scratch: &mut AdaptiveScratch,
) {
scratch.clear();
filter_sub(row, bpp, &mut scratch.sub);
let mut best_filter = FILTER_SUB;
let mut best_score = score_filter(&scratch.sub);
let early_stop = (row.len() as u64 / 8).saturating_add(1);
if best_score <= early_stop {
output.push(best_filter);
output.extend_from_slice(&scratch.sub);
return;
}
filter_up(row, prev_row, &mut scratch.up);
let up_score = score_filter(&scratch.up);
if up_score < best_score {
best_score = up_score;
best_filter = FILTER_UP;
}
if best_score <= early_stop {
output.push(best_filter);
match best_filter {
FILTER_SUB => output.extend_from_slice(&scratch.sub),
FILTER_UP => output.extend_from_slice(&scratch.up),
_ => {}
}
return;
}
filter_paeth(row, prev_row, bpp, &mut scratch.paeth);
let paeth_score = score_filter(&scratch.paeth);
if paeth_score < best_score {
best_filter = FILTER_PAETH;
}
output.push(best_filter);
match best_filter {
FILTER_SUB => output.extend_from_slice(&scratch.sub),
FILTER_UP => output.extend_from_slice(&scratch.up),
FILTER_PAETH => output.extend_from_slice(&scratch.paeth),
_ => unreachable!(),
}
}
fn filter_row(
row: &[u8],
prev_row: &[u8],
bpp: usize,
strategy: FilterStrategy,
output: &mut Vec<u8>,
scratch: &mut AdaptiveScratch,
) {
match strategy {
FilterStrategy::None => {
output.push(FILTER_NONE);
output.extend_from_slice(row);
}
FilterStrategy::Sub => {
output.push(FILTER_SUB);
filter_sub(row, bpp, output);
}
FilterStrategy::Up => {
output.push(FILTER_UP);
filter_up(row, prev_row, output);
}
FilterStrategy::Average => {
output.push(FILTER_AVERAGE);
filter_average(row, prev_row, bpp, output);
}
FilterStrategy::Paeth => {
output.push(FILTER_PAETH);
filter_paeth(row, prev_row, bpp, output);
}
FilterStrategy::MinSum => {
minsum_filter(row, prev_row, bpp, output, scratch);
}
FilterStrategy::Adaptive => {
adaptive_filter(row, prev_row, bpp, output, scratch);
}
FilterStrategy::AdaptiveFast => {
adaptive_filter_fast(row, prev_row, bpp, output, scratch);
}
FilterStrategy::Bigrams => {
bigrams_filter(row, prev_row, bpp, output, scratch);
}
}
}
#[cfg(feature = "parallel")]
fn apply_filters_parallel(
data: &[u8],
height: usize,
row_bytes: usize,
bpp: usize,
filtered_row_size: usize,
strategy: FilterStrategy,
) -> Vec<u8> {
let zero_row = vec![0u8; row_bytes];
let mut output = vec![0u8; filtered_row_size * height];
output
.par_chunks_mut(filtered_row_size)
.enumerate()
.for_each(|(y, out_row)| {
let row_start = y * row_bytes;
let row = &data[row_start..row_start + row_bytes];
let prev = if y == 0 {
&zero_row[..]
} else {
&data[(y - 1) * row_bytes..y * row_bytes]
};
let mut scratch = AdaptiveScratch::new(row_bytes);
let mut row_buf = Vec::with_capacity(filtered_row_size);
filter_row(row, prev, bpp, strategy, &mut row_buf, &mut scratch);
debug_assert_eq!(
row_buf.len(),
filtered_row_size,
"filtered row size mismatch"
);
out_row.copy_from_slice(&row_buf);
});
output
}
#[inline]
fn score_filter(filtered: &[u8]) -> u64 {
#[cfg(feature = "simd")]
{
simd::score_filter(filtered)
}
#[cfg(not(feature = "simd"))]
{
filtered
.iter()
.map(|&b| (b as i8).unsigned_abs() as u64)
.sum()
}
}
#[inline]
fn score_bigrams(filtered: &[u8]) -> usize {
let mut seen = [false; 65536];
filtered
.windows(2)
.filter(|w| {
let key = (w[0] as usize) << 8 | w[1] as usize;
if seen[key] {
false
} else {
seen[key] = true;
true
}
})
.count()
}
#[allow(dead_code)]
fn is_high_entropy_row(row: &[u8]) -> bool {
if row.len() < 1024 {
return false;
}
let mut equal_neighbors = 0usize;
let mut delta_hist = [0u32; 256];
let mut total_deltas = 0usize;
for w in row.windows(2) {
if w[0] == w[1] {
equal_neighbors += 1;
}
let delta = w[1].wrapping_sub(w[0]);
delta_hist[delta as usize] += 1;
total_deltas += 1;
}
let ratio = equal_neighbors as f32 / (row.len().saturating_sub(1) as f32);
let max_delta = delta_hist.iter().copied().max().unwrap_or(0);
let max_delta_ratio = if total_deltas == 0 {
1.0
} else {
max_delta as f32 / total_deltas as f32
};
ratio < 0.01 && max_delta_ratio < 0.10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_paeth_predictor() {
assert_eq!(paeth_predictor(100, 100, 100), 100);
assert_eq!(paeth_predictor(0, 0, 0), 0);
assert_eq!(paeth_predictor(10, 20, 15), 15);
}
#[test]
fn test_filter_sub() {
let row = vec![10, 20, 30, 40, 50, 60];
let mut output = Vec::new();
filter_sub(&row, 3, &mut output);
assert_eq!(output[0], 10);
assert_eq!(output[1], 20);
assert_eq!(output[2], 30);
assert_eq!(output[3], 40u8.wrapping_sub(10)); assert_eq!(output[4], 50u8.wrapping_sub(20)); assert_eq!(output[5], 60u8.wrapping_sub(30)); }
#[test]
fn test_filter_up() {
let row = vec![50, 60, 70];
let prev = vec![10, 20, 30];
let mut output = Vec::new();
filter_up(&row, &prev, &mut output);
assert_eq!(output[0], 40); assert_eq!(output[1], 40); assert_eq!(output[2], 40); }
#[test]
fn test_apply_filters_none() {
let data = vec![100, 150, 200, 50, 100, 150];
let options = PngOptions {
filter_strategy: FilterStrategy::None,
..Default::default()
};
let filtered = apply_filters(&data, 2, 1, 3, &options);
assert_eq!(filtered[0], FILTER_NONE);
assert_eq!(&filtered[1..], &data[..]);
}
#[test]
fn test_apply_filters_multiple_rows() {
let data = vec![
10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, ];
let options = PngOptions {
filter_strategy: FilterStrategy::None,
..Default::default()
};
let filtered = apply_filters(&data, 2, 2, 3, &options);
assert_eq!(filtered.len(), 2 * (1 + 6)); assert_eq!(filtered[0], FILTER_NONE);
assert_eq!(filtered[7], FILTER_NONE);
}
#[test]
fn test_apply_filters_adaptive_fast() {
let data = vec![
10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, ];
let options = PngOptions {
filter_strategy: FilterStrategy::AdaptiveFast,
..Default::default()
};
let filtered = apply_filters(&data, 2, 2, 3, &options);
assert_eq!(filtered.len(), 2 * (1 + 6));
assert!(matches!(filtered[0], FILTER_SUB | FILTER_UP | FILTER_PAETH));
assert!(matches!(filtered[7], FILTER_SUB | FILTER_UP | FILTER_PAETH));
}
#[test]
fn test_filter_average() {
let row = vec![100, 100, 100];
let prev = vec![50, 50, 50];
let mut output = Vec::new();
filter_average(&row, &prev, 1, &mut output);
assert_eq!(output[0], 100u8.wrapping_sub(25)); assert_eq!(output[1], 100u8.wrapping_sub(75)); assert_eq!(output[2], 100u8.wrapping_sub(75)); }
#[test]
fn test_filter_paeth() {
let row = vec![100, 100, 100];
let prev = vec![50, 50, 50];
let mut output = Vec::new();
filter_paeth(&row, &prev, 1, &mut output);
assert_eq!(output[0], 100u8.wrapping_sub(50));
assert_eq!(output.len(), 3);
}
#[test]
fn test_score_filter_all_zeros() {
let data = vec![0u8; 100];
let score = score_filter(&data);
assert_eq!(score, 0);
}
#[test]
fn test_score_filter_high_values() {
let data = vec![0x80u8; 10];
let score = score_filter(&data);
assert_eq!(score, 128 * 10);
}
#[test]
fn test_score_filter_mixed() {
let data = vec![1, 0xFF, 2, 0xFE]; let score = score_filter(&data);
assert_eq!(score, 6);
}
#[test]
fn test_score_bigrams_all_same() {
let data = vec![42u8; 100];
let score = score_bigrams(&data);
assert_eq!(score, 1); }
#[test]
fn test_score_bigrams_all_unique() {
let data: Vec<u8> = (0..10).collect();
let score = score_bigrams(&data);
assert_eq!(score, 9);
}
#[test]
fn test_score_bigrams_repeating_pattern() {
let data = vec![1, 2, 1, 2, 1, 2, 1, 2];
let score = score_bigrams(&data);
assert_eq!(score, 2);
}
#[test]
fn test_score_bigrams_single_byte() {
let data = vec![42u8];
let score = score_bigrams(&data);
assert_eq!(score, 0);
}
#[test]
fn test_score_bigrams_empty() {
let data: Vec<u8> = vec![];
let score = score_bigrams(&data);
assert_eq!(score, 0);
}
#[test]
fn test_is_high_entropy_row_short() {
let row = vec![0u8; 100];
assert!(!is_high_entropy_row(&row));
}
#[test]
fn test_is_high_entropy_row_uniform() {
let row = vec![42u8; 2000];
assert!(!is_high_entropy_row(&row));
}
#[test]
fn test_is_high_entropy_row_gradient() {
let row: Vec<u8> = (0..2000).map(|i| (i % 256) as u8).collect();
assert!(!is_high_entropy_row(&row));
}
#[test]
fn test_paeth_predictor_edge_cases() {
assert_eq!(paeth_predictor(100, 0, 0), 100);
assert_eq!(paeth_predictor(0, 100, 0), 100);
assert_eq!(paeth_predictor(100, 100, 100), 100);
assert_eq!(paeth_predictor(255, 0, 0), 255);
assert_eq!(paeth_predictor(0, 255, 0), 255);
}
#[test]
fn test_paeth_predictor_tie_breaking() {
assert_eq!(paeth_predictor(100, 100, 100), 100);
assert_eq!(paeth_predictor(50, 100, 75), 75);
}
#[test]
fn test_adaptive_scratch_reuse() {
let mut scratch = AdaptiveScratch::new(100);
scratch.none.extend_from_slice(&[1, 2, 3]);
scratch.sub.extend_from_slice(&[4, 5, 6]);
assert_eq!(scratch.none.len(), 3);
assert_eq!(scratch.sub.len(), 3);
scratch.clear();
assert_eq!(scratch.none.len(), 0);
assert_eq!(scratch.sub.len(), 0);
}
#[test]
fn test_filter_sub_bpp_variations() {
for bpp in 1..=4 {
let row: Vec<u8> = (0..20).collect();
let mut output = Vec::new();
filter_sub(&row, bpp, &mut output);
assert_eq!(output.len(), row.len());
for i in 0..bpp {
assert_eq!(output[i], row[i]);
}
}
}
#[test]
fn test_filter_up_first_row() {
let row = vec![10, 20, 30, 40];
let zero_row = vec![0u8; 4];
let mut output = Vec::new();
filter_up(&row, &zero_row, &mut output);
assert_eq!(output, row);
}
#[test]
fn test_apply_filters_sub_strategy() {
let data = vec![10, 20, 30, 40, 50, 60]; let options = PngOptions {
filter_strategy: FilterStrategy::Sub,
..Default::default()
};
let filtered = apply_filters(&data, 2, 1, 3, &options);
assert_eq!(filtered[0], FILTER_SUB);
assert_eq!(filtered.len(), 1 + 6); }
#[test]
fn test_apply_filters_up_strategy() {
let data = vec![
10, 20, 30, 50, 60, 70, ];
let options = PngOptions {
filter_strategy: FilterStrategy::Up,
..Default::default()
};
let filtered = apply_filters(&data, 1, 2, 3, &options);
assert_eq!(filtered[0], FILTER_UP);
assert_eq!(filtered[4], FILTER_UP);
}
#[test]
fn test_apply_filters_average_strategy() {
let data = vec![100, 100, 100];
let options = PngOptions {
filter_strategy: FilterStrategy::Average,
..Default::default()
};
let filtered = apply_filters(&data, 1, 1, 3, &options);
assert_eq!(filtered[0], FILTER_AVERAGE);
}
#[test]
fn test_apply_filters_paeth_strategy() {
let data = vec![100, 100, 100];
let options = PngOptions {
filter_strategy: FilterStrategy::Paeth,
..Default::default()
};
let filtered = apply_filters(&data, 1, 1, 3, &options);
assert_eq!(filtered[0], FILTER_PAETH);
}
#[test]
fn test_apply_filters_minsum_strategy() {
let data = vec![0u8; 100]; let options = PngOptions {
filter_strategy: FilterStrategy::MinSum,
..Default::default()
};
let filtered = apply_filters(&data, 10, 1, 10, &options);
assert_eq!(filtered.len(), 1 + 100); }
#[test]
fn test_apply_filters_bigrams_strategy() {
let width = 100;
let height = 50;
let bytes_per_pixel = 3;
let row_bytes = width * bytes_per_pixel;
let data: Vec<u8> = (0..(width * height * bytes_per_pixel))
.map(|i| (i % 256) as u8)
.collect();
let options = PngOptions {
filter_strategy: FilterStrategy::Bigrams,
..Default::default()
};
let filtered = apply_filters(
&data,
width as u32,
height as u32,
bytes_per_pixel,
&options,
);
assert_eq!(filtered.len(), height * (1 + row_bytes));
assert!(filtered[0] <= 4); }
#[test]
fn test_apply_filters_adaptive_strategy() {
let data: Vec<u8> = (0..200).map(|i| (i % 256) as u8).collect();
let options = PngOptions {
filter_strategy: FilterStrategy::Adaptive,
..Default::default()
};
let filtered = apply_filters(&data, 10, 2, 10, &options);
assert_eq!(filtered.len(), 2 * (1 + 100)); }
#[test]
fn test_filter_wrapping() {
let row = vec![5, 10, 15];
let prev = vec![10, 20, 30];
let mut output = Vec::new();
filter_up(&row, &prev, &mut output);
assert_eq!(output[0], 5u8.wrapping_sub(10));
assert_eq!(output[1], 10u8.wrapping_sub(20));
}
#[test]
fn test_small_image_uses_sub() {
let data = vec![0u8; 64 * 3]; let options = PngOptions {
filter_strategy: FilterStrategy::Adaptive,
..Default::default()
};
let filtered = apply_filters(&data, 8, 8, 3, &options);
assert_eq!(filtered[0], FILTER_SUB);
}
#[test]
fn test_verbose_filter_log_does_not_panic() {
let data: Vec<u8> = (0..100).map(|i| (i % 256) as u8).collect();
let options = PngOptions {
filter_strategy: FilterStrategy::Adaptive,
verbose_filter_log: true,
..Default::default()
};
let filtered = apply_filters(&data, 10, 1, 10, &options);
assert_eq!(filtered.len(), 1 + 100);
}
#[test]
fn test_verbose_filter_log_with_multiple_rows() {
let data: Vec<u8> = (0..500).map(|i| (i % 256) as u8).collect();
let options = PngOptions {
filter_strategy: FilterStrategy::MinSum,
verbose_filter_log: true,
..Default::default()
};
let filtered = apply_filters(&data, 10, 5, 10, &options);
assert_eq!(filtered.len(), 5 * (1 + 100));
}
#[test]
fn test_apply_filters_parallel_large_image() {
let width = 100;
let height = 64; let bytes_per_pixel = 3;
let data: Vec<u8> = (0..(width * height * bytes_per_pixel))
.map(|i| (i % 256) as u8)
.collect();
let options = PngOptions {
filter_strategy: FilterStrategy::Adaptive,
..Default::default()
};
let filtered = apply_filters(
&data,
width as u32,
height as u32,
bytes_per_pixel,
&options,
);
let row_bytes = width * bytes_per_pixel;
assert_eq!(filtered.len(), height * (1 + row_bytes));
}
#[test]
fn test_apply_filters_parallel_bigrams() {
let width = 100;
let height = 64;
let bytes_per_pixel = 4; let data: Vec<u8> = (0..(width * height * bytes_per_pixel))
.map(|i| (i % 256) as u8)
.collect();
let options = PngOptions {
filter_strategy: FilterStrategy::Bigrams,
..Default::default()
};
let filtered = apply_filters(
&data,
width as u32,
height as u32,
bytes_per_pixel,
&options,
);
let row_bytes = width * bytes_per_pixel;
assert_eq!(filtered.len(), height * (1 + row_bytes));
}
#[test]
fn test_apply_filters_parallel_adaptive_fast() {
let width = 100;
let height = 64;
let bytes_per_pixel = 3;
let data: Vec<u8> = (0..(width * height * bytes_per_pixel))
.map(|i| (i % 256) as u8)
.collect();
let options = PngOptions {
filter_strategy: FilterStrategy::AdaptiveFast,
..Default::default()
};
let filtered = apply_filters(
&data,
width as u32,
height as u32,
bytes_per_pixel,
&options,
);
let row_bytes = width * bytes_per_pixel;
assert_eq!(filtered.len(), height * (1 + row_bytes));
}
#[test]
fn test_filter_paeth_predictor_edge_cases() {
let row = vec![100, 50, 25, 75];
let prev = vec![50, 100, 75, 25];
let mut output = Vec::new();
filter_paeth(&row, &prev, 1, &mut output);
assert_eq!(output.len(), 4);
}
#[test]
fn test_filter_average_multi_bpp() {
let row = vec![10, 20, 30, 40, 50, 60]; let prev = vec![20, 40, 60, 80, 100, 120];
let mut output = Vec::new();
filter_average(&row, &prev, 3, &mut output);
assert_eq!(output.len(), 6);
}
#[test]
fn test_score_filter_all_types() {
let row = vec![100, 110, 120, 130, 140];
let prev = vec![50, 60, 70, 80, 90];
let score_none = score_filter(&row);
let score_sub = {
let mut out = Vec::new();
filter_sub(&row, 1, &mut out);
score_filter(&out)
};
let score_up = {
let mut out = Vec::new();
filter_up(&row, &prev, &mut out);
score_filter(&out)
};
let score_avg = {
let mut out = Vec::new();
filter_average(&row, &prev, 1, &mut out);
score_filter(&out)
};
let score_paeth = {
let mut out = Vec::new();
filter_paeth(&row, &prev, 1, &mut out);
score_filter(&out)
};
assert!(
score_none > 0 || score_sub > 0 || score_up > 0 || score_avg > 0 || score_paeth > 0
);
}
#[test]
fn test_filter_strategies_produce_different_results() {
let width = 50;
let height = 50;
let bpp = 3;
let data: Vec<u8> = (0..(width * height * bpp))
.map(|i| ((i * 7) % 256) as u8)
.collect();
let none_opts = PngOptions {
filter_strategy: FilterStrategy::None,
..Default::default()
};
let sub_opts = PngOptions {
filter_strategy: FilterStrategy::Sub,
..Default::default()
};
let filtered_none = apply_filters(&data, width as u32, height as u32, bpp, &none_opts);
let filtered_sub = apply_filters(&data, width as u32, height as u32, bpp, &sub_opts);
assert_eq!(filtered_none.len(), filtered_sub.len());
assert_eq!(filtered_none[0], FILTER_NONE);
assert_eq!(filtered_sub[0], FILTER_SUB);
}
#[test]
fn test_adaptive_scratch_clear_and_reuse() {
let row_len = 100;
let mut scratch = AdaptiveScratch::new(row_len);
scratch.none.extend_from_slice(&[0u8; 50]);
scratch.sub.extend_from_slice(&[1u8; 50]);
scratch.avg.extend_from_slice(&[3u8; 25]);
scratch.paeth.extend_from_slice(&[4u8; 25]);
scratch.clear();
assert!(scratch.none.is_empty());
assert!(scratch.sub.is_empty());
assert!(scratch.up.is_empty());
assert!(scratch.avg.is_empty());
assert!(scratch.paeth.is_empty());
scratch.up.extend_from_slice(&[2u8; 100]);
assert_eq!(scratch.up.len(), 100);
}
}