gardesk/garshot / 244da88

Browse files

add region capture with geometry parsing

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
244da886cab5971143d9a1b880efa21f9abd68e0
Parents
e5f6550
Tree
dfc783e

2 changed files

StatusFile+-
M garshot/src/capture/mod.rs 2 0
A garshot/src/capture/region.rs 182 0
garshot/src/capture/mod.rsmodified
@@ -1,5 +1,7 @@
11
 //! Screen capture functionality.
22
 
3
+pub mod region;
34
 pub mod screen;
45
 
6
+pub use region::{capture_region, Region, RegionCaptureResult};
57
 pub use screen::capture_full_screen;
garshot/src/capture/region.rsadded
@@ -0,0 +1,182 @@
1
+//! Region capture functionality.
2
+
3
+use crate::error::{GarshotError, Result};
4
+use crate::x11::{shm::bgra_to_rgba, Connection, ShmCapture};
5
+
6
+/// A rectangular region on the screen.
7
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+pub struct Region {
9
+    pub x: i16,
10
+    pub y: i16,
11
+    pub width: u16,
12
+    pub height: u16,
13
+}
14
+
15
+impl Region {
16
+    /// Create a new region.
17
+    pub fn new(x: i16, y: i16, width: u16, height: u16) -> Self {
18
+        Self { x, y, width, height }
19
+    }
20
+
21
+    /// Parse a geometry string in the format WxH+X+Y or WxH-X-Y.
22
+    ///
23
+    /// Examples: "800x600+100+50", "1920x1080+0+0", "640x480-10-10"
24
+    pub fn from_geometry(s: &str) -> Result<Self> {
25
+        let s = s.trim();
26
+
27
+        // Find the 'x' separator between width and height
28
+        let x_pos = s
29
+            .find('x')
30
+            .ok_or_else(|| GarshotError::InvalidRegion("missing 'x' separator".into()))?;
31
+
32
+        let width: u16 = s[..x_pos]
33
+            .parse()
34
+            .map_err(|_| GarshotError::InvalidRegion("invalid width".into()))?;
35
+
36
+        let rest = &s[x_pos + 1..];
37
+
38
+        // Find the +/- separator for x coordinate
39
+        let sign_pos = rest
40
+            .find(|c| c == '+' || c == '-')
41
+            .ok_or_else(|| GarshotError::InvalidRegion("missing +/- for x coordinate".into()))?;
42
+
43
+        let height: u16 = rest[..sign_pos]
44
+            .parse()
45
+            .map_err(|_| GarshotError::InvalidRegion("invalid height".into()))?;
46
+
47
+        let x_negative = rest.chars().nth(sign_pos) == Some('-');
48
+        let rest = &rest[sign_pos + 1..];
49
+
50
+        // Find the +/- separator for y coordinate
51
+        let sign_pos = rest
52
+            .find(|c| c == '+' || c == '-')
53
+            .ok_or_else(|| GarshotError::InvalidRegion("missing +/- for y coordinate".into()))?;
54
+
55
+        let x_val: i16 = rest[..sign_pos]
56
+            .parse()
57
+            .map_err(|_| GarshotError::InvalidRegion("invalid x coordinate".into()))?;
58
+
59
+        let y_negative = rest.chars().nth(sign_pos) == Some('-');
60
+        let y_val: i16 = rest[sign_pos + 1..]
61
+            .parse()
62
+            .map_err(|_| GarshotError::InvalidRegion("invalid y coordinate".into()))?;
63
+
64
+        let x = if x_negative { -x_val } else { x_val };
65
+        let y = if y_negative { -y_val } else { y_val };
66
+
67
+        Ok(Self { x, y, width, height })
68
+    }
69
+
70
+    /// Clip region to screen bounds.
71
+    pub fn clip_to_screen(&self, screen_width: u16, screen_height: u16) -> Self {
72
+        let x = self.x.max(0);
73
+        let y = self.y.max(0);
74
+
75
+        let max_width = (screen_width as i16 - x).max(0) as u16;
76
+        let max_height = (screen_height as i16 - y).max(0) as u16;
77
+
78
+        let width = self.width.min(max_width);
79
+        let height = self.height.min(max_height);
80
+
81
+        Self { x, y, width, height }
82
+    }
83
+
84
+    /// Check if the region is valid (non-zero dimensions).
85
+    pub fn is_valid(&self) -> bool {
86
+        self.width > 0 && self.height > 0
87
+    }
88
+}
89
+
90
+/// Result of a region capture operation.
91
+pub struct RegionCaptureResult {
92
+    /// RGBA pixel data.
93
+    pub data: Vec<u8>,
94
+    /// Image width in pixels.
95
+    pub width: u32,
96
+    /// Image height in pixels.
97
+    pub height: u32,
98
+    /// The actual region that was captured (may differ from requested due to clipping).
99
+    pub region: Region,
100
+}
101
+
102
+/// Capture a specific region of the screen.
103
+pub fn capture_region(
104
+    conn: &Connection,
105
+    shm: &ShmCapture,
106
+    region: &Region,
107
+) -> Result<RegionCaptureResult> {
108
+    // Clip to screen bounds
109
+    let clipped = region.clip_to_screen(conn.width, conn.height);
110
+
111
+    if !clipped.is_valid() {
112
+        return Err(GarshotError::InvalidRegion(format!(
113
+            "region {}x{}+{}+{} is outside screen bounds",
114
+            region.width, region.height, region.x, region.y
115
+        )));
116
+    }
117
+
118
+    tracing::debug!(
119
+        "Capturing region {}x{}+{}+{} (clipped from {}x{}+{}+{})",
120
+        clipped.width,
121
+        clipped.height,
122
+        clipped.x,
123
+        clipped.y,
124
+        region.width,
125
+        region.height,
126
+        region.x,
127
+        region.y
128
+    );
129
+
130
+    let data = shm.capture(conn, clipped.x, clipped.y, clipped.width, clipped.height)?;
131
+    let rgba = bgra_to_rgba(data);
132
+
133
+    Ok(RegionCaptureResult {
134
+        data: rgba,
135
+        width: clipped.width as u32,
136
+        height: clipped.height as u32,
137
+        region: clipped,
138
+    })
139
+}
140
+
141
+#[cfg(test)]
142
+mod tests {
143
+    use super::*;
144
+
145
+    #[test]
146
+    fn test_region_from_geometry() {
147
+        let r = Region::from_geometry("800x600+100+50").unwrap();
148
+        assert_eq!(r.width, 800);
149
+        assert_eq!(r.height, 600);
150
+        assert_eq!(r.x, 100);
151
+        assert_eq!(r.y, 50);
152
+    }
153
+
154
+    #[test]
155
+    fn test_region_from_geometry_negative() {
156
+        let r = Region::from_geometry("640x480-10-20").unwrap();
157
+        assert_eq!(r.width, 640);
158
+        assert_eq!(r.height, 480);
159
+        assert_eq!(r.x, -10);
160
+        assert_eq!(r.y, -20);
161
+    }
162
+
163
+    #[test]
164
+    fn test_region_clip() {
165
+        let r = Region::new(-10, -10, 100, 100);
166
+        let clipped = r.clip_to_screen(1920, 1080);
167
+        assert_eq!(clipped.x, 0);
168
+        assert_eq!(clipped.y, 0);
169
+        assert_eq!(clipped.width, 90);
170
+        assert_eq!(clipped.height, 90);
171
+    }
172
+
173
+    #[test]
174
+    fn test_region_clip_overflow() {
175
+        let r = Region::new(1900, 1000, 100, 200);
176
+        let clipped = r.clip_to_screen(1920, 1080);
177
+        assert_eq!(clipped.x, 1900);
178
+        assert_eq!(clipped.y, 1000);
179
+        assert_eq!(clipped.width, 20);
180
+        assert_eq!(clipped.height, 80);
181
+    }
182
+}