diff --git a/docs/source/release-history/v11.0.0.md b/docs/source/release-history/v11.0.0.md index 15ed8b8c..bcb75929 100644 --- a/docs/source/release-history/v11.0.0.md +++ b/docs/source/release-history/v11.0.0.md @@ -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 diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index b7b0f6cf..ba448a89 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -3,6 +3,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING from mss.exception import ScreenShotError @@ -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 @@ -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." @@ -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 @@ -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 @@ -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: @@ -139,14 +162,21 @@ 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) @@ -154,7 +184,11 @@ def rgb(self) -> memoryview: 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 diff --git a/src/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py index deed4b60..6cd46816 100644 --- a/src/tests/test_bgra_to_rgb.py +++ b/src/tests/test_bgra_to_rgb.py @@ -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") diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index c784a6bd..1c980931 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -2,6 +2,7 @@ Source: https://github.com/BoboTiG/python-mss. """ +import ctypes import itertools from collections.abc import Callable @@ -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: @@ -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]