1
use agg_rust::{
2
    basics::{FillingRule, VertexSource},
3
    color::Rgba8,
4
    conv_transform::ConvTransform,
5
    gradient_lut::GradientLut,
6
    path_storage::PathStorage,
7
    pixfmt_rgba::PixfmtRgba32,
8
    rasterizer_scanline_aa::RasterizerScanlineAa,
9
    renderer_base::RendererBase,
10
    renderer_scanline::{render_scanlines_aa, render_scanlines_aa_solid},
11
    rendering_buffer::RowAccessor,
12
    scanline_u::ScanlineU8,
13
    span_allocator::SpanAllocator,
14
    span_gradient::{GradientFunction, SpanGradient},
15
    span_interpolator_linear::SpanInterpolatorLinear,
16
    trans_affine::TransAffine,
17
};
18
use azul_core::geom::{LogicalPosition, LogicalRect, LogicalSize};
19

            
20
use super::*;
21

            
22
pub const IDENTITY_EPSILON_F64: f64 = 0.0001;
23

            
24
/// Compute the intersection of two logical rects.
25
#[must_use]
26
20
pub fn rect_intersection(a: &LogicalRect, b: &LogicalRect) -> Option<LogicalRect> {
27
20
    let x1 = a.origin.x.max(b.origin.x);
28
20
    let y1 = a.origin.y.max(b.origin.y);
29
20
    let x2 = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
30
20
    let y2 = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
31
20
    if x2 > x1 && y2 > y1 {
32
15
        Some(LogicalRect {
33
15
            origin: LogicalPosition { x: x1, y: y1 },
34
15
            size: LogicalSize {
35
15
                width: x2 - x1,
36
15
                height: y2 - y1,
37
15
            },
38
15
        })
39
    } else {
40
5
        None
41
    }
42
20
}
43

            
44
/// Blit `src` onto `dst` at pixel position (`px_x`, `px_y`) with opacity.
45
#[allow(
46
    clippy::cast_possible_truncation,
47
    clippy::cast_possible_wrap,
48
    clippy::cast_sign_loss
49
)] // bounded pixel/coord/colour/glyph cast
50
238
pub fn blit_pixmap(src: &AzulPixmap, dst: &mut AzulPixmap, px_x: i32, px_y: i32, opacity: f32) {
51
238
    blit_pixmap_clipped(src, dst, px_x, px_y, opacity, None);
52
238
}
53

            
54
/// [`blit_pixmap`] with an optional DEST-space clip rect `(x0, y0, x1, y1)`
55
/// in device pixels (half-open). What keeps a layer nested inside a scroll
56
/// frame from painting outside that frame once the frame has scrolled it
57
/// past an edge.
58
276
pub fn blit_pixmap_clipped(
59
276
    src: &AzulPixmap,
60
276
    dst: &mut AzulPixmap,
61
276
    px_x: i32,
62
276
    px_y: i32,
63
276
    opacity: f32,
64
276
    clip: Option<(i32, i32, i32, i32)>,
65
276
) {
66
276
    let sw = src.width as i32;
67
276
    let sh = src.height as i32;
68
276
    let dw = dst.width as i32;
69
276
    let dh = dst.height as i32;
70
276
    let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
71

            
72
71074
    for sy in 0..sh {
73
        // saturating: px_y/px_x are caller-supplied device offsets that a large-but-legal
74
        // CSS transform can push to ~i32::MAX; a plain `+` overflows. A saturated result
75
        // fails the bounds check below and is skipped, which is the intended outcome.
76
71074
        let dy = px_y.saturating_add(sy);
77
71074
        if dy < 0 || dy >= dh {
78
99
            continue;
79
70975
        }
80
70975
        if let Some((_, cy0, _, cy1)) = clip {
81
1650
            if dy < cy0 || dy >= cy1 {
82
25
                continue;
83
1625
            }
84
69325
        }
85
34354571
        for sx in 0..sw {
86
34354571
            let dx = px_x.saturating_add(sx);
87
34354571
            if dx < 0 || dx >= dw {
88
128865
                continue;
89
34225706
            }
90
34225706
            if let Some((cx0, _, cx1, _)) = clip {
91
443300
                if dx < cx0 || dx >= cx1 {
92
400
                    continue;
93
442900
                }
94
33782406
            }
95
34225306
            let si = ((sy * sw + sx) * 4) as usize;
96
34225306
            let di = ((dy * dw + dx) * 4) as usize;
97
34225306
            if si + 3 >= src.data.len() || di + 3 >= dst.data.len() {
98
                continue;
99
34225306
            }
100

            
101
34225306
            let sr = u32::from(src.data[si]);
102
34225306
            let sg = u32::from(src.data[si + 1]);
103
34225306
            let sb = u32::from(src.data[si + 2]);
104
34225306
            let sa = (u32::from(src.data[si + 3]) * op) / 255;
105

            
106
34225306
            if sa == 0 {
107
56031
                continue;
108
34169275
            }
109
34169275
            if sa == 255 {
110
34169274
                dst.data[di] = sr as u8;
111
34169274
                dst.data[di + 1] = sg as u8;
112
34169274
                dst.data[di + 2] = sb as u8;
113
34169274
                dst.data[di + 3] = 255;
114
34169274
            } else {
115
1
                let inv_sa = 255 - sa;
116
1
                dst.data[di] = ((sr * sa + u32::from(dst.data[di]) * inv_sa) / 255) as u8;
117
1
                dst.data[di + 1] = ((sg * sa + u32::from(dst.data[di + 1]) * inv_sa) / 255) as u8;
118
1
                dst.data[di + 2] = ((sb * sa + u32::from(dst.data[di + 2]) * inv_sa) / 255) as u8;
119
1
                dst.data[di + 3] =
120
1
                    ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
121
1
            }
122
        }
123
    }
124
276
}
125

            
126
/// Blit `src` into `dst` through an affine map from SRC DEVICE-PIXEL
127
/// coordinates to DST DEVICE-PIXEL coordinates.
128
///
129
/// This is how a composited layer with a live transform (drag, CSS
130
/// `transform`, diff-driven animation) reaches the screen on the CPU path:
131
/// the layer's pixbuf holds its content at LAYOUT position in layer-local
132
/// space, and this maps every pixel through the layer's matrix at composite
133
/// time. `blit_pixmap` above is the identity fast path — for years it was the
134
/// ONLY path, which is why a transformed layer rendered at its layout
135
/// position no matter what its matrix said.
136
///
137
/// Inverse mapping with bilinear sampling: iterate the dest-space bounding box
138
/// of the transformed src rect, map each dest pixel back through the inverted
139
/// matrix, sample src bilinearly (edge-clamped), blend with `opacity`. A
140
/// non-invertible matrix (degenerate scale) draws nothing — a collapsed layer
141
/// has no area.
142
#[allow(
143
    clippy::cast_possible_truncation,
144
    clippy::cast_sign_loss,
145
    clippy::cast_precision_loss
146
)]
147
// bounded pixel/coord/colour casts
148
1
pub fn blit_pixmap_affine(src: &AzulPixmap, dst: &mut AzulPixmap, m: &TransAffine, opacity: f32) {
149
1
    blit_pixmap_affine_clipped(src, dst, m, opacity, None);
150
1
}
151

            
152
/// [`blit_pixmap_affine`] with an optional DEST-space clip rect
153
/// `(x0, y0, x1, y1)` in device pixels. The clip is what keeps a keyframed
154
/// exit inside its own box: `-azul-animation-out` may translate the retained
155
/// content, and without a clip the slide paints over neighbouring components
156
/// (the sliding-out scrollbar over the body margin was the reported case).
157
#[allow(
158
    clippy::cast_possible_truncation,
159
    clippy::cast_sign_loss,
160
    clippy::cast_precision_loss
161
)]
162
// bounded pixel/coord/colour casts
163
63
pub fn blit_pixmap_affine_clipped(
164
63
    src: &AzulPixmap,
165
63
    dst: &mut AzulPixmap,
166
63
    m: &TransAffine,
167
63
    opacity: f32,
168
63
    clip: Option<(i32, i32, i32, i32)>,
169
63
) {
170
63
    let sw = src.width as i32;
171
63
    let sh = src.height as i32;
172
63
    let dw = dst.width as i32;
173
63
    let dh = dst.height as i32;
174
63
    if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
175
        return;
176
63
    }
177
63
    let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
178
63
    if op == 0 {
179
        return;
180
63
    }
181

            
182
63
    let mut inv = *m;
183
    // agg's invert() on a degenerate matrix produces non-finite values; the
184
    // finite-check below skips those pixels, so the collapsed-layer case
185
    // degrades to "draws nothing" rather than UB or garbage.
186
63
    inv.invert();
187

            
188
    // Dest-space bounding box of the four transformed src corners.
189
63
    let corners = [
190
63
        (0.0, 0.0),
191
63
        (f64::from(sw), 0.0),
192
63
        (0.0, f64::from(sh)),
193
63
        (f64::from(sw), f64::from(sh)),
194
63
    ];
195
63
    let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
196
63
    let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
197
315
    for (cx, cy) in corners {
198
252
        let (mut x, mut y) = (cx, cy);
199
252
        m.transform(&mut x, &mut y);
200
252
        min_x = min_x.min(x);
201
252
        min_y = min_y.min(y);
202
252
        max_x = max_x.max(x);
203
252
        max_y = max_y.max(y);
204
252
    }
205
63
    if !(min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite()) {
206
        return;
207
63
    }
208
63
    let mut x0 = (min_x.floor() as i32).max(0);
209
63
    let mut y0 = (min_y.floor() as i32).max(0);
210
63
    let mut x1 = (max_x.ceil() as i32).min(dw);
211
63
    let mut y1 = (max_y.ceil() as i32).min(dh);
212
63
    if let Some((cx0, cy0, cx1, cy1)) = clip {
213
24
        x0 = x0.max(cx0);
214
24
        y0 = y0.max(cy0);
215
24
        x1 = x1.min(cx1);
216
24
        y1 = y1.min(cy1);
217
39
    }
218

            
219
13129
    for dy in y0..y1 {
220
2753276
        for dx in x0..x1 {
221
            // Sample at the dest pixel CENTER, mapped back to src space.
222
2753276
            let (mut fx, mut fy) = (f64::from(dx) + 0.5, f64::from(dy) + 0.5);
223
2753276
            inv.transform(&mut fx, &mut fy);
224
2753276
            if !(fx.is_finite() && fy.is_finite()) {
225
                return;
226
2753276
            }
227
            // Back to texel space (pixel centers at n + 0.5).
228
2753276
            let sx_f = fx - 0.5;
229
2753276
            let sy_f = fy - 0.5;
230
            // Outside the src rect entirely (with the half-texel skirt the
231
            // bilinear kernel needs) → transparent, nothing to blend.
232
2753276
            if sx_f <= -1.0 || sy_f <= -1.0 || sx_f >= f64::from(sw) || sy_f >= f64::from(sh) {
233
9
                continue;
234
2753267
            }
235
2753267
            let x_lo = sx_f.floor() as i32;
236
2753267
            let y_lo = sy_f.floor() as i32;
237
2753267
            let wx = (sx_f - f64::from(x_lo)) as f32;
238
2753267
            let wy = (sy_f - f64::from(y_lo)) as f32;
239

            
240
            // Edge-clamped 2x2 fetch. Weights of taps that clamp collapse
241
            // onto the edge texel, which is the standard clamp-to-edge rule.
242
11013068
            let fetch = |x: i32, y: i32| -> [f32; 4] {
243
11013068
                let cx = x.clamp(0, sw - 1);
244
11013068
                let cy = y.clamp(0, sh - 1);
245
11013068
                let i = ((cy * sw + cx) * 4) as usize;
246
11013068
                [
247
11013068
                    f32::from(src.data[i]),
248
11013068
                    f32::from(src.data[i + 1]),
249
11013068
                    f32::from(src.data[i + 2]),
250
11013068
                    f32::from(src.data[i + 3]),
251
11013068
                ]
252
11013068
            };
253
2753267
            let p00 = fetch(x_lo, y_lo);
254
2753267
            let p10 = fetch(x_lo + 1, y_lo);
255
2753267
            let p01 = fetch(x_lo, y_lo + 1);
256
2753267
            let p11 = fetch(x_lo + 1, y_lo + 1);
257
            // Outside-the-rect taps are transparent, not clamped: without
258
            // this the border row of an opaque layer smears outward to the
259
            // whole bbox edge instead of fading over one pixel.
260
11013068
            let zero_if_out = |x: i32, y: i32, p: [f32; 4]| -> [f32; 4] {
261
11013068
                if x < 0 || y < 0 || x >= sw || y >= sh {
262
48599
                    [0.0, 0.0, 0.0, 0.0]
263
                } else {
264
10964469
                    p
265
                }
266
11013068
            };
267
2753267
            let p00 = zero_if_out(x_lo, y_lo, p00);
268
2753267
            let p10 = zero_if_out(x_lo + 1, y_lo, p10);
269
2753267
            let p01 = zero_if_out(x_lo, y_lo + 1, p01);
270
2753267
            let p11 = zero_if_out(x_lo + 1, y_lo + 1, p11);
271

            
272
8259801
            let lerp2 = |a: [f32; 4], b: [f32; 4], t: f32| -> [f32; 4] {
273
8259801
                [
274
8259801
                    a[0] + (b[0] - a[0]) * t,
275
8259801
                    a[1] + (b[1] - a[1]) * t,
276
8259801
                    a[2] + (b[2] - a[2]) * t,
277
8259801
                    a[3] + (b[3] - a[3]) * t,
278
8259801
                ]
279
8259801
            };
280
2753267
            let top = lerp2(p00, p10, wx);
281
2753267
            let bot = lerp2(p01, p11, wx);
282
2753267
            let px = lerp2(top, bot, wy);
283

            
284
2753267
            let sa = ((px[3] as u32) * op) / 255;
285
2753267
            if sa == 0 {
286
248557
                continue;
287
2504710
            }
288
2504710
            let di = ((dy * dw + dx) * 4) as usize;
289
2504710
            if di + 3 >= dst.data.len() {
290
                continue;
291
2504710
            }
292
2504710
            let (sr, sg, sb) = (px[0] as u32, px[1] as u32, px[2] as u32);
293
2504710
            if sa >= 255 {
294
2465121
                dst.data[di] = sr as u8;
295
2465121
                dst.data[di + 1] = sg as u8;
296
2465121
                dst.data[di + 2] = sb as u8;
297
2465121
                dst.data[di + 3] = 255;
298
2465162
            } else {
299
39589
                let inv_sa = 255 - sa;
300
39589
                dst.data[di] = ((sr * sa + u32::from(dst.data[di]) * inv_sa) / 255) as u8;
301
39589
                dst.data[di + 1] = ((sg * sa + u32::from(dst.data[di + 1]) * inv_sa) / 255) as u8;
302
39589
                dst.data[di + 2] = ((sb * sa + u32::from(dst.data[di + 2]) * inv_sa) / 255) as u8;
303
39589
                dst.data[di + 3] =
304
39589
                    ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
305
39589
            }
306
        }
307
    }
308
63
}
309

            
310
/// Projective twin of [`blit_pixmap_affine`]: `h` is a row-major 3x3
311
/// homography over column vectors (`compositor::Mat3`) mapping SRC pixels to
312
/// DEST pixels with a perspective divide. This is how a `perspective()
313
/// rotateX()` tilt reaches the screen on the CPU path — the affine blit
314
/// cannot foreshorten. Inverse mapping per dest pixel (the homography's
315
/// adjugate), bilinear sampling, the same blend as the affine blit. Dest
316
/// pixels that map behind the eye (`w <= 0`) or outside the source draw
317
/// nothing; a singular matrix draws nothing.
318
#[allow(
319
    clippy::cast_possible_truncation,
320
    clippy::cast_sign_loss,
321
    clippy::cast_precision_loss
322
)]
323
#[allow(
324
    clippy::many_single_char_names,
325
    clippy::similar_names,
326
    clippy::too_many_lines
327
)]
328
4
pub fn blit_pixmap_projective(src: &AzulPixmap, dst: &mut AzulPixmap, h: &[f64; 9], opacity: f32) {
329
4
    blit_pixmap_projective_clipped(src, dst, h, opacity, None);
330
4
}
331

            
332
/// [`blit_pixmap_projective`] with an optional DEST-space clip rect
333
/// `(x0, y0, x1, y1)` in device pixels (half-open), intersected with the
334
/// projected bounding box.
335
#[allow(
336
    clippy::cast_possible_truncation,
337
    clippy::cast_sign_loss,
338
    clippy::cast_precision_loss
339
)]
340
// bounded pixel/coord/colour casts
341
#[allow(clippy::many_single_char_names)] // homography coefficients: a..i is THE notation
342
4
pub fn blit_pixmap_projective_clipped(
343
4
    src: &AzulPixmap,
344
4
    dst: &mut AzulPixmap,
345
4
    h: &[f64; 9],
346
4
    opacity: f32,
347
4
    clip: Option<(i32, i32, i32, i32)>,
348
4
) {
349
4
    let sw = src.width as i32;
350
4
    let sh = src.height as i32;
351
4
    let dw = dst.width as i32;
352
4
    let dh = dst.height as i32;
353
4
    if sw == 0 || sh == 0 || dw == 0 || dh == 0 {
354
        return;
355
4
    }
356
4
    let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
357
4
    if op == 0 {
358
        return;
359
4
    }
360

            
361
    // Inverse via the adjugate; a singular matrix has no area.
362
4
    let (a, b, c, d, e, f, g, hh, i) = (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8]);
363
4
    let det = a * (e * i - f * hh) - b * (d * i - f * g) + c * (d * hh - e * g);
364
4
    if !det.is_finite() || det.abs() < 1e-12 {
365
1
        return;
366
3
    }
367
3
    let inv = [
368
3
        (e * i - f * hh) / det,
369
3
        (c * hh - b * i) / det,
370
3
        (b * f - c * e) / det,
371
3
        (f * g - d * i) / det,
372
3
        (a * i - c * g) / det,
373
3
        (c * d - a * f) / det,
374
3
        (d * hh - e * g) / det,
375
3
        (b * g - a * hh) / det,
376
3
        (a * e - b * d) / det,
377
3
    ];
378

            
379
    // Dest-space bounding box of the four projected src corners. A corner
380
    // behind the eye has no finite image: fall back to the whole dest.
381
3
    let corners = [
382
3
        (0.0, 0.0),
383
3
        (f64::from(sw), 0.0),
384
3
        (0.0, f64::from(sh)),
385
3
        (f64::from(sw), f64::from(sh)),
386
3
    ];
387
3
    let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
388
3
    let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
389
3
    let mut behind = false;
390
13
    for (cx, cy) in corners {
391
11
        let w = g * cx + hh * cy + i;
392
11
        if w <= 1e-9 {
393
1
            behind = true;
394
1
            break;
395
10
        }
396
10
        let x = (a * cx + b * cy + c) / w;
397
10
        let y = (d * cx + e * cy + f) / w;
398
10
        min_x = min_x.min(x);
399
10
        min_y = min_y.min(y);
400
10
        max_x = max_x.max(x);
401
10
        max_y = max_y.max(y);
402
    }
403
3
    let (x0, y0, x1, y1) = if behind
404
2
        || !(min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite())
405
    {
406
1
        (0, 0, dw, dh)
407
    } else {
408
2
        (
409
2
            (min_x.floor() as i32).max(0),
410
2
            (min_y.floor() as i32).max(0),
411
2
            (max_x.ceil() as i32).min(dw),
412
2
            (max_y.ceil() as i32).min(dh),
413
2
        )
414
    };
415
3
    let (x0, y0, x1, y1) = match clip {
416
        Some((cx0, cy0, cx1, cy1)) => (x0.max(cx0), y0.max(cy0), x1.min(cx1), y1.min(cy1)),
417
3
        None => (x0, y0, x1, y1),
418
    };
419

            
420
11
    for dy in y0..y1 {
421
158
        for dx in x0..x1 {
422
158
            let (px_c, py_c) = (f64::from(dx) + 0.5, f64::from(dy) + 0.5);
423
158
            let s = inv[6] * px_c + inv[7] * py_c + inv[8];
424
158
            if !(s.is_finite()) || s <= 1e-9 {
425
                continue;
426
158
            }
427
158
            let fx = (inv[0] * px_c + inv[1] * py_c + inv[2]) / s;
428
158
            let fy = (inv[3] * px_c + inv[4] * py_c + inv[5]) / s;
429
158
            if !(fx.is_finite() && fy.is_finite()) {
430
                continue;
431
158
            }
432
158
            let sx_f = fx - 0.5;
433
158
            let sy_f = fy - 0.5;
434
158
            if sx_f <= -1.0 || sy_f <= -1.0 || sx_f >= f64::from(sw) || sy_f >= f64::from(sh) {
435
55
                continue;
436
103
            }
437
103
            let x_lo = sx_f.floor() as i32;
438
103
            let y_lo = sy_f.floor() as i32;
439
103
            let wx = (sx_f - f64::from(x_lo)) as f32;
440
103
            let wy = (sy_f - f64::from(y_lo)) as f32;
441
412
            let fetch = |x: i32, y: i32| -> [f32; 4] {
442
412
                if x < 0 || y < 0 || x >= sw || y >= sh {
443
68
                    return [0.0, 0.0, 0.0, 0.0];
444
344
                }
445
344
                let idx = ((y * sw + x) * 4) as usize;
446
344
                [
447
344
                    f32::from(src.data[idx]),
448
344
                    f32::from(src.data[idx + 1]),
449
344
                    f32::from(src.data[idx + 2]),
450
344
                    f32::from(src.data[idx + 3]),
451
344
                ]
452
412
            };
453
309
            let lerp2 = |p: [f32; 4], q: [f32; 4], t: f32| -> [f32; 4] {
454
309
                [
455
309
                    p[0] + (q[0] - p[0]) * t,
456
309
                    p[1] + (q[1] - p[1]) * t,
457
309
                    p[2] + (q[2] - p[2]) * t,
458
309
                    p[3] + (q[3] - p[3]) * t,
459
309
                ]
460
309
            };
461
103
            let top = lerp2(fetch(x_lo, y_lo), fetch(x_lo + 1, y_lo), wx);
462
103
            let bot = lerp2(fetch(x_lo, y_lo + 1), fetch(x_lo + 1, y_lo + 1), wx);
463
103
            let px = lerp2(top, bot, wy);
464

            
465
103
            let sa = ((px[3] as u32) * op) / 255;
466
103
            if sa == 0 {
467
                continue;
468
103
            }
469
103
            let di = ((dy * dw + dx) * 4) as usize;
470
103
            if di + 3 >= dst.data.len() {
471
                continue;
472
103
            }
473
103
            let (sr, sg, sb) = (px[0] as u32, px[1] as u32, px[2] as u32);
474
103
            if sa >= 255 {
475
53
                dst.data[di] = sr as u8;
476
53
                dst.data[di + 1] = sg as u8;
477
53
                dst.data[di + 2] = sb as u8;
478
53
                dst.data[di + 3] = 255;
479
53
            } else {
480
50
                let inv_sa = 255 - sa;
481
50
                dst.data[di] = ((sr * sa + u32::from(dst.data[di]) * inv_sa) / 255) as u8;
482
50
                dst.data[di + 1] = ((sg * sa + u32::from(dst.data[di + 1]) * inv_sa) / 255) as u8;
483
50
                dst.data[di + 2] = ((sb * sa + u32::from(dst.data[di + 2]) * inv_sa) / 255) as u8;
484
50
                dst.data[di + 3] =
485
50
                    ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
486
50
            }
487
        }
488
    }
489
4
}
490

            
491
/// Shift pixel data in a pixmap by (dx, dy) pixels, clearing exposed regions.
492
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph
493
                                                             // cast
494
20
pub fn shift_pixbuf(pixmap: &mut AzulPixmap, dx: i32, dy: i32) {
495
    use core::cmp::Ordering;
496
20
    let w = pixmap.width as i32;
497
20
    let h = pixmap.height as i32;
498
    // `i32::MIN.abs()` panics — MIN has no positive i32 counterpart. `unsigned_abs`
499
    // is total. `w`/`h` come from unsigned dimensions, so they are never negative.
500
20
    if dx.unsigned_abs() >= w.unsigned_abs() || dy.unsigned_abs() >= h.unsigned_abs() {
501
        // Entire buffer is exposed — just clear it
502
7
        pixmap.fill(0, 0, 0, 0);
503
7
        return;
504
13
    }
505

            
506
13
    let stride = (w * 4) as usize;
507
13
    let data = &mut pixmap.data;
508

            
509
    // Shift rows vertically
510
13
    match dy.cmp(&0) {
511
        Ordering::Greater => {
512
            // Shift down: copy from top to bottom
513
30
            for row in (0..h - dy).rev() {
514
30
                let src_start = (row * w * 4) as usize;
515
30
                let dst_start = ((row + dy) * w * 4) as usize;
516
30
                data.copy_within(src_start..src_start + stride, dst_start);
517
30
            }
518
            // Clear top rows
519
16
            for row in 0..dy {
520
16
                let start = (row * w * 4) as usize;
521
16
                data[start..start + stride].fill(0);
522
16
            }
523
        }
524
        Ordering::Less => {
525
3
            let ady = -dy;
526
            // Shift up: copy from bottom to top
527
6
            for row in ady..h {
528
6
                let src_start = (row * w * 4) as usize;
529
6
                let dst_start = ((row - ady) * w * 4) as usize;
530
6
                data.copy_within(src_start..src_start + stride, dst_start);
531
6
            }
532
            // Clear bottom rows
533
5
            for row in (h - ady)..h {
534
5
                let start = (row * w * 4) as usize;
535
5
                data[start..start + stride].fill(0);
536
5
            }
537
        }
538
5
        Ordering::Equal => {}
539
    }
540

            
541
    // Shift columns horizontally
542
13
    match dx.cmp(&0) {
543
        Ordering::Greater => {
544
14
            for row in 0..h {
545
14
                let row_start = (row * w * 4) as usize;
546
14
                let shift = (dx * 4) as usize;
547
14
                // Shift right within the row
548
14
                data.copy_within(row_start..row_start + stride - shift, row_start + shift);
549
14
                // Clear left columns
550
14
                data[row_start..row_start + shift].fill(0);
551
14
            }
552
        }
553
        Ordering::Less => {
554
3
            let adx = (-dx * 4) as usize;
555
11
            for row in 0..h {
556
11
                let row_start = (row * w * 4) as usize;
557
11
                data.copy_within(row_start + adx..row_start + stride, row_start);
558
11
                // Clear right columns
559
11
                data[row_start + stride - adx..row_start + stride].fill(0);
560
11
            }
561
        }
562
6
        Ordering::Equal => {}
563
    }
564
20
}
565

            
566
/// (#27) Pixmap storage: owned heap, or BORROWED external memory — a
567
/// platform backbuffer such as a mapped Wayland shm slot. All slice
568
/// access goes through `Deref`, so the raster/compositor sites are
569
/// storage-blind; only construction and resize know the difference
570
/// (resize always converts to owned — a borrowed target's size is the
571
/// creator's contract). Borrowed storage is NEVER freed here.
572
pub enum PixBuf {
573
    Owned(Vec<u8>),
574
    /// SAFETY (creator's obligations, see [`AzulPixmap::from_external`]):
575
    /// `ptr` stays valid and EXCLUSIVELY ours for the pixmap's lifetime.
576
    Borrowed {
577
        ptr: *mut u8,
578
        len: usize,
579
    },
580
}
581

            
582
impl core::ops::Deref for PixBuf {
583
    type Target = [u8];
584
    #[inline]
585
264988291
    fn deref(&self) -> &[u8] {
586
264988291
        match self {
587
264988287
            Self::Owned(v) => v,
588
4
            Self::Borrowed { ptr, len } => unsafe { core::slice::from_raw_parts(*ptr, *len) },
589
        }
590
264988291
    }
591
}
592

            
593
impl core::ops::DerefMut for PixBuf {
594
    #[inline]
595
153113384
    fn deref_mut(&mut self) -> &mut [u8] {
596
153113384
        match self {
597
153113383
            Self::Owned(v) => v,
598
1
            Self::Borrowed { ptr, len } => unsafe { core::slice::from_raw_parts_mut(*ptr, *len) },
599
        }
600
153113384
    }
601
}
602

            
603
/// Byte-content equality, storage-blind (tests compare rendered frames).
604
impl PartialEq for PixBuf {
605
2
    fn eq(&self, other: &Self) -> bool {
606
2
        **self == **other
607
2
    }
608
}
609

            
610
impl Clone for PixBuf {
611
    /// Cloning SNAPSHOTS: a borrowed frame clones to an owned copy (the
612
    /// clone must not alias the platform buffer).
613
84
    fn clone(&self) -> Self {
614
84
        Self::Owned(self.to_vec())
615
84
    }
616
}
617

            
618
impl PixBuf {
619
    /// Take the bytes as an owned Vec (borrowed storage copies out).
620
19
    pub(crate) fn into_vec(self) -> Vec<u8> {
621
19
        match self {
622
18
            Self::Owned(v) => v,
623
1
            Self::Borrowed { .. } => self.to_vec(),
624
        }
625
19
    }
626
}
627

            
628
impl From<Vec<u8>> for PixBuf {
629
3955
    fn from(v: Vec<u8>) -> Self {
630
3955
        Self::Owned(v)
631
3955
    }
632
}
633

            
634
impl core::fmt::Debug for PixBuf {
635
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
636
        match self {
637
            Self::Owned(v) => write!(f, "PixBuf::Owned({} B)", v.len()),
638
            Self::Borrowed { len, .. } => write!(f, "PixBuf::Borrowed({len} B)"),
639
        }
640
    }
641
}
642

            
643
// SAFETY: the raw pointer is only reachable while the creator's
644
// validity+exclusivity contract holds (from_external is unsafe); under
645
// that contract the buffer is an ordinary exclusive byte region and may
646
// move across threads with its pixmap.
647
unsafe impl Send for PixBuf {}
648

            
649
/// A simple RGBA pixel buffer. Replaces `tiny_skia::Pixmap`.
650
#[derive(Debug)]
651
pub struct AzulPixmap {
652
    pub(crate) data: PixBuf,
653
    /// Width in DEVICE pixels — for a render at `dpi_factor` N this is the
654
    /// logical width times N, not the logical width.
655
    pub width: u32,
656
    /// Height in DEVICE pixels. See [`AzulPixmap::width`].
657
    pub height: u32,
658
}
659

            
660
impl AzulPixmap {
661
    /// (#27) Wrap EXTERNAL storage — a platform backbuffer such as a
662
    /// mapped Wayland shm slot — as a render target, no copy, no
663
    /// ownership. Returns `None` on zero dimensions.
664
    ///
665
    /// # Safety
666
    /// The caller guarantees, for the ENTIRE lifetime of the returned
667
    /// pixmap (and anything it moves into): `ptr..ptr+width*height*4`
668
    /// is valid, writable, and accessed EXCLUSIVELY through this pixmap
669
    /// (for shm: the compositor has released the buffer and it will not
670
    /// be re-attached while this pixmap lives).
671
5
    pub unsafe fn from_external(ptr: *mut u8, width: u32, height: u32) -> Option<Self> {
672
5
        if width == 0 || height == 0 || ptr.is_null() {
673
3
            return None;
674
2
        }
675
2
        let len = (width as usize)
676
2
            .checked_mul(height as usize)?
677
2
            .checked_mul(4)?;
678
2
        Some(Self {
679
2
            data: PixBuf::Borrowed { ptr, len },
680
2
            width,
681
2
            height,
682
2
        })
683
5
    }
684

            
685
    /// (#27) Whether this pixmap renders into borrowed platform memory.
686
    #[must_use]
687
2
    pub const fn is_external(&self) -> bool {
688
2
        matches!(self.data, PixBuf::Borrowed { .. })
689
2
    }
690

            
691
    /// Create a new pixmap filled with opaque white.
692
    #[must_use]
693
3552
    pub fn new(width: u32, height: u32) -> Option<Self> {
694
3552
        if width == 0 || height == 0 {
695
21
            return None;
696
3531
        }
697
        // checked: author-controllable dimensions (e.g. an SVG intrinsic size) can make
698
        // width*height*4 overflow usize — a debug panic, and a silently-undersized
699
        // buffer in release. Refuse absurd sizes instead.
700
3531
        let len = (width as usize)
701
3531
            .checked_mul(height as usize)
702
3531
            .and_then(|n| n.checked_mul(4))?;
703
3530
        let data = PixBuf::from(vec![255u8; len]); // opaque white
704
3530
        Some(Self {
705
3530
            data,
706
3530
            width,
707
3530
            height,
708
3530
        })
709
3552
    }
710

            
711
    /// Fill the entire pixmap with a single color.
712
3649
    pub fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) {
713
808043361
        for chunk in self.data.chunks_exact_mut(4) {
714
808043361
            chunk[0] = r;
715
808043361
            chunk[1] = g;
716
808043361
            chunk[2] = b;
717
808043361
            chunk[3] = a;
718
808043361
        }
719
3649
    }
720

            
721
    /// Fill a rectangular region with a single color (pixel coordinates).
722
    #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
723
    #[allow(clippy::many_single_char_names)] // domain-standard coordinate/geometry/short-lived
724
                                             // names
725
10977
    pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, r: u8, g: u8, b: u8, a: u8) {
726
10977
        let pw = self.width as i32;
727
10977
        let ph = self.height as i32;
728
10977
        let x0 = x.max(0).min(pw);
729
10977
        let y0 = y.max(0).min(ph);
730
        // saturating: a non-finite/huge layout size casts to i32::MAX, and `x + w`
731
        // would then overflow (debug panic). Clamp instead.
732
        // `.clamp(x0, ..)` (not just `.max(0)`): a NEGATIVE w gives x1 < x0, and the
733
        // `data[start..end]` slice below panics on a reversed range. Force x1 >= x0.
734
10977
        let x1 = x.saturating_add(w).clamp(x0, pw);
735
10977
        let y1 = y.saturating_add(h).clamp(y0, ph);
736
1332805
        for row in y0..y1 {
737
1332805
            let start = (row * pw + x0) as usize * 4;
738
1332805
            let end = (row * pw + x1) as usize * 4;
739
1332805
            if end <= self.data.len() {
740
651742719
                for chunk in self.data[start..end].chunks_exact_mut(4) {
741
651742719
                    chunk[0] = r;
742
651742719
                    chunk[1] = g;
743
651742719
                    chunk[2] = b;
744
651742719
                    chunk[3] = a;
745
651742719
                }
746
            }
747
        }
748
10977
    }
749

            
750
    /// Raw RGBA pixel data.
751
    #[must_use]
752
18714
    pub fn data(&self) -> &[u8] {
753
18714
        &self.data
754
18714
    }
755

            
756
    /// Mutable raw RGBA pixel data.
757
412
    pub fn data_mut(&mut self) -> &mut [u8] {
758
412
        &mut self.data
759
412
    }
760

            
761
    /// Width in pixels.
762
    #[must_use]
763
36247
    pub const fn width(&self) -> u32 {
764
36247
        self.width
765
36247
    }
766

            
767
    /// Height in pixels.
768
    #[must_use]
769
35665
    pub const fn height(&self) -> u32 {
770
35665
        self.height
771
35665
    }
772

            
773
    /// Create a clone of this pixmap (for filter application).
774
    #[must_use]
775
83
    pub fn clone_pixmap(&self) -> Self {
776
83
        Self {
777
83
            data: self.data.clone(),
778
83
            width: self.width,
779
83
            height: self.height,
780
83
        }
781
83
    }
782

            
783
    /// Resize the pixmap preserving existing content in the top-left corner.
784
    /// New right/bottom strips are filled with the specified color.
785
    /// Only grows — returns None if new dimensions are smaller (caller should realloc).
786
15
    pub fn resize_grow_only(
787
15
        &mut self,
788
15
        new_width: u32,
789
15
        new_height: u32,
790
15
        fill_r: u8,
791
15
        fill_g: u8,
792
15
        fill_b: u8,
793
15
        fill_a: u8,
794
15
    ) -> Option<()> {
795
15
        if new_width < self.width || new_height < self.height {
796
4
            return None;
797
11
        }
798
11
        if new_width == self.width && new_height == self.height {
799
1
            return Some(());
800
10
        }
801

            
802
10
        let old_w = self.width as usize;
803
10
        let old_h = self.height as usize;
804
10
        let new_w = new_width as usize;
805
10
        let new_h = new_height as usize;
806
10
        let mut new_data = vec![fill_a; new_w * new_h * 4];
807

            
808
        // Fill entire buffer with fill color first (covers right + bottom strips)
809
3242428
        for chunk in new_data.chunks_exact_mut(4) {
810
3242428
            chunk[0] = fill_r;
811
3242428
            chunk[1] = fill_g;
812
3242428
            chunk[2] = fill_b;
813
3242428
            chunk[3] = fill_a;
814
3242428
        }
815

            
816
        // Copy old rows into top-left corner
817
10
        let old_stride = old_w * 4;
818
10
        let new_stride = new_w * 4;
819
3304
        for row in 0..old_h {
820
3304
            let src = row * old_stride;
821
3304
            let dst = row * new_stride;
822
3304
            new_data[dst..dst + old_stride].copy_from_slice(&self.data[src..src + old_stride]);
823
3304
        }
824

            
825
10
        self.data = new_data.into();
826
10
        self.width = new_width;
827
10
        self.height = new_height;
828
10
        Some(())
829
15
    }
830

            
831
    /// Resize the pixmap, reusing existing content for the overlapping region.
832
    /// Works for both growing and shrinking. New areas are filled with the given color.
833
36
    pub fn resize_reuse(
834
36
        &mut self,
835
36
        new_width: u32,
836
36
        new_height: u32,
837
36
        fill_r: u8,
838
36
        fill_g: u8,
839
36
        fill_b: u8,
840
36
        fill_a: u8,
841
36
    ) {
842
36
        if new_width == self.width && new_height == self.height {
843
1
            return;
844
35
        }
845

            
846
35
        let old_w = self.width as usize;
847
35
        let old_h = self.height as usize;
848
35
        let new_w = new_width as usize;
849
35
        let new_h = new_height as usize;
850
35
        let new_stride = new_w * 4;
851
35
        let old_stride = old_w * 4;
852

            
853
35
        let mut new_data = vec![0u8; new_w * new_h * 4];
854

            
855
        // Fill entire buffer with fill color
856
4800003
        for chunk in new_data.chunks_exact_mut(4) {
857
4800003
            chunk[0] = fill_r;
858
4800003
            chunk[1] = fill_g;
859
4800003
            chunk[2] = fill_b;
860
4800003
            chunk[3] = fill_a;
861
4800003
        }
862

            
863
        // Copy overlapping region from old to new
864
35
        let copy_rows = old_h.min(new_h);
865
35
        let copy_cols_bytes = old_stride.min(new_stride);
866
5897
        for row in 0..copy_rows {
867
5897
            let src = row * old_stride;
868
5897
            let dst = row * new_stride;
869
5897
            new_data[dst..dst + copy_cols_bytes]
870
5897
                .copy_from_slice(&self.data[src..src + copy_cols_bytes]);
871
5897
        }
872

            
873
35
        self.data = new_data.into();
874
35
        self.width = new_width;
875
35
        self.height = new_height;
876
36
    }
877

            
878
    /// Encode to PNG using the `png` crate.
879
    /// # Errors
880
    ///
881
    /// Returns an error string if PNG encoding fails.
882
775
    pub fn encode_png(&self) -> Result<Vec<u8>, String> {
883
775
        let mut buf = Vec::new();
884
        {
885
775
            let mut encoder = png::Encoder::new(&mut buf, self.width, self.height);
886
775
            encoder.set_color(png::ColorType::Rgba);
887
775
            encoder.set_depth(png::BitDepth::Eight);
888
775
            let mut writer = encoder
889
775
                .write_header()
890
775
                .map_err(|e| format!("PNG header error: {e}"))?;
891
774
            writer
892
774
                .write_image_data(&self.data)
893
774
                .map_err(|e| format!("PNG write error: {e}"))?;
894
        }
895
774
        Ok(buf)
896
775
    }
897

            
898
    /// Decode a PNG byte slice into an `AzulPixmap`.
899
    /// # Errors
900
    ///
901
    /// Returns an error string if `png_bytes` is not a valid PNG.
902
403
    pub fn decode_png(png_bytes: &[u8]) -> Result<Self, String> {
903
403
        let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
904
403
        let mut reader = decoder
905
403
            .read_info()
906
403
            .map_err(|e| format!("PNG decode error: {e}"))?;
907
382
        let buf_size = reader
908
382
            .output_buffer_size()
909
382
            .ok_or_else(|| "PNG: unknown output buffer size".to_string())?;
910
382
        let mut buf = vec![0u8; buf_size];
911
382
        let info = reader
912
382
            .next_frame(&mut buf)
913
382
            .map_err(|e| format!("PNG frame error: {e}"))?;
914
        // Require the stream to reach IEND. next_frame() returns Ok as soon as the
915
        // deflate stream has yielded enough bytes for the declared image, so a TRUNCATED
916
        // PNG still "decodes"; finish() enforces a proper end and rejects the truncation.
917
381
        reader
918
381
            .finish()
919
381
            .map_err(|e| format!("PNG truncated or corrupt: {e}"))?;
920
380
        let width = info.width;
921
380
        let height = info.height;
922

            
923
        // Convert to RGBA if needed
924
380
        let data = match info.color_type {
925
377
            png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
926
            png::ColorType::Rgb => {
927
1
                let mut rgba = Vec::with_capacity((width * height * 4) as usize);
928
2
                for chunk in buf[..info.buffer_size()].chunks_exact(3) {
929
2
                    rgba.push(chunk[0]);
930
2
                    rgba.push(chunk[1]);
931
2
                    rgba.push(chunk[2]);
932
2
                    rgba.push(255);
933
2
                }
934
1
                rgba
935
            }
936
            png::ColorType::Grayscale => {
937
1
                let mut rgba = Vec::with_capacity((width * height * 4) as usize);
938
2
                for &v in &buf[..info.buffer_size()] {
939
2
                    rgba.push(v);
940
2
                    rgba.push(v);
941
2
                    rgba.push(v);
942
2
                    rgba.push(255);
943
2
                }
944
1
                rgba
945
            }
946
1
            other => return Err(format!("Unsupported PNG color type: {other:?}")),
947
        };
948

            
949
379
        Ok(Self {
950
379
            data: data.into(),
951
379
            width,
952
379
            height,
953
379
        })
954
403
    }
955
}
956

            
957
// ============================================================================
958
// Pixel-diff comparison for regression testing
959
// ============================================================================
960

            
961
/// Result of comparing two pixmaps pixel-by-pixel.
962
#[derive(Copy, Debug, Clone)]
963
pub struct PixelDiffResult {
964
    /// Number of pixels that differ beyond the threshold.
965
    pub diff_count: u64,
966
    /// Total number of pixels compared.
967
    pub total_pixels: u64,
968
    /// Maximum per-channel delta found across all pixels.
969
    pub max_delta: u8,
970
    /// Whether dimensions matched.
971
    pub dimensions_match: bool,
972
    /// Width of the reference image.
973
    pub ref_width: u32,
974
    /// Height of the reference image.
975
    pub ref_height: u32,
976
    /// Width of the test image.
977
    pub test_width: u32,
978
    /// Height of the test image.
979
    pub test_height: u32,
980
}
981

            
982
impl PixelDiffResult {
983
    /// True if the images are identical within tolerance.
984
    #[must_use]
985
16
    pub const fn is_match(&self) -> bool {
986
16
        self.dimensions_match && self.diff_count == 0
987
16
    }
988

            
989
    /// Fraction of pixels that differ (0.0 = identical, 1.0 = all different).
990
    #[allow(clippy::cast_precision_loss)] // bounded pixel/coord/colour/glyph cast
991
    #[must_use]
992
13
    pub fn diff_ratio(&self) -> f64 {
993
13
        if self.total_pixels == 0 {
994
3
            0.0
995
        } else {
996
10
            self.diff_count as f64 / self.total_pixels as f64
997
        }
998
13
    }
999
}
/// Compare two pixmaps pixel-by-pixel with a per-channel tolerance.
///
/// `threshold` is the maximum allowed per-channel difference (0 = exact match,
/// 2-3 = anti-aliasing tolerance, 10+ = loose match).
#[allow(clippy::cast_possible_truncation)] // bounded pixel/coord/colour/glyph cast
#[must_use]
136
pub fn pixel_diff(reference: &AzulPixmap, test: &AzulPixmap, threshold: u8) -> PixelDiffResult {
136
    let dimensions_match = reference.width == test.width && reference.height == test.height;
136
    if !dimensions_match {
2
        return PixelDiffResult {
2
            diff_count: 0,
2
            total_pixels: 0,
2
            max_delta: 0,
2
            dimensions_match: false,
2
            ref_width: reference.width,
2
            ref_height: reference.height,
2
            test_width: test.width,
2
            test_height: test.height,
2
        };
134
    }
134
    let total_pixels = u64::from(reference.width) * u64::from(reference.height);
134
    let mut diff_count = 0u64;
134
    let mut max_delta = 0u8;
5248149
    for (ref_chunk, test_chunk) in reference
134
        .data
134
        .chunks_exact(4)
134
        .zip(test.data.chunks_exact(4))
    {
5248149
        let mut pixel_differs = false;
26240745
        for c in 0..4 {
20992596
            let delta = (i16::from(ref_chunk[c]) - i16::from(test_chunk[c])).unsigned_abs() as u8;
20992596
            if delta > threshold {
102660
                pixel_differs = true;
20889936
            }
20992596
            if delta > max_delta {
100
                max_delta = delta;
20992496
            }
        }
5248149
        if pixel_differs {
45877
            diff_count += 1;
5202305
        }
    }
134
    PixelDiffResult {
134
        diff_count,
134
        total_pixels,
134
        max_delta,
134
        dimensions_match: true,
134
        ref_width: reference.width,
134
        ref_height: reference.height,
134
        test_width: test.width,
134
        test_height: test.height,
134
    }
136
}
/// Compare a rendered pixmap against a reference PNG file.
///
/// Returns `Ok(result)` with the diff stats, or `Err` if the reference
/// file cannot be read/decoded.
/// # Errors
///
/// Returns an error string if the images cannot be loaded or compared.
15
pub fn compare_against_reference(
15
    rendered: &AzulPixmap,
15
    reference_png_path: &str,
15
    threshold: u8,
15
) -> Result<PixelDiffResult, String> {
15
    let ref_bytes = std::fs::read(reference_png_path)
15
        .map_err(|e| format!("Cannot read reference image {reference_png_path}: {e}"))?;
3
    let reference = AzulPixmap::decode_png(&ref_bytes)?;
2
    Ok(pixel_diff(&reference, rendered, threshold))
15
}
// ============================================================================
// Simple rect type (replaces tiny_skia::Rect)
// ============================================================================
#[derive(Debug, Clone, Copy)]
pub struct AzRect {
    pub(crate) x: f32,
    pub(crate) y: f32,
    pub(crate) width: f32,
    pub(crate) height: f32,
}
/// Intersect a freshly-pushed clip with the currently-active one.
///
/// `None`
/// means "no clip". An EMPTY intersection clips everything (zero-area rect) —
/// it must NOT degrade to `None`/unclipped, or nested clips could escape
/// their parents.
#[must_use]
1352
pub fn intersect_clips(current: Option<AzRect>, new: Option<AzRect>) -> Option<AzRect> {
1352
    match (current, new) {
503
        (Some(cur), Some(new)) => {
503
            let x0 = cur.x.max(new.x);
503
            let y0 = cur.y.max(new.y);
503
            let x1 = (cur.x + cur.width).min(new.x + new.width);
503
            let y1 = (cur.y + cur.height).min(new.y + new.height);
503
            Some(AzRect {
503
                x: x0,
503
                y: y0,
503
                width: (x1 - x0).max(0.0),
503
                height: (y1 - y0).max(0.0),
503
            })
        }
1
        (Some(cur), None) => Some(cur),
848
        (None, new) => new,
    }
1352
}
impl AzRect {
    /// A zero-area rect used as an explicit "clip away everything" value — distinct from
    /// `None`, which means "no clip / unclipped".
    pub(crate) const DENY_ALL: Self = Self {
        x: 0.0,
        y: 0.0,
        width: 0.0,
        height: 0.0,
    };
20409
    pub(crate) fn from_xywh(x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
20409
        if w <= 0.0
20344
            || h <= 0.0
20339
            || !x.is_finite()
20313
            || !y.is_finite()
20310
            || !w.is_finite()
20296
            || !h.is_finite()
        {
115
            return None;
20294
        }
20294
        Some(Self {
20294
            x,
20294
            y,
20294
            width: w,
20294
            height: h,
20294
        })
20409
    }
    /// Intersect this rect with a clip rect. Returns None if fully clipped.
7903
    pub(crate) fn clip(&self, clip: &Self) -> Option<Self> {
7903
        let x1 = self.x.max(clip.x);
7903
        let y1 = self.y.max(clip.y);
7903
        let x2 = (self.x + self.width).min(clip.x + clip.width);
7903
        let y2 = (self.y + self.height).min(clip.y + clip.height);
7903
        if x2 > x1 && y2 > y1 {
7665
            Some(Self {
7665
                x: x1,
7665
                y: y1,
7665
                width: x2 - x1,
7665
                height: y2 - y1,
7665
            })
        } else {
238
            None
        }
7903
    }
}
// ============================================================================
// AGG helper: fill a PathStorage with a solid color into an AzulPixmap
// ============================================================================
/// Wraps a `VertexSource` and clamps every coordinate to a finite range the AGG
/// rasterizer can handle. A coordinate of ~1e30 (or ±inf) saturates the rasterizer's
/// 24.8 fixed-point conversion to ~`i32::MAX` and makes its scanline sweep run once per
/// row crossed — O(coordinate magnitude), i.e. an effective hang (>1GB RAM, spins
/// forever) on a large-but-legal transform or an SVG numeric attribute. Anything far
/// outside the target contributes no visible pixels, so clamping it to just off-screen
/// is visually equivalent and bounds the work to the visible area.
struct ClampVertexSource<'a> {
    inner: &'a mut dyn VertexSource,
    limit: f64,
}
impl VertexSource for ClampVertexSource<'_> {
8585
    fn rewind(&mut self, path_id: u32) {
8585
        self.inner.rewind(path_id);
8585
    }
237822
    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
237822
        let cmd = self.inner.vertex(x, y);
237822
        *x = if x.is_nan() {
61
            0.0
        } else {
237761
            x.clamp(-self.limit, self.limit)
        };
237822
        *y = if y.is_nan() {
57
            0.0
        } else {
237765
            y.clamp(-self.limit, self.limit)
        };
237822
        cmd
237822
    }
}
/// Coordinate clamp limit for a `w`×`h` target: far larger than any on-screen coordinate
/// (so real geometry is untouched) yet small enough that the rasterizer's work stays
/// bounded. Off-screen geometry gets pinned just outside the target.
8585
fn coord_clamp_limit(w: u32, h: u32) -> f64 {
8585
    (f64::from(w) + f64::from(h)).mul_add(4.0, 4096.0)
8585
}
8216
pub fn agg_fill_path(
8216
    pixmap: &mut AzulPixmap,
8216
    path: &mut dyn VertexSource,
8216
    color: &Rgba8,
8216
    rule: FillingRule,
8216
) {
8216
    agg_fill_path_clipped(pixmap, path, color, rule, None);
8216
}
/// Fill a path with an optional pixel-level clip box.
///
/// When `clip` is `Some`, `RendererBase::clip_box_i()` restricts all
/// scanline output to the clip region.  This handles scroll-frame clips,
/// border-radius is TODO (would need a mask), transforms are handled by
/// transforming the clip box through the inverse transform before setting it.
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // bounded pixel/coord/colour/glyph cast
#[allow(clippy::similar_names)] // clip-box coordinate names (clip_x0/y0/x1/y1)
/// Build an agg path from SVG geometry, mapping every coordinate on the way.
///
/// `mx`/`my` take USER SPACE to device pixels. Mapping the POINTS (rather than
/// pre-transforming the path) is what lets a stroke scale correctly: a
/// transform applied to the finished outline would scale the stroke WIDTH with
/// it, so a 2-unit rule in a 16-unit icon would come out 8px wide in a 64px
/// slot.
///
/// One builder for both consumers - the clip-mask rasteriser and the stroke -
/// because two copies of a curve-by-curve translation drift apart silently:
/// the reader only sees the shape being wrong, never which copy did it.
60
pub fn svg_path_to_agg(
60
    path: &azul_core::svg::SvgMultiPolygon,
60
    mx: &dyn Fn(f32) -> f64,
60
    my: &dyn Fn(f32) -> f64,
60
) -> PathStorage {
    use azul_core::svg::SvgPathElement;
60
    let mut out = PathStorage::new();
60
    for ring in path.rings.as_ref() {
60
        let mut first = true;
120
        for item in ring.items.as_ref() {
120
            match item {
120
                SvgPathElement::Line(l) => {
120
                    if first {
60
                        out.move_to(mx(l.start.x), my(l.start.y));
60
                        first = false;
60
                    }
120
                    out.line_to(mx(l.end.x), my(l.end.y));
                }
                SvgPathElement::QuadraticCurve(q) => {
                    if first {
                        out.move_to(mx(q.start.x), my(q.start.y));
                        first = false;
                    }
                    out.curve3(mx(q.ctrl.x), my(q.ctrl.y), mx(q.end.x), my(q.end.y));
                }
                SvgPathElement::CubicCurve(c) => {
                    if first {
                        out.move_to(mx(c.start.x), my(c.start.y));
                        first = false;
                    }
                    out.curve4(
                        mx(c.ctrl_1.x),
                        my(c.ctrl_1.y),
                        mx(c.ctrl_2.x),
                        my(c.ctrl_2.y),
                        mx(c.end.x),
                        my(c.end.y),
                    );
                }
            }
        }
    }
60
    out
60
}
/// The user-space -> device-pixel mapping for SVG geometry painted into
/// `bounds`.
///
/// With a `view_box` the geometry is in the `<svg>`'s own coordinate system
/// and is SCALED into the box the element ended up with, then translated to
/// it. Without one the geometry is already window-logical and only the scale
/// factor applies. Returns `(scale_x, scale_y, offset_x, offset_y)` in device
/// px, to be used as `(x + off) * scale`-style closures by the caller.
#[must_use]
60
pub fn svg_user_space_mapping(
60
    bounds: &LogicalRect,
60
    view_box: Option<(f32, f32, f32, f32)>,
60
    dpi_factor: f32,
60
) -> (f32, f32, f32, f32) {
60
    match view_box {
60
        Some((min_x, min_y, vb_w, vb_h)) if vb_w > 0.0 && vb_h > 0.0 => (
60
            bounds.size.width * dpi_factor / vb_w,
60
            bounds.size.height * dpi_factor / vb_h,
60
            -min_x,
60
            -min_y,
60
        ),
        _ => (dpi_factor, dpi_factor, 0.0, 0.0),
    }
60
}
8560
pub fn agg_fill_path_clipped(
8560
    pixmap: &mut AzulPixmap,
8560
    path: &mut dyn VertexSource,
8560
    color: &Rgba8,
8560
    rule: FillingRule,
8560
    clip: Option<AzRect>,
8560
) {
8560
    let w = pixmap.width;
8560
    let h = pixmap.height;
8560
    let stride = (w * 4) as i32;
8560
    let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
8560
    let mut pf = PixfmtRgba32::new(&mut ra);
8560
    let mut rb = RendererBase::new(pf);
8560
    if let Some(c) = clip {
        // A degenerate (non-positive-area) clip means "nothing visible". Bail BEFORE
        // building the integer clip box: `(c.x + c.width) as i32 - 1` for width 0 is
        // `c.x - 1`, an INVERTED box that clip_box_i()'s normalize() silently repairs
        // into a small VALID box — so an empty clip used to paint a few pixels.
80
        if c.width <= 0.0 || c.height <= 0.0 {
2
            return;
78
        }
78
        rb.clip_box_i(
78
            c.x as i32,
78
            c.y as i32,
78
            (c.x + c.width) as i32 - 1,
78
            (c.y + c.height) as i32 - 1,
        );
8480
    }
8558
    let mut ras = RasterizerScanlineAa::new();
8558
    ras.filling_rule(rule);
    // Clip GEOMETRY to the target pixmap (intersected with any caller clip) before
    // rasterizing. Without this, the scanline sweep runs once per row the path crosses,
    // so a huge/infinite coordinate — reachable from a large-but-legal CSS transform or
    // SVG attribute — is O(coordinate magnitude), i.e. an effective hang. Clamping the
    // rasterizer's clip box bounds the work to the visible area.
8558
    let (clip_x0, clip_y0, clip_x1, clip_y1) = clip.map_or_else(
8480
        || (0.0, 0.0, f64::from(w), f64::from(h)),
78
        |c| {
78
            (
78
                f64::from(c.x).max(0.0),
78
                f64::from(c.y).max(0.0),
78
                f64::from(c.x + c.width).min(f64::from(w)),
78
                f64::from(c.y + c.height).min(f64::from(h)),
78
            )
78
        },
    );
8558
    ras.clip_box(clip_x0, clip_y0, clip_x1, clip_y1);
    // Clamp coordinates before rasterizing — see ClampVertexSource. This is the actual
    // guard against the huge/infinite-coordinate hang (the rasterizer's own clip_box
    // above does not bound the work for saturated fixed-point coords).
8558
    let mut clamped = ClampVertexSource {
8558
        inner: path,
8558
        limit: coord_clamp_limit(w, h),
8558
    };
8558
    ras.add_path(&mut clamped, 0);
8558
    let mut sl = ScanlineU8::new();
8558
    render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
8560
}
#[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot
                                             // pixel/coord path or to avoid churning call sites for
                                             // a perf-neutral change)
4
fn agg_fill_transformed_path(
4
    pixmap: &mut AzulPixmap,
4
    path: &mut PathStorage,
4
    color: &Rgba8,
4
    rule: FillingRule,
4
    transform: &TransAffine,
4
) {
4
    agg_fill_transformed_path_clipped(pixmap, path, color, rule, transform, None);
4
}
#[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot
                                             // pixel/coord path or to avoid churning call sites for
                                             // a perf-neutral change)
6
fn agg_fill_transformed_path_clipped(
6
    pixmap: &mut AzulPixmap,
6
    path: &mut PathStorage,
6
    color: &Rgba8,
6
    rule: FillingRule,
6
    transform: &TransAffine,
6
    clip: Option<AzRect>,
6
) {
6
    if transform.is_identity(IDENTITY_EPSILON_F64) {
2
        agg_fill_path_clipped(pixmap, path, color, rule, clip);
4
    } else {
4
        let mut transformed = ConvTransform::new(path, *transform);
4
        agg_fill_path_clipped(pixmap, &mut transformed, color, rule, clip);
4
    }
6
}
// ============================================================================
// AGG helper: fill a path with a gradient into an AzulPixmap
// ============================================================================
3
fn agg_fill_gradient<G: GradientFunction>(
3
    pixmap: &mut AzulPixmap,
3
    path: &mut dyn VertexSource,
3
    lut: &GradientLut,
3
    gradient_fn: G,
3
    transform: TransAffine,
3
    d1: f64,
3
    d2: f64,
3
) {
3
    agg_fill_gradient_clipped(pixmap, path, lut, gradient_fn, transform, d1, d2, None);
3
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // bounded pixel/coord/colour/glyph cast
#[allow(clippy::similar_names)] // clip-box / gradient coordinate names (clip_x0/y0/x1/y1, d1/d2)
28
pub fn agg_fill_gradient_clipped<G: GradientFunction>(
28
    pixmap: &mut AzulPixmap,
28
    path: &mut dyn VertexSource,
28
    lut: &GradientLut,
28
    gradient_fn: G,
28
    transform: TransAffine,
28
    d1: f64,
28
    d2: f64,
28
    clip: Option<AzRect>,
28
) {
28
    let w = pixmap.width;
28
    let h = pixmap.height;
28
    let stride = (w * 4) as i32;
28
    let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
28
    let mut pf = PixfmtRgba32::new(&mut ra);
28
    let mut rb = RendererBase::new(pf);
28
    if let Some(c) = clip {
        // Degenerate clip = nothing visible; bail before the inverted-box trap (see
        // agg_fill_path_clipped).
2
        if c.width <= 0.0 || c.height <= 0.0 {
1
            return;
1
        }
1
        rb.clip_box_i(
1
            c.x as i32,
1
            c.y as i32,
1
            (c.x + c.width) as i32 - 1,
1
            (c.y + c.height) as i32 - 1,
        );
26
    }
27
    let mut ras = RasterizerScanlineAa::new();
27
    ras.filling_rule(FillingRule::NonZero);
27
    let (clip_x0, clip_y0, clip_x1, clip_y1) = clip.map_or_else(
26
        || (0.0, 0.0, f64::from(w), f64::from(h)),
1
        |c| {
1
            (
1
                f64::from(c.x).max(0.0),
1
                f64::from(c.y).max(0.0),
1
                f64::from(c.x + c.width).min(f64::from(w)),
1
                f64::from(c.y + c.height).min(f64::from(h)),
1
            )
1
        },
    );
27
    ras.clip_box(clip_x0, clip_y0, clip_x1, clip_y1);
27
    let mut clamped = ClampVertexSource {
27
        inner: path,
27
        limit: coord_clamp_limit(w, h),
27
    };
27
    ras.add_path(&mut clamped, 0);
27
    let mut sl = ScanlineU8::new();
27
    let interp = SpanInterpolatorLinear::new(transform);
    // Clamp gradient distances to a range ALL of SpanGradient's internal i32 math can
    // hold. It rounds d1/d2 to 24.8 fixed-point AND later computes `(d - d1) * color_size`
    // (span_gradient.rs) — with color_size 256 and the extra ×256 of fixed-point, even a
    // few-million value overflows i32. ±8192 keeps every product in range and is far
    // beyond any real gradient extent (callers pass 0..100); pathological NaN/±inf/f64::MAX
    // just pin to the edge.
54
    let clamp_d = |d: f64| {
54
        if d.is_nan() {
3
            0.0
        } else {
51
            d.clamp(-8192.0, 8192.0)
        }
54
    };
27
    let mut sg = SpanGradient::new(interp, gradient_fn, lut, clamp_d(d1), clamp_d(d2));
27
    let mut alloc = SpanAllocator::<Rgba8>::new();
27
    render_scanlines_aa(&mut ras, &mut sl, &mut rb, &mut alloc, &mut sg);
28
}
// ============================================================================
// Gradient helpers
// ============================================================================
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
/// [`blit_buffer`] restricted to a sub-rectangle of the SOURCE buffer.
/// `(sx, sy, w, h)` select the source region; `(dx, dy)` is where that
/// region's top-left lands in `dst`. Exists for the box-shadow ring blit:
/// an outset shadow must not paint inside the border box (CSS), and the
/// interior of a page-sized shadow buffer is the single largest
/// alpha-blend a repaint used to do.
198
pub fn blit_buffer_sub(
198
    dst: &mut AzulPixmap,
198
    src: &[u8],
198
    src_w: u32,
198
    src_h: u32,
198
    sx: u32,
198
    sy: u32,
198
    w: u32,
198
    h: u32,
198
    dx: i32,
198
    dy: i32,
198
) {
198
    let dw = dst.width as i32;
198
    let dh = dst.height as i32;
198
    let x_end = sx.saturating_add(w).min(src_w);
198
    let y_end = sy.saturating_add(h).min(src_h);
5953
    for py in sy..y_end {
5953
        let ty = dy.saturating_add((py - sy) as i32);
5953
        if ty < 0 || ty >= dh {
            continue;
5953
        }
107051
        for px in sx..x_end {
107051
            let tx = dx.saturating_add((px - sx) as i32);
107051
            if tx < 0 || tx >= dw {
                continue;
107051
            }
107051
            let si = ((py * src_w + px) * 4) as usize;
107051
            let di = ((ty as u32 * dst.width + tx as u32) * 4) as usize;
107051
            if si + 3 >= src.len() || di + 3 >= dst.data.len() {
                continue;
107051
            }
107051
            let sa = u32::from(src[si + 3]);
107051
            if sa == 0 {
5768
                continue;
101283
            }
101283
            if sa == 255 {
1335
                dst.data[di] = src[si];
1335
                dst.data[di + 1] = src[si + 1];
1335
                dst.data[di + 2] = src[si + 2];
1335
                dst.data[di + 3] = 255;
99948
            } else {
99948
                let inv_sa = 255 - sa;
99948
                dst.data[di] =
99948
                    ((u32::from(src[si]) + u32::from(dst.data[di]) * inv_sa / 255).min(255)) as u8;
99948
                dst.data[di + 1] = ((u32::from(src[si + 1])
99948
                    + u32::from(dst.data[di + 1]) * inv_sa / 255)
99948
                    .min(255)) as u8;
99948
                dst.data[di + 2] = ((u32::from(src[si + 2])
99948
                    + u32::from(dst.data[di + 2]) * inv_sa / 255)
99948
                    .min(255)) as u8;
99948
                dst.data[di + 3] =
99948
                    ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
99948
            }
        }
    }
198
}
/// Alpha-blend one premultiplied-alpha RGBA buffer onto another at (dx, dy).
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
19
pub fn blit_buffer(dst: &mut AzulPixmap, src: &[u8], src_w: u32, src_h: u32, dx: i32, dy: i32) {
19
    let dw = dst.width as i32;
19
    let dh = dst.height as i32;
241
    for py in 0..src_h as i32 {
        // saturating: see blit_pixmap — a saturated offset just fails the bounds check.
241
        let ty = dy.saturating_add(py);
241
        if ty < 0 || ty >= dh {
7
            continue;
234
        }
44031
        for px in 0..src_w as i32 {
44031
            let tx = dx.saturating_add(px);
44031
            if tx < 0 || tx >= dw {
5
                continue;
44026
            }
44026
            let si = ((py as u32 * src_w + px as u32) * 4) as usize;
44026
            let di = ((ty as u32 * dst.width + tx as u32) * 4) as usize;
44026
            if si + 3 >= src.len() || di + 3 >= dst.data.len() {
18
                continue;
44008
            }
44008
            let sa = u32::from(src[si + 3]);
44008
            if sa == 0 {
42083
                continue;
1925
            }
1925
            if sa == 255 {
253
                dst.data[di] = src[si];
253
                dst.data[di + 1] = src[si + 1];
253
                dst.data[di + 2] = src[si + 2];
253
                dst.data[di + 3] = 255;
1672
            } else {
1672
                // Premultiplied-alpha compositing: src RGB already premultiplied by AGG
1672
                let inv_sa = 255 - sa;
1672
                dst.data[di] =
1672
                    ((u32::from(src[si]) + u32::from(dst.data[di]) * inv_sa / 255).min(255)) as u8;
1672
                dst.data[di + 1] = ((u32::from(src[si + 1])
1672
                    + u32::from(dst.data[di + 1]) * inv_sa / 255)
1672
                    .min(255)) as u8;
1672
                dst.data[di + 2] = ((u32::from(src[si + 2])
1672
                    + u32::from(dst.data[di + 2]) * inv_sa / 255)
1672
                    .min(255)) as u8;
1672
                dst.data[di + 3] =
1672
                    ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
1672
            }
        }
    }
19
}
// ============================================================================
// Image mask clipping
// ============================================================================
/// Take a snapshot of a rectangular region of the pixmap.
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
#[must_use]
202
pub fn snapshot_region(pixmap: &AzulPixmap, x: i32, y: i32, w: u32, h: u32) -> Vec<u8> {
202
    let pw = pixmap.width as i32;
202
    let ph = pixmap.height as i32;
202
    let mut snap = vec![0u8; (w as usize) * (h as usize) * 4];
7949
    for py in 0..h as i32 {
        // saturating: an extreme snapshot origin would overflow a plain `+`.
7949
        let sy = y.saturating_add(py);
7949
        if sy < 0 || sy >= ph {
50
            continue;
7899
        }
720037
        for px in 0..w as i32 {
720037
            let sx = x.saturating_add(px);
720037
            if sx < 0 || sx >= pw {
10
                continue;
720027
            }
720027
            let si = ((sy as u32 * pixmap.width + sx as u32) * 4) as usize;
720027
            let di = ((py as u32 * w + px as u32) * 4) as usize;
720027
            if si + 3 < pixmap.data.len() && di + 3 < snap.len() {
720027
                snap[di] = pixmap.data[si];
720027
                snap[di + 1] = pixmap.data[si + 1];
720027
                snap[di + 2] = pixmap.data[si + 2];
720027
                snap[di + 3] = pixmap.data[si + 3];
720027
            }
        }
    }
202
    snap
202
}
/// Overwrite (direct copy, no alpha blending) a `w`×`h` RGBA region of `dst` at
/// `(x, y)` with the pixels in `src`.
///
/// Out-of-bounds pixels are skipped. This is the inverse of [`snapshot_region`]
/// and is used to write a filtered backdrop copy back into the output buffer for
/// `backdrop-filter`.
// bounded image-dimension / non-negative-loop-index coordinate casts
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
12
pub fn write_region(dst: &mut AzulPixmap, src: &[u8], w: u32, h: u32, x: i32, y: i32) {
12
    let dw = dst.width as i32;
12
    let dh = dst.height as i32;
119
    for py in 0..h as i32 {
        // saturating: an extreme write-region origin would overflow a plain `+`.
119
        let dy = y.saturating_add(py);
119
        if dy < 0 || dy >= dh {
6
            continue;
113
        }
5036
        for px in 0..w as i32 {
5036
            let dx = x.saturating_add(px);
5036
            if dx < 0 || dx >= dw {
6
                continue;
5030
            }
5030
            let si = ((py as u32 * w + px as u32) * 4) as usize;
5030
            let di = ((dy as u32 * dst.width + dx as u32) * 4) as usize;
5030
            if si + 3 < src.len() && di + 3 < dst.data.len() {
5015
                dst.data[di] = src[si];
5015
                dst.data[di + 1] = src[si + 1];
5015
                dst.data[di + 2] = src[si + 2];
5015
                dst.data[di + 3] = src[si + 3];
5015
            }
        }
    }
12
}
#[must_use]
6912
pub fn union_rect(a: &LogicalRect, b: &LogicalRect) -> LogicalRect {
6912
    let x = a.origin.x.min(b.origin.x);
6912
    let y = a.origin.y.min(b.origin.y);
6912
    let right = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
6912
    let bottom = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
6912
    LogicalRect {
6912
        origin: LogicalPosition { x, y },
6912
        size: LogicalSize {
6912
            width: right - x,
6912
            height: bottom - y,
6912
        },
6912
    }
6912
}
#[must_use]
16843
pub fn logical_rect_to_az_rect(bounds: &LogicalRect, dpi_factor: f32) -> Option<AzRect> {
16843
    let x = bounds.origin.x * dpi_factor;
16843
    let y = bounds.origin.y * dpi_factor;
16843
    let width = bounds.size.width * dpi_factor;
16843
    let height = bounds.size.height * dpi_factor;
16843
    AzRect::from_xywh(x, y, width, height)
16843
}
#[cfg(test)]
#[allow(clippy::many_single_char_names, clippy::float_cmp)]
mod autotest_generated {
    use agg_rust::span_gradient::GradientX;
    use super::*;
    // ------------------------------------------------------------------
    // helpers
    // ------------------------------------------------------------------
    const WHITE: [u8; 4] = [255, 255, 255, 255];
    const CLEAR: [u8; 4] = [0, 0, 0, 0];
    fn pm(w: u32, h: u32) -> AzulPixmap {
        AzulPixmap::new(w, h).expect("AzulPixmap::new failed for a valid size")
    }
    // ------------------------------------------------------------------
    // blit_pixmap_projective
    // ------------------------------------------------------------------
    fn opaque_run(dst: &AzulPixmap, row: u32) -> u32 {
        (0..dst.width)
            .filter(|&x| dst.data[((row * dst.width + x) * 4 + 3) as usize] > 128)
            .count() as u32
    }
    #[test]
    fn the_projective_blit_equals_the_affine_blit_for_an_affine_homography() {
        let mut src = pm(6, 4);
        src.fill(200, 40, 90, 255);
        let m = TransAffine::new_custom(1.5, 0.2, -0.1, 0.8, 7.0, 5.0);
        let mut a = pm(24, 16);
        a.fill(0, 0, 0, 0);
        blit_pixmap_affine(&src, &mut a, &m, 0.9);
        let mut b = pm(24, 16);
        b.fill(0, 0, 0, 0);
        // agg: x' = x*sx + y*shx + tx ; y' = x*shy + y*sy + ty
        let h = [m.sx, m.shx, m.tx, m.shy, m.sy, m.ty, 0.0, 0.0, 1.0];
        blit_pixmap_projective(&src, &mut b, &h, 0.9);
        assert_eq!(
            a.data, b.data,
            "with a trivial third row the two blits are the same blit"
        );
    }
    #[test]
    fn a_perspective_homography_foreshortens_into_a_trapezoid() {
        // w = 1 + 0.06 y: rows further down are divided more, so the square
        // comes out narrower (and shorter) towards the bottom — a tilt.
        let mut src = pm(20, 20);
        src.fill(255, 255, 255, 255);
        let mut dst = pm(40, 40);
        dst.fill(0, 0, 0, 0);
        let h = [1.0, 0.0, 10.0, 0.0, 1.0, 10.0, 0.0, 0.06, 1.0];
        blit_pixmap_projective(&src, &mut dst, &h, 1.0);
        let top = opaque_run(&dst, 10);
        let painted_rows: Vec<u32> = (0..40).filter(|&r| opaque_run(&dst, r) > 0).collect();
        let last = *painted_rows.last().expect("something was painted");
        let bottom = opaque_run(&dst, last);
        assert!(
            top > bottom,
            "top row {top} px wide must be wider than the last painted row {bottom} px"
        );
        assert!(top >= 17 && bottom <= 14, "top {top}, bottom {bottom}");
        assert!(
            last < 20,
            "the bottom edge moved UP (y' = 30 / 2.2): last painted row {last}"
        );
        // nothing outside the destination and nothing behind the eye panics
        let mut tiny = pm(2, 2);
        blit_pixmap_projective(
            &src,
            &mut tiny,
            &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 1.0],
            1.0,
        );
        blit_pixmap_projective(&src, &mut tiny, &[0.0; 9], 1.0);
    }
    fn filled(w: u32, h: u32, c: [u8; 4]) -> AzulPixmap {
        let mut p = pm(w, h);
        p.fill(c[0], c[1], c[2], c[3]);
        p
    }
    /// A pixmap whose R channel encodes `y * width + x` — makes shifts/copies verifiable.
    fn marked(w: u32, h: u32) -> AzulPixmap {
        let mut p = pm(w, h);
        for y in 0..h {
            for x in 0..w {
                let idx = u8::try_from(y * w + x).expect("marker fits in u8");
                set(&mut p, x, y, [idx, 0, 0, 255]);
            }
        }
        p
    }
    /// A pixmap with width == height == 0 (only reachable via `resize_reuse`, never `new`).
    fn zero_sized() -> AzulPixmap {
        let mut p = pm(2, 2);
        p.resize_reuse(0, 0, 0, 0, 0, 0);
        p
    }
    /// Number of pixels that are no longer opaque white.
    fn painted_count(p: &AzulPixmap) -> usize {
        p.data()
            .chunks_exact(4)
            .filter(|c| c[0] != 255 || c[1] != 255 || c[2] != 255 || c[3] != 255)
            .count()
    }
    fn get(p: &AzulPixmap, x: u32, y: u32) -> [u8; 4] {
        let i = ((y * p.width() + x) * 4) as usize;
        let d = p.data();
        [d[i], d[i + 1], d[i + 2], d[i + 3]]
    }
    fn set(p: &mut AzulPixmap, x: u32, y: u32, c: [u8; 4]) {
        let i = ((y * p.width() + x) * 4) as usize;
        p.data_mut()[i..i + 4].copy_from_slice(&c);
    }
    fn lrect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
        LogicalRect {
            origin: LogicalPosition { x, y },
            size: LogicalSize {
                width: w,
                height: h,
            },
        }
    }
    fn approx(a: f32, b: f32) -> bool {
        (a - b).abs() < 1e-4
    }
    fn rect_path(x: f64, y: f64, w: f64, h: f64) -> PathStorage {
        let mut p = PathStorage::new();
        p.move_to(x, y);
        p.line_to(x + w, y);
        p.line_to(x + w, y + h);
        p.line_to(x, y + h);
        p.close_polygon(0);
        p
    }
    fn red() -> Rgba8 {
        Rgba8::new(255, 0, 0, 255)
    }
    fn two_stop_lut() -> GradientLut {
        let mut lut = GradientLut::new(256);
        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
        lut.build_lut();
        lut
    }
    /// Encode a PNG with an arbitrary colour type, so `decode_png`'s non-RGBA
    /// branches get a positive (and a negative) control.
    fn encode_custom_png(w: u32, h: u32, ct: png::ColorType, data: &[u8]) -> Vec<u8> {
        let mut buf = Vec::new();
        {
            let mut enc = png::Encoder::new(&mut buf, w, h);
            enc.set_color(ct);
            enc.set_depth(png::BitDepth::Eight);
            let mut wr = enc.write_header().expect("test PNG header");
            wr.write_image_data(data).expect("test PNG data");
        }
        buf
    }
    fn temp_path(tag: &str) -> std::path::PathBuf {
        std::env::temp_dir().join(format!(
            "azul_autotest_pixmap_{}_{tag}.bin",
            std::process::id()
        ))
    }
    // ==================================================================
    // rect_intersection (numeric)
    // ==================================================================
    #[test]
    fn rect_intersection_overlap_is_the_common_area() {
        let a = lrect(0.0, 0.0, 10.0, 10.0);
        let b = lrect(5.0, 5.0, 10.0, 10.0);
        let i = rect_intersection(&a, &b).expect("rects overlap");
        assert!(approx(i.origin.x, 5.0));
        assert!(approx(i.origin.y, 5.0));
        assert!(approx(i.size.width, 5.0));
        assert!(approx(i.size.height, 5.0));
    }
    #[test]
    fn rect_intersection_is_commutative() {
        let a = lrect(-3.0, 2.0, 8.0, 4.0);
        let b = lrect(1.0, 1.0, 9.0, 9.0);
        let ab = rect_intersection(&a, &b).expect("overlap");
        let ba = rect_intersection(&b, &a).expect("overlap");
        assert!(approx(ab.origin.x, ba.origin.x));
        assert!(approx(ab.origin.y, ba.origin.y));
        assert!(approx(ab.size.width, ba.size.width));
        assert!(approx(ab.size.height, ba.size.height));
    }
    #[test]
    fn rect_intersection_zero_sized_rect_is_none() {
        let a = lrect(0.0, 0.0, 0.0, 0.0);
        let b = lrect(0.0, 0.0, 10.0, 10.0);
        // A zero-area rect can never satisfy `x2 > x1 && y2 > y1`.
        assert!(rect_intersection(&a, &b).is_none());
        assert!(rect_intersection(&b, &a).is_none());
    }
    #[test]
    fn rect_intersection_touching_edges_is_none() {
        // a's right edge == b's left edge: zero-width overlap, not an intersection.
        let a = lrect(0.0, 0.0, 5.0, 5.0);
        let b = lrect(5.0, 0.0, 5.0, 5.0);
        assert!(rect_intersection(&a, &b).is_none());
    }
    #[test]
    fn rect_intersection_disjoint_is_none() {
        let a = lrect(0.0, 0.0, 1.0, 1.0);
        let b = lrect(100.0, 100.0, 1.0, 1.0);
        assert!(rect_intersection(&a, &b).is_none());
    }
    #[test]
    fn rect_intersection_negative_coordinates() {
        let a = lrect(-10.0, -10.0, 5.0, 5.0);
        let b = lrect(-7.0, -7.0, 5.0, 5.0);
        let i = rect_intersection(&a, &b).expect("overlap in negative quadrant");
        assert!(approx(i.origin.x, -7.0));
        assert!(approx(i.origin.y, -7.0));
        assert!(approx(i.size.width, 2.0));
        assert!(approx(i.size.height, 2.0));
    }
    #[test]
    fn rect_intersection_result_never_exceeds_either_input() {
        let a = lrect(-1.0, -1.0, 3.0, 100.0);
        let b = lrect(0.0, 0.0, 100.0, 3.0);
        let i = rect_intersection(&a, &b).expect("overlap");
        assert!(i.size.width <= a.size.width && i.size.width <= b.size.width);
        assert!(i.size.height <= a.size.height && i.size.height <= b.size.height);
    }
    #[test]
    fn rect_intersection_f32_max_does_not_panic() {
        let a = lrect(0.0, 0.0, f32::MAX, f32::MAX);
        let b = lrect(0.0, 0.0, f32::MAX, f32::MAX);
        let i = rect_intersection(&a, &b).expect("both cover the same huge area");
        assert!(i.size.width > 0.0 && i.size.height > 0.0);
    }
    #[test]
    fn rect_intersection_nan_does_not_panic_and_stays_finite() {
        let nan = lrect(f32::NAN, f32::NAN, f32::NAN, f32::NAN);
        let ok = lrect(0.0, 0.0, 10.0, 10.0);
        // f32::max/min ignore NaN, so the NaN rect degrades to "the other rect".
        // The only hard requirement: no panic, and no NaN leaking into the result.
        for r in [
            rect_intersection(&nan, &ok),
            rect_intersection(&ok, &nan),
            rect_intersection(&nan, &nan),
        ]
        .into_iter()
        .flatten()
        {
            assert!(!r.size.width.is_nan(), "NaN width leaked out: {r:?}");
            assert!(!r.size.height.is_nan(), "NaN height leaked out: {r:?}");
            assert!(r.size.width >= 0.0 && r.size.height >= 0.0);
        }
    }
    #[test]
    fn rect_intersection_infinite_size_does_not_panic() {
        let inf = lrect(0.0, 0.0, f32::INFINITY, f32::INFINITY);
        let ok = lrect(2.0, 2.0, 4.0, 4.0);
        let i = rect_intersection(&inf, &ok).expect("infinite rect contains the finite one");
        assert!(approx(i.size.width, 4.0));
        assert!(approx(i.size.height, 4.0));
    }
    // ==================================================================
    // union_rect (numeric)
    // ==================================================================
    #[test]
    fn union_rect_covers_both_inputs() {
        let a = lrect(0.0, 0.0, 2.0, 2.0);
        let b = lrect(8.0, 8.0, 2.0, 2.0);
        let u = union_rect(&a, &b);
        assert!(approx(u.origin.x, 0.0));
        assert!(approx(u.origin.y, 0.0));
        assert!(approx(u.size.width, 10.0));
        assert!(approx(u.size.height, 10.0));
    }
    #[test]
    fn union_rect_with_self_is_identity() {
        let a = lrect(3.0, 4.0, 5.0, 6.0);
        let u = union_rect(&a, &a);
        assert!(approx(u.origin.x, 3.0) && approx(u.origin.y, 4.0));
        assert!(approx(u.size.width, 5.0) && approx(u.size.height, 6.0));
    }
    #[test]
    fn union_rect_negative_origins() {
        let a = lrect(-5.0, -5.0, 1.0, 1.0);
        let b = lrect(5.0, 5.0, 1.0, 1.0);
        let u = union_rect(&a, &b);
        assert!(approx(u.origin.x, -5.0));
        assert!(approx(u.size.width, 11.0));
        assert!(approx(u.size.height, 11.0));
    }
    #[test]
    fn union_rect_zero_size_still_extends_bounds() {
        // A zero-area rect at (20, 20) must still push the union's extent out to 20.
        let a = lrect(0.0, 0.0, 1.0, 1.0);
        let b = lrect(20.0, 20.0, 0.0, 0.0);
        let u = union_rect(&a, &b);
        assert!(approx(u.size.width, 20.0));
        assert!(approx(u.size.height, 20.0));
    }
    #[test]
    fn union_rect_never_shrinks_below_its_inputs() {
        let a = lrect(1.0, 1.0, 4.0, 4.0);
        let b = lrect(2.0, 2.0, 1.0, 1.0); // fully inside a
        let u = union_rect(&a, &b);
        assert!(u.size.width >= a.size.width);
        assert!(u.size.height >= a.size.height);
    }
    #[test]
    fn union_rect_infinite_inputs_do_not_panic() {
        let a = lrect(0.0, 0.0, f32::INFINITY, f32::INFINITY);
        let b = lrect(1.0, 1.0, 1.0, 1.0);
        let u = union_rect(&a, &b);
        assert!(u.size.width.is_infinite());
        assert!(u.size.height.is_infinite());
    }
    // ==================================================================
    // logical_rect_to_az_rect + AzRect::from_xywh (constructor / numeric)
    // ==================================================================
    #[test]
    fn logical_rect_to_az_rect_scales_by_dpi() {
        let r = lrect(1.0, 2.0, 3.0, 4.0);
        let a = logical_rect_to_az_rect(&r, 2.0).expect("positive size");
        assert!(approx(a.x, 2.0));
        assert!(approx(a.y, 4.0));
        assert!(approx(a.width, 6.0));
        assert!(approx(a.height, 8.0));
    }
    #[test]
    fn logical_rect_to_az_rect_zero_dpi_is_none() {
        // dpi 0 collapses the rect to zero area -> from_xywh rejects it.
        let r = lrect(1.0, 2.0, 3.0, 4.0);
        assert!(logical_rect_to_az_rect(&r, 0.0).is_none());
    }
    #[test]
    fn logical_rect_to_az_rect_negative_dpi_is_none() {
        let r = lrect(1.0, 2.0, 3.0, 4.0);
        assert!(logical_rect_to_az_rect(&r, -1.0).is_none());
    }
    #[test]
    fn logical_rect_to_az_rect_zero_size_is_none() {
        assert!(logical_rect_to_az_rect(&lrect(0.0, 0.0, 0.0, 10.0), 1.0).is_none());
        assert!(logical_rect_to_az_rect(&lrect(0.0, 0.0, 10.0, 0.0), 1.0).is_none());
    }
    #[test]
    fn logical_rect_to_az_rect_nan_dpi_is_none() {
        let r = lrect(1.0, 2.0, 3.0, 4.0);
        assert!(logical_rect_to_az_rect(&r, f32::NAN).is_none());
    }
    #[test]
    fn logical_rect_to_az_rect_infinite_dpi_is_none() {
        let r = lrect(1.0, 2.0, 3.0, 4.0);
        assert!(logical_rect_to_az_rect(&r, f32::INFINITY).is_none());
        assert!(logical_rect_to_az_rect(&r, f32::NEG_INFINITY).is_none());
    }
    #[test]
    fn logical_rect_to_az_rect_overflow_to_inf_is_none() {
        // f32::MAX * 2.0 saturates to +inf, which from_xywh must reject.
        let r = lrect(0.0, 0.0, f32::MAX, f32::MAX);
        assert!(logical_rect_to_az_rect(&r, 2.0).is_none());
    }
    #[test]
    fn logical_rect_to_az_rect_nan_bounds_is_none() {
        let r = lrect(f32::NAN, 0.0, 10.0, 10.0);
        assert!(logical_rect_to_az_rect(&r, 1.0).is_none());
    }
    #[test]
    fn az_rect_from_xywh_rejects_nonpositive_size() {
        assert!(AzRect::from_xywh(0.0, 0.0, 0.0, 1.0).is_none());
        assert!(AzRect::from_xywh(0.0, 0.0, 1.0, 0.0).is_none());
        assert!(AzRect::from_xywh(0.0, 0.0, -1.0, 1.0).is_none());
        assert!(AzRect::from_xywh(0.0, 0.0, 1.0, -1.0).is_none());
    }
    #[test]
    fn az_rect_from_xywh_rejects_nonfinite() {
        for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
            assert!(AzRect::from_xywh(bad, 0.0, 1.0, 1.0).is_none(), "x={bad}");
            assert!(AzRect::from_xywh(0.0, bad, 1.0, 1.0).is_none(), "y={bad}");
            assert!(AzRect::from_xywh(0.0, 0.0, bad, 1.0).is_none(), "w={bad}");
            assert!(AzRect::from_xywh(0.0, 0.0, 1.0, bad).is_none(), "h={bad}");
        }
    }
    #[test]
    fn az_rect_from_xywh_keeps_fields_verbatim() {
        let r = AzRect::from_xywh(-3.5, 7.25, 1.5, 2.5).expect("valid");
        assert!(approx(r.x, -3.5));
        assert!(approx(r.y, 7.25));
        assert!(approx(r.width, 1.5));
        assert!(approx(r.height, 2.5));
    }
    #[test]
    fn az_rect_from_xywh_smallest_positive_size_is_accepted() {
        assert!(AzRect::from_xywh(0.0, 0.0, f32::MIN_POSITIVE, f32::MIN_POSITIVE).is_some());
    }
    // ==================================================================
    // AzRect::clip (other)
    // ==================================================================
    #[test]
    fn az_rect_clip_contained_returns_self() {
        let inner = AzRect::from_xywh(2.0, 2.0, 2.0, 2.0).expect("valid");
        let outer = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).expect("valid");
        let c = inner.clip(&outer).expect("inner is fully inside outer");
        assert!(approx(c.x, 2.0) && approx(c.y, 2.0));
        assert!(approx(c.width, 2.0) && approx(c.height, 2.0));
    }
    #[test]
    fn az_rect_clip_partial_overlap() {
        let a = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).expect("valid");
        let b = AzRect::from_xywh(5.0, 5.0, 10.0, 10.0).expect("valid");
        let c = a.clip(&b).expect("overlap");
        assert!(approx(c.x, 5.0) && approx(c.width, 5.0));
    }
    #[test]
    fn az_rect_clip_disjoint_is_none() {
        let a = AzRect::from_xywh(0.0, 0.0, 1.0, 1.0).expect("valid");
        let b = AzRect::from_xywh(50.0, 50.0, 1.0, 1.0).expect("valid");
        assert!(a.clip(&b).is_none());
    }
    #[test]
    fn az_rect_clip_touching_edge_is_none() {
        let a = AzRect::from_xywh(0.0, 0.0, 5.0, 5.0).expect("valid");
        let b = AzRect::from_xywh(5.0, 0.0, 5.0, 5.0).expect("valid");
        assert!(a.clip(&b).is_none());
    }
    #[test]
    fn az_rect_clip_huge_rect_does_not_panic() {
        let a = AzRect::from_xywh(0.0, 0.0, f32::MAX, f32::MAX).expect("valid");
        let b = AzRect::from_xywh(1.0, 1.0, 2.0, 2.0).expect("valid");
        let c = a.clip(&b).expect("b is inside a");
        assert!(approx(c.width, 2.0) && approx(c.height, 2.0));
    }
    // ==================================================================
    // intersect_clips (numeric) — the "empty clip must not become unclipped" contract
    // ==================================================================
    #[test]
    fn intersect_clips_none_none_is_none() {
        assert!(intersect_clips(None, None).is_none());
    }
    #[test]
    fn intersect_clips_none_current_adopts_new() {
        let new = AzRect::from_xywh(1.0, 2.0, 3.0, 4.0).expect("valid");
        let r = intersect_clips(None, Some(new)).expect("adopts new");
        assert!(approx(r.x, 1.0) && approx(r.width, 3.0));
    }
    #[test]
    fn intersect_clips_none_new_keeps_current() {
        let cur = AzRect::from_xywh(1.0, 2.0, 3.0, 4.0).expect("valid");
        let r = intersect_clips(Some(cur), None).expect("keeps current");
        assert!(approx(r.x, 1.0) && approx(r.width, 3.0));
    }
    #[test]
    fn intersect_clips_nested_shrinks_to_inner() {
        let outer = AzRect::from_xywh(0.0, 0.0, 10.0, 10.0).expect("valid");
        let inner = AzRect::from_xywh(2.0, 2.0, 3.0, 3.0).expect("valid");
        let r = intersect_clips(Some(outer), Some(inner)).expect("overlap");
        assert!(approx(r.x, 2.0) && approx(r.y, 2.0));
        assert!(approx(r.width, 3.0) && approx(r.height, 3.0));
    }
    #[test]
    fn intersect_clips_never_grows() {
        let a = AzRect::from_xywh(0.0, 0.0, 10.0, 4.0).expect("valid");
        let b = AzRect::from_xywh(3.0, 0.0, 10.0, 10.0).expect("valid");
        let r = intersect_clips(Some(a), Some(b)).expect("overlap");
        assert!(r.width <= a.width && r.width <= b.width);
        assert!(r.height <= a.height && r.height <= b.height);
    }
    #[test]
    fn intersect_clips_disjoint_is_some_zero_area_not_none() {
        // Documented invariant: an EMPTY intersection clips everything and must
        // NOT degrade to `None` (which means "unclipped").
        let a = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
        let b = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
        let r = intersect_clips(Some(a), Some(b)).expect("must stay Some, not unclipped");
        assert!(approx(r.width, 0.0), "empty clip must have zero width");
        assert!(approx(r.height, 0.0), "empty clip must have zero height");
    }
    #[test]
    fn intersect_clips_never_yields_negative_extent() {
        let a = AzRect::from_xywh(0.0, 0.0, 1.0, 1.0).expect("valid");
        let b = AzRect::from_xywh(100.0, 100.0, 1.0, 1.0).expect("valid");
        let r = intersect_clips(Some(a), Some(b)).expect("some");
        assert!(r.width >= 0.0 && r.height >= 0.0);
    }
    #[test]
    fn intersect_clips_with_self_is_idempotent() {
        let a = AzRect::from_xywh(2.0, 3.0, 4.0, 5.0).expect("valid");
        let r = intersect_clips(Some(a), Some(a)).expect("some");
        assert!(approx(r.x, 2.0) && approx(r.y, 3.0));
        assert!(approx(r.width, 4.0) && approx(r.height, 5.0));
    }
    #[test]
    fn intersect_clips_huge_rects_do_not_panic() {
        let a = AzRect::from_xywh(0.0, 0.0, f32::MAX, f32::MAX).expect("valid");
        let b = AzRect::from_xywh(-1.0e30, -1.0e30, f32::MAX, f32::MAX).expect("valid");
        let r = intersect_clips(Some(a), Some(b)).expect("some");
        assert!(!r.width.is_nan() && !r.height.is_nan());
        assert!(r.width >= 0.0 && r.height >= 0.0);
    }
    // ==================================================================
    // AzulPixmap::new (constructor) + getters
    // ==================================================================
    #[test]
    fn pixmap_new_zero_dimension_is_none() {
        assert!(AzulPixmap::new(0, 0).is_none());
        assert!(AzulPixmap::new(0, 16).is_none());
        assert!(AzulPixmap::new(16, 0).is_none());
    }
    #[test]
    fn pixmap_new_invariants_hold() {
        let p = pm(3, 5);
        assert_eq!(p.width(), 3);
        assert_eq!(p.height(), 5);
        assert_eq!(p.data().len(), 3 * 5 * 4);
        assert!(
            p.data().iter().all(|&b| b == 255),
            "new() is documented to be opaque white"
        );
    }
    #[test]
    fn pixmap_new_1x1_is_a_single_white_pixel() {
        let p = pm(1, 1);
        assert_eq!(p.data(), &WHITE);
    }
    #[test]
    fn pixmap_new_absurd_dimensions_return_none_instead_of_aborting() {
        // `new` returns Option, so the documented failure mode for a size that
        // cannot be allocated is None — not a `capacity overflow` panic.
        assert!(AzulPixmap::new(u32::MAX, u32::MAX).is_none());
    }
    #[test]
    fn pixmap_getters_on_zero_sized_instance_do_not_panic() {
        let p = zero_sized();
        assert_eq!(p.width(), 0);
        assert_eq!(p.height(), 0);
        assert!(p.data().is_empty());
    }
    #[test]
    fn data_mut_writes_are_visible_through_data() {
        let mut p = pm(2, 1);
        p.data_mut()[0] = 7;
        assert_eq!(p.data()[0], 7);
        assert_eq!(p.data().len(), 8);
    }
    #[test]
    fn data_mut_on_zero_sized_instance_is_empty_not_panic() {
        let mut p = zero_sized();
        assert!(p.data_mut().is_empty());
    }
    #[test]
    fn clone_pixmap_is_a_deep_copy() {
        let src = filled(2, 2, [1, 2, 3, 4]);
        let mut cloned = src.clone_pixmap();
        assert_eq!(cloned.width(), src.width());
        assert_eq!(cloned.height(), src.height());
        assert_eq!(cloned.data(), src.data());
        set(&mut cloned, 0, 0, [9, 9, 9, 9]);
        assert_eq!(get(&src, 0, 0), [1, 2, 3, 4], "clone must not alias source");
    }
    #[test]
    fn clone_pixmap_of_zero_sized_instance_does_not_panic() {
        let p = zero_sized();
        let c = p.clone_pixmap();
        assert_eq!(c.width(), 0);
        assert!(c.data().is_empty());
    }
    // ==================================================================
    // AzulPixmap::fill (numeric)
    // ==================================================================
    #[test]
    fn fill_sets_every_channel_of_every_pixel() {
        let p = filled(4, 3, [10, 20, 30, 40]);
        for y in 0..3 {
            for x in 0..4 {
                assert_eq!(get(&p, x, y), [10, 20, 30, 40]);
            }
        }
    }
    #[test]
    fn fill_with_u8_extremes() {
        let p = filled(2, 2, [0, 0, 0, 0]);
        assert!(p.data().iter().all(|&b| b == 0));
        let p = filled(2, 2, [255, 255, 255, 255]);
        assert!(p.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn fill_on_zero_sized_pixmap_does_not_panic() {
        let mut p = zero_sized();
        p.fill(1, 2, 3, 4);
        assert!(p.data().is_empty());
    }
    // ==================================================================
    // AzulPixmap::fill_rect (numeric)
    // ==================================================================
    #[test]
    fn fill_rect_fills_exactly_the_requested_box() {
        let mut p = filled(5, 5, CLEAR);
        p.fill_rect(1, 1, 2, 2, 1, 2, 3, 4);
        assert_eq!(get(&p, 1, 1), [1, 2, 3, 4]);
        assert_eq!(get(&p, 2, 2), [1, 2, 3, 4]);
        assert_eq!(get(&p, 0, 0), CLEAR, "outside the box must be untouched");
        assert_eq!(get(&p, 3, 3), CLEAR, "x1/y1 are exclusive");
    }
    #[test]
    fn fill_rect_zero_size_is_a_noop() {
        let mut p = filled(4, 4, CLEAR);
        p.fill_rect(1, 1, 0, 0, 255, 0, 0, 255);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn fill_rect_negative_origin_clips_to_the_pixmap() {
        let mut p = filled(4, 4, CLEAR);
        p.fill_rect(-2, -2, 4, 4, 9, 9, 9, 9);
        // Only the on-screen quadrant (0..2, 0..2) is written.
        assert_eq!(get(&p, 0, 0), [9, 9, 9, 9]);
        assert_eq!(get(&p, 1, 1), [9, 9, 9, 9]);
        assert_eq!(get(&p, 2, 2), CLEAR);
    }
    #[test]
    fn fill_rect_fully_offscreen_is_a_noop() {
        let mut p = filled(4, 4, CLEAR);
        p.fill_rect(100, 100, 10, 10, 255, 0, 0, 255);
        p.fill_rect(-100, -100, 10, 10, 255, 0, 0, 255);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn fill_rect_negative_width_does_not_panic() {
        // A negative width must be treated as an empty rect (a no-op), not turned
        // into a reversed slice range.
        let mut p = filled(8, 8, CLEAR);
        p.fill_rect(3, 0, -5, 2, 255, 0, 0, 255);
        assert!(
            p.data().iter().all(|&b| b == 0),
            "a negative-width rect must paint nothing"
        );
    }
    #[test]
    fn fill_rect_negative_height_is_a_noop() {
        let mut p = filled(8, 8, CLEAR);
        p.fill_rect(0, 3, 2, -5, 255, 0, 0, 255);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn fill_rect_i32_extremes_do_not_panic() {
        let mut p = filled(4, 4, CLEAR);
        p.fill_rect(i32::MIN, i32::MIN, i32::MAX, i32::MAX, 1, 1, 1, 1);
        p.fill_rect(i32::MAX, i32::MAX, i32::MAX, i32::MAX, 2, 2, 2, 2);
        p.fill_rect(0, 0, i32::MAX, i32::MAX, 3, 3, 3, 3);
        // The last call saturates to the full pixmap.
        assert_eq!(get(&p, 0, 0), [3, 3, 3, 3]);
        assert_eq!(get(&p, 3, 3), [3, 3, 3, 3]);
    }
    #[test]
    fn fill_rect_on_zero_sized_pixmap_does_not_panic() {
        let mut p = zero_sized();
        p.fill_rect(0, 0, 10, 10, 1, 2, 3, 4);
        assert!(p.data().is_empty());
    }
    // ==================================================================
    // resize_grow_only (numeric)
    // ==================================================================
    #[test]
    fn resize_grow_only_rejects_shrinking() {
        let mut p = filled(4, 4, [1, 2, 3, 4]);
        assert!(p.resize_grow_only(2, 4, 0, 0, 0, 0).is_none());
        assert!(p.resize_grow_only(4, 2, 0, 0, 0, 0).is_none());
        assert!(p.resize_grow_only(2, 2, 0, 0, 0, 0).is_none());
        // Mixed grow/shrink is still a rejection.
        assert!(p.resize_grow_only(8, 2, 0, 0, 0, 0).is_none());
        assert_eq!(p.width(), 4, "a rejected resize must not mutate");
        assert_eq!(p.height(), 4);
        assert_eq!(p.data().len(), 4 * 4 * 4);
    }
    #[test]
    fn resize_grow_only_same_dimensions_is_a_noop_some() {
        let mut p = filled(3, 3, [7, 7, 7, 7]);
        assert!(p.resize_grow_only(3, 3, 0, 0, 0, 0).is_some());
        assert_eq!(p.width(), 3);
        assert!(p.data().iter().all(|&b| b == 7));
    }
    #[test]
    fn resize_grow_only_preserves_topleft_and_fills_the_new_strips() {
        let mut p = marked(2, 2);
        p.resize_grow_only(4, 4, 9, 8, 7, 6)
            .expect("growing is allowed");
        assert_eq!(p.width(), 4);
        assert_eq!(p.height(), 4);
        assert_eq!(p.data().len(), 4 * 4 * 4);
        // old content stays in the top-left
        assert_eq!(get(&p, 0, 0), [0, 0, 0, 255]);
        assert_eq!(get(&p, 1, 0), [1, 0, 0, 255]);
        assert_eq!(get(&p, 0, 1), [2, 0, 0, 255]);
        assert_eq!(get(&p, 1, 1), [3, 0, 0, 255]);
        // new right/bottom strips carry the fill colour
        assert_eq!(get(&p, 3, 0), [9, 8, 7, 6]);
        assert_eq!(get(&p, 0, 3), [9, 8, 7, 6]);
        assert_eq!(get(&p, 3, 3), [9, 8, 7, 6]);
    }
    #[test]
    fn resize_grow_only_grow_one_axis_only() {
        let mut p = marked(2, 2);
        p.resize_grow_only(2, 4, 1, 1, 1, 1)
            .expect("height-only growth");
        assert_eq!(p.width(), 2);
        assert_eq!(p.height(), 4);
        assert_eq!(get(&p, 1, 1), [3, 0, 0, 255]);
        assert_eq!(get(&p, 0, 3), [1, 1, 1, 1]);
    }
    #[test]
    fn resize_grow_only_from_zero_sized_pixmap_does_not_panic() {
        let mut p = zero_sized();
        p.resize_grow_only(2, 2, 5, 5, 5, 5)
            .expect("0x0 -> 2x2 is a growth");
        assert_eq!(p.width(), 2);
        assert_eq!(p.data().len(), 16);
        assert_eq!(get(&p, 0, 0), [5, 5, 5, 5]);
    }
    // ==================================================================
    // resize_reuse (numeric)
    // ==================================================================
    #[test]
    fn resize_reuse_same_dimensions_is_a_noop() {
        let mut p = filled(3, 3, [4, 4, 4, 4]);
        p.resize_reuse(3, 3, 0, 0, 0, 0);
        assert!(p.data().iter().all(|&b| b == 4));
    }
    #[test]
    fn resize_reuse_grow_preserves_overlap_and_fills_the_rest() {
        let mut p = marked(2, 2);
        p.resize_reuse(4, 3, 1, 2, 3, 4);
        assert_eq!(p.width(), 4);
        assert_eq!(p.height(), 3);
        assert_eq!(p.data().len(), 4 * 3 * 4);
        assert_eq!(get(&p, 0, 0), [0, 0, 0, 255]);
        assert_eq!(get(&p, 1, 1), [3, 0, 0, 255]);
        assert_eq!(get(&p, 3, 0), [1, 2, 3, 4]);
        assert_eq!(get(&p, 0, 2), [1, 2, 3, 4]);
    }
    #[test]
    fn resize_reuse_shrink_crops_to_the_topleft() {
        let mut p = marked(4, 4);
        p.resize_reuse(2, 2, 0, 0, 0, 0);
        assert_eq!(p.width(), 2);
        assert_eq!(p.height(), 2);
        assert_eq!(p.data().len(), 2 * 2 * 4);
        // markers were y*4 + x on the old 4x4 grid
        assert_eq!(get(&p, 0, 0), [0, 0, 0, 255]);
        assert_eq!(get(&p, 1, 0), [1, 0, 0, 255]);
        assert_eq!(get(&p, 0, 1), [4, 0, 0, 255]);
        assert_eq!(get(&p, 1, 1), [5, 0, 0, 255]);
    }
    #[test]
    fn resize_reuse_shrink_width_only_keeps_rows_aligned() {
        let mut p = marked(4, 2);
        p.resize_reuse(2, 2, 0, 0, 0, 0);
        assert_eq!(get(&p, 0, 1), [4, 0, 0, 255], "row 1 must not be smeared");
        assert_eq!(get(&p, 1, 1), [5, 0, 0, 255]);
    }
    #[test]
    fn resize_reuse_to_zero_yields_an_empty_buffer() {
        let mut p = filled(4, 4, [1, 1, 1, 1]);
        p.resize_reuse(0, 0, 2, 2, 2, 2);
        assert_eq!(p.width(), 0);
        assert_eq!(p.height(), 0);
        assert!(p.data().is_empty());
    }
    #[test]
    fn resize_reuse_from_zero_sized_pixmap_does_not_panic() {
        let mut p = zero_sized();
        p.resize_reuse(2, 2, 3, 3, 3, 3);
        assert_eq!(p.width(), 2);
        assert_eq!(get(&p, 0, 0), [3, 3, 3, 3]);
    }
    #[test]
    fn resize_reuse_data_len_always_matches_the_new_dimensions() {
        let mut p = marked(3, 3);
        for (w, h) in [(1u32, 1u32), (5, 2), (2, 5), (7, 7), (1, 9)] {
            p.resize_reuse(w, h, 0, 0, 0, 0);
            assert_eq!(p.width(), w);
            assert_eq!(p.height(), h);
            assert_eq!(p.data().len(), (w as usize) * (h as usize) * 4);
        }
    }
    // ==================================================================
    // encode_png / decode_png (round-trip + parser)
    // ==================================================================
    #[test]
    fn png_round_trip_preserves_dimensions_and_pixels() {
        let src = marked(5, 3);
        let bytes = src.encode_png().expect("encode");
        let back = AzulPixmap::decode_png(&bytes).expect("decode");
        assert_eq!(back.width(), src.width());
        assert_eq!(back.height(), src.height());
        assert_eq!(back.data(), src.data());
    }
    #[test]
    fn png_round_trip_1x1() {
        let mut src = pm(1, 1);
        set(&mut src, 0, 0, [1, 2, 3, 4]);
        let bytes = src.encode_png().expect("encode");
        let back = AzulPixmap::decode_png(&bytes).expect("decode");
        assert_eq!(back.data(), &[1, 2, 3, 4]);
    }
    #[test]
    fn png_round_trip_preserves_full_alpha_range() {
        let mut src = pm(4, 1);
        set(&mut src, 0, 0, [0, 0, 0, 0]);
        set(&mut src, 1, 0, [255, 255, 255, 255]);
        set(&mut src, 2, 0, [255, 0, 0, 1]);
        set(&mut src, 3, 0, [0, 255, 0, 254]);
        let bytes = src.encode_png().expect("encode");
        let back = AzulPixmap::decode_png(&bytes).expect("decode");
        assert_eq!(back.data(), src.data(), "PNG must not premultiply alpha");
    }
    #[test]
    fn encode_png_starts_with_the_png_signature() {
        let bytes = pm(2, 2).encode_png().expect("encode");
        assert_eq!(
            &bytes[..8],
            &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]
        );
    }
    #[test]
    fn encode_png_of_a_zero_sized_pixmap_is_err_not_panic() {
        let p = zero_sized();
        let e = p.encode_png().expect_err("PNG forbids zero dimensions");
        assert!(e.contains("PNG header error"), "unexpected message: {e}");
    }
    #[test]
    fn decode_png_empty_input_is_err() {
        assert!(AzulPixmap::decode_png(&[]).is_err());
    }
    #[test]
    fn decode_png_whitespace_only_is_err() {
        assert!(AzulPixmap::decode_png(b"   ").is_err());
        assert!(AzulPixmap::decode_png(b"\t\n\r\n").is_err());
    }
    #[test]
    fn decode_png_garbage_is_err() {
        assert!(AzulPixmap::decode_png(b"not a png at all").is_err());
        assert!(AzulPixmap::decode_png(&[0x00, 0x01, 0x02, 0x03]).is_err());
    }
    #[test]
    fn decode_png_invalid_utf8_bytes_are_err() {
        assert!(AzulPixmap::decode_png(&[0xFF, 0xFE, 0x00]).is_err());
        assert!(AzulPixmap::decode_png(&[0xC0, 0x80, 0xED, 0xA0, 0x80]).is_err());
    }
    #[test]
    fn decode_png_unicode_text_is_err() {
        assert!(AzulPixmap::decode_png("\u{1F600} héllo n\u{0303}".as_bytes()).is_err());
    }
    #[test]
    fn decode_png_boundary_number_strings_are_err() {
        for s in [
            "0",
            "-0",
            "9223372036854775807",
            "NaN",
            "inf",
            "-inf",
            "1e999",
        ] {
            assert!(
                AzulPixmap::decode_png(s.as_bytes()).is_err(),
                "{s:?} is not a PNG"
            );
        }
    }
    #[test]
    fn decode_png_extremely_long_garbage_is_err_and_terminates() {
        let junk = vec![0x41u8; 1_000_000];
        assert!(AzulPixmap::decode_png(&junk).is_err());
    }
    #[test]
    fn decode_png_deeply_nested_brackets_is_err() {
        let mut nested = vec![b'['; 10_000];
        nested.extend(std::iter::repeat_n(b']', 10_000));
        assert!(AzulPixmap::decode_png(&nested).is_err());
    }
    #[test]
    fn decode_png_signature_without_chunks_is_err() {
        let sig = [0x89u8, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
        assert!(AzulPixmap::decode_png(&sig).is_err());
    }
    #[test]
    fn decode_png_truncated_valid_png_is_err() {
        let full = pm(4, 4).encode_png().expect("encode");
        for cut in [9, full.len() / 2, full.len() - 1] {
            assert!(
                AzulPixmap::decode_png(&full[..cut]).is_err(),
                "a PNG truncated to {cut} bytes must not decode"
            );
        }
    }
    #[test]
    fn decode_png_leading_junk_is_rejected() {
        let full = pm(2, 2).encode_png().expect("encode");
        let mut with_junk = b"garbage".to_vec();
        with_junk.extend_from_slice(&full);
        assert!(
            AzulPixmap::decode_png(&with_junk).is_err(),
            "the decoder must not scan forward for a signature"
        );
    }
    #[test]
    fn decode_png_trailing_junk_after_iend_still_decodes() {
        let src = marked(2, 2);
        let mut bytes = src.encode_png().expect("encode");
        bytes.extend_from_slice(b"garbage;after;iend");
        let back = AzulPixmap::decode_png(&bytes).expect("bytes past IEND are outside the stream");
        assert_eq!(back.data(), src.data());
    }
    #[test]
    fn decode_png_rgb_expands_to_opaque_rgba() {
        let bytes = encode_custom_png(2, 1, png::ColorType::Rgb, &[10, 20, 30, 40, 50, 60]);
        let p = AzulPixmap::decode_png(&bytes).expect("RGB is supported");
        assert_eq!(p.width(), 2);
        assert_eq!(p.height(), 1);
        assert_eq!(p.data(), &[10, 20, 30, 255, 40, 50, 60, 255]);
    }
    #[test]
    fn decode_png_grayscale_expands_to_rgba() {
        let bytes = encode_custom_png(2, 1, png::ColorType::Grayscale, &[7, 9]);
        let p = AzulPixmap::decode_png(&bytes).expect("grayscale is supported");
        assert_eq!(p.data(), &[7, 7, 7, 255, 9, 9, 9, 255]);
    }
    #[test]
    fn decode_png_unsupported_color_type_is_err_not_panic() {
        let bytes = encode_custom_png(1, 1, png::ColorType::GrayscaleAlpha, &[7, 128]);
        let e = AzulPixmap::decode_png(&bytes).expect_err("gray+alpha is not handled");
        assert!(
            e.contains("Unsupported PNG color type"),
            "unexpected message: {e}"
        );
    }
    // ==================================================================
    // PixelDiffResult (predicate + getter)
    // ==================================================================
    fn diff_result(diff_count: u64, total_pixels: u64, dimensions_match: bool) -> PixelDiffResult {
        PixelDiffResult {
            diff_count,
            total_pixels,
            max_delta: 0,
            dimensions_match,
            ref_width: 1,
            ref_height: 1,
            test_width: 1,
            test_height: 1,
        }
    }
    #[test]
    fn is_match_is_true_only_when_dimensions_match_and_no_pixel_differs() {
        assert!(diff_result(0, 100, true).is_match());
        assert!(!diff_result(1, 100, true).is_match());
        assert!(!diff_result(0, 100, false).is_match());
        assert!(!diff_result(u64::MAX, 100, true).is_match());
    }
    #[test]
    fn is_match_on_an_empty_comparison_is_deterministic() {
        // Zero pixels compared but dimensions agree -> vacuously a match.
        assert!(diff_result(0, 0, true).is_match());
        assert!(!diff_result(0, 0, false).is_match());
    }
    #[test]
    fn diff_ratio_of_zero_total_is_zero_not_nan() {
        let r = diff_result(0, 0, true).diff_ratio();
        assert!(r.is_finite() && r == 0.0, "0/0 must not become NaN");
    }
    #[test]
    fn diff_ratio_basic_values() {
        assert!((diff_result(0, 100, true).diff_ratio() - 0.0).abs() < 1e-12);
        assert!((diff_result(50, 100, true).diff_ratio() - 0.5).abs() < 1e-12);
        assert!((diff_result(100, 100, true).diff_ratio() - 1.0).abs() < 1e-12);
    }
    #[test]
    fn diff_ratio_at_u64_max_stays_finite() {
        let r = diff_result(u64::MAX, u64::MAX, true).diff_ratio();
        assert!(r.is_finite(), "u64::MAX/u64::MAX must not overflow to inf");
        assert!((r - 1.0).abs() < 1e-9);
    }
    #[test]
    fn diff_ratio_on_a_dimension_mismatch_is_zero_yet_not_a_match() {
        // A caller that only checks diff_ratio() would think a size mismatch is
        // a perfect match — pin the (surprising but documented) behaviour down.
        let r = diff_result(0, 0, false);
        assert!((r.diff_ratio() - 0.0).abs() < 1e-12);
        assert!(!r.is_match());
    }
    // ==================================================================
    // pixel_diff (numeric)
    // ==================================================================
    #[test]
    fn pixel_diff_identical_images_match() {
        let a = filled(4, 4, [1, 2, 3, 4]);
        let b = filled(4, 4, [1, 2, 3, 4]);
        let r = pixel_diff(&a, &b, 0);
        assert!(r.is_match());
        assert_eq!(r.diff_count, 0);
        assert_eq!(r.max_delta, 0);
        assert_eq!(r.total_pixels, 16);
    }
    #[test]
    fn pixel_diff_threshold_is_exclusive_at_the_boundary() {
        let a = filled(2, 2, [10, 10, 10, 255]);
        let b = filled(2, 2, [13, 10, 10, 255]); // delta of exactly 3 on R
        assert!(
            pixel_diff(&a, &b, 3).is_match(),
            "delta == threshold is within tolerance"
        );
        assert!(
            !pixel_diff(&a, &b, 2).is_match(),
            "delta > threshold must be reported"
        );
        assert_eq!(pixel_diff(&a, &b, 2).diff_count, 4);
        assert_eq!(
            pixel_diff(&a, &b, 3).max_delta,
            3,
            "max_delta ignores the threshold"
        );
    }
    #[test]
    fn pixel_diff_threshold_zero_catches_a_single_bit() {
        let a = filled(2, 2, [0, 0, 0, 0]);
        let mut b = filled(2, 2, [0, 0, 0, 0]);
        set(&mut b, 1, 1, [0, 0, 0, 1]);
        let r = pixel_diff(&a, &b, 0);
        assert!(!r.is_match());
        assert_eq!(r.diff_count, 1);
        assert_eq!(r.max_delta, 1);
    }
    #[test]
    fn pixel_diff_threshold_255_always_matches() {
        // The largest possible per-channel delta is 255, and the check is `>`.
        let a = filled(3, 3, [0, 0, 0, 0]);
        let b = filled(3, 3, [255, 255, 255, 255]);
        let r = pixel_diff(&a, &b, 255);
        assert!(r.is_match(), "threshold 255 tolerates every possible delta");
        assert_eq!(r.diff_count, 0);
        assert_eq!(r.max_delta, 255);
    }
    #[test]
    fn pixel_diff_max_delta_is_unsigned_and_direction_independent() {
        let a = filled(1, 1, [0, 0, 0, 0]);
        let b = filled(1, 1, [255, 0, 0, 0]);
        assert_eq!(pixel_diff(&a, &b, 0).max_delta, 255, "no wraparound");
        assert_eq!(
            pixel_diff(&b, &a, 0).max_delta,
            255,
            "reference/test order must not change |delta|"
        );
    }
    #[test]
    fn pixel_diff_all_pixels_different_ratio_is_one() {
        let a = filled(4, 4, [0, 0, 0, 0]);
        let b = filled(4, 4, [255, 255, 255, 255]);
        let r = pixel_diff(&a, &b, 0);
        assert_eq!(r.diff_count, r.total_pixels);
        assert!((r.diff_ratio() - 1.0).abs() < 1e-12);
    }
    #[test]
    fn pixel_diff_dimension_mismatch_reports_both_sizes_and_no_match() {
        let a = filled(4, 4, CLEAR);
        let b = filled(2, 8, CLEAR);
        let r = pixel_diff(&a, &b, 0);
        assert!(!r.is_match());
        assert!(!r.dimensions_match);
        assert_eq!(r.total_pixels, 0);
        assert_eq!(r.diff_count, 0);
        assert_eq!((r.ref_width, r.ref_height), (4, 4));
        assert_eq!((r.test_width, r.test_height), (2, 8));
    }
    #[test]
    fn pixel_diff_ratio_is_always_within_0_and_1() {
        let a = marked(4, 4);
        let b = filled(4, 4, [0, 0, 0, 0]);
        for t in [0u8, 1, 127, 254, 255] {
            let r = pixel_diff(&a, &b, t);
            let ratio = r.diff_ratio();
            assert!(
                (0.0..=1.0).contains(&ratio),
                "ratio {ratio} out of range at t={t}"
            );
            assert!(r.diff_count <= r.total_pixels);
        }
    }
    #[test]
    fn pixel_diff_on_zero_sized_pixmaps_does_not_panic() {
        let a = zero_sized();
        let b = zero_sized();
        let r = pixel_diff(&a, &b, 0);
        assert!(r.is_match());
        assert_eq!(r.total_pixels, 0);
        assert!((r.diff_ratio() - 0.0).abs() < 1e-12);
    }
    // ==================================================================
    // compare_against_reference (parser / IO)
    // ==================================================================
    #[test]
    fn compare_against_reference_missing_file_is_err() {
        let p = pm(2, 2);
        let e = compare_against_reference(&p, "/nonexistent/azul/does_not_exist.png", 0)
            .expect_err("missing file");
        assert!(e.contains("Cannot read reference image"), "got: {e}");
    }
    #[test]
    fn compare_against_reference_empty_path_is_err() {
        let p = pm(2, 2);
        assert!(compare_against_reference(&p, "", 0).is_err());
    }
    #[test]
    fn compare_against_reference_whitespace_path_is_err() {
        let p = pm(2, 2);
        assert!(compare_against_reference(&p, "   ", 0).is_err());
        assert!(compare_against_reference(&p, "\t\n", 0).is_err());
    }
    #[test]
    fn compare_against_reference_nul_byte_path_is_err_not_panic() {
        let p = pm(2, 2);
        assert!(compare_against_reference(&p, "bad\0path.png", 0).is_err());
    }
    #[test]
    fn compare_against_reference_unicode_path_is_err() {
        let p = pm(2, 2);
        assert!(compare_against_reference(&p, "/tmp/\u{1F600}/nope.png", 0).is_err());
    }
    #[test]
    fn compare_against_reference_extremely_long_path_is_err_not_panic() {
        let p = pm(2, 2);
        let long = format!("/tmp/{}.png", "a".repeat(100_000));
        assert!(compare_against_reference(&p, &long, 0).is_err());
    }
    #[test]
    fn compare_against_reference_boundary_number_paths_are_err() {
        let p = pm(2, 2);
        for s in ["0", "-0", "NaN", "inf", "9223372036854775807"] {
            assert!(compare_against_reference(&p, s, 0).is_err(), "{s:?}");
        }
    }
    #[test]
    fn compare_against_reference_non_png_file_is_err() {
        let path = temp_path("not_a_png");
        std::fs::write(&path, b"definitely not a png").expect("write temp file");
        let p = pm(2, 2);
        let res = compare_against_reference(&p, path.to_str().expect("utf8 path"), 0);
        let _ = std::fs::remove_file(&path);
        let e = res.expect_err("a non-PNG file must not decode");
        assert!(e.contains("PNG decode error"), "got: {e}");
    }
    #[test]
    fn compare_against_reference_valid_png_matches_itself() {
        let src = marked(3, 2);
        let path = temp_path("valid_ref");
        std::fs::write(&path, src.encode_png().expect("encode")).expect("write temp file");
        let res = compare_against_reference(&src, path.to_str().expect("utf8 path"), 0);
        let _ = std::fs::remove_file(&path);
        let r = res.expect("a freshly written reference must decode");
        assert!(r.is_match(), "an image must match its own PNG: {r:?}");
        assert_eq!(r.total_pixels, 6);
    }
    #[test]
    fn compare_against_reference_size_mismatch_is_ok_but_not_a_match() {
        let src = marked(3, 2);
        let path = temp_path("size_mismatch");
        std::fs::write(&path, src.encode_png().expect("encode")).expect("write temp file");
        let other = pm(4, 4);
        let res = compare_against_reference(&other, path.to_str().expect("utf8 path"), 0);
        let _ = std::fs::remove_file(&path);
        let r = res.expect("decoding succeeds; only the comparison fails");
        assert!(!r.is_match());
        assert!(!r.dimensions_match);
    }
    // ==================================================================
    // blit_pixmap (numeric)
    // ==================================================================
    #[test]
    fn blit_pixmap_full_opacity_opaque_src_overwrites_dst() {
        let src = filled(2, 2, [10, 20, 30, 255]);
        let mut dst = filled(4, 4, CLEAR);
        blit_pixmap(&src, &mut dst, 1, 1, 1.0);
        assert_eq!(get(&dst, 1, 1), [10, 20, 30, 255]);
        assert_eq!(get(&dst, 2, 2), [10, 20, 30, 255]);
        assert_eq!(get(&dst, 0, 0), CLEAR);
        assert_eq!(get(&dst, 3, 3), CLEAR);
    }
    #[test]
    fn blit_pixmap_zero_opacity_leaves_dst_untouched() {
        let src = filled(2, 2, [10, 20, 30, 255]);
        let mut dst = filled(2, 2, WHITE);
        blit_pixmap(&src, &mut dst, 0, 0, 0.0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn blit_pixmap_transparent_src_leaves_dst_untouched() {
        let src = filled(2, 2, [10, 20, 30, 0]);
        let mut dst = filled(2, 2, WHITE);
        blit_pixmap(&src, &mut dst, 0, 0, 1.0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn blit_pixmap_nan_opacity_does_not_panic_and_blits_nothing() {
        // (NaN * 255).clamp(..) is NaN, and `NaN as u32` saturates to 0.
        let src = filled(2, 2, [10, 20, 30, 255]);
        let mut dst = filled(2, 2, WHITE);
        blit_pixmap(&src, &mut dst, 0, 0, f32::NAN);
        assert!(
            dst.data().iter().all(|&b| b == 255),
            "a NaN opacity must not paint anything"
        );
    }
    #[test]
    fn blit_pixmap_infinite_opacity_clamps_to_fully_opaque() {
        let src = filled(1, 1, [10, 20, 30, 255]);
        let mut dst = filled(1, 1, WHITE);
        blit_pixmap(&src, &mut dst, 0, 0, f32::INFINITY);
        assert_eq!(get(&dst, 0, 0), [10, 20, 30, 255]);
    }
    #[test]
    fn blit_pixmap_negative_infinite_opacity_clamps_to_transparent() {
        let src = filled(1, 1, [10, 20, 30, 255]);
        let mut dst = filled(1, 1, WHITE);
        blit_pixmap(&src, &mut dst, 0, 0, f32::NEG_INFINITY);
        assert_eq!(get(&dst, 0, 0), WHITE);
    }
    #[test]
    fn blit_pixmap_opacity_above_one_clamps_to_one() {
        let src = filled(1, 1, [10, 20, 30, 255]);
        let mut a = filled(1, 1, CLEAR);
        let mut b = filled(1, 1, CLEAR);
        blit_pixmap(&src, &mut a, 0, 0, 1.0);
        blit_pixmap(&src, &mut b, 0, 0, 1.0e30);
        assert_eq!(a.data(), b.data(), "opacity is clamped to [0, 1]");
    }
    #[test]
    fn blit_pixmap_half_alpha_blend_is_exact() {
        let src = filled(1, 1, [200, 100, 50, 128]);
        let mut dst = filled(1, 1, CLEAR);
        blit_pixmap(&src, &mut dst, 0, 0, 1.0);
        // sa = 128, inv = 127, dst starts at 0 => (c * 128) / 255
        assert_eq!(get(&dst, 0, 0), [100, 50, 25, 128]);
    }
    #[test]
    fn blit_pixmap_negative_position_clips_to_dst() {
        let src = marked(2, 2);
        let mut dst = filled(4, 4, CLEAR);
        blit_pixmap(&src, &mut dst, -1, -1, 1.0);
        // Only src(1,1) (marker 3) lands on dst(0,0).
        assert_eq!(get(&dst, 0, 0), [3, 0, 0, 255]);
        assert_eq!(get(&dst, 1, 1), CLEAR);
    }
    #[test]
    fn blit_pixmap_fully_offscreen_is_a_noop() {
        let src = filled(2, 2, [1, 2, 3, 255]);
        let mut dst = filled(4, 4, CLEAR);
        blit_pixmap(&src, &mut dst, 100, 100, 1.0);
        blit_pixmap(&src, &mut dst, -100, -100, 1.0);
        assert!(dst.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn blit_pixmap_into_smaller_dst_clips_instead_of_panicking() {
        let src = filled(8, 8, [1, 2, 3, 255]);
        let mut dst = filled(2, 2, CLEAR);
        blit_pixmap(&src, &mut dst, 0, 0, 1.0);
        assert_eq!(get(&dst, 1, 1), [1, 2, 3, 255]);
    }
    #[test]
    fn blit_pixmap_extreme_positions_do_not_panic() {
        // Every source pixel is off-screen, so this must be a no-op — not an
        // `px_x + sx` overflow.
        let src = filled(2, 2, [1, 2, 3, 255]);
        let mut dst = filled(4, 4, CLEAR);
        blit_pixmap(&src, &mut dst, i32::MAX, 0, 1.0);
        blit_pixmap(&src, &mut dst, 0, i32::MAX, 1.0);
        blit_pixmap(&src, &mut dst, i32::MIN, i32::MIN, 1.0);
        assert!(dst.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn blit_pixmap_zero_sized_src_is_a_noop() {
        let src = zero_sized();
        let mut dst = filled(2, 2, WHITE);
        blit_pixmap(&src, &mut dst, 0, 0, 1.0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    // ==================================================================
    // shift_pixbuf (numeric)
    // ==================================================================
    #[test]
    fn shift_pixbuf_zero_delta_is_a_noop() {
        let mut p = marked(3, 3);
        let before = p.data().to_vec();
        shift_pixbuf(&mut p, 0, 0);
        assert_eq!(p.data(), &before[..]);
    }
    #[test]
    fn shift_pixbuf_right_moves_columns_and_clears_the_left() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 1, 0);
        assert_eq!(get(&p, 0, 0), CLEAR, "the exposed column must be cleared");
        assert_eq!(get(&p, 1, 0), [0, 0, 0, 255]);
        assert_eq!(get(&p, 2, 0), [1, 0, 0, 255]);
        assert_eq!(get(&p, 1, 2), [6, 0, 0, 255]);
    }
    #[test]
    fn shift_pixbuf_left_moves_columns_and_clears_the_right() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, -1, 0);
        assert_eq!(get(&p, 0, 0), [1, 0, 0, 255]);
        assert_eq!(get(&p, 1, 0), [2, 0, 0, 255]);
        assert_eq!(get(&p, 2, 0), CLEAR);
        assert_eq!(get(&p, 0, 2), [7, 0, 0, 255]);
    }
    #[test]
    fn shift_pixbuf_down_moves_rows_and_clears_the_top() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 0, 1);
        assert_eq!(get(&p, 0, 0), CLEAR);
        assert_eq!(get(&p, 0, 1), [0, 0, 0, 255]);
        assert_eq!(get(&p, 1, 1), [1, 0, 0, 255]);
        assert_eq!(get(&p, 0, 2), [3, 0, 0, 255]);
    }
    #[test]
    fn shift_pixbuf_up_moves_rows_and_clears_the_bottom() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 0, -1);
        assert_eq!(get(&p, 0, 0), [3, 0, 0, 255]);
        assert_eq!(get(&p, 0, 1), [6, 0, 0, 255]);
        assert_eq!(get(&p, 0, 2), CLEAR);
        assert_eq!(get(&p, 2, 2), CLEAR);
    }
    #[test]
    fn shift_pixbuf_diagonal_composes_both_axes() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 1, 1);
        assert_eq!(get(&p, 1, 1), [0, 0, 0, 255]);
        assert_eq!(get(&p, 2, 2), [4, 0, 0, 255]);
        assert_eq!(get(&p, 0, 0), CLEAR);
        assert_eq!(get(&p, 2, 0), CLEAR);
        assert_eq!(get(&p, 0, 2), CLEAR);
    }
    #[test]
    fn shift_pixbuf_by_exactly_the_size_clears_everything() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 3, 0);
        assert!(p.data().iter().all(|&b| b == 0));
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 0, -3);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn shift_pixbuf_beyond_the_size_clears_everything() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 100, 100);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn shift_pixbuf_i32_max_clears_everything() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, i32::MAX, i32::MAX);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn shift_pixbuf_i32_min_does_not_panic() {
        // `dx.abs()` on i32::MIN overflows; the buffer is entirely exposed, so
        // the documented outcome is "clear everything".
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, i32::MIN, 0);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn shift_pixbuf_i32_min_dy_does_not_panic() {
        let mut p = marked(3, 3);
        shift_pixbuf(&mut p, 0, i32::MIN);
        assert!(p.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn shift_pixbuf_on_zero_sized_pixmap_does_not_panic() {
        let mut p = zero_sized();
        shift_pixbuf(&mut p, 1, 1);
        assert!(p.data().is_empty());
    }
    #[test]
    fn shift_pixbuf_preserves_the_buffer_length() {
        let mut p = marked(4, 4);
        for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1), (3, 3), (-3, -3)] {
            shift_pixbuf(&mut p, dx, dy);
            assert_eq!(p.data().len(), 4 * 4 * 4);
        }
    }
    // ==================================================================
    // blit_buffer (numeric)
    // ==================================================================
    #[test]
    fn blit_buffer_opaque_src_overwrites_dst() {
        let src = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
        let mut dst = filled(4, 1, CLEAR);
        blit_buffer(&mut dst, &src, 2, 1, 1, 0);
        assert_eq!(get(&dst, 0, 0), CLEAR);
        assert_eq!(get(&dst, 1, 0), [10, 20, 30, 255]);
        assert_eq!(get(&dst, 2, 0), [40, 50, 60, 255]);
        assert_eq!(get(&dst, 3, 0), CLEAR);
    }
    #[test]
    fn blit_buffer_transparent_src_is_a_noop() {
        let src = vec![10u8, 20, 30, 0];
        let mut dst = filled(1, 1, WHITE);
        blit_buffer(&mut dst, &src, 1, 1, 0, 0);
        assert_eq!(get(&dst, 0, 0), WHITE);
    }
    #[test]
    fn blit_buffer_premultiplied_half_alpha_blend_is_exact() {
        // src RGB is already premultiplied, so dst = src + dst * (255 - sa) / 255.
        let src = vec![100u8, 50, 25, 128];
        let mut dst = filled(1, 1, CLEAR);
        blit_buffer(&mut dst, &src, 1, 1, 0, 0);
        assert_eq!(get(&dst, 0, 0), [100, 50, 25, 128]);
    }
    #[test]
    fn blit_buffer_premultiplied_blend_saturates_instead_of_wrapping() {
        // src is (illegally) not premultiplied: 255 + white*127/255 would exceed
        // u8 — it must clamp to 255, not wrap to a dark pixel.
        let src = vec![255u8, 255, 255, 128];
        let mut dst = filled(1, 1, WHITE);
        blit_buffer(&mut dst, &src, 1, 1, 0, 0);
        assert_eq!(get(&dst, 0, 0), WHITE);
    }
    #[test]
    fn blit_buffer_negative_offset_clips() {
        let src = vec![1u8, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255, 4, 4, 4, 255];
        let mut dst = filled(2, 2, CLEAR);
        blit_buffer(&mut dst, &src, 2, 2, -1, -1);
        // Only src(1,1) lands on dst(0,0).
        assert_eq!(get(&dst, 0, 0), [4, 4, 4, 255]);
        assert_eq!(get(&dst, 1, 1), CLEAR);
    }
    #[test]
    fn blit_buffer_fully_offscreen_is_a_noop() {
        let src = vec![9u8, 9, 9, 255];
        let mut dst = filled(2, 2, CLEAR);
        blit_buffer(&mut dst, &src, 1, 1, 50, 50);
        blit_buffer(&mut dst, &src, 1, 1, -50, -50);
        assert!(dst.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn blit_buffer_src_shorter_than_its_declared_size_is_skipped_not_panic() {
        // Claims 4x4 but only carries 2 pixels — the bounds guard must skip the rest.
        let src = vec![7u8, 7, 7, 255, 8, 8, 8, 255];
        let mut dst = filled(4, 4, CLEAR);
        blit_buffer(&mut dst, &src, 4, 4, 0, 0);
        assert_eq!(get(&dst, 0, 0), [7, 7, 7, 255]);
        assert_eq!(get(&dst, 1, 0), [8, 8, 8, 255]);
        assert_eq!(get(&dst, 2, 0), CLEAR);
        assert_eq!(get(&dst, 3, 3), CLEAR);
    }
    #[test]
    fn blit_buffer_empty_src_is_a_noop() {
        let mut dst = filled(2, 2, WHITE);
        blit_buffer(&mut dst, &[], 2, 2, 0, 0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn blit_buffer_zero_src_dimensions_are_a_noop() {
        let src = vec![9u8, 9, 9, 255];
        let mut dst = filled(2, 2, WHITE);
        blit_buffer(&mut dst, &src, 0, 0, 0, 0);
        blit_buffer(&mut dst, &src, 0, 1, 0, 0);
        blit_buffer(&mut dst, &src, 1, 0, 0, 0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn blit_buffer_u32_max_src_dimensions_are_a_noop() {
        // `src_w as i32` is -1, so both loops are empty.
        let src = vec![9u8, 9, 9, 255];
        let mut dst = filled(2, 2, WHITE);
        blit_buffer(&mut dst, &src, u32::MAX, u32::MAX, 0, 0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn blit_buffer_extreme_offsets_do_not_panic() {
        let src = vec![9u8; 2 * 2 * 4];
        let mut dst = filled(4, 4, CLEAR);
        blit_buffer(&mut dst, &src, 2, 2, i32::MAX, 0);
        blit_buffer(&mut dst, &src, 2, 2, 0, i32::MAX);
        blit_buffer(&mut dst, &src, 2, 2, i32::MIN, i32::MIN);
        assert!(dst.data().iter().all(|&b| b == 0));
    }
    // ==================================================================
    // snapshot_region / write_region (numeric + round-trip)
    // ==================================================================
    #[test]
    fn snapshot_region_copies_the_requested_box() {
        let p = marked(4, 4);
        let snap = snapshot_region(&p, 1, 1, 2, 2);
        assert_eq!(snap.len(), 2 * 2 * 4);
        assert_eq!(&snap[0..4], &[5, 0, 0, 255]); // (1,1)
        assert_eq!(&snap[4..8], &[6, 0, 0, 255]); // (2,1)
        assert_eq!(&snap[8..12], &[9, 0, 0, 255]); // (1,2)
        assert_eq!(&snap[12..16], &[10, 0, 0, 255]); // (2,2)
    }
    #[test]
    fn snapshot_region_zero_size_is_an_empty_vec() {
        let p = marked(4, 4);
        assert!(snapshot_region(&p, 0, 0, 0, 0).is_empty());
        assert!(snapshot_region(&p, 0, 0, 4, 0).is_empty());
        assert!(snapshot_region(&p, 0, 0, 0, 4).is_empty());
    }
    #[test]
    fn snapshot_region_out_of_bounds_pixels_are_zero_filled() {
        let p = filled(2, 2, WHITE);
        let snap = snapshot_region(&p, 1, 1, 2, 2);
        assert_eq!(snap.len(), 16);
        assert_eq!(&snap[0..4], &WHITE, "only (1,1) is inside the pixmap");
        assert_eq!(&snap[4..8], &CLEAR);
        assert_eq!(&snap[8..12], &CLEAR);
        assert_eq!(&snap[12..16], &CLEAR);
    }
    #[test]
    fn snapshot_region_negative_origin_is_partially_zero_filled() {
        let p = marked(2, 2);
        let snap = snapshot_region(&p, -1, -1, 2, 2);
        assert_eq!(&snap[0..4], &CLEAR);
        assert_eq!(&snap[4..8], &CLEAR);
        assert_eq!(&snap[8..12], &CLEAR);
        assert_eq!(&snap[12..16], &[0, 0, 0, 255], "pixel (0,0) of the source");
    }
    #[test]
    fn snapshot_region_fully_offscreen_is_all_zeroes() {
        let p = filled(4, 4, WHITE);
        let snap = snapshot_region(&p, 100, 100, 2, 2);
        assert!(snap.iter().all(|&b| b == 0));
    }
    #[test]
    fn snapshot_region_extreme_origin_does_not_panic() {
        // Every sampled pixel is off-screen, so the result must be all zeroes —
        // not an `x + px` overflow.
        let p = filled(4, 4, WHITE);
        let snap = snapshot_region(&p, i32::MAX, 0, 2, 2);
        assert!(snap.iter().all(|&b| b == 0));
        let snap = snapshot_region(&p, 0, i32::MAX, 2, 2);
        assert!(snap.iter().all(|&b| b == 0));
        let snap = snapshot_region(&p, i32::MIN, i32::MIN, 2, 2);
        assert!(snap.iter().all(|&b| b == 0));
    }
    #[test]
    fn write_region_overwrites_without_blending() {
        // Unlike blit_buffer, a fully transparent src must still clobber dst.
        let src = vec![0u8; 4];
        let mut dst = filled(2, 2, WHITE);
        write_region(&mut dst, &src, 1, 1, 0, 0);
        assert_eq!(get(&dst, 0, 0), CLEAR, "write_region is a direct copy");
        assert_eq!(get(&dst, 1, 1), WHITE);
    }
    #[test]
    fn write_region_places_pixels_at_the_offset() {
        let src = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
        let mut dst = filled(4, 1, CLEAR);
        write_region(&mut dst, &src, 2, 1, 2, 0);
        assert_eq!(get(&dst, 2, 0), [1, 2, 3, 4]);
        assert_eq!(get(&dst, 3, 0), [5, 6, 7, 8]);
        assert_eq!(get(&dst, 0, 0), CLEAR);
    }
    #[test]
    fn write_region_out_of_bounds_pixels_are_skipped() {
        let src = vec![9u8; 2 * 2 * 4];
        let mut dst = filled(2, 2, CLEAR);
        write_region(&mut dst, &src, 2, 2, 1, 1);
        assert_eq!(get(&dst, 1, 1), [9, 9, 9, 9]);
        assert_eq!(get(&dst, 0, 0), CLEAR);
    }
    #[test]
    fn write_region_negative_coords_clip() {
        let src = vec![1u8, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4];
        let mut dst = filled(2, 2, CLEAR);
        write_region(&mut dst, &src, 2, 2, -1, -1);
        assert_eq!(get(&dst, 0, 0), [4, 4, 4, 4]);
        assert_eq!(get(&dst, 1, 1), CLEAR);
    }
    #[test]
    fn write_region_short_src_is_skipped_not_panic() {
        let src = vec![7u8, 7, 7, 7];
        let mut dst = filled(4, 4, CLEAR);
        write_region(&mut dst, &src, 4, 4, 0, 0);
        assert_eq!(get(&dst, 0, 0), [7, 7, 7, 7]);
        assert_eq!(get(&dst, 1, 0), CLEAR);
    }
    #[test]
    fn write_region_zero_dimensions_are_a_noop() {
        let src = vec![9u8; 16];
        let mut dst = filled(2, 2, WHITE);
        write_region(&mut dst, &src, 0, 0, 0, 0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn write_region_u32_max_dimensions_are_a_noop() {
        let src = vec![9u8; 16];
        let mut dst = filled(2, 2, WHITE);
        write_region(&mut dst, &src, u32::MAX, u32::MAX, 0, 0);
        assert!(dst.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn write_region_extreme_coords_do_not_panic() {
        let src = vec![9u8; 2 * 2 * 4];
        let mut dst = filled(4, 4, CLEAR);
        write_region(&mut dst, &src, 2, 2, i32::MAX, 0);
        write_region(&mut dst, &src, 2, 2, 0, i32::MAX);
        write_region(&mut dst, &src, 2, 2, i32::MIN, i32::MIN);
        assert!(dst.data().iter().all(|&b| b == 0));
    }
    #[test]
    fn snapshot_region_then_write_region_round_trips() {
        let mut p = marked(6, 6);
        let snap = snapshot_region(&p, 1, 1, 3, 3);
        let expected = p.data().to_vec();
        p.fill_rect(1, 1, 3, 3, 0, 0, 0, 0); // destroy the region
        assert_eq!(get(&p, 2, 2), CLEAR);
        write_region(&mut p, &snap, 3, 3, 1, 1); // restore it
        assert_eq!(
            p.data(),
            &expected[..],
            "write_region must invert snapshot_region"
        );
    }
    // ==================================================================
    // agg_fill_path / agg_fill_path_clipped (other + numeric)
    // ==================================================================
    #[test]
    fn agg_fill_path_paints_the_path_interior_only() {
        let mut p = filled(10, 10, WHITE);
        let mut path = rect_path(2.0, 2.0, 6.0, 6.0);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert_eq!(get(&p, 5, 5), [255, 0, 0, 255], "interior is fully covered");
        assert_eq!(get(&p, 0, 0), WHITE, "outside the path is untouched");
        assert_eq!(get(&p, 9, 9), WHITE);
    }
    #[test]
    fn agg_fill_path_empty_path_paints_nothing() {
        let mut p = filled(8, 8, WHITE);
        let mut path = PathStorage::new();
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert!(p.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn agg_fill_path_on_a_1x1_pixmap_does_not_panic() {
        let mut p = filled(1, 1, WHITE);
        let mut path = rect_path(0.0, 0.0, 1.0, 1.0);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert_eq!(get(&p, 0, 0), [255, 0, 0, 255]);
    }
    #[test]
    fn agg_fill_path_offscreen_path_paints_nothing() {
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(100.0, 100.0, 10.0, 10.0);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert!(p.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn agg_fill_path_nan_coordinates_do_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = PathStorage::new();
        path.move_to(f64::NAN, f64::NAN);
        path.line_to(4.0, f64::NAN);
        path.line_to(f64::NAN, 4.0);
        path.close_polygon(0);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert_eq!(p.data().len(), 8 * 8 * 4, "the buffer must stay intact");
    }
    #[test]
    fn agg_fill_path_huge_coordinates_do_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(-1.0e30, -1.0e30, 2.0e30, 2.0e30);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert_eq!(p.data().len(), 8 * 8 * 4);
    }
    #[test]
    fn agg_fill_path_infinite_coordinates_do_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = PathStorage::new();
        path.move_to(f64::NEG_INFINITY, f64::NEG_INFINITY);
        path.line_to(f64::INFINITY, f64::NEG_INFINITY);
        path.line_to(f64::INFINITY, f64::INFINITY);
        path.close_polygon(0);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::NonZero);
        assert_eq!(p.data().len(), 8 * 8 * 4);
    }
    #[test]
    fn agg_fill_path_clipped_none_clip_equals_unclipped() {
        let mut a = filled(10, 10, WHITE);
        let mut b = filled(10, 10, WHITE);
        let mut pa = rect_path(1.0, 1.0, 5.0, 5.0);
        let mut pb = rect_path(1.0, 1.0, 5.0, 5.0);
        agg_fill_path(&mut a, &mut pa, &red(), FillingRule::NonZero);
        agg_fill_path_clipped(&mut b, &mut pb, &red(), FillingRule::NonZero, None);
        assert_eq!(a.data(), b.data());
    }
    #[test]
    fn agg_fill_path_clipped_restricts_output_to_the_clip_box() {
        let mut p = filled(10, 10, WHITE);
        let mut path = rect_path(0.0, 0.0, 10.0, 10.0); // the whole canvas
        let clip = AzRect::from_xywh(2.0, 2.0, 3.0, 3.0).expect("valid");
        agg_fill_path_clipped(&mut p, &mut path, &red(), FillingRule::NonZero, Some(clip));
        assert_eq!(get(&p, 3, 3), [255, 0, 0, 255], "inside the clip");
        assert_eq!(get(&p, 6, 6), WHITE, "outside the clip");
        assert_eq!(get(&p, 0, 0), WHITE);
    }
    #[test]
    fn agg_fill_path_clipped_offscreen_clip_paints_nothing() {
        let mut p = filled(10, 10, WHITE);
        let mut path = rect_path(0.0, 0.0, 10.0, 10.0);
        let clip = AzRect::from_xywh(100.0, 100.0, 5.0, 5.0).expect("valid");
        agg_fill_path_clipped(&mut p, &mut path, &red(), FillingRule::NonZero, Some(clip));
        assert!(
            p.data().iter().all(|&b| b == 255),
            "a clip box outside the buffer must reject everything"
        );
    }
    #[test]
    fn agg_fill_path_clipped_empty_clip_paints_nothing() {
        // `intersect_clips` returns a zero-area rect for non-overlapping nested
        // clips, and documents that it must clip EVERYTHING. Nothing may leak.
        let outer = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
        let inner = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
        let empty = intersect_clips(Some(outer), Some(inner)).expect("stays Some");
        assert!(approx(empty.width, 0.0) && approx(empty.height, 0.0));
        let mut p = filled(20, 20, WHITE);
        let mut path = rect_path(0.0, 0.0, 20.0, 20.0);
        agg_fill_path_clipped(&mut p, &mut path, &red(), FillingRule::NonZero, Some(empty));
        let painted = painted_count(&p);
        assert_eq!(
            painted, 0,
            "an empty clip leaked {painted} painted pixel(s)"
        );
    }
    #[test]
    fn agg_fill_path_evenodd_leaves_the_hole_of_a_donut_unpainted() {
        let mut p = filled(12, 12, WHITE);
        let mut path = PathStorage::new();
        // outer ring
        path.move_to(1.0, 1.0);
        path.line_to(11.0, 1.0);
        path.line_to(11.0, 11.0);
        path.line_to(1.0, 11.0);
        path.close_polygon(0);
        // inner ring (same winding — only EvenOdd punches a hole)
        path.move_to(4.0, 4.0);
        path.line_to(8.0, 4.0);
        path.line_to(8.0, 8.0);
        path.line_to(4.0, 8.0);
        path.close_polygon(0);
        agg_fill_path(&mut p, &mut path, &red(), FillingRule::EvenOdd);
        assert_eq!(get(&p, 2, 2), [255, 0, 0, 255], "the ring is painted");
        assert_eq!(get(&p, 6, 6), WHITE, "the hole is not");
    }
    // ==================================================================
    // agg_fill_transformed_path / _clipped (other + numeric)
    // ==================================================================
    #[test]
    fn agg_fill_transformed_path_identity_matches_the_untransformed_fill() {
        let mut a = filled(10, 10, WHITE);
        let mut b = filled(10, 10, WHITE);
        let mut pa = rect_path(2.0, 2.0, 4.0, 4.0);
        let mut pb = rect_path(2.0, 2.0, 4.0, 4.0);
        agg_fill_path(&mut a, &mut pa, &red(), FillingRule::NonZero);
        agg_fill_transformed_path(
            &mut b,
            &mut pb,
            &red(),
            FillingRule::NonZero,
            &TransAffine::new(),
        );
        assert_eq!(a.data(), b.data(), "an identity transform must be a no-op");
    }
    #[test]
    fn agg_fill_transformed_path_translation_moves_the_output() {
        let mut p = filled(12, 12, WHITE);
        let mut path = rect_path(0.0, 0.0, 3.0, 3.0);
        let t = TransAffine::new_translation(6.0, 0.0);
        agg_fill_transformed_path(&mut p, &mut path, &red(), FillingRule::NonZero, &t);
        assert_eq!(get(&p, 7, 1), [255, 0, 0, 255], "moved right by 6");
        assert_eq!(get(&p, 1, 1), WHITE, "the original position is empty");
    }
    #[test]
    fn agg_fill_transformed_path_zero_scale_does_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(1.0, 1.0, 4.0, 4.0);
        let t = TransAffine::new_scaling(0.0, 0.0);
        agg_fill_transformed_path(&mut p, &mut path, &red(), FillingRule::NonZero, &t);
        assert!(
            p.data().iter().all(|&b| b == 255),
            "a degenerate transform collapses the path to a point"
        );
    }
    #[test]
    fn agg_fill_transformed_path_nan_transform_does_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(1.0, 1.0, 4.0, 4.0);
        let t = TransAffine::new_scaling(f64::NAN, 1.0);
        agg_fill_transformed_path(&mut p, &mut path, &red(), FillingRule::NonZero, &t);
        assert_eq!(p.data().len(), 8 * 8 * 4);
    }
    #[test]
    fn agg_fill_transformed_path_clipped_applies_the_clip_after_the_transform() {
        let mut p = filled(12, 12, WHITE);
        let mut path = rect_path(0.0, 0.0, 3.0, 3.0);
        let t = TransAffine::new_translation(6.0, 0.0);
        // The clip box covers the ORIGINAL position, not the transformed one.
        let clip = AzRect::from_xywh(0.0, 0.0, 3.0, 3.0).expect("valid");
        agg_fill_transformed_path_clipped(
            &mut p,
            &mut path,
            &red(),
            FillingRule::NonZero,
            &t,
            Some(clip),
        );
        assert!(
            p.data().iter().all(|&b| b == 255),
            "the translated path lies outside the clip box"
        );
    }
    #[test]
    fn agg_fill_transformed_path_clipped_empty_clip_paints_nothing() {
        let outer = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
        let inner = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
        let empty = intersect_clips(Some(outer), Some(inner)).expect("stays Some");
        let mut p = filled(20, 20, WHITE);
        let mut path = rect_path(0.0, 0.0, 20.0, 20.0);
        agg_fill_transformed_path_clipped(
            &mut p,
            &mut path,
            &red(),
            FillingRule::NonZero,
            &TransAffine::new(),
            Some(empty),
        );
        assert!(
            p.data().iter().all(|&b| b == 255),
            "an empty clip must clip everything, even on the identity fast path"
        );
    }
    // ==================================================================
    // agg_fill_gradient / agg_fill_gradient_clipped (numeric)
    // ==================================================================
    #[test]
    fn agg_fill_gradient_paints_the_path() {
        let mut p = filled(10, 10, WHITE);
        let mut path = rect_path(0.0, 0.0, 10.0, 10.0);
        let lut = two_stop_lut();
        agg_fill_gradient(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            0.0,
            10.0,
        );
        assert_ne!(get(&p, 5, 5), WHITE, "the gradient must paint something");
        assert_eq!(get(&p, 5, 5)[3], 255, "opaque stops produce opaque pixels");
    }
    #[test]
    fn agg_fill_gradient_empty_path_paints_nothing() {
        let mut p = filled(8, 8, WHITE);
        let mut path = PathStorage::new();
        let lut = two_stop_lut();
        agg_fill_gradient(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            0.0,
            8.0,
        );
        assert!(p.data().iter().all(|&b| b == 255));
    }
    #[test]
    fn agg_fill_gradient_zero_length_d1_eq_d2_does_not_panic() {
        // d2 - d1 == 0 must not divide by zero.
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
        let lut = two_stop_lut();
        agg_fill_gradient_clipped(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            5.0,
            5.0,
            None,
        );
        assert_eq!(p.data().len(), 8 * 8 * 4);
    }
    #[test]
    fn agg_fill_gradient_reversed_d2_lt_d1_does_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
        let lut = two_stop_lut();
        agg_fill_gradient_clipped(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            8.0,
            0.0,
            None,
        );
        assert_eq!(p.data().len(), 8 * 8 * 4);
    }
    #[test]
    fn agg_fill_gradient_nan_and_inf_distances_do_not_panic() {
        let lut = two_stop_lut();
        for (d1, d2) in [
            (f64::NAN, f64::NAN),
            (0.0, f64::NAN),
            (f64::NEG_INFINITY, f64::INFINITY),
            (0.0, f64::MAX),
            (f64::MIN, f64::MAX),
        ] {
            let mut p = filled(8, 8, WHITE);
            let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
            agg_fill_gradient_clipped(
                &mut p,
                &mut path,
                &lut,
                GradientX,
                TransAffine::new(),
                d1,
                d2,
                None,
            );
            assert_eq!(p.data().len(), 8 * 8 * 4, "d1={d1}, d2={d2}");
        }
    }
    #[test]
    fn agg_fill_gradient_nan_transform_does_not_panic() {
        let mut p = filled(8, 8, WHITE);
        let mut path = rect_path(0.0, 0.0, 8.0, 8.0);
        let lut = two_stop_lut();
        agg_fill_gradient_clipped(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new_scaling(f64::NAN, f64::NAN),
            0.0,
            8.0,
            None,
        );
        assert_eq!(p.data().len(), 8 * 8 * 4);
    }
    #[test]
    fn agg_fill_gradient_clipped_restricts_output_to_the_clip_box() {
        let mut p = filled(10, 10, WHITE);
        let mut path = rect_path(0.0, 0.0, 10.0, 10.0);
        let lut = two_stop_lut();
        let clip = AzRect::from_xywh(0.0, 0.0, 3.0, 3.0).expect("valid");
        agg_fill_gradient_clipped(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            0.0,
            10.0,
            Some(clip),
        );
        assert_ne!(get(&p, 1, 1), WHITE, "inside the clip");
        assert_eq!(get(&p, 8, 8), WHITE, "outside the clip");
    }
    #[test]
    fn agg_fill_gradient_clipped_empty_clip_paints_nothing() {
        let outer = AzRect::from_xywh(0.0, 0.0, 4.0, 4.0).expect("valid");
        let inner = AzRect::from_xywh(10.0, 10.0, 4.0, 4.0).expect("valid");
        let empty = intersect_clips(Some(outer), Some(inner)).expect("stays Some");
        let mut p = filled(20, 20, WHITE);
        let mut path = rect_path(0.0, 0.0, 20.0, 20.0);
        let lut = two_stop_lut();
        agg_fill_gradient_clipped(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            0.0,
            20.0,
            Some(empty),
        );
        let painted = painted_count(&p);
        assert_eq!(
            painted, 0,
            "an empty clip leaked {painted} painted pixel(s)"
        );
    }
    #[test]
    fn agg_fill_gradient_on_a_1x1_pixmap_does_not_panic() {
        let mut p = filled(1, 1, WHITE);
        let mut path = rect_path(0.0, 0.0, 1.0, 1.0);
        let lut = two_stop_lut();
        agg_fill_gradient(
            &mut p,
            &mut path,
            &lut,
            GradientX,
            TransAffine::new(),
            0.0,
            1.0,
        );
        assert_eq!(p.data().len(), 4);
    }
}
#[cfg(test)]
mod pixbuf_tests {
    use super::*;
    #[test]
1
    fn external_pixmap_reads_and_writes_the_callers_memory_and_never_frees_it() {
1
        let mut backing = vec![7u8; 4 * 2 * 4];
1
        let ptr = backing.as_mut_ptr();
        {
1
            let mut p = unsafe { AzulPixmap::from_external(ptr, 4, 2) }.unwrap();
1
            assert!(p.is_external());
1
            assert_eq!(p.data.len(), 32);
32
            assert!(p.data.iter().all(|&b| b == 7), "reads the caller's bytes");
1
            p.data[0] = 42;
            // Clone SNAPSHOTS to owned — must not alias the backing.
1
            let mut c = AzulPixmap {
1
                data: p.data.clone(),
1
                width: p.width,
1
                height: p.height,
1
            };
1
            assert!(!c.is_external());
1
            c.data[1] = 99;
1
            assert_ne!(backing[1], 99, "clone must not write through");
        } // drop of the borrowed pixmap must NOT free `backing`
1
        assert_eq!(backing[0], 42, "writes went to the caller's memory");
1
        assert_eq!(backing[1], 7);
1
        drop(backing); // and the Vec is still validly ours to free
1
    }
    #[test]
1
    fn into_vec_copies_out_of_borrowed_storage() {
1
        let mut backing = vec![3u8; 16];
1
        let p = unsafe { AzulPixmap::from_external(backing.as_mut_ptr(), 2, 2) }.unwrap();
1
        let v = p.data.into_vec();
1
        assert_eq!(v, vec![3u8; 16]);
1
        backing[0] = 9; // backing untouched and still alive
1
        assert_eq!(v[0], 3);
1
    }
    #[test]
1
    fn from_external_rejects_null_and_zero_dims() {
1
        assert!(unsafe { AzulPixmap::from_external(core::ptr::null_mut(), 4, 4) }.is_none());
1
        let mut b = [0u8; 16];
1
        assert!(unsafe { AzulPixmap::from_external(b.as_mut_ptr(), 0, 2) }.is_none());
1
        assert!(unsafe { AzulPixmap::from_external(b.as_mut_ptr(), 2, 0) }.is_none());
1
    }
}