1use rayon::prelude::*;
31use thiserror::Error;
32
33#[derive(Debug, Clone, PartialEq, Error)]
37pub enum BlendError {
38 #[error("Buffer size mismatch: expected {expected}, got {actual}")]
40 BufferSizeMismatch { expected: usize, actual: usize },
41 #[error("Invalid dimensions: {width}x{height}")]
43 InvalidDimensions { width: u32, height: u32 },
44 #[error("Pixel count overflow for {width}x{height}")]
46 PixelCountOverflow { width: u32, height: u32 },
47 #[error("Mask length mismatch: expected {expected}, got {actual}")]
49 MaskLengthMismatch { expected: usize, actual: usize },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum BlendMode {
57 AlphaComposite,
59 Additive,
61 Multiply,
63 Screen,
65 Overlay,
67 SoftLight,
69 Difference,
71 Dissolve,
73}
74
75impl BlendMode {
76 #[must_use]
78 pub fn label(self) -> &'static str {
79 match self {
80 Self::AlphaComposite => "alpha_composite",
81 Self::Additive => "additive",
82 Self::Multiply => "multiply",
83 Self::Screen => "screen",
84 Self::Overlay => "overlay",
85 Self::SoftLight => "soft_light",
86 Self::Difference => "difference",
87 Self::Dissolve => "dissolve",
88 }
89 }
90}
91
92#[derive(Debug, Clone, Default)]
96pub struct BlendStats {
97 pub pixels_blended: u64,
99 pub mode: Option<BlendMode>,
101 pub opacity: u8,
103}
104
105#[derive(Debug, Clone, Default)]
112pub struct BlendKernel;
113
114impl BlendKernel {
115 fn validate_rgba(buf: &[u8], width: u32, height: u32) -> Result<usize, BlendError> {
118 if width == 0 || height == 0 {
119 return Err(BlendError::InvalidDimensions { width, height });
120 }
121 let pixels = (width as usize)
122 .checked_mul(height as usize)
123 .ok_or(BlendError::PixelCountOverflow { width, height })?;
124 let expected = pixels * 4;
125 if buf.len() != expected {
126 return Err(BlendError::BufferSizeMismatch {
127 expected,
128 actual: buf.len(),
129 });
130 }
131 Ok(pixels)
132 }
133
134 pub fn blend(
145 src: &[u8],
146 dst: &mut [u8],
147 width: u32,
148 height: u32,
149 mode: BlendMode,
150 opacity: u8,
151 ) -> Result<BlendStats, BlendError> {
152 Self::validate_rgba(src, width, height)?;
153 let pixels = Self::validate_rgba(dst, width, height)?;
154 let op = opacity as f32 / 255.0;
155
156 src.par_chunks(4)
157 .zip(dst.par_chunks_mut(4))
158 .for_each(|(s, d)| {
159 blend_pixel(s, d, mode, op);
160 });
161
162 Ok(BlendStats {
163 pixels_blended: pixels as u64,
164 mode: Some(mode),
165 opacity,
166 })
167 }
168
169 pub fn blend_masked(
177 src: &[u8],
178 dst: &mut [u8],
179 mask: &[u8],
180 width: u32,
181 height: u32,
182 mode: BlendMode,
183 global_opacity: u8,
184 ) -> Result<BlendStats, BlendError> {
185 Self::validate_rgba(src, width, height)?;
186 let pixels = Self::validate_rgba(dst, width, height)?;
187 if mask.len() != pixels {
188 return Err(BlendError::MaskLengthMismatch {
189 expected: pixels,
190 actual: mask.len(),
191 });
192 }
193
194 let go = global_opacity as f32 / 255.0;
195
196 src.par_chunks(4)
197 .zip(dst.par_chunks_mut(4))
198 .zip(mask.par_iter())
199 .for_each(|((s, d), &m)| {
200 let op = go * (m as f32 / 255.0);
201 blend_pixel(s, d, mode, op);
202 });
203
204 Ok(BlendStats {
205 pixels_blended: pixels as u64,
206 mode: Some(mode),
207 opacity: global_opacity,
208 })
209 }
210
211 pub fn composite_layers(
220 layers: &[(&[u8], u8)],
221 width: u32,
222 height: u32,
223 ) -> Result<Vec<u8>, BlendError> {
224 if width == 0 || height == 0 {
225 return Err(BlendError::InvalidDimensions { width, height });
226 }
227 let pixels = (width as usize)
228 .checked_mul(height as usize)
229 .ok_or(BlendError::PixelCountOverflow { width, height })?;
230 let buf_size = pixels * 4;
231
232 for (i, (layer, _)) in layers.iter().enumerate() {
233 if layer.len() != buf_size {
234 return Err(BlendError::BufferSizeMismatch {
235 expected: buf_size,
236 actual: layer.len(),
237 });
238 }
239 let _ = i;
240 }
241
242 if layers.is_empty() {
243 return Ok(vec![0u8; buf_size]);
244 }
245
246 let mut acc = vec![0u8; buf_size];
248 for (layer, opacity) in layers {
249 let op = *opacity as f32 / 255.0;
250 layer
251 .par_chunks(4)
252 .zip(acc.par_chunks_mut(4))
253 .for_each(|(s, d)| {
254 blend_pixel(s, d, BlendMode::AlphaComposite, op);
255 });
256 }
257 Ok(acc)
258 }
259
260 pub fn apply_tint(
268 src: &[u8],
269 dst: &mut [u8],
270 width: u32,
271 height: u32,
272 tint: [u8; 4],
273 ) -> Result<(), BlendError> {
274 Self::validate_rgba(src, width, height)?;
275 Self::validate_rgba(dst, width, height)?;
276
277 src.par_chunks(4)
278 .zip(dst.par_chunks_mut(4))
279 .for_each(|(s, d)| {
280 for c in 0..4 {
281 let v = (s[c] as u32 * tint[c] as u32 + 127) / 255;
282 d[c] = v.min(255) as u8;
283 }
284 });
285 Ok(())
286 }
287
288 pub fn premultiply_alpha(buf: &mut [u8], width: u32, height: u32) -> Result<(), BlendError> {
297 Self::validate_rgba(buf, width, height)?;
298 buf.par_chunks_mut(4).for_each(|px| {
299 let a = px[3] as u32;
300 for c in 0..3 {
301 px[c] = ((px[c] as u32 * a + 127) / 255) as u8;
302 }
303 });
304 Ok(())
305 }
306
307 pub fn unpremultiply_alpha(buf: &mut [u8], width: u32, height: u32) -> Result<(), BlendError> {
315 Self::validate_rgba(buf, width, height)?;
316 buf.par_chunks_mut(4).for_each(|px| {
317 let a = px[3] as f32;
318 if a > 0.0 {
319 for c in 0..3 {
320 px[c] = (px[c] as f32 / a * 255.0).round().clamp(0.0, 255.0) as u8;
321 }
322 }
323 });
324 Ok(())
325 }
326}
327
328fn blend_pixel(s: &[u8], d: &mut [u8], mode: BlendMode, opacity: f32) {
335 let sa = (s[3] as f32 / 255.0) * opacity;
336 match mode {
337 BlendMode::AlphaComposite => alpha_composite(s, d, sa),
338 BlendMode::Additive => additive(s, d, sa),
339 BlendMode::Multiply => multiply(s, d, sa),
340 BlendMode::Screen => screen(s, d, sa),
341 BlendMode::Overlay => overlay(s, d, sa),
342 BlendMode::SoftLight => soft_light(s, d, sa),
343 BlendMode::Difference => difference(s, d, sa),
344 BlendMode::Dissolve => dissolve(s, d, sa),
345 }
346}
347
348fn alpha_composite(s: &[u8], d: &mut [u8], sa: f32) {
350 let da = d[3] as f32 / 255.0;
351 let out_a = sa + da * (1.0 - sa);
352 if out_a < 1e-9 {
353 d[0] = 0;
354 d[1] = 0;
355 d[2] = 0;
356 d[3] = 0;
357 return;
358 }
359 for c in 0..3 {
360 let sc = s[c] as f32 / 255.0;
361 let dc = d[c] as f32 / 255.0;
362 let out_c = (sc * sa + dc * da * (1.0 - sa)) / out_a;
363 d[c] = (out_c * 255.0).round().clamp(0.0, 255.0) as u8;
364 }
365 d[3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
366}
367
368fn additive(s: &[u8], d: &mut [u8], sa: f32) {
370 for c in 0..3 {
371 let v = d[c] as f32 + s[c] as f32 * sa;
372 d[c] = v.round().clamp(0.0, 255.0) as u8;
373 }
374 }
376
377fn multiply(s: &[u8], d: &mut [u8], sa: f32) {
379 for c in 0..3 {
380 let dc = d[c] as f32;
381 let sc = s[c] as f32;
382 let blended = dc * sc / 255.0;
383 d[c] = lerp_channel(dc, blended, sa);
384 }
385}
386
387fn screen(s: &[u8], d: &mut [u8], sa: f32) {
389 for c in 0..3 {
390 let dc = d[c] as f32;
391 let sc = s[c] as f32;
392 let blended = 255.0 - (255.0 - dc) * (255.0 - sc) / 255.0;
393 d[c] = lerp_channel(dc, blended, sa);
394 }
395}
396
397fn overlay(s: &[u8], d: &mut [u8], sa: f32) {
399 for c in 0..3 {
400 let dc = d[c] as f32 / 255.0;
401 let sc = s[c] as f32 / 255.0;
402 let blended = if dc < 0.5 {
403 2.0 * dc * sc
404 } else {
405 1.0 - 2.0 * (1.0 - dc) * (1.0 - sc)
406 };
407 d[c] = lerp_channel(d[c] as f32, blended * 255.0, sa);
408 }
409}
410
411fn soft_light(s: &[u8], d: &mut [u8], sa: f32) {
413 for c in 0..3 {
414 let dc = d[c] as f32 / 255.0;
415 let sc = s[c] as f32 / 255.0;
416 let blended = 2.0 * dc * sc + dc * dc * (1.0 - 2.0 * sc);
417 d[c] = lerp_channel(d[c] as f32, blended * 255.0, sa);
418 }
419}
420
421fn difference(s: &[u8], d: &mut [u8], sa: f32) {
423 for c in 0..3 {
424 let dc = d[c] as f32;
425 let sc = s[c] as f32;
426 let blended = (dc - sc).abs();
427 d[c] = lerp_channel(dc, blended, sa);
428 }
429}
430
431fn dissolve(s: &[u8], d: &mut [u8], sa: f32) {
436 let hash =
440 xorshift32(s[0] as u32 ^ (s[1] as u32 * 17) ^ (d[0] as u32 * 31) ^ (d[1] as u32 * 7));
441 let threshold = (hash & 0xFF) as f32 / 255.0;
442 if sa > threshold {
443 for c in 0..3 {
445 d[c] = s[c];
446 }
447 d[3] = s[3];
448 }
449 }
451
452#[inline]
456fn lerp_channel(a: f32, b: f32, t: f32) -> u8 {
457 (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
458}
459
460#[inline]
462fn xorshift32(mut x: u32) -> u32 {
463 x ^= x << 13;
464 x ^= x >> 17;
465 x ^= x << 5;
466 x
467}
468
469#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
478 fn test_blend_mode_labels() {
479 assert_eq!(BlendMode::AlphaComposite.label(), "alpha_composite");
480 assert_eq!(BlendMode::Additive.label(), "additive");
481 assert_eq!(BlendMode::Multiply.label(), "multiply");
482 assert_eq!(BlendMode::Screen.label(), "screen");
483 assert_eq!(BlendMode::Overlay.label(), "overlay");
484 assert_eq!(BlendMode::SoftLight.label(), "soft_light");
485 assert_eq!(BlendMode::Difference.label(), "difference");
486 assert_eq!(BlendMode::Dissolve.label(), "dissolve");
487 }
488
489 #[test]
492 fn test_blend_invalid_dims() {
493 let src = vec![0u8; 4];
494 let mut dst = vec![0u8; 4];
495 let err = BlendKernel::blend(&src, &mut dst, 0, 1, BlendMode::Additive, 255);
496 assert!(matches!(err, Err(BlendError::InvalidDimensions { .. })));
497 }
498
499 #[test]
500 fn test_blend_buffer_mismatch() {
501 let src = vec![0u8; 8]; let mut dst = vec![0u8; 4];
503 let err = BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255);
504 assert!(matches!(err, Err(BlendError::BufferSizeMismatch { .. })));
505 }
506
507 #[test]
508 fn test_blend_masked_mask_mismatch() {
509 let src = vec![255u8; 4 * 4 * 4];
510 let mut dst = vec![0u8; 4 * 4 * 4];
511 let mask = vec![255u8; 10]; let err = BlendKernel::blend_masked(&src, &mut dst, &mask, 4, 4, BlendMode::Multiply, 255);
513 assert!(matches!(err, Err(BlendError::MaskLengthMismatch { .. })));
514 }
515
516 #[test]
519 fn test_opacity_zero_preserves_dst() -> Result<(), BlendError> {
520 let src: Vec<u8> = vec![255, 0, 0, 255]; let original_dst = vec![0u8, 128, 255, 255]; let mut dst = original_dst.clone();
523 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::AlphaComposite, 0)?;
524 for (orig, &out) in original_dst.iter().zip(dst.iter()) {
526 let diff = (*orig as i16 - out as i16).abs();
527 assert!(diff <= 1, "channel diff={diff}");
528 }
529 Ok(())
530 }
531
532 #[test]
535 fn test_alpha_composite_fully_opaque_src() -> Result<(), BlendError> {
536 let src = vec![200u8, 100, 50, 255]; let mut dst = vec![0u8, 0, 0, 255];
538 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::AlphaComposite, 255)?;
539 assert_eq!(dst[0], 200);
540 assert_eq!(dst[1], 100);
541 assert_eq!(dst[2], 50);
542 assert_eq!(dst[3], 255);
543 Ok(())
544 }
545
546 #[test]
549 fn test_additive_blend_clamps_to_255() -> Result<(), BlendError> {
550 let src = vec![200u8, 200, 200, 255];
551 let mut dst = vec![100u8, 100, 100, 255];
552 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255)?;
553 assert_eq!(dst[0], 255, "200+100=300 → clamp to 255");
554 Ok(())
555 }
556
557 #[test]
558 fn test_additive_blend_zero_src() -> Result<(), BlendError> {
559 let src = vec![0u8, 0, 0, 255];
560 let original = vec![100u8, 150, 200, 255];
561 let mut dst = original.clone();
562 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255)?;
563 assert_eq!(dst[..3], original[..3]);
564 Ok(())
565 }
566
567 #[test]
570 fn test_multiply_with_white_src_unchanged() -> Result<(), BlendError> {
571 let src = vec![255u8, 255, 255, 255]; let original = vec![100u8, 150, 200, 255];
573 let mut dst = original.clone();
574 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Multiply, 255)?;
575 for c in 0..3 {
577 let diff = (original[c] as i16 - dst[c] as i16).abs();
578 assert!(diff <= 1, "channel {c}: diff={diff}");
579 }
580 Ok(())
581 }
582
583 #[test]
584 fn test_multiply_with_black_src_yields_zero() -> Result<(), BlendError> {
585 let src = vec![0u8, 0, 0, 255]; let mut dst = vec![200u8, 150, 100, 255];
587 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Multiply, 255)?;
588 for c in 0..3 {
590 assert_eq!(dst[c], 0, "channel {c} should be 0");
591 }
592 Ok(())
593 }
594
595 #[test]
598 fn test_screen_with_black_src_unchanged() -> Result<(), BlendError> {
599 let src = vec![0u8, 0, 0, 255]; let original = vec![100u8, 150, 200, 255];
601 let mut dst = original.clone();
602 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Screen, 255)?;
603 for c in 0..3 {
604 let diff = (original[c] as i16 - dst[c] as i16).abs();
605 assert!(diff <= 1, "channel {c}: diff={diff}");
606 }
607 Ok(())
608 }
609
610 #[test]
611 fn test_screen_with_white_src_yields_white() -> Result<(), BlendError> {
612 let src = vec![255u8, 255, 255, 255]; let mut dst = vec![100u8, 150, 200, 255];
614 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Screen, 255)?;
615 for c in 0..3 {
616 assert_eq!(
617 dst[c], 255,
618 "channel {c} should be 255 after screen with white"
619 );
620 }
621 Ok(())
622 }
623
624 #[test]
627 fn test_difference_with_same_src_dst_yields_black() -> Result<(), BlendError> {
628 let src = vec![100u8, 150, 200, 255];
629 let mut dst = vec![100u8, 150, 200, 255];
630 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Difference, 255)?;
631 for c in 0..3 {
632 assert_eq!(dst[c], 0, "difference of equal values should be 0");
633 }
634 Ok(())
635 }
636
637 #[test]
640 fn test_masked_blend_all_opaque() -> Result<(), BlendError> {
641 let w = 2u32;
642 let h = 2u32;
643 let src = vec![255u8; (w * h * 4) as usize];
644 let mut dst = vec![0u8; (w * h * 4) as usize];
645 let mask = vec![255u8; (w * h) as usize]; BlendKernel::blend_masked(&src, &mut dst, &mask, w, h, BlendMode::AlphaComposite, 255)?;
647 for &v in &dst {
649 assert_eq!(v, 255);
650 }
651 Ok(())
652 }
653
654 #[test]
655 fn test_masked_blend_all_transparent_preserves_dst() -> Result<(), BlendError> {
656 let w = 2u32;
657 let h = 2u32;
658 let src = vec![255u8; (w * h * 4) as usize];
659 let original_dst = vec![100u8; (w * h * 4) as usize];
660 let mut dst = original_dst.clone();
661 let mask = vec![0u8; (w * h) as usize]; BlendKernel::blend_masked(&src, &mut dst, &mask, w, h, BlendMode::AlphaComposite, 255)?;
663 assert_eq!(dst, original_dst);
665 Ok(())
666 }
667
668 #[test]
671 fn test_composite_layers_empty_returns_transparent() -> Result<(), BlendError> {
672 let result = BlendKernel::composite_layers(&[], 4, 4)?;
673 assert_eq!(result.len(), 4 * 4 * 4);
674 assert!(result.iter().all(|&v| v == 0));
675 Ok(())
676 }
677
678 #[test]
679 fn test_composite_layers_single_opaque() -> Result<(), BlendError> {
680 let layer = vec![200u8, 100, 50, 255]; let result = BlendKernel::composite_layers(&[(&layer, 255)], 1, 1)?;
682 assert_eq!(result.len(), 4);
683 assert_eq!(result[0], 200);
685 assert_eq!(result[1], 100);
686 assert_eq!(result[2], 50);
687 Ok(())
688 }
689
690 #[test]
693 fn test_apply_tint_white_tint_unchanged() -> Result<(), BlendError> {
694 let src = vec![100u8, 150, 200, 255];
695 let mut dst = vec![0u8; 4];
696 BlendKernel::apply_tint(&src, &mut dst, 1, 1, [255, 255, 255, 255])?;
697 for c in 0..3 {
698 let diff = (src[c] as i16 - dst[c] as i16).abs();
699 assert!(diff <= 1, "channel {c}: diff={diff}");
700 }
701 Ok(())
702 }
703
704 #[test]
705 fn test_apply_tint_black_tint_yields_black() -> Result<(), BlendError> {
706 let src = vec![200u8, 150, 100, 255];
707 let mut dst = vec![0u8; 4];
708 BlendKernel::apply_tint(&src, &mut dst, 1, 1, [0, 0, 0, 0])?;
709 assert_eq!(dst[0], 0);
710 assert_eq!(dst[1], 0);
711 assert_eq!(dst[2], 0);
712 Ok(())
713 }
714
715 #[test]
718 fn test_premultiply_alpha_full_opaque() -> Result<(), BlendError> {
719 let mut buf = vec![200u8, 100, 50, 255];
720 BlendKernel::premultiply_alpha(&mut buf, 1, 1)?;
721 assert_eq!(buf[0], 200);
723 assert_eq!(buf[1], 100);
724 assert_eq!(buf[2], 50);
725 Ok(())
726 }
727
728 #[test]
729 fn test_premultiply_alpha_half_opacity() -> Result<(), BlendError> {
730 let mut buf = vec![200u8, 200, 200, 128];
731 BlendKernel::premultiply_alpha(&mut buf, 1, 1)?;
732 let expected = (200u32 * 128 + 127) / 255;
734 let diff = (buf[0] as i32 - expected as i32).abs();
735 assert!(
736 diff <= 1,
737 "premultiplied R: got {}, expected ~{}",
738 buf[0],
739 expected
740 );
741 Ok(())
742 }
743
744 #[test]
745 fn test_unpremultiply_alpha_zero_alpha() -> Result<(), BlendError> {
746 let mut buf = vec![100u8, 100, 100, 0]; BlendKernel::unpremultiply_alpha(&mut buf, 1, 1)?;
748 assert_eq!(buf[0], 100); Ok(())
751 }
752
753 #[test]
754 fn test_premultiply_unpremultiply_roundtrip() -> Result<(), BlendError> {
755 let original = vec![200u8, 150, 100, 200];
756 let mut buf = original.clone();
757 BlendKernel::premultiply_alpha(&mut buf, 1, 1)?;
758 BlendKernel::unpremultiply_alpha(&mut buf, 1, 1)?;
759 for c in 0..3 {
760 let diff = (original[c] as i16 - buf[c] as i16).abs();
761 assert!(
762 diff <= 2,
763 "channel {c}: orig={} back={} diff={diff}",
764 original[c],
765 buf[c]
766 );
767 }
768 Ok(())
769 }
770
771 #[test]
774 fn test_blend_stats_returned() -> Result<(), BlendError> {
775 let src = vec![0u8; 4 * 4 * 4];
776 let mut dst = vec![0u8; 4 * 4 * 4];
777 let stats = BlendKernel::blend(&src, &mut dst, 4, 4, BlendMode::Screen, 200)?;
778 assert_eq!(stats.pixels_blended, 16);
779 assert_eq!(stats.mode, Some(BlendMode::Screen));
780 assert_eq!(stats.opacity, 200);
781 Ok(())
782 }
783}