Files

87 lines
2.8 KiB
Python

"""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")