use palette::{LinSrgb, Srgb};
use rayon::prelude::*;
pub mod detail;
pub use detail::{DetailParams, SharpeningParams};
pub mod dehaze;
pub use dehaze::DehazeParams;
pub mod denoise;
pub use denoise::NoiseReductionParams;
pub mod grain;
pub use grain::{GrainParams, GrainType};
pub mod exposure;
pub use exposure::{apply_exposure, exposure_factor};
pub mod white_balance;
pub use white_balance::apply_white_balance;
pub mod basic_tone;
pub use basic_tone::{apply_blacks, apply_contrast, apply_highlights, apply_shadows, apply_whites};
pub mod hsl;
pub use hsl::{apply_hsl, cosine_weight, hue_distance, WeightFn};
pub mod color_grading;
pub use color_grading::{
apply_color_grading_pre, ColorGradingParams, ColorGradingPrecomputed, ColorWheel,
};
pub mod tone_curves;
pub(crate) use tone_curves::build_tone_curve_lut;
pub use tone_curves::{apply_tone_curves_pre, ToneCurve, ToneCurveParams, ToneCurvePrecomputed};
pub mod vignette;
pub use vignette::{
apply_vignette, apply_vignette_buffer, apply_vignette_pre, VignettePrecomputed, VignetteShape,
};
pub(crate) const LUMA_R: f32 = 0.2126;
pub(crate) const LUMA_G: f32 = 0.7152;
pub(crate) const LUMA_B: f32 = 0.0722;
#[inline(always)]
pub fn apply_per_channel(r: f32, g: f32, b: f32, f: impl Fn(f32) -> f32) -> (f32, f32, f32) {
(f(r), f(g), f(b))
}
#[inline]
pub(crate) fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
pub fn linear_to_srgb(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let srgb: Srgb<f32> = LinSrgb::new(r, g, b).into_encoding();
(srgb.red, srgb.green, srgb.blue)
}
pub fn srgb_to_linear(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let lin: LinSrgb<f32> = Srgb::new(r, g, b).into_linear();
(lin.red, lin.green, lin.blue)
}
pub fn apply_white_balance_exposure_buffer(
buf: &mut [[f32; 3]],
temperature: f32,
tint: f32,
exposure: f32,
) {
let factor = exposure_factor(exposure);
for pixel in buf.iter_mut() {
let (r, g, b) = apply_white_balance(pixel[0], pixel[1], pixel[2], temperature, tint);
let (r, g, b) = apply_per_channel(r, g, b, |v| apply_exposure(v, factor));
*pixel = [r, g, b];
}
}
pub struct PerPixelParams<'a> {
pub contrast: f32,
pub highlights: f32,
pub shadows: f32,
pub whites: f32,
pub blacks: f32,
pub tone_curve_pre: Option<&'a ToneCurvePrecomputed>,
pub hsl_active: bool,
pub hue_shifts: [f32; 8],
pub sat_shifts: [f32; 8],
pub lum_shifts: [f32; 8],
pub color_grading_pre: Option<ColorGradingPrecomputed>,
#[allow(clippy::type_complexity)]
pub lut_fn: Option<&'a (dyn Fn(f32, f32, f32) -> (f32, f32, f32) + Sync + 'a)>,
}
pub fn apply_per_pixel_adjustments(buf: &mut [[f32; 3]], pp: &PerPixelParams) {
buf.par_chunks_mut(1024).for_each(|chunk| {
for pixel in chunk.iter_mut() {
let [mut sr, mut sg, mut sb] = *pixel;
if pp.contrast != 0.0 {
(sr, sg, sb) = apply_per_channel(sr, sg, sb, |v| apply_contrast(v, pp.contrast));
}
if pp.highlights != 0.0 {
(sr, sg, sb) =
apply_per_channel(sr, sg, sb, |v| apply_highlights(v, pp.highlights));
}
if pp.shadows != 0.0 {
(sr, sg, sb) = apply_per_channel(sr, sg, sb, |v| apply_shadows(v, pp.shadows));
}
if pp.whites != 0.0 {
(sr, sg, sb) = apply_per_channel(sr, sg, sb, |v| apply_whites(v, pp.whites));
}
if pp.blacks != 0.0 {
(sr, sg, sb) = apply_per_channel(sr, sg, sb, |v| apply_blacks(v, pp.blacks));
}
if let Some(pre) = pp.tone_curve_pre {
let (tr, tg, tb) = apply_tone_curves_pre(sr, sg, sb, pre);
sr = tr;
sg = tg;
sb = tb;
}
if pp.hsl_active {
let (hr, hg, hb) = apply_hsl(
sr,
sg,
sb,
&pp.hue_shifts,
&pp.sat_shifts,
&pp.lum_shifts,
cosine_weight,
);
sr = hr;
sg = hg;
sb = hb;
}
if let Some(ref pre) = pp.color_grading_pre {
let (cr, cg, cb) = apply_color_grading_pre(sr, sg, sb, pre);
sr = cr;
sg = cg;
sb = cb;
}
if let Some(lut_fn) = pp.lut_fn {
let (lr, lg, lb) = lut_fn(sr, sg, sb);
sr = lr;
sg = lg;
sb = lb;
}
*pixel = [sr, sg, sb];
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_srgb_roundtrip() {
let (sr, sg, sb) = linear_to_srgb(0.5, 0.3, 0.1);
let (lr, lg, lb) = srgb_to_linear(sr, sg, sb);
assert!((lr - 0.5).abs() < 1e-5);
assert!((lg - 0.3).abs() < 1e-5);
assert!((lb - 0.1).abs() < 1e-5);
}
#[test]
fn white_balance_exposure_buffer_identity() {
let mut buf = vec![[0.5, 0.3, 0.1], [0.25, 0.25, 0.25]];
let original = buf.clone();
apply_white_balance_exposure_buffer(&mut buf, 0.0, 0.0, 0.0);
for i in 0..buf.len() {
for c in 0..3 {
assert!(
(buf[i][c] - original[i][c]).abs() < 1e-6,
"pixel[{}][{}] changed with neutral params",
i,
c
);
}
}
}
#[test]
fn white_balance_exposure_buffer_applies_exposure() {
let mut buf = vec![[0.25, 0.25, 0.25]];
apply_white_balance_exposure_buffer(&mut buf, 0.0, 0.0, 1.0);
for (c, &v) in buf[0].iter().enumerate() {
assert!((v - 0.5).abs() < 1e-5, "channel {c}: expected 0.5, got {v}");
}
}
#[test]
fn white_balance_exposure_buffer_applies_wb() {
let mut buf = vec![[0.5, 0.5, 0.5]];
apply_white_balance_exposure_buffer(&mut buf, 50.0, 0.0, 0.0);
assert!(buf[0][0] > buf[0][2], "warm WB should make red > blue");
}
#[test]
fn per_pixel_adjustments_neutral_is_identity() {
let mut buf = vec![[0.7, 0.5, 0.3]]; let original = buf.clone();
let pp = PerPixelParams {
contrast: 0.0,
highlights: 0.0,
shadows: 0.0,
whites: 0.0,
blacks: 0.0,
tone_curve_pre: None,
hsl_active: false,
hue_shifts: [0.0; 8],
sat_shifts: [0.0; 8],
lum_shifts: [0.0; 8],
color_grading_pre: None,
lut_fn: None,
};
apply_per_pixel_adjustments(&mut buf, &pp);
for c in 0..3 {
assert!(
(buf[0][c] - original[0][c]).abs() < 1e-6,
"channel {} changed with neutral params",
c
);
}
}
#[test]
fn per_pixel_adjustments_applies_contrast() {
let mut buf = vec![[0.8, 0.8, 0.8]]; let pp = PerPixelParams {
contrast: 50.0,
highlights: 0.0,
shadows: 0.0,
whites: 0.0,
blacks: 0.0,
tone_curve_pre: None,
hsl_active: false,
hue_shifts: [0.0; 8],
sat_shifts: [0.0; 8],
lum_shifts: [0.0; 8],
color_grading_pre: None,
lut_fn: None,
};
apply_per_pixel_adjustments(&mut buf, &pp);
assert!(
buf[0][0] > 0.8,
"contrast should increase value above midpoint"
);
}
}