Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/lint-and-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ jobs:
# Per-package no-binary overrides no-build, allowing
# known pure-Python source-only dependencies to still build.
# The Build job intentionally builds some binary packages from source.
UV_NO_BINARY_PACKAGE: keyboard PyAutoGUI beslogic-ruff-config
# python-xlib: https://github.com/python-xlib/python-xlib/pull/298
UV_NO_BINARY_PACKAGE: keyboard PyAutoGUI beslogic-ruff-config python-xlib
strategy:
fail-fast: false
# Pyright is version and platform sensible
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Not a developer? You can still help through the following methods:
- Sharing AutoSplit with other speedrunners
- Starring the repository <img style="vertical-align: sub" src="./docs/repo_star.png" alt="Example" />
- Upvoting 👍 the following upstream issues in libraries and tools we use:
- <https://github.com/python-xlib/python-xlib/pull/298>
- <https://github.com/python-xlib/python-xlib/issues/286>
- <https://bugs.winehq.org/show_bug.cgi?id=52487>
- <https://qt-project.atlassian.net/browse/QTBUG-114436>
- <https://qt-project.atlassian.net/browse/PYSIDE-2541>
Expand Down
6 changes: 3 additions & 3 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@

##### Linux

- **X11 XCB** (slower, requires XCB)\
Uses the XCB library to take screenshots of the X11 server.
- **Scrot** (fast, may leave files in `/tmp`)\
- **X11** (fastest)\
Request screenshots directly from the X11 server.
- **Scrot** (slow, may leave files in `/tmp`)\
Uses Scrot (SCReenshOT) to take screenshots.\
Leaves behind a screenshot file in `/tmp` if interrupted.
<!-- Keep in sync with src/menu_bar.py -->
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ dependencies = [
"winrt-Windows.Graphics.Imaging >=3.2.1; sys_platform == 'win32'",
#
# Linux-only dependencies
"pillow >=13.0.0; sys_platform == 'linux'",
"python-xlib >=0.33; sys_platform == 'linux'",
]
[dependency-groups]
Expand All @@ -66,7 +65,6 @@ dev = [
"types-pywin32 >=306.0.0.20240130; sys_platform == 'win32'",
#
# OS-specific, but installable on other OSes for convenient typing references
"pillow >=13.0.0",
"pygrabber",
"python-xlib",
"typed-D3DShot[numpy]",
Expand Down Expand Up @@ -98,21 +96,25 @@ override-dependencies = [
# https://github.com/opencv/opencv-python/issues/1201
"numpy >=2.5.2",
]
# python-xlib's setup.py imports pkg_resources, removed in setuptools 81
# https://github.com/python-xlib/python-xlib/issues/286
build-constraint-dependencies = ["setuptools <81"]

exclude-newer = "1 week"
[tool.uv.exclude-newer-package]
typed-D3DShot = false # I own it
pillow = "6 day"

###
# Development channels
###
[tool.uv.sources]
beslogic-ruff-config = { git = "https://github.com/Beslogic/Beslogic-Ruff-Config", rev = "39c27278054fc428b54205581a0e23caca7ad77a" }
# Quadratic receive buffer fix for large GetImage replies (X11 capture)
# https://github.com/python-xlib/python-xlib/pull/298
python-xlib = { git = "https://github.com/Avasam/python-xlib", branch = "fix-quadratic-recv" }
# pywin32 = { git = "https://github.com/mhammond/pywin32.git", marker = "python_version >= '3.15'" }
# pyinstaller = { git = "https://github.com/pyinstaller/pyinstaller.git", marker = "python_version >= '3.15'" }
# numpy = { index = "scientific-python-nightly-wheels", marker = "python_version >= '3.15'" }
# pillow = { index = "scientific-python-nightly-wheels", marker = "python_version >= '3.15'" }

[[tool.uv.index]]
exclude-newer = false # Anaconda index doesn't have upload dates
Expand Down
2 changes: 1 addition & 1 deletion scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ try {
# Icon as PNG (freedesktop doesn't support .ico), converted from res/icon.ico.
# Not splash.png, which uses hard transparency for the Tcl/Tk splash.
New-Item -ItemType Directory -Path build/AppDir/usr/share/icons/hicolor/256x256/apps -Force | Out-Null
uv run --active python -c "from PIL import Image; Image.open('res/icon.ico').save('build/AppDir/AutoSplit.png')"
uv run --active python -c "import sys; from PySide6.QtGui import QImage; sys.exit(not QImage('res/icon.ico').save('build/AppDir/AutoSplit.png'))"
# Top-level -> .DirIcon (file thumbnail); hicolor copy -> desktop integration (menu/taskbar).
Copy-Item build/AppDir/AutoSplit.png build/AppDir/usr/share/icons/hicolor/256x256/apps/AutoSplit.png
$date = Get-Date -Format 'yyyy-MM-dd'
Expand Down
2 changes: 1 addition & 1 deletion src/capture_method/ScrotCaptureMethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def _scrot_screenshot(x: int, y: int, width: int, height: int):

class ScrotCaptureMethod(CaptureMethodBase):
name = "Scrot"
short_description = "fast, may leave files in `/tmp`"
short_description = "slow, may leave files in `/tmp`"
description = """
Uses Scrot (SCReenshOT) to take screenshots.
Leaves behind a screenshot file in `/tmp` if interrupted."""
Expand Down
83 changes: 83 additions & 0 deletions src/capture_method/X11CaptureMethod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import sys

if sys.platform != "linux":
raise OSError

from typing import TYPE_CHECKING, override

import cv2
import numpy as np
from pywinctl import getWindowsWithTitle
from Xlib import X
from Xlib.display import Display
from Xlib.error import BadMatch, BadWindow

from capture_method.CaptureMethodBase import CaptureMethodBase
from utils import ALPHA_CHANNEL_COUNT, is_valid_image

if TYPE_CHECKING:
from Xlib.protocol.request import GetImage

from AutoSplit import AutoSplit

ALL_PLANES = 0xFFFFFFFF


def _image_reply_to_bgr(reply: GetImage, width: int, height: int):
image = np.frombuffer(reply.data, dtype=np.uint8)
# X pads 24-bit depth to 32 bits per pixel (BGRX). The 4th byte is padding, not alpha.
# Other depths (16, 30) use different pixel layouts we don't decode, so return None.
if image.size != width * height * ALPHA_CHANNEL_COUNT:
return None
image = image.reshape((height, width, ALPHA_CHANNEL_COUNT))
if not is_valid_image(image):
return None
return cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)


class X11CaptureMethod(CaptureMethodBase):
name = "X11"
short_description = "fastest"
description = "\nRequests screenshots directly from the X11 server."

def __init__(self, autosplit: AutoSplit):
super().__init__(autosplit)
self._display = Display()

@override
def close(self):
self._display.close()

@override
def get_frame(self):
if not self.check_selected_region_exists():
return None

root = self._display.screen().root
try:
window_coords = root.translate_coords(self._autosplit_ref.hwnd, 0, 0)._data # noqa: SLF001
except BadWindow:
return None

selection = self._autosplit_ref.settings_dict["capture_region"]
try:
reply = root.get_image(
selection["x"] + window_coords["x"],
selection["y"] + window_coords["y"],
selection["width"],
selection["height"],
X.ZPixmap,
ALL_PLANES,
)
except BadMatch:
# Region is (partially) outside the screen
return None
return _image_reply_to_bgr(reply, selection["width"], selection["height"])

@override
def recover_window(self, captured_window_title: str):
windows = getWindowsWithTitle(captured_window_title)
if len(windows) == 0:
return False
self._autosplit_ref.hwnd = windows[0].getHandle()
return self.check_selected_region_exists()
80 changes: 0 additions & 80 deletions src/capture_method/XcbCaptureMethod.py

This file was deleted.

9 changes: 3 additions & 6 deletions src/capture_method/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@
if sys.platform == "linux":
import fcntl

from PIL import features

from capture_method.ScrotCaptureMethod import IS_SCROT_SUPPORTED, ScrotCaptureMethod
from capture_method.XcbCaptureMethod import XcbCaptureMethod
from capture_method.X11CaptureMethod import X11CaptureMethod
from vidioc_querycap import V4L2_CAP_VIDEO_CAPTURE, VIDIOC_QUERYCAP, v4l2_capability


Expand Down Expand Up @@ -82,7 +80,7 @@ def _generate_next_value_(
PRINTWINDOW_RENDERFULLCONTENT = auto()
DESKTOP_DUPLICATION = auto()
SCROT = auto()
XCB = auto()
X11 = auto()
VIDEO_CAPTURE_DEVICE = auto()


Expand Down Expand Up @@ -138,8 +136,7 @@ def get(self, key: CaptureMethodEnum, default: object = None, /):
ForceFullContentRenderingCaptureMethod
)
elif sys.platform == "linux":
if features.check_feature(feature="xcb"):
CAPTURE_METHODS[CaptureMethodEnum.XCB] = XcbCaptureMethod
CAPTURE_METHODS[CaptureMethodEnum.X11] = X11CaptureMethod
if IS_SCROT_SUPPORTED:
CAPTURE_METHODS[CaptureMethodEnum.SCROT] = ScrotCaptureMethod
CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = VideoCaptureDeviceCaptureMethod
Expand Down
2 changes: 1 addition & 1 deletion tests/test_import_all_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"linux": {
"capture_method.Screenshot using QT attempt",
"capture_method.ScrotCaptureMethod",
"capture_method.XcbCaptureMethod",
"capture_method.X11CaptureMethod",
},
}
EXPECTED_OS_ERRORS = frozenset(
Expand Down
Loading
Loading