| 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<String, String> {
|
| 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<String, String> {
|
| 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<String, String> {
|
| browser_capture_viewport(app, layout)
|
| }
|
|
|
| fn capture_screen_region(x: i32, y: i32, width: u32, height: u32) -> Result<String, String> {
|
| 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<u8> = 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}"))
|
| }
|
|
|