Skip to content
Draft
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
8 changes: 5 additions & 3 deletions docs/source/release-history/v11.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ writing, but should be by the time we release 11.0!)

#### ScreenShot attributes

The {py:attr}`mss.ScreenShot.raw` attribute has been removed. Use the {py:attr}`mss.ScreenShot.bgra` property instead.
The {py:attr}`mss.ScreenShot.raw` attribute has been deprecated, and will soon be removed. Use the
{py:attr}`mss.ScreenShot.bgra` property instead.

The {py:attr}`mss.ScreenShot.bgra` and {py:attr}`mss.ScreenShot.rgb` properties now will return read-only bytes-like
The {py:attr}`mss.ScreenShot.bgra` and {py:attr}`mss.ScreenShot.rgb` properties now will return bytes-like
{py:type}`memoryview` objects, not necessarily {py:type}`bytes` or {py:type}`bytearray` objects. For practical use
cases, this should not be noticible. This change was allows faster access to screenshot data, with fewer memory copies.
cases, this should not be noticible. This change was allows faster access to screenshot data, with fewer memory
copies.

### Python 3.9 EOL

Expand Down
90 changes: 62 additions & 28 deletions src/mss/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import warnings
from typing import TYPE_CHECKING

from mss.exception import ScreenShotError
Expand All @@ -26,7 +27,6 @@ class ScreenShot:
__slots__ = {"__bgra", "__pixels", "__rgb", "_raw", "pos", "size"}

def __init__(self, data: Buffer, monitor: Monitor, /, *, size: Size | None = None) -> None:
self.__bgra: memoryview | None = None
self.__pixels: Pixels | None = None
self.__rgb: memoryview | None = None

Expand All @@ -36,13 +36,10 @@ def __init__(self, data: Buffer, monitor: Monitor, /, *, size: Size | None = Non
#: NamedTuple of the screenshot size.
self.size: Size = Size(monitor["width"], monitor["height"]) if size is None else size

# Buffer of the raw BGRA pixels, retrieved by the
# platform-specific implementations. This is kept read-write
# if it was originally so, in order for _merge to work.
# However, it should be made read-only before returning to the
# user (via bgra), so that the cached values for __pixels and
# __rgb aren't potentially inconsistent if the user changes
# data.
# Buffer of the raw BGRA pixels, retrieved by the platform-specific implementations. This is kept read-write if
# it was originally so, in order for _merge to work, and so it can be used with ctypes. However, it should not
# be modified once __pixels or __rgb have been accessed, so that the cached values for __pixels and __rgb aren't
# potentially inconsistent if the user changes data.
self._raw: memoryview = memoryview(data)
assert self._raw.nbytes == self.size.width * self.size.height * 4, ( # noqa: S101
"Data size does not match screenshot dimensions."
Expand All @@ -57,14 +54,18 @@ def __array_interface__(self) -> dict[str, Any]:
"""NumPy array interface support.

This is used by NumPy, many SciPy projects, CuPy, PyTorch (via
``torch.from_numpy``), TensorFlow (via ``tf.convert_to_tensor``),
JAX (via ``jax.numpy.asarray``), Pandas, scikit-learn, Matplotlib,
:py:func:`torch.from_numpy`), TensorFlow (via
:py:func:`tf.convert_to_tensor`), JAX (via
:py:func:`jax.numpy.asarray`), Pandas, scikit-learn, Matplotlib,
some OpenCV functions, and others. This allows you to pass a
:class:`ScreenShot` instance directly to these libraries without
needing to convert it first.

This is in HWC order, with 4 channels (BGRA).

The array is read-write, for maximum compatibility. However,
actually modifying the data may cause undefined behavior.

.. seealso::

https://numpy.org/doc/stable/reference/arrays.interface.html
Expand All @@ -74,7 +75,7 @@ def __array_interface__(self) -> dict[str, Any]:
"version": 3,
"shape": (self.height, self.width, 4),
"typestr": "|u1",
"data": self.bgra,
"data": self._raw,
}

@classmethod
Expand All @@ -91,21 +92,43 @@ def bgra(self) -> memoryview:
BGRxBGRx... sequence. A specific pixel can be accessed as
``bgra[(y * width + x) * 4:(y * width + x) * 4 + 4].``

The memoryview is read-only. PyTorch will issue a warning
when given a read-only buffer, but will still work. However,
actually modifying the data may cause undefined behavior.
The memoryview is read-write, for compatibility with ctypes'
:py:func:`from_buffer`. However, actually modifying the data
may cause undefined behavior.

.. note::
While the name is ``bgra``, the alpha channel may or may
not be valid.

.. version-changed:: 11.0.0
Prior to this version, this was a :py:class:`bytes` object.
It was changed to a memoryview for improved performance.
Most practical uses are unaffected by this change, as
``memoryview`` supports most of the same operations as
``bytes``. If needed, you can use
:py:meth:`memoryview.tobytes` to get a ``bytes`` object.
"""
# Making a read-only copy of a memoryview is very cheap. But
# we still always return the same memoryview: somebody using a
# property may expect it to be identical (under the `is`
# operator) every time.
if self.__bgra is None:
self.__bgra = self._raw.toreadonly()
return self.__bgra
return self._raw

@property
def raw(self) -> memoryview:
"""Deprecated alias for :py:attr:`bgra`.

.. version-deprecated:: 10.2.0
Use :py:attr:`bgra` instead. This alias will be removed in
a future version.

.. version-changed:: 11.0.0
Prior to this version, this was a :py:class:`bytearray`.
This :py:attr:`raw` alias is retained, although as a
:py:class:`memoryview`, for backwards compatibility: most
existing uses are not affected, as ``memoryview`` supports
most of the same operations as ``bytearray``. If needed,
you can use ``bytearray(raw)`` to get a ``bytearray``
object.
"""
warnings.warn("The raw property is deprecated. Use bgra instead.", DeprecationWarning, stacklevel=2)
return self._raw

@property
def pixels(self) -> Pixels:
Expand Down Expand Up @@ -139,22 +162,33 @@ def rgb(self) -> memoryview:
RGBRGB... sequence. A specific pixel can be accessed as
``rgb[(y * width + x) * 4:(y * width + x) * 4 + 4].``

The memoryview is read-only. PyTorch will issue a warning
when given a read-only buffer, but will still work. However,
actually modifying the data may cause undefined behavior.
The memoryview is read-write, for compatibility with ctypes'
:py:func:`from_buffer`. However, actually modifying the data
may cause undefined behavior.

:: note::
.. note::
This is a computed property. If possible, using the
:py:attr:`bgra` property directly is usually more
efficient.
:py:attr:`bgra` property directly is usually more efficient.

.. version-changed:: 11.0.0
Prior to this version, this was a :py:class:`bytes` object.
It was changed to a memoryview for improved performance.
Most practical uses are unaffected by this change, as
``memoryview`` supports most of the same operations as
``bytes``. If needed, you can use
:py:meth:`memoryview.tobytes` to get a ``bytes`` object.
"""
if self.__rgb is None:
rgb = bytearray(self.height * self.width * 3)
raw = self._raw
rgb[::3] = raw[2::4]
rgb[1::3] = raw[1::4]
rgb[2::3] = raw[::4]
self.__rgb = memoryview(rgb).toreadonly()
# We could just return the bytearray directly. However, we would rather give ourselves the flexibility to
# use other buffer types in the future. Rather than declaring our return type as just generically Buffer
# (which doesn't guarantee the indexing behavior of memoryview), we always return a memoryview, which is
# cheap.
self.__rgb = memoryview(rgb)

return self.__rgb

Expand Down
17 changes: 16 additions & 1 deletion src/tests/test_bgra_to_rgb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@
Source: https://github.com/BoboTiG/python-mss.
"""

import ctypes

from mss.base import ScreenShot


def test_good_types(raw: bytes) -> None:
image = ScreenShot.from_size(bytearray(raw), 1024, 768)
assert isinstance(image.rgb, memoryview)
assert image.rgb.readonly


def test_contents() -> None:
image = ScreenShot.from_size(b"BGRA" * 1024 * 768, 1024, 768)
assert bytes(image.rgb) == b"RGB" * 1024 * 768


def test_ctypes_pointers_from_rgb() -> None:
image = ScreenShot.from_size(b"BGRA" * 4, 2, 2)
assert image.rgb.readonly is False

rgb_array = (ctypes.c_uint8 * len(image.rgb)).from_buffer(image.rgb)
void_ptr = ctypes.c_void_p(ctypes.addressof(rgb_array))
uint8_ptr = ctypes.cast(void_ptr, ctypes.POINTER(ctypes.c_uint8))

assert void_ptr.value is not None
assert uint8_ptr[0] == ord("R")
assert uint8_ptr[1] == ord("G")
assert uint8_ptr[2] == ord("B")
17 changes: 16 additions & 1 deletion src/tests/test_get_pixels.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Source: https://github.com/BoboTiG/python-mss.
"""

import ctypes
import itertools
from collections.abc import Callable

Expand All @@ -17,7 +18,6 @@ def test_grab_monitor(mss_impl: Callable[..., MSS]) -> None:
image = sct.grab(mon)
assert isinstance(image, ScreenShot)
assert isinstance(image.bgra, memoryview)
assert image.bgra.readonly


def test_grab_part_of_screen(mss_impl: Callable[..., MSS]) -> None:
Expand Down Expand Up @@ -45,3 +45,18 @@ def test_get_pixel(raw: bytes) -> None:

with pytest.raises(ScreenShotError):
image.pixel(image.width + 1, 12)


def test_ctypes_pointers_from_bgra(raw: bytes) -> None:
image = ScreenShot.from_size(bytearray(raw), 1024, 768)
assert image.bgra.readonly is False

bgra_array = (ctypes.c_uint8 * len(image.bgra)).from_buffer(image.bgra)
void_ptr = ctypes.c_void_p(ctypes.addressof(bgra_array))
uint8_ptr = ctypes.cast(void_ptr, ctypes.POINTER(ctypes.c_uint8))

assert void_ptr.value is not None
assert uint8_ptr[0] == raw[0]
assert uint8_ptr[1] == raw[1]
assert uint8_ptr[2] == raw[2]
assert uint8_ptr[3] == raw[3]
Loading