// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #![allow(clippy::needless_range_loop)] use crate::headers::extra_channels::{ExtraChannel, ExtraChannelInfo}; use super::patches::{PatchBlendMode, PatchBlending}; #[inline] fn maybe_clamp(v: f32, clamp: bool) -> f32 { if clamp { v.clamp(0.0, 1.0) } else { v } } pub fn perform_blending, V: AsMut<[f32]>>( bg: &mut [V], fg: &[T], color_blending: &PatchBlending, ec_blending: &[PatchBlending], extra_channel_info: &[ExtraChannelInfo], ) { let has_alpha = extra_channel_info .iter() .any(|info| info.ec_type == ExtraChannel::Alpha); let num_ec = extra_channel_info.len(); let xsize = bg[0].as_mut().len(); let mut tmp = vec![vec![0.0f32; xsize]; 3 + num_ec]; for i in 0..num_ec { let alpha = ec_blending[i].alpha_channel; let clamp = ec_blending[i].clamp; let alpha_associated = extra_channel_info[alpha].alpha_associated(); match ec_blending[i].mode { PatchBlendMode::Add => { for x in 0..xsize { tmp[3 + i][x] = bg[3 + i].as_mut()[x] + fg[3 + i].as_ref()[x]; } } PatchBlendMode::BlendAbove => { if i == alpha { for x in 0..xsize { let fa = maybe_clamp(fg[3 + alpha].as_ref()[x], clamp); tmp[3 + i][x] = 1.0 - (1.0 - fa) * (1.0 - bg[3 + i].as_mut()[x]); } } else if alpha_associated { for x in 0..xsize { let fa = maybe_clamp(fg[3 + alpha].as_ref()[x], clamp); tmp[3 + i][x] = fg[3 + i].as_ref()[x] + bg[3 + i].as_mut()[x] * (1.0 - fa); } } else { for x in 0..xsize { let fa = maybe_clamp(fg[3 + alpha].as_ref()[x], clamp); let new_a = 1.0 - (1.0 - fa) * (1.0 - bg[3 + alpha].as_mut()[x]); let rnew_a = if new_a > 0.0 { 1.0 / new_a } else { 0.0 }; tmp[3 + i][x] = (fg[3 + i].as_ref()[x] * fa + bg[3 + i].as_mut()[x] * bg[3 + alpha].as_mut()[x] * (1.0 - fa)) * rnew_a; } } } PatchBlendMode::BlendBelow => { if i == alpha { for x in 0..xsize { let ba = maybe_clamp(bg[3 + alpha].as_mut()[x], clamp); tmp[3 + i][x] = 1.0 - (1.0 - ba) * (1.0 - fg[3 + i].as_ref()[x]); } } else if alpha_associated { for x in 0..xsize { let ba = maybe_clamp(bg[3 + alpha].as_mut()[x], clamp); tmp[3 + i][x] = bg[3 + i].as_mut()[x] + fg[3 + i].as_ref()[x] * (1.0 - ba); } } else { for x in 0..xsize { let ba = maybe_clamp(bg[3 + alpha].as_mut()[x], clamp); let new_a = 1.0 - (1.0 - ba) * (1.0 - fg[3 + alpha].as_ref()[x]); let rnew_a = if new_a > 0.0 { 1.0 / new_a } else { 0.0 }; tmp[3 + i][x] = (bg[3 + i].as_mut()[x] * ba + fg[3 + i].as_ref()[x] * fg[3 + alpha].as_ref()[x] * (1.0 - ba)) * rnew_a; } } } PatchBlendMode::AlphaWeightedAddAbove => { if i == alpha { tmp[3 + i].copy_from_slice(bg[3 + i].as_mut()); } else if clamp { for x in 0..xsize { tmp[3 + i][x] = bg[3 + i].as_mut()[x] + fg[3 + i].as_ref()[x] * fg[3 + alpha].as_ref()[x].clamp(0.0, 1.0); } } else { for x in 0..xsize { tmp[3 + i][x] = bg[3 + i].as_mut()[x] + fg[3 + i].as_ref()[x] * fg[3 + alpha].as_ref()[x]; } } } PatchBlendMode::AlphaWeightedAddBelow => { if i == alpha { tmp[3 + i].copy_from_slice(fg[3 + i].as_ref()); } else if clamp { for x in 0..xsize { tmp[3 + i][x] = fg[3 + i].as_ref()[x] + bg[3 + i].as_mut()[x] * bg[3 + alpha].as_mut()[x].clamp(0.0, 1.0); } } else { for x in 0..xsize { tmp[3 + i][x] = fg[3 + i].as_ref()[x] + bg[3 + i].as_mut()[x] * bg[3 + alpha].as_mut()[x]; } } } PatchBlendMode::Mul => { if clamp { for x in 0..xsize { tmp[3 + i][x] = bg[3 + i].as_mut()[x] * fg[3 + i].as_ref()[x].clamp(0.0, 1.0); } } else { for x in 0..xsize { tmp[3 + i][x] = bg[3 + i].as_mut()[x] * fg[3 + i].as_ref()[x]; } } } PatchBlendMode::Replace => { tmp[3 + i].copy_from_slice(fg[3 + i].as_ref()); } PatchBlendMode::None => { tmp[3 + i].copy_from_slice(bg[3 + i].as_mut()); } } } let alpha = color_blending.alpha_channel; let clamp = color_blending.clamp; match color_blending.mode { PatchBlendMode::Add => { for c in 0..3 { for x in 0..xsize { tmp[c][x] = bg[c].as_mut()[x] + fg[c].as_ref()[x]; } } } PatchBlendMode::AlphaWeightedAddAbove => { for c in 0..3 { if !has_alpha { for x in 0..xsize { tmp[c][x] = bg[c].as_mut()[x] + fg[c].as_ref()[x]; } } else if clamp { for x in 0..xsize { tmp[c][x] = bg[c].as_mut()[x] + fg[c].as_ref()[x] * fg[3 + alpha].as_ref()[x].clamp(0.0, 1.0); } } else { for x in 0..xsize { tmp[c][x] = bg[c].as_mut()[x] + fg[c].as_ref()[x] * fg[3 + alpha].as_ref()[x]; } } } } PatchBlendMode::AlphaWeightedAddBelow => { for c in 0..3 { if !has_alpha { for x in 0..xsize { tmp[c][x] = bg[c].as_mut()[x] + fg[c].as_ref()[x]; } } else if clamp { for x in 0..xsize { tmp[c][x] = fg[c].as_ref()[x] + bg[c].as_mut()[x] * bg[3 + alpha].as_mut()[x].clamp(0.0, 1.0); } } else { for x in 0..xsize { tmp[c][x] = fg[c].as_ref()[x] + bg[c].as_mut()[x] * bg[3 + alpha].as_mut()[x]; } } } } PatchBlendMode::BlendAbove => { if !has_alpha { for c in 0..3 { tmp[c].copy_from_slice(fg[c].as_ref()); } } else if extra_channel_info[alpha].alpha_associated() { for x in 0..xsize { let fa = maybe_clamp(fg[3 + alpha].as_ref()[x], clamp); for c in 0..3 { tmp[c][x] = fg[c].as_ref()[x] + bg[c].as_mut()[x] * (1.0 - fa); } tmp[3 + alpha][x] = 1.0 - (1.0 - fa) * (1.0 - bg[3 + alpha].as_mut()[x]); } } else { for x in 0..xsize { let fa = maybe_clamp(fg[3 + alpha].as_ref()[x], clamp); let new_a = 1.0 - (1.0 - fa) * (1.0 - bg[3 + alpha].as_mut()[x]); let rnew_a = if new_a > 0.0 { 1.0 / new_a } else { 0.0 }; for c in 0..3 { tmp[c][x] = (fg[c].as_ref()[x] * fa + bg[c].as_mut()[x] * bg[3 + alpha].as_mut()[x] * (1.0 - fa)) * rnew_a; } tmp[3 + alpha][x] = new_a; } } } PatchBlendMode::BlendBelow => { if !has_alpha { for c in 0..3 { tmp[c].copy_from_slice(bg[c].as_mut()); } } else if extra_channel_info[alpha].alpha_associated() { for x in 0..xsize { let ba = maybe_clamp(bg[3 + alpha].as_mut()[x], clamp); for c in 0..3 { tmp[c][x] = bg[c].as_mut()[x] + fg[c].as_ref()[x] * (1.0 - ba); } tmp[3 + alpha][x] = 1.0 - (1.0 - ba) * (1.0 - fg[3 + alpha].as_ref()[x]); } } else { for x in 0..xsize { let ba = maybe_clamp(bg[3 + alpha].as_mut()[x], clamp); let new_a = 1.0 - (1.0 - ba) * (1.0 - fg[3 + alpha].as_ref()[x]); let rnew_a = if new_a > 0.0 { 1.0 / new_a } else { 0.0 }; for c in 0..3 { tmp[c][x] = (bg[c].as_mut()[x] * ba + fg[c].as_ref()[x] * fg[3 + alpha].as_ref()[x] * (1.0 - ba)) * rnew_a; } tmp[3 + alpha][x] = new_a; } } } PatchBlendMode::Mul => { for c in 0..3 { for x in 0..xsize { tmp[c][x] = bg[c].as_mut()[x] * maybe_clamp(fg[c].as_ref()[x], clamp); } } } PatchBlendMode::Replace => { for c in 0..3 { tmp[c].copy_from_slice(fg[c].as_ref()); } } PatchBlendMode::None => { for c in 0..3 { tmp[c].copy_from_slice(bg[c].as_mut()); } } } for i in 0..(3 + num_ec) { bg[i].as_mut().copy_from_slice(&tmp[i]); } } #[cfg(test)] mod tests { fn clamp(x: f32) -> f32 { x.clamp(0.0, 1.0) } mod perform_blending_tests { use super::{super::*, *}; use crate::{headers::bit_depth::BitDepth, util::test::assert_all_almost_abs_eq}; use test_log::test; const ABS_DELTA: f32 = 1e-6; // Helper for expected value calculations based on C++ logic // Alpha compositing formula: Ao = As + Ab * (1 - As) // Used for kBlend modes for the alpha channel itself. fn expected_alpha_blend(fg_a: f32, bg_a: f32) -> f32 { fg_a + bg_a * (1.0 - fg_a) } // Color compositing for kBlend, premultiplied alpha: Co = Cs_premult + Cb_premult * (1 - As) fn expected_color_blend_premultiplied(c_fg: f32, c_bg: f32, fg_a: f32) -> f32 { c_fg + c_bg * (1.0 - fg_a) } // Color compositing for kBlend, non-premultiplied alpha: Co = (Cs * As + Cb * Ab * (1 - As)) / Ao_blend fn expected_color_blend_non_premultiplied( c_fg: f32, fg_a: f32, // Foreground color and its alpha c_bg: f32, bg_a: f32, // Background color and its alpha alpha_blend_out: f32, // The resulting alpha from expected_alpha_blend(fg_a, bg_a) ) -> f32 { if alpha_blend_out.abs() < ABS_DELTA { // Avoid division by zero 0.0 } else { (c_fg * fg_a + c_bg * bg_a * (1.0 - fg_a)) / alpha_blend_out } } // For kAlphaWeightedAdd modes: Co = Cb + Cs * As fn expected_alpha_weighted_add(c_bg: f32, c_fg: f32, fg_a: f32) -> f32 { c_bg + c_fg * fg_a } // For kMul mode: Co = Cb * Cs fn expected_mul_blend(c_bg: f32, c_fg: f32) -> f32 { c_bg * c_fg } #[test] fn test_color_replace_fg_over_bg() { let mut bg_r = [0.1]; let mut bg_g = [0.2]; let mut bg_b = [0.3]; let fg_r = [0.7]; let fg_g = [0.8]; let fg_b = [0.9]; let mut bg_channels: [&mut [f32]; 3] = [&mut bg_r, &mut bg_g, &mut bg_b]; let fg_channels: [&[f32]; 3] = [&fg_r, &fg_g, &fg_b]; let color_blending = PatchBlending { mode: PatchBlendMode::Replace, alpha_channel: 0, // Not used for Replace clamp: false, }; let ec_blending: [PatchBlending; 0] = []; let extra_channel_info: [ExtraChannelInfo; 0] = []; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); // Expected: output color is fg color assert_all_almost_abs_eq(&bg_r, &fg_r, ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &fg_g, ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &fg_b, ABS_DELTA); } #[test] fn test_color_add() { let mut bg_r = [0.1]; let mut bg_g = [0.2]; let mut bg_b = [0.3]; let fg_r = [0.7]; let fg_g = [0.6]; let fg_b = [0.5]; let expected_r = [bg_r[0] + fg_r[0]]; let expected_g = [bg_g[0] + fg_g[0]]; let expected_b = [bg_b[0] + fg_b[0]]; let mut bg_channels: [&mut [f32]; 3] = [&mut bg_r, &mut bg_g, &mut bg_b]; let fg_channels: [&[f32]; 3] = [&fg_r, &fg_g, &fg_b]; let color_blending = PatchBlending { mode: PatchBlendMode::Add, alpha_channel: 0, // Not used clamp: false, }; let ec_blending: [PatchBlending; 0] = []; let extra_channel_info: [ExtraChannelInfo; 0] = []; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); assert_all_almost_abs_eq(&bg_r, &expected_r, ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &expected_g, ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &expected_b, ABS_DELTA); } #[test] fn test_color_blend_above_premultiplied_alpha() { // BG: R=0.1, G=0.2, B=0.3, A=0.8 (premultiplied) // FG: R=0.4, G=0.3, B=0.2, A=0.5 (premultiplied) let mut bg_r = [0.1]; let mut bg_g = [0.2]; let mut bg_b = [0.3]; let mut bg_a = [0.8]; let fg_r = [0.4]; let fg_g = [0.3]; let fg_b = [0.2]; let fg_a = [0.5]; let fga = fg_a[0]; // Not clamped let bga = bg_a[0]; // Expected alpha: Ao = Afg + Abg * (1 - Afg) let expected_a_val = expected_alpha_blend(fga, bga); // Expected color: Co = Cfg_premult + Cbg_premult * (1 - Afg) let expected_r_val = expected_color_blend_premultiplied(fg_r[0], bg_r[0], fga); let expected_g_val = expected_color_blend_premultiplied(fg_g[0], bg_g[0], fga); let expected_b_val = expected_color_blend_premultiplied(fg_b[0], bg_b[0], fga); let mut bg_channels: [&mut [f32]; 4] = [&mut bg_r, &mut bg_g, &mut bg_b, &mut bg_a]; let fg_channels: [&[f32]; 4] = [&fg_r, &fg_g, &fg_b, &fg_a]; let color_blending = PatchBlending { mode: PatchBlendMode::BlendAbove, alpha_channel: 0, // EC0 is the alpha channel clamp: false, }; // EC0 (alpha) blending rule. // For BlendAbove color mode, the alpha channel itself is also blended using source-over. // So this ec_blending rule for alpha will be effectively overridden by color blending's alpha calculation. let ec_blending = [PatchBlending { mode: PatchBlendMode::Replace, // Arbitrary, will be overwritten by color blend alpha_channel: 0, clamp: false, }]; let extra_channel_info = [ExtraChannelInfo::new( false, ExtraChannel::Alpha, BitDepth::f32(), // Assuming f32 0, "alpha".to_string(), true, // alpha_associated = true (premultiplied) None, None, )]; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); assert_all_almost_abs_eq(&bg_a, &[expected_a_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_r, &[expected_r_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &[expected_g_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &[expected_b_val], ABS_DELTA); } #[test] fn test_color_blend_above_non_premultiplied_alpha() { // BG: R=0.1, G=0.2, B=0.3 (unpremult), A=0.8 // FG: R=0.7, G=0.6, B=0.5 (unpremult), A=0.5 let mut bg_r = [0.1]; let mut bg_g = [0.2]; let mut bg_b = [0.3]; let mut bg_a = [0.8]; let fg_r = [0.7]; let fg_g = [0.6]; let fg_b = [0.5]; let fg_a = [0.5]; let fga = fg_a[0]; let bga = bg_a[0]; // Expected alpha: Ao = Afg + Abg * (1 - Afg) let expected_a_val = expected_alpha_blend(fga, bga); // Expected color: Co = (Cfg_unpremult * Afg + Cbg_unpremult * Abg * (1 - Afg)) / Ao_blend let expected_r_val = expected_color_blend_non_premultiplied(fg_r[0], fga, bg_r[0], bga, expected_a_val); let expected_g_val = expected_color_blend_non_premultiplied(fg_g[0], fga, bg_g[0], bga, expected_a_val); let expected_b_val = expected_color_blend_non_premultiplied(fg_b[0], fga, bg_b[0], bga, expected_a_val); let mut bg_channels: [&mut [f32]; 4] = [&mut bg_r, &mut bg_g, &mut bg_b, &mut bg_a]; let fg_channels: [&[f32]; 4] = [&fg_r, &fg_g, &fg_b, &fg_a]; let color_blending = PatchBlending { mode: PatchBlendMode::BlendAbove, alpha_channel: 0, // EC0 clamp: false, }; let ec_blending = [PatchBlending { // This will be overwritten for the alpha channel by color blending mode: PatchBlendMode::Replace, alpha_channel: 0, clamp: false, }]; let extra_channel_info = [ExtraChannelInfo::new( false, ExtraChannel::Alpha, BitDepth::f32(), 0, "alpha".to_string(), false, // alpha_associated = false (non-premultiplied) None, None, )]; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); assert_all_almost_abs_eq(&bg_a, &[expected_a_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_r, &[expected_r_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &[expected_g_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &[expected_b_val], ABS_DELTA); } #[test] fn test_color_alpha_weighted_add_above() { let mut bg_r = [0.1]; let mut bg_g = [0.2]; let mut bg_b = [0.3]; let mut bg_a = [0.8]; // bg alpha used by ec_blending let fg_r = [0.7]; let fg_g = [0.6]; let fg_b = [0.5]; let fg_a = [0.5]; // fg alpha used for weighting let fga_for_weighting = fg_a[0]; // Not clamped as color_blending.clamp is false // Expected color: Co = Cbg + Cfg * Afg_for_weighting let expected_r_val = expected_alpha_weighted_add(bg_r[0], fg_r[0], fga_for_weighting); let expected_g_val = expected_alpha_weighted_add(bg_g[0], fg_g[0], fga_for_weighting); let expected_b_val = expected_alpha_weighted_add(bg_b[0], fg_b[0], fga_for_weighting); // Expected alpha (EC0): Blended according to ec_blending[0]. // Mode is BlendAbove, alpha_channel is 0 (itself). // C++: PerformAlphaBlending(bg[3+0], bg[3+0], fg[3+0], fg[3+0], tmp.Row(3+0), ...) // This means it's the "alpha channel blends itself" case: Ao = Afg + Abg * (1 - Afg) // fg_alpha_for_ec0 = fg_a[0], bg_alpha_for_ec0 = bg_a[0]. ec_blending[0].clamp is false. let expected_a_val = expected_alpha_blend(fg_a[0], bg_a[0]); let mut bg_channels: [&mut [f32]; 4] = [&mut bg_r, &mut bg_g, &mut bg_b, &mut bg_a]; let fg_channels: [&[f32]; 4] = [&fg_r, &fg_g, &fg_b, &fg_a]; let color_blending = PatchBlending { mode: PatchBlendMode::AlphaWeightedAddAbove, alpha_channel: 0, // EC0 is alpha clamp: false, }; // For AlphaWeightedAdd color mode, the alpha channel (EC0) value is determined by its ec_blending rule. // Let's make EC0 blend itself using BlendAbove mode. let ec_blending = [PatchBlending { mode: PatchBlendMode::BlendAbove, // Alpha channel EC0 blends itself alpha_channel: 0, // using itself as alpha reference clamp: false, }]; let extra_channel_info = [ExtraChannelInfo::new( false, ExtraChannel::Alpha, BitDepth::f32(), 0, "alpha".to_string(), true, // alpha_associated (doesn't directly affect AWA color, but affects EC0's own blend) None, None, )]; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); assert_all_almost_abs_eq(&bg_r, &[expected_r_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &[expected_g_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &[expected_b_val], ABS_DELTA); assert_all_almost_abs_eq(&bg_a, &[expected_a_val], ABS_DELTA); } #[test] fn test_color_mul_with_clamp() { let mut bg_r = [0.5]; let mut bg_g = [0.8]; let mut bg_b = [1.0]; let fg_r = [1.5]; let fg_g = [-0.2]; let fg_b = [0.5]; // fg values will be clamped let expected_r = [expected_mul_blend(bg_r[0], clamp(fg_r[0]))]; // 0.5 * 1.0 = 0.5 let expected_g = [expected_mul_blend(bg_g[0], clamp(fg_g[0]))]; // 0.8 * 0.0 = 0.0 let expected_b = [expected_mul_blend(bg_b[0], clamp(fg_b[0]))]; // 1.0 * 0.5 = 0.5 let mut bg_channels: [&mut [f32]; 3] = [&mut bg_r, &mut bg_g, &mut bg_b]; let fg_channels: [&[f32]; 3] = [&fg_r, &fg_g, &fg_b]; let color_blending = PatchBlending { mode: PatchBlendMode::Mul, alpha_channel: 0, // Not used clamp: true, // Clamp fg values }; let ec_blending: [PatchBlending; 0] = []; let extra_channel_info: [ExtraChannelInfo; 0] = []; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); assert_all_almost_abs_eq(&bg_r, &expected_r, ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &expected_g, ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &expected_b, ABS_DELTA); } #[test] fn test_ec_blend_data_with_separate_alpha_premultiplied() { // Color: Replace FG over BG (to keep it simple) // EC0: Data channel // EC1: Alpha channel for EC0 let mut bg_r = [0.1]; let mut bg_g = [0.1]; let mut bg_b = [0.1]; let mut bg_ec0 = [0.2]; let mut bg_ec1_alpha = [0.9]; // EC1 is alpha for EC0 let fg_r = [0.5]; let fg_g = [0.5]; let fg_b = [0.5]; let fg_ec0 = [0.6]; let fg_ec1_alpha = [0.4]; // EC1 (Alpha channel for EC0) blending: BlendAbove, uses itself as alpha. // fg_alpha = fg_ec1_alpha[0], bg_alpha = bg_ec1_alpha[0] let expected_out_ec1_alpha = expected_alpha_blend(fg_ec1_alpha[0], bg_ec1_alpha[0]); // EC0 (Data channel) blending: BlendAbove, uses EC1 as alpha. // fg_alpha_for_ec0 = fg_ec1_alpha[0] (not clamped as ec_blending[0].clamp is false) // is_premultiplied = extra_channel_info[ec_blending[0].alpha_channel (is 1)].alpha_associated = true. // Formula: out = fg_data + bg_data * (1.f - fg_alpha_of_data) let expected_out_ec0 = expected_color_blend_premultiplied(fg_ec0[0], bg_ec0[0], fg_ec1_alpha[0]); let mut bg_channels: [&mut [f32]; 5] = [ &mut bg_r, &mut bg_g, &mut bg_b, &mut bg_ec0, &mut bg_ec1_alpha, ]; let fg_channels: [&[f32]; 5] = [&fg_r, &fg_g, &fg_b, &fg_ec0, &fg_ec1_alpha]; let color_blending = PatchBlending { // Simple color replace mode: PatchBlendMode::Replace, alpha_channel: 0, clamp: false, }; let ec_blending = [ PatchBlending { // EC0 (data) uses EC1 as alpha mode: PatchBlendMode::BlendAbove, alpha_channel: 1, // EC1 is alpha for EC0 clamp: false, }, PatchBlending { // EC1 (alpha) blends itself mode: PatchBlendMode::BlendAbove, alpha_channel: 1, // EC1 uses itself as alpha clamp: false, }, ]; let extra_channel_info = [ ExtraChannelInfo::new( false, ExtraChannel::Unknown, BitDepth::f32(), 0, "ec0".to_string(), false, None, None, ), // EC0 data ExtraChannelInfo::new( false, ExtraChannel::Alpha, BitDepth::f32(), 0, "alpha_for_ec0".to_string(), true, // EC1 is premultiplied alpha None, None, ), // EC1 alpha ]; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); // Expected Color (Replace) assert_all_almost_abs_eq(&bg_r, &fg_r, ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &fg_g, ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &fg_b, ABS_DELTA); assert_all_almost_abs_eq(&bg_ec1_alpha, &[expected_out_ec1_alpha], ABS_DELTA); assert_all_almost_abs_eq(&bg_ec0, &[expected_out_ec0], ABS_DELTA); } #[test] fn test_no_alpha_channel_blend_above_falls_back_to_copy_fg() { let mut bg_r = [0.1]; let mut bg_g = [0.2]; let mut bg_b = [0.3]; let fg_r = [0.7]; let fg_g = [0.8]; let fg_b = [0.9]; let mut bg_channels: [&mut [f32]; 3] = [&mut bg_r, &mut bg_g, &mut bg_b]; let fg_channels: [&[f32]; 3] = [&fg_r, &fg_g, &fg_b]; let color_blending = PatchBlending { mode: PatchBlendMode::BlendAbove, alpha_channel: 0, // Irrelevant as no alpha EIs clamp: false, }; let ec_blending: [PatchBlending; 0] = []; // No ExtraChannelInfo means has_alpha will be false. let extra_channel_info: [ExtraChannelInfo; 0] = []; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); // Expected: output color is fg color due to fallback assert_all_almost_abs_eq(&bg_r, &fg_r, ABS_DELTA); assert_all_almost_abs_eq(&bg_g, &fg_g, ABS_DELTA); assert_all_almost_abs_eq(&bg_b, &fg_b, ABS_DELTA); } #[test] fn test_empty_pixels() { let mut bg_r: [f32; 0] = []; let mut bg_g: [f32; 0] = []; let mut bg_b: [f32; 0] = []; let fg_r: [f32; 0] = []; let fg_g: [f32; 0] = []; let fg_b: [f32; 0] = []; let mut bg_channels: [&mut [f32]; 3] = [&mut bg_r, &mut bg_g, &mut bg_b]; let fg_channels: [&[f32]; 3] = [&fg_r, &fg_g, &fg_b]; let color_blending = PatchBlending { mode: PatchBlendMode::Replace, alpha_channel: 0, clamp: false, }; let ec_blending: [PatchBlending; 0] = []; let extra_channel_info: [ExtraChannelInfo; 0] = []; perform_blending( &mut bg_channels, &fg_channels, &color_blending, &ec_blending, &extra_channel_info, ); // Expect output slices to also be empty and unchanged. assert_eq!(bg_r.len(), 0); assert_eq!(bg_g.len(), 0); assert_eq!(bg_b.len(), 0); } } }