#![allow(dead_code)]
#![allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompositeOp {
Over,
Under,
In,
Out,
Atop,
Xor,
Clear,
}
impl CompositeOp {
#[must_use]
pub fn porter_duff_factors(&self, alpha_s: f32, alpha_d: f32) -> (f32, f32) {
match self {
CompositeOp::Over => (1.0, 1.0 - alpha_s),
CompositeOp::Under => (1.0 - alpha_d, 1.0),
CompositeOp::In => (alpha_d, 0.0),
CompositeOp::Out => (1.0 - alpha_d, 0.0),
CompositeOp::Atop => (alpha_d, 1.0 - alpha_s),
CompositeOp::Xor => (1.0 - alpha_d, 1.0 - alpha_s),
CompositeOp::Clear => (0.0, 0.0),
}
}
#[must_use]
pub fn requires_both_opaque(&self) -> bool {
matches!(self, CompositeOp::In)
}
}
#[must_use]
pub fn composite_premul(
src: (f32, f32, f32, f32),
dst: (f32, f32, f32, f32),
op: CompositeOp,
) -> (f32, f32, f32, f32) {
let (sr, sg, sb, sa) = src;
let (dr, dg, db, da) = dst;
let (fa, fb) = op.porter_duff_factors(sa, da);
let out_r = (sr * fa + dr * fb).clamp(0.0, 1.0);
let out_g = (sg * fa + dg * fb).clamp(0.0, 1.0);
let out_b = (sb * fa + db * fb).clamp(0.0, 1.0);
let out_a = (sa * fa + da * fb).clamp(0.0, 1.0);
(out_r, out_g, out_b, out_a)
}
#[must_use]
pub fn composite_straight(
src: (f32, f32, f32, f32),
dst: (f32, f32, f32, f32),
op: CompositeOp,
) -> (f32, f32, f32, f32) {
let premul = |c: (f32, f32, f32, f32)| (c.0 * c.3, c.1 * c.3, c.2 * c.3, c.3);
let (pr, pg, pb, pa) = composite_premul(premul(src), premul(dst), op);
if pa < 1e-8 {
return (0.0, 0.0, 0.0, 0.0);
}
(pr / pa, pg / pa, pb / pa, pa)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names
)]
pub fn composite_buffers(src_rgba: &[u8], dst_rgba: &mut [u8], op: CompositeOp) {
assert_eq!(
src_rgba.len(),
dst_rgba.len(),
"buffers must be the same size"
);
for (s, d) in src_rgba.chunks_exact(4).zip(dst_rgba.chunks_exact_mut(4)) {
let sr = f32::from(s[0]) / 255.0;
let sg = f32::from(s[1]) / 255.0;
let sb = f32::from(s[2]) / 255.0;
let sa = f32::from(s[3]) / 255.0;
let dr = f32::from(d[0]) / 255.0;
let dg = f32::from(d[1]) / 255.0;
let db = f32::from(d[2]) / 255.0;
let da = f32::from(d[3]) / 255.0;
let src_pre = (sr * sa, sg * sa, sb * sa, sa);
let dst_pre = (dr * da, dg * da, db * da, da);
let (or_p, og_p, ob_p, oa) = composite_premul(src_pre, dst_pre, op);
let (or_, og_, ob_) = if oa < 1e-8 {
(0.0, 0.0, 0.0)
} else {
(or_p / oa, og_p / oa, ob_p / oa)
};
d[0] = (or_ * 255.0).round().clamp(0.0, 255.0) as u8;
d[1] = (og_ * 255.0).round().clamp(0.0, 255.0) as u8;
d[2] = (ob_ * 255.0).round().clamp(0.0, 255.0) as u8;
d[3] = (oa * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn px(r: f32, g: f32, b: f32, a: f32) -> (f32, f32, f32, f32) {
(r, g, b, a)
}
#[test]
fn test_composite_over_opaque_src() {
let src = px(1.0, 0.0, 0.0, 1.0);
let dst = px(0.0, 1.0, 0.0, 1.0);
let (r, g, b, a) = composite_straight(src, dst, CompositeOp::Over);
assert!((r - 1.0).abs() < 1e-5);
assert!(g.abs() < 1e-5);
assert!(b.abs() < 1e-5);
assert!((a - 1.0).abs() < 1e-5);
}
#[test]
fn test_composite_over_transparent_src() {
let src = px(1.0, 0.0, 0.0, 0.0);
let dst = px(0.0, 1.0, 0.0, 1.0);
let (r, g, b, a) = composite_straight(src, dst, CompositeOp::Over);
assert!(r.abs() < 1e-5);
assert!((g - 1.0).abs() < 1e-5);
assert!(b.abs() < 1e-5);
assert!((a - 1.0).abs() < 1e-5);
}
#[test]
fn test_composite_clear() {
let src = px(1.0, 1.0, 1.0, 1.0);
let dst = px(0.5, 0.5, 0.5, 1.0);
let (_, _, _, a) = composite_premul(
(src.0 * src.3, src.1 * src.3, src.2 * src.3, src.3),
(dst.0 * dst.3, dst.1 * dst.3, dst.2 * dst.3, dst.3),
CompositeOp::Clear,
);
assert!(a.abs() < 1e-5);
}
#[test]
fn test_composite_in_transparent_dst() {
let src = px(1.0, 0.0, 0.0, 1.0);
let dst = px(0.0, 0.0, 0.0, 0.0);
let (_, _, _, a) = composite_straight(src, dst, CompositeOp::In);
assert!(a.abs() < 1e-5);
}
#[test]
fn test_composite_out_opaque_dst() {
let src = px(1.0, 0.0, 0.0, 1.0);
let dst = px(0.0, 1.0, 0.0, 1.0);
let (_, _, _, a) = composite_premul(
(src.0 * src.3, src.1 * src.3, src.2 * src.3, src.3),
(dst.0 * dst.3, dst.1 * dst.3, dst.2 * dst.3, dst.3),
CompositeOp::Out,
);
assert!(a.abs() < 1e-5);
}
#[test]
fn test_porter_duff_over_factors() {
let (fa, fb) = CompositeOp::Over.porter_duff_factors(1.0, 1.0);
assert!((fa - 1.0).abs() < 1e-5);
assert!(fb.abs() < 1e-5);
}
#[test]
fn test_porter_duff_xor_equal_alpha() {
let (fa, fb) = CompositeOp::Xor.porter_duff_factors(0.8, 0.6);
assert!((fa - 0.4).abs() < 1e-5);
assert!((fb - 0.2).abs() < 1e-5);
}
#[test]
fn test_requires_both_opaque_in() {
assert!(CompositeOp::In.requires_both_opaque());
}
#[test]
fn test_requires_both_opaque_over() {
assert!(!CompositeOp::Over.requires_both_opaque());
}
#[test]
fn test_composite_buffers_over_opaque() {
let src = vec![255u8, 0, 0, 255];
let mut dst = vec![0u8, 255, 0, 255];
composite_buffers(&src, &mut dst, CompositeOp::Over);
assert_eq!(dst[0], 255);
assert_eq!(dst[1], 0);
assert_eq!(dst[3], 255);
}
#[test]
fn test_composite_buffers_clear() {
let src = vec![255u8, 255, 255, 255];
let mut dst = vec![128u8, 128, 128, 255];
composite_buffers(&src, &mut dst, CompositeOp::Clear);
assert_eq!(dst[3], 0);
}
#[test]
fn test_composite_buffers_size_matches() {
let src = vec![100u8, 100, 100, 200, 50, 50, 50, 128];
let mut dst = vec![0u8; 8];
composite_buffers(&src, &mut dst, CompositeOp::Over);
}
#[test]
fn test_composite_under_reversal() {
let src = px(1.0, 0.0, 0.0, 1.0);
let dst = px(0.0, 1.0, 0.0, 0.5);
let over = composite_straight(src, dst, CompositeOp::Over);
let under = composite_straight(dst, src, CompositeOp::Over);
let under2 = composite_straight(src, dst, CompositeOp::Under);
assert!((under.0 - under2.0).abs() < 1e-5);
assert!((under.1 - under2.1).abs() < 1e-5);
assert!((over.0 - under2.0).abs() > 1e-3 || (over.1 - under2.1).abs() > 1e-3);
}
}