diff --git a/CHANGELOG.md b/CHANGELOG.md index 39068fc1..f0da0127 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.dev0 (2025-xx-xx) +- 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) - 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..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.0.1" +__version__ = "10.1.0.dev0" __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..c89ea2a8 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] in {1, 2} # 1 on the CI, 2 for all other the world 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