1use crate::framebuffer::{pack_rgba, unpack, Framebuffer};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum BlendMode {
32 Normal,
34 Multiply,
36 Screen,
38 Overlay,
40 Darken,
42 Lighten,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct RgbaUnit {
52 pub r: f32,
54 pub g: f32,
56 pub b: f32,
58 pub a: f32,
60}
61
62impl RgbaUnit {
63 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
65 Self {
66 r: r.clamp(0.0, 1.0),
67 g: g.clamp(0.0, 1.0),
68 b: b.clamp(0.0, 1.0),
69 a: a.clamp(0.0, 1.0),
70 }
71 }
72
73 pub fn from_bytes(r: u8, g: u8, b: u8, a: u8) -> Self {
75 Self {
76 r: r as f32 / 255.0,
77 g: g as f32 / 255.0,
78 b: b as f32 / 255.0,
79 a: a as f32 / 255.0,
80 }
81 }
82
83 pub fn to_bytes(self) -> (u8, u8, u8, u8) {
85 (
86 (self.r.clamp(0.0, 1.0) * 255.0).round() as u8,
87 (self.g.clamp(0.0, 1.0) * 255.0).round() as u8,
88 (self.b.clamp(0.0, 1.0) * 255.0).round() as u8,
89 (self.a.clamp(0.0, 1.0) * 255.0).round() as u8,
90 )
91 }
92
93 pub fn premultiply(self) -> Self {
95 Self {
96 r: self.r * self.a,
97 g: self.g * self.a,
98 b: self.b * self.a,
99 a: self.a,
100 }
101 }
102
103 pub fn unpremultiply(self) -> Self {
106 if self.a <= f32::EPSILON {
107 Self {
108 r: 0.0,
109 g: 0.0,
110 b: 0.0,
111 a: 0.0,
112 }
113 } else {
114 Self {
115 r: self.r / self.a,
116 g: self.g / self.a,
117 b: self.b / self.a,
118 a: self.a,
119 }
120 }
121 }
122}
123
124pub fn to_unit(px: u32) -> RgbaUnit {
126 let (r, g, b, a) = unpack(px);
127 RgbaUnit::from_bytes(r, g, b, a)
128}
129
130pub fn from_unit(p: RgbaUnit) -> u32 {
132 let (r, g, b, a) = p.to_bytes();
133 pack_rgba(r, g, b, a)
134}
135
136#[inline]
145fn to_premult(r: u8, g: u8, b: u8, a: u8) -> (u16, u16, u16, u16) {
146 let a16 = a as u16;
147 (
148 (r as u16 * a16 + 127) / 255,
149 (g as u16 * a16 + 127) / 255,
150 (b as u16 * a16 + 127) / 255,
151 a16,
152 )
153}
154
155#[inline]
158fn from_premult(pr: u16, pg: u16, pb: u16, a: u16) -> (u8, u8, u8, u8) {
159 if a == 0 {
160 return (0, 0, 0, 0);
161 }
162 let half = a / 2;
163 (
164 ((pr * 255 + half) / a).min(255) as u8,
165 ((pg * 255 + half) / a).min(255) as u8,
166 ((pb * 255 + half) / a).min(255) as u8,
167 a as u8,
168 )
169}
170
171#[inline]
182#[allow(clippy::too_many_arguments)]
183pub(crate) fn blend_over_premult_u8(
184 src_r: u8,
185 src_g: u8,
186 src_b: u8,
187 src_a: u8,
188 dst_r: u8,
189 dst_g: u8,
190 dst_b: u8,
191 dst_a: u8,
192) -> (u8, u8, u8, u8) {
193 let (spr, spg, spb, sa) = to_premult(src_r, src_g, src_b, src_a);
194 let (dpr, dpg, dpb, da) = to_premult(dst_r, dst_g, dst_b, dst_a);
195 let inv_sa = 255u16 - sa;
196 let out_r = spr + (dpr * inv_sa + 127) / 255;
197 let out_g = spg + (dpg * inv_sa + 127) / 255;
198 let out_b = spb + (dpb * inv_sa + 127) / 255;
199 let out_a = sa + (da * inv_sa + 127) / 255;
200 from_premult(out_r, out_g, out_b, out_a)
201}
202
203pub fn blend_mode(mode: BlendMode, src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
218 match mode {
219 BlendMode::Normal => over(src, dst),
220 BlendMode::Multiply => mode_combine(src, dst, |a, b| a * b),
221 BlendMode::Screen => mode_combine(src, dst, |a, b| a + b - a * b),
222 BlendMode::Overlay => mode_combine(src, dst, |cs, cb| {
223 if cb <= 0.5 {
225 2.0 * cs * cb
226 } else {
227 1.0 - 2.0 * (1.0 - cs) * (1.0 - cb)
228 }
229 }),
230 BlendMode::Darken => mode_combine(src, dst, f32::min),
231 BlendMode::Lighten => mode_combine(src, dst, f32::max),
232 }
233}
234
235fn over(src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
237 let out_a = src.a + dst.a * (1.0 - src.a);
238 if out_a <= f32::EPSILON {
239 return RgbaUnit {
240 r: 0.0,
241 g: 0.0,
242 b: 0.0,
243 a: 0.0,
244 };
245 }
246 let blend = |s: f32, d: f32| (s * src.a + d * dst.a * (1.0 - src.a)) / out_a;
247 RgbaUnit {
248 r: blend(src.r, dst.r),
249 g: blend(src.g, dst.g),
250 b: blend(src.b, dst.b),
251 a: out_a,
252 }
253}
254
255fn mode_combine<F>(src: RgbaUnit, dst: RgbaUnit, blend_fn: F) -> RgbaUnit
258where
259 F: Fn(f32, f32) -> f32,
260{
261 let out_a = src.a + dst.a * (1.0 - src.a);
262 if out_a <= f32::EPSILON {
263 return RgbaUnit {
264 r: 0.0,
265 g: 0.0,
266 b: 0.0,
267 a: 0.0,
268 };
269 }
270 let channel = |cs: f32, cb: f32| -> f32 {
272 let b = blend_fn(cs.clamp(0.0, 1.0), cb.clamp(0.0, 1.0));
273 let v = (1.0 - dst.a) * cs * src.a + (1.0 - src.a) * cb * dst.a + src.a * dst.a * b;
274 (v / out_a).clamp(0.0, 1.0)
276 };
277 RgbaUnit {
278 r: channel(src.r, dst.r),
279 g: channel(src.g, dst.g),
280 b: channel(src.b, dst.b),
281 a: out_a,
282 }
283}
284
285pub fn blend_pixel(fb: &mut Framebuffer, x: u32, y: u32, src: RgbaUnit, mode: BlendMode) {
289 if x >= fb.width() || y >= fb.height() {
290 return;
291 }
292 let Some(dst_px) = fb.get(x, y) else { return };
293 let dst = to_unit(dst_px);
294 let out = blend_mode(mode, src, dst);
295 fb.set(x, y, from_unit(out));
296}
297
298pub fn composite_into(
306 fb: &mut Framebuffer,
307 src: &[u8],
308 w: u32,
309 h: u32,
310 dst_x: i64,
311 dst_y: i64,
312 mode: BlendMode,
313) -> usize {
314 if w == 0 || h == 0 {
315 return 0;
316 }
317 let required_len = (w as usize)
324 .checked_mul(h as usize)
325 .and_then(|n| n.checked_mul(4));
326 let Some(required_len) = required_len else {
327 return 0;
328 };
329 if src.len() < required_len {
330 return 0;
331 }
332 let mut written = 0;
333 for j in 0..h {
334 for i in 0..w {
335 let si = (j as usize * w as usize + i as usize) * 4;
338 let r = src[si];
339 let g = src[si + 1];
340 let b = src[si + 2];
341 let a = src[si + 3];
342 if a == 0 {
343 continue;
344 }
345 let px = dst_x + i as i64;
346 let py = dst_y + j as i64;
347 if px < 0 || py < 0 {
348 continue;
349 }
350 let pu = px as u32;
351 let pv = py as u32;
352 if pu >= fb.width() || pv >= fb.height() {
353 continue;
354 }
355 if mode == BlendMode::Normal {
356 let dst_px = fb.get(pu, pv).unwrap_or(0);
359 let (dr, dg, db, da) = unpack(dst_px);
360 let (or_, og, ob, oa) = blend_over_premult_u8(r, g, b, a, dr, dg, db, da);
361 fb.set(pu, pv, crate::framebuffer::pack_rgba(or_, og, ob, oa));
362 } else {
363 blend_pixel(fb, pu, pv, RgbaUnit::from_bytes(r, g, b, a), mode);
364 }
365 written += 1;
366 }
367 }
368 written
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use oxiui_core::Color;
375
376 fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
377 (a - b).abs() <= eps
378 }
379
380 #[test]
381 fn round_trip_unit_bytes() {
382 let p = RgbaUnit::from_bytes(123, 45, 200, 99);
383 let (r, g, b, a) = p.to_bytes();
384 assert_eq!((r, g, b, a), (123, 45, 200, 99));
385 }
386
387 #[test]
388 fn premultiply_inverse() {
389 let p = RgbaUnit::new(0.8, 0.4, 0.2, 0.5);
390 let q = p.premultiply().unpremultiply();
391 assert!(approx_eq(q.r, p.r, 1e-5));
392 assert!(approx_eq(q.g, p.g, 1e-5));
393 assert!(approx_eq(q.b, p.b, 1e-5));
394 assert!(approx_eq(q.a, p.a, 1e-5));
395 }
396
397 #[test]
398 fn premultiply_zero_alpha_safe() {
399 let p = RgbaUnit::new(0.9, 0.9, 0.9, 0.0);
400 let q = p.premultiply().unpremultiply();
401 assert!(approx_eq(q.a, 0.0, 1e-5));
402 assert!(approx_eq(q.r, 0.0, 1e-5));
403 }
404
405 #[test]
406 fn multiply_white_is_identity() {
407 let src = RgbaUnit::from_bytes(255, 255, 255, 255);
409 let dst = RgbaUnit::from_bytes(255, 0, 0, 255);
410 let out = blend_mode(BlendMode::Multiply, src, dst);
411 let (r, g, b, a) = out.to_bytes();
412 assert_eq!((r, g, b, a), (255, 0, 0, 255));
413 }
414
415 #[test]
416 fn screen_black_is_identity() {
417 let src = RgbaUnit::from_bytes(0, 0, 0, 255);
418 let dst = RgbaUnit::from_bytes(80, 120, 200, 255);
419 let out = blend_mode(BlendMode::Screen, src, dst);
420 let (r, g, b, _) = out.to_bytes();
421 assert!((r as i32 - 80).abs() <= 1);
423 assert!((g as i32 - 120).abs() <= 1);
424 assert!((b as i32 - 200).abs() <= 1);
425 }
426
427 #[test]
428 fn darken_picks_min_per_channel() {
429 let src = RgbaUnit::from_bytes(100, 200, 50, 255);
430 let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
431 let out = blend_mode(BlendMode::Darken, src, dst);
432 let (r, g, b, _) = out.to_bytes();
433 assert!((r as i32 - 100).abs() <= 1);
434 assert!((g as i32 - 50).abs() <= 1);
435 assert!((b as i32 - 50).abs() <= 1);
436 }
437
438 #[test]
439 fn lighten_picks_max_per_channel() {
440 let src = RgbaUnit::from_bytes(100, 200, 50, 255);
441 let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
442 let out = blend_mode(BlendMode::Lighten, src, dst);
443 let (r, g, b, _) = out.to_bytes();
444 assert!((r as i32 - 150).abs() <= 1);
445 assert!((g as i32 - 200).abs() <= 1);
446 assert!((b as i32 - 50).abs() <= 1);
447 }
448
449 #[test]
450 fn overlay_changes_with_dst_mid() {
451 let src = RgbaUnit::from_bytes(128, 128, 128, 255);
454 let dst = RgbaUnit::from_bytes(128, 128, 128, 255);
455 let out = blend_mode(BlendMode::Overlay, src, dst);
456 let (r, _, _, a) = out.to_bytes();
457 assert_eq!(a, 255);
458 let _ = r;
461 }
462
463 #[test]
464 fn over_zero_alpha_safe() {
465 let src = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
466 let dst = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
467 let out = blend_mode(BlendMode::Normal, src, dst);
468 let (_, _, _, a) = out.to_bytes();
469 assert_eq!(a, 0);
470 }
471
472 #[test]
473 fn composite_into_writes_pixels() {
474 let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
475 let src = vec![
477 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
478 ];
479 let written = composite_into(&mut fb, &src, 2, 2, 1, 1, BlendMode::Normal);
480 assert_eq!(written, 4);
481 let result = fb.get_rgba(1, 1).unwrap_or_default();
482 assert_eq!(result, (255, 255, 255, 255));
483 }
484
485 #[test]
486 fn blend_pixel_out_of_bounds_noop() {
487 let mut fb = Framebuffer::with_fill(2, 2, Color(10, 20, 30, 255));
488 blend_pixel(
489 &mut fb,
490 99,
491 99,
492 RgbaUnit::from_bytes(255, 255, 255, 255),
493 BlendMode::Normal,
494 );
495 let v = fb.get_rgba(0, 0).unwrap_or((0, 0, 0, 0));
496 assert_eq!(v, (10, 20, 30, 255));
497 }
498
499 #[test]
500 fn blend_multiply_black_is_black() {
501 let black = RgbaUnit::from_bytes(0, 0, 0, 255);
503 let src = RgbaUnit::from_bytes(200, 100, 50, 255);
504 let out = blend_mode(BlendMode::Multiply, src, black);
505 let (r, g, b, _) = out.to_bytes();
506 assert_eq!((r, g, b), (0, 0, 0), "multiply with black must give black");
507 }
508
509 #[test]
510 fn blend_screen_white_is_white() {
511 let white = RgbaUnit::from_bytes(255, 255, 255, 255);
513 let src = RgbaUnit::from_bytes(100, 150, 200, 255);
514 let out = blend_mode(BlendMode::Screen, src, white);
515 let (r, g, b, _) = out.to_bytes();
516 assert_eq!(
517 (r, g, b),
518 (255, 255, 255),
519 "screen with white must give white"
520 );
521 }
522
523 #[test]
524 fn premultiply_roundtrip() {
525 let p = RgbaUnit::new(0.7, 0.3, 0.9, 0.6);
527 let q = p.premultiply().unpremultiply();
528 assert!(approx_eq(q.r, p.r, 1e-4), "r: {:.5} vs {:.5}", q.r, p.r);
529 assert!(approx_eq(q.g, p.g, 1e-4), "g: {:.5} vs {:.5}", q.g, p.g);
530 assert!(approx_eq(q.b, p.b, 1e-4), "b: {:.5} vs {:.5}", q.b, p.b);
531 assert!(approx_eq(q.a, p.a, 1e-5), "a: {:.5} vs {:.5}", q.a, p.a);
532 }
533
534 #[test]
539 fn test_premult_blend_golden() {
540 let (r, g, b, a) = blend_over_premult_u8(255, 0, 0, 128, 255, 255, 255, 255);
552 assert!((r as i32 - 255).abs() <= 2, "r={r} expected ~255");
553 assert!((g as i32 - 127).abs() <= 2, "g={g} expected ~127");
554 assert!((b as i32 - 127).abs() <= 2, "b={b} expected ~127");
555 assert!((a as i32 - 255).abs() <= 2, "a={a} expected ~255");
556 }
557
558 #[test]
559 fn test_blend_signatures_stable() {
560 let src = RgbaUnit::from_bytes(128, 64, 32, 200);
565 let dst = RgbaUnit::from_bytes(10, 20, 30, 255);
566 let _ = blend_mode(BlendMode::Normal, src, dst);
567 let _ = blend_mode(BlendMode::Multiply, src, dst);
568 let _ = blend_mode(BlendMode::Screen, src, dst);
569 let _ = blend_mode(BlendMode::Overlay, src, dst);
570 let _ = blend_mode(BlendMode::Darken, src, dst);
571 let _ = blend_mode(BlendMode::Lighten, src, dst);
572
573 let px = crate::framebuffer::pack_rgba(100, 150, 200, 180);
575 let unit = to_unit(px);
576 let _ = from_unit(unit);
577
578 let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
580 blend_pixel(&mut fb, 0, 0, src, BlendMode::Normal);
581 blend_pixel(&mut fb, 99, 99, src, BlendMode::Screen); let src_bytes = [255u8, 0, 0, 128];
585 let written = composite_into(&mut fb, &src_bytes, 1, 1, 0, 0, BlendMode::Normal);
586 assert!(written <= 1);
587 }
588
589 #[test]
594 fn composite_into_near_u32_boundary_does_not_overflow_or_panic() {
595 let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
603 let src = vec![0u8; 16]; let written = composite_into(&mut fb, &src, 70_000, 70_000, 0, 0, BlendMode::Normal);
605 assert_eq!(written, 0, "oversized w*h*4 must be rejected, not wrapped");
606
607 for y in 0..4 {
609 for x in 0..4 {
610 assert_eq!(fb.get_rgba(x, y), Some((0, 0, 0, 255)));
611 }
612 }
613 }
614
615 #[test]
616 fn composite_into_exact_u32_boundary_dims_no_panic() {
617 let mut fb = Framebuffer::with_fill(2, 2, Color(0, 0, 0, 255));
623 let src = vec![0u8; 4];
624 let written = composite_into(&mut fb, &src, 65_536, 65_536, 0, 0, BlendMode::Normal);
625 assert_eq!(written, 0);
626 }
627}