This commit is contained in:
2024-11-29 18:15:30 +00:00
parent 40aade2d8e
commit bc9415586e
5298 changed files with 1938676 additions and 80 deletions

View File

@ -0,0 +1,27 @@
"""An ultra fast cross-platform multiple screenshots module in pure python
using ctypes.
This module is maintained by Mickaël Schoentgen <contact@tiger-222.fr>.
You can always get the latest version of this module at:
https://github.com/BoboTiG/python-mss
If that URL should fail, try contacting the author.
"""
from mss.exception import ScreenShotError
from mss.factory import mss
__version__ = "10.0.0"
__author__ = "Mickaël Schoentgen"
__date__ = "2013-2024"
__copyright__ = f"""
Copyright (c) {__date__}, {__author__}
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee or royalty is hereby
granted, provided that the above copyright notice appear in all copies
and that both that copyright notice and this permission notice appear
in supporting documentation or portions thereof, including
modifications, that you make.
"""
__all__ = ("ScreenShotError", "mss")

View File

@ -0,0 +1,83 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
import os.path
import sys
from argparse import ArgumentParser
from mss import __version__
from mss.exception import ScreenShotError
from mss.factory import mss
from mss.tools import to_png
def main(*args: str) -> int:
"""Main logic."""
cli_args = ArgumentParser(prog="mss")
cli_args.add_argument(
"-c",
"--coordinates",
default="",
type=str,
help="the part of the screen to capture: top, left, width, height",
)
cli_args.add_argument(
"-l",
"--level",
default=6,
type=int,
choices=list(range(10)),
help="the PNG compression level",
)
cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screenshot")
cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name")
cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor")
cli_args.add_argument(
"-q",
"--quiet",
default=False,
action="store_true",
help="do not print created files",
)
cli_args.add_argument("-v", "--version", action="version", version=__version__)
options = cli_args.parse_args(args or None)
kwargs = {"mon": options.monitor, "output": options.output}
if options.coordinates:
try:
top, left, width, height = options.coordinates.split(",")
except ValueError:
print("Coordinates syntax: top, left, width, height")
return 2
kwargs["mon"] = {
"top": int(top),
"left": int(left),
"width": int(width),
"height": int(height),
}
if options.output == "monitor-{mon}.png":
kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png"
try:
with mss(with_cursor=options.with_cursor) as sct:
if options.coordinates:
output = kwargs["output"].format(**kwargs["mon"])
sct_img = sct.grab(kwargs["mon"])
to_png(sct_img.rgb, sct_img.size, level=options.level, output=output)
if not options.quiet:
print(os.path.realpath(output))
else:
for file_name in sct.save(**kwargs):
if not options.quiet:
print(os.path.realpath(file_name))
return 0
except ScreenShotError:
if options.quiet:
return 1
raise
if __name__ == "__main__": # pragma: nocover
sys.exit(main())

View File

@ -0,0 +1,261 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
from abc import ABCMeta, abstractmethod
from datetime import datetime
from threading import Lock
from typing import TYPE_CHECKING, Any
from mss.exception import ScreenShotError
from mss.screenshot import ScreenShot
from mss.tools import to_png
if TYPE_CHECKING: # pragma: nocover
from collections.abc import Callable, Iterator
from mss.models import Monitor, Monitors
try:
from datetime import UTC
except ImportError: # pragma: nocover
# Python < 3.11
from datetime import timezone
UTC = timezone.utc
lock = Lock()
OPAQUE = 255
class MSSBase(metaclass=ABCMeta):
"""This class will be overloaded by a system specific one."""
__slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"}
def __init__(
self,
/,
*,
compression_level: int = 6,
with_cursor: bool = False,
# Linux only
display: bytes | str | None = None, # noqa: ARG002
# Mac only
max_displays: int = 32, # noqa: ARG002
) -> None:
self.cls_image: type[ScreenShot] = ScreenShot
self.compression_level = compression_level
self.with_cursor = with_cursor
self._monitors: Monitors = []
def __enter__(self) -> MSSBase: # noqa:PYI034
"""For the cool call `with MSS() as mss:`."""
return self
def __exit__(self, *_: object) -> None:
"""For the cool call `with MSS() as mss:`."""
self.close()
@abstractmethod
def _cursor_impl(self) -> ScreenShot | None:
"""Retrieve all cursor data. Pixels have to be RGB."""
@abstractmethod
def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
"""Retrieve all pixels from a monitor. Pixels have to be RGB.
That method has to be run using a threading lock.
"""
@abstractmethod
def _monitors_impl(self) -> None:
"""Get positions of monitors (has to be run using a threading lock).
It must populate self._monitors.
"""
def close(self) -> None: # noqa:B027
"""Clean-up."""
def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot:
"""Retrieve screen pixels for a given monitor.
Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts.
:param monitor: The coordinates and size of the box to capture.
See :meth:`monitors <monitors>` for object details.
:return :class:`ScreenShot <ScreenShot>`.
"""
# Convert PIL bbox style
if isinstance(monitor, tuple):
monitor = {
"left": monitor[0],
"top": monitor[1],
"width": monitor[2] - monitor[0],
"height": monitor[3] - monitor[1],
}
with lock:
screenshot = self._grab_impl(monitor)
if self.with_cursor and (cursor := self._cursor_impl()):
return self._merge(screenshot, cursor)
return screenshot
@property
def monitors(self) -> Monitors:
"""Get positions of all monitors.
If the monitor has rotation, you have to deal with it
inside this method.
This method has to fill self._monitors with all information
and use it as a cache:
self._monitors[0] is a dict of all monitors together
self._monitors[N] is a dict of the monitor N (with N > 0)
Each monitor is a dict with:
{
'left': the x-coordinate of the upper-left corner,
'top': the y-coordinate of the upper-left corner,
'width': the width,
'height': the height
}
"""
if not self._monitors:
with lock:
self._monitors_impl()
return self._monitors
def save(
self,
/,
*,
mon: int = 0,
output: str = "monitor-{mon}.png",
callback: Callable[[str], None] | None = None,
) -> Iterator[str]:
"""Grab a screenshot and save it to a file.
:param int mon: The monitor to screenshot (default=0).
-1: grab one screenshot of all monitors
0: grab one screenshot by monitor
N: grab the screenshot of the monitor N
:param str output: The output filename.
It can take several keywords to customize the filename:
- `{mon}`: the monitor number
- `{top}`: the screenshot y-coordinate of the upper-left corner
- `{left}`: the screenshot x-coordinate of the upper-left corner
- `{width}`: the screenshot's width
- `{height}`: the screenshot's height
- `{date}`: the current date using the default formatter
As it is using the `format()` function, you can specify
formatting options like `{date:%Y-%m-%s}`.
:param callable callback: Callback called before saving the
screenshot to a file. Take the `output` argument as parameter.
:return generator: Created file(s).
"""
monitors = self.monitors
if not monitors:
msg = "No monitor found."
raise ScreenShotError(msg)
if mon == 0:
# One screenshot by monitor
for idx, monitor in enumerate(monitors[1:], 1):
fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor)
if callable(callback):
callback(fname)
sct = self.grab(monitor)
to_png(sct.rgb, sct.size, level=self.compression_level, output=fname)
yield fname
else:
# A screenshot of all monitors together or
# a screenshot of the monitor N.
mon = 0 if mon == -1 else mon
try:
monitor = monitors[mon]
except IndexError as exc:
msg = f"Monitor {mon!r} does not exist."
raise ScreenShotError(msg) from exc
output = output.format(mon=mon, date=datetime.now(UTC) if "{date" in output else None, **monitor)
if callable(callback):
callback(output)
sct = self.grab(monitor)
to_png(sct.rgb, sct.size, level=self.compression_level, output=output)
yield output
def shot(self, /, **kwargs: Any) -> str:
"""Helper to save the screenshot of the 1st monitor, by default.
You can pass the same arguments as for ``save``.
"""
kwargs["mon"] = kwargs.get("mon", 1)
return next(self.save(**kwargs))
@staticmethod
def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot:
"""Create composite image by blending screenshot and mouse cursor."""
(cx, cy), (cw, ch) = cursor.pos, cursor.size
(x, y), (w, h) = screenshot.pos, screenshot.size
cx2, cy2 = cx + cw, cy + ch
x2, y2 = x + w, y + h
overlap = cx < x2 and cx2 > x and cy < y2 and cy2 > y
if not overlap:
return screenshot
screen_raw = screenshot.raw
cursor_raw = cursor.raw
cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4
cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4
start_count_y = -cy if cy < 0 else 0
start_count_x = -cx if cx < 0 else 0
stop_count_y = ch * 4 - max(cy2, 0)
stop_count_x = cw * 4 - max(cx2, 0)
rgb = range(3)
for count_y in range(start_count_y, stop_count_y, 4):
pos_s = (count_y + cy) * w + cx
pos_c = count_y * cw
for count_x in range(start_count_x, stop_count_x, 4):
spos = pos_s + count_x
cpos = pos_c + count_x
alpha = cursor_raw[cpos + 3]
if not alpha:
continue
if alpha == OPAQUE:
screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3]
else:
alpha2 = alpha / 255
for i in rgb:
screen_raw[spos + i] = int(cursor_raw[cpos + i] * alpha2 + screen_raw[spos + i] * (1 - alpha2))
return screenshot
@staticmethod
def _cfactory(
attr: Any,
func: str,
argtypes: list[Any],
restype: Any,
/,
errcheck: Callable | None = None,
) -> None:
"""Factory to create a ctypes function and automatically manage errors."""
meth = getattr(attr, func)
meth.argtypes = argtypes
meth.restype = restype
if errcheck:
meth.errcheck = errcheck

View File

@ -0,0 +1,211 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
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 platform import mac_ver
from typing import TYPE_CHECKING, Any
from mss.base import MSSBase
from mss.exception import ScreenShotError
from mss.screenshot import ScreenShot, Size
if TYPE_CHECKING: # pragma: nocover
from mss.models import CFunctions, Monitor
__all__ = ("MSS",)
MAC_VERSION_CATALINA = 10.16
def cgfloat() -> type[c_double | c_float]:
"""Get the appropriate value for a float."""
return c_double if sys.maxsize > 2**32 else c_float
class CGPoint(Structure):
"""Structure that contains coordinates of a rectangle."""
_fields_ = (("x", cgfloat()), ("y", cgfloat()))
def __repr__(self) -> str:
return f"{type(self).__name__}(left={self.x} top={self.y})"
class CGSize(Structure):
"""Structure that contains dimensions of an rectangle."""
_fields_ = (("width", cgfloat()), ("height", cgfloat()))
def __repr__(self) -> str:
return f"{type(self).__name__}(width={self.width} height={self.height})"
class CGRect(Structure):
"""Structure that contains information about a rectangle."""
_fields_ = (("origin", CGPoint), ("size", CGSize))
def __repr__(self) -> str:
return f"{type(self).__name__}<{self.origin} {self.size}>"
# C functions that will be initialised later.
#
# Available attr: core.
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
# Syntax: cfunction: (attr, argtypes, restype)
"CGDataProviderCopyData": ("core", [c_void_p], c_void_p),
"CGDisplayBounds": ("core", [c_uint32], CGRect),
"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),
"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),
"CGRectStandardize": ("core", [CGRect], CGRect),
"CGRectUnion": ("core", [CGRect, CGRect], CGRect),
"CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p),
}
class MSS(MSSBase):
"""Multiple ScreenShots implementation for macOS.
It uses intensively the CoreGraphics library.
"""
__slots__ = {"core", "max_displays"}
def __init__(self, /, **kwargs: Any) -> None:
"""MacOS initialisations."""
super().__init__(**kwargs)
self.max_displays = kwargs.get("max_displays", 32)
self._init_library()
self._set_cfunctions()
def _init_library(self) -> None:
"""Load the CoreGraphics library."""
version = float(".".join(mac_ver()[0].split(".")[:2]))
if version < MAC_VERSION_CATALINA:
coregraphics = ctypes.util.find_library("CoreGraphics")
else:
# macOS Big Sur and newer
coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics"
if not coregraphics:
msg = "No CoreGraphics library found."
raise ScreenShotError(msg)
self.core = ctypes.cdll.LoadLibrary(coregraphics)
def _set_cfunctions(self) -> None:
"""Set all ctypes functions and attach them to attributes."""
cfactory = self._cfactory
attrs = {"core": self.core}
for func, (attr, argtypes, restype) in CFUNCTIONS.items():
cfactory(attrs[attr], func, argtypes, restype)
def _monitors_impl(self) -> None:
"""Get positions of monitors. It will populate self._monitors."""
int_ = int
core = self.core
# All monitors
# We need to update the value with every single monitor found
# using CGRectUnion. Else we will end with infinite values.
all_monitors = CGRect()
self._monitors.append({})
# Each monitor
display_count = c_uint32(0)
active_displays = (c_uint32 * self.max_displays)()
core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count))
for idx in range(display_count.value):
display = active_displays[idx]
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}:
width, height = height, width
self._monitors.append(
{
"left": int_(rect.origin.x),
"top": int_(rect.origin.y),
"width": int_(width),
"height": int_(height),
},
)
# Update AiO monitor's values
all_monitors = core.CGRectUnion(all_monitors, rect)
# Set the AiO monitor's values
self._monitors[0] = {
"left": int_(all_monitors.origin.x),
"top": int_(all_monitors.origin.y),
"width": int_(all_monitors.size.width),
"height": int_(all_monitors.size.height),
}
def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
"""Retrieve all pixels from a monitor. Pixels have to be RGB."""
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:
msg = "CoreGraphics.CGWindowListCreateImage() failed."
raise ScreenShotError(msg)
width = core.CGImageGetWidth(image_ref)
height = core.CGImageGetHeight(image_ref)
prov = copy_data = None
try:
prov = core.CGImageGetDataProvider(image_ref)
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_pixel = (bytes_per_pixel + 7) // 8
if bytes_per_pixel * width != bytes_per_row:
cropped = bytearray()
for row in range(height):
start = row * bytes_per_row
end = start + width * bytes_per_pixel
cropped.extend(data[start:end])
data = cropped
finally:
if prov:
core.CGDataProviderRelease(prov)
if copy_data:
core.CFRelease(copy_data)
return self.cls_image(data, monitor, size=Size(width, height))
def _cursor_impl(self) -> ScreenShot | None:
"""Retrieve all cursor data. Pixels have to be RGB."""
return None

View File

@ -0,0 +1,15 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
from typing import Any
class ScreenShotError(Exception):
"""Error handling class."""
def __init__(self, message: str, /, *, details: dict[str, Any] | None = None) -> None:
super().__init__(message)
self.details = details or {}

View File

@ -0,0 +1,40 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
import platform
from typing import Any
from mss.base import MSSBase
from mss.exception import ScreenShotError
def mss(**kwargs: Any) -> MSSBase:
"""Factory returning a proper MSS class instance.
It detects the platform we are running on
and chooses the most adapted mss_class to take
screenshots.
It then proxies its arguments to the class for
instantiation.
"""
os_ = platform.system().lower()
if os_ == "darwin":
from mss import darwin
return darwin.MSS(**kwargs)
if os_ == "linux":
from mss import linux
return linux.MSS(**kwargs)
if os_ == "windows":
from mss import windows
return windows.MSS(**kwargs)
msg = f"System {os_!r} not (yet?) implemented."
raise ScreenShotError(msg)

View File

@ -0,0 +1,481 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
import os
from contextlib import suppress
from ctypes import (
CFUNCTYPE,
POINTER,
Structure,
byref,
c_char_p,
c_int,
c_int32,
c_long,
c_short,
c_ubyte,
c_uint,
c_uint32,
c_ulong,
c_ushort,
c_void_p,
cast,
cdll,
create_string_buffer,
)
from ctypes.util import find_library
from threading import current_thread, local
from typing import TYPE_CHECKING, Any
from mss.base import MSSBase, lock
from mss.exception import ScreenShotError
if TYPE_CHECKING: # pragma: nocover
from mss.models import CFunctions, Monitor
from mss.screenshot import ScreenShot
__all__ = ("MSS",)
PLAINMASK = 0x00FFFFFF
ZPIXMAP = 2
BITS_PER_PIXELS_32 = 32
SUPPORTED_BITS_PER_PIXELS = {
BITS_PER_PIXELS_32,
}
class Display(Structure):
"""Structure that serves as the connection to the X server
and that contains all the information about that X server.
https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831.
"""
class XErrorEvent(Structure):
"""XErrorEvent to debug eventual errors.
https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html.
"""
_fields_ = (
("type", c_int),
("display", POINTER(Display)), # Display the event was read from
("serial", c_ulong), # serial number of failed request
("error_code", c_ubyte), # error code of failed request
("request_code", c_ubyte), # major op-code of failed request
("minor_code", c_ubyte), # minor op-code of failed request
("resourceid", c_void_p), # resource ID
)
class XFixesCursorImage(Structure):
"""Cursor structure.
/usr/include/X11/extensions/Xfixes.h
https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96.
"""
_fields_ = (
("x", c_short),
("y", c_short),
("width", c_ushort),
("height", c_ushort),
("xhot", c_ushort),
("yhot", c_ushort),
("cursor_serial", c_ulong),
("pixels", POINTER(c_ulong)),
("atom", c_ulong),
("name", c_char_p),
)
class XImage(Structure):
"""Description of an image as it exists in the client's memory.
https://tronche.com/gui/x/xlib/graphics/images.html.
"""
_fields_ = (
("width", c_int), # size of image
("height", c_int), # size of image
("xoffset", c_int), # number of pixels offset in X direction
("format", c_int), # XYBitmap, XYPixmap, ZPixmap
("data", c_void_p), # pointer to image data
("byte_order", c_int), # data byte order, LSBFirst, MSBFirst
("bitmap_unit", c_int), # quant. of scanline 8, 16, 32
("bitmap_bit_order", c_int), # LSBFirst, MSBFirst
("bitmap_pad", c_int), # 8, 16, 32 either XY or ZPixmap
("depth", c_int), # depth of image
("bytes_per_line", c_int), # accelerator to next line
("bits_per_pixel", c_int), # bits per pixel (ZPixmap)
("red_mask", c_ulong), # bits in z arrangement
("green_mask", c_ulong), # bits in z arrangement
("blue_mask", c_ulong), # bits in z arrangement
)
class XRRCrtcInfo(Structure):
"""Structure that contains CRTC information.
https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360.
"""
_fields_ = (
("timestamp", c_ulong),
("x", c_int),
("y", c_int),
("width", c_uint),
("height", c_uint),
("mode", c_long),
("rotation", c_int),
("noutput", c_int),
("outputs", POINTER(c_long)),
("rotations", c_ushort),
("npossible", c_int),
("possible", POINTER(c_long)),
)
class XRRModeInfo(Structure):
"""https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248."""
class XRRScreenResources(Structure):
"""Structure that contains arrays of XIDs that point to the
available outputs and associated CRTCs.
https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265.
"""
_fields_ = (
("timestamp", c_ulong),
("configTimestamp", c_ulong),
("ncrtc", c_int),
("crtcs", POINTER(c_long)),
("noutput", c_int),
("outputs", POINTER(c_long)),
("nmode", c_int),
("modes", POINTER(XRRModeInfo)),
)
class XWindowAttributes(Structure):
"""Attributes for the specified window."""
_fields_ = (
("x", c_int32), # location of window
("y", c_int32), # location of window
("width", c_int32), # width of window
("height", c_int32), # height of window
("border_width", c_int32), # border width of window
("depth", c_int32), # depth of window
("visual", c_ulong), # the associated visual structure
("root", c_ulong), # root of screen containing window
("class", c_int32), # InputOutput, InputOnly
("bit_gravity", c_int32), # one of bit gravity values
("win_gravity", c_int32), # one of the window gravity values
("backing_store", c_int32), # NotUseful, WhenMapped, Always
("backing_planes", c_ulong), # planes to be preserved if possible
("backing_pixel", c_ulong), # value to be used when restoring planes
("save_under", c_int32), # boolean, should bits under be saved?
("colormap", c_ulong), # color map to be associated with window
("mapinstalled", c_uint32), # boolean, is color map currently installed
("map_state", c_uint32), # IsUnmapped, IsUnviewable, IsViewable
("all_event_masks", c_ulong), # set of events all people have interest in
("your_event_mask", c_ulong), # my event mask
("do_not_propagate_mask", c_ulong), # set of events that should not propagate
("override_redirect", c_int32), # boolean value for override-redirect
("screen", c_ulong), # back pointer to correct screen
)
_ERROR = {}
_X11 = find_library("X11")
_XFIXES = find_library("Xfixes")
_XRANDR = find_library("Xrandr")
@CFUNCTYPE(c_int, POINTER(Display), POINTER(XErrorEvent))
def _error_handler(display: Display, event: XErrorEvent) -> int:
"""Specifies the program's supplied error handler."""
# Get the specific error message
xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type]
get_error = xlib.XGetErrorText
get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int]
get_error.restype = c_void_p
evt = event.contents
error = create_string_buffer(1024)
get_error(display, evt.error_code, error, len(error))
_ERROR[current_thread()] = {
"error": error.value.decode("utf-8"),
"error_code": evt.error_code,
"minor_code": evt.minor_code,
"request_code": evt.request_code,
"serial": evt.serial,
"type": evt.type,
}
return 0
def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, Any]:
"""Validate the returned value of a C function call."""
thread = current_thread()
if retval != 0 and thread not in _ERROR:
return args
details = _ERROR.pop(thread, {})
msg = f"{func.__name__}() failed"
raise ScreenShotError(msg, details=details)
# C functions that will be initialised later.
# See https://tronche.com/gui/x/xlib/function-index.html for details.
#
# Available attr: xfixes, xlib, xrandr.
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
# Syntax: cfunction: (attr, argtypes, restype)
"XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p),
"XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)),
"XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p),
"XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)),
"XGetImage": (
"xlib",
[POINTER(Display), POINTER(Display), c_int, c_int, c_uint, c_uint, c_ulong, c_int],
POINTER(XImage),
),
"XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int),
"XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)),
"XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint),
"XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p),
"XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p),
"XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)),
"XRRGetScreenResources": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)),
"XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)),
"XSetErrorHandler": ("xlib", [c_void_p], c_void_p),
}
class MSS(MSSBase):
"""Multiple ScreenShots implementation for GNU/Linux.
It uses intensively the Xlib and its Xrandr extension.
"""
__slots__ = {"xfixes", "xlib", "xrandr", "_handles"}
def __init__(self, /, **kwargs: Any) -> None:
"""GNU/Linux initialisations."""
super().__init__(**kwargs)
# Available thread-specific variables
self._handles = local()
self._handles.display = None
self._handles.drawable = None
self._handles.original_error_handler = None
self._handles.root = None
display = kwargs.get("display", b"")
if not display:
try:
display = os.environ["DISPLAY"].encode("utf-8")
except KeyError:
msg = "$DISPLAY not set."
raise ScreenShotError(msg) from None
if not isinstance(display, bytes):
display = display.encode("utf-8")
if b":" not in display:
msg = f"Bad display value: {display!r}."
raise ScreenShotError(msg)
if not _X11:
msg = "No X11 library found."
raise ScreenShotError(msg)
self.xlib = cdll.LoadLibrary(_X11)
if not _XRANDR:
msg = "No Xrandr extension found."
raise ScreenShotError(msg)
self.xrandr = cdll.LoadLibrary(_XRANDR)
if self.with_cursor:
if _XFIXES:
self.xfixes = cdll.LoadLibrary(_XFIXES)
else:
self.with_cursor = False
self._set_cfunctions()
# Install the error handler to prevent interpreter crashes: any error will raise a ScreenShotError exception
self._handles.original_error_handler = self.xlib.XSetErrorHandler(_error_handler)
self._handles.display = self.xlib.XOpenDisplay(display)
if not self._handles.display:
msg = f"Unable to open display: {display!r}."
raise ScreenShotError(msg)
if not self._is_extension_enabled("RANDR"):
msg = "Xrandr not enabled."
raise ScreenShotError(msg)
self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display)
# Fix for XRRGetScreenResources and XGetImage:
# expected LP_Display instance instead of LP_XWindowAttributes
self._handles.drawable = cast(self._handles.root, POINTER(Display))
def close(self) -> None:
# Clean-up
if self._handles.display:
with lock:
self.xlib.XCloseDisplay(self._handles.display)
self._handles.display = None
self._handles.drawable = None
self._handles.root = None
# Remove our error handler
if self._handles.original_error_handler:
# It's required when exiting MSS to prevent letting `_error_handler()` as default handler.
# Doing so would crash when using Tk/Tkinter, see issue #220.
# Interesting technical stuff can be found here:
# https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50
# https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c
self.xlib.XSetErrorHandler(self._handles.original_error_handler)
self._handles.original_error_handler = None
# Also empty the error dict
_ERROR.clear()
def _is_extension_enabled(self, name: str, /) -> bool:
"""Return True if the given *extension* is enabled on the server."""
major_opcode_return = c_int()
first_event_return = c_int()
first_error_return = c_int()
try:
with lock:
self.xlib.XQueryExtension(
self._handles.display,
name.encode("latin1"),
byref(major_opcode_return),
byref(first_event_return),
byref(first_error_return),
)
except ScreenShotError:
return False
return True
def _set_cfunctions(self) -> None:
"""Set all ctypes functions and attach them to attributes."""
cfactory = self._cfactory
attrs = {
"xfixes": getattr(self, "xfixes", None),
"xlib": self.xlib,
"xrandr": self.xrandr,
}
for func, (attr, argtypes, restype) in CFUNCTIONS.items():
with suppress(AttributeError):
errcheck = None if func == "XSetErrorHandler" else _validate
cfactory(attrs[attr], func, argtypes, restype, errcheck=errcheck)
def _monitors_impl(self) -> None:
"""Get positions of monitors. It will populate self._monitors."""
display = self._handles.display
int_ = int
xrandr = self.xrandr
# All monitors
gwa = XWindowAttributes()
self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa))
self._monitors.append(
{"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)},
)
# Each monitor
# A simple benchmark calling 10 times those 2 functions:
# XRRGetScreenResources(): 0.1755971429956844 s
# XRRGetScreenResourcesCurrent(): 0.0039125580078689 s
# The second is faster by a factor of 44! So try to use it first.
try:
mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents
except AttributeError:
mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents
crtcs = mon.crtcs
for idx in range(mon.ncrtc):
crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents
if crtc.noutput == 0:
xrandr.XRRFreeCrtcInfo(crtc)
continue
self._monitors.append(
{
"left": int_(crtc.x),
"top": int_(crtc.y),
"width": int_(crtc.width),
"height": int_(crtc.height),
},
)
xrandr.XRRFreeCrtcInfo(crtc)
xrandr.XRRFreeScreenResources(mon)
def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
"""Retrieve all pixels from a monitor. Pixels have to be RGB."""
ximage = self.xlib.XGetImage(
self._handles.display,
self._handles.drawable,
monitor["left"],
monitor["top"],
monitor["width"],
monitor["height"],
PLAINMASK,
ZPIXMAP,
)
try:
bits_per_pixel = ximage.contents.bits_per_pixel
if bits_per_pixel not in SUPPORTED_BITS_PER_PIXELS:
msg = f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}."
raise ScreenShotError(msg)
raw_data = cast(
ximage.contents.data,
POINTER(c_ubyte * monitor["height"] * monitor["width"] * 4),
)
data = bytearray(raw_data.contents)
finally:
# Free
self.xlib.XDestroyImage(ximage)
return self.cls_image(data, monitor)
def _cursor_impl(self) -> ScreenShot:
"""Retrieve all cursor data. Pixels have to be RGB."""
# Read data of cursor/mouse-pointer
ximage = self.xfixes.XFixesGetCursorImage(self._handles.display)
if not (ximage and ximage.contents):
msg = "Cannot read XFixesGetCursorImage()"
raise ScreenShotError(msg)
cursor_img: XFixesCursorImage = ximage.contents
region = {
"left": cursor_img.x - cursor_img.xhot,
"top": cursor_img.y - cursor_img.yhot,
"width": cursor_img.width,
"height": cursor_img.height,
}
raw_data = cast(cursor_img.pixels, POINTER(c_ulong * region["height"] * region["width"]))
raw = bytearray(raw_data.contents)
data = bytearray(region["height"] * region["width"] * 4)
data[3::4] = raw[3::8]
data[2::4] = raw[2::8]
data[1::4] = raw[1::8]
data[::4] = raw[::8]
return self.cls_image(data, region)

View File

@ -0,0 +1,23 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from typing import Any, NamedTuple
Monitor = dict[str, int]
Monitors = list[Monitor]
Pixel = tuple[int, int, int]
Pixels = list[tuple[Pixel, ...]]
CFunctions = dict[str, tuple[str, list[Any], Any]]
class Pos(NamedTuple):
left: int
top: int
class Size(NamedTuple):
width: int
height: int

View File

@ -0,0 +1,125 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from mss.exception import ScreenShotError
from mss.models import Monitor, Pixel, Pixels, Pos, Size
if TYPE_CHECKING: # pragma: nocover
from collections.abc import Iterator
class ScreenShot:
"""Screenshot object.
.. note::
A better name would have been *Image*, but to prevent collisions
with PIL.Image, it has been decided to use *ScreenShot*.
"""
__slots__ = {"__pixels", "__rgb", "pos", "raw", "size"}
def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = None) -> None:
self.__pixels: Pixels | None = None
self.__rgb: bytes | None = None
#: Bytearray of the raw BGRA pixels retrieved by ctypes
#: OS independent implementations.
self.raw = data
#: NamedTuple of the screenshot coordinates.
self.pos = Pos(monitor["left"], monitor["top"])
#: NamedTuple of the screenshot size.
self.size = Size(monitor["width"], monitor["height"]) if size is None else size
def __repr__(self) -> str:
return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>"
@property
def __array_interface__(self) -> dict[str, Any]:
"""Numpy array interface support.
It uses raw data in BGRA form.
See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html
"""
return {
"version": 3,
"shape": (self.height, self.width, 4),
"typestr": "|u1",
"data": self.raw,
}
@classmethod
def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, /) -> ScreenShot:
"""Instantiate a new class given only screenshot's data and size."""
monitor = {"left": 0, "top": 0, "width": width, "height": height}
return cls(data, monitor)
@property
def bgra(self) -> bytes:
"""BGRA values from the BGRA raw pixels."""
return bytes(self.raw)
@property
def height(self) -> int:
"""Convenient accessor to the height size."""
return self.size.height
@property
def left(self) -> int:
"""Convenient accessor to the left position."""
return self.pos.left
@property
def pixels(self) -> Pixels:
""":return list: RGB tuples."""
if not self.__pixels:
rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4])
self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width))
return self.__pixels
@property
def rgb(self) -> bytes:
"""Compute RGB values from the BGRA raw pixels.
:return bytes: RGB pixels.
"""
if not self.__rgb:
rgb = bytearray(self.height * self.width * 3)
raw = self.raw
rgb[::3] = raw[2::4]
rgb[1::3] = raw[1::4]
rgb[2::3] = raw[::4]
self.__rgb = bytes(rgb)
return self.__rgb
@property
def top(self) -> int:
"""Convenient accessor to the top position."""
return self.pos.top
@property
def width(self) -> int:
"""Convenient accessor to the width size."""
return self.size.width
def pixel(self, coord_x: int, coord_y: int) -> Pixel:
"""Returns the pixel value at a given position.
:param int coord_x: The x coordinate.
:param int coord_y: The y coordinate.
:return tuple: The pixel value as (R, G, B).
"""
try:
return self.pixels[coord_y][coord_x]
except IndexError as exc:
msg = f"Pixel location ({coord_x}, {coord_y}) is out of range."
raise ScreenShotError(msg) from exc

View File

@ -0,0 +1,65 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
import os
import struct
import zlib
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: Path | str | None = None) -> bytes | None:
"""Dump data to a PNG file. If `output` is `None`, create no file but return
the whole PNG data.
:param bytes data: RGBRGB...RGB data.
:param tuple size: The (width, height) pair.
:param int level: PNG compression level.
:param str output: Output file name.
"""
pack = struct.pack
crc32 = zlib.crc32
width, height = size
line = width * 3
png_filter = pack(">B", 0)
scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)])
magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10)
# Header: size, marker, data, CRC32
ihdr = [b"", b"IHDR", b"", b""]
ihdr[2] = pack(">2I5B", width, height, 8, 2, 0, 0, 0)
ihdr[3] = pack(">I", crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF)
ihdr[0] = pack(">I", len(ihdr[2]))
# Data: size, marker, data, CRC32
idat = [b"", b"IDAT", zlib.compress(scanlines, level), b""]
idat[3] = pack(">I", crc32(b"".join(idat[1:3])) & 0xFFFFFFFF)
idat[0] = pack(">I", len(idat[2]))
# Footer: size, marker, None, CRC32
iend = [b"", b"IEND", b"", b""]
iend[3] = pack(">I", crc32(iend[1]) & 0xFFFFFFFF)
iend[0] = pack(">I", len(iend[2]))
if not output:
# Returns raw bytes of the whole PNG data
return magic + b"".join(ihdr + idat + iend)
with open(output, "wb") as fileh: # noqa: PTH123
fileh.write(magic)
fileh.write(b"".join(ihdr))
fileh.write(b"".join(idat))
fileh.write(b"".join(iend))
# Force write of file to disk
fileh.flush()
os.fsync(fileh.fileno())
return None

View File

@ -0,0 +1,250 @@
"""This is part of the MSS Python's module.
Source: https://github.com/BoboTiG/python-mss.
"""
from __future__ import annotations
import ctypes
import sys
from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p
from ctypes.wintypes import (
BOOL,
DOUBLE,
DWORD,
HBITMAP,
HDC,
HGDIOBJ,
HWND,
INT,
LONG,
LPARAM,
LPRECT,
RECT,
UINT,
WORD,
)
from threading import local
from typing import TYPE_CHECKING, Any
from mss.base import MSSBase
from mss.exception import ScreenShotError
if TYPE_CHECKING: # pragma: nocover
from mss.models import CFunctions, Monitor
from mss.screenshot import ScreenShot
__all__ = ("MSS",)
CAPTUREBLT = 0x40000000
DIB_RGB_COLORS = 0
SRCCOPY = 0x00CC0020
class BITMAPINFOHEADER(Structure):
"""Information about the dimensions and color format of a DIB."""
_fields_ = (
("biSize", DWORD),
("biWidth", LONG),
("biHeight", LONG),
("biPlanes", WORD),
("biBitCount", WORD),
("biCompression", DWORD),
("biSizeImage", DWORD),
("biXPelsPerMeter", LONG),
("biYPelsPerMeter", LONG),
("biClrUsed", DWORD),
("biClrImportant", DWORD),
)
class BITMAPINFO(Structure):
"""Structure that defines the dimensions and color information for a DIB."""
_fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3))
MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE)
# C functions that will be initialised later.
#
# Available attr: gdi32, user32.
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
# Syntax: cfunction: (attr, argtypes, restype)
"BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL),
"CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP),
"CreateCompatibleDC": ("gdi32", [HDC], HDC),
"DeleteDC": ("gdi32", [HDC], HDC),
"DeleteObject": ("gdi32", [HGDIOBJ], INT),
"EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL),
"GetDeviceCaps": ("gdi32", [HWND, INT], INT),
"GetDIBits": ("gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL),
"GetSystemMetrics": ("user32", [INT], INT),
"GetWindowDC": ("user32", [HWND], HDC),
"ReleaseDC": ("user32", [HWND, HDC], c_int),
"SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ),
}
class MSS(MSSBase):
"""Multiple ScreenShots implementation for Microsoft Windows."""
__slots__ = {"gdi32", "user32", "_handles"}
def __init__(self, /, **kwargs: Any) -> None:
"""Windows initialisations."""
super().__init__(**kwargs)
self.user32 = ctypes.WinDLL("user32")
self.gdi32 = ctypes.WinDLL("gdi32")
self._set_cfunctions()
self._set_dpi_awareness()
# Available thread-specific variables
self._handles = local()
self._handles.region_width_height = (0, 0)
self._handles.bmp = None
self._handles.srcdc = self.user32.GetWindowDC(0)
self._handles.memdc = self.gdi32.CreateCompatibleDC(self._handles.srcdc)
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biPlanes = 1 # Always 1
bmi.bmiHeader.biBitCount = 32 # See grab.__doc__ [2]
bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression)
bmi.bmiHeader.biClrUsed = 0 # See grab.__doc__ [3]
bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3]
self._handles.bmi = bmi
def close(self) -> None:
# Clean-up
if self._handles.bmp:
self.gdi32.DeleteObject(self._handles.bmp)
self._handles.bmp = None
if self._handles.memdc:
self.gdi32.DeleteDC(self._handles.memdc)
self._handles.memdc = None
if self._handles.srcdc:
self.user32.ReleaseDC(0, self._handles.srcdc)
self._handles.srcdc = None
def _set_cfunctions(self) -> None:
"""Set all ctypes functions and attach them to attributes."""
cfactory = self._cfactory
attrs = {
"gdi32": self.gdi32,
"user32": self.user32,
}
for func, (attr, argtypes, restype) in CFUNCTIONS.items():
cfactory(attrs[attr], func, argtypes, restype)
def _set_dpi_awareness(self) -> None:
"""Set DPI awareness to capture full screen on Hi-DPI monitors."""
version = sys.getwindowsversion()[:2]
if version >= (6, 3):
# Windows 8.1+
# Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means:
# per monitor DPI aware. This app checks for the DPI when it is
# created and adjusts the scale factor whenever the DPI changes.
# These applications are not automatically scaled by the system.
ctypes.windll.shcore.SetProcessDpiAwareness(2)
elif (6, 0) <= version < (6, 3):
# Windows Vista, 7, 8, and Server 2012
self.user32.SetProcessDPIAware()
def _monitors_impl(self) -> None:
"""Get positions of monitors. It will populate self._monitors."""
int_ = int
user32 = self.user32
get_system_metrics = user32.GetSystemMetrics
# All monitors
self._monitors.append(
{
"left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN
"top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN
"width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN
"height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN
},
)
# Each monitor
def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int:
"""Callback for monitorenumproc() function, it will return
a RECT with appropriate values.
"""
rct = rect.contents
self._monitors.append(
{
"left": int_(rct.left),
"top": int_(rct.top),
"width": int_(rct.right) - int_(rct.left),
"height": int_(rct.bottom) - int_(rct.top),
},
)
return 1
callback = MONITORNUMPROC(_callback)
user32.EnumDisplayMonitors(0, 0, callback, 0)
def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
"""Retrieve all pixels from a monitor. Pixels have to be RGB.
In the code, there are a few interesting things:
[1] bmi.bmiHeader.biHeight = -height
A bottom-up DIB is specified by setting the height to a
positive number, while a top-down DIB is specified by
setting the height to a negative number.
https://msdn.microsoft.com/en-us/library/ms787796.aspx
https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx
[2] bmi.bmiHeader.biBitCount = 32
image_data = create_string_buffer(height * width * 4)
We grab the image in RGBX mode, so that each word is 32bit
and we have no striding.
Inspired by https://github.com/zoofIO/flexx
[3] bmi.bmiHeader.biClrUsed = 0
bmi.bmiHeader.biClrImportant = 0
When biClrUsed and biClrImportant are set to zero, there
is "no" color table, so we can read the pixels of the bitmap
retrieved by gdi32.GetDIBits() as a sequence of RGB values.
Thanks to http://stackoverflow.com/a/3688682
"""
srcdc, memdc = self._handles.srcdc, self._handles.memdc
gdi = self.gdi32
width, height = monitor["width"], monitor["height"]
if self._handles.region_width_height != (width, height):
self._handles.region_width_height = (width, height)
self._handles.bmi.bmiHeader.biWidth = width
self._handles.bmi.bmiHeader.biHeight = -height # Why minus? [1]
self._handles.data = ctypes.create_string_buffer(width * height * 4) # [2]
if self._handles.bmp:
gdi.DeleteObject(self._handles.bmp)
self._handles.bmp = gdi.CreateCompatibleBitmap(srcdc, width, height)
gdi.SelectObject(memdc, self._handles.bmp)
gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT)
bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS)
if bits != height:
msg = "gdi32.GetDIBits() failed."
raise ScreenShotError(msg)
return self.cls_image(bytearray(self._handles.data), monitor)
def _cursor_impl(self) -> ScreenShot | None:
"""Retrieve all cursor data. Pixels have to be RGB."""
return None