Skip to content
Open
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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ with mss() as sct:
An ultra-fast cross-platform multiple screenshots module in pure python using ctypes.

- **Python 3.9+**, PEP8 compliant, no dependency, thread-safe;
- very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file;
- very basic, it will grab one screenshot by monitor or window, or a screenshot of all monitors and save it to a PNG file;
- but you can use PIL and benefit from all its formats (or add yours directly);
- integrate well with Numpy and OpenCV;
- it could be easily embedded into games and other software which require fast and platform optimized methods to grab screenshots (like AI, Computer Vision);
- get the [source code on GitHub](https://github.com/BoboTiG/python-mss);
- learn with a [bunch of examples](https://python-mss.readthedocs.io/examples.html);
- you can [report a bug](https://github.com/BoboTiG/python-mss/issues);
- need some help? Use the tag *python-mss* on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-mss);
- need some help? Use the tag _python-mss_ on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-mss);
- and there is a [complete, and beautiful, documentation](https://python-mss.readthedocs.io) :)
- **MSS** stands for Multiple ScreenShots;


## Installation

You can install it with pip:
Expand Down
7 changes: 5 additions & 2 deletions src/mss/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ def main(*args: str) -> int:
help="the PNG compression level",
)
cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screenshot")
cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name")
cli_args.add_argument("-w", "--window", default=None, help="the window to screenshot")
cli_args.add_argument("-p", "--process", default=None, help="the process to screenshot")
cli_args.add_argument("-o", "--output", default=None, help="the output file name")
cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor")
cli_args.add_argument(
"-q",
Expand All @@ -43,7 +45,8 @@ def main(*args: str) -> int:
cli_args.add_argument("-v", "--version", action="version", version=__version__)

options = cli_args.parse_args(args or None)
kwargs = {"mon": options.monitor, "output": options.output}
output = options.output or ("window-{win}.png" if options.window or options.process else "monitor-{mon}.png")
kwargs = {"mon": options.monitor, "output": output, "win": options.window, "proc": options.process}
if options.coordinates:
try:
top, left, width, height = options.coordinates.split(",")
Expand Down
101 changes: 98 additions & 3 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
if TYPE_CHECKING: # pragma: nocover
from collections.abc import Callable, Iterator

from mss.models import Monitor, Monitors
from mss.models import Monitor, Monitors, Window, Windows

try:
from datetime import UTC
Expand All @@ -34,7 +34,7 @@
class MSSBase(metaclass=ABCMeta):
"""This class will be overloaded by a system specific one."""

__slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"}
__slots__ = {"_monitors", "_windows", "cls_image", "compression_level", "with_cursor"}

def __init__(
self,
Expand All @@ -51,6 +51,7 @@ def __init__(
self.compression_level = compression_level
self.with_cursor = with_cursor
self._monitors: Monitors = []
self._windows: Windows = []

def __enter__(self) -> MSSBase: # noqa:PYI034
"""For the cool call `with MSS() as mss:`."""
Expand All @@ -70,12 +71,24 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
That method has to be run using a threading lock.
"""

@abstractmethod
def _grab_window_impl(self, window: Window, /) -> ScreenShot:
"""Retrieve all pixels from a window. Pixels have to be RGB.
That method has to be run using a threading lock.
"""

@abstractmethod
def _monitors_impl(self) -> None:
"""Get positions of monitors (has to be run using a threading lock).
It must populate self._monitors.
"""

@abstractmethod
def _windows_impl(self) -> None:
"""Get ids of windows (has to be run using a threading lock).
It must populate self._windows.
"""

def close(self) -> None: # noqa:B027
"""Clean-up."""

Expand Down Expand Up @@ -103,6 +116,31 @@ def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot:
return self._merge(screenshot, cursor)
return screenshot

def grab_window(
self, window: Window | str | None = None, /, *, name: str | None = None, process: str | None = None
) -> ScreenShot:
"""Retrieve screen pixels for a given window.

:param window: The window to capture or its name.
See :meth:`windows <windows>` for object details.
:param str name: The window name.
:param str process: The window process name.
:return :class:`ScreenShot <ScreenShot>`.
"""
if isinstance(window, str):
name = window
window = None

if window is None:
windows = self.find_windows(name, process)
if not windows:
msg = f"Window {window!r} not found."
raise ScreenShotError(msg)
window = windows[0]

with lock:
return self._grab_window_impl(window)

@property
def monitors(self) -> Monitors:
"""Get positions of all monitors.
Expand All @@ -128,11 +166,54 @@ def monitors(self) -> Monitors:

return self._monitors

@property
def windows(self) -> Windows:
"""Get ids, names, and proceesses of all windows.
Unlike monitors, this method does not use a cache, as the list of
windows can change at any time.

Each window is a dict with:
{
'id': the window id or handle,
'name': the window name,
'process': the window process name,
'bounds': the window bounds as a dict with:
{
'left': the x-coordinate of the upper-left corner,
'top': the y-coordinate of the upper-left corner,
'width': the width,
'height': the height
}
}
"""
with lock:
self._windows_impl()

return self._windows

def find_windows(self, name: str | None = None, process: str | None = None) -> Windows:
"""Find windows by name and/or process name.

:param str name: The window name.
:param str process: The window process name.
:return list: List of windows.
"""
windows = self.windows
if name is None and process is None:
return windows
if name is None:
return [window for window in windows if window["process"] == process]
if process is None:
return [window for window in windows if window["name"] == name]
return [window for window in windows if window["name"] == name and window["process"] == process]

def save(
self,
/,
*,
mon: int = 0,
win: str | None = None,
proc: str | None = None,
output: str = "monitor-{mon}.png",
callback: Callable[[str], None] | None = None,
) -> Iterator[str]:
Expand Down Expand Up @@ -166,7 +247,21 @@ def save(
msg = "No monitor found."
raise ScreenShotError(msg)

if mon == 0:
if win or proc:
windows = self.find_windows(win, proc)
if not windows:
msg = f"Window {(win or proc)!r} not found."
raise ScreenShotError(msg)
window = windows[0]

fname = output.format(win=win or proc, date=datetime.now(UTC) if "{date" in output else None)
if callable(callback):
callback(fname)

sct = self.grab_window(window)
to_png(sct.rgb, sct.size, level=self.compression_level, output=fname)
yield fname
elif mon == 0:
# One screenshot by monitor
for idx, monitor in enumerate(monitors[1:], 1):
fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor)
Expand Down
Loading
Loading