use std::collections::HashMap;
use image::RgbImage;
use crate::element::Element;
pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
if r >> 4 == r & 0x0F && g >> 4 == g & 0x0F && b >> 4 == b & 0x0F {
format!("#{:X}{:X}{:X}", r >> 4, g >> 4, b >> 4)
} else {
format!("#{:02X}{:02X}{:02X}", r, g, b)
}
}
fn quantize_color(r: u8, g: u8, b: u8, bits: u8) -> (u8, u8, u8) {
if bits >= 8 {
return (r, g, b);
}
let mask = !((1u8 << bits) - 1u8);
(r & mask, g & mask, b & mask)
}
fn quantized_color_distance(a: &(u8, u8, u8), b: &(u8, u8, u8)) -> u32 {
let dr = a.0 as i32 - b.0 as i32;
let dg = a.1 as i32 - b.1 as i32;
let db = a.2 as i32 - b.2 as i32;
(dr * dr + dg * dg + db * db) as u32
}
fn average_color_of_pixels(pixels: &[(u8, u8, u8)]) -> (u8, u8, u8) {
if pixels.is_empty() {
return (255, 255, 255);
}
let n = pixels.len() as u32;
let sum_r: u32 = pixels.iter().map(|p| p.0 as u32).sum();
let sum_g: u32 = pixels.iter().map(|p| p.1 as u32).sum();
let sum_b: u32 = pixels.iter().map(|p| p.2 as u32).sum();
((sum_r / n) as u8, (sum_g / n) as u8, (sum_b / n) as u8)
}
fn sample_border_pixels_detailed(
img: &RgbImage,
x: i32,
y: i32,
w: i32,
h: i32,
bits: u8,
) -> HashMap<(u8, u8, u8), (u32, Vec<(u8, u8, u8)>)> {
let mut buckets: HashMap<(u8, u8, u8), (u32, Vec<(u8, u8, u8)>)> = HashMap::new();
let step = 1.max((w.max(h) / 20) as u32);
let top_y = y as u32;
let bottom_y = (y + h - 1) as u32;
for px in (0..w as u32).step_by(step as usize) {
let px_abs = (x as u32) + px;
if top_y < img.height() && px_abs < img.width() {
let p = img.get_pixel(px_abs, top_y);
let key = quantize_color(p[0], p[1], p[2], bits);
let entry = buckets.entry(key).or_insert((0, Vec::new()));
entry.0 += 1;
entry.1.push((p[0], p[1], p[2]));
}
if bottom_y < img.height() && px_abs < img.width() {
let p = img.get_pixel(px_abs, bottom_y);
let key = quantize_color(p[0], p[1], p[2], bits);
let entry = buckets.entry(key).or_insert((0, Vec::new()));
entry.0 += 1;
entry.1.push((p[0], p[1], p[2]));
}
}
let left_x = x as u32;
let right_x = (x + w - 1) as u32;
for py in (1..(h - 1).max(1) as u32).step_by(step as usize) {
let py_abs = (y as u32) + py;
if left_x < img.width() && py_abs < img.height() {
let p = img.get_pixel(left_x, py_abs);
let key = quantize_color(p[0], p[1], p[2], bits);
let entry = buckets.entry(key).or_insert((0, Vec::new()));
entry.0 += 1;
entry.1.push((p[0], p[1], p[2]));
}
if right_x < img.width() && py_abs < img.height() {
let p = img.get_pixel(right_x, py_abs);
let key = quantize_color(p[0], p[1], p[2], bits);
let entry = buckets.entry(key).or_insert((0, Vec::new()));
entry.0 += 1;
entry.1.push((p[0], p[1], p[2]));
}
}
buckets
}
pub fn detect_background_color(img: &RgbImage, x: i32, y: i32, w: i32, h: i32) -> String {
if w <= 0 || h <= 0 {
return "#FFFFFF".to_string();
}
let bits = 6;
let buckets = sample_border_pixels_detailed(img, x, y, w, h, bits);
let (_max_q, (_, pixels)) = buckets
.into_iter()
.max_by_key(|(_, (count, _))| *count)
.unwrap_or_else(|| {
let mask = !((1u8 << bits) - 1u8);
let default_q = (255u8 & mask, 255u8 & mask, 255u8 & mask);
(default_q, (0, vec![(255, 255, 255)]))
});
let (r, g, b) = average_color_of_pixels(&pixels);
rgb_to_hex(r, g, b)
}
pub fn detect_dominant_color(img: &RgbImage, x: i32, y: i32, w: i32, h: i32) -> String {
if w <= 0 || h <= 0 || w > img.width() as i32 || h > img.height() as i32 {
return "#000000".to_string();
}
let x = x.max(0).min(img.width() as i32 - 1);
let y = y.max(0).min(img.height() as i32 - 1);
let w = w.min(img.width() as i32 - x);
let h = h.min(img.height() as i32 - y);
if w <= 1 || h <= 1 {
return "#000000".to_string();
}
let bits = 4;
let border_buckets = sample_border_pixels_detailed(img, x, y, w, h, bits);
let (bg_q, (_, bg_pixels)) = border_buckets
.into_iter()
.max_by_key(|(_, (count, _))| *count)
.unwrap_or_else(|| {
((240, 240, 240), (0, vec![(255, 255, 255)]))
});
let mut all_buckets: HashMap<(u8, u8, u8), (u32, Vec<(u8, u8, u8)>)> = HashMap::new();
let area = w as u64 * h as u64;
let step = if area > 50000 {
((area / 5000) as f64).sqrt().round().max(1.0) as u32
} else {
1
};
for py in (0..h as u32).step_by(step as usize) {
for px in (0..w as u32).step_by(step as usize) {
let px_abs = (x as u32) + px;
let py_abs = (y as u32) + py;
if px_abs >= img.width() || py_abs >= img.height() {
continue;
}
let p = img.get_pixel(px_abs, py_abs);
let q = quantize_color(p[0], p[1], p[2], bits);
let entry = all_buckets.entry(q).or_insert((0, Vec::new()));
entry.0 += 1;
entry.1.push((p[0], p[1], p[2]));
}
}
const MIN_CONTRAST: u32 = 3 * (32u32 * 32u32);
let mut best_count = 0u32;
let mut best_pixels: Option<Vec<(u8, u8, u8)>> = None;
for (_q, (count, pixels)) in &all_buckets {
let dist = quantized_color_distance(&bg_q, _q);
if dist >= MIN_CONTRAST && *count > best_count {
best_count = *count;
best_pixels = Some(pixels.clone());
}
}
if let Some(pixels) = best_pixels {
let (r, g, b) = average_color_of_pixels(&pixels);
rgb_to_hex(r, g, b)
} else {
let mut all_pixels = bg_pixels;
for (_, (_, pixels)) in &all_buckets {
all_pixels.extend(pixels.iter());
}
let (r, g, b) = average_color_of_pixels(&all_pixels);
rgb_to_hex(r, g, b)
}
}
pub fn detect_element_color(img: &RgbImage, element: &Element) -> String {
let (x, y, x2, y2) = element.put_bbox();
let w = x2 - x;
let h = y2 - y;
let hex = match element.class.as_str() {
"Text" | "Icon" => detect_dominant_color(img, x, y, w, h),
_ => detect_background_color(img, x, y, w, h),
};
match element.class.as_str() {
"Text" | "Icon" => format!("fg({})", hex),
_ => format!("bg({})", hex),
}
}
pub fn detect_colors(img: &RgbImage, elements: &mut [Element]) {
for element in elements.iter_mut() {
let color = detect_element_color(img, element);
element.color = Some(color);
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::Rgb;
#[test]
fn test_rgb_to_hex() {
assert_eq!(rgb_to_hex(255, 0, 0), "#F00");
assert_eq!(rgb_to_hex(0, 255, 0), "#0F0");
assert_eq!(rgb_to_hex(0, 0, 255), "#00F");
assert_eq!(rgb_to_hex(255, 255, 255), "#FFF");
assert_eq!(rgb_to_hex(0, 0, 0), "#000");
assert_eq!(rgb_to_hex(170, 187, 204), "#ABC");
assert_eq!(rgb_to_hex(18, 52, 86), "#123456");
assert_eq!(rgb_to_hex(255, 128, 64), "#FF8040");
assert_eq!(rgb_to_hex(200, 100, 50), "#C86432");
}
#[test]
fn test_quantize_color() {
assert_eq!(quantize_color(255, 255, 255, 4), (240, 240, 240));
assert_eq!(quantize_color(128, 128, 128, 4), (128, 128, 128));
assert_eq!(quantize_color(0, 0, 0, 4), (0, 0, 0));
assert_eq!(quantize_color(123, 45, 67, 8), (123, 45, 67));
}
#[test]
fn test_average_color() {
let pixels = vec![
(255, 0, 0),
(255, 0, 0),
(0, 255, 0),
];
let (r, g, b) = average_color_of_pixels(&pixels);
assert_eq!((r, g, b), (170, 85, 0));
}
#[test]
fn test_detect_background_solid() {
let mut img = RgbImage::new(50, 50);
for y in 0..50 {
for x in 0..50 {
img.put_pixel(x, y, Rgb([0, 0, 255]));
}
}
let color = detect_background_color(&img, 0, 0, 50, 50);
assert_eq!(color, "#00F");
}
#[test]
fn test_detect_dominant_text() {
let mut img = RgbImage::new(30, 30);
for y in 0..30 {
for x in 0..30 {
img.put_pixel(x, y, Rgb([255, 255, 255]));
}
}
for y in 10..20 {
for x in 10..20 {
img.put_pixel(x, y, Rgb([0, 0, 0]));
}
}
let color = detect_dominant_color(&img, 0, 0, 30, 30);
assert!(
color == "#000" || color == "#080808" || color == "#101010",
"Expected black-ish, got {}",
color
);
}
#[test]
fn test_detect_dominant_icon_green() {
let mut img = RgbImage::new(32, 32);
for y in 0..32 {
for x in 0..32 {
img.put_pixel(x, y, Rgb([200, 200, 200])); }
}
for i in 8..24 {
img.put_pixel(i, i, Rgb([0, 255, 0]));
img.put_pixel(i, 31 - i, Rgb([0, 255, 0]));
}
let color = detect_dominant_color(&img, 0, 0, 32, 32);
assert!(
color == "#0F0" || color == "#00F000" || color.contains("0F0") || color == "#00FF00"
|| color.contains("0FF") || color == "#08F808",
"Expected green-ish, got {}",
color
);
}
#[test]
fn test_small_element() {
let img = RgbImage::new(10, 10);
let color = detect_background_color(&img, 0, 0, 10, 10);
assert!(!color.is_empty());
}
#[test]
fn test_detect_element_color_prefix() {
let mut img = RgbImage::new(50, 50);
for y in 0..50 {
for x in 0..50 {
img.put_pixel(x, y, Rgb([255, 255, 255])); }
}
let btn = Element::from_parts(0, 0, 0, 50, 50, "Button");
let color = detect_element_color(&img, &btn);
assert_eq!(color, "bg(#FFF)", "Button should return bg prefix");
let txt = Element::from_parts(1, 0, 0, 50, 50, "Text");
let color = detect_element_color(&img, &txt);
assert_eq!(color, "fg(#FFF)", "Text should return fg prefix");
let icn = Element::from_parts(2, 0, 0, 50, 50, "Icon");
let color = detect_element_color(&img, &icn);
assert_eq!(color, "fg(#FFF)", "Icon should return fg prefix");
let blk = Element::from_parts(3, 0, 0, 50, 50, "Block");
let color = detect_element_color(&img, &blk);
assert_eq!(color, "bg(#FFF)", "Block should return bg prefix");
}
}