//! Capture d'écran via xcap. //! //! Fournit la capture du moniteur principal, l'encodage JPEG en base64, //! et un hash perceptuel rapide pour la déduplication des heartbeats. use base64::Engine; use image::codecs::jpeg::JpegEncoder; use image::DynamicImage; use std::io::Cursor; /// Capture le moniteur principal et retourne un DynamicImage. /// /// Utilise xcap pour la capture cross-platform (DXGI sur Windows, X11/Wayland sur Linux). pub fn capture_screenshot() -> Option { let monitors = match xcap::Monitor::all() { Ok(m) => m, Err(e) => { eprintln!("[CAPTURE] Erreur enumeration moniteurs : {}", e); return None; } }; let primary = monitors .into_iter() .find(|m| m.is_primary().unwrap_or(false)); let monitor = match primary { Some(m) => m, None => { eprintln!("[CAPTURE] Aucun moniteur principal trouve"); return None; } }; match monitor.capture_image() { Ok(rgba_image) => Some(DynamicImage::ImageRgba8(rgba_image)), Err(e) => { eprintln!("[CAPTURE] Erreur capture ecran : {}", e); None } } } /// Encode une image en JPEG et retourne le résultat en base64. /// /// La qualité doit être entre 1 (mauvaise) et 100 (excellente). /// 85 est un bon compromis taille/qualité pour le streaming réseau. pub fn screenshot_to_jpeg_base64(img: &DynamicImage, quality: u8) -> String { let rgb = img.to_rgb8(); let mut buffer = Cursor::new(Vec::new()); let mut encoder = JpegEncoder::new_with_quality(&mut buffer, quality); if let Err(e) = encoder.encode( rgb.as_raw(), rgb.width(), rgb.height(), image::ExtendedColorType::Rgb8, ) { eprintln!("[CAPTURE] Erreur encodage JPEG : {}", e); return String::new(); } base64::engine::general_purpose::STANDARD.encode(buffer.into_inner()) } /// Encode une image en JPEG et retourne les bytes bruts. pub fn screenshot_to_jpeg_bytes(img: &DynamicImage, quality: u8) -> Vec { let rgb = img.to_rgb8(); let mut buffer = Cursor::new(Vec::new()); let mut encoder = JpegEncoder::new_with_quality(&mut buffer, quality); if let Err(e) = encoder.encode( rgb.as_raw(), rgb.width(), rgb.height(), image::ExtendedColorType::Rgb8, ) { eprintln!("[CAPTURE] Erreur encodage JPEG : {}", e); return Vec::new(); } buffer.into_inner() } /// Calcule un hash perceptuel rapide pour la déduplication. /// /// Réduit l'image à 16x16 en niveaux de gris, puis calcule /// un hash simple basé sur les pixels. Identique à la logique /// Python (_quick_hash) dans agent_v1. pub fn image_hash(img: &DynamicImage) -> u64 { let small = img.resize_exact(16, 16, image::imageops::FilterType::Nearest); let gray = small.to_luma8(); // Hash FNV-1a simple sur les pixels (rapide, pas besoin de crypto) let mut hash: u64 = 0xcbf29ce484222325; for pixel in gray.as_raw() { hash ^= *pixel as u64; hash = hash.wrapping_mul(0x100000001b3); } hash } /// Retourne les dimensions du moniteur principal (largeur, hauteur). /// /// xcap utilise DXGI sur Windows qui retourne toujours les pixels physiques, /// independamment du DPI awareness. Ceci est coherent avec les coordonnees /// physiques d'enigo quand le process est DPI-aware. pub fn screen_dimensions() -> Option<(u32, u32)> { let monitors = xcap::Monitor::all().ok()?; let primary = monitors .into_iter() .find(|m| m.is_primary().unwrap_or(false))?; let w = primary.width().ok()?; let h = primary.height().ok()?; Some((w, h)) }