use std::cell::RefCell;
use crate::pipe::{self, PipeSrc, PipeState, blend};
use crate::types::BlendMode;
use color::Pixel;
use color::convert::div255;
const MAX_COMPS: usize = 8;
thread_local! {
static PAT_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors C++ SplashPipe API; all parameters are necessary"
)]
pub(crate) fn render_span_general<P: Pixel>(
pipe: &PipeState<'_>,
src: &PipeSrc<'_>,
dst_pixels: &mut [u8],
dst_alpha: Option<&mut [u8]>,
shape: Option<&[u8]>,
x0: i32,
x1: i32,
y: i32,
) {
debug_assert!(x1 >= x0, "render_span_general: x1={x1} < x0={x0}");
#[expect(
clippy::cast_sign_loss,
reason = "x1 >= x0 is asserted above, so x1 - x0 + 1 >= 1 > 0"
)]
let count = (x1 - x0 + 1) as usize;
let ncomps = P::BYTES;
debug_assert_eq!(dst_pixels.len(), count * ncomps);
if let Some(sh) = shape {
debug_assert_eq!(sh.len(), count);
}
if let Some(sm) = pipe.soft_mask {
debug_assert_eq!(sm.len(), count, "soft_mask length must equal span count");
}
let a_input = u32::from(pipe.a_input);
let is_nonseparable = matches!(
pipe.blend_mode,
BlendMode::Hue | BlendMode::Saturation | BlendMode::Color | BlendMode::Luminosity
);
let is_cmyk_like = ncomps == 4 || ncomps == 8;
let shape_at = |i: usize| shape.map_or(0xFFu8, |s| s[i]);
let soft_mask_at = |i: usize| pipe.soft_mask.map_or(0xFFu8, |s| s[i]);
let alpha0_at = |i: usize| pipe.alpha0.map(|a| a[i]);
match src {
PipeSrc::Solid(color) => {
debug_assert_eq!(color.len(), ncomps);
render_span_general_inner(
pipe,
|_i| color,
dst_pixels,
dst_alpha,
shape,
count,
ncomps,
a_input,
is_nonseparable,
is_cmyk_like,
&shape_at,
&soft_mask_at,
&alpha0_at,
);
}
PipeSrc::Pattern(pat) => {
PAT_BUF.with(|cell| {
let mut buf = cell.borrow_mut();
buf.resize(count * ncomps, 0);
pat.fill_span(y, x0, x1, &mut buf[..count * ncomps]);
render_span_general_inner(
pipe,
|i| &buf[i * ncomps..(i + 1) * ncomps],
dst_pixels,
dst_alpha,
shape,
count,
ncomps,
a_input,
is_nonseparable,
is_cmyk_like,
&shape_at,
&soft_mask_at,
&alpha0_at,
);
});
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "all params necessary; closure eliminates solid/pattern duplication"
)]
#[expect(
clippy::too_many_lines,
reason = "compositing formula has many branches that cannot be meaningfully split"
)]
#[expect(
clippy::single_match_else,
reason = "both Some and None arms have substantial independent logic; if-let would be less clear"
)]
fn render_span_general_inner<'src>(
pipe: &PipeState<'_>,
src_px_at: impl Fn(usize) -> &'src [u8],
dst_pixels: &mut [u8],
dst_alpha: Option<&mut [u8]>,
shape: Option<&[u8]>,
count: usize,
ncomps: usize,
a_input: u32,
is_nonseparable: bool,
is_cmyk_like: bool,
shape_at: &dyn Fn(usize) -> u8,
soft_mask_at: &dyn Fn(usize) -> u8,
alpha0_at: &dyn Fn(usize) -> Option<u8>,
) {
let has_soft_mask = pipe.soft_mask.is_some();
let has_shape = shape.is_some();
match dst_alpha {
Some(dst_alpha) => {
debug_assert_eq!(dst_alpha.len(), count);
for i in 0..count {
let src_px = src_px_at(i);
let dst_px = &mut dst_pixels[i * ncomps..(i + 1) * ncomps];
let a_dst = u32::from(dst_alpha[i]);
let shape_v = u32::from(shape_at(i));
let soft_v = u32::from(soft_mask_at(i));
let a_src = compute_a_src(a_input, soft_v, shape_v, has_soft_mask, has_shape);
if pipe.knockout && shape_v >= u32::from(pipe.knockout_opacity) {
dst_alpha[i] = 0;
}
let mut c_src_corr: [u8; MAX_COMPS] = [0; MAX_COMPS];
let c_src: &[u8] = if pipe.non_isolated_group && shape_v != 0 {
let t = (a_dst * 255) / shape_v - a_dst;
let t_i = t.cast_signed(); for j in 0..ncomps {
let v = i32::from(src_px[j])
+ (i32::from(src_px[j]) - i32::from(dst_px[j])) * t_i / 255;
#[expect(
clippy::cast_sign_loss,
reason = "value is clamped to [0, 255] above"
)]
{
c_src_corr[j] = v.clamp(0, 255) as u8;
}
}
&c_src_corr[..ncomps]
} else {
src_px
};
let mut c_blend: [u8; MAX_COMPS] = [0; MAX_COMPS];
if pipe.blend_mode != BlendMode::Normal {
apply_blend_fn(
pipe.blend_mode,
c_src,
dst_px,
&mut c_blend[..ncomps],
is_cmyk_like,
is_nonseparable,
);
}
let a_dst_eff = u32::from(dst_alpha[i]); let (a_result, alpha_i, alpha_im1) =
compute_alphas(a_src, a_dst_eff, shape_v, alpha0_at(i), pipe.knockout);
if a_result == 0 {
dst_px.fill(0);
} else {
debug_assert!(alpha_i > 0, "alpha_i must be > 0 when a_result > 0");
for j in 0..ncomps {
let c_src_j = u32::from(c_src[j]);
let c_dst_j = u32::from(dst_px[j]);
let c_b_j = u32::from(c_blend[j]);
let c = if pipe.blend_mode == BlendMode::Normal {
((alpha_i - a_src) * c_dst_j + a_src * c_src_j) / alpha_i
} else {
((alpha_i - a_src) * c_dst_j
+ a_src * ((255 - alpha_im1) * c_src_j + alpha_im1 * c_b_j) / 255)
/ alpha_i
};
#[expect(
clippy::cast_possible_truncation,
reason = "c is a weighted average of values ≤ 255, so c ≤ 255"
)]
{
dst_px[j] = c as u8;
}
}
finish_pixel(pipe, dst_px, src_px, ncomps);
}
#[expect(
clippy::cast_possible_truncation,
reason = "a_result is clamped to ≤ 255 in compute_alphas"
)]
{
dst_alpha[i] = a_result as u8;
}
}
}
None => {
debug_assert!(
!pipe.non_isolated_group && !pipe.knockout,
"non_isolated_group/knockout require dst_alpha; None arm uses implicit a_dst=255"
);
for i in 0..count {
let src_px = src_px_at(i);
let dst_px = &mut dst_pixels[i * ncomps..(i + 1) * ncomps];
let shape_v = u32::from(shape_at(i));
let soft_v = u32::from(soft_mask_at(i));
let a_src = compute_a_src(a_input, soft_v, shape_v, has_soft_mask, has_shape);
debug_assert!(a_src <= 255, "a_src={a_src} out of [0, 255]");
let mut c_blend: [u8; MAX_COMPS] = [0; MAX_COMPS];
if pipe.blend_mode != BlendMode::Normal {
apply_blend_fn(
pipe.blend_mode,
src_px,
dst_px,
&mut c_blend[..ncomps],
is_cmyk_like,
is_nonseparable,
);
}
for j in 0..ncomps {
let c_src_j = u32::from(src_px[j]);
let c_dst_j = u32::from(dst_px[j]);
let c_b_j = u32::from(c_blend[j]);
let c = if pipe.blend_mode == BlendMode::Normal {
u32::from(div255((255 - a_src) * c_dst_j + a_src * c_src_j))
} else {
u32::from(div255((255 - a_src) * c_dst_j + a_src * c_b_j))
};
#[expect(
clippy::cast_possible_truncation,
reason = "c is a weighted average of values ≤ 255, so c ≤ 255"
)]
{
dst_px[j] = c as u8;
}
}
finish_pixel(pipe, dst_px, src_px, ncomps);
}
}
}
}
#[inline]
fn compute_a_src(
a_input: u32,
soft_v: u32,
shape_v: u32,
has_soft_mask: bool,
has_shape: bool,
) -> u32 {
if has_soft_mask {
if has_shape {
u32::from(div255(u32::from(div255(a_input * soft_v)) * shape_v))
} else {
u32::from(div255(a_input * soft_v))
}
} else if has_shape {
u32::from(div255(a_input * shape_v))
} else {
a_input
}
}
#[expect(
clippy::option_if_let_else,
reason = "if-let form is clearer than map_or_else for this multi-branch alpha computation"
)]
fn compute_alphas(
a_src: u32,
a_dst: u32,
shape: u32,
alpha0: Option<u8>,
knockout: bool,
) -> (u32, u32, u32) {
if let Some(a0) = alpha0 {
let a0 = u32::from(a0);
if knockout {
let a_result = a_src + u32::from(div255(a_dst * (255 - shape)));
let alpha_i = a_result + a0 - u32::from(div255(a_result * a0));
(a_result.min(255), alpha_i.min(255), a0)
} else {
let a_result = a_src + a_dst - u32::from(div255(a_src * a_dst));
let alpha_i = a_result + a0 - u32::from(div255(a_result * a0));
let alpha_im1 = a0 + a_dst - u32::from(div255(a0 * a_dst));
(a_result.min(255), alpha_i.min(255), alpha_im1.min(255))
}
} else if knockout {
let a_result = a_src + u32::from(div255(a_dst * (255 - shape)));
(a_result.min(255), a_result.min(255), 0)
} else {
let a_result = a_src + a_dst - u32::from(div255(a_src * a_dst));
(a_result.min(255), a_result.min(255), a_dst)
}
}
fn apply_blend_fn(
mode: BlendMode,
src: &[u8],
dst: &[u8],
c_blend: &mut [u8],
is_cmyk_like: bool,
is_nonseparable: bool,
) {
debug_assert_eq!(src.len(), dst.len());
debug_assert_eq!(src.len(), c_blend.len());
let ncomps = src.len();
if is_cmyk_like {
let mut src2 = [0u8; MAX_COMPS];
let mut dst2 = [0u8; MAX_COMPS];
for j in 0..ncomps {
src2[j] = 255 - src[j];
dst2[j] = 255 - dst[j];
}
if is_nonseparable {
let s3 = [src2[0], src2[1], src2[2]];
let d3 = [dst2[0], dst2[1], dst2[2]];
let r3 = blend::apply_nonseparable_rgb(mode, s3, d3);
c_blend[0] = 255 - r3[0];
c_blend[1] = 255 - r3[1];
c_blend[2] = 255 - r3[2];
if ncomps >= 4 {
c_blend[3] = 255
- (if mode == BlendMode::Luminosity {
src2[3]
} else {
dst2[3]
});
}
for j in 4..ncomps {
c_blend[j] = 255 - dst2[j];
}
} else {
blend::apply_separable(
mode,
&src2[..ncomps.min(4)],
&dst2[..ncomps.min(4)],
&mut c_blend[..ncomps.min(4)],
);
for v in &mut c_blend[..ncomps.min(4)] {
*v = 255 - *v;
}
c_blend[4..ncomps].copy_from_slice(&dst[4..ncomps]);
}
} else if is_nonseparable {
let n = ncomps.min(3);
let mut s3 = [0u8; 3];
let mut d3 = [0u8; 3];
s3[..n].copy_from_slice(&src[..n]);
d3[..n].copy_from_slice(&dst[..n]);
if ncomps == 1 {
s3[1] = s3[0];
s3[2] = s3[0];
d3[1] = d3[0];
d3[2] = d3[0];
}
let r3 = blend::apply_nonseparable_rgb(mode, s3, d3);
c_blend[..n].copy_from_slice(&r3[..n]);
} else {
blend::apply_separable(mode, src, dst, c_blend);
}
}
#[inline]
fn finish_pixel(pipe: &PipeState<'_>, dst_px: &mut [u8], src_px: &[u8], ncomps: usize) {
pipe::apply_transfer_in_place(pipe, dst_px);
if pipe.overprint_mask != 0xFFFF_FFFF {
apply_overprint(pipe, dst_px, src_px, ncomps);
}
}
fn apply_overprint(pipe: &PipeState<'_>, dst_px: &mut [u8], src_px: &[u8], ncomps: usize) {
if pipe.overprint_additive {
for j in 0..ncomps {
if pipe.overprint_mask & (1 << j) == 0 {
continue;
}
dst_px[j] = (u16::from(dst_px[j]) + u16::from(src_px[j])).min(255) as u8;
}
} else {
panic!(
"general pipe: replace overprint (mask={:#010x}) is not yet implemented; \
use overprint_additive=true or preserve pre-blend dst in the caller",
pipe.overprint_mask,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_a_src_no_mask_no_shape_returns_a_input() {
assert_eq!(compute_a_src(200, 0xFF, 0xFF, false, false), 200);
}
#[test]
fn compute_a_src_shape_zero_gives_zero() {
assert_eq!(compute_a_src(255, 0xFF, 0, false, true), 0);
}
#[test]
fn compute_a_src_soft_mask_scales_alpha() {
let result = compute_a_src(255, 128, 0xFF, true, false);
let expected = u32::from(div255(255 * 128));
assert_eq!(result, expected);
}
#[test]
fn compute_a_src_soft_and_shape_combines_both() {
let result = compute_a_src(255, 128, 128, true, true);
let expected = u32::from(div255(u32::from(div255(255 * 128)) * 128));
assert_eq!(result, expected);
}
use crate::pipe::{PipeSrc, PipeState};
use crate::state::TransferSet;
use color::{Gray8, Rgb8, TransferLut};
fn normal_pipe(a: u8) -> PipeState<'static> {
PipeState {
blend_mode: BlendMode::Normal,
a_input: a,
overprint_mask: 0xFFFF_FFFF,
overprint_additive: false,
transfer: TransferSet::identity_rgb(),
soft_mask: None,
alpha0: None,
knockout: false,
knockout_opacity: 255,
non_isolated_group: false,
}
}
#[test]
fn opaque_src_over_any_dst_gives_src() {
let pipe = normal_pipe(255);
let src_color = [200u8, 100, 50];
let src = PipeSrc::Solid(&src_color);
let mut dst = vec![10u8, 20, 30];
let mut alpha = vec![128u8];
render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
assert_eq!(&dst, &[200, 100, 50]);
assert_eq!(alpha[0], 255);
}
#[test]
fn transparent_src_leaves_dst_unchanged() {
let pipe = normal_pipe(0);
let src = PipeSrc::Solid(&[255u8, 255, 255]);
let mut dst = vec![10u8, 20, 30];
let mut alpha = vec![200u8];
render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
assert_eq!(&dst, &[10, 20, 30]);
assert_eq!(alpha[0], 200);
}
#[test]
fn blend_multiply_with_dst() {
let mut pipe = normal_pipe(255);
pipe.blend_mode = BlendMode::Multiply;
let src = PipeSrc::Solid(&[128u8, 128, 128]);
let mut dst = vec![200u8, 200, 200];
let mut alpha = vec![255u8];
render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
let v = dst[0];
assert!((i32::from(v) - 100).abs() <= 1, "expected ~100, got {v}");
}
#[test]
fn compute_alphas_isolated_non_knockout() {
let (ar, ai, aim1) = compute_alphas(128, 200, 255, None, false);
assert!((226..=230).contains(&ar), "a_result={ar}");
assert_eq!(ai, ar, "isolated: alpha_i == a_result");
assert_eq!(aim1, 200, "isolated non-knockout: alpha_im1 == a_dst");
}
#[test]
fn soft_mask_modulates_alpha() {
let soft = vec![128u8];
let mut dst = vec![0u8; 3];
let mut alpha = vec![0u8];
let pipe = PipeState {
blend_mode: BlendMode::Normal,
a_input: 255,
overprint_mask: 0xFFFF_FFFF,
overprint_additive: false,
transfer: TransferSet::identity_rgb(),
soft_mask: Some(soft.as_slice()),
alpha0: None,
knockout: false,
knockout_opacity: 255,
non_isolated_group: false,
};
let src = PipeSrc::Solid(&[255u8, 255, 255]);
render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
assert_eq!(dst[0], 255);
assert!((i32::from(alpha[0]) - 128).abs() <= 2, "alpha={}", alpha[0]);
}
#[test]
fn gray_transfer_lut_applied_correctly() {
static DN: [[u8; 256]; 8] = [TransferLut::IDENTITY.0; 8];
let id = TransferLut::IDENTITY.as_array();
let inv = TransferLut::INVERTED.as_array();
let transfer = TransferSet {
gray: inv, rgb: [id; 3],
cmyk: [id; 4],
device_n: &DN,
};
let pipe = PipeState {
blend_mode: BlendMode::Normal,
a_input: 255,
overprint_mask: 0xFFFF_FFFF,
overprint_additive: false,
transfer,
soft_mask: None,
alpha0: None,
knockout: false,
knockout_opacity: 255,
non_isolated_group: false,
};
let src_color = [100u8];
let src = PipeSrc::Solid(&src_color);
let mut dst = vec![0u8; 1];
let mut alpha = vec![0u8; 1];
render_span_general::<Gray8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
assert_eq!(dst[0], 155, "gray transfer must use gray LUT, not rgb[0]");
}
}