From 4ee92e19001acc61f2b6a03990b2eb1ff59b82a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 10:52:56 +0100 Subject: [PATCH 1/5] mac: take screenshots at the nominal resolution --- CHANGELOG.md | 6 +++--- CHANGES.md | 8 ++++++++ src/mss/__init__.py | 2 +- src/mss/darwin.py | 8 +++++++- src/mss/linux.py | 2 +- src/mss/windows.py | 2 +- src/tests/conftest.py | 17 ----------------- src/tests/test_get_pixels.py | 6 +++--- src/tests/test_implementation.py | 17 ++++++----------- src/tests/test_macos.py | 15 +++++++++++++++ src/tests/third_party/test_numpy.py | 4 ++-- 11 files changed, 47 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39068fc1..a91deda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ See Git checking messages for full history. -## 10.0.1 (202x-xx-xx) -- -- :heart: contributors: @ +## 10.1.0-dev (2025-xx-xx) +- Mac: better performances (up to -60%) by taking screenshots at nominal resolution (e.g. scaling is off by default). To enable back scaling, set `mss.darwin.IMAGE_OPTIONS = 0`. (#257) +- :heart: contributors: @brycedrennan ## 10.0.0 (2024-11-14) - removed support for Python 3.8 diff --git a/CHANGES.md b/CHANGES.md index 2b456f87..f1030bd0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # Technical Changes +## 10.1.0 (2025-xx-xx) + +### darwin.py +- Added `IMAGE_OPTIONS` +- Added `kCGWindowImageBoundsIgnoreFraming` +- Added `kCGWindowImageNominalResolution` +- Added `kCGWindowImageShouldBeOpaque` + ## 10.0.0 (2024-11-14) ### base.py diff --git a/src/mss/__init__.py b/src/mss/__init__.py index f0282e4f..8d8ac6c9 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from mss.exception import ScreenShotError from mss.factory import mss -__version__ = "10.0.1" +__version__ = "10.1.0-dev" __author__ = "Mickaël Schoentgen" __date__ = "2013-2025" __copyright__ = f""" diff --git a/src/mss/darwin.py b/src/mss/darwin.py index a56e05a8..f001398b 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -22,6 +22,12 @@ MAC_VERSION_CATALINA = 10.16 +kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816 +kCGWindowImageNominalResolution = 1 << 4 # noqa: N816 +kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816 +# Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information) +IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution + def cgfloat() -> type[c_double | c_float]: """Get the appropriate value for a float.""" @@ -170,7 +176,7 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) - image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0) + image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) if not image_ref: msg = "CoreGraphics.CGWindowListCreateImage() failed." raise ScreenShotError(msg) diff --git a/src/mss/linux.py b/src/mss/linux.py index 20f85507..009b4234 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -264,7 +264,7 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"xfixes", "xlib", "xrandr", "_handles"} + __slots__ = {"_handles", "xfixes", "xlib", "xrandr"} def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" diff --git a/src/mss/windows.py b/src/mss/windows.py index 7a3a78f5..d5e2bb78 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -93,7 +93,7 @@ class BITMAPINFO(Structure): class MSS(MSSBase): """Multiple ScreenShots implementation for Microsoft Windows.""" - __slots__ = {"gdi32", "user32", "_handles"} + __slots__ = {"_handles", "gdi32", "user32"} def __init__(self, /, **kwargs: Any) -> None: """Windows initialisations.""" diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 5d455821..97928b78 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -2,7 +2,6 @@ Source: https://github.com/BoboTiG/python-mss. """ -import platform from collections.abc import Generator from hashlib import sha256 from pathlib import Path @@ -10,8 +9,6 @@ import pytest -from mss import mss - @pytest.fixture(autouse=True) def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: @@ -48,17 +45,3 @@ def raw() -> bytes: assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd" return data - - -@pytest.fixture(scope="session") -def pixel_ratio() -> int: - """Get the pixel, used to adapt test checks.""" - if platform.system().lower() != "darwin": - return 1 - - # Grab a 1x1 screenshot - region = {"top": 0, "left": 0, "width": 1, "height": 1} - - with mss() as sct: - # On macOS with Retina display, the width can be 2 instead of 1 - return sct.grab(region).size[0] diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index 211c7108..486823c3 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -21,7 +21,7 @@ def test_grab_monitor() -> None: assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(pixel_ratio: int) -> None: +def test_grab_part_of_screen() -> None: with mss(display=os.getenv("DISPLAY")) as sct: for width, height in itertools.product(range(1, 42), range(1, 42)): monitor = {"top": 160, "left": 160, "width": width, "height": height} @@ -29,8 +29,8 @@ def test_grab_part_of_screen(pixel_ratio: int) -> None: assert image.top == 160 assert image.left == 160 - assert image.width == width * pixel_ratio - assert image.height == height * pixel_ratio + assert image.width == width + assert image.height == height def test_get_pixel(raw: bytes) -> None: diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 5672f044..294ccc80 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -64,14 +64,9 @@ def test_bad_monitor() -> None: sct.shot(mon=222) -def test_repr(pixel_ratio: int) -> None: +def test_repr() -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} - expected_box = { - "top": 0, - "left": 0, - "width": 10 * pixel_ratio, - "height": 10 * pixel_ratio, - } + expected_box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss.mss(display=os.getenv("DISPLAY")) as sct: img = sct.grab(box) ref = ScreenShot(bytearray(b"42"), expected_box) @@ -195,7 +190,7 @@ def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None: assert "usage: mss" in captured.out -def test_grab_with_tuple(pixel_ratio: int) -> None: +def test_grab_with_tuple() -> None: left = 100 top = 100 right = 500 @@ -207,7 +202,7 @@ def test_grab_with_tuple(pixel_ratio: int) -> None: # PIL like box = (left, top, right, lower) im = sct.grab(box) - assert im.size == (width * pixel_ratio, height * pixel_ratio) + assert im.size == (width, height) # MSS like box2 = {"left": left, "top": top, "width": width, "height": height} @@ -217,7 +212,7 @@ def test_grab_with_tuple(pixel_ratio: int) -> None: assert im.rgb == im2.rgb -def test_grab_with_tuple_percents(pixel_ratio: int) -> None: +def test_grab_with_tuple_percents() -> None: with mss.mss(display=os.getenv("DISPLAY")) as sct: monitor = sct.monitors[1] left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left @@ -230,7 +225,7 @@ def test_grab_with_tuple_percents(pixel_ratio: int) -> None: # PIL like box = (left, top, right, lower) im = sct.grab(box) - assert im.size == (width * pixel_ratio, height * pixel_ratio) + assert im.size == (width, height) # MSS like box2 = {"left": left, "top": top, "width": width, "height": height} diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index cce8121b..f3927764 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -4,6 +4,7 @@ import ctypes.util import platform +from unittest.mock import patch import pytest @@ -67,3 +68,17 @@ def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *_: None) with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) + + +def test_scaling_on() -> None: + """Screnshots are taken at the nominal resolution by default, but scaling can be turned on manually.""" + # Grab a 1x1 screenshot + region = {"top": 0, "left": 0, "width": 1, "height": 1} + + with mss.mss() as sct: + # Nominal resolution, i.e.: scaling is off + assert sct.grab(region).size[0] == 1 + + # Retina resolution, i.e.: scaling is on + with patch.object(mss.darwin, "IMAGE_OPTIONS", 0): + assert sct.grab(region).size[0] == 2 diff --git a/src/tests/third_party/test_numpy.py b/src/tests/third_party/test_numpy.py index 6a2f2e09..6d5cf286 100644 --- a/src/tests/third_party/test_numpy.py +++ b/src/tests/third_party/test_numpy.py @@ -12,8 +12,8 @@ np = pytest.importorskip("numpy", reason="Numpy module not available.") -def test_numpy(pixel_ratio: int) -> None: +def test_numpy() -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss(display=os.getenv("DISPLAY")) as sct: img = np.array(sct.grab(box)) - assert len(img) == 10 * pixel_ratio + assert len(img) == 10 From 2951a3dfcc1d64eba489ba425cd8773bf22a9cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 11:00:31 +0100 Subject: [PATCH 2/5] fix --- CHANGELOG.md | 2 +- src/mss/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91deda3..7afa7b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ See Git checking messages for full history. -## 10.1.0-dev (2025-xx-xx) +## 10.1.0.dev0 (2025-xx-xx) - Mac: better performances (up to -60%) by taking screenshots at nominal resolution (e.g. scaling is off by default). To enable back scaling, set `mss.darwin.IMAGE_OPTIONS = 0`. (#257) - :heart: contributors: @brycedrennan diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 8d8ac6c9..ef0faaae 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from mss.exception import ScreenShotError from mss.factory import mss -__version__ = "10.1.0-dev" +__version__ = "10.1.0.dev0" __author__ = "Mickaël Schoentgen" __date__ = "2013-2025" __copyright__ = f""" From 48cad055c2eeed5e6f6f426fc583605e1e12c73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 11:01:07 +0100 Subject: [PATCH 3/5] debug --- src/mss/darwin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mss/darwin.py b/src/mss/darwin.py index f001398b..fafe928e 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -176,6 +176,7 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) + print(f"{IMAGE_OPTIONS = }") image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) if not image_ref: msg = "CoreGraphics.CGWindowListCreateImage() failed." From 65f941050146dabab32a8d4aeb9aa980f429193a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 11:03:40 +0100 Subject: [PATCH 4/5] clean-up --- src/mss/darwin.py | 1 - src/tests/test_macos.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mss/darwin.py b/src/mss/darwin.py index fafe928e..f001398b 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -176,7 +176,6 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) - print(f"{IMAGE_OPTIONS = }") image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) if not image_ref: msg = "CoreGraphics.CGWindowListCreateImage() failed." diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index f3927764..c89ea2a8 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -81,4 +81,4 @@ def test_scaling_on() -> None: # Retina resolution, i.e.: scaling is on with patch.object(mss.darwin, "IMAGE_OPTIONS", 0): - assert sct.grab(region).size[0] == 2 + assert sct.grab(region).size[0] in {1, 2} # 1 on the CI, 2 for all other the world From b815c9ed7b3bc57a9695b4a30a325368480ab9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 11:10:39 +0100 Subject: [PATCH 5/5] docs: tweak [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7afa7b65..f0da0127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ See Git checking messages for full history. ## 10.1.0.dev0 (2025-xx-xx) -- Mac: better performances (up to -60%) by taking screenshots at nominal resolution (e.g. scaling is off by default). To enable back scaling, set `mss.darwin.IMAGE_OPTIONS = 0`. (#257) +- Mac: up to 60% performances improvement by taking screenshots at nominal resolution (e.g. scaling is off by default). To enable back scaling, set `mss.darwin.IMAGE_OPTIONS = 0`. (#257) - :heart: contributors: @brycedrennan ## 10.0.0 (2024-11-14)