1
use super::*;
2

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

            
20
pub const IDENTITY_EPSILON_F64: f64 = 0.0001;
21

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

            
41
/// Blit `src` onto `dst` at pixel position (`px_x`, `px_y`) with opacity.
42
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
43
108
pub fn blit_pixmap(src: &AzulPixmap, dst: &mut AzulPixmap, px_x: i32, px_y: i32, opacity: f32) {
44
108
    let sw = src.width as i32;
45
108
    let sh = src.height as i32;
46
108
    let dw = dst.width as i32;
47
108
    let dh = dst.height as i32;
48
108
    let op = (opacity * 255.0).clamp(0.0, 255.0) as u32;
49

            
50
51080
    for sy in 0..sh {
51
51080
        let dy = px_y + sy;
52
51080
        if dy < 0 || dy >= dh {
53
            continue;
54
51080
        }
55
32578200
        for sx in 0..sw {
56
32578200
            let dx = px_x + sx;
57
32578200
            if dx < 0 || dx >= dw {
58
                continue;
59
32578200
            }
60
32578200
            let si = ((sy * sw + sx) * 4) as usize;
61
32578200
            let di = ((dy * dw + dx) * 4) as usize;
62
32578200
            if si + 3 >= src.data.len() || di + 3 >= dst.data.len() {
63
                continue;
64
32578200
            }
65

            
66
32578200
            let sr = u32::from(src.data[si]);
67
32578200
            let sg = u32::from(src.data[si + 1]);
68
32578200
            let sb = u32::from(src.data[si + 2]);
69
32578200
            let sa = (u32::from(src.data[si + 3]) * op) / 255;
70

            
71
32578200
            if sa == 0 {
72
5000
                continue;
73
32573200
            }
74
32573200
            if sa == 255 {
75
32573200
                dst.data[di] = sr as u8;
76
32573200
                dst.data[di + 1] = sg as u8;
77
32573200
                dst.data[di + 2] = sb as u8;
78
32573200
                dst.data[di + 3] = 255;
79
32573200
            } else {
80
                let inv_sa = 255 - sa;
81
                dst.data[di] = ((sr * sa + u32::from(dst.data[di]) * inv_sa) / 255) as u8;
82
                dst.data[di + 1] = ((sg * sa + u32::from(dst.data[di + 1]) * inv_sa) / 255) as u8;
83
                dst.data[di + 2] = ((sb * sa + u32::from(dst.data[di + 2]) * inv_sa) / 255) as u8;
84
                dst.data[di + 3] = ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
85
            }
86
        }
87
    }
88
108
}
89

            
90
/// Shift pixel data in a pixmap by (dx, dy) pixels, clearing exposed regions.
91
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
92
pub fn shift_pixbuf(pixmap: &mut AzulPixmap, dx: i32, dy: i32) {
93
    use core::cmp::Ordering;
94
    let w = pixmap.width as i32;
95
    let h = pixmap.height as i32;
96
    if dx.abs() >= w || dy.abs() >= h {
97
        // Entire buffer is exposed — just clear it
98
        pixmap.fill(0, 0, 0, 0);
99
        return;
100
    }
101

            
102
    let stride = (w * 4) as usize;
103
    let data = &mut pixmap.data;
104

            
105
    // Shift rows vertically
106
    match dy.cmp(&0) {
107
        Ordering::Greater => {
108
            // Shift down: copy from top to bottom
109
            for row in (0..h - dy).rev() {
110
                let src_start = (row * w * 4) as usize;
111
                let dst_start = ((row + dy) * w * 4) as usize;
112
                data.copy_within(src_start..src_start + stride, dst_start);
113
            }
114
            // Clear top rows
115
            for row in 0..dy {
116
                let start = (row * w * 4) as usize;
117
                data[start..start + stride].fill(0);
118
            }
119
        }
120
        Ordering::Less => {
121
            let ady = -dy;
122
            // Shift up: copy from bottom to top
123
            for row in ady..h {
124
                let src_start = (row * w * 4) as usize;
125
                let dst_start = ((row - ady) * w * 4) as usize;
126
                data.copy_within(src_start..src_start + stride, dst_start);
127
            }
128
            // Clear bottom rows
129
            for row in (h - ady)..h {
130
                let start = (row * w * 4) as usize;
131
                data[start..start + stride].fill(0);
132
            }
133
        }
134
        Ordering::Equal => {}
135
    }
136

            
137
    // Shift columns horizontally
138
    match dx.cmp(&0) {
139
        Ordering::Greater => {
140
            for row in 0..h {
141
                let row_start = (row * w * 4) as usize;
142
                let shift = (dx * 4) as usize;
143
                // Shift right within the row
144
                data.copy_within(row_start..row_start + stride - shift, row_start + shift);
145
                // Clear left columns
146
                data[row_start..row_start + shift].fill(0);
147
            }
148
        }
149
        Ordering::Less => {
150
            let adx = (-dx * 4) as usize;
151
            for row in 0..h {
152
                let row_start = (row * w * 4) as usize;
153
                data.copy_within(row_start + adx..row_start + stride, row_start);
154
                // Clear right columns
155
                data[row_start + stride - adx..row_start + stride].fill(0);
156
            }
157
        }
158
        Ordering::Equal => {}
159
    }
160
}
161

            
162
/// A simple RGBA pixel buffer. Replaces `tiny_skia::Pixmap`.
163
#[derive(Debug)]
164
pub struct AzulPixmap {
165
    pub(crate) data: Vec<u8>,
166
    pub(crate) width: u32,
167
    pub(crate) height: u32,
168
}
169

            
170
impl AzulPixmap {
171
    /// Create a new pixmap filled with opaque white.
172
2771
    #[must_use] pub fn new(width: u32, height: u32) -> Option<Self> {
173
2771
        if width == 0 || height == 0 {
174
            return None;
175
2771
        }
176
2771
        let len = (width as usize) * (height as usize) * 4;
177
2771
        let data = vec![255u8; len]; // opaque white
178
2771
        Some(Self {
179
2771
            data,
180
2771
            width,
181
2771
            height,
182
2771
        })
183
2771
    }
184

            
185
    /// Fill the entire pixmap with a single color.
186
2660
    pub fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) {
187
224972412
        for chunk in self.data.chunks_exact_mut(4) {
188
224972412
            chunk[0] = r;
189
224972412
            chunk[1] = g;
190
224972412
            chunk[2] = b;
191
224972412
            chunk[3] = a;
192
224972412
        }
193
2660
    }
194

            
195
    /// Fill a rectangular region with a single color (pixel coordinates).
196
    #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
197
    #[allow(clippy::many_single_char_names)] // domain-standard coordinate/geometry/short-lived names
198
53
    pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, r: u8, g: u8, b: u8, a: u8) {
199
53
        let pw = self.width as i32;
200
53
        let ph = self.height as i32;
201
53
        let x0 = x.max(0).min(pw);
202
53
        let y0 = y.max(0).min(ph);
203
        // saturating: a non-finite/huge layout size casts to i32::MAX, and `x + w`
204
        // would then overflow (debug panic). Clamp instead.
205
53
        let x1 = x.saturating_add(w).max(0).min(pw);
206
53
        let y1 = y.saturating_add(h).max(0).min(ph);
207
21306
        for row in y0..y1 {
208
21306
            let start = (row * pw + x0) as usize * 4;
209
21306
            let end = (row * pw + x1) as usize * 4;
210
21306
            if end <= self.data.len() {
211
13294944
                for chunk in self.data[start..end].chunks_exact_mut(4) {
212
13294944
                    chunk[0] = r;
213
13294944
                    chunk[1] = g;
214
13294944
                    chunk[2] = b;
215
13294944
                    chunk[3] = a;
216
13294944
                }
217
            }
218
        }
219
53
    }
220

            
221
    /// Raw RGBA pixel data.
222
2349
    #[must_use] pub fn data(&self) -> &[u8] {
223
2349
        &self.data
224
2349
    }
225

            
226
    /// Mutable raw RGBA pixel data.
227
8
    pub fn data_mut(&mut self) -> &mut [u8] {
228
8
        &mut self.data
229
8
    }
230

            
231
    /// Width in pixels.
232
1606
    #[must_use] pub const fn width(&self) -> u32 {
233
1606
        self.width
234
1606
    }
235

            
236
    /// Height in pixels.
237
1594
    #[must_use] pub const fn height(&self) -> u32 {
238
1594
        self.height
239
1594
    }
240

            
241
    /// Create a clone of this pixmap (for filter application).
242
424
    #[must_use] pub fn clone_pixmap(&self) -> Self {
243
424
        Self {
244
424
            data: self.data.clone(),
245
424
            width: self.width,
246
424
            height: self.height,
247
424
        }
248
424
    }
249

            
250
    /// Resize the pixmap preserving existing content in the top-left corner.
251
    /// New right/bottom strips are filled with the specified color.
252
    /// Only grows — returns None if new dimensions are smaller (caller should realloc).
253
    pub fn resize_grow_only(
254
        &mut self,
255
        new_width: u32,
256
        new_height: u32,
257
        fill_r: u8,
258
        fill_g: u8,
259
        fill_b: u8,
260
        fill_a: u8,
261
    ) -> Option<()> {
262
        if new_width < self.width || new_height < self.height {
263
            return None;
264
        }
265
        if new_width == self.width && new_height == self.height {
266
            return Some(());
267
        }
268

            
269
        let old_w = self.width as usize;
270
        let old_h = self.height as usize;
271
        let new_w = new_width as usize;
272
        let new_h = new_height as usize;
273
        let mut new_data = vec![fill_a; new_w * new_h * 4];
274

            
275
        // Fill entire buffer with fill color first (covers right + bottom strips)
276
        for chunk in new_data.chunks_exact_mut(4) {
277
            chunk[0] = fill_r;
278
            chunk[1] = fill_g;
279
            chunk[2] = fill_b;
280
            chunk[3] = fill_a;
281
        }
282

            
283
        // Copy old rows into top-left corner
284
        let old_stride = old_w * 4;
285
        let new_stride = new_w * 4;
286
        for row in 0..old_h {
287
            let src = row * old_stride;
288
            let dst = row * new_stride;
289
            new_data[dst..dst + old_stride].copy_from_slice(&self.data[src..src + old_stride]);
290
        }
291

            
292
        self.data = new_data;
293
        self.width = new_width;
294
        self.height = new_height;
295
        Some(())
296
    }
297

            
298
    /// Resize the pixmap, reusing existing content for the overlapping region.
299
    /// Works for both growing and shrinking. New areas are filled with the given color.
300
    pub fn resize_reuse(
301
        &mut self,
302
        new_width: u32,
303
        new_height: u32,
304
        fill_r: u8,
305
        fill_g: u8,
306
        fill_b: u8,
307
        fill_a: u8,
308
    ) {
309
        if new_width == self.width && new_height == self.height {
310
            return;
311
        }
312

            
313
        let old_w = self.width as usize;
314
        let old_h = self.height as usize;
315
        let new_w = new_width as usize;
316
        let new_h = new_height as usize;
317
        let new_stride = new_w * 4;
318
        let old_stride = old_w * 4;
319

            
320
        let mut new_data = vec![0u8; new_w * new_h * 4];
321

            
322
        // Fill entire buffer with fill color
323
        for chunk in new_data.chunks_exact_mut(4) {
324
            chunk[0] = fill_r;
325
            chunk[1] = fill_g;
326
            chunk[2] = fill_b;
327
            chunk[3] = fill_a;
328
        }
329

            
330
        // Copy overlapping region from old to new
331
        let copy_rows = old_h.min(new_h);
332
        let copy_cols_bytes = old_stride.min(new_stride);
333
        for row in 0..copy_rows {
334
            let src = row * old_stride;
335
            let dst = row * new_stride;
336
            new_data[dst..dst + copy_cols_bytes]
337
                .copy_from_slice(&self.data[src..src + copy_cols_bytes]);
338
        }
339

            
340
        self.data = new_data;
341
        self.width = new_width;
342
        self.height = new_height;
343
    }
344

            
345
    /// Encode to PNG using the `png` crate.
346
    /// # Errors
347
    ///
348
    /// Returns an error string if PNG encoding fails.
349
1908
    pub fn encode_png(&self) -> Result<Vec<u8>, String> {
350
1908
        let mut buf = Vec::new();
351
        {
352
1908
            let mut encoder = png::Encoder::new(&mut buf, self.width, self.height);
353
1908
            encoder.set_color(png::ColorType::Rgba);
354
1908
            encoder.set_depth(png::BitDepth::Eight);
355
1908
            let mut writer = encoder
356
1908
                .write_header()
357
1908
                .map_err(|e| format!("PNG header error: {e}"))?;
358
1908
            writer
359
1908
                .write_image_data(&self.data)
360
1908
                .map_err(|e| format!("PNG write error: {e}"))?;
361
        }
362
1908
        Ok(buf)
363
1908
    }
364

            
365
    /// Decode a PNG byte slice into an `AzulPixmap`.
366
    /// # Errors
367
    ///
368
    /// Returns an error string if `png_bytes` is not a valid PNG.
369
1113
    pub fn decode_png(png_bytes: &[u8]) -> Result<Self, String> {
370
1113
        let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
371
1113
        let mut reader = decoder
372
1113
            .read_info()
373
1113
            .map_err(|e| format!("PNG decode error: {e}"))?;
374
1113
        let buf_size = reader
375
1113
            .output_buffer_size()
376
1113
            .ok_or_else(|| "PNG: unknown output buffer size".to_string())?;
377
1113
        let mut buf = vec![0u8; buf_size];
378
1113
        let info = reader
379
1113
            .next_frame(&mut buf)
380
1113
            .map_err(|e| format!("PNG frame error: {e}"))?;
381
1113
        let width = info.width;
382
1113
        let height = info.height;
383

            
384
        // Convert to RGBA if needed
385
1113
        let data = match info.color_type {
386
1113
            png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
387
            png::ColorType::Rgb => {
388
                let mut rgba = Vec::with_capacity((width * height * 4) as usize);
389
                for chunk in buf[..info.buffer_size()].chunks_exact(3) {
390
                    rgba.push(chunk[0]);
391
                    rgba.push(chunk[1]);
392
                    rgba.push(chunk[2]);
393
                    rgba.push(255);
394
                }
395
                rgba
396
            }
397
            png::ColorType::Grayscale => {
398
                let mut rgba = Vec::with_capacity((width * height * 4) as usize);
399
                for &v in &buf[..info.buffer_size()] {
400
                    rgba.push(v);
401
                    rgba.push(v);
402
                    rgba.push(v);
403
                    rgba.push(255);
404
                }
405
                rgba
406
            }
407
            other => return Err(format!("Unsupported PNG color type: {other:?}")),
408
        };
409

            
410
1113
        Ok(Self {
411
1113
            data,
412
1113
            width,
413
1113
            height,
414
1113
        })
415
1113
    }
416
}
417

            
418
// ============================================================================
419
// Pixel-diff comparison for regression testing
420
// ============================================================================
421

            
422
/// Result of comparing two pixmaps pixel-by-pixel.
423
#[derive(Copy, Debug, Clone)]
424
pub struct PixelDiffResult {
425
    /// Number of pixels that differ beyond the threshold.
426
    pub diff_count: u64,
427
    /// Total number of pixels compared.
428
    pub total_pixels: u64,
429
    /// Maximum per-channel delta found across all pixels.
430
    pub max_delta: u8,
431
    /// Whether dimensions matched.
432
    pub dimensions_match: bool,
433
    /// Width of the reference image.
434
    pub ref_width: u32,
435
    /// Height of the reference image.
436
    pub ref_height: u32,
437
    /// Width of the test image.
438
    pub test_width: u32,
439
    /// Height of the test image.
440
    pub test_height: u32,
441
}
442

            
443
impl PixelDiffResult {
444
    /// True if the images are identical within tolerance.
445
    #[must_use] pub const fn is_match(&self) -> bool {
446
        self.dimensions_match && self.diff_count == 0
447
    }
448

            
449
    /// Fraction of pixels that differ (0.0 = identical, 1.0 = all different).
450
    #[allow(clippy::cast_precision_loss)] // bounded pixel/coord/colour/glyph cast
451
    #[must_use] pub fn diff_ratio(&self) -> f64 {
452
        if self.total_pixels == 0 {
453
            0.0
454
        } else {
455
            self.diff_count as f64 / self.total_pixels as f64
456
        }
457
    }
458
}
459

            
460
/// Compare two pixmaps pixel-by-pixel with a per-channel tolerance.
461
///
462
/// `threshold` is the maximum allowed per-channel difference (0 = exact match,
463
/// 2-3 = anti-aliasing tolerance, 10+ = loose match).
464
#[allow(clippy::cast_possible_truncation)] // bounded pixel/coord/colour/glyph cast
465
477
#[must_use] pub fn pixel_diff(reference: &AzulPixmap, test: &AzulPixmap, threshold: u8) -> PixelDiffResult {
466
477
    let dimensions_match = reference.width == test.width && reference.height == test.height;
467
477
    if !dimensions_match {
468
        return PixelDiffResult {
469
            diff_count: 0,
470
            total_pixels: 0,
471
            max_delta: 0,
472
            dimensions_match: false,
473
            ref_width: reference.width,
474
            ref_height: reference.height,
475
            test_width: test.width,
476
            test_height: test.height,
477
        };
478
477
    }
479

            
480
477
    let total_pixels = u64::from(reference.width) * u64::from(reference.height);
481
477
    let mut diff_count = 0u64;
482
477
    let mut max_delta = 0u8;
483

            
484
11787200
    for (ref_chunk, test_chunk) in reference
485
477
        .data
486
477
        .chunks_exact(4)
487
477
        .zip(test.data.chunks_exact(4))
488
    {
489
11787200
        let mut pixel_differs = false;
490
58936000
        for c in 0..4 {
491
47148800
            let delta = (i16::from(ref_chunk[c]) - i16::from(test_chunk[c])).unsigned_abs() as u8;
492
47148800
            if delta > threshold {
493
                pixel_differs = true;
494
47148800
            }
495
47148800
            if delta > max_delta {
496
                max_delta = delta;
497
47148800
            }
498
        }
499
11787200
        if pixel_differs {
500
            diff_count += 1;
501
11787200
        }
502
    }
503

            
504
477
    PixelDiffResult {
505
477
        diff_count,
506
477
        total_pixels,
507
477
        max_delta,
508
477
        dimensions_match: true,
509
477
        ref_width: reference.width,
510
477
        ref_height: reference.height,
511
477
        test_width: test.width,
512
477
        test_height: test.height,
513
477
    }
514
477
}
515

            
516
/// Compare a rendered pixmap against a reference PNG file.
517
///
518
/// Returns `Ok(result)` with the diff stats, or `Err` if the reference
519
/// file cannot be read/decoded.
520
/// # Errors
521
///
522
/// Returns an error string if the images cannot be loaded or compared.
523
pub fn compare_against_reference(
524
    rendered: &AzulPixmap,
525
    reference_png_path: &str,
526
    threshold: u8,
527
) -> Result<PixelDiffResult, String> {
528
    let ref_bytes = std::fs::read(reference_png_path)
529
        .map_err(|e| format!("Cannot read reference image {reference_png_path}: {e}"))?;
530
    let reference = AzulPixmap::decode_png(&ref_bytes)?;
531
    Ok(pixel_diff(&reference, rendered, threshold))
532
}
533

            
534
// ============================================================================
535
// Simple rect type (replaces tiny_skia::Rect)
536
// ============================================================================
537

            
538
#[derive(Debug, Clone, Copy)]
539
pub struct AzRect {
540
    pub(crate) x: f32,
541
    pub(crate) y: f32,
542
    pub(crate) width: f32,
543
    pub(crate) height: f32,
544
}
545

            
546
/// Intersect a freshly-pushed clip with the currently-active one.
547
///
548
/// `None`
549
/// means "no clip". An EMPTY intersection clips everything (zero-area rect) —
550
/// it must NOT degrade to `None`/unclipped, or nested clips could escape
551
/// their parents.
552
477
#[must_use] pub fn intersect_clips(current: Option<AzRect>, new: Option<AzRect>) -> Option<AzRect> {
553
477
    match (current, new) {
554
371
        (Some(cur), Some(new)) => {
555
371
            let x0 = cur.x.max(new.x);
556
371
            let y0 = cur.y.max(new.y);
557
371
            let x1 = (cur.x + cur.width).min(new.x + new.width);
558
371
            let y1 = (cur.y + cur.height).min(new.y + new.height);
559
371
            Some(AzRect {
560
371
                x: x0,
561
371
                y: y0,
562
371
                width: (x1 - x0).max(0.0),
563
371
                height: (y1 - y0).max(0.0),
564
371
            })
565
        }
566
        (Some(cur), None) => Some(cur),
567
106
        (None, new) => new,
568
    }
569
477
}
570

            
571
impl AzRect {
572
16749
    pub(crate) fn from_xywh(x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
573
16749
        if w <= 0.0
574
16749
            || h <= 0.0
575
16749
            || !x.is_finite()
576
16749
            || !y.is_finite()
577
16749
            || !w.is_finite()
578
16749
            || !h.is_finite()
579
        {
580
            return None;
581
16749
        }
582
16749
        Some(Self {
583
16749
            x,
584
16749
            y,
585
16749
            width: w,
586
16749
            height: h,
587
16749
        })
588
16749
    }
589

            
590
    /// Intersect this rect with a clip rect. Returns None if fully clipped.
591
6519
    pub(crate) fn clip(&self, clip: &Self) -> Option<Self> {
592
6519
        let x1 = self.x.max(clip.x);
593
6519
        let y1 = self.y.max(clip.y);
594
6519
        let x2 = (self.x + self.width).min(clip.x + clip.width);
595
6519
        let y2 = (self.y + self.height).min(clip.y + clip.height);
596
6519
        if x2 > x1 && y2 > y1 {
597
1060
            Some(Self {
598
1060
                x: x1,
599
1060
                y: y1,
600
1060
                width: x2 - x1,
601
1060
                height: y2 - y1,
602
1060
            })
603
        } else {
604
5459
            None
605
        }
606
6519
    }
607
}
608

            
609
// ============================================================================
610
// AGG helper: fill a PathStorage with a solid color into an AzulPixmap
611
// ============================================================================
612

            
613
16589
pub fn agg_fill_path(
614
16589
    pixmap: &mut AzulPixmap,
615
16589
    path: &mut dyn VertexSource,
616
16589
    color: &Rgba8,
617
16589
    rule: FillingRule,
618
16589
) {
619
16589
    agg_fill_path_clipped(pixmap, path, color, rule, None);
620
16589
}
621

            
622
/// Fill a path with an optional pixel-level clip box.
623
///
624
/// When `clip` is `Some`, `RendererBase::clip_box_i()` restricts all
625
/// scanline output to the clip region.  This handles scroll-frame clips,
626
/// border-radius is TODO (would need a mask), transforms are handled by
627
/// transforming the clip box through the inverse transform before setting it.
628
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // bounded pixel/coord/colour/glyph cast
629
17066
pub fn agg_fill_path_clipped(
630
17066
    pixmap: &mut AzulPixmap,
631
17066
    path: &mut dyn VertexSource,
632
17066
    color: &Rgba8,
633
17066
    rule: FillingRule,
634
17066
    clip: Option<AzRect>,
635
17066
) {
636
17066
    let w = pixmap.width;
637
17066
    let h = pixmap.height;
638
17066
    let stride = (w * 4) as i32;
639
17066
    let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
640
17066
    let mut pf = PixfmtRgba32::new(&mut ra);
641
17066
    let mut rb = RendererBase::new(pf);
642
17066
    if let Some(c) = clip {
643
        rb.clip_box_i(
644
            c.x as i32,
645
            c.y as i32,
646
            (c.x + c.width) as i32 - 1,
647
            (c.y + c.height) as i32 - 1,
648
        );
649
17066
    }
650
17066
    let mut ras = RasterizerScanlineAa::new();
651
17066
    ras.filling_rule(rule);
652
17066
    ras.add_path(path, 0);
653
17066
    let mut sl = ScanlineU8::new();
654
17066
    render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
655
17066
}
656

            
657
#[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)
658
fn agg_fill_transformed_path(
659
    pixmap: &mut AzulPixmap,
660
    path: &mut PathStorage,
661
    color: &Rgba8,
662
    rule: FillingRule,
663
    transform: &TransAffine,
664
) {
665
    agg_fill_transformed_path_clipped(pixmap, path, color, rule, transform, None);
666
}
667

            
668
#[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)
669
fn agg_fill_transformed_path_clipped(
670
    pixmap: &mut AzulPixmap,
671
    path: &mut PathStorage,
672
    color: &Rgba8,
673
    rule: FillingRule,
674
    transform: &TransAffine,
675
    clip: Option<AzRect>,
676
) {
677
    if transform.is_identity(IDENTITY_EPSILON_F64) {
678
        agg_fill_path_clipped(pixmap, path, color, rule, clip);
679
    } else {
680
        let mut transformed = ConvTransform::new(path, *transform);
681
        agg_fill_path_clipped(pixmap, &mut transformed, color, rule, clip);
682
    }
683
}
684

            
685
// ============================================================================
686
// AGG helper: fill a path with a gradient into an AzulPixmap
687
// ============================================================================
688

            
689
fn agg_fill_gradient<G: GradientFunction>(
690
    pixmap: &mut AzulPixmap,
691
    path: &mut dyn VertexSource,
692
    lut: &GradientLut,
693
    gradient_fn: G,
694
    transform: TransAffine,
695
    d1: f64,
696
    d2: f64,
697
) {
698
    agg_fill_gradient_clipped(pixmap, path, lut, gradient_fn, transform, d1, d2, None);
699
}
700

            
701
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // bounded pixel/coord/colour/glyph cast
702
53
pub fn agg_fill_gradient_clipped<G: GradientFunction>(
703
53
    pixmap: &mut AzulPixmap,
704
53
    path: &mut dyn VertexSource,
705
53
    lut: &GradientLut,
706
53
    gradient_fn: G,
707
53
    transform: TransAffine,
708
53
    d1: f64,
709
53
    d2: f64,
710
53
    clip: Option<AzRect>,
711
53
) {
712
53
    let w = pixmap.width;
713
53
    let h = pixmap.height;
714
53
    let stride = (w * 4) as i32;
715
53
    let mut ra = unsafe { RowAccessor::new_with_buf(pixmap.data.as_mut_ptr(), w, h, stride) };
716
53
    let mut pf = PixfmtRgba32::new(&mut ra);
717
53
    let mut rb = RendererBase::new(pf);
718
53
    if let Some(c) = clip {
719
        rb.clip_box_i(
720
            c.x as i32,
721
            c.y as i32,
722
            (c.x + c.width) as i32 - 1,
723
            (c.y + c.height) as i32 - 1,
724
        );
725
53
    }
726
53
    let mut ras = RasterizerScanlineAa::new();
727
53
    ras.filling_rule(FillingRule::NonZero);
728
53
    ras.add_path(path, 0);
729
53
    let mut sl = ScanlineU8::new();
730

            
731
53
    let interp = SpanInterpolatorLinear::new(transform);
732
53
    let mut sg = SpanGradient::new(interp, gradient_fn, lut, d1, d2);
733
53
    let mut alloc = SpanAllocator::<Rgba8>::new();
734
53
    render_scanlines_aa(&mut ras, &mut sl, &mut rb, &mut alloc, &mut sg);
735
53
}
736

            
737
// ============================================================================
738
// Gradient helpers
739
// ============================================================================
740

            
741
/// Alpha-blend one premultiplied-alpha RGBA buffer onto another at (dx, dy).
742
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
743
215
pub fn blit_buffer(dst: &mut AzulPixmap, src: &[u8], src_w: u32, src_h: u32, dx: i32, dy: i32) {
744
215
    let dw = dst.width as i32;
745
215
    let dh = dst.height as i32;
746

            
747
16332
    for py in 0..src_h as i32 {
748
16332
        let ty = dy + py;
749
16332
        if ty < 0 || ty >= dh {
750
            continue;
751
16332
        }
752
1268512
        for px in 0..src_w as i32 {
753
1268512
            let tx = dx + px;
754
1268512
            if tx < 0 || tx >= dw {
755
                continue;
756
1268512
            }
757

            
758
1268512
            let si = ((py as u32 * src_w + px as u32) * 4) as usize;
759
1268512
            let di = ((ty as u32 * dst.width + tx as u32) * 4) as usize;
760

            
761
1268512
            if si + 3 >= src.len() || di + 3 >= dst.data.len() {
762
                continue;
763
1268512
            }
764

            
765
1268512
            let sa = u32::from(src[si + 3]);
766
1268512
            if sa == 0 {
767
69218
                continue;
768
1199294
            }
769
1199294
            if sa == 255 {
770
248
                dst.data[di] = src[si];
771
248
                dst.data[di + 1] = src[si + 1];
772
248
                dst.data[di + 2] = src[si + 2];
773
248
                dst.data[di + 3] = 255;
774
1199046
            } else {
775
1199046
                // Premultiplied-alpha compositing: src RGB already premultiplied by AGG
776
1199046
                let inv_sa = 255 - sa;
777
1199046
                dst.data[di] =
778
1199046
                    ((u32::from(src[si]) + u32::from(dst.data[di]) * inv_sa / 255).min(255)) as u8;
779
1199046
                dst.data[di + 1] =
780
1199046
                    ((u32::from(src[si + 1]) + u32::from(dst.data[di + 1]) * inv_sa / 255).min(255)) as u8;
781
1199046
                dst.data[di + 2] =
782
1199046
                    ((u32::from(src[si + 2]) + u32::from(dst.data[di + 2]) * inv_sa / 255).min(255)) as u8;
783
1199046
                dst.data[di + 3] = ((sa + u32::from(dst.data[di + 3]) * inv_sa / 255).min(255)) as u8;
784
1199046
            }
785
        }
786
    }
787
215
}
788

            
789
// ============================================================================
790
// Image mask clipping
791
// ============================================================================
792

            
793
/// Take a snapshot of a rectangular region of the pixmap.
794
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded pixel/coord/colour/glyph cast
795
266
#[must_use] pub fn snapshot_region(pixmap: &AzulPixmap, x: i32, y: i32, w: u32, h: u32) -> Vec<u8> {
796
266
    let pw = pixmap.width as i32;
797
266
    let ph = pixmap.height as i32;
798
266
    let mut snap = vec![0u8; (w as usize) * (h as usize) * 4];
799

            
800
26600
    for py in 0..h as i32 {
801
26600
        let sy = y + py;
802
26600
        if sy < 0 || sy >= ph {
803
            continue;
804
26600
        }
805
3185000
        for px in 0..w as i32 {
806
3185000
            let sx = x + px;
807
3185000
            if sx < 0 || sx >= pw {
808
                continue;
809
3185000
            }
810
3185000
            let si = ((sy as u32 * pixmap.width + sx as u32) * 4) as usize;
811
3185000
            let di = ((py as u32 * w + px as u32) * 4) as usize;
812
3185000
            if si + 3 < pixmap.data.len() && di + 3 < snap.len() {
813
3185000
                snap[di] = pixmap.data[si];
814
3185000
                snap[di + 1] = pixmap.data[si + 1];
815
3185000
                snap[di + 2] = pixmap.data[si + 2];
816
3185000
                snap[di + 3] = pixmap.data[si + 3];
817
3185000
            }
818
        }
819
    }
820
266
    snap
821
266
}
822

            
823
/// Overwrite (direct copy, no alpha blending) a `w`×`h` RGBA region of `dst` at
824
/// `(x, y)` with the pixels in `src`.
825
///
826
/// Out-of-bounds pixels are skipped. This is the inverse of [`snapshot_region`]
827
/// and is used to write a filtered backdrop copy back into the output buffer for
828
/// `backdrop-filter`.
829
// bounded image-dimension / non-negative-loop-index coordinate casts
830
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
831
1
pub fn write_region(dst: &mut AzulPixmap, src: &[u8], w: u32, h: u32, x: i32, y: i32) {
832
1
    let dw = dst.width as i32;
833
1
    let dh = dst.height as i32;
834
100
    for py in 0..h as i32 {
835
100
        let dy = y + py;
836
100
        if dy < 0 || dy >= dh {
837
            continue;
838
100
        }
839
5000
        for px in 0..w as i32 {
840
5000
            let dx = x + px;
841
5000
            if dx < 0 || dx >= dw {
842
                continue;
843
5000
            }
844
5000
            let si = ((py as u32 * w + px as u32) * 4) as usize;
845
5000
            let di = ((dy as u32 * dst.width + dx as u32) * 4) as usize;
846
5000
            if si + 3 < src.len() && di + 3 < dst.data.len() {
847
5000
                dst.data[di] = src[si];
848
5000
                dst.data[di + 1] = src[si + 1];
849
5000
                dst.data[di + 2] = src[si + 2];
850
5000
                dst.data[di + 3] = src[si + 3];
851
5000
            }
852
        }
853
    }
854
1
}
855

            
856
#[must_use] pub fn union_rect(a: &LogicalRect, b: &LogicalRect) -> LogicalRect {
857
    let x = a.origin.x.min(b.origin.x);
858
    let y = a.origin.y.min(b.origin.y);
859
    let right = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
860
    let bottom = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
861
    LogicalRect {
862
        origin: LogicalPosition { x, y },
863
        size: LogicalSize {
864
            width: right - x,
865
            height: bottom - y,
866
        },
867
    }
868
}
869

            
870
15159
#[must_use] pub fn logical_rect_to_az_rect(bounds: &LogicalRect, dpi_factor: f32) -> Option<AzRect> {
871
15159
    let x = bounds.origin.x * dpi_factor;
872
15159
    let y = bounds.origin.y * dpi_factor;
873
15159
    let width = bounds.size.width * dpi_factor;
874
15159
    let height = bounds.size.height * dpi_factor;
875

            
876
15159
    AzRect::from_xywh(x, y, width, height)
877
15159
}
878