agent-image-diff 0.2.4

Structured image diff with JSON output for agent workflows
Documentation
use image::{Rgba, RgbaImage};

use crate::region::Region;

const BOX_FILL_ALPHA: f64 = 0.15;
const BOX_BORDER_WIDTH: u32 = 2;

/// 16 visually distinct colors for per-region coloring.
const PALETTE: [[u8; 3]; 16] = [
    [230, 25, 75],    // red
    [60, 180, 75],    // green
    [0, 130, 200],    // blue
    [255, 225, 25],   // yellow
    [245, 130, 48],   // orange
    [145, 30, 180],   // purple
    [70, 240, 240],   // cyan
    [240, 50, 230],   // magenta
    [210, 245, 60],   // lime
    [250, 190, 212],  // pink
    [0, 128, 128],    // teal
    [220, 190, 255],  // lavender
    [170, 110, 40],   // brown
    [255, 250, 200],  // beige
    [128, 0, 0],      // maroon
    [170, 255, 195],  // mint
];

/// Generate a visual diff image showing the candidate with highlighted change regions.
///
/// The candidate image is shown at full fidelity. Each region's bounding box
/// gets a semi-transparent color fill and a solid border outline, making it
/// easy to see exactly which areas of the page changed.
pub fn render_diff_image(
    candidate: &RgbaImage,
    _diff_mask: &[bool],
    _component_labels: &[u32],
    regions: &[Region],
) -> RgbaImage {
    let (width, height) = candidate.dimensions();
    let mut output = candidate.clone();

    // Draw semi-transparent fill over each region's bounding box
    for (idx, region) in regions.iter().enumerate() {
        let bb = &region.bounding_box;
        let c = PALETTE[idx % PALETTE.len()];
        let x_end = (bb.x + bb.width).min(width);
        let y_end = (bb.y + bb.height).min(height);

        for y in bb.y..y_end {
            for x in bb.x..x_end {
                let px = output.get_pixel(x, y);
                let r = (c[0] as f64 * BOX_FILL_ALPHA + px[0] as f64 * (1.0 - BOX_FILL_ALPHA)) as u8;
                let g = (c[1] as f64 * BOX_FILL_ALPHA + px[1] as f64 * (1.0 - BOX_FILL_ALPHA)) as u8;
                let b = (c[2] as f64 * BOX_FILL_ALPHA + px[2] as f64 * (1.0 - BOX_FILL_ALPHA)) as u8;
                output.put_pixel(x, y, Rgba([r, g, b, 255]));
            }
        }

        // Draw solid border
        draw_rect(&mut output, bb.x, bb.y, bb.width, bb.height, Rgba([c[0], c[1], c[2], 255]), BOX_BORDER_WIDTH);
    }

    output
}

/// Draw a rectangle outline on the image with a given stroke width.
fn draw_rect(img: &mut RgbaImage, x: u32, y: u32, w: u32, h: u32, color: Rgba<u8>, stroke: u32) {
    let (img_w, img_h) = img.dimensions();
    let x0 = x.saturating_sub(stroke);
    let y0 = y.saturating_sub(stroke);
    let x1 = (x + w + stroke - 1).min(img_w - 1);
    let y1 = (y + h + stroke - 1).min(img_h - 1);

    // Top and bottom edges
    for py in 0..stroke {
        for px in x0..=x1 {
            let top = y0 + py;
            let bot = y1 - py;
            if top < img_h {
                img.put_pixel(px, top, color);
            }
            if bot < img_h {
                img.put_pixel(px, bot, color);
            }
        }
    }
    // Left and right edges
    for px in 0..stroke {
        for py in y0..=y1 {
            let left = x0 + px;
            let right = x1 - px;
            if left < img_w {
                img.put_pixel(left, py, color);
            }
            if right < img_w {
                img.put_pixel(right, py, color);
            }
        }
    }
}