Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Technical Changes

## 10.0.0 (2024-11-xx)

### base.py
- Added `OPAQUE`

### darwin.py
- Added `MAC_VERSION_CATALINA`

### linux.py
- Added `BITS_PER_PIXELS_32`
- Added `SUPPORTED_BITS_PER_PIXELS`

## 9.0.0 (2023-04-18)

### linux.py
Expand Down
12 changes: 6 additions & 6 deletions docs/source/examples/callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
Screenshot of the monitor 1, with callback.
"""

import os
import os.path
from pathlib import Path

import mss


def on_exists(fname: str) -> None:
"""Callback example when we try to overwrite an existing screenshot."""
if os.path.isfile(fname):
newfile = f"{fname}.old"
print(f"{fname} -> {newfile}")
os.rename(fname, newfile)
file = Path(fname)
if file.is_file():
newfile = file.with_name(f"{file.name}.old")
print(f"{fname} → {newfile}")
file.rename(newfile)


with mss.mss() as sct:
Expand Down
46 changes: 26 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,34 @@ line-length = 120
indent-width = 4
target-version = "py39"

[tool.ruff.lint]
extend-select = ["ALL"]
ignore = [
"ANN101",
"ANN401",
"C90",
"COM812",
"D", # TODO
"ERA",
"FBT",
"INP001",
"ISC001",
"PTH",
"PL",
"S",
"SLF",
"T201",
]
fixable = ["ALL"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[tool.ruff.lint]
fixable = ["ALL"]
extend-select = ["ALL"]
ignore = [
"ANN401", # typing.Any
"C90", # complexity
"COM812", # conflict
"D", # TODO
"ISC001", # conflict
"T201", # `print()`
]

[tool.ruff.lint.per-file-ignores]
"docs/source/*" = [
"ERA001", # commented code
"INP001", # file `xxx` is part of an implicit namespace package
]
"src/tests/*" = [
"FBT001", # boolean-typed positional argument in function definition
"PLR2004", # magic value used in comparison
"S101", # use of `assert` detected
"S602", # `subprocess` call with `shell=True`
"S603", # `subprocess` call
"SLF001", # private member accessed
]
5 changes: 3 additions & 2 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

lock = Lock()

OPAQUE = 255


class MSSBase(metaclass=ABCMeta):
"""This class will be overloaded by a system specific one."""
Expand Down Expand Up @@ -200,7 +202,6 @@ def shot(self, /, **kwargs: Any) -> str:
@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

Expand Down Expand Up @@ -234,7 +235,7 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot:
if not alpha:
continue

if alpha == 255:
if alpha == OPAQUE:
screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3]
else:
alpha2 = alpha / 255
Expand Down
13 changes: 9 additions & 4 deletions src/mss/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

__all__ = ("MSS",)

MAC_VERSION_CATALINA = 10.16


def cgfloat() -> type[c_double | c_float]:
"""Get the appropriate value for a float."""
Expand Down Expand Up @@ -59,7 +61,7 @@ def __repr__(self) -> str:
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
# cfunction: (attr, argtypes, restype)
# Syntax: cfunction: (attr, argtypes, restype)
"CGDataProviderCopyData": ("core", [c_void_p], c_void_p),
"CGDisplayBounds": ("core", [c_uint32], CGRect),
"CGDisplayRotation": ("core", [c_uint32], c_float),
Expand Down Expand Up @@ -98,7 +100,7 @@ def __init__(self, /, **kwargs: Any) -> None:
def _init_library(self) -> None:
"""Load the CoreGraphics library."""
version = float(".".join(mac_ver()[0].split(".")[:2]))
if version < 10.16:
if version < MAC_VERSION_CATALINA:
coregraphics = ctypes.util.find_library("CoreGraphics")
else:
# macOS Big Sur and newer
Expand Down Expand Up @@ -136,9 +138,13 @@ def _monitors_impl(self) -> None:
rect = core.CGDisplayBounds(display)
rect = core.CGRectStandardize(rect)
width, height = rect.size.width, rect.size.height

# 0.0: normal
# 90.0: right
# -90.0: left
if core.CGDisplayRotation(display) in {90.0, -90.0}:
# {0.0: "normal", 90.0: "right", -90.0: "left"}
width, height = height, width

self._monitors.append(
{
"left": int_(rect.origin.x),
Expand All @@ -161,7 +167,6 @@ def _monitors_impl(self) -> None:

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"]))

Expand Down
1 change: 0 additions & 1 deletion src/mss/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def mss(**kwargs: Any) -> MSSBase:
It then proxies its arguments to the class for
instantiation.
"""

os_ = platform.system().lower()

if os_ == "darwin":
Expand Down
8 changes: 6 additions & 2 deletions src/mss/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@

PLAINMASK = 0x00FFFFFF
ZPIXMAP = 2
BITS_PER_PIXELS_32 = 32
SUPPORTED_BITS_PER_PIXELS = {
BITS_PER_PIXELS_32,
}


class Display(Structure):
Expand Down Expand Up @@ -233,7 +237,7 @@ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, An
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
# cfunction: (attr, argtypes, restype)
# 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),
Expand Down Expand Up @@ -433,7 +437,7 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:

try:
bits_per_pixel = ximage.contents.bits_per_pixel
if bits_per_pixel != 32:
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)

Expand Down
9 changes: 6 additions & 3 deletions src/mss/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
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: str | None = None) -> bytes | None:

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.

Expand All @@ -18,7 +22,6 @@ def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str
:param int level: PNG compression level.
:param str output: Output file name.
"""

pack = struct.pack
crc32 = zlib.crc32

Expand Down Expand Up @@ -49,7 +52,7 @@ def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str
# Returns raw bytes of the whole PNG data
return magic + b"".join(ihdr + idat + iend)

with open(output, "wb") as fileh:
with open(output, "wb") as fileh: # noqa: PTH123
fileh.write(magic)
fileh.write(b"".join(ihdr))
fileh.write(b"".join(idat))
Expand Down
3 changes: 1 addition & 2 deletions src/mss/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class BITMAPINFO(Structure):
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
# cfunction: (attr, argtypes, restype)
# 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),
Expand Down Expand Up @@ -179,7 +179,6 @@ 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(
{
Expand Down
Empty file added src/tests/__init__.py
Empty file.
18 changes: 8 additions & 10 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
Source: https://github.com/BoboTiG/python-mss.
"""

import glob
import os
import platform
from collections.abc import Generator
from hashlib import md5
from hashlib import sha256
from pathlib import Path
from zipfile import ZipFile

Expand All @@ -28,13 +26,13 @@ def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator:

def purge_files() -> None:
"""Remove all generated files from previous runs."""
for fname in glob.glob("*.png"):
print(f"Deleting {fname!r} ...")
os.unlink(fname)
for file in Path().glob("*.png"):
print(f"Deleting {file} ...")
file.unlink()

for fname in glob.glob("*.png.old"):
print(f"Deleting {fname!r} ...")
os.unlink(fname)
for file in Path().glob("*.png.old"):
print(f"Deleting {file} ...")
file.unlink()


@pytest.fixture(scope="module", autouse=True)
Expand All @@ -48,7 +46,7 @@ def raw() -> bytes:
with ZipFile(file) as fh:
data = fh.read(file.with_suffix("").name)

assert md5(data).hexdigest() == "125696266e2a8f5240f6bc17e4df98c6"
assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd"
return data


Expand Down
44 changes: 23 additions & 21 deletions src/tests/test_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import platform
import sys
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import Mock, patch

Expand Down Expand Up @@ -104,53 +105,54 @@ def main(*args: str, ret: int = 0) -> None:
main()
captured = capsys.readouterr()
for mon, line in enumerate(captured.out.splitlines(), 1):
filename = f"monitor-{mon}.png"
assert line.endswith(filename)
assert os.path.isfile(filename)
os.remove(filename)
filename = Path(f"monitor-{mon}.png")
assert line.endswith(filename.name)
assert filename.is_file()
filename.unlink()

file = Path("monitor-1.png")
for opt in ("-m", "--monitor"):
main(opt, "1")
captured = capsys.readouterr()
assert captured.out.endswith("monitor-1.png\n")
assert os.path.isfile("monitor-1.png")
os.remove("monitor-1.png")
assert captured.out.endswith(f"{file.name}\n")
assert filename.is_file()
filename.unlink()

for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]):
main(*opts)
captured = capsys.readouterr()
assert not captured.out
assert os.path.isfile("monitor-1.png")
os.remove("monitor-1.png")
assert filename.is_file()
filename.unlink()

fmt = "sct-{mon}-{width}x{height}.png"
for opt in ("-o", "--out"):
main(opt, fmt)
captured = capsys.readouterr()
with mss.mss(display=os.getenv("DISPLAY")) as sct:
for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1):
filename = fmt.format(mon=mon, **monitor)
assert line.endswith(filename)
assert os.path.isfile(filename)
os.remove(filename)
filename = Path(fmt.format(mon=mon, **monitor))
assert line.endswith(filename.name)
assert filename.is_file()
filename.unlink()

fmt = "sct_{mon}-{date:%Y-%m-%d}.png"
for opt in ("-o", "--out"):
main("-m 1", opt, fmt)
filename = fmt.format(mon=1, date=datetime.now(tz=UTC))
filename = Path(fmt.format(mon=1, date=datetime.now(tz=UTC)))
captured = capsys.readouterr()
assert captured.out.endswith(filename + "\n")
assert os.path.isfile(filename)
os.remove(filename)
assert captured.out.endswith(f"{filename}\n")
assert filename.is_file()
filename.unlink()

coordinates = "2,12,40,67"
filename = "sct-2x12_40x67.png"
filename = Path("sct-2x12_40x67.png")
for opt in ("-c", "--coordinates"):
main(opt, coordinates)
captured = capsys.readouterr()
assert captured.out.endswith(filename + "\n")
assert os.path.isfile(filename)
os.remove(filename)
assert captured.out.endswith(f"{filename}\n")
assert filename.is_file()
filename.unlink()

coordinates = "2,12,40"
for opt in ("-c", "--coordinates"):
Expand Down
Loading