MàJ du 2013-10-21 : support de python 3
MàJ du 2013-08-17 : correction de la segfault, voir
§ Erreurs ; meilleur gestion du serveur X
MàJ du 2013-08-16 : correction de la détermination des coordonnées et dimensions (merci à
Oros)
Afin de décrire plus en détails le fonctionnement du module
MSS, je vais présenter une manière de prendre une capture d'écran (ou des écrans) en passant par le module
ctypes. Il existe déjà un article qui
détaille le processus pour Windows.
La bonne façon de jouer avec ctypes, surtout pour ne pas avoir de surprises suivant les versions du système d'exploitation et des architectures, est de déclarer le type des arguments des fonctions ainsi que le type qu'elles retournent. Cela se fait grâce aux attributs
argtypes
et
restypes
respectivement.
GNU/Linux, ce n'est pas un système unique, donc il va falloir jouer sur plusieurs tableaux, charger les bonnes bibliothèques, etc. Ici, nous nous préoccuperons seulement de
Xlib, tout simplement parce qu'il est quasiment omniprésent, et ensuite parce que c'est ce qui gère mon système (Debian GNU/Linux jessie 64 bits + Mate). J'ai ajouté une
liste des choses à améliorer, j'y travaille activement.
Processus de l'impression écran sous GNU/Linux
Par défaut, vous vous retrouverez avec une image qui contient tous les écrans dans une seule capture. Ce fonctionnement est aussi celui par défaut lorsqu'on utilise la bibliothèque Xlib. Ce qu'il faudra mettre en place manuellement, c'est une capture par écran.
Voici le processus pour capturer
une zone du serveur X :
- connexion au serveur
- récupérer un ID vers la fenêtre racine
- récupérer une image de la zone désirée
- copier les pixels de le l'image
- enregistrer les pixels dans une image physique
Les mains dans le cambouis
Imports
Voici la liste des imports nécessaires :
# Pour la capture d'un seul écran, il faut lire un XML de l'utilisateur
from os.path import expanduser, isfile
import xml.etree.ElementTree as ET
# Pour la variable d'environnement $DISPLAY
from os import environ
from struct import pack
from ctypes.util import find_library
from ctypes import byref, cast, cdll
from ctypes import (
c_char_p, c_int, c_int32, c_uint, c_uint32,
c_ulong, c_void_p, POINTER, Structure
)
class Display(Structure):
pass
class XWindowAttributes(Structure):
_fields_ = [
("x", c_int32),
("y", c_int32),
("width", c_int32),
("height", c_int32),
("border_width", c_int32),
("depth", c_int32),
("visual", c_ulong),
("root", c_ulong),
("class", c_int32),
("bit_gravity", c_int32),
("win_gravity", c_int32),
("backing_store", c_int32),
("backing_planes", c_ulong),
("backing_pixel", c_ulong),
("save_under", c_int32),
("colourmap", c_ulong),
("mapinstalled", c_uint32),
("map_state", c_uint32),
("all_event_masks", c_ulong),
("your_event_mask", c_ulong),
("do_not_propagate_mask", c_ulong),
("override_redirect", c_int32),
("screen", c_ulong)
]
class XImage(Structure):
_fields_ = [
('width' , c_int),
('height' , c_int),
('xoffset' , c_int),
('format' , c_int),
('data' , c_char_p),
('byte_order' , c_int),
('bitmap_unit' , c_int),
('bitmap_bit_order' , c_int),
('bitmap_pad' , c_int),
('depth' , c_int),
('bytes_per_line' , c_int),
('bits_per_pixel' , c_int),
('red_mask' , c_ulong),
('green_mask' , c_ulong),
('blue_mask' , c_ulong)
]
def b(x):
return pack(b'
Initilisations
Par soucis de lisibilité et de maintenabilité, nous allons définir nos propres variables qui seront en fait des alias de fonctions.
# Chargement de la bibliothèque Xlib
x11 = find_library('X11')
if x11 is None:
raise OSError('MSSLinux: no X11 library found.')
else:
xlib = cdll.LoadLibrary(x11)
XOpenDisplay = xlib.XOpenDisplay
XDefaultScreen = xlib.XDefaultScreen
XDefaultRootWindow = xlib.XDefaultRootWindow
XGetWindowAttributes = xlib.XGetWindowAttributes
XAllPlanes = xlib.XAllPlanes
XGetImage = xlib.XGetImage
XGetPixel = xlib.XGetPixel
XCreateImage = xlib.XCreateImage
XFree = xlib.XFree
Pour plus d'informations sur une fonction, reportez-vous à sa
documentation en ligne.
Type d'arguments
Bon à savoir,
argtypes
ne peut être qu'une liste.
XOpenDisplay.argtypes = [c_char_p]
XDefaultScreen.argtypes = [POINTER(Display)]
XDefaultRootWindow.argtypes = [POINTER(Display)]
XGetWindowAttributes.argtypes = [POINTER(Display),
POINTER(XWindowAttributes), POINTER(XWindowAttributes)]
XAllPlanes.argtypes = []
XGetImage.argtypes = [POINTER(Display), POINTER(Display),
c_int, c_int, c_uint, c_uint, c_ulong, c_int]
XGetPixel.argtypes = [POINTER(XImage), c_int, c_int]
XCreateImage.argtypes = [POINTER(Display), POINTER(Display),
c_int, c_int, c_uint, c_uint, c_ulong, c_int]
XFree.argtypes = [POINTER(XImage)]
Type de fonction
Rien de bien difficile ici, juste des types, encore et toujours.
XOpenDisplay.restype = POINTER(Display)
XDefaultScreen.restype = c_int
XDefaultRootWindow.restype = POINTER(XWindowAttributes)
XGetWindowAttributes.restype = c_int
XAllPlanes.restype = c_ulong
XGetImage.restype = POINTER(XImage)
XGetPixel.restype = c_ulong
XCreateImage.restype = POINTER(XImage)
XFree.restype = c_void_p
C'est parti !
Informations des écrans
Parce qu'il faut bien commencer quelque part, récupérons les dimensions et coordonnées des écrans disponibles :
def enum_display_monitors(oneshot=False):
# Si oneshot est à True, alors on récupère les informations de tous
# les écrans d'un coup.
# Retourne une liste de dictionnaires contenant les informations
# des écran.
results = []
if oneshot:
gwa = XWindowAttributes()
XGetWindowAttributes(display, root_win, byref(gwa))
infos = {
b'left' : int(gwa.x),
b'top' : int(gwa.y),
b'width' : int(gwa.width),
b'height': int(gwa.height)
}
results.append(infos)
else:
# C'est un chouilla plus compliqué, nous devons trouver les infos
# dans le fichier ~/.config/monitors.xml, si présent. Je n'ai pas
# encore trouvé un moyen de le faire à l'aide de Xlib.
monitors = expanduser('~/.config/monitors.xml')
if not isfile(monitors):
print('MSSLinux: _enum_display_monitors() failed (no monitors.xml).')
return enum_display_monitors(oneshot=True)
conf = []
tree = ET.parse(monitors)
root = tree.getroot()
config = root.findall('configuration')[-1]
for output in config.findall('output'):
name = output.get('name')
if name != 'default':
x = output.find('x')
y = output.find('y')
width = output.find('width')
height = output.find('height')
rotation = output.find('rotation')
if None not in [x, y, width, height] and name not in conf:
conf.append(name)
if rotation.text == 'left' or rotation.text == 'right':
width, height = height, width
results.append({
b'left' : int(x.text),
b'top' : int(y.text),
b'width' : int(width.text),
b'height' : int(height.text),
b'rotation': rotation.text
})
return results
Exemple de fichier
~/.config/monitors.xml :
<configuration>
<clone>no</clone>
<output name="VGA-0">
<vendor>AIC</vendor>
<product>0x4191</product>
<serial>0x00000346</serial>
<width>1280</width>
<height>1024</height>
<rate>60</rate>
<x>0</x>
<y>0</y>
<rotation>normal</rotation>
<reflect_x>no</reflect_x>
<reflect_y>no</reflect_y>
<primary>yes</primary>
</output>
<output name="DVI-I-0">
</output>
<output name="HDMI-0">
</output>
<output name="DVI-I-1">
<vendor>AOC</vendor>
<product>0x2260</product>
<serial>0x0000051a</serial>
<width>1920</width>
<height>1080</height>
<rate>60</rate>
<x>1280</x>
<y>0</y>
<rotation>left</rotation>
<reflect_x>no</reflect_x>
<reflect_y>no</reflect_y>
<primary>no</primary>
</output>
</configuration>
Exemples de retour :
# Un seul écran :
[{'width': 1280, 'top': 0, 'height': 1024, 'left': 0}]
# Deux écrans :
[
{'width': 1280, 'top': 0, 'rotation': 'normal', 'height': 1024, 'left': 0},
{'width': 1920, 'top': 0, 'rotation': 'left', 'height': 1080, 'left': 1280}
]
# Deux écrans, oneshot=True :
[{'width': 2360, 'top': 0, 'height': 1920, 'left': 0}]
Récupération des pixels
C'est ici que sont traduites les étapes du
§ Processus de l'impression écran sous GNU/Linux.
def get_pixels(monitor):
# Récupérer les pixels d'un écran.
width, height = monitor[b'width'], monitor[b'height']
left, top = monitor[b'left'], monitor[b'top']
ZPixmap = 2
allplanes = XAllPlanes()
ZPixmap = 2
# Fix pour XGetImage: expected LP_Display instance instead of LP_XWindowAttributes
root = cast(root_win, POINTER(Display))
image = XGetImage(display, root, left, top, width, height, allplanes, ZPixmap)
if image is None:
raise ValueError('MSSLinux: XGetImage() failed.')
pixels = [b'0'] * (3 * width * height)
for x in range(width):
for y in range(height):
pixel = XGetPixel(image, x, y)
blue = pixel & 255
green = (pixel & 65280) >> 8
red = (pixel & 16711680) >> 16
offset = (x + width * y) * 3
pixels[offset:offset+3] = b(red), b(green), b(blue)
XFree(image)
return b''.join(pixels)
/!\ Dans le code, aux lignes de calcul des pixels
r, g, b
, il y a un
&
qui traine, il faut le remplacer par
&
.
Boucle générale
Parce qu'il serait trop long d'inclure la classe
MSSImage
(qui permettra de se passer de module tierce pour la sauvegarde des images), nous utiliserons
Pillow pour enregistrer l'image dans un fichier.
if __name__ == '__main__':
# Utilisation de Pillow (ou PIL) pour enregistrer l'image.
# MSSImage est prêt mais trop long à inclure ici.
from PIL import Image, ImageFile
def pil_save(filename, width, height):
buffer_len = (width * 3 + 3) & -4
img = Image.frombuffer('RGB', (width, height), pixels, 'raw',
'RGB', buffer_len, 1)
ImageFile.MAXBLOCK = width * height
img.save(filename, quality=95, optimize=True, progressive=True)
print('Fichier {0} créé.'.format(filename))
# Une capture par écran
i = 1
for monitor in enum_display_monitors():
pixels = get_pixels(monitor)
filename = 'mss-capture-{0}.jpg'.format(i)
pil_save(filename, monitor['width'], monitor['height'])
i += 1
# Capture complète
monitor = enum_display_monitors(oneshot=True)[0]
pixels = get_pixels(monitor)
filename = 'mss-capture-complet.jpg'
pil_save(filename, monitor['width'], monitor['height'])
/!\ Dans le code, à la ligne de définition de
buffer_len
, il y a un
&
qui traine, il faut le remplacer par
&
.
Voici le
script complet, et la
capture d'écran c'est cadeau :)
Erreurs
Si vous prenez une machine par SSH, pensez à activé le transfert de X11, grâce à l'option
-X
. Sinon, vous tomberez sur une faute de segmentation.
Bonus
Juste pour le fun, j'ai mis en place une
galerie d'images sans prétention afin d'exposer les captures d'écran
oneshot que vous me ferez parvenir ☺
Remarques
Le code présenté ne peut, dans l'immédiat, être certain de fonctionner suivant votre configuration. Je sais, c'est moche de présenter un code bancal, mais c'est un 1
er jet et ça devrait être correct pour la plupart d'entre vous. Pour ceux et celles qui sont motivés, voici quelques points à améliorer (le code est simple, passez par
GitHub) :
- c'est lent, très lent, trop lent à mon goût : il faudrait pouvoir se passer de la boucle du
XGetPixel()
, voir si on peut copier/coller directement une zone de l'écran vers un fichier physique (JPEG/PNG)
- capturer le bureau virtuel actif OK
- comprendre pourquoi il y a une faute de segmentation (aléatoire ?) sur les systèmes 32 bits OK, voir § Erreurs
- supporter python 3 OK
- supporter XCB
- qu'en est-il de Wayland ?
- autres ?
Sources diverses