1use std::collections::HashMap;
18use std::io::Read as _;
19use std::sync::Mutex;
20
21use x11rb::connection::Connection;
22use x11rb::protocol::xproto::{ConnectionExt as _, ImageFormat};
23use x11rb::rust_connection::RustConnection;
24use zbus::blocking::{Connection as ZbusConnection, MessageIterator, Proxy};
25use zbus::message::Type as MessageType;
26use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
27use zbus::MatchRule;
28
29use xa11y_core::{CaptureMapping, Error, Point, Rect, Result, Screenshot, ScreenshotProvider};
30
31use crate::session::{select_backend, DesktopBackend};
32
33pub struct LinuxScreenshot {
35 backend: Backend,
36}
37
38enum Backend {
43 X11(Box<X11Backend>),
44 Wayland { conn: ZbusConnection },
45}
46
47struct X11Backend {
48 conn: Mutex<RustConnection>,
49 root_width: u16,
50 root_height: u16,
51 root: u32,
52}
53
54impl LinuxScreenshot {
55 pub fn new() -> Result<Self> {
56 match select_backend("XA11Y_LINUX_SCREENSHOT_BACKEND", "screenshot capture")? {
57 DesktopBackend::X11 => {
58 let (conn, screen_num) =
59 RustConnection::connect(None).map_err(|e| Error::Platform {
60 code: -1,
61 message: format!("X11 connect: {e}"),
62 })?;
63 let screen = conn
64 .setup()
65 .roots
66 .get(screen_num)
67 .ok_or_else(|| Error::Platform {
68 code: -1,
69 message: "X server reported no screens".into(),
70 })?;
71 let root = screen.root;
72 let root_width = screen.width_in_pixels;
73 let root_height = screen.height_in_pixels;
74 Ok(Self {
75 backend: Backend::X11(Box::new(X11Backend {
76 conn: Mutex::new(conn),
77 root,
78 root_width,
79 root_height,
80 })),
81 })
82 }
83 DesktopBackend::Wayland => {
84 let conn = ZbusConnection::session().map_err(|e| Error::Platform {
85 code: -1,
86 message: format!("session bus connect for Wayland screenshot portal: {e}"),
87 })?;
88 Ok(Self {
89 backend: Backend::Wayland { conn },
90 })
91 }
92 }
93 }
94
95 fn capture_x11(
96 &self,
97 conn: &Mutex<RustConnection>,
98 root: u32,
99 root_w: u16,
100 root_h: u16,
101 rect: Option<Rect>,
102 ) -> Result<Screenshot> {
103 let (x, y, w, h) = match rect {
104 None => (0_i16, 0_i16, root_w, root_h),
105 Some(r) => {
106 let x = i16::try_from(r.x).map_err(|_| Error::Platform {
107 code: -1,
108 message: "rect x out of i16 range".into(),
109 })?;
110 let y = i16::try_from(r.y).map_err(|_| Error::Platform {
111 code: -1,
112 message: "rect y out of i16 range".into(),
113 })?;
114 let w = u16::try_from(r.width).map_err(|_| Error::Platform {
115 code: -1,
116 message: "rect width out of u16 range".into(),
117 })?;
118 let h = u16::try_from(r.height).map_err(|_| Error::Platform {
119 code: -1,
120 message: "rect height out of u16 range".into(),
121 })?;
122 (x, y, w, h)
123 }
124 };
125 if w == 0 || h == 0 {
126 return Err(Error::Platform {
127 code: -1,
128 message: "zero-sized capture rect".into(),
129 });
130 }
131
132 let guard = conn.lock().unwrap_or_else(|e| e.into_inner());
133 let reply = guard
134 .get_image(ImageFormat::Z_PIXMAP, root, x, y, w, h, !0)
135 .map_err(|e| Error::Platform {
136 code: -1,
137 message: format!("GetImage: {e}"),
138 })?
139 .reply()
140 .map_err(|e| Error::Platform {
141 code: -1,
142 message: format!("GetImage reply: {e}"),
143 })?;
144
145 let bpp = (reply.data.len() / (w as usize * h as usize)) as u32;
148 if bpp != 4 {
149 return Err(Error::Platform {
150 code: -1,
151 message: format!("unsupported X11 pixmap layout: {bpp} bytes/pixel (expected 4)"),
152 });
153 }
154
155 let mut rgba = Vec::with_capacity(reply.data.len());
156 for chunk in reply.data.as_chunks::<4>().0 {
157 rgba.push(chunk[2]);
159 rgba.push(chunk[1]);
160 rgba.push(chunk[0]);
161 rgba.push(0xFF);
162 }
163
164 Ok(Screenshot::new(w as u32, h as u32, rgba, 1.0))
165 }
166
167 fn capture_wayland(&self, conn: &ZbusConnection, rect: Option<Rect>) -> Result<Screenshot> {
168 let proxy = Proxy::new(
169 conn,
170 "org.freedesktop.portal.Desktop",
171 "/org/freedesktop/portal/desktop",
172 "org.freedesktop.portal.Screenshot",
173 )
174 .map_err(|e| Error::Platform {
175 code: -1,
176 message: format!("portal Screenshot proxy: {e}"),
177 })?;
178
179 let mut options: HashMap<&str, Value> = HashMap::new();
180 options.insert("interactive", Value::Bool(false));
181 options.insert("modal", Value::Bool(false));
182 let handle_token = portal_handle_token()?;
183 options.insert("handle_token", Value::from(handle_token.as_str()));
184
185 let response_rule = MatchRule::builder()
191 .msg_type(MessageType::Signal)
192 .interface("org.freedesktop.portal.Request")
193 .map_err(|e| Error::Platform {
194 code: -1,
195 message: format!("portal Response match interface: {e}"),
196 })?
197 .member("Response")
198 .map_err(|e| Error::Platform {
199 code: -1,
200 message: format!("portal Response match member: {e}"),
201 })?
202 .build();
203 let mut responses =
204 MessageIterator::for_match_rule(response_rule, conn, Some(8)).map_err(|e| {
205 Error::Platform {
206 code: -1,
207 message: format!("subscribe to portal Response: {e}"),
208 }
209 })?;
210
211 let request_path: OwnedObjectPath =
212 proxy
213 .call("Screenshot", &("", options))
214 .map_err(|e| Error::Platform {
215 code: -1,
216 message: format!("portal Screenshot call: {e}"),
217 })?;
218
219 let msg = loop {
221 let msg = responses
222 .next()
223 .ok_or_else(|| Error::Platform {
224 code: -1,
225 message: "portal Response signal channel closed".into(),
226 })?
227 .map_err(|e| Error::Platform {
228 code: -1,
229 message: format!("receive portal Response: {e}"),
230 })?;
231 if msg
232 .header()
233 .path()
234 .is_some_and(|path| path.as_str() == request_path.as_str())
235 {
236 break msg;
237 }
238 };
239 let (response, results): (u32, HashMap<String, OwnedValue>) =
240 msg.body().deserialize().map_err(|e| Error::Platform {
241 code: -1,
242 message: format!("portal Response deserialize: {e}"),
243 })?;
244 if response != 0 {
245 return Err(Error::PermissionDenied {
246 instructions: format!("xdg-desktop-portal Screenshot denied (response={response})"),
247 });
248 }
249 let uri_val = results.get("uri").ok_or_else(|| Error::Platform {
250 code: -1,
251 message: "portal Response missing 'uri' key".into(),
252 })?;
253 let uri: String = uri_val
254 .downcast_ref::<String>()
255 .map_err(|_| Error::Platform {
256 code: -1,
257 message: "portal Response 'uri' is not a string".into(),
258 })?;
259 let path = uri.strip_prefix("file://").ok_or_else(|| Error::Platform {
260 code: -1,
261 message: format!("portal URI not file://: {uri}"),
262 })?;
263 let bytes = std::fs::read(path).map_err(|e| Error::Platform {
264 code: e.raw_os_error().unwrap_or(-1) as i64,
265 message: format!("read portal PNG: {e}"),
266 })?;
267 let _ = std::fs::remove_file(path);
269
270 let shot = decode_png_to_rgba(&bytes)?;
271 match rect {
272 None => Ok(shot),
273 Some(r) => crop_rgba(shot, r),
274 }
275 }
276}
277
278fn portal_handle_token() -> Result<String> {
282 let mut entropy = [0_u8; 16];
283 std::fs::File::open("/dev/urandom")
284 .and_then(|mut file| file.read_exact(&mut entropy))
285 .map_err(|e| Error::Platform {
286 code: e.raw_os_error().unwrap_or(-1) as i64,
287 message: format!("generate portal handle token: {e}"),
288 })?;
289
290 const HEX: &[u8; 16] = b"0123456789abcdef";
291 let mut token = String::with_capacity(6 + entropy.len() * 2);
292 token.push_str("xa11y_");
293 for byte in entropy {
294 token.push(HEX[(byte >> 4) as usize] as char);
295 token.push(HEX[(byte & 0x0f) as usize] as char);
296 }
297 Ok(token)
298}
299
300impl ScreenshotProvider for LinuxScreenshot {
301 fn capture_full(&self) -> Result<(Screenshot, Point)> {
302 let scale = crate::scale::screenshot_scale();
306 let mut shot = match &self.backend {
307 Backend::X11(x) => self.capture_x11(&x.conn, x.root, x.root_width, x.root_height, None),
308 Backend::Wayland { conn } => self.capture_wayland(conn, None),
309 }?;
310 shot.scale = scale as f32;
311 let (width, height, shot_scale) = (shot.width, shot.height, shot.scale);
312 shot = shot.with_mapping(CaptureMapping::single(
313 Point::new(0, 0),
314 width,
315 height,
316 shot_scale,
317 ));
318 Ok((shot, Point::new(0, 0)))
325 }
326
327 fn capture_region(&self, rect: Rect) -> Result<Screenshot> {
328 let scale = crate::scale::screenshot_scale();
331 let phys = rect.to_physical(scale);
332 let mut shot = match &self.backend {
333 Backend::X11(x) => {
334 self.capture_x11(&x.conn, x.root, x.root_width, x.root_height, Some(phys))
335 }
336 Backend::Wayland { conn } => self.capture_wayland(conn, Some(phys)),
337 }?;
338 shot.scale = scale as f32;
339 let (width, height, shot_scale) = (shot.width, shot.height, shot.scale);
340 shot = shot.with_mapping(CaptureMapping::single(
341 Point::new(rect.x, rect.y),
342 width,
343 height,
344 shot_scale,
345 ));
346 Ok(shot)
347 }
348}
349
350fn decode_png_to_rgba(bytes: &[u8]) -> Result<Screenshot> {
351 let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
352 let mut reader = decoder.read_info().map_err(|e| Error::Platform {
353 code: -1,
354 message: format!("png decode header: {e}"),
355 })?;
356 let info = reader.info().clone();
357 let buf_size = reader.output_buffer_size().ok_or_else(|| Error::Platform {
358 code: -1,
359 message: "png decode: output buffer size overflowed usize".to_string(),
360 })?;
361 let mut buf = vec![0u8; buf_size];
362 let frame = reader.next_frame(&mut buf).map_err(|e| Error::Platform {
363 code: -1,
364 message: format!("png decode frame: {e}"),
365 })?;
366 buf.truncate(frame.buffer_size());
367
368 let rgba = match (info.color_type, info.bit_depth) {
369 (png::ColorType::Rgba, png::BitDepth::Eight) => buf,
370 (png::ColorType::Rgb, png::BitDepth::Eight) => {
371 let mut out = Vec::with_capacity((info.width * info.height * 4) as usize);
372 for px in buf.as_chunks::<3>().0 {
373 out.extend_from_slice(&[px[0], px[1], px[2], 0xFF]);
374 }
375 out
376 }
377 (ct, bd) => {
378 return Err(Error::Platform {
379 code: -1,
380 message: format!("unsupported portal PNG format: {ct:?} @ {bd:?}"),
381 });
382 }
383 };
384
385 Ok(Screenshot::new(info.width, info.height, rgba, 1.0))
386}
387
388fn crop_rgba(shot: Screenshot, rect: Rect) -> Result<Screenshot> {
389 let (sw, sh, pixels, scale) = (shot.width, shot.height, shot.pixels, shot.scale);
390 let x = rect.x.max(0) as u32;
391 let y = rect.y.max(0) as u32;
392 if x >= sw || y >= sh {
393 return Err(Error::Platform {
394 code: -1,
395 message: "crop rect outside captured image".into(),
396 });
397 }
398 let w = rect.width.min(sw - x);
399 let h = rect.height.min(sh - y);
400 if w == 0 || h == 0 {
401 return Err(Error::Platform {
402 code: -1,
403 message: "crop rect has zero size".into(),
404 });
405 }
406 let mut out = Vec::with_capacity((w * h * 4) as usize);
407 for row in 0..h {
408 let start = ((y + row) * sw + x) as usize * 4;
409 let end = start + (w as usize) * 4;
410 out.extend_from_slice(&pixels[start..end]);
411 }
412 Ok(Screenshot::new(w, h, out, scale))
413}