"""X11 window capture for Eternal Lands analysis. Captures a specific window's pixels even when it is not focused. On X11, focus only affects input routing, never pixel retrieval: XGetImage reads the window's own pixmap. Cinnamon's compositor keeps the redirected content available, so this works regardless of focus or occlusion. If window-level capture is unavailable (e.g. compositing is disabled), we fall back to grabbing the visible region from the root window at the window's screen coordinates. """ import re from PIL import Image from Xlib import X, display from Xlib.error import XError class WindowNotFound(Exception): """Raised when no window matching the title pattern is found.""" class WindowCapture: def __init__(self, title_pattern, display_name=None): self.title_pattern = re.compile(title_pattern, re.IGNORECASE) self.display = display.Display(display_name) self.root = self.display.screen().root self.window = None def find_window(self): """Return the cached or freshly-searched X window for the title.""" if self.window is not None: try: self.window.get_geometry() return self.window except XError: self.window = None self.window = self._search(self.root) return self.window def _search(self, window, depth=0): if depth > 24: return None try: name = window.get_wm_name() except XError: name = None if name and self.title_pattern.search(name): try: window.get_geometry() return window except XError: return None try: children = window.query_tree().children except XError: return None for child in children: found = self._search(child, depth + 1) if found: return found return None def capture(self): """Capture the target window and return a PIL.Image.""" window = self.find_window() if window is None: raise WindowNotFound( f"no window matching {self.title_pattern.pattern!r} " f"on display {self.display.display_string}" ) geo = window.get_geometry() try: ximage = window.get_image(0, 0, geo.width, geo.height, X.ZPixmap, 0xFFFFFFFF) except XError: coords = self.root.translate_coords(window, 0, 0) ximage = self.root.get_image( coords.x, coords.y, geo.width, geo.height, X.ZPixmap, 0xFFFFFFFF ) return _to_image(ximage, geo.width, geo.height) def _to_image(ximage, width, height): return Image.frombytes("RGBA", (width, height), ximage.data, "raw", "BGRA")