AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -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.1.0"
|
||||
__author__ = "Mickaël Schoentgen"
|
||||
__date__ = "2013-2025"
|
||||
__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")
|
||||
@@ -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())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,217 @@
|
||||
"""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
|
||||
|
||||
kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816
|
||||
kCGWindowImageNominalResolution = 1 << 4 # noqa: N816
|
||||
kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816
|
||||
# Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information)
|
||||
IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution
|
||||
|
||||
|
||||
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, IMAGE_OPTIONS)
|
||||
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
|
||||
@@ -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 {}
|
||||
@@ -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 # noqa: PLC0415
|
||||
|
||||
return darwin.MSS(**kwargs)
|
||||
|
||||
if os_ == "linux":
|
||||
from mss import linux # noqa: PLC0415
|
||||
|
||||
return linux.MSS(**kwargs)
|
||||
|
||||
if os_ == "windows":
|
||||
from mss import windows # noqa: PLC0415
|
||||
|
||||
return windows.MSS(**kwargs)
|
||||
|
||||
msg = f"System {os_!r} not (yet?) implemented."
|
||||
raise ScreenShotError(msg)
|
||||
@@ -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__ = {"_handles", "xfixes", "xlib", "xrandr"}
|
||||
|
||||
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)
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
@@ -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__ = {"_handles", "gdi32", "user32"}
|
||||
|
||||
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