Skip to content

Commit e5a7294

Browse files
karanlyonsBoboTiG
authored andcommitted
Mac: Properly support all display scaling and resolutions (fix BoboTiG#23)
Also: - Generate namedtuple classes only once. Improves performance whilst also allowing the classes to be instantiated elsewhere.
1 parent 96b18f6 commit e5a7294

File tree

7 files changed

+60
-48
lines changed

7 files changed

+60
-48
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dev 2017/xx/xx
88
- tests: a lot of tests added for better coverage
99
- MSS: possibility to use custom class to handle screen shot data
1010
- Mac: handle screenshot's width not divisible by 16 (fix #14, #19, #21)
11+
- Mac: properly support all display scaling and resolutions (fix #23)
1112
- Mac: fix memory leak (fix #24)
1213
- Linux: handle bad display value
1314
- Windows: Take into account zoom factor for high-DPI displays (fix #20)

CONTRIBUTORS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ David Becker [https://davide.me] and redodo [https://github.com/redodo]
2121
Jochen 'cycomanic' Schroeder [https://github.com/cycomanic]
2222
- GNU/Linux: use errcheck instead of deprecated restype with callable, for enum_display_monitors()
2323

24+
Karan Lyons <karan@karanlyons.com> [https://karanlyons.com] [https://github.com/karanlyons]
25+
- MacOS: Proper support for display scaling
26+
2427
Oros <oros@ecirtam.net> [https://ecirtam.net]
2528
- GNU/Linux tester
2629

mss/darwin.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
Source: https://github.com/BoboTiG/python-mss
55
"""
66

7-
# pylint: disable=import-error
7+
# pylint: disable=import-error, too-many-locals
88

99
from __future__ import division
1010

1111
import ctypes
1212
import ctypes.util
13-
import math
1413
import sys
1514

1615
from .base import MSSBase
1716
from .exception import ScreenShotError
17+
from .screenshot import Size
1818

19-
__all__ = ['MSS']
19+
__all__ = ('MSS',)
2020

2121

2222
def cgfloat():
@@ -94,10 +94,13 @@ def _set_argtypes(self):
9494
ctypes.c_uint32,
9595
ctypes.c_uint32]
9696
self.core.CGImageGetWidth.argtypes = [ctypes.c_void_p]
97+
self.core.CGImageGetHeight.argtypes = [ctypes.c_void_p]
9798
self.core.CGImageGetDataProvider.argtypes = [ctypes.c_void_p]
9899
self.core.CGDataProviderCopyData.argtypes = [ctypes.c_void_p]
99100
self.core.CFDataGetBytePtr.argtypes = [ctypes.c_void_p]
100101
self.core.CFDataGetLength.argtypes = [ctypes.c_void_p]
102+
self.core.CGImageGetBytesPerRow.argtypes = [ctypes.c_void_p]
103+
self.core.CGImageGetBitsPerPixel.argtypes = [ctypes.c_void_p]
101104
self.core.CGDataProviderRelease.argtypes = [ctypes.c_void_p]
102105
self.core.CFRelease.argtypes = [ctypes.c_void_p]
103106

@@ -111,11 +114,14 @@ def _set_restypes(self):
111114
self.core.CGRectUnion.restype = CGRect
112115
self.core.CGDisplayRotation.restype = ctypes.c_float
113116
self.core.CGWindowListCreateImage.restype = ctypes.c_void_p
114-
self.core.CGImageGetWidth.restype = ctypes.c_uint32
117+
self.core.CGImageGetWidth.restype = ctypes.c_size_t
118+
self.core.CGImageGetHeight.restype = ctypes.c_size_t
115119
self.core.CGImageGetDataProvider.restype = ctypes.c_void_p
116120
self.core.CGDataProviderCopyData.restype = ctypes.c_void_p
117121
self.core.CFDataGetBytePtr.restype = ctypes.c_void_p
118122
self.core.CFDataGetLength.restype = ctypes.c_uint64
123+
self.core.CGImageGetBytesPerRow.restype = ctypes.c_size_t
124+
self.core.CGImageGetBitsPerPixel.restype = ctypes.c_size_t
119125
self.core.CGDataProviderRelease.restype = ctypes.c_void_p
120126
self.core.CFRelease.restype = ctypes.c_void_p
121127

@@ -181,45 +187,42 @@ def grab(self, monitor):
181187
'height': monitor[3] - monitor[1],
182188
}
183189

184-
# When the monitor width is not divisible by 16, extra padding
185-
# is added by macOS in the form of black pixels, which results
186-
# in a screenshot with shifted pixels. To counter this, we
187-
# round the width to the nearest integer divisible by 16, and
188-
# we remove the extra width from the image after taking the
189-
# screenshot.
190-
rounded_width = math.ceil(monitor['width'] / 16) * 16
191-
192190
rect = CGRect((monitor['left'], monitor['top']),
193-
(rounded_width, monitor['height']))
191+
(monitor['width'], monitor['height']))
194192

195193
image_ref = self.core.CGWindowListCreateImage(rect, 1, 0, 0)
196194
if not image_ref:
197195
raise ScreenShotError(
198196
'CoreGraphics.CGWindowListCreateImage() failed.', locals())
199197

200198
width = int(self.core.CGImageGetWidth(image_ref))
201-
prov = self.core.CGImageGetDataProvider(image_ref)
202-
copy_data = self.core.CGDataProviderCopyData(prov)
203-
data_ref = self.core.CFDataGetBytePtr(copy_data)
204-
buf_len = self.core.CFDataGetLength(copy_data)
205-
raw = ctypes.cast(data_ref, ctypes.POINTER(ctypes.c_ubyte * buf_len))
206-
data = bytearray(raw.contents)
207-
self.core.CGDataProviderRelease(prov)
208-
self.core.CFRelease(copy_data)
209-
210-
if rounded_width != monitor['width']:
211-
data = self._crop_width(data, monitor, width)
212-
213-
return self.cls_image(data, monitor)
214-
215-
@staticmethod
216-
def _crop_width(image, monitor, width_to):
217-
# type: (bytearray, Dict[str, int], int) -> bytearray
218-
""" Cut off the pixels from an image buffer at a particular width. """
219-
220-
cropped = bytearray()
221-
for row in range(monitor['height']):
222-
start = row * width_to * 4
223-
end = start + monitor['width'] * 4
224-
cropped.extend(image[start:end])
225-
return cropped
199+
height = int(self.core.CGImageGetHeight(image_ref))
200+
prov = copy_data = None
201+
try:
202+
prov = self.core.CGImageGetDataProvider(image_ref)
203+
copy_data = self.core.CGDataProviderCopyData(prov)
204+
data_ref = self.core.CFDataGetBytePtr(copy_data)
205+
buf_len = self.core.CFDataGetLength(copy_data)
206+
raw = ctypes.cast(
207+
data_ref, ctypes.POINTER(ctypes.c_ubyte * buf_len))
208+
data = bytearray(raw.contents)
209+
210+
# Remove padding per row
211+
bytes_per_row = int(self.core.CGImageGetBytesPerRow(image_ref))
212+
bytes_per_pixel = int(self.core.CGImageGetBitsPerPixel(image_ref))
213+
bytes_per_pixel = (bytes_per_pixel + 7) // 8
214+
215+
if bytes_per_pixel * width != bytes_per_row:
216+
cropped = bytearray()
217+
for row in range(height):
218+
start = row * bytes_per_row
219+
end = start + width * bytes_per_pixel
220+
cropped.extend(data[start:end])
221+
data = cropped
222+
finally:
223+
if prov:
224+
self.core.CGDataProviderRelease(prov)
225+
if copy_data:
226+
self.core.CFRelease(copy_data)
227+
228+
return self.cls_image(data, monitor, size=Size(width, height))

mss/linux.py

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

14-
__all__ = ['MSS']
14+
__all__ = ('MSS',)
1515

1616

1717
class Display(ctypes.Structure):

mss/screenshot.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
from .exception import ScreenShotError
1010

1111

12+
Pos = collections.namedtuple('Pos', 'left, top')
13+
Size = collections.namedtuple('Size', 'width, height')
14+
15+
1216
class ScreenShot(object):
1317
"""
1418
Screen shot object.
@@ -22,19 +26,20 @@ class ScreenShot(object):
2226
__pixels = None # type: List[Tuple[int, int, int]]
2327
__rgb = None # type: bytes
2428

25-
def __init__(self, data, monitor):
26-
# type: (bytearray, Dict[str, int]) -> None
29+
def __init__(self, data, monitor, size=None):
30+
# type: (bytearray, Dict[str, int], Any) -> None
2731
#: Bytearray of the raw BGRA pixels retrieved by ctype
2832
#: OS independent implementations.
2933
self.raw = bytearray(data) # type: bytearray
3034

3135
#: NamedTuple of the screen shot coordinates.
32-
self.pos = collections.namedtuple('pos', 'left, top')(
33-
monitor['left'], monitor['top']) # type: Any
36+
self.pos = Pos(monitor['left'], monitor['top']) # type: Pos
3437

35-
#: NamedTuple of the screen shot size.
36-
self.size = collections.namedtuple('size', 'width, height')(
37-
monitor['width'], monitor['height']) # type: Any
38+
if size is not None:
39+
#: NamedTuple of the screen shot size.
40+
self.size = size # type: Size
41+
else:
42+
self.size = Size(monitor['width'], monitor['height']) # type: Size
3843

3944
def __repr__(self):
4045
# type: () -> str

mss/windows.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .base import MSSBase
1313
from .exception import ScreenShotError
1414

15-
__all__ = ['MSS']
15+
__all__ = ('MSS',)
1616

1717

1818
class BITMAPINFOHEADER(ctypes.Structure):

tests/test_cls_image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
class SimpleScreenShot(object):
55

6-
def __init__(self, data, monitor):
6+
def __init__(self, data, monitor, **kwargs):
77
self.raw = bytes(data)
88
self.monitor = monitor
99

0 commit comments

Comments
 (0)