use crate::core::pix::RgbComponent;
use crate::core::{Pix, PixMut, PixelDepth, pixel};
use crate::filter::{FilterError, FilterResult, Kernel};
const ENHANCE_SCALE_FACTOR: f64 = 5.0;
pub type TrcLut = [u8; 256];
pub fn gamma_trc(gamma: f32, minval: i32, maxval: i32) -> FilterResult<TrcLut> {
if minval >= maxval {
return Err(FilterError::InvalidParameters(
"minval must be less than maxval".into(),
));
}
if gamma <= 0.0 {
return Err(FilterError::InvalidParameters("gamma must be > 0.0".into()));
}
let inv_gamma = 1.0_f32 / gamma;
let range = (maxval - minval) as f32;
let mut lut = [0u8; 256];
for i in 0..256i32 {
let val = if i < minval {
0
} else if i > maxval {
255
} else {
let x = (i - minval) as f32 / range;
let mapped = 255.0 * x.powf(inv_gamma) + 0.5;
(mapped as i32).clamp(0, 255)
};
lut[i as usize] = val as u8;
}
Ok(lut)
}
pub fn contrast_trc(factor: f32) -> FilterResult<TrcLut> {
if factor < 0.0 {
return Err(FilterError::InvalidParameters(
"factor must be >= 0.0".into(),
));
}
let mut lut = [0u8; 256];
if factor == 0.0 {
for (i, entry) in lut.iter_mut().enumerate() {
*entry = i as u8;
}
return Ok(lut);
}
let scale = ENHANCE_SCALE_FACTOR;
let factor_d = factor as f64;
let ymax = (1.0 * factor_d * scale).atan();
let ymin = (-127.0 * factor_d * scale / 128.0).atan();
let dely = ymax - ymin;
for (i, entry) in lut.iter_mut().enumerate() {
let x = i as f64;
let val = (255.0 / dely) * (-ymin + (factor_d * scale * (x - 127.0) / 128.0).atan()) + 0.5;
*entry = (val as i32).clamp(0, 255) as u8;
}
Ok(lut)
}
pub fn equalize_trc(pix: &Pix, fract: f32, factor: u32) -> FilterResult<TrcLut> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8 bpp",
actual: pix.depth().bits(),
});
}
if !(0.0..=1.0).contains(&fract) {
return Err(FilterError::InvalidParameters(
"fract must be in [0.0, 1.0]".into(),
));
}
if factor < 1 {
return Err(FilterError::InvalidParameters("factor must be >= 1".into()));
}
let hist = pix.gray_histogram(factor)?;
let sum: f32 = hist.sum().unwrap_or(0.0);
if sum == 0.0 || !sum.is_normal() {
let mut lut = [0u8; 256];
for (i, entry) in lut.iter_mut().enumerate() {
*entry = i as u8;
}
return Ok(lut);
}
let partial = hist.partial_sums();
let mut lut = [0u8; 256];
for (iin, entry) in lut.iter_mut().enumerate() {
let cumul = partial.get(iin).unwrap_or(0.0);
let itarg = (255.0 * cumul / sum + 0.5) as i32;
let iout = iin as i32 + (fract * (itarg - iin as i32) as f32) as i32;
*entry = iout.clamp(0, 255) as u8;
}
Ok(lut)
}
pub fn trc_map(pix: &mut PixMut, mask: Option<&Pix>, lut: &TrcLut) -> FilterResult<()> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
if let Some(m) = mask
&& m.depth() != PixelDepth::Bit1
{
return Err(FilterError::UnsupportedDepth {
expected: "1 bpp mask",
actual: m.depth().bits(),
});
}
let w = pix.width();
let h = pix.height();
match (d, mask) {
(PixelDepth::Bit8, None) => {
for y in 0..h {
for x in 0..w {
let val = pix.get_pixel_unchecked(x, y) as u8;
pix.set_pixel_unchecked(x, y, lut[val as usize] as u32);
}
}
}
(PixelDepth::Bit32, None) => {
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, _) = pixel::extract_rgba(pixel);
let nr = lut[r as usize];
let ng = lut[g as usize];
let nb = lut[b as usize];
pix.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
}
(PixelDepth::Bit8, Some(m)) => {
let mw = m.width();
let mh = m.height();
for y in 0..h.min(mh) {
for x in 0..w.min(mw) {
if m.get_pixel_unchecked(x, y) == 0 {
continue;
}
let val = pix.get_pixel_unchecked(x, y) as u8;
pix.set_pixel_unchecked(x, y, lut[val as usize] as u32);
}
}
}
(PixelDepth::Bit32, Some(m)) => {
let mw = m.width();
let mh = m.height();
for y in 0..h.min(mh) {
for x in 0..w.min(mw) {
if m.get_pixel_unchecked(x, y) == 0 {
continue;
}
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, _) = pixel::extract_rgba(pixel);
let nr = lut[r as usize];
let ng = lut[g as usize];
let nb = lut[b as usize];
pix.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
}
_ => unreachable!(),
}
Ok(())
}
pub fn trc_map_general(
pix: &mut PixMut,
mask: Option<&Pix>,
lut_r: &TrcLut,
lut_g: &TrcLut,
lut_b: &TrcLut,
) -> FilterResult<()> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if let Some(m) = mask
&& m.depth() != PixelDepth::Bit1
{
return Err(FilterError::UnsupportedDepth {
expected: "1 bpp mask",
actual: m.depth().bits(),
});
}
let w = pix.width();
let h = pix.height();
match mask {
None => {
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, _) = pixel::extract_rgba(pixel);
let nr = lut_r[r as usize];
let ng = lut_g[g as usize];
let nb = lut_b[b as usize];
pix.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
}
Some(m) => {
let mw = m.width();
let mh = m.height();
for y in 0..h.min(mh) {
for x in 0..w.min(mw) {
if m.get_pixel_unchecked(x, y) == 0 {
continue;
}
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, _) = pixel::extract_rgba(pixel);
let nr = lut_r[r as usize];
let ng = lut_g[g as usize];
let nb = lut_b[b as usize];
pix.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
}
}
Ok(())
}
pub fn gamma_trc_pix(pix: &Pix, gamma: f32, minval: i32, maxval: i32) -> FilterResult<Pix> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
let lut = gamma_trc(gamma, minval, maxval)?;
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
trc_map(&mut pm, None, &lut)?;
Ok(pm.into())
}
pub fn gamma_trc_masked(
pix: &Pix,
mask: Option<&Pix>,
gamma: f32,
minval: i32,
maxval: i32,
) -> FilterResult<Pix> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
let lut = gamma_trc(gamma, minval, maxval)?;
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
trc_map(&mut pm, mask, &lut)?;
Ok(pm.into())
}
pub fn gamma_trc_with_alpha(pix: &Pix, gamma: f32, minval: i32, maxval: i32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
let alpha = pix.get_rgb_component(RgbComponent::Alpha)?;
let lut = gamma_trc(gamma, minval, maxval)?;
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
trc_map(&mut pm, None, &lut)?;
pm.set_rgb_component(&alpha, RgbComponent::Alpha)?;
Ok(pm.into())
}
pub fn contrast_trc_pix(pix: &Pix, factor: f32) -> FilterResult<Pix> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
let lut = contrast_trc(factor)?;
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
trc_map(&mut pm, None, &lut)?;
Ok(pm.into())
}
pub fn contrast_trc_masked(pix: &Pix, mask: Option<&Pix>, factor: f32) -> FilterResult<Pix> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
let lut = contrast_trc(factor)?;
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
trc_map(&mut pm, mask, &lut)?;
Ok(pm.into())
}
pub fn equalize_trc_pix(pix: &Pix, fract: f32, factor: u32) -> FilterResult<Pix> {
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
if d == PixelDepth::Bit8 {
let lut = equalize_trc(pix, fract, factor)?;
trc_map(&mut pm, None, &lut)?;
} else {
let pix_r = pix.get_rgb_component(RgbComponent::Red)?;
let pix_g = pix.get_rgb_component(RgbComponent::Green)?;
let pix_b = pix.get_rgb_component(RgbComponent::Blue)?;
let lut_r = equalize_trc(&pix_r, fract, factor)?;
let lut_g = equalize_trc(&pix_g, fract, factor)?;
let lut_b = equalize_trc(&pix_b, fract, factor)?;
trc_map_general(&mut pm, None, &lut_r, &lut_g, &lut_b)?;
}
Ok(pm.into())
}
pub fn modify_hue(pix: &Pix, fract: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if fract.abs() > 1.0 {
return Err(FilterError::InvalidParameters(
"fract must be in [-1.0, 1.0]".into(),
));
}
let delhue = (240.0 * fract) as i32;
if delhue == 0 || delhue == 240 || delhue == -240 {
return Ok(pix.deep_clone());
}
let delhue = if delhue < 0 { delhue + 240 } else { delhue };
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let mut hsv = pixel::rgb_to_hsv(r, g, b);
hsv.h = (hsv.h + delhue) % 240;
let (nr, ng, nb) = pixel::hsv_to_rgb(hsv);
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(pm.into())
}
pub fn modify_saturation(pix: &Pix, fract: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if fract.abs() > 1.0 {
return Err(FilterError::InvalidParameters(
"fract must be in [-1.0, 1.0]".into(),
));
}
if fract == 0.0 {
return Ok(pix.deep_clone());
}
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let mut hsv = pixel::rgb_to_hsv(r, g, b);
if fract < 0.0 {
hsv.s = (hsv.s as f32 * (1.0 + fract)) as i32;
} else {
hsv.s = (hsv.s as f32 + fract * (255.0 - hsv.s as f32)) as i32;
}
hsv.s = hsv.s.clamp(0, 255);
let (nr, ng, nb) = pixel::hsv_to_rgb(hsv);
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(pm.into())
}
pub fn modify_brightness(pix: &Pix, fract: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if fract.abs() > 1.0 {
return Err(FilterError::InvalidParameters(
"fract must be in [-1.0, 1.0]".into(),
));
}
if fract == 0.0 {
return Ok(pix.deep_clone());
}
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let mut hsv = pixel::rgb_to_hsv(r, g, b);
if fract > 0.0 {
hsv.v = (hsv.v as f32 + fract * (255.0 - hsv.v as f32)) as i32;
} else {
hsv.v = (hsv.v as f32 * (1.0 + fract)) as i32;
}
hsv.v = hsv.v.clamp(0, 255);
let (nr, ng, nb) = pixel::hsv_to_rgb(hsv);
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(pm.into())
}
pub fn measure_saturation(pix: &Pix, factor: u32) -> FilterResult<f32> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if factor < 1 {
return Err(FilterError::InvalidParameters("factor must be >= 1".into()));
}
let w = pix.width();
let h = pix.height();
let mut sum: i64 = 0;
let mut count: i64 = 0;
let mut y = 0;
while y < h {
let mut x = 0;
while x < w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let hsv = pixel::rgb_to_hsv(r, g, b);
sum += hsv.s as i64;
count += 1;
x += factor;
}
y += factor;
}
if count > 0 {
Ok(sum as f32 / count as f32)
} else {
Ok(0.0)
}
}
pub fn color_shift_rgb(pix: &Pix, rfract: f32, gfract: f32, bfract: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
for (name, fract) in [("rfract", rfract), ("gfract", gfract), ("bfract", bfract)] {
if !(-1.0..=1.0).contains(&fract) {
return Err(FilterError::InvalidParameters(format!(
"{name} must be in [-1.0, 1.0]"
)));
}
}
if rfract == 0.0 && gfract == 0.0 && bfract == 0.0 {
return Ok(pix.deep_clone());
}
let build_lut = |fract: f32| -> [u8; 256] {
let mut lut = [0u8; 256];
for (i, entry) in lut.iter_mut().enumerate() {
let val = i as f32;
let out = if fract >= 0.0 {
val + (255.0 - val) * fract
} else {
val * (1.0 + fract)
};
*entry = out.round().clamp(0.0, 255.0) as u8;
}
lut
};
let rlut = build_lut(rfract);
let glut = build_lut(gfract);
let blut = build_lut(bfract);
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
pm.set_pixel_unchecked(
x,
y,
pixel::compose_rgb(rlut[r as usize], glut[g as usize], blut[b as usize]),
);
}
}
Ok(pm.into())
}
pub fn darken_gray(pix: &Pix, thresh: u32, satlimit: u32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if thresh > 255 {
return Err(FilterError::InvalidParameters(
"thresh must be in [0, 255]".into(),
));
}
if satlimit < 1 {
return Err(FilterError::InvalidParameters(
"satlimit must be >= 1".into(),
));
}
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let ri = r as i32;
let gi = g as i32;
let bi = b as i32;
let min = ri.min(gi).min(bi);
let max = ri.max(gi).max(bi);
let sat = max - min;
if max >= thresh as i32 || sat >= satlimit as i32 {
continue;
}
let ratio = sat as f32 / satlimit as f32;
let nr = (ri as f32 * ratio) as u8;
let ng = (gi as f32 * ratio) as u8;
let nb = (bi as f32 * ratio) as u8;
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(pm.into())
}
pub fn mult_constant_color(pix: &Pix, rfact: f32, gfact: f32, bfact: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
for (name, fact) in [("rfact", rfact), ("gfact", gfact), ("bfact", bfact)] {
if fact < 0.0 {
return Err(FilterError::InvalidParameters(format!(
"{name} must be >= 0.0"
)));
}
}
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let nr = (r as f32 * rfact).round().min(255.0) as u8;
let ng = (g as f32 * gfact).round().min(255.0) as u8;
let nb = (b as f32 * bfact).round().min(255.0) as u8;
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(pm.into())
}
pub fn mult_matrix_color(pix: &Pix, kel: &Kernel) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if kel.width() != 3 || kel.height() != 3 {
return Err(FilterError::InvalidParameters("kernel must be 3x3".into()));
}
let mut v = [0.0f32; 9];
for row in 0..3u32 {
for col in 0..3u32 {
v[(row * 3 + col) as usize] = kel.get(col, row).unwrap_or(0.0);
}
}
let cloned = pix.deep_clone();
let mut pm = cloned.try_into_mut().unwrap();
let w = pm.width();
let h = pm.height();
for y in 0..h {
for x in 0..w {
let pixel = pm.get_pixel_unchecked(x, y);
let (r, g, b) = pixel::extract_rgb(pixel);
let rf = r as f32;
let gf = g as f32;
let bf = b as f32;
let nr = (v[0] * rf + v[1] * gf + v[2] * bf)
.round()
.clamp(0.0, 255.0) as u8;
let ng = (v[3] * rf + v[4] * gf + v[5] * bf)
.round()
.clamp(0.0, 255.0) as u8;
let nb = (v[6] * rf + v[7] * gf + v[8] * bf)
.round()
.clamp(0.0, 255.0) as u8;
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(nr, ng, nb));
}
}
Ok(pm.into())
}
pub fn unsharp_masking_gray(pix: &Pix, halfwidth: u32, fract: f32) -> FilterResult<Pix> {
if pix.depth() != PixelDepth::Bit8 {
return Err(FilterError::UnsupportedDepth {
expected: "8bpp",
actual: pix.depth().bits(),
});
}
if pix.colormap().is_some() {
return Err(FilterError::InvalidParameters(
"colormapped images not supported".into(),
));
}
if fract <= 0.0 || halfwidth == 0 {
return Ok(pix.deep_clone());
}
if halfwidth <= 2 {
use crate::filter::edge::unsharp_masking_gray_fast;
return unsharp_masking_gray_fast(pix, halfwidth, fract);
}
let blurred = crate::filter::block_conv::blockconv_gray(pix, None, halfwidth, halfwidth)?;
let w = pix.width();
let h = pix.height();
let out = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let src = pix.get_pixel_unchecked(x, y) as f32;
let blur = blurred.get_pixel_unchecked(x, y) as f32;
let result = (src + fract * (src - blur) + 0.5) as i32;
out_mut.set_pixel_unchecked(x, y, result.clamp(0, 255) as u32);
}
}
Ok(out_mut.into())
}
pub fn half_edge_by_bandpass(
pix: &Pix,
sm1h: u32,
sm1v: u32,
sm2h: u32,
sm2v: u32,
) -> FilterResult<Pix> {
if sm1h == sm2h && sm1v == sm2v {
return Err(FilterError::InvalidParameters(
"sm1 and sm2 must differ".to_string(),
));
}
let d = pix.depth();
if d != PixelDepth::Bit8 && d != PixelDepth::Bit32 {
return Err(FilterError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: d.bits(),
});
}
if d == PixelDepth::Bit8 && pix.has_colormap() {
return Err(FilterError::InvalidParameters(
"8bpp input must not have a colormap".to_string(),
));
}
let pixg = if d == PixelDepth::Bit32 {
pix.convert_rgb_to_luminance()?
} else {
pix.clone()
};
let pixacc = crate::filter::blockconv_accum(&pixg)?;
let pixc1 = crate::filter::blockconv_gray(&pixg, Some(&pixacc), sm1h, sm1v)?;
let pixc2 = crate::filter::blockconv_gray(&pixg, Some(&pixacc), sm2h, sm2v)?;
let w = pixc1.width();
let h = pixc1.height();
let out_pix = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let v1 = pixc1.get_pixel_unchecked(x, y) as i32;
let v2 = pixc2.get_pixel_unchecked(x, y) as i32;
let diff = (v1 - v2).max(0) as u32;
out_mut.set_pixel_unchecked(x, y, diff);
}
}
Ok(out_mut.into())
}
pub fn unsharp_masking(pix: &Pix, halfwidth: u32, fract: f32) -> FilterResult<Pix> {
if pix.depth() == PixelDepth::Bit1 {
return Err(FilterError::UnsupportedDepth {
expected: "not 1bpp",
actual: 1,
});
}
if fract <= 0.0 || halfwidth == 0 {
return Ok(pix.deep_clone());
}
if halfwidth <= 2 {
use crate::filter::edge::unsharp_masking_fast;
return unsharp_masking_fast(pix, halfwidth, fract);
}
match pix.depth() {
PixelDepth::Bit8 => unsharp_masking_gray(pix, halfwidth, fract),
PixelDepth::Bit32 => {
use crate::core::pix::RgbComponent;
let pix_r = pix.get_rgb_component(RgbComponent::Red)?;
let pix_g = pix.get_rgb_component(RgbComponent::Green)?;
let pix_b = pix.get_rgb_component(RgbComponent::Blue)?;
let res_r = unsharp_masking_gray(&pix_r, halfwidth, fract)?;
let res_g = unsharp_masking_gray(&pix_g, halfwidth, fract)?;
let res_b = unsharp_masking_gray(&pix_b, halfwidth, fract)?;
let mut result = Pix::create_rgb_image(&res_r, &res_g, &res_b)?;
if pix.spp() == 4 {
let pix_a = pix.get_rgb_component(RgbComponent::Alpha)?;
let mut result_mut = result.try_into_mut().unwrap();
result_mut.set_rgb_component(&pix_a, RgbComponent::Alpha)?;
result = result_mut.into();
}
Ok(result)
}
_ => {
let converted = pix.convert_to_8_or_32()?;
unsharp_masking(&converted, halfwidth, fract)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DynamicRangeScale {
Linear,
Log,
}
pub fn max_dynamic_range(pix: &Pix, scale: DynamicRangeScale) -> FilterResult<Pix> {
let depth = pix.depth();
if !matches!(
depth,
PixelDepth::Bit4 | PixelDepth::Bit8 | PixelDepth::Bit16 | PixelDepth::Bit32
) {
return Err(FilterError::UnsupportedDepth {
expected: "4, 8, 16, or 32 bpp",
actual: depth.bits(),
});
}
let w = pix.width();
let h = pix.height();
let mut max: u32 = 0;
for y in 0..h {
for x in 0..w {
let v = pix.get_pixel_unchecked(x, y);
if v > max {
max = v;
}
}
}
let pixd = Pix::new(w, h, PixelDepth::Bit8).map_err(FilterError::Core)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
if max == 0 {
return Ok(pixd_mut.into());
}
match scale {
DynamicRangeScale::Linear => {
let factor = 255.0f32 / max as f32;
for y in 0..h {
for x in 0..w {
let sval = pix.get_pixel_unchecked(x, y);
let dval = ((factor * sval as f32 + 0.5) as u32).min(255);
pixd_mut.set_pixel_unchecked(x, y, dval);
}
}
}
DynamicRangeScale::Log => {
let log_max = log_base2(max);
if log_max <= 0.0 {
for y in 0..h {
for x in 0..w {
let sval = pix.get_pixel_unchecked(x, y);
pixd_mut.set_pixel_unchecked(x, y, if sval == 0 { 0 } else { 255 });
}
}
} else {
let factor = 255.0f32 / log_max;
for y in 0..h {
for x in 0..w {
let sval = pix.get_pixel_unchecked(x, y);
let dval = ((factor * log_base2(sval) + 0.5) as u32).min(255);
pixd_mut.set_pixel_unchecked(x, y, dval);
}
}
}
}
}
Ok(pixd_mut.into())
}
#[inline]
fn log_base2(val: u32) -> f32 {
if val == 0 {
return 0.0;
}
if val < 0x100 {
(val as f32).log2()
} else if val < 0x1_0000 {
8.0 + ((val >> 8) as f32).log2()
} else if val < 0x100_0000 {
16.0 + ((val >> 16) as f32).log2()
} else {
24.0 + ((val >> 24) as f32).log2()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Pix;
#[test]
fn test_gamma_trc_identity() {
let lut = gamma_trc(1.0, 0, 255).unwrap();
for (i, &val) in lut.iter().enumerate() {
assert_eq!(val, i as u8, "identity mismatch at {}", i);
}
}
#[test]
fn test_gamma_trc_lighten() {
let lut = gamma_trc(2.0, 0, 255).unwrap();
assert_eq!(lut[0], 0);
assert_eq!(lut[255], 255);
assert!(lut[128] > 128, "expected > 128, got {}", lut[128]);
}
#[test]
fn test_gamma_trc_darken() {
let lut = gamma_trc(0.5, 0, 255).unwrap();
assert_eq!(lut[0], 0);
assert_eq!(lut[255], 255);
assert!(lut[128] < 128, "expected < 128, got {}", lut[128]);
}
#[test]
fn test_gamma_trc_custom_range() {
let lut = gamma_trc(1.0, 50, 200).unwrap();
assert_eq!(lut[0], 0);
assert_eq!(lut[49], 0);
assert_eq!(lut[200], 255);
assert_eq!(lut[255], 255);
assert!((lut[125] as i32 - 128).abs() <= 2, "got {}", lut[125]);
}
#[test]
fn test_gamma_trc_invalid_params() {
assert!(gamma_trc(1.0, 200, 100).is_err()); assert!(gamma_trc(0.0, 0, 255).is_err()); assert!(gamma_trc(-1.0, 0, 255).is_err()); }
#[test]
fn test_contrast_trc_zero_factor() {
let lut = contrast_trc(0.0).unwrap();
for (i, &val) in lut.iter().enumerate() {
assert_eq!(val, i as u8, "identity mismatch at {}", i);
}
}
#[test]
fn test_contrast_trc_enhancement() {
let lut = contrast_trc(0.5).unwrap();
assert_eq!(lut[0], 0);
assert_eq!(lut[255], 255);
assert!((lut[127] as i32 - 127).abs() <= 2, "got {}", lut[127]);
assert!(lut[64] < 64, "expected < 64, got {}", lut[64]);
assert!(lut[192] > 192, "expected > 192, got {}", lut[192]);
}
#[test]
fn test_contrast_trc_monotonic() {
let lut = contrast_trc(0.8).unwrap();
for i in 1..256 {
assert!(lut[i] >= lut[i - 1], "not monotonic at {}", i);
}
}
#[test]
fn test_contrast_trc_invalid_factor() {
assert!(contrast_trc(-0.5).is_err());
}
#[test]
fn test_equalize_trc_uniform() {
let pix = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let lut = equalize_trc(&pix, 1.0, 1).unwrap();
assert_eq!(lut[0], 255);
}
#[test]
fn test_equalize_trc_fract_zero() {
let pix = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let lut = equalize_trc(&pix, 0.0, 1).unwrap();
for (i, &val) in lut.iter().enumerate() {
assert_eq!(val, i as u8, "identity mismatch at {}", i);
}
}
#[test]
fn test_equalize_trc_invalid_params() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
assert!(equalize_trc(&pix, -0.1, 1).is_err());
assert!(equalize_trc(&pix, 1.5, 1).is_err());
assert!(equalize_trc(&pix, 0.5, 0).is_err());
let pix32 = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
assert!(equalize_trc(&pix32, 0.5, 1).is_err());
}
#[test]
fn test_trc_map_8bpp_identity() {
let pix = Pix::new(3, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 0);
pm.set_pixel_unchecked(1, 0, 128);
pm.set_pixel_unchecked(2, 0, 255);
let lut: TrcLut = core::array::from_fn(|i| i as u8);
trc_map(&mut pm, None, &lut).unwrap();
assert_eq!(pm.get_pixel_unchecked(0, 0), 0);
assert_eq!(pm.get_pixel_unchecked(1, 0), 128);
assert_eq!(pm.get_pixel_unchecked(2, 0), 255);
}
#[test]
fn test_trc_map_8bpp_invert() {
let pix = Pix::new(3, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 0);
pm.set_pixel_unchecked(1, 0, 128);
pm.set_pixel_unchecked(2, 0, 255);
let lut: TrcLut = core::array::from_fn(|i| (255 - i) as u8);
trc_map(&mut pm, None, &lut).unwrap();
assert_eq!(pm.get_pixel_unchecked(0, 0), 255);
assert_eq!(pm.get_pixel_unchecked(1, 0), 127);
assert_eq!(pm.get_pixel_unchecked(2, 0), 0);
}
#[test]
fn test_trc_map_32bpp() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 150, 200));
let lut: TrcLut = core::array::from_fn(|i| (i * 2).min(255) as u8);
trc_map(&mut pm, None, &lut).unwrap();
let (r, g, b) = pixel::extract_rgb(pm.get_pixel_unchecked(0, 0));
assert_eq!(r, 200);
assert_eq!(g, 255); assert_eq!(b, 255); }
#[test]
fn test_trc_map_with_mask() {
let pix = Pix::new(3, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 100);
pm.set_pixel_unchecked(1, 0, 100);
pm.set_pixel_unchecked(2, 0, 100);
let mask = Pix::new(3, 1, PixelDepth::Bit1).unwrap();
let mut mm = mask.try_into_mut().unwrap();
mm.set_pixel_unchecked(0, 0, 1); mm.set_pixel_unchecked(1, 0, 0); mm.set_pixel_unchecked(2, 0, 1); let mask: Pix = mm.into();
let lut: TrcLut = core::array::from_fn(|i| (255 - i) as u8); trc_map(&mut pm, Some(&mask), &lut).unwrap();
assert_eq!(pm.get_pixel_unchecked(0, 0), 155); assert_eq!(pm.get_pixel_unchecked(1, 0), 100); assert_eq!(pm.get_pixel_unchecked(2, 0), 155); }
#[test]
fn test_trc_map_invalid_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
let mut pm = pix.try_into_mut().unwrap();
let lut: TrcLut = core::array::from_fn(|i| i as u8);
assert!(trc_map(&mut pm, None, &lut).is_err());
}
#[test]
fn test_trc_map_general_separate_channels() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 100, 100));
let lut_r: TrcLut = core::array::from_fn(|i| (255 - i) as u8);
let lut_g: TrcLut = core::array::from_fn(|i| i as u8);
let lut_b: TrcLut = [0u8; 256];
trc_map_general(&mut pm, None, &lut_r, &lut_g, &lut_b).unwrap();
let (r, g, b) = pixel::extract_rgb(pm.get_pixel_unchecked(0, 0));
assert_eq!(r, 155); assert_eq!(g, 100); assert_eq!(b, 0); }
#[test]
fn test_trc_map_general_invalid_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
let lut: TrcLut = core::array::from_fn(|i| i as u8);
assert!(trc_map_general(&mut pm, None, &lut, &lut, &lut).is_err());
}
#[test]
fn test_gamma_trc_pix_8bpp() {
let pix = Pix::new(3, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 0);
pm.set_pixel_unchecked(1, 0, 128);
pm.set_pixel_unchecked(2, 0, 255);
let pix: Pix = pm.into();
let result = gamma_trc_pix(&pix, 2.0, 0, 255).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
assert_eq!(result.get_pixel_unchecked(0, 0), 0);
assert_eq!(result.get_pixel_unchecked(2, 0), 255);
assert!(result.get_pixel_unchecked(1, 0) > 128);
}
#[test]
fn test_gamma_trc_pix_32bpp() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 100, 100));
let pix: Pix = pm.into();
let result = gamma_trc_pix(&pix, 2.0, 0, 255).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert!(r > 100);
assert_eq!(r, g);
assert_eq!(g, b);
}
#[test]
fn test_gamma_trc_pix_invalid_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
assert!(gamma_trc_pix(&pix, 1.0, 0, 255).is_err());
}
#[test]
fn test_gamma_trc_masked_partial() {
let pix = Pix::new(2, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 128);
pm.set_pixel_unchecked(1, 0, 128);
let pix: Pix = pm.into();
let mask = Pix::new(2, 1, PixelDepth::Bit1).unwrap();
let mut mm = mask.try_into_mut().unwrap();
mm.set_pixel_unchecked(0, 0, 1);
mm.set_pixel_unchecked(1, 0, 0);
let mask: Pix = mm.into();
let result = gamma_trc_masked(&pix, Some(&mask), 2.0, 0, 255).unwrap();
assert!(result.get_pixel_unchecked(0, 0) > 128);
assert_eq!(result.get_pixel_unchecked(1, 0), 128);
}
#[test]
fn test_gamma_trc_masked_no_mask() {
let pix = Pix::new(1, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 128);
let pix: Pix = pm.into();
let result = gamma_trc_masked(&pix, None, 2.0, 0, 255).unwrap();
assert!(result.get_pixel_unchecked(0, 0) > 128);
}
#[test]
fn test_gamma_trc_with_alpha_preserves_alpha() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_spp(4);
pm.set_pixel_unchecked(0, 0, pixel::compose_rgba(100, 100, 100, 128));
let pix: Pix = pm.into();
let result = gamma_trc_with_alpha(&pix, 2.0, 0, 255).unwrap();
let (r, _, _, a) = pixel::extract_rgba(result.get_pixel_unchecked(0, 0));
assert!(r > 100);
assert_eq!(a, 128);
}
#[test]
fn test_gamma_trc_with_alpha_invalid_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
assert!(gamma_trc_with_alpha(&pix, 1.0, 0, 255).is_err());
}
#[test]
fn test_contrast_trc_pix_enhancement() {
let pix = Pix::new(3, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 64);
pm.set_pixel_unchecked(1, 0, 128);
pm.set_pixel_unchecked(2, 0, 192);
let pix: Pix = pm.into();
let result = contrast_trc_pix(&pix, 0.5).unwrap();
assert!(result.get_pixel_unchecked(0, 0) < 64);
assert!(result.get_pixel_unchecked(2, 0) > 192);
}
#[test]
fn test_contrast_trc_pix_zero_factor() {
let pix = Pix::new(1, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 100);
let pix: Pix = pm.into();
let result = contrast_trc_pix(&pix, 0.0).unwrap();
assert_eq!(result.get_pixel_unchecked(0, 0), 100);
}
#[test]
fn test_contrast_trc_masked() {
let pix = Pix::new(2, 1, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 64);
pm.set_pixel_unchecked(1, 0, 64);
let pix: Pix = pm.into();
let mask = Pix::new(2, 1, PixelDepth::Bit1).unwrap();
let mut mm = mask.try_into_mut().unwrap();
mm.set_pixel_unchecked(0, 0, 1);
mm.set_pixel_unchecked(1, 0, 0);
let mask: Pix = mm.into();
let result = contrast_trc_masked(&pix, Some(&mask), 0.5).unwrap();
assert!(result.get_pixel_unchecked(0, 0) < 64);
assert_eq!(result.get_pixel_unchecked(1, 0), 64);
}
#[test]
fn test_equalize_trc_pix_8bpp() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..5 {
pm.set_pixel_unchecked(x, y, 50);
}
for x in 5..10 {
pm.set_pixel_unchecked(x, y, 200);
}
}
let pix: Pix = pm.into();
let result = equalize_trc_pix(&pix, 0.5, 1).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
let v0 = result.get_pixel_unchecked(0, 0);
let v1 = result.get_pixel_unchecked(5, 0);
assert!(v1 > v0);
}
#[test]
fn test_equalize_trc_pix_zero_fract() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 100);
let pix: Pix = pm.into();
let result = equalize_trc_pix(&pix, 0.0, 1).unwrap();
assert_eq!(result.get_pixel_unchecked(0, 0), 100);
}
#[test]
fn test_modify_hue_shift() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(255, 0, 0));
let pix: Pix = pm.into();
let result = modify_hue(&pix, 1.0 / 3.0).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert!(g > r, "expected green > red, got r={r} g={g} b={b}");
}
#[test]
fn test_modify_hue_zero() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 150, 200));
let pix: Pix = pm.into();
let result = modify_hue(&pix, 0.0).unwrap();
assert_eq!(
result.get_pixel_unchecked(0, 0),
pix.get_pixel_unchecked(0, 0)
);
}
#[test]
fn test_modify_hue_invalid() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
assert!(modify_hue(&pix, 1.5).is_err());
let pix8 = Pix::new(1, 1, PixelDepth::Bit8).unwrap();
assert!(modify_hue(&pix8, 0.5).is_err());
}
#[test]
fn test_modify_saturation_increase() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(200, 100, 100));
let pix: Pix = pm.into();
let result = modify_saturation(&pix, 0.5).unwrap();
let (r, _, _) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert!(r >= 200, "expected r >= 200, got {r}");
}
#[test]
fn test_modify_saturation_zero() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 150, 200));
let pix: Pix = pm.into();
let result = modify_saturation(&pix, 0.0).unwrap();
assert_eq!(
result.get_pixel_unchecked(0, 0),
pix.get_pixel_unchecked(0, 0)
);
}
#[test]
fn test_modify_saturation_desaturate() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(200, 100, 50));
let pix: Pix = pm.into();
let result = modify_saturation(&pix, -1.0).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, g);
assert_eq!(g, b);
assert_eq!(r, 200);
}
#[test]
fn test_modify_brightness_increase() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 50, 25));
let pix: Pix = pm.into();
let result = modify_brightness(&pix, 0.5).unwrap();
let (r, _, _) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert!(r > 100, "expected brighter, got r={r}");
}
#[test]
fn test_modify_brightness_decrease() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(200, 150, 100));
let pix: Pix = pm.into();
let result = modify_brightness(&pix, -0.5).unwrap();
let (r, _, _) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert!(r < 200, "expected darker, got r={r}");
}
#[test]
fn test_modify_brightness_zero() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 150, 200));
let pix: Pix = pm.into();
let result = modify_brightness(&pix, 0.0).unwrap();
assert_eq!(
result.get_pixel_unchecked(0, 0),
pix.get_pixel_unchecked(0, 0)
);
}
#[test]
fn test_measure_saturation_gray() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(128, 128, 128));
}
}
let pix: Pix = pm.into();
let sat = measure_saturation(&pix, 1).unwrap();
assert_eq!(sat, 0.0);
}
#[test]
fn test_measure_saturation_colored() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(255, 0, 0));
}
}
let pix: Pix = pm.into();
let sat = measure_saturation(&pix, 1).unwrap();
assert_eq!(sat, 255.0);
}
#[test]
fn test_measure_saturation_invalid() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
assert!(measure_saturation(&pix, 1).is_err());
let pix32 = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
assert!(measure_saturation(&pix32, 0).is_err());
}
#[test]
fn test_color_shift_rgb_positive() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 100, 100));
let pix: Pix = pm.into();
let result = color_shift_rgb(&pix, 0.5, 0.0, 0.0).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert!(r > 150, "expected r > 150, got r={r}");
assert_eq!(g, 100);
assert_eq!(b, 100);
}
#[test]
fn test_color_shift_rgb_negative() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(200, 200, 200));
let pix: Pix = pm.into();
let result = color_shift_rgb(&pix, 0.0, 0.0, -0.5).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 200);
assert_eq!(g, 200);
assert_eq!(b, 100);
}
#[test]
fn test_color_shift_rgb_zero() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 150, 200));
let pix: Pix = pm.into();
let result = color_shift_rgb(&pix, 0.0, 0.0, 0.0).unwrap();
assert_eq!(
result.get_pixel_unchecked(0, 0),
pix.get_pixel_unchecked(0, 0)
);
}
#[test]
fn test_color_shift_rgb_invalid() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
assert!(color_shift_rgb(&pix, 1.5, 0.0, 0.0).is_err());
let pix8 = Pix::new(1, 1, PixelDepth::Bit8).unwrap();
assert!(color_shift_rgb(&pix8, 0.5, 0.0, 0.0).is_err());
}
#[test]
fn test_darken_gray_low_saturation() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(128, 128, 128));
let pix: Pix = pm.into();
let result = darken_gray(&pix, 200, 10).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 0);
}
#[test]
fn test_darken_gray_saturated_unchanged() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(255, 55, 55));
let pix: Pix = pm.into();
let result = darken_gray(&pix, 200, 10).unwrap();
assert_eq!(
result.get_pixel_unchecked(0, 0),
pix.get_pixel_unchecked(0, 0)
);
}
#[test]
fn test_darken_gray_bright_unchanged() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(250, 248, 248));
let pix: Pix = pm.into();
let result = darken_gray(&pix, 200, 10).unwrap();
assert_eq!(
result.get_pixel_unchecked(0, 0),
pix.get_pixel_unchecked(0, 0)
);
}
#[test]
fn test_darken_gray_invalid() {
let pix8 = Pix::new(1, 1, PixelDepth::Bit8).unwrap();
assert!(darken_gray(&pix8, 200, 10).is_err());
}
#[test]
fn test_mult_constant_color_basic() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 200, 50));
let pix: Pix = pm.into();
let result = mult_constant_color(&pix, 0.5, 1.0, 2.0).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 50);
assert_eq!(g, 200);
assert_eq!(b, 100);
}
#[test]
fn test_mult_constant_color_clipping() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(200, 200, 200));
let pix: Pix = pm.into();
let result = mult_constant_color(&pix, 2.0, 2.0, 2.0).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 255);
assert_eq!(g, 255);
assert_eq!(b, 255);
}
#[test]
fn test_mult_constant_color_invalid() {
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
assert!(mult_constant_color(&pix, -0.5, 1.0, 1.0).is_err());
}
#[test]
fn test_mult_matrix_color_identity() {
use crate::filter::Kernel;
let kel = Kernel::from_slice(3, 3, &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).unwrap();
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 150, 200));
let pix: Pix = pm.into();
let result = mult_matrix_color(&pix, &kel).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 100);
assert_eq!(g, 150);
assert_eq!(b, 200);
}
#[test]
fn test_mult_matrix_color_swap_rg() {
use crate::filter::Kernel;
let kel = Kernel::from_slice(3, 3, &[0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]).unwrap();
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 200, 50));
let pix: Pix = pm.into();
let result = mult_matrix_color(&pix, &kel).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 200);
assert_eq!(g, 100);
assert_eq!(b, 50);
}
#[test]
fn test_mult_matrix_color_clipping() {
use crate::filter::Kernel;
let kel = Kernel::from_slice(3, 3, &[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]).unwrap();
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, pixel::compose_rgb(100, 100, 100));
let pix: Pix = pm.into();
let result = mult_matrix_color(&pix, &kel).unwrap();
let (r, g, b) = pixel::extract_rgb(result.get_pixel_unchecked(0, 0));
assert_eq!(r, 255); assert_eq!(g, 255);
assert_eq!(b, 255);
}
#[test]
fn test_mult_matrix_color_invalid_size() {
use crate::filter::Kernel;
let kel = Kernel::from_slice(2, 2, &[1.0, 0.0, 0.0, 1.0]).unwrap();
let pix = Pix::new(1, 1, PixelDepth::Bit32).unwrap();
assert!(mult_matrix_color(&pix, &kel).is_err());
}
fn create_8bpp_gradient() -> Pix {
let pix = Pix::new(30, 30, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..30u32 {
for x in 0..30u32 {
pm.set_pixel_unchecked(x, y, (x * 8).min(255));
}
}
pm.into()
}
#[test]
fn test_unsharp_masking_gray_basic() {
let pix = create_8bpp_gradient();
let result = unsharp_masking_gray(&pix, 3, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit8);
}
#[test]
fn test_unsharp_masking_gray_no_sharpening() {
let pix = create_8bpp_gradient();
let result = unsharp_masking_gray(&pix, 3, 0.0).unwrap();
for y in 0..pix.height() {
for x in 0..pix.width() {
assert_eq!(
result.get_pixel_unchecked(x, y),
pix.get_pixel_unchecked(x, y)
);
}
}
}
#[test]
fn test_unsharp_masking_color() {
let pix = Pix::new(30, 30, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..30u32 {
for x in 0..30u32 {
let v = (x * 8).min(255) as u8;
pm.set_pixel_unchecked(x, y, crate::core::pixel::compose_rgb(v, v, v));
}
}
let pix: Pix = pm.into();
let result = unsharp_masking(&pix, 3, 0.5).unwrap();
assert_eq!(result.width(), pix.width());
assert_eq!(result.height(), pix.height());
assert_eq!(result.depth(), PixelDepth::Bit32);
}
#[test]
fn test_unsharp_masking_invalid_depth() {
let pix = Pix::new(10, 10, PixelDepth::Bit1).unwrap();
assert!(unsharp_masking(&pix, 3, 0.5).is_err());
}
fn make_8bpp_gradient(w: u32, h: u32, max_val: u32) -> Pix {
let pix = Pix::new(w, h, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let v = ((x + y * w) * max_val / (w * h)).min(max_val);
pm.set_pixel_unchecked(x, y, v);
}
}
pm.into()
}
#[test]
fn test_max_dynamic_range_all_zeros() {
let pix = Pix::new(4, 4, PixelDepth::Bit8).unwrap();
let result = max_dynamic_range(&pix, DynamicRangeScale::Linear).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
for y in 0..4 {
for x in 0..4 {
assert_eq!(result.get_pixel_unchecked(x, y), 0);
}
}
}
#[test]
fn test_max_dynamic_range_linear_max_pixel_is_255() {
let pix = make_8bpp_gradient(4, 4, 100);
let result = max_dynamic_range(&pix, DynamicRangeScale::Linear).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
let mut max_out = 0u32;
for y in 0..4 {
for x in 0..4 {
let v = result.get_pixel_unchecked(x, y);
if v > max_out {
max_out = v;
}
}
}
assert_eq!(max_out, 255, "max pixel should be 255 after linear scaling");
}
#[test]
fn test_max_dynamic_range_log_max_pixel_is_255() {
let pix = make_8bpp_gradient(4, 4, 200);
let result = max_dynamic_range(&pix, DynamicRangeScale::Log).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
let max_out = (0..4u32)
.flat_map(|y| (0..4u32).map(move |x| (x, y)))
.map(|(x, y)| result.get_pixel_unchecked(x, y))
.max()
.unwrap();
assert_eq!(max_out, 255, "max pixel should be 255 after log scaling");
}
#[test]
fn test_max_dynamic_range_log_max_one() {
let pix = Pix::new(2, 2, PixelDepth::Bit8).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_pixel_unchecked(0, 0, 0);
pm.set_pixel_unchecked(1, 0, 1);
pm.set_pixel_unchecked(0, 1, 1);
pm.set_pixel_unchecked(1, 1, 0);
let pix: Pix = pm.into();
let result = max_dynamic_range(&pix, DynamicRangeScale::Log).unwrap();
assert_eq!(result.get_pixel_unchecked(0, 0), 0, "zero stays zero");
assert_eq!(result.get_pixel_unchecked(1, 0), 255, "one maps to 255");
}
#[test]
fn test_max_dynamic_range_unsupported_depth() {
let pix = Pix::new(4, 4, PixelDepth::Bit1).unwrap();
assert!(max_dynamic_range(&pix, DynamicRangeScale::Linear).is_err());
}
#[test]
fn test_max_dynamic_range_16bpp() {
let pix = Pix::new(4, 4, PixelDepth::Bit16).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..4u32 {
for x in 0..4u32 {
pm.set_pixel_unchecked(x, y, (x + y * 100) * 256);
}
}
let pix: Pix = pm.into();
let result = max_dynamic_range(&pix, DynamicRangeScale::Linear).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit8);
}
}