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() {
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).unwrap();
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 }
530
531 #[test]
534 fn test_alpha_composite_fully_opaque_src() {
535 let src = vec![200u8, 100, 50, 255]; let mut dst = vec![0u8, 0, 0, 255];
537 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::AlphaComposite, 255).unwrap();
538 assert_eq!(dst[0], 200);
539 assert_eq!(dst[1], 100);
540 assert_eq!(dst[2], 50);
541 assert_eq!(dst[3], 255);
542 }
543
544 #[test]
547 fn test_additive_blend_clamps_to_255() {
548 let src = vec![200u8, 200, 200, 255];
549 let mut dst = vec![100u8, 100, 100, 255];
550 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255).unwrap();
551 assert_eq!(dst[0], 255, "200+100=300 → clamp to 255");
552 }
553
554 #[test]
555 fn test_additive_blend_zero_src() {
556 let src = vec![0u8, 0, 0, 255];
557 let original = vec![100u8, 150, 200, 255];
558 let mut dst = original.clone();
559 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255).unwrap();
560 assert_eq!(dst[..3], original[..3]);
561 }
562
563 #[test]
566 fn test_multiply_with_white_src_unchanged() {
567 let src = vec![255u8, 255, 255, 255]; let original = vec![100u8, 150, 200, 255];
569 let mut dst = original.clone();
570 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Multiply, 255).unwrap();
571 for c in 0..3 {
573 let diff = (original[c] as i16 - dst[c] as i16).abs();
574 assert!(diff <= 1, "channel {c}: diff={diff}");
575 }
576 }
577
578 #[test]
579 fn test_multiply_with_black_src_yields_zero() {
580 let src = vec![0u8, 0, 0, 255]; let mut dst = vec![200u8, 150, 100, 255];
582 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Multiply, 255).unwrap();
583 for c in 0..3 {
585 assert_eq!(dst[c], 0, "channel {c} should be 0");
586 }
587 }
588
589 #[test]
592 fn test_screen_with_black_src_unchanged() {
593 let src = vec![0u8, 0, 0, 255]; let original = vec![100u8, 150, 200, 255];
595 let mut dst = original.clone();
596 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Screen, 255).unwrap();
597 for c in 0..3 {
598 let diff = (original[c] as i16 - dst[c] as i16).abs();
599 assert!(diff <= 1, "channel {c}: diff={diff}");
600 }
601 }
602
603 #[test]
604 fn test_screen_with_white_src_yields_white() {
605 let src = vec![255u8, 255, 255, 255]; let mut dst = vec![100u8, 150, 200, 255];
607 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Screen, 255).unwrap();
608 for c in 0..3 {
609 assert_eq!(
610 dst[c], 255,
611 "channel {c} should be 255 after screen with white"
612 );
613 }
614 }
615
616 #[test]
619 fn test_difference_with_same_src_dst_yields_black() {
620 let src = vec![100u8, 150, 200, 255];
621 let mut dst = vec![100u8, 150, 200, 255];
622 BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Difference, 255).unwrap();
623 for c in 0..3 {
624 assert_eq!(dst[c], 0, "difference of equal values should be 0");
625 }
626 }
627
628 #[test]
631 fn test_masked_blend_all_opaque() {
632 let w = 2u32;
633 let h = 2u32;
634 let src = vec![255u8; (w * h * 4) as usize];
635 let mut dst = vec![0u8; (w * h * 4) as usize];
636 let mask = vec![255u8; (w * h) as usize]; BlendKernel::blend_masked(&src, &mut dst, &mask, w, h, BlendMode::AlphaComposite, 255)
638 .unwrap();
639 for &v in &dst {
641 assert_eq!(v, 255);
642 }
643 }
644
645 #[test]
646 fn test_masked_blend_all_transparent_preserves_dst() {
647 let w = 2u32;
648 let h = 2u32;
649 let src = vec![255u8; (w * h * 4) as usize];
650 let original_dst = vec![100u8; (w * h * 4) as usize];
651 let mut dst = original_dst.clone();
652 let mask = vec![0u8; (w * h) as usize]; BlendKernel::blend_masked(&src, &mut dst, &mask, w, h, BlendMode::AlphaComposite, 255)
654 .unwrap();
655 assert_eq!(dst, original_dst);
657 }
658
659 #[test]
662 fn test_composite_layers_empty_returns_transparent() {
663 let result = BlendKernel::composite_layers(&[], 4, 4).unwrap();
664 assert_eq!(result.len(), 4 * 4 * 4);
665 assert!(result.iter().all(|&v| v == 0));
666 }
667
668 #[test]
669 fn test_composite_layers_single_opaque() {
670 let layer = vec![200u8, 100, 50, 255]; let result = BlendKernel::composite_layers(&[(&layer, 255)], 1, 1).unwrap();
672 assert_eq!(result.len(), 4);
673 assert_eq!(result[0], 200);
675 assert_eq!(result[1], 100);
676 assert_eq!(result[2], 50);
677 }
678
679 #[test]
682 fn test_apply_tint_white_tint_unchanged() {
683 let src = vec![100u8, 150, 200, 255];
684 let mut dst = vec![0u8; 4];
685 BlendKernel::apply_tint(&src, &mut dst, 1, 1, [255, 255, 255, 255]).unwrap();
686 for c in 0..3 {
687 let diff = (src[c] as i16 - dst[c] as i16).abs();
688 assert!(diff <= 1, "channel {c}: diff={diff}");
689 }
690 }
691
692 #[test]
693 fn test_apply_tint_black_tint_yields_black() {
694 let src = vec![200u8, 150, 100, 255];
695 let mut dst = vec![0u8; 4];
696 BlendKernel::apply_tint(&src, &mut dst, 1, 1, [0, 0, 0, 0]).unwrap();
697 assert_eq!(dst[0], 0);
698 assert_eq!(dst[1], 0);
699 assert_eq!(dst[2], 0);
700 }
701
702 #[test]
705 fn test_premultiply_alpha_full_opaque() {
706 let mut buf = vec![200u8, 100, 50, 255];
707 BlendKernel::premultiply_alpha(&mut buf, 1, 1).unwrap();
708 assert_eq!(buf[0], 200);
710 assert_eq!(buf[1], 100);
711 assert_eq!(buf[2], 50);
712 }
713
714 #[test]
715 fn test_premultiply_alpha_half_opacity() {
716 let mut buf = vec![200u8, 200, 200, 128];
717 BlendKernel::premultiply_alpha(&mut buf, 1, 1).unwrap();
718 let expected = (200u32 * 128 + 127) / 255;
720 let diff = (buf[0] as i32 - expected as i32).abs();
721 assert!(
722 diff <= 1,
723 "premultiplied R: got {}, expected ~{}",
724 buf[0],
725 expected
726 );
727 }
728
729 #[test]
730 fn test_unpremultiply_alpha_zero_alpha() {
731 let mut buf = vec![100u8, 100, 100, 0]; BlendKernel::unpremultiply_alpha(&mut buf, 1, 1).unwrap();
733 assert_eq!(buf[0], 100); }
736
737 #[test]
738 fn test_premultiply_unpremultiply_roundtrip() {
739 let original = vec![200u8, 150, 100, 200];
740 let mut buf = original.clone();
741 BlendKernel::premultiply_alpha(&mut buf, 1, 1).unwrap();
742 BlendKernel::unpremultiply_alpha(&mut buf, 1, 1).unwrap();
743 for c in 0..3 {
744 let diff = (original[c] as i16 - buf[c] as i16).abs();
745 assert!(
746 diff <= 2,
747 "channel {c}: orig={} back={} diff={diff}",
748 original[c],
749 buf[c]
750 );
751 }
752 }
753
754 #[test]
757 fn test_blend_stats_returned() {
758 let src = vec![0u8; 4 * 4 * 4];
759 let mut dst = vec![0u8; 4 * 4 * 4];
760 let stats = BlendKernel::blend(&src, &mut dst, 4, 4, BlendMode::Screen, 200).unwrap();
761 assert_eq!(stats.pixels_blended, 16);
762 assert_eq!(stats.mode, Some(BlendMode::Screen));
763 assert_eq!(stats.opacity, 200);
764 }
765}