|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from typing import TYPE_CHECKING, Any |
| 4 | + |
| 5 | +from mss.base import MSSBase |
| 6 | +from mss.exception import ScreenShotError |
| 7 | + |
| 8 | +from . import xcb |
| 9 | +from .xcb import LIB |
| 10 | + |
| 11 | +if TYPE_CHECKING: |
| 12 | + from mss.screenshot import ScreenShot |
| 13 | + |
| 14 | +SUPPORTED_DEPTHS = {24, 32} |
| 15 | +SUPPORTED_BITS_PER_PIXEL = 32 |
| 16 | +SUPPORTED_RED_MASK = 0xFF0000 |
| 17 | +SUPPORTED_GREEN_MASK = 0x00FF00 |
| 18 | +SUPPORTED_BLUE_MASK = 0x0000FF |
| 19 | +ALL_PLANES = 0xFFFFFFFF # XCB doesn't define AllPlanes |
| 20 | + |
| 21 | + |
| 22 | +class MSSXCBBase(MSSBase): |
| 23 | + """Base class for XCB-based screenshot implementations. |
| 24 | +
|
| 25 | + This class provides common XCB initialization and monitor detection logic |
| 26 | + that can be shared across different XCB screenshot methods (XGetImage, |
| 27 | + XShmGetImage, XComposite, etc.). |
| 28 | + """ |
| 29 | + |
| 30 | + def __init__(self, /, **kwargs: Any) -> None: # noqa: PLR0912 |
| 31 | + """Initialize an XCB connection and validate the display configuration. |
| 32 | +
|
| 33 | + Args: |
| 34 | + **kwargs: Keyword arguments, including optional 'display' for X11 display string. |
| 35 | +
|
| 36 | + Raises: |
| 37 | + ScreenShotError: If the display configuration is not supported. |
| 38 | + """ |
| 39 | + super().__init__(**kwargs) |
| 40 | + |
| 41 | + display = kwargs.get("display", b"") |
| 42 | + if not display: |
| 43 | + display = None |
| 44 | + |
| 45 | + self.conn: xcb.Connection | None |
| 46 | + self.conn, pref_screen_num = xcb.connect(display) |
| 47 | + |
| 48 | + # Get the connection setup information that was included when we connected. |
| 49 | + xcb_setup = xcb.get_setup(self.conn) |
| 50 | + screens = xcb.setup_roots(xcb_setup) |
| 51 | + pref_screen = screens[pref_screen_num] |
| 52 | + self.root = self.drawable = pref_screen.root |
| 53 | + |
| 54 | + # We don't probe the XFixes presence or version until we need it. |
| 55 | + self._xfixes_ready: bool | None = None |
| 56 | + |
| 57 | + # Probe the visuals (and related information), and make sure that our drawable is in an acceptable format. |
| 58 | + # These iterations and tests don't involve any traffic with the server; it's all stuff that was included in |
| 59 | + # the connection setup. Effectively all modern setups will be acceptable, but we verify to be sure. |
| 60 | + |
| 61 | + # Currently, we assume that the drawable we're capturing is the root; when we add single-window capture, |
| 62 | + # we'll have to ask the server for its depth and visual. |
| 63 | + assert self.root == self.drawable # noqa: S101 |
| 64 | + self.drawable_depth = pref_screen.root_depth |
| 65 | + self.drawable_visual_id = pref_screen.root_visual.value |
| 66 | + # Server image byte order |
| 67 | + if xcb_setup.image_byte_order != xcb.ImageOrder.LSBFirst: |
| 68 | + msg = "Only X11 servers using LSB-First images are supported." |
| 69 | + raise ScreenShotError(msg) |
| 70 | + # Depth |
| 71 | + if self.drawable_depth not in SUPPORTED_DEPTHS: |
| 72 | + msg = f"Only screens of color depth 24 or 32 are supported, not {self.drawable_depth}" |
| 73 | + raise ScreenShotError(msg) |
| 74 | + # Format (i.e., bpp, padding) |
| 75 | + for format_ in xcb.setup_pixmap_formats(xcb_setup): |
| 76 | + if format_.depth == self.drawable_depth: |
| 77 | + break |
| 78 | + else: |
| 79 | + msg = f"Internal error: drawable's depth {self.drawable_depth} not found in screen's supported formats" |
| 80 | + raise ScreenShotError(msg) |
| 81 | + drawable_format = format_ |
| 82 | + if drawable_format.bits_per_pixel != SUPPORTED_BITS_PER_PIXEL: |
| 83 | + msg = ( |
| 84 | + f"Only screens at 32 bpp (regardless of color depth) are supported; " |
| 85 | + f"got {drawable_format.bits_per_pixel} bpp" |
| 86 | + ) |
| 87 | + raise ScreenShotError(msg) |
| 88 | + if drawable_format.scanline_pad != SUPPORTED_BITS_PER_PIXEL: |
| 89 | + # To clarify the padding: the scanline_pad is the multiple that the scanline gets padded to. If there |
| 90 | + # is no padding, then it will be the same as one pixel's size. |
| 91 | + msg = "Screens with scanline padding are not supported" |
| 92 | + raise ScreenShotError(msg) |
| 93 | + # Visual, the interpretation of pixels (like indexed, grayscale, etc). (Visuals are arranged by depth, so |
| 94 | + # we iterate over the depths first.) |
| 95 | + for xcb_depth in xcb.screen_allowed_depths(pref_screen): |
| 96 | + if xcb_depth.depth == self.drawable_depth: |
| 97 | + break |
| 98 | + else: |
| 99 | + msg = "Internal error: drawable's depth not found in screen's supported depths" |
| 100 | + raise ScreenShotError(msg) |
| 101 | + for visual_info in xcb.depth_visuals(xcb_depth): |
| 102 | + if visual_info.visual_id.value == self.drawable_visual_id: |
| 103 | + break |
| 104 | + else: |
| 105 | + msg = "Internal error: drawable's visual not found in screen's supported visuals" |
| 106 | + raise ScreenShotError(msg) |
| 107 | + if visual_info.class_ not in {xcb.VisualClass.TrueColor, xcb.VisualClass.DirectColor}: |
| 108 | + msg = "Only TrueColor and DirectColor visuals are supported" |
| 109 | + raise ScreenShotError(msg) |
| 110 | + if ( |
| 111 | + visual_info.red_mask != SUPPORTED_RED_MASK |
| 112 | + or visual_info.green_mask != SUPPORTED_GREEN_MASK |
| 113 | + or visual_info.blue_mask != SUPPORTED_BLUE_MASK |
| 114 | + ): |
| 115 | + # There are two ways to phrase this layout: BGRx accounts for the byte order, while xRGB implies the |
| 116 | + # native word order. Since we return the data as a byte array, we use the former. By the time we get |
| 117 | + # to this point, we've already checked the endianness and depth, so this is pretty much never going to |
| 118 | + # happen anyway. |
| 119 | + msg = "Only visuals with BGRx ordering are supported" |
| 120 | + raise ScreenShotError(msg) |
| 121 | + |
| 122 | + def close(self) -> None: |
| 123 | + """Close the XCB connection.""" |
| 124 | + if self.conn is not None: |
| 125 | + xcb.disconnect(self.conn) |
| 126 | + self.conn = None |
| 127 | + |
| 128 | + def _monitors_impl(self) -> None: |
| 129 | + """Get positions of monitors. It will populate self._monitors.""" |
| 130 | + if self.conn is None: |
| 131 | + msg = "Cannot identify monitors while the connection is closed" |
| 132 | + raise ScreenShotError(msg) |
| 133 | + |
| 134 | + # The first entry is the whole X11 screen that the root is on. That's the one that covers all the |
| 135 | + # monitors. |
| 136 | + root_geom = xcb.get_geometry(self.conn, self.root) |
| 137 | + self._monitors.append( |
| 138 | + { |
| 139 | + "left": root_geom.x, |
| 140 | + "top": root_geom.y, |
| 141 | + "width": root_geom.width, |
| 142 | + "height": root_geom.height, |
| 143 | + } |
| 144 | + ) |
| 145 | + |
| 146 | + # After that, we have one for each monitor on that X11 screen. For decades, that's been handled by |
| 147 | + # Xrandr. We don't presently try to work with Xinerama. So, we're going to check the different outputs, |
| 148 | + # according to Xrandr. If that fails, we'll just leave the one root covering everything. |
| 149 | + |
| 150 | + # Make sure we have the Xrandr extension we need. This will query the cache that we started populating in |
| 151 | + # __init__. |
| 152 | + randr_ext_data = xcb.get_extension_data(self.conn, LIB.randr_id) |
| 153 | + if not randr_ext_data.present: |
| 154 | + return |
| 155 | + |
| 156 | + # We ask the server to give us anything up to the version we support (i.e., what we expect the reply |
| 157 | + # structs to look like). If the server only supports 1.2, then that's what it'll give us, and we're ok |
| 158 | + # with that, but we also use a faster path if the server implements at least 1.3. |
| 159 | + randr_version_data = xcb.randr_query_version(self.conn, xcb.RANDR_MAJOR_VERSION, xcb.RANDR_MINOR_VERSION) |
| 160 | + randr_version = (randr_version_data.major_version, randr_version_data.minor_version) |
| 161 | + if randr_version < (1, 2): |
| 162 | + return |
| 163 | + |
| 164 | + screen_resources: xcb.RandrGetScreenResourcesReply | xcb.RandrGetScreenResourcesCurrentReply |
| 165 | + # Check to see if we have the xcb_randr_get_screen_resources_current function in libxcb-randr, and that |
| 166 | + # the server supports it. |
| 167 | + if hasattr(LIB.randr, "xcb_randr_get_screen_resources_current") and randr_version >= (1, 3): |
| 168 | + screen_resources = xcb.randr_get_screen_resources_current(self.conn, self.drawable.value) |
| 169 | + crtcs = xcb.randr_get_screen_resources_current_crtcs(screen_resources) |
| 170 | + else: |
| 171 | + # Either the client or the server doesn't support the _current form. That's ok; we'll use the old |
| 172 | + # function, which forces a new query to the physical monitors. |
| 173 | + screen_resources = xcb.randr_get_screen_resources(self.conn, self.drawable) |
| 174 | + crtcs = xcb.randr_get_screen_resources_crtcs(screen_resources) |
| 175 | + |
| 176 | + for crtc in crtcs: |
| 177 | + crtc_info = xcb.randr_get_crtc_info(self.conn, crtc, screen_resources.config_timestamp) |
| 178 | + if crtc_info.num_outputs == 0: |
| 179 | + continue |
| 180 | + self._monitors.append( |
| 181 | + {"left": crtc_info.x, "top": crtc_info.y, "width": crtc_info.width, "height": crtc_info.height} |
| 182 | + ) |
| 183 | + |
| 184 | + # Extra credit would be to enumerate the virtual desktops; see |
| 185 | + # https://specifications.freedesktop.org/wm/latest/ar01s03.html. But I don't know how widely-used that |
| 186 | + # style is. |
| 187 | + |
| 188 | + def _cursor_impl_check_xfixes(self) -> bool: |
| 189 | + if self.conn is None: |
| 190 | + msg = "Cannot take screenshot while the connection is closed" |
| 191 | + raise ScreenShotError(msg) |
| 192 | + |
| 193 | + xfixes_ext_data = xcb.get_extension_data(self.conn, LIB.xfixes_id) |
| 194 | + if not xfixes_ext_data.present: |
| 195 | + return False |
| 196 | + |
| 197 | + reply = xcb.xfixes_query_version(self.conn, xcb.XFIXES_MAJOR_VERSION, xcb.XFIXES_MINOR_VERSION) |
| 198 | + # We can work with 2.0 and later, but not sure about the actual minimum version we can use. That's ok; |
| 199 | + # everything these days is much more modern. |
| 200 | + return (reply.major_version, reply.minor_version) >= (2, 0) |
| 201 | + |
| 202 | + def _cursor_impl(self) -> ScreenShot: |
| 203 | + """Retrieve all cursor data. Pixels have to be RGBx.""" |
| 204 | + |
| 205 | + if self.conn is None: |
| 206 | + msg = "Cannot take screenshot while the connection is closed" |
| 207 | + raise ScreenShotError(msg) |
| 208 | + |
| 209 | + if self._xfixes_ready is None: |
| 210 | + self._xfixes_ready = self._cursor_impl_check_xfixes() |
| 211 | + if not self._xfixes_ready: |
| 212 | + msg = "Server does not have XFixes, or the version is too old." |
| 213 | + raise ScreenShotError(msg) |
| 214 | + |
| 215 | + cursor_img = xcb.xfixes_get_cursor_image(self.conn) |
| 216 | + region = { |
| 217 | + "left": cursor_img.x - cursor_img.xhot, |
| 218 | + "top": cursor_img.y - cursor_img.yhot, |
| 219 | + "width": cursor_img.width, |
| 220 | + "height": cursor_img.height, |
| 221 | + } |
| 222 | + |
| 223 | + data_arr = xcb.xfixes_get_cursor_image_cursor_image(cursor_img) |
| 224 | + data = bytearray(data_arr) |
| 225 | + # We don't need to do the same array slice-and-dice work as the Xlib-based implementation: Xlib has an |
| 226 | + # unfortunate historical accident that makes it have to return the cursor image in a different format. |
| 227 | + |
| 228 | + return self.cls_image(data, region) |
0 commit comments