Skip to main content

agent_image_diff/output/
image.rs

1use image::{Rgba, RgbaImage};
2
3use crate::region::Region;
4
5const BOX_FILL_ALPHA: f64 = 0.15;
6const BOX_BORDER_WIDTH: u32 = 2;
7
8/// 16 visually distinct colors for per-region coloring.
9const PALETTE: [[u8; 3]; 16] = [
10    [230, 25, 75],    // red
11    [60, 180, 75],    // green
12    [0, 130, 200],    // blue
13    [255, 225, 25],   // yellow
14    [245, 130, 48],   // orange
15    [145, 30, 180],   // purple
16    [70, 240, 240],   // cyan
17    [240, 50, 230],   // magenta
18    [210, 245, 60],   // lime
19    [250, 190, 212],  // pink
20    [0, 128, 128],    // teal
21    [220, 190, 255],  // lavender
22    [170, 110, 40],   // brown
23    [255, 250, 200],  // beige
24    [128, 0, 0],      // maroon
25    [170, 255, 195],  // mint
26];
27
28/// Generate a visual diff image showing the candidate with highlighted change regions.
29///
30/// The candidate image is shown at full fidelity. Each region's bounding box
31/// gets a semi-transparent color fill and a solid border outline, making it
32/// easy to see exactly which areas of the page changed.
33pub fn render_diff_image(
34    candidate: &RgbaImage,
35    _diff_mask: &[bool],
36    _component_labels: &[u32],
37    regions: &[Region],
38) -> RgbaImage {
39    let (width, height) = candidate.dimensions();
40    let mut output = candidate.clone();
41
42    // Draw semi-transparent fill over each region's bounding box
43    for (idx, region) in regions.iter().enumerate() {
44        let bb = &region.bounding_box;
45        let c = PALETTE[idx % PALETTE.len()];
46        let x_end = (bb.x + bb.width).min(width);
47        let y_end = (bb.y + bb.height).min(height);
48
49        for y in bb.y..y_end {
50            for x in bb.x..x_end {
51                let px = output.get_pixel(x, y);
52                let r = (c[0] as f64 * BOX_FILL_ALPHA + px[0] as f64 * (1.0 - BOX_FILL_ALPHA)) as u8;
53                let g = (c[1] as f64 * BOX_FILL_ALPHA + px[1] as f64 * (1.0 - BOX_FILL_ALPHA)) as u8;
54                let b = (c[2] as f64 * BOX_FILL_ALPHA + px[2] as f64 * (1.0 - BOX_FILL_ALPHA)) as u8;
55                output.put_pixel(x, y, Rgba([r, g, b, 255]));
56            }
57        }
58
59        // Draw solid border
60        draw_rect(&mut output, bb.x, bb.y, bb.width, bb.height, Rgba([c[0], c[1], c[2], 255]), BOX_BORDER_WIDTH);
61    }
62
63    output
64}
65
66/// Draw a rectangle outline on the image with a given stroke width.
67fn draw_rect(img: &mut RgbaImage, x: u32, y: u32, w: u32, h: u32, color: Rgba<u8>, stroke: u32) {
68    let (img_w, img_h) = img.dimensions();
69    let x0 = x.saturating_sub(stroke);
70    let y0 = y.saturating_sub(stroke);
71    let x1 = (x + w + stroke - 1).min(img_w - 1);
72    let y1 = (y + h + stroke - 1).min(img_h - 1);
73
74    // Top and bottom edges
75    for py in 0..stroke {
76        for px in x0..=x1 {
77            let top = y0 + py;
78            let bot = y1 - py;
79            if top < img_h {
80                img.put_pixel(px, top, color);
81            }
82            if bot < img_h {
83                img.put_pixel(px, bot, color);
84            }
85        }
86    }
87    // Left and right edges
88    for px in 0..stroke {
89        for py in y0..=y1 {
90            let left = x0 + px;
91            let right = x1 - px;
92            if left < img_w {
93                img.put_pixel(left, py, color);
94            }
95            if right < img_w {
96                img.put_pixel(right, py, color);
97            }
98        }
99    }
100}