#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum NonSeparableBlend {
Hue,
Saturation,
Color,
Luminosity,
}
impl NonSeparableBlend {
pub(crate) fn from_name(name: &str) -> Option<Self> {
match name {
"Hue" => Some(Self::Hue),
"Saturation" => Some(Self::Saturation),
"Color" => Some(Self::Color),
"Luminosity" => Some(Self::Luminosity),
_ => None,
}
}
}
pub(crate) fn compose_in_place(dest: &mut [u8], source: &[u8], mode: NonSeparableBlend) {
debug_assert_eq!(dest.len(), source.len());
debug_assert_eq!(dest.len() % 4, 0);
for px in 0..(dest.len() / 4) {
let off = px * 4;
let src_a = source[off + 3];
if src_a == 0 {
continue;
}
let sa = src_a as f32 / 255.0;
let (sr, sg, sb) = unpremultiply(source[off], source[off + 1], source[off + 2], sa);
let da = dest[off + 3] as f32 / 255.0;
let (dr, dg, db) = unpremultiply(dest[off], dest[off + 1], dest[off + 2], da);
let (br, bg, bb) = match mode {
NonSeparableBlend::Hue => {
let sat_cb = sat((dr, dg, db));
let sat_applied = set_sat((sr, sg, sb), sat_cb);
set_lum(sat_applied, lum((dr, dg, db)))
},
NonSeparableBlend::Saturation => {
let sat_cs = sat((sr, sg, sb));
let sat_applied = set_sat((dr, dg, db), sat_cs);
set_lum(sat_applied, lum((dr, dg, db)))
},
NonSeparableBlend::Color => {
set_lum((sr, sg, sb), lum((dr, dg, db)))
},
NonSeparableBlend::Luminosity => {
set_lum((dr, dg, db), lum((sr, sg, sb)))
},
};
let inv_sa = 1.0 - sa;
let inv_da = 1.0 - da;
let out_a = sa + da * inv_sa;
let (out_r, out_g, out_b) = if out_a <= 0.0 {
(0.0, 0.0, 0.0)
} else {
let blend_r = inv_sa * da * dr + sa * (inv_da * sr + da * br);
let blend_g = inv_sa * da * dg + sa * (inv_da * sg + da * bg);
let blend_b = inv_sa * da * db + sa * (inv_da * sb + da * bb);
(blend_r / out_a, blend_g / out_a, blend_b / out_a)
};
let out_r_premul = (out_r.clamp(0.0, 1.0) * out_a).clamp(0.0, 1.0);
let out_g_premul = (out_g.clamp(0.0, 1.0) * out_a).clamp(0.0, 1.0);
let out_b_premul = (out_b.clamp(0.0, 1.0) * out_a).clamp(0.0, 1.0);
dest[off] = (out_r_premul * 255.0).round() as u8;
dest[off + 1] = (out_g_premul * 255.0).round() as u8;
dest[off + 2] = (out_b_premul * 255.0).round() as u8;
dest[off + 3] = (out_a.clamp(0.0, 1.0) * 255.0).round() as u8;
}
}
fn unpremultiply(r: u8, g: u8, b: u8, alpha: f32) -> (f32, f32, f32) {
if alpha <= 0.0 {
return (0.0, 0.0, 0.0);
}
let inv = 1.0 / alpha;
(
(r as f32 / 255.0 * inv).clamp(0.0, 1.0),
(g as f32 / 255.0 * inv).clamp(0.0, 1.0),
(b as f32 / 255.0 * inv).clamp(0.0, 1.0),
)
}
fn lum(c: (f32, f32, f32)) -> f32 {
0.30 * c.0 + 0.59 * c.1 + 0.11 * c.2
}
fn sat(c: (f32, f32, f32)) -> f32 {
c.0.max(c.1).max(c.2) - c.0.min(c.1).min(c.2)
}
fn set_lum(c: (f32, f32, f32), l: f32) -> (f32, f32, f32) {
let d = l - lum(c);
let shifted = (c.0 + d, c.1 + d, c.2 + d);
clip_color(shifted)
}
fn clip_color(c: (f32, f32, f32)) -> (f32, f32, f32) {
let l = lum(c);
let n = c.0.min(c.1).min(c.2);
let x = c.0.max(c.1).max(c.2);
let (mut r, mut g, mut b) = c;
if n < 0.0 {
let denom = l - n;
if denom.abs() > 1e-9 {
r = l + (r - l) * l / denom;
g = l + (g - l) * l / denom;
b = l + (b - l) * l / denom;
}
}
if x > 1.0 {
let denom = x - l;
if denom.abs() > 1e-9 {
r = l + (r - l) * (1.0 - l) / denom;
g = l + (g - l) * (1.0 - l) / denom;
b = l + (b - l) * (1.0 - l) / denom;
}
}
(r, g, b)
}
fn set_sat(c: (f32, f32, f32), s: f32) -> (f32, f32, f32) {
let (r, g, b) = c;
let mut chans = [(r, 0u8), (g, 1u8), (b, 2u8)];
chans.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (cmin, cmid, cmax) = (chans[0].0, chans[1].0, chans[2].0);
let (imin, imid, imax) = (chans[0].1, chans[1].1, chans[2].1);
let (new_min, new_mid, new_max) = if cmax > cmin {
(0.0_f32, ((cmid - cmin) * s) / (cmax - cmin), s)
} else {
(0.0_f32, 0.0_f32, 0.0_f32)
};
let mut out = [0.0_f32; 3];
out[imin as usize] = new_min;
out[imid as usize] = new_mid;
out[imax as usize] = new_max;
(out[0], out[1], out[2])
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
#[test]
fn lum_matches_bt601_weights() {
let l = lum((1.0, 0.0, 0.0));
assert!(approx(l, 0.30));
let l = lum((0.0, 1.0, 0.0));
assert!(approx(l, 0.59));
let l = lum((0.0, 0.0, 1.0));
assert!(approx(l, 0.11));
}
#[test]
fn sat_of_grey_is_zero() {
assert!(approx(sat((0.5, 0.5, 0.5)), 0.0));
}
#[test]
fn sat_of_pure_red_is_one() {
assert!(approx(sat((1.0, 0.0, 0.0)), 1.0));
}
#[test]
fn luminosity_blend_grey_source_over_red_preserves_red_hue() {
let mut dest = [255u8, 0, 0, 255];
let source = [128u8, 128, 128, 255];
compose_in_place(&mut dest, &source, NonSeparableBlend::Luminosity);
assert!(
dest[0] > dest[1] + 60 && dest[0] > dest[2] + 60,
"Luminosity grey-over-red should preserve red hue; got {:?}",
dest
);
}
#[test]
fn hue_blend_red_source_over_blue_yields_red() {
let mut dest = [0u8, 0, 255, 255];
let source = [255u8, 0, 0, 255];
compose_in_place(&mut dest, &source, NonSeparableBlend::Hue);
assert!(
dest[0] > 50 && dest[1] < 30 && dest[2] < 30,
"Hue red-over-blue should yield red-dominant; got {:?}",
dest
);
}
#[test]
fn saturation_blend_grey_source_desaturates_dest() {
let mut dest = [255u8, 0, 0, 255];
let source = [128u8, 128, 128, 255];
compose_in_place(&mut dest, &source, NonSeparableBlend::Saturation);
let max_diff = (dest[0] as i32 - dest[1] as i32)
.abs()
.max((dest[0] as i32 - dest[2] as i32).abs())
.max((dest[1] as i32 - dest[2] as i32).abs());
assert!(max_diff < 30, "Saturation grey-over-red should desaturate; got {:?}", dest);
}
const PA_BACKDROP: [u8; 4] = [128, 0, 0, 128];
const PA_SOURCE: [u8; 4] = [0, 0, 179, 179];
#[test]
fn hue_blend_partial_alpha_is_byte_exact() {
let mut dest = PA_BACKDROP;
compose_in_place(&mut dest, &PA_SOURCE, NonSeparableBlend::Hue);
assert_eq!(
dest,
[57, 19, 179, 217],
"Hue blend partial-alpha: §11.3.4 + §11.3.5.3 produce \
byte-exact (57, 19, 179, 217); got {:?}",
dest
);
}
#[test]
fn saturation_blend_partial_alpha_is_byte_exact() {
let mut dest = PA_BACKDROP;
compose_in_place(&mut dest, &PA_SOURCE, NonSeparableBlend::Saturation);
assert_eq!(
dest,
[128, 0, 89, 217],
"Saturation blend partial-alpha: §11.3.4 + §11.3.5.3 \
produce byte-exact (128, 0, 89, 217); got {:?}",
dest
);
}
#[test]
fn color_blend_partial_alpha_is_byte_exact() {
let mut dest = PA_BACKDROP;
compose_in_place(&mut dest, &PA_SOURCE, NonSeparableBlend::Color);
assert_eq!(
dest,
[57, 19, 179, 217],
"Color blend partial-alpha: §11.3.4 + §11.3.5.3 produce \
byte-exact (57, 19, 179, 217); got {:?}",
dest
);
}
#[test]
fn luminosity_blend_partial_alpha_is_byte_exact() {
let mut dest = PA_BACKDROP;
compose_in_place(&mut dest, &PA_SOURCE, NonSeparableBlend::Luminosity);
assert_eq!(
dest,
[71, 0, 89, 217],
"Luminosity blend partial-alpha: §11.3.4 + §11.3.5.3 \
produce byte-exact (71, 0, 89, 217); got {:?}",
dest
);
}
}