Skip to main content

fotos_lib/capture/
xcap_backend.rs

1/// Screenshot capture via xcap crate.
2///
3/// Used on X11 Linux and Windows where direct capture APIs are available
4/// without requiring a portal.
5use anyhow::Result;
6use image::{DynamicImage, ImageBuffer, Rgba};
7use xcap::{Monitor, Window};
8
9pub async fn capture_fullscreen() -> Result<DynamicImage> {
10    // Run xcap in a blocking task to avoid nested runtime issues
11    // xcap uses zbus which creates a tokio runtime internally on Wayland
12    tokio::task::spawn_blocking(|| {
13        // Capture all monitors and composite into a single image
14        let monitors = Monitor::all()?;
15
16        if monitors.is_empty() {
17            anyhow::bail!("No monitors detected");
18        }
19
20        // Collect monitor geometry using actual position offsets.
21        // This correctly handles vertical stacking, non-contiguous monitors,
22        // and negative offsets (e.g. a monitor placed to the left of the primary).
23        struct MonitorGeom {
24            x: i32,
25            y: i32,
26            width: u32,
27            height: u32,
28            image: ImageBuffer<Rgba<u8>, Vec<u8>>,
29        }
30
31        let mut geoms: Vec<MonitorGeom> = Vec::with_capacity(monitors.len());
32        for monitor in monitors {
33            geoms.push(MonitorGeom {
34                x: monitor.x()?,
35                y: monitor.y()?,
36                width: monitor.width()?,
37                height: monitor.height()?,
38                image: monitor.capture_image()?,
39            });
40        }
41
42        // Compute bounding box across all monitor positions.
43        let min_x = geoms.iter().map(|g| g.x).min().unwrap();
44        let min_y = geoms.iter().map(|g| g.y).min().unwrap();
45        let max_x = geoms.iter().map(|g| g.x + g.width as i32).max().unwrap();
46        let max_y = geoms.iter().map(|g| g.y + g.height as i32).max().unwrap();
47
48        let canvas_w = (max_x - min_x) as u32;
49        let canvas_h = (max_y - min_y) as u32;
50
51        let mut composite = ImageBuffer::from_pixel(canvas_w, canvas_h, Rgba([0, 0, 0, 255]));
52
53        for geom in geoms {
54            let offset_x = (geom.x - min_x) as i64;
55            let offset_y = (geom.y - min_y) as i64;
56            image::imageops::overlay(&mut composite, &geom.image, offset_x, offset_y);
57        }
58
59        Ok(DynamicImage::ImageRgba8(composite))
60    })
61    .await
62    .map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
63}
64
65pub async fn capture_monitor(index: u32) -> Result<DynamicImage> {
66    tokio::task::spawn_blocking(move || {
67        let monitors = Monitor::all()?;
68        let monitor = monitors
69            .into_iter()
70            .nth(index as usize)
71            .ok_or_else(|| anyhow::anyhow!("Monitor index {} out of range", index))?;
72        let image = monitor.capture_image()?;
73        Ok(DynamicImage::ImageRgba8(image))
74    })
75    .await
76    .map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
77}
78
79pub async fn capture_window(window_id: u32) -> Result<DynamicImage> {
80    tokio::task::spawn_blocking(move || {
81        let windows = Window::all()?;
82        let window = windows
83            .into_iter()
84            .find(|w| w.id().ok() == Some(window_id))
85            .ok_or_else(|| anyhow::anyhow!("No window found with id {}", window_id))?;
86        if window.is_minimized()? {
87            anyhow::bail!("Window {} is minimized and cannot be captured", window_id);
88        }
89        let image = window.capture_image()?;
90        Ok(DynamicImage::ImageRgba8(image))
91    })
92    .await
93    .map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
94}