#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
pub use zenblend::BlendMode;
use zenpixels::PixelDescriptor;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CompositeError {
PremultipliedInput,
}
impl core::fmt::Display for CompositeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::PremultipliedInput => write!(
f,
"compositing with RgbaPremul input is not supported \
(premultiply + composite = mathematically incorrect)"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for CompositeError {}
pub trait Background {
fn fill_row(&mut self, dst: &mut [f32], y: u32, channels: u8);
fn is_transparent(&self) -> bool {
false
}
fn is_opaque(&self) -> bool {
false
}
fn solid_pixel(&self) -> Option<&[f32; 4]> {
None
}
}
#[derive(Clone, Copy)]
pub struct NoBackground;
impl Background for NoBackground {
fn fill_row(&mut self, _dst: &mut [f32], _y: u32, _channels: u8) {
}
#[inline(always)]
fn is_transparent(&self) -> bool {
true
}
}
#[derive(Clone)]
pub struct SolidBackground {
pixel: [f32; 4],
opaque: bool,
}
impl SolidBackground {
pub fn from_srgb_u8(r: u8, g: u8, b: u8, a: u8, desc: PixelDescriptor) -> Self {
let lr = linear_srgb::precise::srgb_to_linear(r as f32 / 255.0);
let lg = linear_srgb::precise::srgb_to_linear(g as f32 / 255.0);
let lb = linear_srgb::precise::srgb_to_linear(b as f32 / 255.0);
let fa = if desc.has_alpha() {
a as f32 / 255.0
} else {
1.0
};
Self {
pixel: [lr * fa, lg * fa, lb * fa, fa],
opaque: fa >= 1.0,
}
}
pub fn from_linear(r: f32, g: f32, b: f32, a: f32, desc: PixelDescriptor) -> Self {
let fa = if desc.has_alpha() { a } else { 1.0 };
Self {
pixel: [r * fa, g * fa, b * fa, fa],
opaque: fa >= 1.0,
}
}
pub fn transparent(_desc: PixelDescriptor) -> Self {
Self {
pixel: [0.0; 4],
opaque: false,
}
}
pub fn white(desc: PixelDescriptor) -> Self {
Self::from_srgb_u8(255, 255, 255, 255, desc)
}
pub fn black(desc: PixelDescriptor) -> Self {
Self::from_srgb_u8(0, 0, 0, 255, desc)
}
}
impl Background for SolidBackground {
fn fill_row(&mut self, _dst: &mut [f32], _y: u32, _channels: u8) {
}
#[inline(always)]
fn is_transparent(&self) -> bool {
self.pixel[3] == 0.0
}
#[inline(always)]
fn is_opaque(&self) -> bool {
self.opaque
}
#[inline(always)]
fn solid_pixel(&self) -> Option<&[f32; 4]> {
Some(&self.pixel)
}
}
#[derive(Clone, Copy)]
pub struct SliceBackground<'a> {
data: &'a [f32],
row_len: usize,
}
impl<'a> SliceBackground<'a> {
pub fn new(data: &'a [f32], row_len: usize) -> Self {
Self { data, row_len }
}
}
impl Background for SliceBackground<'_> {
fn fill_row(&mut self, dst: &mut [f32], y: u32, _channels: u8) {
let start = y as usize * self.row_len;
let end = start + self.row_len;
let src = &self.data[start..end];
let copy_len = dst.len().min(src.len());
dst[..copy_len].copy_from_slice(&src[..copy_len]);
}
}
pub struct StreamedBackground {
ring: Vec<Vec<f32>>,
capacity: usize,
write_idx: usize,
rows_pushed: u32,
}
impl StreamedBackground {
pub fn new(capacity: usize, row_len: usize) -> Self {
Self {
ring: (0..capacity).map(|_| vec![0.0f32; row_len]).collect(),
capacity,
write_idx: 0,
rows_pushed: 0,
}
}
pub fn push_row(&mut self, row: &[f32]) {
let slot = self.write_idx % self.capacity;
let dest = &mut self.ring[slot];
let copy_len = dest.len().min(row.len());
dest[..copy_len].copy_from_slice(&row[..copy_len]);
self.write_idx += 1;
self.rows_pushed += 1;
}
pub fn rows_pushed(&self) -> u32 {
self.rows_pushed
}
}
impl Background for StreamedBackground {
fn fill_row(&mut self, dst: &mut [f32], y: u32, _channels: u8) {
let slot = y as usize % self.capacity;
let src = &self.ring[slot];
let copy_len = dst.len().min(src.len());
dst[..copy_len].copy_from_slice(&src[..copy_len]);
}
}
#[inline]
pub fn composite_over_premul(src: &mut [f32], bg: &[f32], channels: u8) {
if channels != 4 {
return; }
zenblend::blend_row(src, bg, BlendMode::SrcOver);
}
#[inline]
pub fn composite_over_solid_premul(src: &mut [f32], pixel: &[f32; 4]) {
zenblend::blend_row_solid(src, pixel, BlendMode::SrcOver);
}
#[inline]
pub fn composite_over_solid_opaque_premul(src: &mut [f32], pixel: &[f32; 4]) {
zenblend::blend_row_solid_opaque(src, pixel, BlendMode::SrcOver);
}
#[inline]
pub fn unpremultiply_f32_row(row: &mut [f32]) {
crate::simd::unpremultiply_alpha_row(row);
}
#[inline]
pub(crate) fn composite_dispatch<B: Background>(
src: &mut [f32],
bg: &mut B,
bg_row_buf: &mut [f32],
out_y: u32,
channels: u8,
mode: BlendMode,
) {
if bg.is_transparent() {
return;
}
if let Some(pixel) = bg.solid_pixel() {
if bg.is_opaque() && mode == BlendMode::SrcOver {
zenblend::blend_row_solid_opaque(src, pixel, mode);
} else {
zenblend::blend_row_solid(src, pixel, mode);
}
} else {
if channels != 4 {
return; }
let row_len = src.len();
bg.fill_row(&mut bg_row_buf[..row_len], out_y, channels);
zenblend::blend_row(src, &bg_row_buf[..row_len], mode);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opaque_fg_ignores_bg() {
let mut src = [1.0, 0.0, 0.0, 1.0];
let bg = [0.0, 1.0, 0.0, 1.0];
composite_over_premul(&mut src, &bg, 4);
assert_eq!(src, [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn transparent_fg_passes_bg() {
let mut src = [0.0, 0.0, 0.0, 0.0];
let bg = [0.0, 0.5, 0.0, 1.0];
composite_over_premul(&mut src, &bg, 4);
assert_eq!(src, [0.0, 0.5, 0.0, 1.0]);
}
#[test]
fn semi_transparent_blend() {
let mut src = [0.5, 0.0, 0.0, 0.5];
let bg = [0.0, 1.0, 0.0, 1.0];
composite_over_premul(&mut src, &bg, 4);
assert!((src[0] - 0.5).abs() < 1e-6);
assert!((src[1] - 0.5).abs() < 1e-6);
assert!((src[2] - 0.0).abs() < 1e-6);
assert!((src[3] - 1.0).abs() < 1e-6);
}
#[test]
fn non_4ch_is_noop() {
let mut src = [0.5, 0.3, 0.1, 0.5, 0.3, 0.1];
let bg = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
let original = src;
composite_over_premul(&mut src, &bg, 3);
assert_eq!(src, original);
}
#[test]
fn solid_opaque_fast_path() {
let mut src = [0.3, 0.0, 0.0, 0.3];
let pixel = [1.0, 1.0, 1.0, 1.0];
composite_over_solid_opaque_premul(&mut src, &pixel);
assert!((src[0] - 1.0).abs() < 1e-6);
assert!((src[1] - 0.7).abs() < 1e-6);
assert!((src[2] - 0.7).abs() < 1e-6);
assert_eq!(src[3], 1.0);
}
#[test]
fn solid_non_opaque_fast_path() {
let mut src = [0.0, 0.0, 0.0, 0.0];
let pixel = [0.0, 0.25, 0.0, 0.5];
composite_over_solid_premul(&mut src, &pixel);
assert_eq!(src, [0.0, 0.25, 0.0, 0.5]);
}
#[test]
fn solid_background_white() {
let bg = SolidBackground::white(PixelDescriptor::RGBA8_SRGB);
assert!(bg.is_opaque());
assert!(!bg.is_transparent());
let pixel = bg.solid_pixel().unwrap();
assert!((pixel[0] - 1.0).abs() < 1e-6);
assert!((pixel[1] - 1.0).abs() < 1e-6);
assert!((pixel[2] - 1.0).abs() < 1e-6);
assert_eq!(pixel[3], 1.0);
}
#[test]
fn solid_background_transparent() {
let bg = SolidBackground::transparent(PixelDescriptor::RGBA8_SRGB);
assert!(bg.is_transparent());
assert!(!bg.is_opaque());
let pixel = bg.solid_pixel().unwrap();
assert_eq!(*pixel, [0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn solid_background_from_srgb() {
let bg = SolidBackground::from_srgb_u8(128, 0, 0, 128, PixelDescriptor::RGBA8_SRGB);
assert!(!bg.is_opaque());
assert!(!bg.is_transparent());
let pixel = bg.solid_pixel().unwrap();
let expected_a = 128.0 / 255.0;
assert!((pixel[3] - expected_a).abs() < 1e-3);
let lr = linear_srgb::precise::srgb_to_linear(128.0 / 255.0);
assert!((pixel[0] - lr * expected_a).abs() < 1e-3);
}
#[test]
fn no_background_is_transparent() {
let bg = NoBackground;
assert!(bg.is_transparent());
}
#[test]
fn slice_background_fill_row() {
let row_len = 2 * 4;
let data: Vec<f32> = (0..3 * row_len).map(|i| i as f32 * 0.01).collect();
let mut bg = SliceBackground::new(&data, row_len);
let mut dst = vec![0.0f32; row_len];
bg.fill_row(&mut dst, 1, 4);
assert_eq!(dst, &data[row_len..2 * row_len]);
}
#[test]
fn streamed_background_push_and_fill() {
let row_len = 8; let mut bg = StreamedBackground::new(3, row_len);
let row0: Vec<f32> = (0..row_len).map(|i| i as f32).collect();
let row1: Vec<f32> = (0..row_len).map(|i| (i + 10) as f32).collect();
let row2: Vec<f32> = (0..row_len).map(|i| (i + 20) as f32).collect();
bg.push_row(&row0);
bg.push_row(&row1);
bg.push_row(&row2);
assert_eq!(bg.rows_pushed(), 3);
let mut dst = vec![0.0f32; row_len];
bg.fill_row(&mut dst, 0, 4);
assert_eq!(dst, row0);
bg.fill_row(&mut dst, 1, 4);
assert_eq!(dst, row1);
bg.fill_row(&mut dst, 2, 4);
assert_eq!(dst, row2);
}
#[test]
fn composite_dispatch_no_background() {
let mut bg = NoBackground;
let mut src = [0.5, 0.3, 0.1, 0.7];
let original = src;
let mut buf = vec![0.0f32; 4];
composite_dispatch(&mut src, &mut bg, &mut buf, 0, 4, BlendMode::SrcOver);
assert_eq!(src, original);
}
#[test]
fn composite_dispatch_solid_opaque() {
let mut bg = SolidBackground::white(PixelDescriptor::RGBA8_SRGB);
let mut src = [0.0, 0.0, 0.0, 0.0]; let mut buf = Vec::new(); composite_dispatch(&mut src, &mut bg, &mut buf, 0, 4, BlendMode::SrcOver);
assert!((src[0] - 1.0).abs() < 1e-6);
assert!((src[3] - 1.0).abs() < 1e-6);
}
#[test]
fn composite_dispatch_slice_background() {
let row_len = 4;
let data = vec![0.0f32, 0.5, 0.0, 1.0]; let mut bg = SliceBackground::new(&data, row_len);
let mut src = [0.0, 0.0, 0.0, 0.0]; let mut buf = vec![0.0f32; row_len];
composite_dispatch(&mut src, &mut bg, &mut buf, 0, 4, BlendMode::SrcOver);
assert_eq!(src, [0.0, 0.5, 0.0, 1.0]);
}
#[test]
fn multi_pixel_composite() {
let mut src = [
1.0, 0.0, 0.0, 1.0, 0.0, 0.25, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, ];
let bg = [
0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, ];
composite_over_premul(&mut src, &bg, 4);
assert_eq!(&src[0..4], &[1.0, 0.0, 0.0, 1.0]);
assert!((src[4] - 0.0).abs() < 1e-6);
assert!((src[5] - 0.25).abs() < 1e-6);
assert!((src[6] - 0.5).abs() < 1e-6);
assert!((src[7] - 1.0).abs() < 1e-6);
assert_eq!(&src[8..12], &[0.0, 0.0, 1.0, 1.0]);
}
}