1use std::collections::HashMap;
18use std::sync::Mutex;
19
20use x11rb::connection::Connection;
21use x11rb::protocol::xproto::{ConnectionExt as _, ImageFormat};
22use x11rb::rust_connection::RustConnection;
23use zbus::blocking::Connection as ZbusConnection;
24use zbus::blocking::Proxy;
25use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
26
27use xa11y_core::{Error, Rect, Result, Screenshot, ScreenshotProvider};
28
29pub struct LinuxScreenshot {
31 backend: Backend,
32}
33
34enum Backend {
39 X11(Box<X11Backend>),
40 Wayland { conn: ZbusConnection },
41}
42
43struct X11Backend {
44 conn: Mutex<RustConnection>,
45 root_width: u16,
46 root_height: u16,
47 root: u32,
48}
49
50impl LinuxScreenshot {
51 pub fn new() -> Result<Self> {
52 let display_set = std::env::var_os("DISPLAY").is_some();
53 let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some();
54
55 if display_set {
56 let (conn, screen_num) =
57 RustConnection::connect(None).map_err(|e| Error::Platform {
58 code: -1,
59 message: format!("X11 connect: {e}"),
60 })?;
61 let screen = conn
62 .setup()
63 .roots
64 .get(screen_num)
65 .ok_or_else(|| Error::Platform {
66 code: -1,
67 message: "X server reported no screens".into(),
68 })?;
69 let root = screen.root;
70 let root_width = screen.width_in_pixels;
71 let root_height = screen.height_in_pixels;
72 Ok(Self {
73 backend: Backend::X11(Box::new(X11Backend {
74 conn: Mutex::new(conn),
75 root,
76 root_width,
77 root_height,
78 })),
79 })
80 } else if wayland {
81 let conn = ZbusConnection::session().map_err(|e| Error::Platform {
82 code: -1,
83 message: format!("session bus connect: {e}"),
84 })?;
85 Ok(Self {
86 backend: Backend::Wayland { conn },
87 })
88 } else {
89 Err(Error::Unsupported {
90 feature: "screenshot (no DISPLAY or WAYLAND_DISPLAY set)".into(),
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.chunks_exact(4) {
157 rgba.push(chunk[2]);
159 rgba.push(chunk[1]);
160 rgba.push(chunk[0]);
161 rgba.push(0xFF);
162 }
163
164 Ok(Screenshot {
165 width: w as u32,
166 height: h as u32,
167 pixels: rgba,
168 scale: 1.0,
169 })
170 }
171
172 fn capture_wayland(&self, conn: &ZbusConnection, rect: Option<Rect>) -> Result<Screenshot> {
173 let proxy = Proxy::new(
174 conn,
175 "org.freedesktop.portal.Desktop",
176 "/org/freedesktop/portal/desktop",
177 "org.freedesktop.portal.Screenshot",
178 )
179 .map_err(|e| Error::Platform {
180 code: -1,
181 message: format!("portal Screenshot proxy: {e}"),
182 })?;
183
184 let mut options: HashMap<&str, Value> = HashMap::new();
185 options.insert("interactive", Value::Bool(false));
186 options.insert("modal", Value::Bool(false));
187
188 let request_path: OwnedObjectPath =
189 proxy
190 .call("Screenshot", &("", options))
191 .map_err(|e| Error::Platform {
192 code: -1,
193 message: format!("portal Screenshot call: {e}"),
194 })?;
195
196 let request = Proxy::new(
197 conn,
198 "org.freedesktop.portal.Desktop",
199 &request_path,
200 "org.freedesktop.portal.Request",
201 )
202 .map_err(|e| Error::Platform {
203 code: -1,
204 message: format!("portal Request proxy: {e}"),
205 })?;
206
207 let mut signals = request
209 .receive_signal("Response")
210 .map_err(|e| Error::Platform {
211 code: -1,
212 message: format!("receive_signal: {e}"),
213 })?;
214 let msg = signals.next().ok_or_else(|| Error::Platform {
215 code: -1,
216 message: "portal Response signal channel closed".into(),
217 })?;
218 let (response, results): (u32, HashMap<String, OwnedValue>) =
219 msg.body().deserialize().map_err(|e| Error::Platform {
220 code: -1,
221 message: format!("portal Response deserialize: {e}"),
222 })?;
223 if response != 0 {
224 return Err(Error::PermissionDenied {
225 instructions: format!("xdg-desktop-portal Screenshot denied (response={response})"),
226 });
227 }
228 let uri_val = results.get("uri").ok_or_else(|| Error::Platform {
229 code: -1,
230 message: "portal Response missing 'uri' key".into(),
231 })?;
232 let uri: String = uri_val
233 .downcast_ref::<String>()
234 .map_err(|_| Error::Platform {
235 code: -1,
236 message: "portal Response 'uri' is not a string".into(),
237 })?;
238 let path = uri.strip_prefix("file://").ok_or_else(|| Error::Platform {
239 code: -1,
240 message: format!("portal URI not file://: {uri}"),
241 })?;
242 let bytes = std::fs::read(path).map_err(|e| Error::Platform {
243 code: e.raw_os_error().unwrap_or(-1) as i64,
244 message: format!("read portal PNG: {e}"),
245 })?;
246 let _ = std::fs::remove_file(path);
248
249 let shot = decode_png_to_rgba(&bytes)?;
250 match rect {
251 None => Ok(shot),
252 Some(r) => crop_rgba(shot, r),
253 }
254 }
255}
256
257impl ScreenshotProvider for LinuxScreenshot {
258 fn capture_full(&self) -> Result<Screenshot> {
259 let scale = crate::scale::screenshot_scale();
263 let mut shot = match &self.backend {
264 Backend::X11(x) => self.capture_x11(&x.conn, x.root, x.root_width, x.root_height, None),
265 Backend::Wayland { conn } => self.capture_wayland(conn, None),
266 }?;
267 shot.scale = scale as f32;
268 Ok(shot)
269 }
270
271 fn capture_region(&self, rect: Rect) -> Result<Screenshot> {
272 let scale = crate::scale::screenshot_scale();
275 let phys = rect.to_physical(scale);
276 let mut shot = match &self.backend {
277 Backend::X11(x) => {
278 self.capture_x11(&x.conn, x.root, x.root_width, x.root_height, Some(phys))
279 }
280 Backend::Wayland { conn } => self.capture_wayland(conn, Some(phys)),
281 }?;
282 shot.scale = scale as f32;
283 Ok(shot)
284 }
285}
286
287fn decode_png_to_rgba(bytes: &[u8]) -> Result<Screenshot> {
288 let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
289 let mut reader = decoder.read_info().map_err(|e| Error::Platform {
290 code: -1,
291 message: format!("png decode header: {e}"),
292 })?;
293 let info = reader.info().clone();
294 let buf_size = reader.output_buffer_size().ok_or_else(|| Error::Platform {
295 code: -1,
296 message: "png decode: output buffer size overflowed usize".to_string(),
297 })?;
298 let mut buf = vec![0u8; buf_size];
299 let frame = reader.next_frame(&mut buf).map_err(|e| Error::Platform {
300 code: -1,
301 message: format!("png decode frame: {e}"),
302 })?;
303 buf.truncate(frame.buffer_size());
304
305 let rgba = match (info.color_type, info.bit_depth) {
306 (png::ColorType::Rgba, png::BitDepth::Eight) => buf,
307 (png::ColorType::Rgb, png::BitDepth::Eight) => {
308 let mut out = Vec::with_capacity((info.width * info.height * 4) as usize);
309 for px in buf.chunks_exact(3) {
310 out.extend_from_slice(&[px[0], px[1], px[2], 0xFF]);
311 }
312 out
313 }
314 (ct, bd) => {
315 return Err(Error::Platform {
316 code: -1,
317 message: format!("unsupported portal PNG format: {ct:?} @ {bd:?}"),
318 });
319 }
320 };
321
322 Ok(Screenshot {
323 width: info.width,
324 height: info.height,
325 pixels: rgba,
326 scale: 1.0,
327 })
328}
329
330fn crop_rgba(shot: Screenshot, rect: Rect) -> Result<Screenshot> {
331 let Screenshot {
332 width: sw,
333 height: sh,
334 pixels,
335 scale,
336 } = shot;
337 let x = rect.x.max(0) as u32;
338 let y = rect.y.max(0) as u32;
339 if x >= sw || y >= sh {
340 return Err(Error::Platform {
341 code: -1,
342 message: "crop rect outside captured image".into(),
343 });
344 }
345 let w = rect.width.min(sw - x);
346 let h = rect.height.min(sh - y);
347 if w == 0 || h == 0 {
348 return Err(Error::Platform {
349 code: -1,
350 message: "crop rect has zero size".into(),
351 });
352 }
353 let mut out = Vec::with_capacity((w * h * 4) as usize);
354 for row in 0..h {
355 let start = ((y + row) * sw + x) as usize * 4;
356 let end = start + (w as usize) * 4;
357 out.extend_from_slice(&pixels[start..end]);
358 }
359 Ok(Screenshot {
360 width: w,
361 height: h,
362 pixels: out,
363 scale,
364 })
365}