From 672ee765cf37f4df5672f77dbf0d8a639461652e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 1 May 2023 00:36:33 +0200 Subject: [PATCH] Mac: disable auto-scaling --- .github/workflows/tests.yml | 90 +++++++++++++++++------------------ src/mss/darwin.py | 70 +++++++++++++++++---------- src/tests/conftest.py | 18 ------- src/tests/test_get_pixels.py | 6 +-- src/tests/test_macos.py | 23 ++++++--- src/tests/test_third_party.py | 25 ++++------ 6 files changed, 117 insertions(+), 115 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f46daa3..fafaad4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,38 +8,38 @@ on: workflow_dispatch: jobs: - quality: - name: Quality - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - cache: pip - - name: Install dependencies - run: | - python -m pip install -U pip - python -m pip install -e '.[dev]' - - name: Tests - run: ./check.sh + # quality: + # name: Quality + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-python@v4 + # with: + # python-version: "3.x" + # cache: pip + # - name: Install dependencies + # run: | + # python -m pip install -U pip + # python -m pip install -e '.[dev]' + # - name: Tests + # run: ./check.sh - documentation: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - cache: pip - - name: Install test dependencies - run: | - python -m pip install -U pip - python -m pip install -e '.[test]' - - name: Tests - run: | - sphinx-build -d docs docs/source docs_out --color -W -bhtml + # documentation: + # name: Documentation + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-python@v4 + # with: + # python-version: "3.x" + # cache: pip + # - name: Install test dependencies + # run: | + # python -m pip install -U pip + # python -m pip install -e '.[test]' + # - name: Tests + # run: | + # sphinx-build -d docs docs/source docs_out --color -W -bhtml tests: name: "${{ matrix.os.emoji }} ${{ matrix.python.name }}" @@ -48,25 +48,25 @@ jobs: fail-fast: false matrix: os: - - emoji: 🐧 - runs-on: [ubuntu-latest] + # - emoji: 🐧 + # runs-on: [ubuntu-latest] - emoji: 🍎 runs-on: [macos-latest] - - emoji: 🪟 - runs-on: [windows-latest] + # - emoji: 🪟 + # runs-on: [windows-latest] python: - name: CPython 3.8 runs-on: "3.8" - - name: CPython 3.9 - runs-on: "3.9" - - name: CPython 3.10 - runs-on: "3.10" - - name: CPython 3.11 - runs-on: "3.11" - - name: CPython 3.12 - runs-on: "3.12-dev" - - name: PyPy 3.9 - runs-on: "pypy-3.9" + # - name: CPython 3.9 + # runs-on: "3.9" + # - name: CPython 3.10 + # runs-on: "3.10" + # - name: CPython 3.11 + # runs-on: "3.11" + # - name: CPython 3.12 + # runs-on: "3.12-dev" + # - name: PyPy 3.9 + # runs-on: "pypy-3.9" steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/src/mss/darwin.py b/src/mss/darwin.py index 1dd37ea..752f1b3 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -5,7 +5,7 @@ import ctypes import ctypes.util import sys -from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p +from ctypes import POINTER, Structure, c_bool, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p from platform import mac_ver from typing import Any, Optional, Type, Union @@ -23,6 +23,19 @@ def cgfloat() -> Union[Type[c_double], Type[c_float]]: return c_double if sys.maxsize > 2**32 else c_float +class CGImage(Structure): + """Structure that contains information about a composite image.""" + + _fields_ = [ + ("isMask", c_bool), + ("width", c_uint32), + ("height", c_uint32), + ("bitsPerComponent", c_uint32), + ("bitsPerPixel", c_uint32), + ("bytesPerRow", c_uint32), + ] + + class CGPoint(Structure): """Structure that contains coordinates of a rectangle.""" @@ -64,19 +77,25 @@ def __repr__(self) -> str: "CGDisplayRotation": ("core", [c_uint32], c_float), "CFDataGetBytePtr": ("core", [c_void_p], c_void_p), "CFDataGetLength": ("core", [c_void_p], c_uint64), - "CFRelease": ("core", [c_void_p], c_void_p), - "CGDataProviderRelease": ("core", [c_void_p], c_void_p), + "CFRelease": ("core", [POINTER(CGImage)], c_void_p), + "CGDataProviderRelease": ("core", [POINTER(CGImage)], c_void_p), "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32), - "CGImageGetBitsPerPixel": ("core", [c_void_p], int), - "CGImageGetBytesPerRow": ("core", [c_void_p], int), - "CGImageGetDataProvider": ("core", [c_void_p], c_void_p), - "CGImageGetHeight": ("core", [c_void_p], int), - "CGImageGetWidth": ("core", [c_void_p], int), + # "CGImageGetBitsPerPixel": ("core", [c_void_p], int), + # "CGImageGetBytesPerRow": ("core", [c_void_p], int), + "CGImageGetDataProvider": ("core", [POINTER(CGImage)], c_void_p), + # "CGImageGetHeight": ("core", [c_void_p], int), + # "CGImageGetWidth": ("core", [c_void_p], int), "CGRectStandardize": ("core", [CGRect], CGRect), "CGRectUnion": ("core", [CGRect, CGRect], CGRect), - "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p), + "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], CGImage), } +_CORE = ( + ctypes.util.find_library("CoreGraphics") + if float(".".join(mac_ver()[0].split(".")[:2])) < 10.16 + else "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" +) + class MSS(MSSBase): """ @@ -98,16 +117,9 @@ def __init__(self, /, **kwargs: Any) -> None: def _init_library(self) -> None: """Load the CoreGraphics library.""" - version = float(".".join(mac_ver()[0].split(".")[:2])) - if version < 10.16: - coregraphics = ctypes.util.find_library("CoreGraphics") - else: - # macOS Big Sur and newer - coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" - - if not coregraphics: + if not _CORE: raise ScreenShotError("No CoreGraphics library found.") - self.core = ctypes.cdll.LoadLibrary(coregraphics) + self.core = ctypes.cdll.LoadLibrary(_CORE) def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" @@ -138,9 +150,11 @@ def _monitors_impl(self) -> None: rect = core.CGDisplayBounds(display) rect = core.CGRectStandardize(rect) width, height = rect.size.width, rect.size.height + + # {0.0: "normal", 90.0: "right", -90.0: "left"} if core.CGDisplayRotation(display) in {90.0, -90.0}: - # {0.0: "normal", 90.0: "right", -90.0: "left"} width, height = height, width + self._monitors.append( { "left": int_(rect.origin.x), @@ -169,15 +183,17 @@ 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) - if not image_ref: + cg_image = core.CGWindowListCreateImage(rect, 1, 0, 0) + if not cg_image: raise ScreenShotError("CoreGraphics.CGWindowListCreateImage() failed.") - width = core.CGImageGetWidth(image_ref) - height = core.CGImageGetHeight(image_ref) + # width = core.CGImageGetWidth(cg_image) + # height = core.CGImageGetHeight(cg_image) + width = cg_image.width + height = cg_image.height prov = copy_data = None try: - prov = core.CGImageGetDataProvider(image_ref) + prov = core.CGImageGetDataProvider(POINTER(cg_image)) copy_data = core.CGDataProviderCopyData(prov) data_ref = core.CFDataGetBytePtr(copy_data) buf_len = core.CFDataGetLength(copy_data) @@ -185,8 +201,10 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: data = bytearray(raw.contents) # Remove padding per row - bytes_per_row = core.CGImageGetBytesPerRow(image_ref) - bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref) + # bytes_per_row = core.CGImageGetBytesPerRow(cg_image) + # bytes_per_pixel = core.CGImageGetBitsPerPixel(cg_image) + bytes_per_row = cg_image.bytesPerRow + bytes_per_pixel = cg_image.bitsPerPixel bytes_per_pixel = (bytes_per_pixel + 7) // 8 if bytes_per_pixel * width != bytes_per_row: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 72da4df..5ac0a33 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -4,15 +4,12 @@ """ import glob import os -import platform from hashlib import md5 from pathlib import Path from zipfile import ZipFile import pytest -from mss import mss - @pytest.fixture(autouse=True) def no_warnings(recwarn): @@ -51,18 +48,3 @@ def raw() -> bytes: assert md5(data).hexdigest() == "125696266e2a8f5240f6bc17e4df98c6" 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 8535538..097f37f 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -22,7 +22,7 @@ def test_grab_monitor(): assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(pixel_ratio): +def test_grab_part_of_screen(): 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} @@ -30,8 +30,8 @@ def test_grab_part_of_screen(pixel_ratio): 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): diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index 113b33e..c67ce47 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -7,16 +7,16 @@ import pytest -import mss -from mss.exception import ScreenShotError - if platform.system().lower() != "darwin": pytestmark = pytest.mark.skip +from mss import mss +from mss.darwin import CGPoint, CGRect, CGSize +from mss.exception import ScreenShotError +from mss.models import Size -def test_repr(): - from mss.darwin import CGPoint, CGRect, CGSize +def test_repr(): # CGPoint point = CGPoint(2.0, 1.0) ref = CGPoint() @@ -48,10 +48,10 @@ def test_implementation(monkeypatch): if version < 10.16: monkeypatch.setattr(ctypes.util, "find_library", lambda x: None) with pytest.raises(ScreenShotError): - mss.mss() + mss() monkeypatch.undo() - with mss.mss() as sct: + with mss() as sct: # Test monitor's rotation original = sct.monitors[1] monkeypatch.setattr(sct.core, "CGDisplayRotation", lambda x: -90.0) @@ -65,3 +65,12 @@ def test_implementation(monkeypatch): monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *args: None) with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) + + +def test_no_auto_scaling(): + # Grab a 1x1 screenshot + region = {"top": 0, "left": 0, "width": 1, "height": 1} + + with mss() as sct: + # MSS < 4.0, the width/height used to be 2 instead of 1 on Retina display + assert sct.grab(region).size == Size(1, 1) diff --git a/src/tests/test_third_party.py b/src/tests/test_third_party.py index e89afbd..a50ce39 100644 --- a/src/tests/test_third_party.py +++ b/src/tests/test_third_party.py @@ -10,28 +10,19 @@ from mss import mss -try: - import numpy -except (ImportError, RuntimeError): - # RuntimeError on Python 3.9 (macOS): Polyfit sanity test emitted a warning, ... - numpy = None -try: - from PIL import Image -except ImportError: - Image = None +def test_numpy(): + numpy = pytest.importorskip("numpy") - -@pytest.mark.skipif(numpy is None, reason="Numpy module not available.") -def test_numpy(pixel_ratio): box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss(display=os.getenv("DISPLAY")) as sct: img = numpy.array(sct.grab(box)) - assert len(img) == 10 * pixel_ratio + assert len(img) == 10 -@pytest.mark.skipif(Image is None, reason="PIL module not available.") def test_pil(): + Image = pytest.importorskip("PIL.Image") + width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: @@ -48,8 +39,9 @@ def test_pil(): assert os.path.isfile("box.png") -@pytest.mark.skipif(Image is None, reason="PIL module not available.") def test_pil_bgra(): + Image = pytest.importorskip("PIL.Image") + width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: @@ -66,8 +58,9 @@ def test_pil_bgra(): assert os.path.isfile("box-bgra.png") -@pytest.mark.skipif(Image is None, reason="PIL module not available.") def test_pil_not_16_rounded(): + Image = pytest.importorskip("PIL.Image") + width, height = 10, 10 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: