use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CaptureClip { pub x: f64, pub y: f64, pub width: f64, pub height: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContentLayout { pub x: f64, pub y: f64, pub width: f64, pub height: f64, } #[tauri::command] pub fn browser_capture_viewport(app: AppHandle, layout: ContentLayout) -> Result { if layout.width < 10.0 || layout.height < 10.0 { return Err("content area is too small to capture".into()); } let window = app.get_window("main").ok_or("main window not found")?; let scale = window.scale_factor().map_err(|e| format!("scale_factor: {e}"))?; let outer = window.outer_position().map_err(|e| format!("outer_position: {e}"))?; let abs_x = outer.x + (layout.x * scale).round() as i32; let abs_y = outer.y + (layout.y * scale).round() as i32; let abs_w = (layout.width * scale).round().max(1.0) as u32; let abs_h = (layout.height * scale).round().max(1.0) as u32; capture_screen_region(abs_x, abs_y, abs_w, abs_h) } #[tauri::command] pub fn browser_capture_clip(app: AppHandle, layout: ContentLayout, clip: CaptureClip) -> Result { if clip.width < 1.0 || clip.height < 1.0 { return Err("clip area is too small".into()); } if layout.width < 10.0 || layout.height < 10.0 { return Err("content area is too small".into()); } let window = app.get_window("main").ok_or("main window not found")?; let scale = window.scale_factor().map_err(|e| format!("scale_factor: {e}"))?; let outer = window.outer_position().map_err(|e| format!("outer_position: {e}"))?; let abs_x = outer.x + ((layout.x + clip.x) * scale).round() as i32; let abs_y = outer.y + ((layout.y + clip.y) * scale).round() as i32; let abs_w = (clip.width * scale).round().max(1.0) as u32; let abs_h = (clip.height * scale).round().max(1.0) as u32; capture_screen_region(abs_x, abs_y, abs_w, abs_h) } #[tauri::command] pub fn browser_capture_full_page(app: AppHandle, layout: ContentLayout) -> Result { browser_capture_viewport(app, layout) } fn capture_screen_region(x: i32, y: i32, width: u32, height: u32) -> Result { use base64::{engine::general_purpose, Engine as _}; use screenshots::Screen; if width < 1 || height < 1 { return Err("capture region is empty".into()); } let screens = Screen::all().map_err(|e| format!("failed to enumerate screens: {e}"))?; let cx = x + (width as i32 / 2); let cy = y + (height as i32 / 2); let screen = screens.iter().find(|s| { let di = s.display_info; let sx = di.x as i32; let sy = di.y as i32; let sw = di.width as i32; let sh = di.height as i32; cx >= sx && cy >= sy && cx < sx + sw && cy < sy + sh }).or_else(|| screens.first()).ok_or("no screen found")?; let capture = screen.capture_area(x, y, width, height) .map_err(|e| format!("screen capture failed (x={x}, y={y}, w={width}, h={height}): {e}"))?; let cw = capture.width(); let ch = capture.height(); let raw = capture.into_raw(); let img_buf = image::RgbaImage::from_raw(cw, ch, raw) .ok_or_else(|| "failed to create image buffer".to_string())?; let dynamic = image::DynamicImage::ImageRgba8(img_buf); let mut png_bytes: Vec = Vec::new(); dynamic.write_to(&mut std::io::Cursor::new(&mut png_bytes), image::ImageFormat::Png) .map_err(|e| format!("PNG encode failed: {e}"))?; let b64 = general_purpose::STANDARD.encode(&png_bytes); Ok(format!("data:image/png;base64,{b64}")) }