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

View File

@ -0,0 +1,54 @@
import logging
from typing import List, Optional, Tuple
from PIL import Image
from pyscreenshot.about import __version__
from pyscreenshot.childproc import childprocess_backend_version
from pyscreenshot.loader import FailedBackendError, backend_dict, backend_grab
ADDITIONAL_IMPORTS = [FailedBackendError]
log = logging.getLogger(__name__)
log.debug("version=%s", __version__)
def grab(
bbox: Optional[Tuple[int, int, int, int]] = None,
childprocess: bool = True,
backend: Optional[str] = None,
) -> "Image":
"""Copy the contents of the screen to PIL image memory.
:param bbox: optional bounding box (x1,y1,x2,y2)
:param childprocess: run back-end in new process using popen. (bool)
This isolates back-ends from each other and from main process.
Leave it as it is (True) to have a safe setting.
Set it False to improve performance, but then conflicts are possible.
:param backend: back-end can be forced if set (examples:scrot, wx,..),
otherwise back-end is automatic
"""
if bbox:
x1, y1, x2, y2 = bbox
if x2 <= x1:
raise ValueError("bbox x2<=x1")
if y2 <= y1:
raise ValueError("bbox y2<=y1")
return backend_grab(backend, bbox, childprocess)
def backends() -> List[str]:
"""Back-end names as a list.
:return: back-ends as string list
"""
return list(backend_dict.keys())
def backend_version(backend: str) -> str:
"""Back-end version.
:param backend: back-end (examples:scrot, wx,..)
:return: version as string
"""
return childprocess_backend_version(backend)

View File

@ -0,0 +1 @@
__version__ = "3.1"

View File

@ -0,0 +1,25 @@
import time
from entrypoint2 import entrypoint
import pyscreenshot
from pyscreenshot import backends
@entrypoint
def show():
im = []
blist = []
for x in backends():
try:
print("--> grabbing by " + x)
im.append(pyscreenshot.grab(bbox=(500, 400, 800, 600), backend=x))
blist.append(x)
except Exception as e:
print(e)
print(im)
print(blist)
for x in im:
x.show()
time.sleep(0.5)

View File

@ -0,0 +1,119 @@
import sys
import time
from entrypoint2 import entrypoint
import pyscreenshot
from pyscreenshot.plugins.freedesktop_dbus import FreedesktopDBusWrapper
from pyscreenshot.plugins.gnome_dbus import GnomeDBusWrapper
from pyscreenshot.plugins.gnome_screenshot import GnomeScreenshotWrapper
from pyscreenshot.plugins.kwin_dbus import KwinDBusWrapper
from pyscreenshot.util import run_mod_as_subproc
def run(force_backend, n, childprocess, bbox=None):
sys.stdout.write("%-20s\t" % force_backend)
sys.stdout.flush() # before any crash
# if force_backend == "freedesktop_dbus":
# return
if force_backend == "default":
force_backend = None
try:
start = time.time()
for _ in range(n):
pyscreenshot.grab(
backend=force_backend, childprocess=childprocess, bbox=bbox
)
end = time.time()
dt = end - start
s = "%-4.2g sec\t" % dt
s += "(%5d ms per call)" % (1000.0 * dt / n)
sys.stdout.write(s)
finally:
print("")
novirt = [
GnomeDBusWrapper.name,
KwinDBusWrapper.name,
GnomeScreenshotWrapper.name,
FreedesktopDBusWrapper.name,
]
def run_all(n, childprocess_param, virtual_only=True, bbox=None):
debug = True
print("")
print("n=%s" % n)
print("------------------------------------------------------")
if bbox:
x1, y1, x2, y2 = map(str, bbox)
bbox = ":".join(map(str, (x1, y1, x2, y2)))
bboxpar = ["--bbox", bbox]
else:
bboxpar = []
if debug:
debugpar = ["--debug"]
else:
debugpar = []
for x in ["default"] + pyscreenshot.backends():
if x == "freedesktop_dbus":
continue
backendpar = ["--backend", x]
# skip non X backends
if virtual_only and x in novirt:
continue
p = run_mod_as_subproc(
"pyscreenshot.check.speedtest",
["--childprocess", childprocess_param, "--number", str(n)]
+ bboxpar
+ debugpar
+ backendpar,
)
print(p.stdout)
@entrypoint
def speedtest(virtual_display=False, backend="", childprocess="", bbox="", number=10):
"""Performance test of all back-ends.
:param virtual_display: run with Xvfb
:param bbox: bounding box coordinates x1:y1:x2:y2
:param backend: back-end can be forced if set (example:default, scrot, wx,..),
otherwise all back-ends are tested
:param childprocess: pyscreenshot parameter childprocess (0/1)
:param number: number of screenshots for each backend (default:10)
"""
childprocess_param = childprocess
if childprocess == "":
childprocess = True # default
elif childprocess == "0":
childprocess = False
elif childprocess == "1":
childprocess = True
else:
raise ValueError("invalid childprocess value")
if bbox:
x1, y1, x2, y2 = map(int, bbox.split(":"))
bbox = x1, y1, x2, y2
else:
bbox = None
def f(virtual_only):
if backend:
try:
run(backend, number, childprocess, bbox=bbox)
except pyscreenshot.FailedBackendError:
pass
else:
run_all(number, childprocess_param, virtual_only=virtual_only, bbox=bbox)
if virtual_display:
from pyvirtualdisplay import Display
with Display(visible=0):
f(virtual_only=True)
else:
f(virtual_only=False)

View File

@ -0,0 +1,24 @@
import platform
import pyscreenshot
from pyscreenshot import backend_version
def print_name_version(name, version):
s = "{:<20} {}".format(name, version)
print(s)
def print_versions():
print_name_version("python", platform.python_version())
print_name_version("pyscreenshot", pyscreenshot.__version__)
for name in pyscreenshot.backends():
v = backend_version(name)
if not v:
v = ""
print_name_version(name, v)
if __name__ == "__main__":
print_versions()

View File

@ -0,0 +1,41 @@
import logging
import os
from tempfile import TemporaryDirectory
from pyscreenshot.err import FailedBackendError
from pyscreenshot.imcodec import codec
from pyscreenshot.util import run_mod_as_subproc
log = logging.getLogger(__name__)
def childprocess_backend_version(backend):
p = run_mod_as_subproc("pyscreenshot.cli.print_backend_version", [backend])
if p.return_code != 0:
log.warning(p)
raise FailedBackendError(p)
return p.stdout
def childprocess_grab(backend, bbox):
with TemporaryDirectory(prefix="pyscreenshot") as tmpdirname:
filename = os.path.join(tmpdirname, "screenshot.png")
cmd = ["--filename", filename]
if bbox:
x1, y1, x2, y2 = map(str, bbox)
bbox = ":".join(map(str, (x1, y1, x2, y2)))
cmd += ["--bbox", bbox]
if backend:
cmd += ["--backend", backend]
if log.isEnabledFor(logging.DEBUG):
cmd += ["--debug"]
p = run_mod_as_subproc("pyscreenshot.cli.grab", cmd)
if p.return_code != 0:
# log.debug(p)
raise FailedBackendError(p)
data = open(filename, "rb").read()
data = codec[1](data)
return data

View File

@ -0,0 +1,30 @@
from entrypoint2 import entrypoint
import pyscreenshot
from pyscreenshot.imcodec import codec
@entrypoint
def main(filename="", bbox="", backend="", show=False):
"""Copy the contents of the screen to file.
:param filename: output file
:param show: show image
:param bbox: bounding box coordinates x1:y1:x2:y2
:param backend: back-end can be forced if set (example:scrot, wx,..),
otherwise back-end is automatic
"""
backend = backend if backend else None
if bbox:
x1, y1, x2, y2 = map(int, bbox.split(":"))
bbox = x1, y1, x2, y2
else:
bbox = None
im = pyscreenshot.grab(bbox=bbox, childprocess=False, backend=backend)
if filename:
b = codec[0](im)
with open(filename, "wb") as f:
f.write(b)
if show:
im.show()

View File

@ -0,0 +1,23 @@
import logging
from entrypoint2 import entrypoint
from pyscreenshot.loader import backend_version2
log = logging.getLogger(__name__)
@entrypoint
def main(backend):
"""Print pyscreenshot back-end version.
:param backend: back-end (example:scrot, wx,..)
"""
backend = backend if backend else None
try:
v = backend_version2(backend)
except Exception as e:
log.warning(e)
v = ""
print(v)

View File

@ -0,0 +1,2 @@
class FailedBackendError(Exception):
pass

View File

@ -0,0 +1,8 @@
"Grab the part of the screen"
import pyscreenshot as ImageGrab
# part of the screen
im = ImageGrab.grab(bbox=(10, 10, 510, 510)) # X1,Y1,X2,Y2
# save image file
im.save("box.png")

View File

@ -0,0 +1,8 @@
"Grab the whole screen"
import pyscreenshot as ImageGrab
# grab fullscreen
im = ImageGrab.grab()
# save image file
im.save("fullscreen.png")

View File

@ -0,0 +1,14 @@
"Create screenshot of xmessage with Xvfb"
from time import sleep
from easyprocess import EasyProcess
from pyvirtualdisplay import Display
import pyscreenshot as ImageGrab
with Display(size=(100, 60)) as disp: # start Xvfb display
# display is available
with EasyProcess(["xmessage", "hello"]): # start xmessage
sleep(1) # wait for displaying window
img = ImageGrab.grab()
img.save("xmessage.png")

View File

@ -0,0 +1,36 @@
import io
from PIL import Image
# def _coder(im):
# if im:
# data = {
# 'pixels': im.tobytes(),
# 'size': im.size,
# 'mode': im.mode,
# }
# return data
#
#
# def _decoder(data):
# if data:
# im = Image.frombytes(data['mode'], data['size'], data['pixels'])
# return im
def _coder(im):
if im:
b = io.BytesIO()
im.save(b, format="png")
data = b.getvalue()
return data
def _decoder(data):
if data:
b = io.BytesIO(data)
im = Image.open(b)
return im
codec = (_coder, _decoder)

View File

@ -0,0 +1,178 @@
import logging
import traceback
from pyscreenshot.childproc import childprocess_grab
from pyscreenshot.err import FailedBackendError
from pyscreenshot.plugins.freedesktop_dbus import FreedesktopDBusWrapper
from pyscreenshot.plugins.gdk3pixbuf import Gdk3PixbufWrapper
from pyscreenshot.plugins.gnome_dbus import GnomeDBusWrapper
from pyscreenshot.plugins.gnome_screenshot import GnomeScreenshotWrapper
from pyscreenshot.plugins.grim import GrimWrapper
from pyscreenshot.plugins.imagemagick import ImagemagickWrapper
# from pyscreenshot.plugins.kwin_dbus import KwinDBusWrapper
from pyscreenshot.plugins.mac_quartz import MacQuartzWrapper
from pyscreenshot.plugins.mac_screencapture import ScreencaptureWrapper
from pyscreenshot.plugins.maim import MaimWrapper
from pyscreenshot.plugins.msswrap import MssWrapper
from pyscreenshot.plugins.pilwrap import PilWrapper
from pyscreenshot.plugins.pyside2_grabwindow import PySide2GrabWindow
# from pyscreenshot.plugins.pyside_grabwindow import PySideGrabWindow
# from pyscreenshot.plugins.qt4grabwindow import Qt4GrabWindow
from pyscreenshot.plugins.qt5grabwindow import Qt5GrabWindow
from pyscreenshot.plugins.scrot import ScrotWrapper
from pyscreenshot.plugins.wxscreen import WxScreen
# from pyscreenshot.plugins.ksnip import KsnipWrapper
from pyscreenshot.util import (
platform_is_linux,
platform_is_osx,
platform_is_win,
use_x_display,
)
log = logging.getLogger(__name__)
backend_dict = {
PilWrapper.name: PilWrapper,
MssWrapper.name: MssWrapper,
ScrotWrapper.name: ScrotWrapper,
GrimWrapper.name: GrimWrapper,
MaimWrapper.name: MaimWrapper,
ImagemagickWrapper.name: ImagemagickWrapper,
Qt5GrabWindow.name: Qt5GrabWindow,
# Qt4GrabWindow.name: Qt4GrabWindow,
PySide2GrabWindow.name: PySide2GrabWindow,
# PySideGrabWindow.name: PySideGrabWindow,
WxScreen.name: WxScreen,
Gdk3PixbufWrapper.name: Gdk3PixbufWrapper,
ScreencaptureWrapper.name: ScreencaptureWrapper,
MacQuartzWrapper.name: MacQuartzWrapper,
FreedesktopDBusWrapper.name: FreedesktopDBusWrapper,
GnomeDBusWrapper.name: GnomeDBusWrapper,
GnomeScreenshotWrapper.name: GnomeScreenshotWrapper,
# KwinDBusWrapper.name: KwinDBusWrapper,
# XwdWrapper.name: XwdWrapper,
# KsnipWrapper.name: KsnipWrapper,
}
def qt():
yield Qt5GrabWindow
# yield Qt4GrabWindow
yield PySide2GrabWindow
# yield PySideGrabWindow
def backends(childprocess):
# the order is based on performance
if platform_is_linux():
if use_x_display():
if childprocess:
yield ScrotWrapper
yield PilWrapper
yield MssWrapper
else:
yield PilWrapper
yield MssWrapper
yield ScrotWrapper
yield MaimWrapper
yield ImagemagickWrapper
yield Gdk3PixbufWrapper
yield WxScreen
for x in qt():
yield x
yield GnomeDBusWrapper
# on screen notification
# "The process is not authorized to take a screenshot"
# yield KwinDBusWrapper
# flash effect
yield GnomeScreenshotWrapper
yield GrimWrapper
# yield KsnipWrapper
# confirmation dialog
yield FreedesktopDBusWrapper
elif platform_is_osx():
yield PilWrapper
yield MssWrapper
# alternatives for older pillow versions
yield ScreencaptureWrapper
yield MacQuartzWrapper
# qt has some color difference
# does not work: Gdk3, wx, Imagemagick
elif platform_is_win():
yield PilWrapper
yield MssWrapper
else:
yield PilWrapper
yield MssWrapper
for x in backend_dict.values():
yield x
def select_childprocess(childprocess, backend_class):
if backend_class.is_subprocess:
# backend is always a subprocess -> nothing to do
return False
return childprocess
def auto(bbox, childprocess):
im = None
for backend_class in backends(childprocess):
backend_name = backend_class.name
try:
if select_childprocess(childprocess, backend_class):
log.debug('running "%s" in child process', backend_name)
im = childprocess_grab(backend_name, bbox)
else:
obj = backend_class()
im = obj.grab(bbox)
break
except Exception:
msg = traceback.format_exc()
log.debug(msg)
if not im:
msg = "All backends failed!"
raise FailedBackendError(msg)
return im
def force(backend_name, bbox, childprocess):
backend_class = backend_dict[backend_name]
if select_childprocess(childprocess, backend_class):
log.debug('running "%s" in child process', backend_name)
return childprocess_grab(backend_name, bbox)
else:
obj = backend_class()
im = obj.grab(bbox)
return im
def backend_grab(backend, bbox, childprocess):
if backend:
return force(backend, bbox, childprocess)
else:
return auto(bbox, childprocess)
def backend_version2(backend_name):
backend_class = backend_dict[backend_name]
obj = backend_class()
v = obj.backend_version()
return v

View File

@ -0,0 +1,5 @@
UNKNOWN_VERSION = "?.?"
class CBackend(object):
is_subprocess = False

View File

@ -0,0 +1,76 @@
import logging
import os
from PIL import Image
from pyscreenshot.plugins.backend import UNKNOWN_VERSION, CBackend
log = logging.getLogger(__name__)
class FreedesktopDBusError(Exception):
pass
class FreedesktopDBusWrapper(CBackend):
name = "freedesktop_dbus"
is_subprocess = True
def grab(self, bbox=None):
has_jeepney = False
try:
from jeepney import DBusAddress, new_method_call
from jeepney.bus_messages import MatchRule, message_bus
from jeepney.io.blocking import Proxy, open_dbus_connection
has_jeepney = True
except ImportError:
pass
if not has_jeepney:
raise FreedesktopDBusError("jeepney library is missing")
portal = DBusAddress(
object_path="/org/freedesktop/portal/desktop",
bus_name="org.freedesktop.portal.Desktop",
)
screenshot = portal.with_interface("org.freedesktop.portal.Screenshot")
conn = open_dbus_connection()
token = "pyscreenshot"
sender_name = conn.unique_name[1:].replace(".", "_")
handle = f"/org/freedesktop/portal/desktop/request/{sender_name}/{token}"
response_rule = MatchRule(
type="signal", interface="org.freedesktop.portal.Request", path=handle
)
Proxy(message_bus, conn).AddMatch(response_rule)
with conn.filter(response_rule) as responses:
req = new_method_call(
screenshot,
"Screenshot",
"sa{sv}",
("", {"handle_token": ("s", token), "interactive": ("b", False)}),
)
conn.send_and_get_reply(req)
response_msg = conn.recv_until_filtered(responses)
response, results = response_msg.body
im = False
if response == 0:
filename = results["uri"][1].split("file://", 1)[-1]
if os.path.isfile(filename):
im = Image.open(filename)
os.remove(filename)
conn.close()
if bbox and im:
im = im.crop(bbox)
return im
def backend_version(self):
return UNKNOWN_VERSION

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""Gdk3-based screenshotting.
Adapted from https://stackoverflow.com/a/37768950/81636, but uses
buffers directly instead of saving intermediate files (which is slow).
"""
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.util import platform_is_osx
class Gdk3BackendError(Exception):
pass
class Gdk3PixbufWrapper(CBackend):
name = "pygdk3"
def grab(self, bbox=None):
"""Grabs an image directly to a buffer.
:param bbox: Optional tuple or list containing (x1, y1, x2, y2) coordinates
of sub-region to capture.
:return: PIL RGB image
:raises: ValueError, if image data does not have 3 channels (RGB), each with 8
bits.
:rtype: Image
"""
if platform_is_osx():
raise Gdk3BackendError("osx not supported")
import gi # type: ignore
gi.require_version("Gdk", "3.0")
# gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gdk, GdkPixbuf # type: ignore
# read_pixel_bytes: New in version 2.32.
if GdkPixbuf.PIXBUF_MAJOR == 2:
if GdkPixbuf.PIXBUF_MINOR < 32:
raise ValueError(
"GdkPixbuf min supported version: 2.32 current:"
+ GdkPixbuf.PIXBUF_VERSION
)
w = Gdk.get_default_root_window()
if bbox is not None:
g = [bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1]]
else:
g = w.get_geometry()
pb = Gdk.pixbuf_get_from_window(w, *g)
if not pb:
raise Gdk3BackendError("empty buffer")
if pb.get_bits_per_sample() != 8:
raise Gdk3BackendError("Expected 8 bits per pixel.")
elif pb.get_n_channels() != 3:
raise Gdk3BackendError("Expected RGB image.")
# Read the entire buffer into a python bytes object.
# read_pixel_bytes: New in version 2.32.
pixel_bytes = pb.read_pixel_bytes().get_data() # type: bytes
width, height = g[2], g[3]
# Probably for SSE alignment reasons, the pixbuf has extra data in each line.
# The args after "raw" help handle this; see
# http://effbot.org/imagingbook/decoder.htm#the-raw-decoder
return Image.frombytes(
"RGB", (width, height), pixel_bytes, "raw", "RGB", pb.get_rowstride(), 1
)
def backend_version(self):
import gi
return ".".join(map(str, gi.version_info))

View File

@ -0,0 +1,79 @@
import logging
from pyscreenshot.plugins.backend import UNKNOWN_VERSION, CBackend
from pyscreenshot.tempexport import read_func_img
log = logging.getLogger(__name__)
class GnomeDBusError(Exception):
pass
class GnomeDBusWrapper(CBackend):
name = "gnome_dbus"
is_subprocess = True
def grab(self, bbox=None):
im = read_func_img(self._grab_to_file, bbox)
return im
def _grab_to_file(self, filename, bbox=None):
has_jeepney = False
try:
# from jeepney import new_method_call
from jeepney.io.blocking import open_dbus_connection # type: ignore
from jeepney.wrappers import MessageGenerator # type: ignore
from jeepney.wrappers import new_method_call
has_jeepney = True
except ImportError:
pass
if not has_jeepney:
raise GnomeDBusError("jeepney library is missing")
class Screenshot(MessageGenerator):
interface = "org.gnome.Shell.Screenshot"
def __init__(
self,
object_path="/org/gnome/Shell/Screenshot",
bus_name="org.gnome.Shell.Screenshot",
):
super().__init__(object_path=object_path, bus_name=bus_name)
def Screenshot(self, include_cursor, flash, filename):
return new_method_call(
self, "Screenshot", "bbs", (include_cursor, flash, filename)
)
def ScreenshotArea(self, x, y, width, height, flash, filename):
return new_method_call(
self,
"ScreenshotArea",
"iiiibs",
(x, y, width, height, flash, filename),
)
# https://jeepney.readthedocs.io/en/latest/integrate.html
connection = open_dbus_connection(bus="SESSION")
dbscr = Screenshot()
if bbox:
msg = dbscr.ScreenshotArea(
bbox[0],
bbox[1],
bbox[2] - bbox[0],
bbox[3] - bbox[1],
False,
filename,
)
else:
msg = dbscr.Screenshot(False, False, filename)
reply = connection.send_and_get_reply(msg)
result = reply.body[0]
if not result:
raise GnomeDBusError("dbus error: %s %s" % (msg, result))
def backend_version(self):
return UNKNOWN_VERSION

View File

@ -0,0 +1,37 @@
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import read_prog_img
from pyscreenshot.util import extract_version
PROGRAM = "gnome-screenshot"
# https://gitlab.gnome.org/GNOME/gnome-screenshot/blob/master/src/screenshot-utils.c
# DBus is used for screenshot.
# If it doesn't succeed or $GNOME_SCREENSHOT_FORCE_FALLBACK is set then X DISPLAY is used.
# Flash effect! https://bugzilla.gnome.org/show_bug.cgi?id=672759
class GnomeScreenshotWrapper(CBackend):
"""Plugin for ``pyscreenshot`` that uses ``gnome-screenshot``
https://git.gnome.org/browse/gnome-screenshot/
This plugin can take screenshot when system is running Wayland.
Info: https://bugs.freedesktop.org/show_bug.cgi?id=98672#c1
"""
name = "gnome-screenshot"
is_subprocess = True
def grab(self, bbox=None):
im = read_prog_img([PROGRAM, "-f"])
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
p = EasyProcess([PROGRAM, "--version"])
p.enable_stdout_log = False
p.enable_stderr_log = False
p.call()
return extract_version(p.stdout.replace("-", " "))

View File

@ -0,0 +1,47 @@
"""
Backend for grim (https://github.com/emersion/grim), a Wayland screen tool for
environments other than Gnome and KDE, such as Sway.
"""
import logging
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import UNKNOWN_VERSION, CBackend
from pyscreenshot.tempexport import read_prog_img
log = logging.getLogger(__name__)
PROGRAM = "grim"
class GrimWrapper(CBackend):
name = "grim"
is_subprocess = True
def _bbox_to_grim_region(self, bbox):
"""
Translate pyscreenshot's bbox tuple convention of (x1, y1, x2, y2) to
grim's bbox convention, which is a string of the following format:
<x>,<y> <width>x<height>
"""
x1, y1, x2, y2 = bbox
width = x2 - x1
height = y2 - y1
return "{},{} {}x{}".format(x1, y1, width, height)
def grab(self, bbox=None):
if bbox:
# using grim's built-in cropping feature
region = self._bbox_to_grim_region(bbox)
return read_prog_img([PROGRAM, "-g", region])
return read_prog_img([PROGRAM])
def backend_version(self):
# grim doesn't have a version flag for some reason
p = EasyProcess([PROGRAM, "-help"])
p.enable_stdout_log = False
p.enable_stderr_log = False
p.call()
if p.return_code == 0:
return UNKNOWN_VERSION

View File

@ -0,0 +1,35 @@
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import read_prog_img
from pyscreenshot.util import extract_version, platform_is_osx
PROGRAM = "import"
# http://www.imagemagick.org/
class ImagemagickBackendError(Exception):
pass
class ImagemagickWrapper(CBackend):
name = "imagemagick"
is_subprocess = True
def grab(self, bbox=None):
if platform_is_osx():
raise ImagemagickBackendError("osx not supported")
command = [PROGRAM, "-silent", "-window", "root"]
if bbox:
pbox = "{}x{}+{}+{}".format(
bbox[2] - bbox[0], bbox[3] - bbox[1], bbox[0], bbox[1]
)
command += ["-crop", pbox]
im = read_prog_img(command)
return im
def backend_version(self):
stdout = EasyProcess([PROGRAM, "-version"]).call().stdout
s = stdout.splitlines()[0]
return extract_version(s.replace("-", " "))

View File

@ -0,0 +1,41 @@
import logging
import os
from easyprocess import EasyProcess
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import RunProgError
log = logging.getLogger(__name__)
PROGRAM = "ksnip"
class KsnipWrapper(CBackend):
name = "ksnip"
is_subprocess = True
def grab(self, bbox=None):
cmd = [PROGRAM, "--fullscreen", "--save"]
p = EasyProcess(cmd)
p.call()
if p.return_code != 0:
raise RunProgError(p.stderr)
lastline = p.stdout.splitlines()[-1]
if "Image Saved" not in lastline:
raise RunProgError(p.stderr)
filename = lastline.split()[-1]
im = Image.open(filename)
os.remove(filename)
# TODO: bbox param
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
for line in EasyProcess([PROGRAM, "--version"]).call().stderr.splitlines():
if "version" in line.lower():
return line.split()[-1]

View File

@ -0,0 +1,77 @@
import logging
import os
from PIL import Image
from pyscreenshot.plugins.backend import UNKNOWN_VERSION, CBackend
log = logging.getLogger(__name__)
class KdeDBusError(Exception):
pass
# https://gitlab.gnome.org/GNOME/gimp/-/issues/6626
# The org.kde.kwin.Screenshot interface is deprecated in KDE Plasma 5.22.
# "The process is not authorized to take a screenshot"
class KwinDBusWrapper(CBackend):
name = "kwin_dbus"
is_subprocess = True
def grab(self, bbox=None):
has_jeepney = False
try:
# from jeepney import new_method_call
from jeepney.io.blocking import open_dbus_connection # type: ignore
from jeepney.wrappers import MessageGenerator # type: ignore
from jeepney.wrappers import new_method_call
has_jeepney = True
except ImportError:
pass
if not has_jeepney:
raise KdeDBusError("jeepney library is missing")
class Screenshot(MessageGenerator):
interface = "org.kde.kwin.Screenshot"
def __init__(self, object_path="/Screenshot", bus_name="org.kde.KWin"):
super().__init__(object_path=object_path, bus_name=bus_name)
def screenshotFullscreen(self, captureCursor):
return new_method_call(
self, "screenshotFullscreen", "b", (captureCursor,)
)
def screenshotArea(self, x, y, width, height, captureCursor):
return new_method_call(
self,
"screenshotArea",
"iiiib",
(x, y, width, height, captureCursor),
)
# https://jeepney.readthedocs.io/en/latest/integrate.html
connection = open_dbus_connection(bus="SESSION")
dbscr = Screenshot()
# bbox not working:
# if bbox: msg = dbscr.screenshotArea(bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1], False)
msg = dbscr.screenshotFullscreen(False)
reply = connection.send_and_get_reply(msg)
filename = reply.body[0]
if not filename:
raise KdeDBusError()
im = Image.open(filename)
os.remove(filename)
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
return UNKNOWN_VERSION

View File

@ -0,0 +1,67 @@
# Javier Escalada Gomez
#
# from:
# https://stackoverflow.com/questions/4524723/take-screenshot-in-python-on-mac-os-x
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import read_func_img
class MacQuartzWrapper(CBackend):
name = "mac_quartz"
def grab(self, bbox=None):
im = read_func_img(self._grab_to_file, bbox)
return im
def _grab_to_file(self, filename, bbox=None, dpi=72):
# Should query dpi from somewhere, e.g for retina displays?
import LaunchServices # type: ignore
import Quartz # type: ignore
import Quartz.CoreGraphics as CG # type: ignore
from Cocoa import NSURL # type: ignore
if bbox:
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
region = CG.CGRectMake(bbox[0], bbox[1], width, height)
else:
region = CG.CGRectInfinite
# Create screenshot as CGImage
image = CG.CGWindowListCreateImage(
region,
CG.kCGWindowListOptionOnScreenOnly,
CG.kCGNullWindowID,
CG.kCGWindowImageDefault,
)
file_type = LaunchServices.kUTTypePNG
url = NSURL.fileURLWithPath_(filename)
dest = Quartz.CGImageDestinationCreateWithURL(
url,
file_type,
# 1 image in file
1,
None,
)
properties = {
Quartz.kCGImagePropertyDPIWidth: dpi,
Quartz.kCGImagePropertyDPIHeight: dpi,
}
# Add the image to the destination, characterizing the image with
# the properties dictionary.
Quartz.CGImageDestinationAddImage(dest, image, properties)
# When all the images (only 1 in this example) are added to the destination,
# finalize the CGImageDestination object.
Quartz.CGImageDestinationFinalize(dest)
def backend_version(self):
import objc # type: ignore
return objc.__version__

View File

@ -0,0 +1,38 @@
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import UNKNOWN_VERSION, CBackend
from pyscreenshot.tempexport import read_prog_img
from pyscreenshot.util import platform_is_osx
PROGRAM = "screencapture"
# https://ss64.com/osx/screencapture.html
# By default screenshots are saved as .png files,
class ScreencaptureError(Exception):
pass
class ScreencaptureWrapper(CBackend):
name = "mac_screencapture"
is_subprocess = True
def grab(self, bbox=None):
if not platform_is_osx():
raise ScreencaptureError("This backend runs only on Darwin")
command = [PROGRAM, "-x"]
if bbox:
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
command += ["-R{},{},{},{}".format(bbox[0], bbox[1], width, height)]
im = read_prog_img(command)
return im
def backend_version(self):
p = EasyProcess([PROGRAM, "-help"])
p.enable_stdout_log = False
p.enable_stderr_log = False
p.call()
if p.return_code == 0:
return UNKNOWN_VERSION

View File

@ -0,0 +1,30 @@
import logging
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import read_prog_img
from pyscreenshot.util import extract_version
log = logging.getLogger(__name__)
PROGRAM = "maim"
class MaimWrapper(CBackend):
name = "maim"
is_subprocess = True
def grab(self, bbox=None):
cmd = [PROGRAM, "--hidecursor"]
if bbox:
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
# https://github.com/naelstrof/maim/issues/119
cmd += ["-g", "{}x{}+{}+{}".format(width, height, bbox[0], bbox[1])]
im = read_prog_img(cmd)
return im
def backend_version(self):
return extract_version(EasyProcess([PROGRAM, "--version"]).call().stdout)

View File

@ -0,0 +1,50 @@
# import atexit
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.util import py_minor
# https://python-mss.readthedocs.io/examples.html
class MssError(Exception):
pass
sct = None
# not working without xrandr extension
# only bits_per_pixel == 32 is supported
class MssWrapper(CBackend):
name = "mss"
def grab(self, bbox=None):
if py_minor() < 5:
raise MssError()
import mss # type: ignore
# atexit.register(sct.close())
global sct
if not sct:
sct = mss.mss()
# with self.mss.mss() as sct:
if bbox:
monitor = bbox
else:
monitor = sct.monitors[0]
sct_img = sct.grab(monitor)
im = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
# The same, but less efficient:
# im = Image.frombytes('RGB', sct_img.size, sct_img.rgb)
return im
def backend_version(self):
import mss
return mss.__version__

View File

@ -0,0 +1,22 @@
from PIL import __version__
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.util import use_x_display
class PilWrapper(CBackend):
name = "pil"
def grab(self, bbox=None):
from PIL import ImageGrab
# https://pillow.readthedocs.io/en/stable/reference/ImageGrab.html
# On Linux, if xdisplay is None then gnome-screenshot will be used if it is installed.
# To capture the default X11 display instead, pass xdisplay=""
xdisplay = None
if use_x_display():
xdisplay = ""
return ImageGrab.grab(bbox, xdisplay=xdisplay)
def backend_version(self):
return __version__

View File

@ -0,0 +1,54 @@
import io
import logging
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
log = logging.getLogger(__name__)
class PySide2BugError(Exception):
pass
app = None
class PySide2GrabWindow(CBackend):
name = "pyside2"
def grab_to_buffer(self, buff, file_type="png"):
from PySide2 import QtCore, QtGui, QtWidgets # type: ignore
QApplication = QtWidgets.QApplication
QBuffer = QtCore.QBuffer
QIODevice = QtCore.QIODevice
QScreen = QtGui.QScreen
# QPixmap = self.PySide2.QtGui.QPixmap
global app
if not app:
app = QApplication([])
qbuffer = QBuffer()
qbuffer.open(QIODevice.ReadWrite)
QScreen.grabWindow(
QApplication.primaryScreen(), QApplication.desktop().winId()
).save(qbuffer, file_type)
# https://stackoverflow.com/questions/52291585/pyside2-typeerror-bytes-object-cannot-be-interpreted-as-an-integer
buff.write(qbuffer.data().data())
qbuffer.close()
def grab(self, bbox=None):
strio = io.BytesIO()
self.grab_to_buffer(strio)
strio.seek(0)
im = Image.open(strio)
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
import PySide2
return PySide2.__version__

View File

@ -0,0 +1,48 @@
import io
import logging
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
log = logging.getLogger(__name__)
# based on qt4 backend
app = None
class PySideGrabWindow(CBackend):
name = "pyside"
def grab_to_buffer(self, buff, file_type="png"):
from PySide import QtCore, QtGui # type: ignore
QApplication = QtGui.QApplication
QBuffer = QtCore.QBuffer
QIODevice = QtCore.QIODevice
QPixmap = QtGui.QPixmap
global app
if not app:
app = QApplication([])
qbuffer = QBuffer()
qbuffer.open(QIODevice.ReadWrite)
QPixmap.grabWindow(QApplication.desktop().winId()).save(qbuffer, file_type)
# https://stackoverflow.com/questions/52291585/pyside2-typeerror-bytes-object-cannot-be-interpreted-as-an-integer
buff.write(qbuffer.data().data())
qbuffer.close()
def grab(self, bbox=None):
strio = io.BytesIO()
self.grab_to_buffer(strio)
strio.seek(0)
im = Image.open(strio)
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
import PySide
return PySide.__version__

View File

@ -0,0 +1,49 @@
import io
import logging
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
log = logging.getLogger(__name__)
# based on:
# http://stackoverflow.com/questions/69645/take-a-screenshot-via-a-python-script-linux
app = None
class Qt4GrabWindow(CBackend):
name = "pyqt"
def grab_to_buffer(self, buff, file_type="png"):
from PyQt4 import Qt, QtGui # type: ignore
QApplication = QtGui.QApplication
QBuffer = Qt.QBuffer
QIODevice = Qt.QIODevice
QPixmap = QtGui.QPixmap
global app
if not app:
app = QApplication([])
qbuffer = QBuffer()
qbuffer.open(QIODevice.ReadWrite)
QPixmap.grabWindow(QApplication.desktop().winId()).save(qbuffer, file_type)
buff.write(qbuffer.data())
qbuffer.close()
def grab(self, bbox=None):
strio = io.BytesIO()
self.grab_to_buffer(strio)
strio.seek(0)
im = Image.open(strio)
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
from PyQt4 import Qt
return Qt.PYQT_VERSION_STR

View File

@ -0,0 +1,51 @@
import io
import logging
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
log = logging.getLogger(__name__)
# based on qt4 backend
app = None
class Qt5GrabWindow(CBackend):
name = "pyqt5"
# qt backends have conflict with each other in the same process.
def grab_to_buffer(self, buff, file_type="png"):
from PyQt5 import Qt, QtGui, QtWidgets # type: ignore
QApplication = QtWidgets.QApplication
QBuffer = Qt.QBuffer
QIODevice = Qt.QIODevice
QScreen = QtGui.QScreen
global app
if not app:
app = QApplication([])
qbuffer = QBuffer()
qbuffer.open(QIODevice.ReadWrite)
QScreen.grabWindow(
QApplication.primaryScreen(), QApplication.desktop().winId()
).save(qbuffer, file_type)
buff.write(qbuffer.data())
qbuffer.close()
def grab(self, bbox=None):
strio = io.BytesIO()
self.grab_to_buffer(strio)
strio.seek(0)
im = Image.open(strio)
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
from PyQt5 import Qt
return Qt.PYQT_VERSION_STR

View File

@ -0,0 +1,26 @@
import logging
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import read_prog_img
from pyscreenshot.util import extract_version
log = logging.getLogger(__name__)
PROGRAM = "scrot"
class ScrotWrapper(CBackend):
name = "scrot"
is_subprocess = True
def grab(self, bbox=None):
im = read_prog_img([PROGRAM, "--silent"])
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
return extract_version(EasyProcess([PROGRAM, "-version"]).call().stdout)

View File

@ -0,0 +1,62 @@
import logging
from PIL import Image
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.util import platform_is_osx
log = logging.getLogger(__name__)
# based on:
# http://stackoverflow.com/questions/69645/take-a-screenshot-via-a-python-script-linux
class WxBackendError(Exception):
pass
app = None
class WxScreen(CBackend):
name = "wx"
# conflict with pygdk3
# wx is never installed by default
# pygdk3 is default on Gnome
def grab(self, bbox=None):
if platform_is_osx():
raise WxBackendError("osx not supported")
import wx # type: ignore
global app
if not app:
app = wx.App()
screen = wx.ScreenDC()
size = screen.GetSize()
if wx.__version__ >= "4":
bmp = wx.Bitmap(size[0], size[1])
else:
bmp = wx.EmptyBitmap(size[0], size[1])
mem = wx.MemoryDC(bmp)
mem.Blit(0, 0, size[0], size[1], screen, 0, 0)
del mem
if hasattr(bmp, "ConvertToImage"):
myWxImage = bmp.ConvertToImage()
else:
myWxImage = wx.ImageFromBitmap(bmp)
im = Image.new("RGB", (myWxImage.GetWidth(), myWxImage.GetHeight()))
if hasattr(Image, "frombytes"):
# for Pillow
im.frombytes(bytes(myWxImage.GetData()))
else:
# for PIL
im.fromstring(myWxImage.GetData())
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
import wx
return wx.__version__

View File

@ -0,0 +1,49 @@
import logging
from easyprocess import EasyProcess
from pyscreenshot.plugins.backend import CBackend
from pyscreenshot.tempexport import RunProgError, read_func_img
from pyscreenshot.util import extract_version
log = logging.getLogger(__name__)
# wikipedia: https://en.wikipedia.org/wiki/Xwd
# xwd | xwdtopnm | pnmtopng > Screenshot.png
# xwdtopnm is buggy: https://bugs.launchpad.net/ubuntu/+source/netpbm-free/+bug/1379480
# solution : imagemagick convert
# xwd -root -display :0 | convert xwd:- file.png
# TODO: xwd sometimes grabs the wrong window so this backend will be not added now
PROGRAM = "xwd"
def read_xwd_img():
def run_prog(fpng, bbox=None):
fxwd = fpng + ".xwd"
pxwd = EasyProcess([PROGRAM, "-root", "-out", fxwd])
pxwd.call()
if pxwd.return_code != 0:
raise RunProgError(pxwd.stderr)
pconvert = EasyProcess(["convert", "xwd:" + fxwd, fpng])
pconvert.call()
if pconvert.return_code != 0:
raise RunProgError(pconvert.stderr)
im = read_func_img(run_prog)
return im
class XwdWrapper(CBackend):
name = "xwd"
is_subprocess = True
def grab(self, bbox=None):
im = read_xwd_img()
if bbox:
im = im.crop(bbox)
return im
def backend_version(self):
return extract_version(EasyProcess([PROGRAM, "-version"]).call().stdout)

View File

@ -0,0 +1,28 @@
import os.path
from tempfile import TemporaryDirectory
from easyprocess import EasyProcess
from PIL import Image
class RunProgError(Exception):
pass
def read_func_img(func, bbox=None):
with TemporaryDirectory(prefix="pyscreenshot") as tmpdirname:
filename = os.path.join(tmpdirname, "screenshot.png")
func(filename, bbox)
im = Image.open(filename)
return im
def read_prog_img(cmd):
def run_prog(filename, bbox=None):
p = EasyProcess(cmd + [filename])
p.call()
if p.return_code != 0:
raise RunProgError(p.stderr)
im = read_func_img(run_prog)
return im

View File

@ -0,0 +1,53 @@
import os
import sys
from easyprocess import EasyProcess
def py_minor():
return sys.version_info[1]
def platform_is_osx():
return sys.platform == "darwin"
def platform_is_win():
return sys.platform == "win32"
def platform_is_linux():
return sys.platform.startswith("linux")
def use_x_display():
if platform_is_win():
return False
if platform_is_osx():
return False
DISPLAY = os.environ.get("DISPLAY")
XDG_SESSION_TYPE = os.environ.get("XDG_SESSION_TYPE")
# Xwayland can not be used for screenshot
return DISPLAY and XDG_SESSION_TYPE != "wayland"
def extract_version(txt):
"""This function tries to extract the version from the help text of any
program."""
words = txt.replace(",", " ").split()
version = None
for x in reversed(words):
if len(x) > 2:
if x[0].lower() == "v":
x = x[1:]
if "." in x and x[0].isdigit():
version = x
break
return version
def run_mod_as_subproc(name, params=[]):
python = sys.executable
cmd = [python, "-m", name] + params
p = EasyProcess(cmd).call()
return p