Skip to content

Commit

Permalink
Mac: disable auto-scaling
Browse files Browse the repository at this point in the history
  • Loading branch information
BoboTiG committed Apr 30, 2023
1 parent 0e5808d commit 672ee76
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 115 deletions.
90 changes: 45 additions & 45 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand All @@ -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
Expand Down
70 changes: 44 additions & 26 deletions src/mss/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""

Expand Down Expand Up @@ -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):
"""
Expand All @@ -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."""
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -169,24 +183,28 @@ 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)
raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len))
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:
Expand Down
18 changes: 0 additions & 18 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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]
6 changes: 3 additions & 3 deletions src/tests/test_get_pixels.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ 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}
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):
Expand Down
23 changes: 16 additions & 7 deletions src/tests/test_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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)
25 changes: 9 additions & 16 deletions src/tests/test_third_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down

0 comments on commit 672ee76

Please sign in to comment.