asd
This commit is contained in:
27
venv/lib/python3.12/site-packages/mss/__init__.py
Normal file
27
venv/lib/python3.12/site-packages/mss/__init__.py
Normal 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")
|
83
venv/lib/python3.12/site-packages/mss/__main__.py
Normal file
83
venv/lib/python3.12/site-packages/mss/__main__.py
Normal 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())
|
261
venv/lib/python3.12/site-packages/mss/base.py
Normal file
261
venv/lib/python3.12/site-packages/mss/base.py
Normal 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
|
211
venv/lib/python3.12/site-packages/mss/darwin.py
Normal file
211
venv/lib/python3.12/site-packages/mss/darwin.py
Normal 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
|
15
venv/lib/python3.12/site-packages/mss/exception.py
Normal file
15
venv/lib/python3.12/site-packages/mss/exception.py
Normal 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 {}
|
40
venv/lib/python3.12/site-packages/mss/factory.py
Normal file
40
venv/lib/python3.12/site-packages/mss/factory.py
Normal 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)
|
481
venv/lib/python3.12/site-packages/mss/linux.py
Normal file
481
venv/lib/python3.12/site-packages/mss/linux.py
Normal 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)
|
23
venv/lib/python3.12/site-packages/mss/models.py
Normal file
23
venv/lib/python3.12/site-packages/mss/models.py
Normal 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
|
0
venv/lib/python3.12/site-packages/mss/py.typed
Normal file
0
venv/lib/python3.12/site-packages/mss/py.typed
Normal file
125
venv/lib/python3.12/site-packages/mss/screenshot.py
Normal file
125
venv/lib/python3.12/site-packages/mss/screenshot.py
Normal 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
|
65
venv/lib/python3.12/site-packages/mss/tools.py
Normal file
65
venv/lib/python3.12/site-packages/mss/tools.py
Normal 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
|
250
venv/lib/python3.12/site-packages/mss/windows.py
Normal file
250
venv/lib/python3.12/site-packages/mss/windows.py
Normal 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
|
Reference in New Issue
Block a user