Skip to content

Commit f531c38

Browse files
authored
mac: take screenshots at the nominal resolution (#346)
1 parent 1a0dbf1 commit f531c38

File tree

11 files changed

+47
-40
lines changed

11 files changed

+47
-40
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
See Git checking messages for full history.
44

5-
## 10.0.1 (202x-xx-xx)
6-
-
7-
- :heart: contributors: @
5+
## 10.1.0.dev0 (2025-xx-xx)
6+
- 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)
7+
- :heart: contributors: @brycedrennan
88

99
## 10.0.0 (2024-11-14)
1010
- removed support for Python 3.8

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Technical Changes
22

3+
## 10.1.0 (2025-xx-xx)
4+
5+
### darwin.py
6+
- Added `IMAGE_OPTIONS`
7+
- Added `kCGWindowImageBoundsIgnoreFraming`
8+
- Added `kCGWindowImageNominalResolution`
9+
- Added `kCGWindowImageShouldBeOpaque`
10+
311
## 10.0.0 (2024-11-14)
412

513
### base.py

src/mss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mss.exception import ScreenShotError
1212
from mss.factory import mss
1313

14-
__version__ = "10.0.1"
14+
__version__ = "10.1.0.dev0"
1515
__author__ = "Mickaël Schoentgen"
1616
__date__ = "2013-2025"
1717
__copyright__ = f"""

src/mss/darwin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222

2323
MAC_VERSION_CATALINA = 10.16
2424

25+
kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816
26+
kCGWindowImageNominalResolution = 1 << 4 # noqa: N816
27+
kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816
28+
# Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information)
29+
IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution
30+
2531

2632
def cgfloat() -> type[c_double | c_float]:
2733
"""Get the appropriate value for a float."""
@@ -170,7 +176,7 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
170176
core = self.core
171177
rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"]))
172178

173-
image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0)
179+
image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS)
174180
if not image_ref:
175181
msg = "CoreGraphics.CGWindowListCreateImage() failed."
176182
raise ScreenShotError(msg)

src/mss/linux.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class MSS(MSSBase):
264264
It uses intensively the Xlib and its Xrandr extension.
265265
"""
266266

267-
__slots__ = {"xfixes", "xlib", "xrandr", "_handles"}
267+
__slots__ = {"_handles", "xfixes", "xlib", "xrandr"}
268268

269269
def __init__(self, /, **kwargs: Any) -> None:
270270
"""GNU/Linux initialisations."""

src/mss/windows.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class BITMAPINFO(Structure):
9393
class MSS(MSSBase):
9494
"""Multiple ScreenShots implementation for Microsoft Windows."""
9595

96-
__slots__ = {"gdi32", "user32", "_handles"}
96+
__slots__ = {"_handles", "gdi32", "user32"}
9797

9898
def __init__(self, /, **kwargs: Any) -> None:
9999
"""Windows initialisations."""

src/tests/conftest.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
Source: https://github.com/BoboTiG/python-mss.
33
"""
44

5-
import platform
65
from collections.abc import Generator
76
from hashlib import sha256
87
from pathlib import Path
98
from zipfile import ZipFile
109

1110
import pytest
1211

13-
from mss import mss
14-
1512

1613
@pytest.fixture(autouse=True)
1714
def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator:
@@ -48,17 +45,3 @@ def raw() -> bytes:
4845

4946
assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd"
5047
return data
51-
52-
53-
@pytest.fixture(scope="session")
54-
def pixel_ratio() -> int:
55-
"""Get the pixel, used to adapt test checks."""
56-
if platform.system().lower() != "darwin":
57-
return 1
58-
59-
# Grab a 1x1 screenshot
60-
region = {"top": 0, "left": 0, "width": 1, "height": 1}
61-
62-
with mss() as sct:
63-
# On macOS with Retina display, the width can be 2 instead of 1
64-
return sct.grab(region).size[0]

src/tests/test_get_pixels.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ def test_grab_monitor() -> None:
2121
assert isinstance(image.rgb, bytes)
2222

2323

24-
def test_grab_part_of_screen(pixel_ratio: int) -> None:
24+
def test_grab_part_of_screen() -> None:
2525
with mss(display=os.getenv("DISPLAY")) as sct:
2626
for width, height in itertools.product(range(1, 42), range(1, 42)):
2727
monitor = {"top": 160, "left": 160, "width": width, "height": height}
2828
image = sct.grab(monitor)
2929

3030
assert image.top == 160
3131
assert image.left == 160
32-
assert image.width == width * pixel_ratio
33-
assert image.height == height * pixel_ratio
32+
assert image.width == width
33+
assert image.height == height
3434

3535

3636
def test_get_pixel(raw: bytes) -> None:

src/tests/test_implementation.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,9 @@ def test_bad_monitor() -> None:
6464
sct.shot(mon=222)
6565

6666

67-
def test_repr(pixel_ratio: int) -> None:
67+
def test_repr() -> None:
6868
box = {"top": 0, "left": 0, "width": 10, "height": 10}
69-
expected_box = {
70-
"top": 0,
71-
"left": 0,
72-
"width": 10 * pixel_ratio,
73-
"height": 10 * pixel_ratio,
74-
}
69+
expected_box = {"top": 0, "left": 0, "width": 10, "height": 10}
7570
with mss.mss(display=os.getenv("DISPLAY")) as sct:
7671
img = sct.grab(box)
7772
ref = ScreenShot(bytearray(b"42"), expected_box)
@@ -195,7 +190,7 @@ def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None:
195190
assert "usage: mss" in captured.out
196191

197192

198-
def test_grab_with_tuple(pixel_ratio: int) -> None:
193+
def test_grab_with_tuple() -> None:
199194
left = 100
200195
top = 100
201196
right = 500
@@ -207,7 +202,7 @@ def test_grab_with_tuple(pixel_ratio: int) -> None:
207202
# PIL like
208203
box = (left, top, right, lower)
209204
im = sct.grab(box)
210-
assert im.size == (width * pixel_ratio, height * pixel_ratio)
205+
assert im.size == (width, height)
211206

212207
# MSS like
213208
box2 = {"left": left, "top": top, "width": width, "height": height}
@@ -217,7 +212,7 @@ def test_grab_with_tuple(pixel_ratio: int) -> None:
217212
assert im.rgb == im2.rgb
218213

219214

220-
def test_grab_with_tuple_percents(pixel_ratio: int) -> None:
215+
def test_grab_with_tuple_percents() -> None:
221216
with mss.mss(display=os.getenv("DISPLAY")) as sct:
222217
monitor = sct.monitors[1]
223218
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:
230225
# PIL like
231226
box = (left, top, right, lower)
232227
im = sct.grab(box)
233-
assert im.size == (width * pixel_ratio, height * pixel_ratio)
228+
assert im.size == (width, height)
234229

235230
# MSS like
236231
box2 = {"left": left, "top": top, "width": width, "height": height}

src/tests/test_macos.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import ctypes.util
66
import platform
7+
from unittest.mock import patch
78

89
import pytest
910

@@ -67,3 +68,17 @@ def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None:
6768
monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *_: None)
6869
with pytest.raises(ScreenShotError):
6970
sct.grab(sct.monitors[1])
71+
72+
73+
def test_scaling_on() -> None:
74+
"""Screnshots are taken at the nominal resolution by default, but scaling can be turned on manually."""
75+
# Grab a 1x1 screenshot
76+
region = {"top": 0, "left": 0, "width": 1, "height": 1}
77+
78+
with mss.mss() as sct:
79+
# Nominal resolution, i.e.: scaling is off
80+
assert sct.grab(region).size[0] == 1
81+
82+
# Retina resolution, i.e.: scaling is on
83+
with patch.object(mss.darwin, "IMAGE_OPTIONS", 0):
84+
assert sct.grab(region).size[0] in {1, 2} # 1 on the CI, 2 for all other the world

0 commit comments

Comments
 (0)