Skip to content
Merged
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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/mss/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
8 changes: 7 additions & 1 deletion src/mss/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/mss/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion src/mss/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
17 changes: 0 additions & 17 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
Source: https://github.com/BoboTiG/python-mss.
"""

import platform
from collections.abc import Generator
from hashlib import sha256
from pathlib import Path
from zipfile import ZipFile

import pytest

from mss import mss


@pytest.fixture(autouse=True)
def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator:
Expand Down Expand Up @@ -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]
6 changes: 3 additions & 3 deletions src/tests/test_get_pixels.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ 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}
image = sct.grab(monitor)

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:
Expand Down
17 changes: 6 additions & 11 deletions src/tests/test_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand All @@ -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
Expand All @@ -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}
Expand Down
15 changes: 15 additions & 0 deletions src/tests/test_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import ctypes.util
import platform
from unittest.mock import patch

import pytest

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/tests/third_party/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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