Skip to main content

agg_rust/
comp_op.rs

1//! SVG compositing operations and compositing pixel format.
2//!
3//! Port of the compositing portion of `agg_pixfmt_rgba.h`.
4//! Provides 25 SVG compositing modes (the standard 24 plus `minus`)
5//! and `PixfmtRgba32CompOp`, a pixel format that dispatches blending
6//! through a runtime-selectable compositing operation.
7
8use crate::basics::CoverType;
9use crate::color::Rgba8;
10use crate::pixfmt_rgba::PixelFormat;
11use crate::rendering_buffer::RowAccessor;
12
13// ============================================================================
14// CompOp enum — 25 SVG/AGG compositing modes
15// ============================================================================
16
17/// SVG compositing operation.
18///
19/// Port of C++ `comp_op_e`. Each variant corresponds to a specific
20/// alpha-compositing formula from the SVG Compositing specification.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[repr(u8)]
23pub enum CompOp {
24    Clear = 0,
25    Src = 1,
26    Dst = 2,
27    SrcOver = 3,
28    DstOver = 4,
29    SrcIn = 5,
30    DstIn = 6,
31    SrcOut = 7,
32    DstOut = 8,
33    SrcAtop = 9,
34    DstAtop = 10,
35    Xor = 11,
36    Plus = 12,
37    Minus = 13,
38    Multiply = 14,
39    Screen = 15,
40    Overlay = 16,
41    Darken = 17,
42    Lighten = 18,
43    ColorDodge = 19,
44    ColorBurn = 20,
45    HardLight = 21,
46    SoftLight = 22,
47    Difference = 23,
48    Exclusion = 24,
49}
50
51impl Default for CompOp {
52    fn default() -> Self {
53        CompOp::SrcOver
54    }
55}
56
57// ============================================================================
58// Premultiplied f64 RGBA working space (like C++ blender_base::rgba)
59// ============================================================================
60
61/// Premultiplied RGBA in f64 [0, 1] working space.
62#[derive(Debug, Clone, Copy)]
63struct PremulRgba {
64    r: f64,
65    g: f64,
66    b: f64,
67    a: f64,
68}
69
70impl PremulRgba {
71    /// Read from u8 pixel buffer as premultiplied f64, scaled by cover.
72    #[inline]
73    fn get(r: u8, g: u8, b: u8, a: u8, cover: u8) -> Self {
74        if cover == 0 {
75            return Self {
76                r: 0.0,
77                g: 0.0,
78                b: 0.0,
79                a: 0.0,
80            };
81        }
82        let mut c = Self {
83            r: Rgba8::to_double(r),
84            g: Rgba8::to_double(g),
85            b: Rgba8::to_double(b),
86            a: Rgba8::to_double(a),
87        };
88        if cover < 255 {
89            let x = cover as f64 / 255.0;
90            c.r *= x;
91            c.g *= x;
92            c.b *= x;
93            c.a *= x;
94        }
95        c
96    }
97
98    /// Read from pixel buffer slice (RGBA order) as premultiplied f64.
99    #[inline]
100    fn get_pix(p: &[u8], cover: u8) -> Self {
101        Self::get(p[0], p[1], p[2], p[3], cover)
102    }
103
104    /// Write premultiplied f64 back to pixel buffer.
105    #[inline]
106    fn set(p: &mut [u8], c: &PremulRgba) {
107        p[0] = Rgba8::from_double(c.r);
108        p[1] = Rgba8::from_double(c.g);
109        p[2] = Rgba8::from_double(c.b);
110        p[3] = Rgba8::from_double(c.a);
111    }
112
113    /// Write direct RGBA values.
114    #[inline]
115    fn set_rgba(p: &mut [u8], r: u8, g: u8, b: u8, a: u8) {
116        p[0] = r;
117        p[1] = g;
118        p[2] = b;
119        p[3] = a;
120    }
121
122    /// Clamp all components to [0, 1].
123    #[inline]
124    fn clip(c: &mut PremulRgba) {
125        c.r = c.r.clamp(0.0, 1.0);
126        c.g = c.g.clamp(0.0, 1.0);
127        c.b = c.b.clamp(0.0, 1.0);
128        c.a = c.a.clamp(0.0, 1.0);
129    }
130}
131
132// ============================================================================
133// Per-operation blend functions
134// ============================================================================
135
136/// Blend pixel `p` (RGBA u8) with premultiplied source (r,g,b,a) using `op`.
137///
138/// Source colors are NON-premultiplied; this function premultiplies them
139/// before dispatching (matching C++ `comp_op_adaptor_rgba`).
140#[inline]
141fn comp_op_blend(op: CompOp, p: &mut [u8], sr: u8, sg: u8, sb: u8, sa: u8, cover: u8) {
142    // Premultiply source by alpha (matching comp_op_adaptor_rgba)
143    let r = Rgba8::multiply(sr, sa);
144    let g = Rgba8::multiply(sg, sa);
145    let b = Rgba8::multiply(sb, sa);
146    let a = sa;
147
148    match op {
149        CompOp::Clear => blend_clear(p, cover),
150        CompOp::Src => blend_src(p, r, g, b, a, cover),
151        CompOp::Dst => {} // no-op
152        CompOp::SrcOver => blend_src_over(p, r, g, b, a, cover),
153        CompOp::DstOver => blend_dst_over(p, r, g, b, a, cover),
154        CompOp::SrcIn => blend_src_in(p, r, g, b, a, cover),
155        CompOp::DstIn => blend_dst_in(p, a, cover),
156        CompOp::SrcOut => blend_src_out(p, r, g, b, a, cover),
157        CompOp::DstOut => blend_dst_out(p, a, cover),
158        CompOp::SrcAtop => blend_src_atop(p, r, g, b, a, cover),
159        CompOp::DstAtop => blend_dst_atop(p, r, g, b, a, cover),
160        CompOp::Xor => blend_xor(p, r, g, b, a, cover),
161        CompOp::Plus => blend_plus(p, r, g, b, a, cover),
162        CompOp::Minus => blend_minus(p, r, g, b, a, cover),
163        CompOp::Multiply => blend_multiply(p, r, g, b, a, cover),
164        CompOp::Screen => blend_screen(p, r, g, b, a, cover),
165        CompOp::Overlay => blend_overlay(p, r, g, b, a, cover),
166        CompOp::Darken => blend_darken(p, r, g, b, a, cover),
167        CompOp::Lighten => blend_lighten(p, r, g, b, a, cover),
168        CompOp::ColorDodge => blend_color_dodge(p, r, g, b, a, cover),
169        CompOp::ColorBurn => blend_color_burn(p, r, g, b, a, cover),
170        CompOp::HardLight => blend_hard_light(p, r, g, b, a, cover),
171        CompOp::SoftLight => blend_soft_light(p, r, g, b, a, cover),
172        CompOp::Difference => blend_difference(p, r, g, b, a, cover),
173        CompOp::Exclusion => blend_exclusion(p, r, g, b, a, cover),
174    }
175}
176
177// ---- Clear: Dca' = 0, Da' = 0
178#[inline]
179fn blend_clear(p: &mut [u8], cover: u8) {
180    if cover >= 255 {
181        p[0] = 0;
182        p[1] = 0;
183        p[2] = 0;
184        p[3] = 0;
185    } else if cover > 0 {
186        let d = PremulRgba::get_pix(p, 255 - cover);
187        PremulRgba::set(p, &d);
188    }
189}
190
191// ---- Src: Dca' = Sca, Da' = Sa
192#[inline]
193fn blend_src(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
194    if cover >= 255 {
195        PremulRgba::set_rgba(p, r, g, b, a);
196    } else {
197        let s = PremulRgba::get(r, g, b, a, cover);
198        let d = PremulRgba::get_pix(p, 255 - cover);
199        let out = PremulRgba {
200            r: d.r + s.r,
201            g: d.g + s.g,
202            b: d.b + s.b,
203            a: d.a + s.a,
204        };
205        PremulRgba::set(p, &out);
206    }
207}
208
209// ---- SrcOver: Dca' = Sca + Dca.(1 - Sa)
210#[inline]
211fn blend_src_over(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
212    let s = PremulRgba::get(r, g, b, a, cover);
213    let d = PremulRgba::get_pix(p, 255);
214    let out = PremulRgba {
215        r: d.r + s.r - d.r * s.a,
216        g: d.g + s.g - d.g * s.a,
217        b: d.b + s.b - d.b * s.a,
218        a: d.a + s.a - d.a * s.a,
219    };
220    PremulRgba::set(p, &out);
221}
222
223// ---- DstOver: Dca' = Dca + Sca.(1 - Da)
224#[inline]
225fn blend_dst_over(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
226    let s = PremulRgba::get(r, g, b, a, cover);
227    let mut d = PremulRgba::get_pix(p, 255);
228    let d1a = 1.0 - d.a;
229    d.r += s.r * d1a;
230    d.g += s.g * d1a;
231    d.b += s.b * d1a;
232    d.a += s.a * d1a;
233    PremulRgba::set(p, &d);
234}
235
236// ---- SrcIn: Dca' = Sca.Da
237#[inline]
238fn blend_src_in(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
239    let da = Rgba8::to_double(p[3]);
240    if da > 0.0 {
241        let s = PremulRgba::get(r, g, b, a, cover);
242        let mut d = PremulRgba::get_pix(p, 255 - cover);
243        d.r += s.r * da;
244        d.g += s.g * da;
245        d.b += s.b * da;
246        d.a += s.a * da;
247        PremulRgba::set(p, &d);
248    }
249}
250
251// ---- DstIn: Dca' = Dca.Sa
252#[inline]
253fn blend_dst_in(p: &mut [u8], a: u8, cover: u8) {
254    let sa = Rgba8::to_double(a);
255    let mut d = PremulRgba::get_pix(p, 255 - cover);
256    let d2 = PremulRgba::get_pix(p, cover);
257    d.r += d2.r * sa;
258    d.g += d2.g * sa;
259    d.b += d2.b * sa;
260    d.a += d2.a * sa;
261    PremulRgba::set(p, &d);
262}
263
264// ---- SrcOut: Dca' = Sca.(1 - Da)
265#[inline]
266fn blend_src_out(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
267    let s = PremulRgba::get(r, g, b, a, cover);
268    let mut d = PremulRgba::get_pix(p, 255 - cover);
269    let d1a = 1.0 - Rgba8::to_double(p[3]);
270    d.r += s.r * d1a;
271    d.g += s.g * d1a;
272    d.b += s.b * d1a;
273    d.a += s.a * d1a;
274    PremulRgba::set(p, &d);
275}
276
277// ---- DstOut: Dca' = Dca.(1 - Sa)
278#[inline]
279fn blend_dst_out(p: &mut [u8], a: u8, cover: u8) {
280    let mut d = PremulRgba::get_pix(p, 255 - cover);
281    let dc = PremulRgba::get_pix(p, cover);
282    let s1a = 1.0 - Rgba8::to_double(a);
283    d.r += dc.r * s1a;
284    d.g += dc.g * s1a;
285    d.b += dc.b * s1a;
286    d.a += dc.a * s1a;
287    PremulRgba::set(p, &d);
288}
289
290// ---- SrcAtop: Dca' = Sca.Da + Dca.(1 - Sa), Da' = Da
291#[inline]
292fn blend_src_atop(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
293    let s = PremulRgba::get(r, g, b, a, cover);
294    let mut d = PremulRgba::get_pix(p, 255);
295    let s1a = 1.0 - s.a;
296    d.r = s.r * d.a + d.r * s1a;
297    d.g = s.g * d.a + d.g * s1a;
298    d.b = s.b * d.a + d.b * s1a;
299    // Da' = Da (unchanged)
300    PremulRgba::set(p, &d);
301}
302
303// ---- DstAtop: Dca' = Dca.Sa + Sca.(1 - Da), Da' = Sa
304#[inline]
305fn blend_dst_atop(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
306    let sc = PremulRgba::get(r, g, b, a, cover);
307    let dc = PremulRgba::get_pix(p, cover);
308    let mut d = PremulRgba::get_pix(p, 255 - cover);
309    let sa = Rgba8::to_double(a);
310    let d1a = 1.0 - Rgba8::to_double(p[3]);
311    d.r += dc.r * sa + sc.r * d1a;
312    d.g += dc.g * sa + sc.g * d1a;
313    d.b += dc.b * sa + sc.b * d1a;
314    d.a += sc.a;
315    PremulRgba::set(p, &d);
316}
317
318// ---- Xor: Dca' = Sca.(1 - Da) + Dca.(1 - Sa)
319#[inline]
320fn blend_xor(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
321    let s = PremulRgba::get(r, g, b, a, cover);
322    let mut d = PremulRgba::get_pix(p, 255);
323    let s1a = 1.0 - s.a;
324    let d1a = 1.0 - Rgba8::to_double(p[3]);
325    d.r = s.r * d1a + d.r * s1a;
326    d.g = s.g * d1a + d.g * s1a;
327    d.b = s.b * d1a + d.b * s1a;
328    d.a = s.a + d.a - 2.0 * s.a * d.a;
329    PremulRgba::set(p, &d);
330}
331
332// ---- Plus: Dca' = Sca + Dca (clamped)
333#[inline]
334fn blend_plus(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
335    let s = PremulRgba::get(r, g, b, a, cover);
336    if s.a > 0.0 {
337        let mut d = PremulRgba::get_pix(p, 255);
338        d.a = (d.a + s.a).min(1.0);
339        d.r = (d.r + s.r).min(d.a);
340        d.g = (d.g + s.g).min(d.a);
341        d.b = (d.b + s.b).min(d.a);
342        PremulRgba::clip(&mut d);
343        PremulRgba::set(p, &d);
344    }
345}
346
347// ---- Minus: Dca' = Dca - Sca (clamped)
348#[inline]
349fn blend_minus(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
350    let s = PremulRgba::get(r, g, b, a, cover);
351    if s.a > 0.0 {
352        let mut d = PremulRgba::get_pix(p, 255);
353        d.a += s.a - s.a * d.a;
354        d.r = (d.r - s.r).max(0.0);
355        d.g = (d.g - s.g).max(0.0);
356        d.b = (d.b - s.b).max(0.0);
357        PremulRgba::clip(&mut d);
358        PremulRgba::set(p, &d);
359    }
360}
361
362// ---- Multiply: Dca' = Sca.Dca + Sca.(1 - Da) + Dca.(1 - Sa)
363#[inline]
364fn blend_multiply(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
365    let s = PremulRgba::get(r, g, b, a, cover);
366    if s.a > 0.0 {
367        let mut d = PremulRgba::get_pix(p, 255);
368        let s1a = 1.0 - s.a;
369        let d1a = 1.0 - d.a;
370        d.r = s.r * d.r + s.r * d1a + d.r * s1a;
371        d.g = s.g * d.g + s.g * d1a + d.g * s1a;
372        d.b = s.b * d.b + s.b * d1a + d.b * s1a;
373        d.a += s.a - s.a * d.a;
374        PremulRgba::clip(&mut d);
375        PremulRgba::set(p, &d);
376    }
377}
378
379// ---- Screen: Dca' = Sca + Dca - Sca.Dca
380#[inline]
381fn blend_screen(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
382    let s = PremulRgba::get(r, g, b, a, cover);
383    if s.a > 0.0 {
384        let mut d = PremulRgba::get_pix(p, 255);
385        d.r += s.r - s.r * d.r;
386        d.g += s.g - s.g * d.g;
387        d.b += s.b - s.b * d.b;
388        d.a += s.a - s.a * d.a;
389        PremulRgba::clip(&mut d);
390        PremulRgba::set(p, &d);
391    }
392}
393
394// ---- Overlay
395#[inline]
396fn overlay_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
397    if 2.0 * dca <= da {
398        2.0 * sca * dca + sca * d1a + dca * s1a
399    } else {
400        sada - 2.0 * (da - dca) * (sa - sca) + sca * d1a + dca * s1a
401    }
402}
403
404#[inline]
405fn blend_overlay(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
406    let s = PremulRgba::get(r, g, b, a, cover);
407    if s.a > 0.0 {
408        let mut d = PremulRgba::get_pix(p, 255);
409        let d1a = 1.0 - d.a;
410        let s1a = 1.0 - s.a;
411        let sada = s.a * d.a;
412        d.r = overlay_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
413        d.g = overlay_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
414        d.b = overlay_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
415        d.a += s.a - s.a * d.a;
416        PremulRgba::clip(&mut d);
417        PremulRgba::set(p, &d);
418    }
419}
420
421// ---- Darken: min(Sca.Da, Dca.Sa) + Sca.(1 - Da) + Dca.(1 - Sa)
422#[inline]
423fn blend_darken(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
424    let s = PremulRgba::get(r, g, b, a, cover);
425    if s.a > 0.0 {
426        let mut d = PremulRgba::get_pix(p, 255);
427        let d1a = 1.0 - d.a;
428        let s1a = 1.0 - s.a;
429        d.r = (s.r * d.a).min(d.r * s.a) + s.r * d1a + d.r * s1a;
430        d.g = (s.g * d.a).min(d.g * s.a) + s.g * d1a + d.g * s1a;
431        d.b = (s.b * d.a).min(d.b * s.a) + s.b * d1a + d.b * s1a;
432        d.a += s.a - s.a * d.a;
433        PremulRgba::clip(&mut d);
434        PremulRgba::set(p, &d);
435    }
436}
437
438// ---- Lighten: max(Sca.Da, Dca.Sa) + Sca.(1 - Da) + Dca.(1 - Sa)
439#[inline]
440fn blend_lighten(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
441    let s = PremulRgba::get(r, g, b, a, cover);
442    if s.a > 0.0 {
443        let mut d = PremulRgba::get_pix(p, 255);
444        let d1a = 1.0 - d.a;
445        let s1a = 1.0 - s.a;
446        d.r = (s.r * d.a).max(d.r * s.a) + s.r * d1a + d.r * s1a;
447        d.g = (s.g * d.a).max(d.g * s.a) + s.g * d1a + d.g * s1a;
448        d.b = (s.b * d.a).max(d.b * s.a) + s.b * d1a + d.b * s1a;
449        d.a += s.a - s.a * d.a;
450        PremulRgba::clip(&mut d);
451        PremulRgba::set(p, &d);
452    }
453}
454
455// ---- ColorDodge
456#[inline]
457fn color_dodge_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
458    if sca < sa {
459        sada * (1.0f64).min((dca / da) * sa / (sa - sca)) + sca * d1a + dca * s1a
460    } else if dca > 0.0 {
461        sada + sca * d1a + dca * s1a
462    } else {
463        sca * d1a
464    }
465}
466
467#[inline]
468fn blend_color_dodge(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
469    let s = PremulRgba::get(r, g, b, a, cover);
470    if s.a > 0.0 {
471        let mut d = PremulRgba::get_pix(p, 255);
472        if d.a > 0.0 {
473            let sada = s.a * d.a;
474            let s1a = 1.0 - s.a;
475            let d1a = 1.0 - d.a;
476            d.r = color_dodge_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
477            d.g = color_dodge_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
478            d.b = color_dodge_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
479            d.a += s.a - s.a * d.a;
480            PremulRgba::clip(&mut d);
481            PremulRgba::set(p, &d);
482        } else {
483            PremulRgba::set(p, &s);
484        }
485    }
486}
487
488// ---- ColorBurn
489#[inline]
490fn color_burn_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
491    if sca > 0.0 {
492        sada * (1.0 - (1.0f64).min((1.0 - dca / da) * sa / sca)) + sca * d1a + dca * s1a
493    } else if dca > da {
494        sada + dca * s1a
495    } else {
496        dca * s1a
497    }
498}
499
500#[inline]
501fn blend_color_burn(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
502    let s = PremulRgba::get(r, g, b, a, cover);
503    if s.a > 0.0 {
504        let mut d = PremulRgba::get_pix(p, 255);
505        if d.a > 0.0 {
506            let sada = s.a * d.a;
507            let s1a = 1.0 - s.a;
508            let d1a = 1.0 - d.a;
509            d.r = color_burn_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
510            d.g = color_burn_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
511            d.b = color_burn_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
512            d.a += s.a - sada;
513            PremulRgba::clip(&mut d);
514            PremulRgba::set(p, &d);
515        } else {
516            PremulRgba::set(p, &s);
517        }
518    }
519}
520
521// ---- HardLight
522#[inline]
523fn hard_light_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
524    if 2.0 * sca < sa {
525        2.0 * sca * dca + sca * d1a + dca * s1a
526    } else {
527        sada - 2.0 * (da - dca) * (sa - sca) + sca * d1a + dca * s1a
528    }
529}
530
531#[inline]
532fn blend_hard_light(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
533    let s = PremulRgba::get(r, g, b, a, cover);
534    if s.a > 0.0 {
535        let mut d = PremulRgba::get_pix(p, 255);
536        let d1a = 1.0 - d.a;
537        let s1a = 1.0 - s.a;
538        let sada = s.a * d.a;
539        d.r = hard_light_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
540        d.g = hard_light_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
541        d.b = hard_light_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
542        d.a += s.a - sada;
543        PremulRgba::clip(&mut d);
544        PremulRgba::set(p, &d);
545    }
546}
547
548// ---- SoftLight
549#[inline]
550fn soft_light_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
551    let dcasa = dca * sa;
552    if 2.0 * sca <= sa {
553        dcasa - (sada - 2.0 * sca * da) * dcasa * (sada - dcasa) + sca * d1a + dca * s1a
554    } else if 4.0 * dca <= da {
555        dcasa
556            + (2.0 * sca * da - sada)
557                * ((((16.0 * dcasa - 12.0) * dcasa + 4.0) * dca * da) - dca * da)
558            + sca * d1a
559            + dca * s1a
560    } else {
561        dcasa + (2.0 * sca * da - sada) * (dcasa.sqrt() - dcasa) + sca * d1a + dca * s1a
562    }
563}
564
565#[inline]
566fn blend_soft_light(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
567    let s = PremulRgba::get(r, g, b, a, cover);
568    if s.a > 0.0 {
569        let mut d = PremulRgba::get_pix(p, 255);
570        if d.a > 0.0 {
571            let sada = s.a * d.a;
572            let s1a = 1.0 - s.a;
573            let d1a = 1.0 - d.a;
574            d.r = soft_light_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
575            d.g = soft_light_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
576            d.b = soft_light_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
577            d.a += s.a - sada;
578            PremulRgba::clip(&mut d);
579            PremulRgba::set(p, &d);
580        } else {
581            PremulRgba::set(p, &s);
582        }
583    }
584}
585
586// ---- Difference: Dca' = Sca + Dca - 2.min(Sca.Da, Dca.Sa)
587#[inline]
588fn blend_difference(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
589    let s = PremulRgba::get(r, g, b, a, cover);
590    if s.a > 0.0 {
591        let mut d = PremulRgba::get_pix(p, 255);
592        d.r += s.r - 2.0 * (s.r * d.a).min(d.r * s.a);
593        d.g += s.g - 2.0 * (s.g * d.a).min(d.g * s.a);
594        d.b += s.b - 2.0 * (s.b * d.a).min(d.b * s.a);
595        d.a += s.a - s.a * d.a;
596        PremulRgba::clip(&mut d);
597        PremulRgba::set(p, &d);
598    }
599}
600
601// ---- Exclusion: (Sca.Da + Dca.Sa - 2.Sca.Dca) + Sca.(1 - Da) + Dca.(1 - Sa)
602#[inline]
603fn blend_exclusion(p: &mut [u8], r: u8, g: u8, b: u8, a: u8, cover: u8) {
604    let s = PremulRgba::get(r, g, b, a, cover);
605    if s.a > 0.0 {
606        let mut d = PremulRgba::get_pix(p, 255);
607        let d1a = 1.0 - d.a;
608        let s1a = 1.0 - s.a;
609        d.r = (s.r * d.a + d.r * s.a - 2.0 * s.r * d.r) + s.r * d1a + d.r * s1a;
610        d.g = (s.g * d.a + d.g * s.a - 2.0 * s.g * d.g) + s.g * d1a + d.g * s1a;
611        d.b = (s.b * d.a + d.b * s.a - 2.0 * s.b * d.b) + s.b * d1a + d.b * s1a;
612        d.a += s.a - s.a * d.a;
613        PremulRgba::clip(&mut d);
614        PremulRgba::set(p, &d);
615    }
616}
617
618// ============================================================================
619// PixfmtRgba32CompOp — pixel format with runtime-selectable compositing
620// ============================================================================
621
622const BPP: usize = 4;
623
624/// RGBA32 pixel format with runtime-selectable SVG compositing operations.
625///
626/// Port of C++ `pixfmt_custom_blend_rgba<comp_op_adaptor_rgba<rgba8, order_rgba>, rendering_buffer>`.
627/// Wraps a `RowAccessor` and stores the current compositing operation.
628/// All blending is dispatched through `comp_op_blend()`.
629pub struct PixfmtRgba32CompOp<'a> {
630    rbuf: &'a mut RowAccessor,
631    comp_op: CompOp,
632}
633
634impl<'a> PixfmtRgba32CompOp<'a> {
635    pub fn new(rbuf: &'a mut RowAccessor) -> Self {
636        Self {
637            rbuf,
638            comp_op: CompOp::SrcOver,
639        }
640    }
641
642    pub fn new_with_op(rbuf: &'a mut RowAccessor, op: CompOp) -> Self {
643        Self { rbuf, comp_op: op }
644    }
645
646    pub fn comp_op(&self) -> CompOp {
647        self.comp_op
648    }
649
650    pub fn set_comp_op(&mut self, op: CompOp) {
651        self.comp_op = op;
652    }
653
654    /// Clear the entire buffer to a solid color.
655    pub fn clear(&mut self, c: &Rgba8) {
656        let w = self.rbuf.width();
657        let h = self.rbuf.height();
658        for y in 0..h {
659            let row = unsafe {
660                let ptr = self.rbuf.row_ptr(y as i32);
661                std::slice::from_raw_parts_mut(ptr, (w as usize) * BPP)
662            };
663            for x in 0..w as usize {
664                let off = x * BPP;
665                row[off] = c.r;
666                row[off + 1] = c.g;
667                row[off + 2] = c.b;
668                row[off + 3] = c.a;
669            }
670        }
671    }
672}
673
674impl<'a> PixelFormat for PixfmtRgba32CompOp<'a> {
675    type ColorType = Rgba8;
676
677    fn width(&self) -> u32 {
678        self.rbuf.width()
679    }
680
681    fn height(&self) -> u32 {
682        self.rbuf.height()
683    }
684
685    fn pixel(&self, x: i32, y: i32) -> Rgba8 {
686        let row = unsafe {
687            let ptr = self.rbuf.row_ptr(y);
688            std::slice::from_raw_parts(ptr, (self.rbuf.width() as usize) * BPP)
689        };
690        let off = x as usize * BPP;
691        Rgba8::new(
692            row[off] as u32,
693            row[off + 1] as u32,
694            row[off + 2] as u32,
695            row[off + 3] as u32,
696        )
697    }
698
699    fn copy_pixel(&mut self, x: i32, y: i32, c: &Rgba8) {
700        let row = unsafe {
701            let ptr = self.rbuf.row_ptr(y);
702            std::slice::from_raw_parts_mut(ptr, (self.rbuf.width() as usize) * BPP)
703        };
704        let off = x as usize * BPP;
705        row[off] = c.r;
706        row[off + 1] = c.g;
707        row[off + 2] = c.b;
708        row[off + 3] = c.a;
709    }
710
711    fn copy_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8) {
712        let row = unsafe {
713            let ptr = self.rbuf.row_ptr(y);
714            std::slice::from_raw_parts_mut(ptr, (self.rbuf.width() as usize) * BPP)
715        };
716        for i in 0..len as usize {
717            let off = (x as usize + i) * BPP;
718            row[off] = c.r;
719            row[off + 1] = c.g;
720            row[off + 2] = c.b;
721            row[off + 3] = c.a;
722        }
723    }
724
725    fn blend_pixel(&mut self, x: i32, y: i32, c: &Rgba8, cover: CoverType) {
726        let row = unsafe {
727            let ptr = self.rbuf.row_ptr(y);
728            std::slice::from_raw_parts_mut(ptr, (self.rbuf.width() as usize) * BPP)
729        };
730        let off = x as usize * BPP;
731        comp_op_blend(self.comp_op, &mut row[off..off + BPP], c.r, c.g, c.b, c.a, cover);
732    }
733
734    fn blend_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8, cover: CoverType) {
735        let row = unsafe {
736            let ptr = self.rbuf.row_ptr(y);
737            std::slice::from_raw_parts_mut(ptr, (self.rbuf.width() as usize) * BPP)
738        };
739        for i in 0..len as usize {
740            let off = (x as usize + i) * BPP;
741            comp_op_blend(self.comp_op, &mut row[off..off + BPP], c.r, c.g, c.b, c.a, cover);
742        }
743    }
744
745    fn blend_solid_hspan(&mut self, x: i32, y: i32, len: u32, c: &Rgba8, covers: &[CoverType]) {
746        let row = unsafe {
747            let ptr = self.rbuf.row_ptr(y);
748            std::slice::from_raw_parts_mut(ptr, (self.rbuf.width() as usize) * BPP)
749        };
750        for (i, &cov) in covers.iter().enumerate().take(len as usize) {
751            let off = (x as usize + i) * BPP;
752            comp_op_blend(self.comp_op, &mut row[off..off + BPP], c.r, c.g, c.b, c.a, cov);
753        }
754    }
755
756    fn blend_color_hspan(
757        &mut self,
758        x: i32,
759        y: i32,
760        len: u32,
761        colors: &[Rgba8],
762        covers: &[CoverType],
763        cover: CoverType,
764    ) {
765        let row = unsafe {
766            let ptr = self.rbuf.row_ptr(y);
767            std::slice::from_raw_parts_mut(ptr, (self.rbuf.width() as usize) * BPP)
768        };
769        for i in 0..len as usize {
770            let c = &colors[i];
771            let cov = if !covers.is_empty() { covers[i] } else { cover };
772            let off = (x as usize + i) * BPP;
773            comp_op_blend(self.comp_op, &mut row[off..off + BPP], c.r, c.g, c.b, c.a, cov);
774        }
775    }
776}
777
778// ============================================================================
779// Tests
780// ============================================================================
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use crate::rendering_buffer::RowAccessor;
786
787    fn make_buffer(w: u32, h: u32) -> (Vec<u8>, RowAccessor) {
788        let stride = (w * 4) as i32;
789        let buf = vec![0u8; (h * w * 4) as usize];
790        let mut ra = RowAccessor::new();
791        unsafe {
792            ra.attach(buf.as_ptr() as *mut u8, w, h, stride);
793        }
794        (buf, ra)
795    }
796
797    fn get_pixel(buf: &[u8], w: u32, x: u32, y: u32) -> (u8, u8, u8, u8) {
798        let off = (y * w * 4 + x * 4) as usize;
799        (buf[off], buf[off + 1], buf[off + 2], buf[off + 3])
800    }
801
802    #[test]
803    fn test_comp_op_default() {
804        assert_eq!(CompOp::default(), CompOp::SrcOver);
805    }
806
807    #[test]
808    fn test_clear() {
809        let (_buf, mut ra) = make_buffer(10, 10);
810        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
811        // Set a pixel first
812        pf.copy_pixel(5, 5, &Rgba8::new(255, 0, 0, 255));
813        let p = pf.pixel(5, 5);
814        assert_eq!(p.r, 255);
815
816        // Clear it with comp_op clear
817        pf.set_comp_op(CompOp::Clear);
818        pf.blend_pixel(5, 5, &Rgba8::new(0, 0, 0, 255), 255);
819        let p = pf.pixel(5, 5);
820        assert_eq!(p.r, 0);
821        assert_eq!(p.a, 0);
822    }
823
824    #[test]
825    fn test_src() {
826        let (_buf, mut ra) = make_buffer(10, 10);
827        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
828        pf.copy_pixel(0, 0, &Rgba8::new(100, 100, 100, 255));
829
830        pf.set_comp_op(CompOp::Src);
831        pf.blend_pixel(0, 0, &Rgba8::new(200, 50, 0, 255), 255);
832        let p = pf.pixel(0, 0);
833        // With full cover and full alpha, Src just overwrites
834        assert_eq!(p.r, 200);
835        assert_eq!(p.g, 50);
836        assert_eq!(p.b, 0);
837        assert_eq!(p.a, 255);
838    }
839
840    #[test]
841    fn test_dst_is_noop() {
842        let (_buf, mut ra) = make_buffer(10, 10);
843        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
844        pf.copy_pixel(0, 0, &Rgba8::new(42, 43, 44, 200));
845
846        pf.set_comp_op(CompOp::Dst);
847        pf.blend_pixel(0, 0, &Rgba8::new(255, 255, 255, 255), 255);
848        let p = pf.pixel(0, 0);
849        assert_eq!(p.r, 42);
850        assert_eq!(p.g, 43);
851        assert_eq!(p.b, 44);
852        assert_eq!(p.a, 200);
853    }
854
855    #[test]
856    fn test_src_over_opaque() {
857        let (_buf, mut ra) = make_buffer(10, 10);
858        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
859        pf.copy_pixel(0, 0, &Rgba8::new(100, 100, 100, 255));
860
861        pf.set_comp_op(CompOp::SrcOver);
862        pf.blend_pixel(0, 0, &Rgba8::new(200, 50, 25, 255), 255);
863        let p = pf.pixel(0, 0);
864        // SrcOver with full alpha = complete replacement
865        assert_eq!(p.r, 200);
866        assert_eq!(p.g, 50);
867        assert_eq!(p.b, 25);
868        assert_eq!(p.a, 255);
869    }
870
871    #[test]
872    fn test_src_over_semitransparent() {
873        let (_buf, mut ra) = make_buffer(10, 10);
874        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
875        pf.copy_pixel(0, 0, &Rgba8::new(0, 0, 0, 255));
876
877        pf.set_comp_op(CompOp::SrcOver);
878        // Blend 50% transparent red over black
879        pf.blend_pixel(0, 0, &Rgba8::new(255, 0, 0, 128), 255);
880        let p = pf.pixel(0, 0);
881        // Should get roughly half red
882        assert!(p.r > 100 && p.r < 140, "r={}", p.r);
883        assert_eq!(p.a, 255);
884    }
885
886    #[test]
887    fn test_multiply_mode() {
888        let (_buf, mut ra) = make_buffer(10, 10);
889        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
890        // White background
891        pf.copy_pixel(0, 0, &Rgba8::new(255, 255, 255, 255));
892
893        pf.set_comp_op(CompOp::Multiply);
894        pf.blend_pixel(0, 0, &Rgba8::new(128, 128, 128, 255), 255);
895        let p = pf.pixel(0, 0);
896        // Multiply of white × 50% gray ≈ 128
897        assert!(p.r > 120 && p.r < 140, "r={}", p.r);
898    }
899
900    #[test]
901    fn test_screen_mode() {
902        let (_buf, mut ra) = make_buffer(10, 10);
903        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
904        // Black background
905        pf.copy_pixel(0, 0, &Rgba8::new(0, 0, 0, 255));
906
907        pf.set_comp_op(CompOp::Screen);
908        pf.blend_pixel(0, 0, &Rgba8::new(128, 128, 128, 255), 255);
909        let p = pf.pixel(0, 0);
910        // Screen of black + gray = gray
911        assert!(p.r > 120 && p.r < 140, "r={}", p.r);
912    }
913
914    #[test]
915    fn test_xor_mode() {
916        let (_buf, mut ra) = make_buffer(10, 10);
917        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
918        // Fully opaque red
919        pf.copy_pixel(0, 0, &Rgba8::new(255, 0, 0, 255));
920
921        pf.set_comp_op(CompOp::Xor);
922        // Xor with fully opaque blue → both fully opaque → everything cancels
923        pf.blend_pixel(0, 0, &Rgba8::new(0, 0, 255, 255), 255);
924        let p = pf.pixel(0, 0);
925        // Xor of two fully opaque: Da'=Sa+Da-2*Sa*Da = 1+1-2 = 0
926        assert_eq!(p.a, 0);
927    }
928
929    #[test]
930    fn test_blend_hline() {
931        let (_buf, mut ra) = make_buffer(20, 1);
932        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
933        pf.set_comp_op(CompOp::SrcOver);
934        let red = Rgba8::new(255, 0, 0, 255);
935        pf.blend_hline(5, 0, 10, &red, 255);
936        // Pixel at x=10 should be red
937        let p = pf.pixel(10, 0);
938        assert_eq!(p.r, 255);
939        // Pixel at x=0 should be transparent
940        let p = pf.pixel(0, 0);
941        assert_eq!(p.a, 0);
942    }
943
944    #[test]
945    fn test_blend_solid_hspan() {
946        let (_buf, mut ra) = make_buffer(10, 1);
947        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
948        pf.set_comp_op(CompOp::SrcOver);
949        let green = Rgba8::new(0, 255, 0, 255);
950        let covers = [255u8, 128, 64, 0];
951        pf.blend_solid_hspan(0, 0, 4, &green, &covers);
952        // Full cover pixel
953        let p = pf.pixel(0, 0);
954        assert_eq!(p.g, 255);
955        // Zero cover pixel — unchanged
956        let p = pf.pixel(3, 0);
957        assert_eq!(p.g, 0);
958    }
959
960    #[test]
961    fn test_all_ops_no_panic() {
962        let ops = [
963            CompOp::Clear,
964            CompOp::Src,
965            CompOp::Dst,
966            CompOp::SrcOver,
967            CompOp::DstOver,
968            CompOp::SrcIn,
969            CompOp::DstIn,
970            CompOp::SrcOut,
971            CompOp::DstOut,
972            CompOp::SrcAtop,
973            CompOp::DstAtop,
974            CompOp::Xor,
975            CompOp::Plus,
976            CompOp::Minus,
977            CompOp::Multiply,
978            CompOp::Screen,
979            CompOp::Overlay,
980            CompOp::Darken,
981            CompOp::Lighten,
982            CompOp::ColorDodge,
983            CompOp::ColorBurn,
984            CompOp::HardLight,
985            CompOp::SoftLight,
986            CompOp::Difference,
987            CompOp::Exclusion,
988        ];
989        let (_buf, mut ra) = make_buffer(10, 10);
990        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
991        let c = Rgba8::new(128, 64, 32, 200);
992        // Set some background
993        pf.copy_pixel(0, 0, &Rgba8::new(100, 150, 200, 180));
994        for &op in &ops {
995            pf.set_comp_op(op);
996            pf.blend_pixel(0, 0, &c, 128);
997        }
998    }
999
1000    #[test]
1001    fn test_difference_mode() {
1002        let (_buf, mut ra) = make_buffer(10, 10);
1003        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1004        // White opaque background
1005        pf.copy_pixel(0, 0, &Rgba8::new(255, 255, 255, 255));
1006
1007        pf.set_comp_op(CompOp::Difference);
1008        pf.blend_pixel(0, 0, &Rgba8::new(255, 255, 255, 255), 255);
1009        let p = pf.pixel(0, 0);
1010        // Difference of same colors = black
1011        assert!(p.r < 5, "r={}", p.r);
1012        assert!(p.g < 5, "g={}", p.g);
1013        assert!(p.b < 5, "b={}", p.b);
1014    }
1015
1016    #[test]
1017    fn test_plus_saturates() {
1018        let (_buf, mut ra) = make_buffer(10, 10);
1019        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1020        pf.copy_pixel(0, 0, &Rgba8::new(200, 200, 200, 255));
1021
1022        pf.set_comp_op(CompOp::Plus);
1023        pf.blend_pixel(0, 0, &Rgba8::new(200, 200, 200, 255), 255);
1024        let p = pf.pixel(0, 0);
1025        // Should saturate at 255
1026        assert_eq!(p.r, 255);
1027        assert_eq!(p.g, 255);
1028        assert_eq!(p.b, 255);
1029    }
1030}