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    ///
73    /// `#[inline(always)]`: this sits at the innermost point of every span blend
74    /// loop. Forcing it (and the sibling get/set helpers) to inline lets the
75    /// compiler keep the four f64 channels in XMM registers across the whole blend
76    /// and coalesce the byte loads/stores — the codegen shape MSVC gets on the C++
77    /// raw-pointer walk.
78    #[inline(always)]
79    fn get(r: u8, g: u8, b: u8, a: u8, cover: u8) -> Self {
80        if cover == 0 {
81            return Self {
82                r: 0.0,
83                g: 0.0,
84                b: 0.0,
85                a: 0.0,
86            };
87        }
88        let mut c = Self {
89            r: Rgba8::to_double(r),
90            g: Rgba8::to_double(g),
91            b: Rgba8::to_double(b),
92            a: Rgba8::to_double(a),
93        };
94        if cover < 255 {
95            let x = cover as f64 / 255.0;
96            c.r *= x;
97            c.g *= x;
98            c.b *= x;
99            c.a *= x;
100        }
101        c
102    }
103
104    /// Read from a 4-byte pixel (RGBA order) as premultiplied f64.
105    ///
106    /// Taking `&[u8; 4]` (rather than `&[u8]`) makes all four indices provably in
107    /// bounds, so the reads compile to one 32-bit load instead of four
108    /// bounds-checked byte loads.
109    #[inline(always)]
110    fn get_pix(p: &[u8; 4], cover: u8) -> Self {
111        Self::get(p[0], p[1], p[2], p[3], cover)
112    }
113
114    /// Write premultiplied f64 back to a 4-byte pixel.
115    #[inline(always)]
116    fn set(p: &mut [u8; 4], c: &PremulRgba) {
117        p[0] = Rgba8::from_double(c.r);
118        p[1] = Rgba8::from_double(c.g);
119        p[2] = Rgba8::from_double(c.b);
120        p[3] = Rgba8::from_double(c.a);
121    }
122
123    /// Write direct RGBA values.
124    #[inline(always)]
125    fn set_rgba(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8) {
126        p[0] = r;
127        p[1] = g;
128        p[2] = b;
129        p[3] = a;
130    }
131
132    /// Clamp `a` to [0, 1], then clamp each of `r`, `g`, `b` to [0, a].
133    ///
134    /// Matches C++ `clip` at agg_pixfmt_rgba.h:37-44 exactly: alpha is clamped
135    /// first, and the upper bound for the color channels is the (already
136    /// clamped) alpha — not 1.0. This differs for destinations that are not
137    /// premultiply-valid (a color channel exceeding alpha).
138    #[inline]
139    fn clip(c: &mut PremulRgba) {
140        c.a = c.a.clamp(0.0, 1.0);
141        c.r = c.r.clamp(0.0, c.a);
142        c.g = c.g.clamp(0.0, c.a);
143        c.b = c.b.clamp(0.0, c.a);
144    }
145}
146
147// ============================================================================
148// Per-operation blend functions
149// ============================================================================
150
151/// Blend pixel `p` (RGBA u8) with premultiplied source (r,g,b,a) using `op`.
152///
153/// Source colors are NON-premultiplied; this function premultiplies them
154/// before dispatching (matching C++ `comp_op_adaptor_rgba`).
155#[inline]
156fn comp_op_blend(op: CompOp, p: &mut [u8; 4], sr: u8, sg: u8, sb: u8, sa: u8, cover: u8) {
157    // Premultiply source by alpha (matching comp_op_adaptor_rgba)
158    let r = Rgba8::multiply(sr, sa);
159    let g = Rgba8::multiply(sg, sa);
160    let b = Rgba8::multiply(sb, sa);
161    let a = sa;
162
163    match op {
164        CompOp::Clear => blend_clear(p, cover),
165        CompOp::Src => blend_src(p, r, g, b, a, cover),
166        CompOp::Dst => {} // no-op
167        CompOp::SrcOver => blend_src_over(p, r, g, b, a, cover),
168        CompOp::DstOver => blend_dst_over(p, r, g, b, a, cover),
169        CompOp::SrcIn => blend_src_in(p, r, g, b, a, cover),
170        CompOp::DstIn => blend_dst_in(p, a, cover),
171        CompOp::SrcOut => blend_src_out(p, r, g, b, a, cover),
172        CompOp::DstOut => blend_dst_out(p, a, cover),
173        CompOp::SrcAtop => blend_src_atop(p, r, g, b, a, cover),
174        CompOp::DstAtop => blend_dst_atop(p, r, g, b, a, cover),
175        CompOp::Xor => blend_xor(p, r, g, b, a, cover),
176        CompOp::Plus => blend_plus(p, r, g, b, a, cover),
177        CompOp::Minus => blend_minus(p, r, g, b, a, cover),
178        CompOp::Multiply => blend_multiply(p, r, g, b, a, cover),
179        CompOp::Screen => blend_screen(p, r, g, b, a, cover),
180        CompOp::Overlay => blend_overlay(p, r, g, b, a, cover),
181        CompOp::Darken => blend_darken(p, r, g, b, a, cover),
182        CompOp::Lighten => blend_lighten(p, r, g, b, a, cover),
183        CompOp::ColorDodge => blend_color_dodge(p, r, g, b, a, cover),
184        CompOp::ColorBurn => blend_color_burn(p, r, g, b, a, cover),
185        CompOp::HardLight => blend_hard_light(p, r, g, b, a, cover),
186        CompOp::SoftLight => blend_soft_light(p, r, g, b, a, cover),
187        CompOp::Difference => blend_difference(p, r, g, b, a, cover),
188        CompOp::Exclusion => blend_exclusion(p, r, g, b, a, cover),
189    }
190}
191
192// ---- Clear: Dca' = 0, Da' = 0
193#[inline(always)]
194fn blend_clear(p: &mut [u8; 4], cover: u8) {
195    if cover >= 255 {
196        p[0] = 0;
197        p[1] = 0;
198        p[2] = 0;
199        p[3] = 0;
200    } else if cover > 0 {
201        let d = PremulRgba::get_pix(p, 255 - cover);
202        PremulRgba::set(p, &d);
203    }
204}
205
206// ---- Src: Dca' = Sca, Da' = Sa
207#[inline(always)]
208fn blend_src(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
209    if cover >= 255 {
210        PremulRgba::set_rgba(p, r, g, b, a);
211    } else {
212        let s = PremulRgba::get(r, g, b, a, cover);
213        let d = PremulRgba::get_pix(p, 255 - cover);
214        let out = PremulRgba {
215            r: d.r + s.r,
216            g: d.g + s.g,
217            b: d.b + s.b,
218            a: d.a + s.a,
219        };
220        PremulRgba::set(p, &out);
221    }
222}
223
224// ---- SrcOver: Dca' = Sca + Dca.(1 - Sa), Da' = Sa + Da - Sa.Da
225//
226// C++ `comp_op_rgba_src_over` delegates to `blender_rgba_pre::blend_pix`, which
227// works entirely in integer/fixed-point (`mult_cover` + `prelerp`), NOT the
228// floating-point `rgba` get/set path used by the separable blend modes
229// (multiply, difference, ...). Doing this in `f64` agrees with C++ when the
230// destination is opaque, but rounds differently by up to 1 code when the
231// destination has partial alpha — which is exactly the case where a src-over
232// shape overlaps an already-composited (difference) shape. Match the fixed-point
233// path to stay byte-identical. The incoming (r, g, b, a) are already the
234// premultiplied source from `comp_op_blend`.
235#[inline(always)]
236fn blend_src_over(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
237    let cr = Rgba8::mult_cover(r, cover);
238    let cg = Rgba8::mult_cover(g, cover);
239    let cb = Rgba8::mult_cover(b, cover);
240    let ca = Rgba8::mult_cover(a, cover);
241    // Load the 4 destination bytes once and store once. Each prelerp still reads
242    // the ORIGINAL channel value (writes never feed a later read here), so this is
243    // bit-identical to the per-channel read-modify-write while giving the compiler
244    // a single 32-bit load and store to schedule.
245    let d = *p;
246    *p = [
247        Rgba8::prelerp(d[0], cr, ca),
248        Rgba8::prelerp(d[1], cg, ca),
249        Rgba8::prelerp(d[2], cb, ca),
250        Rgba8::prelerp(d[3], ca, ca),
251    ];
252}
253
254// ---- DstOver: Dca' = Dca + Sca.(1 - Da)
255#[inline(always)]
256fn blend_dst_over(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
257    let s = PremulRgba::get(r, g, b, a, cover);
258    let mut d = PremulRgba::get_pix(p, 255);
259    let d1a = 1.0 - d.a;
260    d.r += s.r * d1a;
261    d.g += s.g * d1a;
262    d.b += s.b * d1a;
263    d.a += s.a * d1a;
264    PremulRgba::set(p, &d);
265}
266
267// ---- SrcIn: Dca' = Sca.Da
268#[inline(always)]
269fn blend_src_in(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
270    let da = Rgba8::to_double(p[3]);
271    if da > 0.0 {
272        let s = PremulRgba::get(r, g, b, a, cover);
273        let mut d = PremulRgba::get_pix(p, 255 - cover);
274        d.r += s.r * da;
275        d.g += s.g * da;
276        d.b += s.b * da;
277        d.a += s.a * da;
278        PremulRgba::set(p, &d);
279    }
280}
281
282// ---- DstIn: Dca' = Dca.Sa
283#[inline(always)]
284fn blend_dst_in(p: &mut [u8; 4], a: u8, cover: u8) {
285    let sa = Rgba8::to_double(a);
286    let mut d = PremulRgba::get_pix(p, 255 - cover);
287    let d2 = PremulRgba::get_pix(p, cover);
288    d.r += d2.r * sa;
289    d.g += d2.g * sa;
290    d.b += d2.b * sa;
291    d.a += d2.a * sa;
292    PremulRgba::set(p, &d);
293}
294
295// ---- SrcOut: Dca' = Sca.(1 - Da)
296#[inline(always)]
297fn blend_src_out(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
298    let s = PremulRgba::get(r, g, b, a, cover);
299    let mut d = PremulRgba::get_pix(p, 255 - cover);
300    let d1a = 1.0 - Rgba8::to_double(p[3]);
301    d.r += s.r * d1a;
302    d.g += s.g * d1a;
303    d.b += s.b * d1a;
304    d.a += s.a * d1a;
305    PremulRgba::set(p, &d);
306}
307
308// ---- DstOut: Dca' = Dca.(1 - Sa)
309#[inline(always)]
310fn blend_dst_out(p: &mut [u8; 4], a: u8, cover: u8) {
311    let mut d = PremulRgba::get_pix(p, 255 - cover);
312    let dc = PremulRgba::get_pix(p, cover);
313    let s1a = 1.0 - Rgba8::to_double(a);
314    d.r += dc.r * s1a;
315    d.g += dc.g * s1a;
316    d.b += dc.b * s1a;
317    d.a += dc.a * s1a;
318    PremulRgba::set(p, &d);
319}
320
321// ---- SrcAtop: Dca' = Sca.Da + Dca.(1 - Sa), Da' = Da
322#[inline(always)]
323fn blend_src_atop(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
324    let s = PremulRgba::get(r, g, b, a, cover);
325    let mut d = PremulRgba::get_pix(p, 255);
326    let s1a = 1.0 - s.a;
327    d.r = s.r * d.a + d.r * s1a;
328    d.g = s.g * d.a + d.g * s1a;
329    // Replicates the upstream AGG 2.6 typo at agg_pixfmt_rgba.h:530, where the
330    // blue channel is computed from `d.g` (green) instead of `d.b` (blue).
331    // C++ executes these three assignments sequentially, so by the time the
332    // blue line runs, `d.g` has already been overwritten by the previous
333    // statement — we must use that just-updated `d.g` here. The port
334    // guarantees byte-identity with the compiled C++ renderer, so the typo is
335    // preserved deliberately rather than "fixed".
336    d.b = s.b * d.a + d.g * s1a;
337    // Da' = Da (unchanged)
338    PremulRgba::set(p, &d);
339}
340
341// ---- DstAtop: Dca' = Dca.Sa + Sca.(1 - Da), Da' = Sa
342#[inline(always)]
343fn blend_dst_atop(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
344    let sc = PremulRgba::get(r, g, b, a, cover);
345    let dc = PremulRgba::get_pix(p, cover);
346    let mut d = PremulRgba::get_pix(p, 255 - cover);
347    let sa = Rgba8::to_double(a);
348    let d1a = 1.0 - Rgba8::to_double(p[3]);
349    d.r += dc.r * sa + sc.r * d1a;
350    d.g += dc.g * sa + sc.g * d1a;
351    d.b += dc.b * sa + sc.b * d1a;
352    d.a += sc.a;
353    PremulRgba::set(p, &d);
354}
355
356// ---- Xor: Dca' = Sca.(1 - Da) + Dca.(1 - Sa)
357#[inline(always)]
358fn blend_xor(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
359    let s = PremulRgba::get(r, g, b, a, cover);
360    let mut d = PremulRgba::get_pix(p, 255);
361    let s1a = 1.0 - s.a;
362    let d1a = 1.0 - Rgba8::to_double(p[3]);
363    d.r = s.r * d1a + d.r * s1a;
364    d.g = s.g * d1a + d.g * s1a;
365    d.b = s.b * d1a + d.b * s1a;
366    d.a = s.a + d.a - 2.0 * s.a * d.a;
367    PremulRgba::set(p, &d);
368}
369
370// ---- Plus: Dca' = Sca + Dca (clamped)
371#[inline(always)]
372fn blend_plus(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
373    let s = PremulRgba::get(r, g, b, a, cover);
374    if s.a > 0.0 {
375        let mut d = PremulRgba::get_pix(p, 255);
376        d.a = (d.a + s.a).min(1.0);
377        d.r = (d.r + s.r).min(d.a);
378        d.g = (d.g + s.g).min(d.a);
379        d.b = (d.b + s.b).min(d.a);
380        PremulRgba::clip(&mut d);
381        PremulRgba::set(p, &d);
382    }
383}
384
385// ---- Minus: Dca' = Dca - Sca (clamped)
386#[inline(always)]
387fn blend_minus(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
388    let s = PremulRgba::get(r, g, b, a, cover);
389    if s.a > 0.0 {
390        let mut d = PremulRgba::get_pix(p, 255);
391        d.a += s.a - s.a * d.a;
392        d.r = (d.r - s.r).max(0.0);
393        d.g = (d.g - s.g).max(0.0);
394        d.b = (d.b - s.b).max(0.0);
395        PremulRgba::clip(&mut d);
396        PremulRgba::set(p, &d);
397    }
398}
399
400// ---- Multiply: Dca' = Sca.Dca + Sca.(1 - Da) + Dca.(1 - Sa)
401#[inline(always)]
402fn blend_multiply(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
403    let s = PremulRgba::get(r, g, b, a, cover);
404    if s.a > 0.0 {
405        let mut d = PremulRgba::get_pix(p, 255);
406        let s1a = 1.0 - s.a;
407        let d1a = 1.0 - d.a;
408        d.r = s.r * d.r + s.r * d1a + d.r * s1a;
409        d.g = s.g * d.g + s.g * d1a + d.g * s1a;
410        d.b = s.b * d.b + s.b * d1a + d.b * s1a;
411        d.a += s.a - s.a * d.a;
412        PremulRgba::clip(&mut d);
413        PremulRgba::set(p, &d);
414    }
415}
416
417// ---- Screen: Dca' = Sca + Dca - Sca.Dca
418#[inline(always)]
419fn blend_screen(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
420    let s = PremulRgba::get(r, g, b, a, cover);
421    if s.a > 0.0 {
422        let mut d = PremulRgba::get_pix(p, 255);
423        d.r += s.r - s.r * d.r;
424        d.g += s.g - s.g * d.g;
425        d.b += s.b - s.b * d.b;
426        d.a += s.a - s.a * d.a;
427        PremulRgba::clip(&mut d);
428        PremulRgba::set(p, &d);
429    }
430}
431
432// ---- Overlay
433#[inline]
434fn overlay_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
435    if 2.0 * dca <= da {
436        2.0 * sca * dca + sca * d1a + dca * s1a
437    } else {
438        sada - 2.0 * (da - dca) * (sa - sca) + sca * d1a + dca * s1a
439    }
440}
441
442#[inline(always)]
443fn blend_overlay(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
444    let s = PremulRgba::get(r, g, b, a, cover);
445    if s.a > 0.0 {
446        let mut d = PremulRgba::get_pix(p, 255);
447        let d1a = 1.0 - d.a;
448        let s1a = 1.0 - s.a;
449        let sada = s.a * d.a;
450        d.r = overlay_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
451        d.g = overlay_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
452        d.b = overlay_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
453        d.a += s.a - s.a * d.a;
454        PremulRgba::clip(&mut d);
455        PremulRgba::set(p, &d);
456    }
457}
458
459// ---- Darken: min(Sca.Da, Dca.Sa) + Sca.(1 - Da) + Dca.(1 - Sa)
460#[inline(always)]
461fn blend_darken(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
462    let s = PremulRgba::get(r, g, b, a, cover);
463    if s.a > 0.0 {
464        let mut d = PremulRgba::get_pix(p, 255);
465        let d1a = 1.0 - d.a;
466        let s1a = 1.0 - s.a;
467        d.r = (s.r * d.a).min(d.r * s.a) + s.r * d1a + d.r * s1a;
468        d.g = (s.g * d.a).min(d.g * s.a) + s.g * d1a + d.g * s1a;
469        d.b = (s.b * d.a).min(d.b * s.a) + s.b * d1a + d.b * s1a;
470        d.a += s.a - s.a * d.a;
471        PremulRgba::clip(&mut d);
472        PremulRgba::set(p, &d);
473    }
474}
475
476// ---- Lighten: max(Sca.Da, Dca.Sa) + Sca.(1 - Da) + Dca.(1 - Sa)
477#[inline(always)]
478fn blend_lighten(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
479    let s = PremulRgba::get(r, g, b, a, cover);
480    if s.a > 0.0 {
481        let mut d = PremulRgba::get_pix(p, 255);
482        let d1a = 1.0 - d.a;
483        let s1a = 1.0 - s.a;
484        d.r = (s.r * d.a).max(d.r * s.a) + s.r * d1a + d.r * s1a;
485        d.g = (s.g * d.a).max(d.g * s.a) + s.g * d1a + d.g * s1a;
486        d.b = (s.b * d.a).max(d.b * s.a) + s.b * d1a + d.b * s1a;
487        d.a += s.a - s.a * d.a;
488        PremulRgba::clip(&mut d);
489        PremulRgba::set(p, &d);
490    }
491}
492
493// ---- ColorDodge
494#[inline]
495fn color_dodge_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
496    if sca < sa {
497        sada * (1.0f64).min((dca / da) * sa / (sa - sca)) + sca * d1a + dca * s1a
498    } else if dca > 0.0 {
499        sada + sca * d1a + dca * s1a
500    } else {
501        sca * d1a
502    }
503}
504
505#[inline(always)]
506fn blend_color_dodge(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
507    let s = PremulRgba::get(r, g, b, a, cover);
508    if s.a > 0.0 {
509        let mut d = PremulRgba::get_pix(p, 255);
510        if d.a > 0.0 {
511            let sada = s.a * d.a;
512            let s1a = 1.0 - s.a;
513            let d1a = 1.0 - d.a;
514            d.r = color_dodge_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
515            d.g = color_dodge_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
516            d.b = color_dodge_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
517            d.a += s.a - s.a * d.a;
518            PremulRgba::clip(&mut d);
519            PremulRgba::set(p, &d);
520        } else {
521            PremulRgba::set(p, &s);
522        }
523    }
524}
525
526// ---- ColorBurn
527#[inline]
528fn color_burn_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
529    if sca > 0.0 {
530        sada * (1.0 - (1.0f64).min((1.0 - dca / da) * sa / sca)) + sca * d1a + dca * s1a
531    } else if dca > da {
532        sada + dca * s1a
533    } else {
534        dca * s1a
535    }
536}
537
538#[inline(always)]
539fn blend_color_burn(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
540    let s = PremulRgba::get(r, g, b, a, cover);
541    if s.a > 0.0 {
542        let mut d = PremulRgba::get_pix(p, 255);
543        if d.a > 0.0 {
544            let sada = s.a * d.a;
545            let s1a = 1.0 - s.a;
546            let d1a = 1.0 - d.a;
547            d.r = color_burn_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
548            d.g = color_burn_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
549            d.b = color_burn_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
550            d.a += s.a - sada;
551            PremulRgba::clip(&mut d);
552            PremulRgba::set(p, &d);
553        } else {
554            PremulRgba::set(p, &s);
555        }
556    }
557}
558
559// ---- HardLight
560#[inline]
561fn hard_light_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
562    if 2.0 * sca < sa {
563        2.0 * sca * dca + sca * d1a + dca * s1a
564    } else {
565        sada - 2.0 * (da - dca) * (sa - sca) + sca * d1a + dca * s1a
566    }
567}
568
569#[inline(always)]
570fn blend_hard_light(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
571    let s = PremulRgba::get(r, g, b, a, cover);
572    if s.a > 0.0 {
573        let mut d = PremulRgba::get_pix(p, 255);
574        let d1a = 1.0 - d.a;
575        let s1a = 1.0 - s.a;
576        let sada = s.a * d.a;
577        d.r = hard_light_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
578        d.g = hard_light_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
579        d.b = hard_light_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
580        d.a += s.a - sada;
581        PremulRgba::clip(&mut d);
582        PremulRgba::set(p, &d);
583    }
584}
585
586// ---- SoftLight
587#[inline]
588fn soft_light_calc(dca: f64, sca: f64, da: f64, sa: f64, sada: f64, d1a: f64, s1a: f64) -> f64 {
589    let dcasa = dca * sa;
590    if 2.0 * sca <= sa {
591        dcasa - (sada - 2.0 * sca * da) * dcasa * (sada - dcasa) + sca * d1a + dca * s1a
592    } else if 4.0 * dca <= da {
593        dcasa
594            + (2.0 * sca * da - sada)
595                * ((((16.0 * dcasa - 12.0) * dcasa + 4.0) * dca * da) - dca * da)
596            + sca * d1a
597            + dca * s1a
598    } else {
599        dcasa + (2.0 * sca * da - sada) * (dcasa.sqrt() - dcasa) + sca * d1a + dca * s1a
600    }
601}
602
603#[inline(always)]
604fn blend_soft_light(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
605    let s = PremulRgba::get(r, g, b, a, cover);
606    if s.a > 0.0 {
607        let mut d = PremulRgba::get_pix(p, 255);
608        if d.a > 0.0 {
609            let sada = s.a * d.a;
610            let s1a = 1.0 - s.a;
611            let d1a = 1.0 - d.a;
612            d.r = soft_light_calc(d.r, s.r, d.a, s.a, sada, d1a, s1a);
613            d.g = soft_light_calc(d.g, s.g, d.a, s.a, sada, d1a, s1a);
614            d.b = soft_light_calc(d.b, s.b, d.a, s.a, sada, d1a, s1a);
615            d.a += s.a - sada;
616            PremulRgba::clip(&mut d);
617            PremulRgba::set(p, &d);
618        } else {
619            PremulRgba::set(p, &s);
620        }
621    }
622}
623
624// ---- Difference: Dca' = Sca + Dca - 2.min(Sca.Da, Dca.Sa)
625#[inline(always)]
626fn blend_difference(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
627    let s = PremulRgba::get(r, g, b, a, cover);
628    if s.a > 0.0 {
629        let mut d = PremulRgba::get_pix(p, 255);
630        d.r += s.r - 2.0 * (s.r * d.a).min(d.r * s.a);
631        d.g += s.g - 2.0 * (s.g * d.a).min(d.g * s.a);
632        d.b += s.b - 2.0 * (s.b * d.a).min(d.b * s.a);
633        d.a += s.a - s.a * d.a;
634        PremulRgba::clip(&mut d);
635        PremulRgba::set(p, &d);
636    }
637}
638
639// ---- Exclusion: (Sca.Da + Dca.Sa - 2.Sca.Dca) + Sca.(1 - Da) + Dca.(1 - Sa)
640#[inline(always)]
641fn blend_exclusion(p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8) {
642    let s = PremulRgba::get(r, g, b, a, cover);
643    if s.a > 0.0 {
644        let mut d = PremulRgba::get_pix(p, 255);
645        let d1a = 1.0 - d.a;
646        let s1a = 1.0 - s.a;
647        d.r = (s.r * d.a + d.r * s.a - 2.0 * s.r * d.r) + s.r * d1a + d.r * s1a;
648        d.g = (s.g * d.a + d.g * s.a - 2.0 * s.g * d.g) + s.g * d1a + d.g * s1a;
649        d.b = (s.b * d.a + d.b * s.a - 2.0 * s.b * d.b) + s.b * d1a + d.b * s1a;
650        d.a += s.a - s.a * d.a;
651        PremulRgba::clip(&mut d);
652        PremulRgba::set(p, &d);
653    }
654}
655
656// ============================================================================
657// Span-level compositing dispatch
658// ============================================================================
659
660/// Dispatch on the compositing op ONCE per span, binding `$blend` to a per-op
661/// closure so the span loop in `$body` is monomorphized for that single op.
662///
663/// This mirrors the C++ template structure
664/// (`pixfmt_custom_blend_rgba<comp_op_adaptor_rgba<...>>`), where the blend
665/// function is a compile-time type: the inner loop compiles to a tight, fully
666/// inlined sequence instead of re-dispatching a 25-way runtime `match` on every
667/// pixel. The per-pixel `comp_op_blend` path (used by `blend_pixel`) still exists
668/// unchanged for the single-pixel case.
669///
670/// `Dst` expands to nothing: it ignores every source value, so skipping both the
671/// premultiply and the entire loop cannot change output — the C++ counterpart
672/// `comp_op_rgba_dst::blend_pix` has an empty body.
673macro_rules! comp_op_span {
674    ($op:expr, |$blend:ident| $body:block) => {
675        match $op {
676            CompOp::Clear => {
677                let $blend = |p: &mut [u8; 4], _r: u8, _g: u8, _b: u8, _a: u8, cover: u8| blend_clear(p, cover);
678                $body
679            }
680            CompOp::Src => {
681                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_src(p, r, g, b, a, cover);
682                $body
683            }
684            CompOp::Dst => {}
685            CompOp::SrcOver => {
686                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_src_over(p, r, g, b, a, cover);
687                $body
688            }
689            CompOp::DstOver => {
690                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_dst_over(p, r, g, b, a, cover);
691                $body
692            }
693            CompOp::SrcIn => {
694                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_src_in(p, r, g, b, a, cover);
695                $body
696            }
697            CompOp::DstIn => {
698                let $blend = |p: &mut [u8; 4], _r: u8, _g: u8, _b: u8, a: u8, cover: u8| blend_dst_in(p, a, cover);
699                $body
700            }
701            CompOp::SrcOut => {
702                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_src_out(p, r, g, b, a, cover);
703                $body
704            }
705            CompOp::DstOut => {
706                let $blend = |p: &mut [u8; 4], _r: u8, _g: u8, _b: u8, a: u8, cover: u8| blend_dst_out(p, a, cover);
707                $body
708            }
709            CompOp::SrcAtop => {
710                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_src_atop(p, r, g, b, a, cover);
711                $body
712            }
713            CompOp::DstAtop => {
714                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_dst_atop(p, r, g, b, a, cover);
715                $body
716            }
717            CompOp::Xor => {
718                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_xor(p, r, g, b, a, cover);
719                $body
720            }
721            CompOp::Plus => {
722                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_plus(p, r, g, b, a, cover);
723                $body
724            }
725            CompOp::Minus => {
726                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_minus(p, r, g, b, a, cover);
727                $body
728            }
729            CompOp::Multiply => {
730                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_multiply(p, r, g, b, a, cover);
731                $body
732            }
733            CompOp::Screen => {
734                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_screen(p, r, g, b, a, cover);
735                $body
736            }
737            CompOp::Overlay => {
738                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_overlay(p, r, g, b, a, cover);
739                $body
740            }
741            CompOp::Darken => {
742                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_darken(p, r, g, b, a, cover);
743                $body
744            }
745            CompOp::Lighten => {
746                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_lighten(p, r, g, b, a, cover);
747                $body
748            }
749            CompOp::ColorDodge => {
750                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_color_dodge(p, r, g, b, a, cover);
751                $body
752            }
753            CompOp::ColorBurn => {
754                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_color_burn(p, r, g, b, a, cover);
755                $body
756            }
757            CompOp::HardLight => {
758                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_hard_light(p, r, g, b, a, cover);
759                $body
760            }
761            CompOp::SoftLight => {
762                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_soft_light(p, r, g, b, a, cover);
763                $body
764            }
765            CompOp::Difference => {
766                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_difference(p, r, g, b, a, cover);
767                $body
768            }
769            CompOp::Exclusion => {
770                let $blend = |p: &mut [u8; 4], r: u8, g: u8, b: u8, a: u8, cover: u8| blend_exclusion(p, r, g, b, a, cover);
771                $body
772            }
773        }
774    };
775}
776
777// ============================================================================
778// PixfmtRgba32CompOp — pixel format with runtime-selectable compositing
779// ============================================================================
780
781const BPP: usize = 4;
782
783/// RGBA32 pixel format with runtime-selectable SVG compositing operations.
784///
785/// Port of C++ `pixfmt_custom_blend_rgba<comp_op_adaptor_rgba<rgba8, order_rgba>, rendering_buffer>`.
786/// Wraps a `RowAccessor` and stores the current compositing operation.
787/// All blending is dispatched through `comp_op_blend()`.
788pub struct PixfmtRgba32CompOp<'a> {
789    rbuf: &'a mut RowAccessor,
790    comp_op: CompOp,
791}
792
793impl<'a> PixfmtRgba32CompOp<'a> {
794    pub fn new(rbuf: &'a mut RowAccessor) -> Self {
795        Self {
796            rbuf,
797            comp_op: CompOp::SrcOver,
798        }
799    }
800
801    pub fn new_with_op(rbuf: &'a mut RowAccessor, op: CompOp) -> Self {
802        Self { rbuf, comp_op: op }
803    }
804
805    pub fn comp_op(&self) -> CompOp {
806        self.comp_op
807    }
808
809    pub fn set_comp_op(&mut self, op: CompOp) {
810        self.comp_op = op;
811    }
812
813    /// Row of pixels at `y` as a byte slice (shared, read-only view).
814    #[inline]
815    fn row(&self, y: i32) -> &[u8] {
816        unsafe {
817            let ptr = self.rbuf.row_ptr(y);
818            std::slice::from_raw_parts(ptr, self.rbuf.width() as usize * BPP)
819        }
820    }
821
822    /// Row of pixels at `y` as a mutable byte slice.
823    #[inline]
824    fn row_mut(&mut self, y: i32) -> &mut [u8] {
825        unsafe {
826            let ptr = self.rbuf.row_ptr(y);
827            std::slice::from_raw_parts_mut(ptr, self.rbuf.width() as usize * BPP)
828        }
829    }
830
831    /// Clear the entire buffer to a solid color.
832    pub fn clear(&mut self, c: &Rgba8) {
833        let w = self.rbuf.width();
834        let h = self.rbuf.height();
835        for y in 0..h {
836            let row = self.row_mut(y as i32);
837            for x in 0..w as usize {
838                let off = x * BPP;
839                row[off] = c.r;
840                row[off + 1] = c.g;
841                row[off + 2] = c.b;
842                row[off + 3] = c.a;
843            }
844        }
845    }
846}
847
848impl<'a> PixelFormat for PixfmtRgba32CompOp<'a> {
849    type ColorType = Rgba8;
850
851    fn width(&self) -> u32 {
852        self.rbuf.width()
853    }
854
855    fn height(&self) -> u32 {
856        self.rbuf.height()
857    }
858
859    fn pixel(&self, x: i32, y: i32) -> Rgba8 {
860        let row = self.row(y);
861        let off = x as usize * BPP;
862        Rgba8::new(
863            row[off] as u32,
864            row[off + 1] as u32,
865            row[off + 2] as u32,
866            row[off + 3] as u32,
867        )
868    }
869
870    fn copy_pixel(&mut self, x: i32, y: i32, c: &Rgba8) {
871        let row = self.row_mut(y);
872        let off = x as usize * BPP;
873        row[off] = c.r;
874        row[off + 1] = c.g;
875        row[off + 2] = c.b;
876        row[off + 3] = c.a;
877    }
878
879    fn copy_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8) {
880        let row = self.row_mut(y);
881        for i in 0..len as usize {
882            let off = (x as usize + i) * BPP;
883            row[off] = c.r;
884            row[off + 1] = c.g;
885            row[off + 2] = c.b;
886            row[off + 3] = c.a;
887        }
888    }
889
890    fn blend_pixel(&mut self, x: i32, y: i32, c: &Rgba8, cover: CoverType) {
891        let comp_op = self.comp_op;
892        let row = self.row_mut(y);
893        let off = x as usize * BPP;
894        let px: &mut [u8; 4] = (&mut row[off..off + BPP]).try_into().unwrap();
895        comp_op_blend(comp_op, px, c.r, c.g, c.b, c.a, cover);
896    }
897
898    fn blend_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8, cover: CoverType) {
899        let comp_op = self.comp_op;
900        let row = self.row_mut(y);
901        // Slice the span once so the inner loop carries no per-pixel bounds checks
902        // or index arithmetic — the C++ path walks a raw pointer here.
903        let span = &mut row[x as usize * BPP..(x as usize + len as usize) * BPP];
904        comp_op_span!(comp_op, |blend| {
905            // Colour and cover are constant across the span, so the premultiply is
906            // loop-invariant (byte-identical to premultiplying per pixel).
907            let r = Rgba8::multiply(c.r, c.a);
908            let g = Rgba8::multiply(c.g, c.a);
909            let b = Rgba8::multiply(c.b, c.a);
910            let a = c.a;
911            // Iterate fixed [u8; 4] pixels so each blend sees a bounds-check-free
912            // 4-byte array, letting the load/compute/store stay in XMM registers.
913            let (pixels, _rem) = span.as_chunks_mut::<BPP>();
914            for px in pixels {
915                blend(px, r, g, b, a, cover);
916            }
917        });
918    }
919
920    fn blend_solid_hspan(&mut self, x: i32, y: i32, len: u32, c: &Rgba8, covers: &[CoverType]) {
921        let comp_op = self.comp_op;
922        let row = self.row_mut(y);
923        let span = &mut row[x as usize * BPP..(x as usize + len as usize) * BPP];
924        comp_op_span!(comp_op, |blend| {
925            // Solid span: constant colour, so premultiply once outside the loop.
926            let r = Rgba8::multiply(c.r, c.a);
927            let g = Rgba8::multiply(c.g, c.a);
928            let b = Rgba8::multiply(c.b, c.a);
929            let a = c.a;
930            let (pixels, _rem) = span.as_chunks_mut::<BPP>();
931            for (px, &cov) in pixels.iter_mut().zip(covers.iter()) {
932                blend(px, r, g, b, a, cov);
933            }
934        });
935    }
936
937    fn blend_color_hspan(
938        &mut self,
939        x: i32,
940        y: i32,
941        len: u32,
942        colors: &[Rgba8],
943        covers: &[CoverType],
944        cover: CoverType,
945    ) {
946        let comp_op = self.comp_op;
947        let row = self.row_mut(y);
948        let span = &mut row[x as usize * BPP..(x as usize + len as usize) * BPP];
949        comp_op_span!(comp_op, |blend| {
950            // Colour differs per pixel here, so the premultiply must stay per pixel
951            // (matching comp_op_adaptor_rgba). The single outer op-dispatch and the
952            // pre-sliced span are what remove the overhead.
953            let (pixels, _rem) = span.as_chunks_mut::<BPP>();
954            if covers.is_empty() {
955                for (px, c) in pixels.iter_mut().zip(colors.iter()) {
956                    let r = Rgba8::multiply(c.r, c.a);
957                    let g = Rgba8::multiply(c.g, c.a);
958                    let b = Rgba8::multiply(c.b, c.a);
959                    blend(px, r, g, b, c.a, cover);
960                }
961            } else {
962                for ((px, c), &cov) in pixels.iter_mut().zip(colors.iter()).zip(covers.iter()) {
963                    let r = Rgba8::multiply(c.r, c.a);
964                    let g = Rgba8::multiply(c.g, c.a);
965                    let b = Rgba8::multiply(c.b, c.a);
966                    blend(px, r, g, b, c.a, cov);
967                }
968            }
969        });
970    }
971}
972
973// ============================================================================
974// Tests
975// ============================================================================
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980    use crate::rendering_buffer::RowAccessor;
981
982    fn make_buffer(w: u32, h: u32) -> (Vec<u8>, RowAccessor) {
983        let stride = (w * 4) as i32;
984        let buf = vec![0u8; (h * w * 4) as usize];
985        let mut ra = RowAccessor::new();
986        unsafe {
987            ra.attach(buf.as_ptr() as *mut u8, w, h, stride);
988        }
989        (buf, ra)
990    }
991
992    fn get_pixel(buf: &[u8], w: u32, x: u32, y: u32) -> (u8, u8, u8, u8) {
993        let off = (y * w * 4 + x * 4) as usize;
994        (buf[off], buf[off + 1], buf[off + 2], buf[off + 3])
995    }
996
997    #[test]
998    fn test_comp_op_default() {
999        assert_eq!(CompOp::default(), CompOp::SrcOver);
1000    }
1001
1002    #[test]
1003    fn test_clear() {
1004        let (_buf, mut ra) = make_buffer(10, 10);
1005        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1006        // Set a pixel first
1007        pf.copy_pixel(5, 5, &Rgba8::new(255, 0, 0, 255));
1008        let p = pf.pixel(5, 5);
1009        assert_eq!(p.r, 255);
1010
1011        // Clear it with comp_op clear
1012        pf.set_comp_op(CompOp::Clear);
1013        pf.blend_pixel(5, 5, &Rgba8::new(0, 0, 0, 255), 255);
1014        let p = pf.pixel(5, 5);
1015        assert_eq!(p.r, 0);
1016        assert_eq!(p.a, 0);
1017    }
1018
1019    #[test]
1020    fn test_src() {
1021        let (_buf, mut ra) = make_buffer(10, 10);
1022        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1023        pf.copy_pixel(0, 0, &Rgba8::new(100, 100, 100, 255));
1024
1025        pf.set_comp_op(CompOp::Src);
1026        pf.blend_pixel(0, 0, &Rgba8::new(200, 50, 0, 255), 255);
1027        let p = pf.pixel(0, 0);
1028        // With full cover and full alpha, Src just overwrites
1029        assert_eq!(p.r, 200);
1030        assert_eq!(p.g, 50);
1031        assert_eq!(p.b, 0);
1032        assert_eq!(p.a, 255);
1033    }
1034
1035    #[test]
1036    fn test_dst_is_noop() {
1037        let (_buf, mut ra) = make_buffer(10, 10);
1038        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1039        pf.copy_pixel(0, 0, &Rgba8::new(42, 43, 44, 200));
1040
1041        pf.set_comp_op(CompOp::Dst);
1042        pf.blend_pixel(0, 0, &Rgba8::new(255, 255, 255, 255), 255);
1043        let p = pf.pixel(0, 0);
1044        assert_eq!(p.r, 42);
1045        assert_eq!(p.g, 43);
1046        assert_eq!(p.b, 44);
1047        assert_eq!(p.a, 200);
1048    }
1049
1050    #[test]
1051    fn test_src_over_opaque() {
1052        let (_buf, mut ra) = make_buffer(10, 10);
1053        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1054        pf.copy_pixel(0, 0, &Rgba8::new(100, 100, 100, 255));
1055
1056        pf.set_comp_op(CompOp::SrcOver);
1057        pf.blend_pixel(0, 0, &Rgba8::new(200, 50, 25, 255), 255);
1058        let p = pf.pixel(0, 0);
1059        // SrcOver with full alpha = complete replacement
1060        assert_eq!(p.r, 200);
1061        assert_eq!(p.g, 50);
1062        assert_eq!(p.b, 25);
1063        assert_eq!(p.a, 255);
1064    }
1065
1066    #[test]
1067    fn test_src_over_semitransparent() {
1068        let (_buf, mut ra) = make_buffer(10, 10);
1069        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1070        pf.copy_pixel(0, 0, &Rgba8::new(0, 0, 0, 255));
1071
1072        pf.set_comp_op(CompOp::SrcOver);
1073        // Blend 50% transparent red over black
1074        pf.blend_pixel(0, 0, &Rgba8::new(255, 0, 0, 128), 255);
1075        let p = pf.pixel(0, 0);
1076        // Should get roughly half red
1077        assert!(p.r > 100 && p.r < 140, "r={}", p.r);
1078        assert_eq!(p.a, 255);
1079    }
1080
1081    #[test]
1082    fn test_multiply_mode() {
1083        let (_buf, mut ra) = make_buffer(10, 10);
1084        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1085        // White background
1086        pf.copy_pixel(0, 0, &Rgba8::new(255, 255, 255, 255));
1087
1088        pf.set_comp_op(CompOp::Multiply);
1089        pf.blend_pixel(0, 0, &Rgba8::new(128, 128, 128, 255), 255);
1090        let p = pf.pixel(0, 0);
1091        // Multiply of white × 50% gray ≈ 128
1092        assert!(p.r > 120 && p.r < 140, "r={}", p.r);
1093    }
1094
1095    #[test]
1096    fn test_screen_mode() {
1097        let (_buf, mut ra) = make_buffer(10, 10);
1098        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1099        // Black background
1100        pf.copy_pixel(0, 0, &Rgba8::new(0, 0, 0, 255));
1101
1102        pf.set_comp_op(CompOp::Screen);
1103        pf.blend_pixel(0, 0, &Rgba8::new(128, 128, 128, 255), 255);
1104        let p = pf.pixel(0, 0);
1105        // Screen of black + gray = gray
1106        assert!(p.r > 120 && p.r < 140, "r={}", p.r);
1107    }
1108
1109    #[test]
1110    fn test_xor_mode() {
1111        let (_buf, mut ra) = make_buffer(10, 10);
1112        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1113        // Fully opaque red
1114        pf.copy_pixel(0, 0, &Rgba8::new(255, 0, 0, 255));
1115
1116        pf.set_comp_op(CompOp::Xor);
1117        // Xor with fully opaque blue → both fully opaque → everything cancels
1118        pf.blend_pixel(0, 0, &Rgba8::new(0, 0, 255, 255), 255);
1119        let p = pf.pixel(0, 0);
1120        // Xor of two fully opaque: Da'=Sa+Da-2*Sa*Da = 1+1-2 = 0
1121        assert_eq!(p.a, 0);
1122    }
1123
1124    #[test]
1125    fn test_blend_hline() {
1126        let (_buf, mut ra) = make_buffer(20, 1);
1127        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1128        pf.set_comp_op(CompOp::SrcOver);
1129        let red = Rgba8::new(255, 0, 0, 255);
1130        pf.blend_hline(5, 0, 10, &red, 255);
1131        // Pixel at x=10 should be red
1132        let p = pf.pixel(10, 0);
1133        assert_eq!(p.r, 255);
1134        // Pixel at x=0 should be transparent
1135        let p = pf.pixel(0, 0);
1136        assert_eq!(p.a, 0);
1137    }
1138
1139    #[test]
1140    fn test_blend_solid_hspan() {
1141        let (_buf, mut ra) = make_buffer(10, 1);
1142        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1143        pf.set_comp_op(CompOp::SrcOver);
1144        let green = Rgba8::new(0, 255, 0, 255);
1145        let covers = [255u8, 128, 64, 0];
1146        pf.blend_solid_hspan(0, 0, 4, &green, &covers);
1147        // Full cover pixel
1148        let p = pf.pixel(0, 0);
1149        assert_eq!(p.g, 255);
1150        // Zero cover pixel — unchanged
1151        let p = pf.pixel(3, 0);
1152        assert_eq!(p.g, 0);
1153    }
1154
1155    #[test]
1156    fn test_all_ops_no_panic() {
1157        let ops = [
1158            CompOp::Clear,
1159            CompOp::Src,
1160            CompOp::Dst,
1161            CompOp::SrcOver,
1162            CompOp::DstOver,
1163            CompOp::SrcIn,
1164            CompOp::DstIn,
1165            CompOp::SrcOut,
1166            CompOp::DstOut,
1167            CompOp::SrcAtop,
1168            CompOp::DstAtop,
1169            CompOp::Xor,
1170            CompOp::Plus,
1171            CompOp::Minus,
1172            CompOp::Multiply,
1173            CompOp::Screen,
1174            CompOp::Overlay,
1175            CompOp::Darken,
1176            CompOp::Lighten,
1177            CompOp::ColorDodge,
1178            CompOp::ColorBurn,
1179            CompOp::HardLight,
1180            CompOp::SoftLight,
1181            CompOp::Difference,
1182            CompOp::Exclusion,
1183        ];
1184        let (_buf, mut ra) = make_buffer(10, 10);
1185        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1186        let c = Rgba8::new(128, 64, 32, 200);
1187        // Set some background
1188        pf.copy_pixel(0, 0, &Rgba8::new(100, 150, 200, 180));
1189        for &op in &ops {
1190            pf.set_comp_op(op);
1191            pf.blend_pixel(0, 0, &c, 128);
1192        }
1193    }
1194
1195    #[test]
1196    fn test_difference_mode() {
1197        let (_buf, mut ra) = make_buffer(10, 10);
1198        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1199        // White opaque background
1200        pf.copy_pixel(0, 0, &Rgba8::new(255, 255, 255, 255));
1201
1202        pf.set_comp_op(CompOp::Difference);
1203        pf.blend_pixel(0, 0, &Rgba8::new(255, 255, 255, 255), 255);
1204        let p = pf.pixel(0, 0);
1205        // Difference of same colors = black
1206        assert!(p.r < 5, "r={}", p.r);
1207        assert!(p.g < 5, "g={}", p.g);
1208        assert!(p.b < 5, "b={}", p.b);
1209    }
1210
1211    #[test]
1212    fn test_plus_saturates() {
1213        let (_buf, mut ra) = make_buffer(10, 10);
1214        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1215        pf.copy_pixel(0, 0, &Rgba8::new(200, 200, 200, 255));
1216
1217        pf.set_comp_op(CompOp::Plus);
1218        pf.blend_pixel(0, 0, &Rgba8::new(200, 200, 200, 255), 255);
1219        let p = pf.pixel(0, 0);
1220        // Should saturate at 255
1221        assert_eq!(p.r, 255);
1222        assert_eq!(p.g, 255);
1223        assert_eq!(p.b, 255);
1224    }
1225
1226    /// Locks in replication of the upstream AGG 2.6 typo in
1227    /// `comp_op_rgba_src_atop::blend_pix` (agg_pixfmt_rgba.h:530), where the
1228    /// blue channel is computed from the *already-updated* green (`d.g`)
1229    /// rather than blue. Because C++ executes the three assignments
1230    /// sequentially, by the time `d.b = s.b*d.a + d.g*s1a` runs, `d.g` has
1231    /// already been overwritten by the previous statement. Byte-identity with
1232    /// the compiled C++ renderer requires the port to carry this typo.
1233    ///
1234    /// Derivation (C++ double-precision formula, verified by hand + scratch):
1235    ///   dest raw bytes = (10, 200, 40, 200); note green (200) differs strongly
1236    ///   from blue (40), making the typo observable.
1237    ///   src (80, 60, 20, 100), cover 255. comp_op_adaptor premultiplies src:
1238    ///     r = multiply(80,100) = 31, g = multiply(60,100) = 24,
1239    ///     b = multiply(20,100) = 8, a = 100.
1240    ///   s = (31,24,8,100)/255 ; d = (10,200,40,200)/255 ; s1a = 1 - 100/255.
1241    ///     d.r = s.r*d.a + d.r*s1a = (31*200 + 10*155)/255^2 -> from_double = 30
1242    ///     d.g = s.g*d.a + d.g*s1a = (24*200 + 200*155)/255^2 -> from_double = 140
1243    ///     d.b = s.b*d.a + d.g_UPDATED*s1a  (uses the new d.g = 35800/255^2)
1244    ///         = 1600/255^2 + (35800/255^2)*(155/255) -> from_double = 92
1245    ///   alpha unchanged: from_double(200/255) = 200.
1246    #[test]
1247    fn test_src_atop_replicates_cpp_blue_channel_typo() {
1248        let (_buf, mut ra) = make_buffer(10, 10);
1249        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1250        // Premultiplied buffer bytes written directly (green 200 vs blue 40).
1251        pf.copy_pixel(0, 0, &Rgba8::new(10, 200, 40, 200));
1252
1253        pf.set_comp_op(CompOp::SrcAtop);
1254        pf.blend_pixel(0, 0, &Rgba8::new(80, 60, 20, 100), 255);
1255        let p = pf.pixel(0, 0);
1256        assert_eq!(p.r, 30);
1257        assert_eq!(p.g, 140);
1258        assert_eq!(p.b, 92);
1259        assert_eq!(p.a, 200);
1260    }
1261
1262    /// Locks in C++ `clip` semantics for the `minus` op (agg_pixfmt_rgba.h:37-44):
1263    /// after clamping alpha to [0,1], r/g/b are clamped to [0, alpha] — the upper
1264    /// bound is the (already-clamped) alpha, not 1.0. With a destination whose
1265    /// red exceeds its alpha (not premul-valid, but reachable since callers can
1266    /// write arbitrary buffer bytes), the red channel must clamp down to the new
1267    /// alpha rather than staying at its large value.
1268    ///
1269    /// Derivation (C++ double-precision formula + C++ clip):
1270    ///   dest raw bytes = (250, 10, 10, 40); src (0,0,0,100), cover 255.
1271    ///   premul src = (0,0,0,100); s = (0,0,0,100)/255 ; d = (250,10,10,40)/255.
1272    ///     d.a' = d.a + s.a - s.a*d.a
1273    ///          = 40/255 + 100/255 - (100/255)(40/255) -> from_double = 124
1274    ///     r,g,b = max(d - s, 0): d.r = 250/255, d.g = 10/255, d.b = 10/255.
1275    ///   C++ clip: a -> [0,1] (0.4875.. unchanged); then
1276    ///     r > a  => d.r clamps to a  -> from_double(a) = 124
1277    ///     g,b < a => unchanged        -> from_double(10/255) = 10
1278    #[test]
1279    fn test_minus_clip_clamps_rgb_to_alpha() {
1280        let (_buf, mut ra) = make_buffer(10, 10);
1281        let mut pf = PixfmtRgba32CompOp::new(&mut ra);
1282        // Deliberately not premul-valid: red (250) > alpha (40).
1283        pf.copy_pixel(0, 0, &Rgba8::new(250, 10, 10, 40));
1284
1285        pf.set_comp_op(CompOp::Minus);
1286        pf.blend_pixel(0, 0, &Rgba8::new(0, 0, 0, 100), 255);
1287        let p = pf.pixel(0, 0);
1288        assert_eq!(p.a, 124);
1289        assert_eq!(p.r, 124); // clamped down to the new alpha, not left at 250
1290        assert_eq!(p.g, 10);
1291        assert_eq!(p.b, 10);
1292    }
1293}