1use std::collections::HashMap;
15use std::sync::Mutex;
16
17use x11rb::connection::Connection;
18use x11rb::protocol::xproto::{ConnectionExt as _, ImageFormat};
19use x11rb::rust_connection::RustConnection;
20use zbus::blocking::Connection as ZbusConnection;
21use zbus::blocking::Proxy;
22use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
23
24use xa11y_core::{Error, Rect, Result, Screenshot, ScreenshotProvider};
25
26pub struct LinuxScreenshot {
28 backend: Backend,
29}
30
31enum Backend {
36 X11(Box<X11Backend>),
37 Wayland { conn: ZbusConnection },
38}
39
40struct X11Backend {
41 conn: Mutex<RustConnection>,
42 root_width: u16,
43 root_height: u16,
44 root: u32,
45}
46
47impl LinuxScreenshot {
48 pub fn new() -> Result<Self> {
49 let display_set = std::env::var_os("DISPLAY").is_some();
50 let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some();
51
52 if display_set {
53 let (conn, screen_num) =
54 RustConnection::connect(None).map_err(|e| Error::Platform {
55 code: -1,
56 message: format!("X11 connect: {e}"),
57 })?;
58 let screen = conn
59 .setup()
60 .roots
61 .get(screen_num)
62 .ok_or_else(|| Error::Platform {
63 code: -1,
64 message: "X server reported no screens".into(),
65 })?;
66 let root = screen.root;
67 let root_width = screen.width_in_pixels;
68 let root_height = screen.height_in_pixels;
69 Ok(Self {
70 backend: Backend::X11(Box::new(X11Backend {
71 conn: Mutex::new(conn),
72 root,
73 root_width,
74 root_height,
75 })),
76 })
77 } else if wayland {
78 let conn = ZbusConnection::session().map_err(|e| Error::Platform {
79 code: -1,
80 message: format!("session bus connect: {e}"),
81 })?;
82 Ok(Self {
83 backend: Backend::Wayland { conn },
84 })
85 } else {
86 Err(Error::Unsupported {
87 feature: "screenshot (no DISPLAY or WAYLAND_DISPLAY set)".into(),
88 })
89 }
90 }
91
92 fn capture_x11(
93 &self,
94 conn: &Mutex<RustConnection>,
95 root: u32,
96 root_w: u16,
97 root_h: u16,
98 rect: Option<Rect>,
99 ) -> Result<Screenshot> {
100 let (x, y, w, h) = match rect {
101 None => (0_i16, 0_i16, root_w, root_h),
102 Some(r) => {
103 let x = i16::try_from(r.x).map_err(|_| Error::Platform {
104 code: -1,
105 message: "rect x out of i16 range".into(),
106 })?;
107 let y = i16::try_from(r.y).map_err(|_| Error::Platform {
108 code: -1,
109 message: "rect y out of i16 range".into(),
110 })?;
111 let w = u16::try_from(r.width).map_err(|_| Error::Platform {
112 code: -1,
113 message: "rect width out of u16 range".into(),
114 })?;
115 let h = u16::try_from(r.height).map_err(|_| Error::Platform {
116 code: -1,
117 message: "rect height out of u16 range".into(),
118 })?;
119 (x, y, w, h)
120 }
121 };
122 if w == 0 || h == 0 {
123 return Err(Error::Platform {
124 code: -1,
125 message: "zero-sized capture rect".into(),
126 });
127 }
128
129 let guard = conn.lock().unwrap_or_else(|e| e.into_inner());
130 let reply = guard
131 .get_image(ImageFormat::Z_PIXMAP, root, x, y, w, h, !0)
132 .map_err(|e| Error::Platform {
133 code: -1,
134 message: format!("GetImage: {e}"),
135 })?
136 .reply()
137 .map_err(|e| Error::Platform {
138 code: -1,
139 message: format!("GetImage reply: {e}"),
140 })?;
141
142 let bpp = (reply.data.len() / (w as usize * h as usize)) as u32;
145 if bpp != 4 {
146 return Err(Error::Platform {
147 code: -1,
148 message: format!("unsupported X11 pixmap layout: {bpp} bytes/pixel (expected 4)"),
149 });
150 }
151
152 let mut rgba = Vec::with_capacity(reply.data.len());
153 for chunk in reply.data.chunks_exact(4) {
154 rgba.push(chunk[2]);
156 rgba.push(chunk[1]);
157 rgba.push(chunk[0]);
158 rgba.push(0xFF);
159 }
160
161 Ok(Screenshot {
162 width: w as u32,
163 height: h as u32,
164 pixels: rgba,
165 scale: 1.0,
166 })
167 }
168
169 fn capture_wayland(&self, conn: &ZbusConnection, rect: Option<Rect>) -> Result<Screenshot> {
170 let proxy = Proxy::new(
171 conn,
172 "org.freedesktop.portal.Desktop",
173 "/org/freedesktop/portal/desktop",
174 "org.freedesktop.portal.Screenshot",
175 )
176 .map_err(|e| Error::Platform {
177 code: -1,
178 message: format!("portal Screenshot proxy: {e}"),
179 })?;
180
181 let mut options: HashMap<&str, Value> = HashMap::new();
182 options.insert("interactive", Value::Bool(false));
183 options.insert("modal", Value::Bool(false));
184
185 let request_path: OwnedObjectPath =
186 proxy
187 .call("Screenshot", &("", options))
188 .map_err(|e| Error::Platform {
189 code: -1,
190 message: format!("portal Screenshot call: {e}"),
191 })?;
192
193 let request = Proxy::new(
194 conn,
195 "org.freedesktop.portal.Desktop",
196 &request_path,
197 "org.freedesktop.portal.Request",
198 )
199 .map_err(|e| Error::Platform {
200 code: -1,
201 message: format!("portal Request proxy: {e}"),
202 })?;
203
204 let mut signals = request
206 .receive_signal("Response")
207 .map_err(|e| Error::Platform {
208 code: -1,
209 message: format!("receive_signal: {e}"),
210 })?;
211 let msg = signals.next().ok_or_else(|| Error::Platform {
212 code: -1,
213 message: "portal Response signal channel closed".into(),
214 })?;
215 let (response, results): (u32, HashMap<String, OwnedValue>) =
216 msg.body().deserialize().map_err(|e| Error::Platform {
217 code: -1,
218 message: format!("portal Response deserialize: {e}"),
219 })?;
220 if response != 0 {
221 return Err(Error::PermissionDenied {
222 instructions: format!("xdg-desktop-portal Screenshot denied (response={response})"),
223 });
224 }
225 let uri_val = results.get("uri").ok_or_else(|| Error::Platform {
226 code: -1,
227 message: "portal Response missing 'uri' key".into(),
228 })?;
229 let uri: String = uri_val
230 .downcast_ref::<String>()
231 .map_err(|_| Error::Platform {
232 code: -1,
233 message: "portal Response 'uri' is not a string".into(),
234 })?;
235 let path = uri.strip_prefix("file://").ok_or_else(|| Error::Platform {
236 code: -1,
237 message: format!("portal URI not file://: {uri}"),
238 })?;
239 let bytes = std::fs::read(path).map_err(|e| Error::Platform {
240 code: e.raw_os_error().unwrap_or(-1) as i64,
241 message: format!("read portal PNG: {e}"),
242 })?;
243 let _ = std::fs::remove_file(path);
245
246 let shot = decode_png_to_rgba(&bytes)?;
247 match rect {
248 None => Ok(shot),
249 Some(r) => crop_rgba(shot, r),
250 }
251 }
252}
253
254impl ScreenshotProvider for LinuxScreenshot {
255 fn capture_full(&self) -> Result<Screenshot> {
256 match &self.backend {
257 Backend::X11(x) => self.capture_x11(&x.conn, x.root, x.root_width, x.root_height, None),
258 Backend::Wayland { conn } => self.capture_wayland(conn, None),
259 }
260 }
261
262 fn capture_region(&self, rect: Rect) -> Result<Screenshot> {
263 match &self.backend {
264 Backend::X11(x) => {
265 self.capture_x11(&x.conn, x.root, x.root_width, x.root_height, Some(rect))
266 }
267 Backend::Wayland { conn } => self.capture_wayland(conn, Some(rect)),
268 }
269 }
270}
271
272fn decode_png_to_rgba(bytes: &[u8]) -> Result<Screenshot> {
273 let decoder = png::Decoder::new(bytes);
274 let mut reader = decoder.read_info().map_err(|e| Error::Platform {
275 code: -1,
276 message: format!("png decode header: {e}"),
277 })?;
278 let info = reader.info().clone();
279 let mut buf = vec![0u8; reader.output_buffer_size()];
280 let frame = reader.next_frame(&mut buf).map_err(|e| Error::Platform {
281 code: -1,
282 message: format!("png decode frame: {e}"),
283 })?;
284 buf.truncate(frame.buffer_size());
285
286 let rgba = match (info.color_type, info.bit_depth) {
287 (png::ColorType::Rgba, png::BitDepth::Eight) => buf,
288 (png::ColorType::Rgb, png::BitDepth::Eight) => {
289 let mut out = Vec::with_capacity((info.width * info.height * 4) as usize);
290 for px in buf.chunks_exact(3) {
291 out.extend_from_slice(&[px[0], px[1], px[2], 0xFF]);
292 }
293 out
294 }
295 (ct, bd) => {
296 return Err(Error::Platform {
297 code: -1,
298 message: format!("unsupported portal PNG format: {ct:?} @ {bd:?}"),
299 });
300 }
301 };
302
303 Ok(Screenshot {
304 width: info.width,
305 height: info.height,
306 pixels: rgba,
307 scale: 1.0,
308 })
309}
310
311fn crop_rgba(shot: Screenshot, rect: Rect) -> Result<Screenshot> {
312 let Screenshot {
313 width: sw,
314 height: sh,
315 pixels,
316 scale,
317 } = shot;
318 let x = rect.x.max(0) as u32;
319 let y = rect.y.max(0) as u32;
320 if x >= sw || y >= sh {
321 return Err(Error::Platform {
322 code: -1,
323 message: "crop rect outside captured image".into(),
324 });
325 }
326 let w = rect.width.min(sw - x);
327 let h = rect.height.min(sh - y);
328 if w == 0 || h == 0 {
329 return Err(Error::Platform {
330 code: -1,
331 message: "crop rect has zero size".into(),
332 });
333 }
334 let mut out = Vec::with_capacity((w * h * 4) as usize);
335 for row in 0..h {
336 let start = ((y + row) * sw + x) as usize * 4;
337 let end = start + (w as usize) * 4;
338 out.extend_from_slice(&pixels[start..end]);
339 }
340 Ok(Screenshot {
341 width: w,
342 height: h,
343 pixels: out,
344 scale,
345 })
346}