87 lines
2.8 KiB
Python
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")
|