pub mod aa;
pub mod blend;
pub mod general;
pub mod simple;
use crate::state::TransferSet;
use crate::types::BlendMode;
use color::Pixel;
pub enum PipeSrc<'a> {
Solid(&'a [u8]),
Pattern(&'a dyn Pattern),
}
pub trait Pattern: Send + Sync {
fn fill_span(&self, y: i32, x0: i32, x1: i32, out: &mut [u8]);
fn is_static_color(&self) -> bool {
false
}
}
#[derive(Copy, Clone, Debug)]
pub struct PipeState<'bmp> {
pub blend_mode: BlendMode,
pub a_input: u8,
pub overprint_mask: u32,
pub overprint_additive: bool,
pub transfer: TransferSet<'bmp>,
pub soft_mask: Option<&'bmp [u8]>,
pub alpha0: Option<&'bmp [u8]>,
pub knockout: bool,
pub knockout_opacity: u8,
pub non_isolated_group: bool,
}
impl PipeState<'_> {
#[must_use]
pub const fn no_transparency(&self, uses_shape: bool) -> bool {
self.a_input == 255
&& self.soft_mask.is_none()
&& !uses_shape
&& !self.non_isolated_group
&& !self.knockout
&& self.alpha0.is_none()
&& self.overprint_mask == 0xFFFF_FFFF
}
#[must_use]
pub fn use_aa_path(&self) -> bool {
self.soft_mask.is_none()
&& self.alpha0.is_none()
&& self.blend_mode == BlendMode::Normal
&& !self.non_isolated_group
}
}
#[expect(
clippy::too_many_arguments,
reason = "render_span mirrors the C++ SplashPipe API; all arguments are necessary"
)]
pub fn render_span<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!(x0 <= x1, "render_span: x0={x0} > x1={x1}");
debug_assert!(P::BYTES > 0, "render_span: Mono1 must be handled by caller");
#[expect(
clippy::cast_sign_loss,
reason = "x1 >= x0 is a precondition, so x1 - x0 + 1 >= 1 > 0"
)]
let count = (x1 - x0 + 1) as usize;
debug_assert_eq!(
dst_pixels.len(),
count * P::BYTES,
"render_span: dst_pixels length mismatch"
);
let uses_shape = shape.is_some();
if pipe.no_transparency(uses_shape) && pipe.blend_mode == BlendMode::Normal {
simple::render_span_simple::<P>(pipe, src, dst_pixels, dst_alpha, x0, x1, y);
} else if uses_shape && pipe.use_aa_path() {
aa::render_span_aa::<P>(
pipe,
src,
dst_pixels,
dst_alpha,
shape.expect("use_aa_path requires shape"),
x0,
x1,
y,
);
} else {
general::render_span_general::<P>(pipe, src, dst_pixels, dst_alpha, shape, x0, x1, y);
}
}
#[expect(
clippy::inline_always,
reason = "called per-pixel in the innermost compositing loop; 10-15 instructions, must inline across crate boundary"
)]
#[inline(always)]
pub(crate) fn apply_transfer_pixel(pipe: &PipeState<'_>, src: &[u8], dst: &mut [u8]) {
debug_assert_eq!(
src.len(),
dst.len(),
"apply_transfer_pixel: length mismatch"
);
let t = &pipe.transfer;
match src.len() {
1 => dst[0] = t.gray[src[0] as usize],
3 => {
dst[0] = t.rgb[0][src[0] as usize];
dst[1] = t.rgb[1][src[1] as usize];
dst[2] = t.rgb[2][src[2] as usize];
}
4 => {
dst[0] = t.cmyk[0][src[0] as usize];
dst[1] = t.cmyk[1][src[1] as usize];
dst[2] = t.cmyk[2][src[2] as usize];
dst[3] = t.cmyk[3][src[3] as usize];
}
8 => {
for (i, (&s, d)) in src.iter().zip(dst.iter_mut()).enumerate() {
*d = t.device_n[i][s as usize];
}
}
n => {
debug_assert!(false, "apply_transfer_pixel: unexpected ncomps={n}");
dst.copy_from_slice(src);
}
}
}
#[expect(
clippy::inline_always,
reason = "called per-pixel in the innermost compositing loop; delegates to apply_transfer_pixel, must inline"
)]
#[inline(always)]
pub(crate) fn apply_transfer_in_place(pipe: &PipeState<'_>, px: &mut [u8]) {
let n = px.len();
debug_assert!(n <= 8, "apply_transfer_in_place: ncomps={n} > 8");
let mut tmp = [0u8; 8];
tmp[..n].copy_from_slice(px);
apply_transfer_pixel(pipe, &tmp[..n], px);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::make_pipe;
use crate::types::BlendMode;
use color::Rgb8;
#[test]
fn no_transparency_opaque_normal() {
let pipe = make_pipe(255, BlendMode::Normal);
assert!(pipe.no_transparency(false));
assert!(!pipe.no_transparency(true)); }
#[test]
fn no_transparency_alpha_less_than_255() {
let pipe = make_pipe(200, BlendMode::Normal);
assert!(!pipe.no_transparency(false));
}
#[test]
fn use_aa_path_requires_no_soft_mask() {
let pipe = make_pipe(200, BlendMode::Normal);
assert!(pipe.use_aa_path());
}
#[test]
fn use_aa_path_false_for_blend_mode() {
let pipe = make_pipe(200, BlendMode::Multiply);
assert!(!pipe.use_aa_path());
}
#[test]
fn render_span_simple_solid_opaque() {
let pipe = make_pipe(255, BlendMode::Normal);
let src_color = [200u8, 100, 50];
let src = PipeSrc::Solid(&src_color);
let mut dst = vec![0u8; 3 * 4]; let mut alpha = vec![0u8; 4];
render_span::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 3, 0);
for i in 0..4 {
assert_eq!(dst[i * 3], 200, "pixel {i} R");
assert_eq!(dst[i * 3 + 1], 100, "pixel {i} G");
assert_eq!(dst[i * 3 + 2], 50, "pixel {i} B");
assert_eq!(alpha[i], 255, "pixel {i} alpha");
}
}
#[test]
fn render_span_aa_blends_with_dst() {
let pipe = make_pipe(255, BlendMode::Normal);
let src_color = [255u8, 255, 255];
let src = PipeSrc::Solid(&src_color);
let shape = vec![128u8; 4];
let mut dst = vec![0u8; 3 * 4];
let mut alpha = vec![255u8; 4]; render_span::<Rgb8>(
&pipe,
&src,
&mut dst,
Some(&mut alpha),
Some(&shape),
0,
3,
0,
);
for i in 0..4 {
let v = dst[i * 3];
assert!(v > 100 && v < 160, "pixel {i} R={v} expected ~128");
}
}
#[test]
fn no_transparency_false_with_overprint() {
let mut pipe = make_pipe(255, BlendMode::Normal);
pipe.overprint_mask = 0x0000_0001; assert!(
!pipe.no_transparency(false),
"overprint must route to general pipe"
);
}
}