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
5 changes: 1 addition & 4 deletions demos/cat-detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,6 @@
import torchvision.models.detection
import torchvision.transforms.v2

# You'll also need to install MSS and Pillow, such as with "pip install mss pillow".
from PIL import Image

import mss

# The model will identify objects even if they only vaguely look like something. It also tell us a score of how
Expand Down Expand Up @@ -221,7 +218,7 @@ def main() -> None:
cat_label = model_labels.index("cat")

with mss.MSS() as sct:
monitor = sct.monitors[1]
monitor = sct.primary_monitor

# Compute the minimum size, in square pixels, that we'll consider reliable.
img_area = monitor.width * monitor.height
Expand Down
3 changes: 2 additions & 1 deletion demos/tinytv-stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@
from collections import deque
from typing import TYPE_CHECKING, Literal

# You'll want to install the following packages, through pip or the like:
# mss pyserial pillow prettytable
import serial
from PIL import Image, ImageOps
from prettytable import PrettyTable, TableStyle
Expand Down Expand Up @@ -517,7 +519,6 @@ def _capture_area_type(value: str) -> Region:
integers.

:param value: The capture area string to validate.
:returns: Capture region.
:raises argparse.ArgumentTypeError: If the format is invalid or extents
are non-positive.
"""
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/custom_cls_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ def __init__(self, data: bytearray, region: Region, **_: Any) -> None:

with mss.MSS() as sct:
sct.cls_image = SimpleScreenShot
image = sct.grab(sct.monitors[1])
image = sct.grab(sct.primary_monitor)
# ...
15 changes: 2 additions & 13 deletions docs/source/release-history/v10.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,22 +260,11 @@ In 11.0, monitor dictionaries will become a dedicated **`Monitor` class**.

To maintain compatibility:

- string-key access will temporarily continue to work

```python
monitor["left"]
monitor["top"]
```

- string-key access (dictionary-style) will temporarily continue to work: `monitor["left"]`, `monitor["top"]`, etc.
- `grab()` will continue accepting dictionaries

The compatibility access does not make `Monitor` a complete mapping. Migrate dictionary methods, membership tests, and
unpacking to attribute access:

```python
monitor.left
monitor.top
```
unpacking to attribute access: `monitor.left`, `monitor.top`, etc.

If you use type annotations, you can switch to the provided `Monitor` type:

Expand Down
16 changes: 14 additions & 2 deletions docs/source/release-history/v11.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ application code from accidentally changing the geometry or metadata held by an
{py:class}`mss.MSS` instance. Use attributes to read geometry and metadata:

```python
monitor = sct.monitors[1]
monitor = sct.primary_monitor # or sct.monitors[1], etc.
print(monitor.left, monitor.top, monitor.width, monitor.height)
print(monitor.is_primary, monitor.name, monitor.unique_id, monitor.output)
region = monitor.as_region()
Expand Down Expand Up @@ -84,10 +84,22 @@ See the documentation section {ref}`accessing_pixel_data` for details.

### General Improvements

The MSS context object will now always surface inner exceptions, even if `__exit__` may also generate an exception during tear-down.
The MSS context object will now always surface inner exceptions, even if `__exit__` may also generate an exception
during tear-down.

The {py:meth}`mss.MSS.save` method now returns a {py:class}`Sequence` of strings, rather than an {py:class}`Iterator` of
strings. Code that used `next(sct.save(...))` should now use `sct.save(...)[0]` (easy to write) or
`next(iter(sct.save(...)))` (compatible with both old and new versions of MSS). Code using for loops or other
constructs based on {py:class}`Iterable` (rather than {py:class}`Iterator`) will continue to work as-is.

If {py:meth}`mss.MSS.save` is given a template with `{date}` in it, the dates are now in the local time zone, instead of
UTC.

## Command-line changes

If the `--output` / `-o` flag is used with `{date}` fields, dates in the filename are now in the local time zone,
instead of UTC. The old behavior can be restored by setting the environment variable `TZ=UTC`.

When invoked from the command line, the `--coordinates` flag can specify coordinates in the format traditionally used by
X11: WIDTHxHEIGHT+LEFT+TOP. Additionally, whether using comma-style or X-style coordinates, a negative left or top can
be used to specify insets from the right or bottom edge of the specified monitor.
2 changes: 1 addition & 1 deletion docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Each entry is an immutable :py:class:`mss.models.Monitor`. Its geometry is avai
``width``, and ``height`` attributes. The ``is_primary``, ``name``, ``unique_id``, and ``output`` metadata attributes
are ``None`` when unavailable::

monitor = sct.monitors[1]
monitor = sct.primary_monitor
print(monitor.width, monitor.height)

Call ``monitor.as_region()`` when you need its geometry as a :py:class:`mss.models.Region`::
Expand Down
84 changes: 49 additions & 35 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

from __future__ import annotations

import dataclasses
import platform
import warnings
from abc import ABC, abstractmethod
from copy import copy
from datetime import datetime
from threading import Lock
from typing import TYPE_CHECKING, Any
Expand All @@ -17,7 +17,7 @@
from mss.tools import to_png

if TYPE_CHECKING:
from collections.abc import Callable, Iterator
from collections.abc import Callable, Sequence
from types import TracebackType

from typing_extensions import Buffer, Self
Expand Down Expand Up @@ -299,14 +299,18 @@ def close(self) -> None:
def grab(self, region: Monitor | Region | dict[str, Any] | tuple[int, int, int, int], /) -> ScreenShot:
"""Retrieve screen pixels for a given region.

``region`` can be a :class:`mss.models.Monitor`, a :class:`mss.models.Region`, a region dictionary, or a tuple
like the one :py:func:`PIL.ImageGrab.grab` accepts: ``(left, top, right, bottom)``.
``region`` can be a :class:`mss.models.Monitor`, a
:class:`mss.models.Region`, a region dictionary, or a tuple
like the one :py:func:`PIL.ImageGrab.grab` accepts: ``(left,
top, right, bottom)``.

:param region: The coordinates and size of the box to capture.
See :meth:`monitors <monitors>` for monitor object details.
:returns: Screenshot of the requested region.
"""
if isinstance(region, tuple):
if len(region) != 4: # noqa: PLR2004
msg = "Tuples for grab must have exactly four elements: left, top, right, bottom"
raise ScreenShotError(msg)
grab_region = Region(
left=region[0],
top=region[1],
Expand All @@ -316,14 +320,19 @@ def grab(self, region: Monitor | Region | dict[str, Any] | tuple[int, int, int,
elif isinstance(region, Monitor):
grab_region = region.as_region()
elif isinstance(region, Region):
grab_region = copy(region)
# Make a copy, in case the user changes the Region object later. We use dataclasses.replace because
# copy.copy doesn't call __post_init__, and we want the type normalization.
grab_region = dataclasses.replace(region)
elif isinstance(region, dict):
grab_region = Region(
left=region["left"],
top=region["top"],
width=region["width"],
height=region["height"],
)
else:
msg = f"Capture area must be a Region, Monitor, tuple, or dict: {region!r}"
raise ScreenShotError(msg)

if grab_region.width <= 0 or grab_region.height <= 0:
msg = f"Region has zero or negative size: {grab_region!r}"
Expand All @@ -344,25 +353,21 @@ def grab(self, region: Monitor | Region | dict[str, Any] | tuple[int, int, int,
@property
def monitors(self) -> Monitors:
"""Get positions of all monitors.
If the monitor has rotation, you have to deal with it
inside this method.

This method has to fill ``self._monitors`` with all information
and use it as a cache:
If a monitor is rotated, its dimensions reflect its displayed
orientation. In other words, height is the visible height, not
the number of scanout lines.

- ``self._monitors[0]`` is all monitors together
- ``self._monitors[N]`` is monitor N (with N > 0)
The first element, ``monitors[0]``, is all the monitors
together. It is the virtual desktop, holding all the monitors.
The remaining elements, ``monitors[N]`` (with N > 0), are the
individual displays.

Each :class:`mss.models.Monitor` has:
If monitors are being mirrored, the list will only include one
of the mirrored copies.

- ``left``: the x-coordinate of the upper-left corner
- ``top``: the y-coordinate of the upper-left corner
- ``width``: the width
- ``height``: the height
- ``is_primary``: true or false when known, otherwise ``None``
- ``name``: human-readable device name, or ``None``
- ``unique_id``: platform-specific stable identifier, or ``None``
- ``output``: Linux output name compatible with xrandr, or ``None``
.. seealso::
:py:attr:`primary_monitor`
"""
with self._lock:
if self._monitors is None:
Expand All @@ -374,9 +379,9 @@ def monitors(self) -> Monitors:
def primary_monitor(self) -> Monitor:
"""Get the primary monitor.

Returns the monitor marked as primary. If no monitor is marked as primary
(or the platform doesn't support primary monitor detection), returns the
first monitor (at index 1).
Returns the monitor marked as primary. If no monitor is marked
as primary (or the platform doesn't support primary monitor
detection), returns the first monitor (at index 1).

:raises ScreenShotError: If no monitors are available.

Expand All @@ -403,7 +408,7 @@ def save(
mon: int = 0,
output: str = "monitor-{mon}.png",
callback: Callable[[str], None] | None = None,
) -> Iterator[str]:
) -> Sequence[str]:
"""Grab a screenshot and save it to a file.

:param int mon: The monitor to screenshot (default=0). ``-1``
Expand All @@ -416,19 +421,26 @@ def save(
unavailable.
:param typing.Callable callback: Called before saving the
screenshot; receives the ``output`` argument.
:return: Created file(s).
:return: Names of created file(s).

.. version-changed:: 11.0.0
Prior to this version an Iterator was returned, rather than
a Sequence.
"""
monitors = self.monitors
if not monitors:
msg = "No monitor found."
raise ScreenShotError(msg)

rv: list[str] = []

date = datetime.now().astimezone()
if 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,
date=date,
top=monitor.top,
left=monitor.left,
width=monitor.width,
Expand All @@ -442,7 +454,8 @@ def save(
callback(fname)
sct = self.grab(monitor)
to_png(sct.rgb, sct.size, level=self.compression_level, output=fname)
yield fname
rv.append(fname)

else:
# A screenshot of all monitors together or
# a screenshot of the monitor N.
Expand All @@ -452,10 +465,9 @@ def save(
except IndexError as exc:
msg = f"Monitor {mon!r} does not exist."
raise ScreenShotError(msg) from exc

output = output.format(
fname = output.format(
mon=mon,
date=datetime.now(UTC) if "{date" in output else None,
date=date,
top=monitor.top,
left=monitor.left,
width=monitor.width,
Expand All @@ -466,17 +478,19 @@ def save(
output=monitor.output,
)
if callable(callback):
callback(output)
callback(fname)
sct = self.grab(monitor)
to_png(sct.rgb, sct.size, level=self.compression_level, output=output)
yield output
to_png(sct.rgb, sct.size, level=self.compression_level, output=fname)
rv.append(fname)

return rv

def shot(self, /, **kwargs: Any) -> str:
"""Helper to save the screenshot of the 1st monitor, by default.
You can pass the same arguments as for :meth:`save`.
"""
kwargs["mon"] = kwargs.get("mon", 1)
return next(self.save(**kwargs))
return self.save(**kwargs)[0]

@staticmethod
def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot:
Expand Down
Loading