1use std::cell::RefCell;
19
20use crate::pipe::{self, PipeSrc, PipeState, blend};
21use crate::types::BlendMode;
22use color::Pixel;
23use color::convert::div255;
24
25const MAX_COMPS: usize = 8; thread_local! {
29 static PAT_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
30}
31
32#[expect(
44 clippy::too_many_arguments,
45 reason = "mirrors C++ SplashPipe API; all parameters are necessary"
46)]
47pub(crate) fn render_span_general<P: Pixel>(
48 pipe: &PipeState<'_>,
49 src: &PipeSrc<'_>,
50 dst_pixels: &mut [u8],
51 dst_alpha: Option<&mut [u8]>,
52 shape: Option<&[u8]>,
53 x0: i32,
54 x1: i32,
55 y: i32,
56) {
57 debug_assert!(x1 >= x0, "render_span_general: x1={x1} < x0={x0}");
58 #[expect(
59 clippy::cast_sign_loss,
60 reason = "x1 >= x0 is asserted above, so x1 - x0 + 1 >= 1 > 0"
61 )]
62 let count = (x1 - x0 + 1) as usize;
63 let ncomps = P::BYTES;
64
65 debug_assert_eq!(dst_pixels.len(), count * ncomps);
66 if let Some(sh) = shape {
67 debug_assert_eq!(sh.len(), count);
68 }
69 if let Some(sm) = pipe.soft_mask {
70 debug_assert_eq!(sm.len(), count, "soft_mask length must equal span count");
71 }
72
73 let a_input = u32::from(pipe.a_input);
74 let is_nonseparable = matches!(
75 pipe.blend_mode,
76 BlendMode::Hue | BlendMode::Saturation | BlendMode::Color | BlendMode::Luminosity
77 );
78 let is_cmyk_like = ncomps == 4 || ncomps == 8;
79
80 let shape_at = |i: usize| shape.map_or(0xFFu8, |s| s[i]);
82 let soft_mask_at = |i: usize| pipe.soft_mask.map_or(0xFFu8, |s| s[i]);
83 let alpha0_at = |i: usize| pipe.alpha0.map(|a| a[i]);
84
85 match src {
86 PipeSrc::Solid(color) => {
87 debug_assert_eq!(color.len(), ncomps);
88 render_span_general_inner(
89 pipe,
90 |_i| color,
91 dst_pixels,
92 dst_alpha,
93 shape,
94 count,
95 ncomps,
96 a_input,
97 is_nonseparable,
98 is_cmyk_like,
99 &shape_at,
100 &soft_mask_at,
101 &alpha0_at,
102 );
103 }
104 PipeSrc::Pattern(pat) => {
105 PAT_BUF.with(|cell| {
106 let mut buf = cell.borrow_mut();
107 buf.resize(count * ncomps, 0);
108 pat.fill_span(y, x0, x1, &mut buf[..count * ncomps]);
109 render_span_general_inner(
110 pipe,
111 |i| &buf[i * ncomps..(i + 1) * ncomps],
112 dst_pixels,
113 dst_alpha,
114 shape,
115 count,
116 ncomps,
117 a_input,
118 is_nonseparable,
119 is_cmyk_like,
120 &shape_at,
121 &soft_mask_at,
122 &alpha0_at,
123 );
124 });
125 }
126 }
127}
128
129#[expect(
130 clippy::too_many_arguments,
131 reason = "all params necessary; closure eliminates solid/pattern duplication"
132)]
133#[expect(
134 clippy::too_many_lines,
135 reason = "compositing formula has many branches that cannot be meaningfully split"
136)]
137#[expect(
138 clippy::single_match_else,
139 reason = "both Some and None arms have substantial independent logic; if-let would be less clear"
140)]
141fn render_span_general_inner<'src>(
142 pipe: &PipeState<'_>,
143 src_px_at: impl Fn(usize) -> &'src [u8],
144 dst_pixels: &mut [u8],
145 dst_alpha: Option<&mut [u8]>,
146 shape: Option<&[u8]>,
147 count: usize,
148 ncomps: usize,
149 a_input: u32,
150 is_nonseparable: bool,
151 is_cmyk_like: bool,
152 shape_at: &dyn Fn(usize) -> u8,
153 soft_mask_at: &dyn Fn(usize) -> u8,
154 alpha0_at: &dyn Fn(usize) -> Option<u8>,
155) {
156 let has_soft_mask = pipe.soft_mask.is_some();
157 let has_shape = shape.is_some();
158
159 match dst_alpha {
160 Some(dst_alpha) => {
161 debug_assert_eq!(dst_alpha.len(), count);
162 for i in 0..count {
163 let src_px = src_px_at(i);
164 let dst_px = &mut dst_pixels[i * ncomps..(i + 1) * ncomps];
165 let a_dst = u32::from(dst_alpha[i]);
166 let shape_v = u32::from(shape_at(i));
167 let soft_v = u32::from(soft_mask_at(i));
168
169 let a_src = compute_a_src(a_input, soft_v, shape_v, has_soft_mask, has_shape);
171
172 if pipe.knockout && shape_v >= u32::from(pipe.knockout_opacity) {
176 dst_alpha[i] = 0;
177 }
178
179 let mut c_src_corr: [u8; MAX_COMPS] = [0; MAX_COMPS];
184 let c_src: &[u8] = if pipe.non_isolated_group && shape_v != 0 {
185 let t = (a_dst * 255) / shape_v - a_dst;
186 let t_i = t.cast_signed(); for j in 0..ncomps {
188 let v = i32::from(src_px[j])
189 + (i32::from(src_px[j]) - i32::from(dst_px[j])) * t_i / 255;
190 #[expect(
191 clippy::cast_sign_loss,
192 reason = "value is clamped to [0, 255] above"
193 )]
194 {
195 c_src_corr[j] = v.clamp(0, 255) as u8;
196 }
197 }
198 &c_src_corr[..ncomps]
199 } else {
200 src_px
201 };
202
203 let mut c_blend: [u8; MAX_COMPS] = [0; MAX_COMPS];
205 if pipe.blend_mode != BlendMode::Normal {
206 apply_blend_fn(
207 pipe.blend_mode,
208 c_src,
209 dst_px,
210 &mut c_blend[..ncomps],
211 is_cmyk_like,
212 is_nonseparable,
213 );
214 }
215
216 let a_dst_eff = u32::from(dst_alpha[i]); let (a_result, alpha_i, alpha_im1) =
219 compute_alphas(a_src, a_dst_eff, shape_v, alpha0_at(i), pipe.knockout);
220
221 if a_result == 0 {
223 dst_px.fill(0);
224 } else {
225 debug_assert!(alpha_i > 0, "alpha_i must be > 0 when a_result > 0");
227 for j in 0..ncomps {
228 let c_src_j = u32::from(c_src[j]);
229 let c_dst_j = u32::from(dst_px[j]);
230 let c_b_j = u32::from(c_blend[j]);
231
232 let c = if pipe.blend_mode == BlendMode::Normal {
233 ((alpha_i - a_src) * c_dst_j + a_src * c_src_j) / alpha_i
235 } else {
236 ((alpha_i - a_src) * c_dst_j
238 + a_src * ((255 - alpha_im1) * c_src_j + alpha_im1 * c_b_j) / 255)
239 / alpha_i
240 };
241 #[expect(
242 clippy::cast_possible_truncation,
243 reason = "c is a weighted average of values ≤ 255, so c ≤ 255"
244 )]
245 {
246 dst_px[j] = c as u8;
247 }
248 }
249 finish_pixel(pipe, dst_px, src_px, ncomps);
250 }
251
252 #[expect(
253 clippy::cast_possible_truncation,
254 reason = "a_result is clamped to ≤ 255 in compute_alphas"
255 )]
256 {
257 dst_alpha[i] = a_result as u8;
258 }
259 }
260 }
261 None => {
262 debug_assert!(
267 !pipe.non_isolated_group && !pipe.knockout,
268 "non_isolated_group/knockout require dst_alpha; None arm uses implicit a_dst=255"
269 );
270 for i in 0..count {
271 let src_px = src_px_at(i);
272 let dst_px = &mut dst_pixels[i * ncomps..(i + 1) * ncomps];
273 let shape_v = u32::from(shape_at(i));
274 let soft_v = u32::from(soft_mask_at(i));
275
276 let a_src = compute_a_src(a_input, soft_v, shape_v, has_soft_mask, has_shape);
277 debug_assert!(a_src <= 255, "a_src={a_src} out of [0, 255]");
280
281 let mut c_blend: [u8; MAX_COMPS] = [0; MAX_COMPS];
282 if pipe.blend_mode != BlendMode::Normal {
283 apply_blend_fn(
284 pipe.blend_mode,
285 src_px,
286 dst_px,
287 &mut c_blend[..ncomps],
288 is_cmyk_like,
289 is_nonseparable,
290 );
291 }
292
293 for j in 0..ncomps {
294 let c_src_j = u32::from(src_px[j]);
295 let c_dst_j = u32::from(dst_px[j]);
296 let c_b_j = u32::from(c_blend[j]);
297
298 let c = if pipe.blend_mode == BlendMode::Normal {
302 u32::from(div255((255 - a_src) * c_dst_j + a_src * c_src_j))
303 } else {
304 u32::from(div255((255 - a_src) * c_dst_j + a_src * c_b_j))
305 };
306 #[expect(
307 clippy::cast_possible_truncation,
308 reason = "c is a weighted average of values ≤ 255, so c ≤ 255"
309 )]
310 {
311 dst_px[j] = c as u8;
312 }
313 }
314 finish_pixel(pipe, dst_px, src_px, ncomps);
315 }
316 }
317 }
318}
319
320#[inline]
326fn compute_a_src(
327 a_input: u32,
328 soft_v: u32,
329 shape_v: u32,
330 has_soft_mask: bool,
331 has_shape: bool,
332) -> u32 {
333 if has_soft_mask {
334 if has_shape {
335 u32::from(div255(u32::from(div255(a_input * soft_v)) * shape_v))
336 } else {
337 u32::from(div255(a_input * soft_v))
338 }
339 } else if has_shape {
340 u32::from(div255(a_input * shape_v))
341 } else {
342 a_input
343 }
344}
345
346#[expect(
352 clippy::option_if_let_else,
353 reason = "if-let form is clearer than map_or_else for this multi-branch alpha computation"
354)]
355fn compute_alphas(
356 a_src: u32,
357 a_dst: u32,
358 shape: u32,
359 alpha0: Option<u8>,
360 knockout: bool,
361) -> (u32, u32, u32) {
362 if let Some(a0) = alpha0 {
363 let a0 = u32::from(a0);
364 if knockout {
365 let a_result = a_src + u32::from(div255(a_dst * (255 - shape)));
367 let alpha_i = a_result + a0 - u32::from(div255(a_result * a0));
368 (a_result.min(255), alpha_i.min(255), a0)
369 } else {
370 let a_result = a_src + a_dst - u32::from(div255(a_src * a_dst));
372 let alpha_i = a_result + a0 - u32::from(div255(a_result * a0));
373 let alpha_im1 = a0 + a_dst - u32::from(div255(a0 * a_dst));
374 (a_result.min(255), alpha_i.min(255), alpha_im1.min(255))
375 }
376 } else if knockout {
377 let a_result = a_src + u32::from(div255(a_dst * (255 - shape)));
379 (a_result.min(255), a_result.min(255), 0)
380 } else {
381 let a_result = a_src + a_dst - u32::from(div255(a_src * a_dst));
383 (a_result.min(255), a_result.min(255), a_dst)
384 }
385}
386
387fn apply_blend_fn(
389 mode: BlendMode,
390 src: &[u8],
391 dst: &[u8],
392 c_blend: &mut [u8],
393 is_cmyk_like: bool,
394 is_nonseparable: bool,
395) {
396 debug_assert_eq!(src.len(), dst.len());
397 debug_assert_eq!(src.len(), c_blend.len());
398 let ncomps = src.len();
399
400 if is_cmyk_like {
401 let mut src2 = [0u8; MAX_COMPS];
404 let mut dst2 = [0u8; MAX_COMPS];
405 for j in 0..ncomps {
406 src2[j] = 255 - src[j];
407 dst2[j] = 255 - dst[j];
408 }
409
410 if is_nonseparable {
411 let s3 = [src2[0], src2[1], src2[2]];
412 let d3 = [dst2[0], dst2[1], dst2[2]];
413 let r3 = blend::apply_nonseparable_rgb(mode, s3, d3);
414 c_blend[0] = 255 - r3[0];
415 c_blend[1] = 255 - r3[1];
416 c_blend[2] = 255 - r3[2];
417 if ncomps >= 4 {
419 c_blend[3] = 255
420 - (if mode == BlendMode::Luminosity {
421 src2[3]
422 } else {
423 dst2[3]
424 });
425 }
426 for j in 4..ncomps {
427 c_blend[j] = 255 - dst2[j];
429 }
430 } else {
431 blend::apply_separable(
432 mode,
433 &src2[..ncomps.min(4)],
434 &dst2[..ncomps.min(4)],
435 &mut c_blend[..ncomps.min(4)],
436 );
437 for v in &mut c_blend[..ncomps.min(4)] {
438 *v = 255 - *v;
439 }
440 c_blend[4..ncomps].copy_from_slice(&dst[4..ncomps]);
442 }
443 } else if is_nonseparable {
444 let n = ncomps.min(3);
446 let mut s3 = [0u8; 3];
447 let mut d3 = [0u8; 3];
448 s3[..n].copy_from_slice(&src[..n]);
449 d3[..n].copy_from_slice(&dst[..n]);
450 if ncomps == 1 {
452 s3[1] = s3[0];
453 s3[2] = s3[0];
454 d3[1] = d3[0];
455 d3[2] = d3[0];
456 }
457 let r3 = blend::apply_nonseparable_rgb(mode, s3, d3);
458 c_blend[..n].copy_from_slice(&r3[..n]);
459 } else {
460 blend::apply_separable(mode, src, dst, c_blend);
461 }
462}
463
464#[inline]
470fn finish_pixel(pipe: &PipeState<'_>, dst_px: &mut [u8], src_px: &[u8], ncomps: usize) {
471 pipe::apply_transfer_in_place(pipe, dst_px);
472 if pipe.overprint_mask != 0xFFFF_FFFF {
473 apply_overprint(pipe, dst_px, src_px, ncomps);
474 }
475}
476
477fn apply_overprint(pipe: &PipeState<'_>, dst_px: &mut [u8], src_px: &[u8], ncomps: usize) {
489 if pipe.overprint_additive {
490 for j in 0..ncomps {
491 if pipe.overprint_mask & (1 << j) == 0 {
494 continue;
495 }
496 dst_px[j] = (u16::from(dst_px[j]) + u16::from(src_px[j])).min(255) as u8;
498 }
499 } else {
500 panic!(
507 "general pipe: replace overprint (mask={:#010x}) is not yet implemented; \
508 use overprint_additive=true or preserve pre-blend dst in the caller",
509 pipe.overprint_mask,
510 );
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn compute_a_src_no_mask_no_shape_returns_a_input() {
520 assert_eq!(compute_a_src(200, 0xFF, 0xFF, false, false), 200);
521 }
522
523 #[test]
524 fn compute_a_src_shape_zero_gives_zero() {
525 assert_eq!(compute_a_src(255, 0xFF, 0, false, true), 0);
526 }
527
528 #[test]
529 fn compute_a_src_soft_mask_scales_alpha() {
530 let result = compute_a_src(255, 128, 0xFF, true, false);
531 let expected = u32::from(div255(255 * 128));
532 assert_eq!(result, expected);
533 }
534
535 #[test]
536 fn compute_a_src_soft_and_shape_combines_both() {
537 let result = compute_a_src(255, 128, 128, true, true);
538 let expected = u32::from(div255(u32::from(div255(255 * 128)) * 128));
539 assert_eq!(result, expected);
540 }
541 use crate::pipe::{PipeSrc, PipeState};
542 use crate::state::TransferSet;
543 use color::{Gray8, Rgb8, TransferLut};
544
545 fn normal_pipe(a: u8) -> PipeState<'static> {
546 PipeState {
547 blend_mode: BlendMode::Normal,
548 a_input: a,
549 overprint_mask: 0xFFFF_FFFF,
550 overprint_additive: false,
551 transfer: TransferSet::identity_rgb(),
552 soft_mask: None,
553 alpha0: None,
554 knockout: false,
555 knockout_opacity: 255,
556 non_isolated_group: false,
557 }
558 }
559
560 #[test]
561 fn opaque_src_over_any_dst_gives_src() {
562 let pipe = normal_pipe(255);
563 let src_color = [200u8, 100, 50];
564 let src = PipeSrc::Solid(&src_color);
565
566 let mut dst = vec![10u8, 20, 30];
567 let mut alpha = vec![128u8];
568
569 render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
570
571 assert_eq!(&dst, &[200, 100, 50]);
573 assert_eq!(alpha[0], 255);
574 }
575
576 #[test]
577 fn transparent_src_leaves_dst_unchanged() {
578 let pipe = normal_pipe(0);
579 let src = PipeSrc::Solid(&[255u8, 255, 255]);
580
581 let mut dst = vec![10u8, 20, 30];
582 let mut alpha = vec![200u8];
583
584 render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
585
586 assert_eq!(&dst, &[10, 20, 30]);
589 assert_eq!(alpha[0], 200);
590 }
591
592 #[test]
593 fn blend_multiply_with_dst() {
594 let mut pipe = normal_pipe(255);
595 pipe.blend_mode = BlendMode::Multiply;
596
597 let src = PipeSrc::Solid(&[128u8, 128, 128]);
599 let mut dst = vec![200u8, 200, 200];
600 let mut alpha = vec![255u8];
601
602 render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
603
604 let v = dst[0];
609 assert!((i32::from(v) - 100).abs() <= 1, "expected ~100, got {v}");
610 }
611
612 #[test]
613 fn compute_alphas_isolated_non_knockout() {
614 let (ar, ai, aim1) = compute_alphas(128, 200, 255, None, false);
616 assert!((226..=230).contains(&ar), "a_result={ar}");
618 assert_eq!(ai, ar, "isolated: alpha_i == a_result");
619 assert_eq!(aim1, 200, "isolated non-knockout: alpha_im1 == a_dst");
620 }
621
622 #[test]
623 fn soft_mask_modulates_alpha() {
624 let soft = vec![128u8];
626 let mut dst = vec![0u8; 3];
627 let mut alpha = vec![0u8];
628
629 let pipe = PipeState {
632 blend_mode: BlendMode::Normal,
633 a_input: 255,
634 overprint_mask: 0xFFFF_FFFF,
635 overprint_additive: false,
636 transfer: TransferSet::identity_rgb(),
637 soft_mask: Some(soft.as_slice()),
638 alpha0: None,
639 knockout: false,
640 knockout_opacity: 255,
641 non_isolated_group: false,
642 };
643
644 let src = PipeSrc::Solid(&[255u8, 255, 255]);
645 render_span_general::<Rgb8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
646
647 assert_eq!(dst[0], 255);
650 assert!((i32::from(alpha[0]) - 128).abs() <= 2, "alpha={}", alpha[0]);
651 }
652
653 #[test]
654 fn gray_transfer_lut_applied_correctly() {
655 static DN: [[u8; 256]; 8] = [TransferLut::IDENTITY.0; 8];
660 let id = TransferLut::IDENTITY.as_array();
661 let inv = TransferLut::INVERTED.as_array();
662 let transfer = TransferSet {
663 gray: inv, rgb: [id; 3],
665 cmyk: [id; 4],
666 device_n: &DN,
667 };
668 let pipe = PipeState {
669 blend_mode: BlendMode::Normal,
670 a_input: 255,
671 overprint_mask: 0xFFFF_FFFF,
672 overprint_additive: false,
673 transfer,
674 soft_mask: None,
675 alpha0: None,
676 knockout: false,
677 knockout_opacity: 255,
678 non_isolated_group: false,
679 };
680
681 let src_color = [100u8];
683 let src = PipeSrc::Solid(&src_color);
684 let mut dst = vec![0u8; 1];
685 let mut alpha = vec![0u8; 1];
686 render_span_general::<Gray8>(&pipe, &src, &mut dst, Some(&mut alpha), None, 0, 0, 0);
687 assert_eq!(dst[0], 155, "gray transfer must use gray LUT, not rgb[0]");
688 }
689}