#![allow(dead_code)]
#![allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct LumaKeyParams {
pub low_threshold: f32,
pub high_threshold: f32,
pub feather: f32,
pub invert: bool,
}
impl LumaKeyParams {
#[must_use]
pub fn new(low_threshold: f32, high_threshold: f32, feather: f32, invert: bool) -> Self {
Self {
low_threshold: low_threshold.clamp(0.0, 1.0),
high_threshold: high_threshold.clamp(0.0, 1.0),
feather: feather.max(0.0),
invert,
}
}
#[must_use]
pub fn bright_key() -> Self {
Self::new(0.5, 1.0, 0.05, false)
}
#[must_use]
pub fn dark_key() -> Self {
Self::new(0.0, 0.5, 0.05, true)
}
}
#[must_use]
pub fn key_pixel_luma(luma: f32, params: &LumaKeyParams) -> f32 {
let luma = luma.clamp(0.0, 1.0);
let lo = params.low_threshold;
let hi = params.high_threshold;
let f = params.feather.max(1e-6);
let alpha = if luma < lo - f {
0.0_f32
} else if luma < lo {
(luma - (lo - f)) / f
} else if luma <= hi {
1.0_f32
} else if luma < hi + f {
1.0_f32 - (luma - hi) / f
} else {
0.0_f32
};
let alpha = alpha.clamp(0.0, 1.0);
if params.invert {
1.0 - alpha
} else {
alpha
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn apply_luma_key(pixels_rgba: &mut [u8], params: &LumaKeyParams) {
for chunk in pixels_rgba.chunks_exact_mut(4) {
let r = f32::from(chunk[0]) / 255.0;
let g = f32::from(chunk[1]) / 255.0;
let b = f32::from(chunk[2]) / 255.0;
let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let alpha = key_pixel_luma(luma, params);
chunk[3] = (alpha * 255.0).round() as u8;
}
}
#[must_use]
pub fn combine_alpha_multiply(a: f32, b: f32) -> f32 {
(a * b).clamp(0.0, 1.0)
}
#[must_use]
pub fn combine_alpha_screen(a: f32, b: f32) -> f32 {
(1.0 - (1.0 - a) * (1.0 - b)).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_pixel_luma_inside_range() {
let params = LumaKeyParams::new(0.2, 0.8, 0.0, false);
assert!((key_pixel_luma(0.5, ¶ms) - 1.0).abs() < 1e-5);
}
#[test]
fn test_key_pixel_luma_below_range() {
let params = LumaKeyParams::new(0.2, 0.8, 0.0, false);
assert!((key_pixel_luma(0.1, ¶ms)).abs() < 1e-5);
}
#[test]
fn test_key_pixel_luma_above_range() {
let params = LumaKeyParams::new(0.2, 0.8, 0.0, false);
assert!((key_pixel_luma(0.9, ¶ms)).abs() < 1e-5);
}
#[test]
fn test_key_pixel_luma_inverted() {
let params = LumaKeyParams::new(0.2, 0.8, 0.0, true);
assert!((key_pixel_luma(0.5, ¶ms)).abs() < 1e-5);
assert!((key_pixel_luma(0.1, ¶ms) - 1.0).abs() < 1e-5);
}
#[test]
fn test_key_pixel_luma_feather_lower() {
let params = LumaKeyParams::new(0.3, 0.7, 0.1, false);
let alpha = key_pixel_luma(0.3, ¶ms);
assert!((alpha - 1.0).abs() < 1e-5);
}
#[test]
fn test_key_pixel_luma_feather_midpoint() {
let params = LumaKeyParams::new(0.3, 0.7, 0.1, false);
let alpha = key_pixel_luma(0.25, ¶ms); assert!((alpha - 0.5).abs() < 1e-5);
}
#[test]
fn test_apply_luma_key_modifies_alpha() {
let mut pixels = vec![
200u8, 200, 200, 255, 10u8, 10, 10, 255, ];
let params = LumaKeyParams::new(0.5, 1.0, 0.0, false);
apply_luma_key(&mut pixels, ¶ms);
assert!(pixels[3] > 0);
assert_eq!(pixels[7], 0);
}
#[test]
fn test_apply_luma_key_empty_slice() {
let mut pixels: Vec<u8> = vec![];
let params = LumaKeyParams::new(0.2, 0.8, 0.0, false);
apply_luma_key(&mut pixels, ¶ms);
}
#[test]
fn test_combine_alpha_multiply() {
assert!((combine_alpha_multiply(0.5, 0.5) - 0.25).abs() < 1e-5);
assert_eq!(combine_alpha_multiply(1.0, 1.0), 1.0);
assert_eq!(combine_alpha_multiply(0.0, 1.0), 0.0);
}
#[test]
fn test_combine_alpha_screen() {
assert!((combine_alpha_screen(0.5, 0.5) - 0.75).abs() < 1e-5);
assert!((combine_alpha_screen(1.0, 1.0) - 1.0).abs() < 1e-5);
}
#[test]
fn test_bright_key_preset() {
let p = LumaKeyParams::bright_key();
assert_eq!(p.invert, false);
assert!(p.low_threshold >= 0.4);
}
#[test]
fn test_dark_key_preset() {
let p = LumaKeyParams::dark_key();
assert_eq!(p.invert, true);
}
#[test]
fn test_luma_key_clamp_out_of_range_input() {
let params = LumaKeyParams::new(0.2, 0.8, 0.0, false);
let a = key_pixel_luma(-0.5, ¶ms);
let b = key_pixel_luma(1.5, ¶ms);
assert!(a >= 0.0 && a <= 1.0);
assert!(b >= 0.0 && b <= 1.0);
}
}