PROJET AUTOBLOG


Tiger-222

Site original : Tiger-222

⇐ retour index

GNU/Linux & python pur : capture d'écran

mercredi 14 août 2013 à 15:39
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  :

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 &amp; 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 &amp; 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 1er 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) :


Sources diverses


Windows 7 Starter & fond d'écran

jeudi 8 août 2013 à 18:38

Qui a la plus grosse


Dans la série « Je suis Microsoft le géant, je pense pouvoir faire ce que je veux, donc je fais n'importe quoi ! », j'ai, à ma grande surprise, constaté qu'il n'est pas possible de changer de fond d'écran sur les versions de Windows 7 Starter et Starter N...
Pourquoi cette tragique disparition ? Nous ne le saurons peut-être jamais. Cependant, grâce aux efforts de bidouilleurs, nos sauveurs, il existe quelques utilitaires qui corrigent le tir.


Solution


Je me suis attardé sur Starter Change Background, écrit par un français, et qui fait parfaitement l'affaire :


L'installation est simplissime, son fonctionnement l'est tout aussi. Pour accéder au gestionnaire d'arrière plan :

L'ordinal 459...


Si, lorsque vous lancez le programme, vous avez ce joli message récurrent pas vraiment compréhensible :
L'ordinal 459 est introuvable dans la bibliothèque de liens dynamiques urlmon.dll.

C'est qu'il y a anguille sous roche ! Cela m'est arrivé parce que j'avais désinstallé ce maudit Internet Explorer. J'ai du le réinstaller, puis faire les mise à jour à l'aide de Windows Update. Je n'en suis pas peu fier...

Windows 7 Starter & fond d'écran

jeudi 8 août 2013 à 16:38

Qui a la plus grosse


Dans la série « Je suis Microsoft le géant, je pense pouvoir faire ce que je veux, donc je fais n'importe quoi ! », j'ai, à ma grande surprise, constaté qu'il n'est pas possible de changer de fond d'écran sur les versions de Windows 7 Starter et Starter N...
Pourquoi cette tragique disparition ? Nous ne le saurons peut-être jamais. Cependant, grâce aux efforts de bidouilleurs, nos sauveurs, il existe quelques utilitaires qui corrigent le tir.


Solution


Je me suis attardé sur Starter Change Background, écrit par un français, et qui fait parfaitement l'affaire :


L'installation est simplissime, son fonctionnement l'est tout aussi. Pour accéder au gestionnaire d'arrière plan :

L'ordinal 459...


Si, lorsque vous lancez le programme, vous avez ce joli message récurrent pas vraiment compréhensible :
L'ordinal 459 est introuvable dans la bibliothèque de liens dynamiques urlmon.dll.

C'est qu'il y a anguille sous roche ! Cela m'est arrivé parce que j'avais désinstallé ce maudit Internet Explorer. J'ai du le réinstaller, puis faire les mise à jour à l'aide de Windows Update. Je n'en suis pas peu fier...

Windows & python pur : capture d'écran

lundi 5 août 2013 à 23:35

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 les ctypes.
Vite fait pour vous mettre dans le bain, ctypes est un module qui permet d'accéder aux fonctions et symboles d'une bibliothèque externe (la plupart du temps, codées en C ou C++). Du coup, on peut accéder aux fonctions du système hôte assez aisément. Cela va nous servir à imiter tout le processus effectué lorsque l'on appuie sur la touche Imp écr Syst (ou PrtScrn).

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.


Processus de l'impression écran sous Windows


Par défaut, vous vous retrouverez avec une image qui contient tous les écrans dans une seule capture. Ce fonctionnement, nous devrons le mettre en place manuellement, sinon nous nous retrouverons avec une capture de l'écran principal seulement.

Il vous faudra appréhender quelques termes techniques dont :
Sans plus tarder, le processus pour capturer un seul écran :
Pour prendre une capture d'un autre écran, le processus est le même, il suffira d'adapter les coordonnées et dimensions lors du transfert des pixels. Idem pour prendre une capture de tous les écrans d'un coup.


Les mains dans le cambouis


Imports


Voici la liste des imports nécessaires ainsi que la définition des structures pour le bitmap :
from ctypes import byref, memset, pointer, sizeof, windll
from ctypes.wintypes import (
c_void_p as LPRECT,
c_void_p as LPVOID,
create_string_buffer,
Structure,
BOOL,
DOUBLE,
DWORD,
HANDLE,
HBITMAP,
HDC,
HGDIOBJ,
HWND,
INT,
LPARAM,
LONG,
POINTER,
RECT,
SHORT,
UINT,
WINFUNCTYPE,
WORD
)

class BITMAPINFOHEADER(Structure):
_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):
_fields_ = [
('bmiHeader', BITMAPINFOHEADER),
('bmiColors', DWORD * 3)
]
BITMAPINFOHEADER est une structure qui défini les entêtes du fichier bitmap, BITMAPINFO en est une autre qui contient une image complète (entêtes + données).


Initilisations


Par soucis de lisibilité et de maintenabilité, nous allons définir nos propres variables qui seront en fait des alias de fonctions.
SM_XVIRTUALSCREEN = 76  # Coordonnée gauche *
SM_YVIRTUALSCREEN = 77 # Coordonnée haute *
SM_CXVIRTUALSCREEN = 78 # Largeur en pixels *
SM_CYVIRTUALSCREEN = 79 # Hauteur en pixels *
SRCCOPY = 0xCC0020 # Code de copie pour la fonction BitBlt()
DIB_RGB_COLORS = 0

GetSystemMetrics = windll.user32.GetSystemMetrics
EnumDisplayMonitors = windll.user32.EnumDisplayMonitors
GetWindowDC = windll.user32.GetWindowDC
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
CreateCompatibleBitmap = windll.gdi32.CreateCompatibleBitmap
SelectObject = windll.gdi32.SelectObject
BitBlt = windll.gdi32.BitBlt
GetDIBits = windll.gdi32.GetDIBits
DeleteObject = windll.gdi32.DeleteObject
* Il s'agit des coordonnées et dimensions de l'écran virtuel, celui qui contient tous les écrans réunis.
Pour plus d'informations sur une fonction, reportez-vous à sa documentation sur le Windows Dev Center (MSDN).


Type d'arguments


Bon à savoir, argtypes ne peut être qu'une liste.
MONITORENUMPROC = WINFUNCTYPE(INT, DWORD, DWORD,
POINTER(RECT), DOUBLE)
GetSystemMetrics.argtypes = [INT]
EnumDisplayMonitors.argtypes = [HDC, LPRECT, MONITORENUMPROC, LPARAM]
GetWindowDC.argtypes = [HWND]
CreateCompatibleDC.argtypes = [HDC]
CreateCompatibleBitmap.argtypes = [HDC, INT, INT]
SelectObject.argtypes = [HDC, HGDIOBJ]
BitBlt.argtypes = [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD]
DeleteObject.argtypes = [HGDIOBJ]
GetDIBits.argtypes = [HDC, HBITMAP, UINT, UINT, LPVOID,
POINTER(BITMAPINFO), UINT]
MONITORENUMPROC() est une fonction de rappel (callback) pour l'énumération des écrans disponibles. Elle permet de récupérer les dimensions, coordonnées et ID de chaque écran ou de tous à la fois.


Type de fonction


Rien de bien difficile ici, juste des types, encore et toujours.
GetSystemMetrics.restypes = INT
EnumDisplayMonitors.restypes = BOOL
GetWindowDC.restypes = HDC
CreateCompatibleDC.restypes = HDC
CreateCompatibleBitmap.restypes = HBITMAP
SelectObject.restypes = HGDIOBJ
BitBlt.restypes = BOOL
GetDIBits.restypes = INT
DeleteObject.restypes = BOOL


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 écrans.

def _callback(monitor, dc, rect, data):
rct = rect.contents
results.append({
b'left'  : int(rct.left),
b'top'  : int(rct.top),
b'width' : int(rct.right - rct.left),
b'height': int(rct.bottom -rct.top)
})
return 1

results = []
if oneshot:
left = GetSystemMetrics(SM_XVIRTUALSCREEN)
right = GetSystemMetrics(SM_CXVIRTUALSCREEN)
top = GetSystemMetrics(SM_YVIRTUALSCREEN)
bottom = GetSystemMetrics(SM_CYVIRTUALSCREEN)
results.append({
b'left'  : int(left),
b'top'  : int(top),
b'width' : int(right - left),
b'height': int(bottom - top)
})
else:
callback = MONITORENUMPROC(_callback)
EnumDisplayMonitors(0, 0, callback, 0)
return results
Exemples de retour :
# Un seul écran :
[{'width': 1024, 'top': 0, 'height': 768, 'left': 0}]

# Deux écrans :
[
{'width': 1366, 'top': 0, 'height': 768, 'left': 0},
{'width': 1280, 'top': 0, 'height': 1024, 'left': 1366}
]

# Deux écrans, oneshot=True :
[{'width': 2646, 'top': 0, 'height': 1024, 'left': 0}]


Récupération des pixels


C'est ici que sont traduites les étapes du § Processus de l'impression écran sous Windows.
def get_pixels(monitor):
# Récupérer les pixels d'un écran.

width, height = monitor['width'], monitor['height']
left, top = monitor['left'], monitor['top']

# Récupérer un ID du DC principal
srcdc = GetWindowDC(0)
# Récupérer un ID vers un DC de la partie cliente
memdc = CreateCompatibleDC(srcdc)
# Créer un bitmap compatible
bmp = CreateCompatibleBitmap(srcdc, width, height)
# Sélection du bitmap nouvellement créé
SelectObject(memdc, bmp)
# Copie des pixels du DC principal vers le DC de la partie cliente ;
# c'est ici qu'on spécifie les coordonnées et la taille de la capture.
BitBlt(memdc, 0, 0, width, height, srcdc, left, top, SRCCOPY)
# Nouvelle image BMP, remplissage des informations
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = width
bmi.bmiHeader.biHeight = height
bmi.bmiHeader.biBitCount = 24
bmi.bmiHeader.biPlanes = 1
# Allocation d'un buffer pour le transfert des données
buffer_len = height * ((width * 3 + 3) & -4)
pixels = create_string_buffer(buffer_len)
# Récupération des données brutes (pixels) du DC de la partie cliente
# vers l'image BMP
bits = GetDIBits(memdc, bmp, 0, height, byref(pixels),
pointer(bmi), DIB_RGB_COLORS)

# Un peu de ménage
DeleteObject(srcdc)
DeleteObject(memdc)
DeleteObject(bmp)

# Vérifions que tout s'est bien passé
if bits != height or len(pixels.raw) != buffer_len:
raise ValueError('MSSWindows: GetDIBits() failed.')

return pixels.raw
/!\ Dans le code, à la ligne de définition de buffer_len, il y a un &amp; qui traine, il faut le remplacer par &.

Spécialité de Microsoft oblige, le début de l'image est en bas à gauche et les pixels sont dans l'ordre BGR... Ne cherchez pas une logique là-dedans o_Ô.


Boucle générale


Parce que je n'ai pas encore terminé 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 &amp; qui traine, il faut le remplacer par &.

Dans la fonction Image.frombuffer(), c'est le dernier argument qui permet de spécifier que le début de l'image est en bas à gauche (-1).

Voici le script complet, et la capture d'écran c'est cadeau :)


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 ☺


Sources diverses


Windows & python pur : capture d'écran

lundi 5 août 2013 à 21:35

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 les ctypes.
Vite fait pour vous mettre dans le bain, ctypes est un module qui permet d'accéder aux fonctions et symboles d'une bibliothèque externe (la plupart du temps, codées en C ou C++). Du coup, on peut accéder aux fonctions du système hôte assez aisément. Cela va nous servir à imiter tout le processus effectué lorsque l'on appuie sur la touche Imp écr Syst (ou PrtScrn).

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.


Processus de l'impression écran sous Windows


Par défaut, vous vous retrouverez avec une image qui contient tous les écrans dans une seule capture. Ce fonctionnement, nous devrons le mettre en place manuellement, sinon nous nous retrouverons avec une capture de l'écran principal seulement.

Il vous faudra appréhender quelques termes techniques dont :
Sans plus tarder, le processus pour capturer un seul écran :
Pour prendre une capture d'un autre écran, le processus est le même, il suffira d'adapter les coordonnées et dimensions lors du transfert des pixels. Idem pour prendre une capture de tous les écrans d'un coup.


Les mains dans le cambouis


Imports


Voici la liste des imports nécessaires ainsi que la définition des structures pour le bitmap :
from ctypes import byref, memset, pointer, sizeof, windll
from ctypes.wintypes import (
c_void_p as LPRECT,
c_void_p as LPVOID,
create_string_buffer,
Structure,
BOOL,
DOUBLE,
DWORD,
HANDLE,
HBITMAP,
HDC,
HGDIOBJ,
HWND,
INT,
LPARAM,
LONG,
POINTER,
RECT,
SHORT,
UINT,
WINFUNCTYPE,
WORD
)

class BITMAPINFOHEADER(Structure):
_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):
_fields_ = [
('bmiHeader', BITMAPINFOHEADER),
('bmiColors', DWORD * 3)
]
BITMAPINFOHEADER est une structure qui défini les entêtes du fichier bitmap, BITMAPINFO en est une autre qui contient une image complète (entêtes + données).


Initilisations


Par soucis de lisibilité et de maintenabilité, nous allons définir nos propres variables qui seront en fait des alias de fonctions.
SM_XVIRTUALSCREEN = 76  # Coordonnée gauche *
SM_YVIRTUALSCREEN = 77 # Coordonnée haute *
SM_CXVIRTUALSCREEN = 78 # Largeur en pixels *
SM_CYVIRTUALSCREEN = 79 # Hauteur en pixels *
SRCCOPY = 0xCC0020 # Code de copie pour la fonction BitBlt()
DIB_RGB_COLORS = 0

GetSystemMetrics = windll.user32.GetSystemMetrics
EnumDisplayMonitors = windll.user32.EnumDisplayMonitors
GetWindowDC = windll.user32.GetWindowDC
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
CreateCompatibleBitmap = windll.gdi32.CreateCompatibleBitmap
SelectObject = windll.gdi32.SelectObject
BitBlt = windll.gdi32.BitBlt
GetDIBits = windll.gdi32.GetDIBits
DeleteObject = windll.gdi32.DeleteObject
* Il s'agit des coordonnées et dimensions de l'écran virtuel, celui qui contient tous les écrans réunis.
Pour plus d'informations sur une fonction, reportez-vous à sa documentation sur le Windows Dev Center (MSDN).


Type d'arguments


Bon à savoir, argtypes ne peut être qu'une liste.
MONITORENUMPROC = WINFUNCTYPE(INT, DWORD, DWORD,
POINTER(RECT), DOUBLE)
GetSystemMetrics.argtypes = [INT]
EnumDisplayMonitors.argtypes = [HDC, LPRECT, MONITORENUMPROC, LPARAM]
GetWindowDC.argtypes = [HWND]
CreateCompatibleDC.argtypes = [HDC]
CreateCompatibleBitmap.argtypes = [HDC, INT, INT]
SelectObject.argtypes = [HDC, HGDIOBJ]
BitBlt.argtypes = [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD]
DeleteObject.argtypes = [HGDIOBJ]
GetDIBits.argtypes = [HDC, HBITMAP, UINT, UINT, LPVOID,
POINTER(BITMAPINFO), UINT]
MONITORENUMPROC() est une fonction de rappel (callback) pour l'énumération des écrans disponibles. Elle permet de récupérer les dimensions, coordonnées et ID de chaque écran ou de tous à la fois.


Type de fonction


Rien de bien difficile ici, juste des types, encore et toujours.
GetSystemMetrics.restypes = INT
EnumDisplayMonitors.restypes = BOOL
GetWindowDC.restypes = HDC
CreateCompatibleDC.restypes = HDC
CreateCompatibleBitmap.restypes = HBITMAP
SelectObject.restypes = HGDIOBJ
BitBlt.restypes = BOOL
GetDIBits.restypes = INT
DeleteObject.restypes = BOOL


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 écrans.

def _callback(monitor, dc, rect, data):
rct = rect.contents
results.append({
b'left'  : int(rct.left),
b'top'  : int(rct.top),
b'width' : int(rct.right - rct.left),
b'height': int(rct.bottom -rct.top)
})
return 1

results = []
if oneshot:
left = GetSystemMetrics(SM_XVIRTUALSCREEN)
right = GetSystemMetrics(SM_CXVIRTUALSCREEN)
top = GetSystemMetrics(SM_YVIRTUALSCREEN)
bottom = GetSystemMetrics(SM_CYVIRTUALSCREEN)
results.append({
b'left'  : int(left),
b'top'  : int(top),
b'width' : int(right - left),
b'height': int(bottom - top)
})
else:
callback = MONITORENUMPROC(_callback)
EnumDisplayMonitors(0, 0, callback, 0)
return results
Exemples de retour :
# Un seul écran :
[{'width': 1024, 'top': 0, 'height': 768, 'left': 0}]

# Deux écrans :
[
{'width': 1366, 'top': 0, 'height': 768, 'left': 0},
{'width': 1280, 'top': 0, 'height': 1024, 'left': 1366}
]

# Deux écrans, oneshot=True :
[{'width': 2646, 'top': 0, 'height': 1024, 'left': 0}]


Récupération des pixels


C'est ici que sont traduites les étapes du § Processus de l'impression écran sous Windows.
def get_pixels(monitor):
# Récupérer les pixels d'un écran.

width, height = monitor['width'], monitor['height']
left, top = monitor['left'], monitor['top']

# Récupérer un ID du DC principal
srcdc = GetWindowDC(0)
# Récupérer un ID vers un DC de la partie cliente
memdc = CreateCompatibleDC(srcdc)
# Créer un bitmap compatible
bmp = CreateCompatibleBitmap(srcdc, width, height)
# Sélection du bitmap nouvellement créé
SelectObject(memdc, bmp)
# Copie des pixels du DC principal vers le DC de la partie cliente ;
# c'est ici qu'on spécifie les coordonnées et la taille de la capture.
BitBlt(memdc, 0, 0, width, height, srcdc, left, top, SRCCOPY)
# Nouvelle image BMP, remplissage des informations
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = width
bmi.bmiHeader.biHeight = height
bmi.bmiHeader.biBitCount = 24
bmi.bmiHeader.biPlanes = 1
# Allocation d'un buffer pour le transfert des données
buffer_len = height * ((width * 3 + 3) & -4)
pixels = create_string_buffer(buffer_len)
# Récupération des données brutes (pixels) du DC de la partie cliente
# vers l'image BMP
bits = GetDIBits(memdc, bmp, 0, height, byref(pixels),
pointer(bmi), DIB_RGB_COLORS)

# Un peu de ménage
DeleteObject(srcdc)
DeleteObject(memdc)
DeleteObject(bmp)

# Vérifions que tout s'est bien passé
if bits != height or len(pixels.raw) != buffer_len:
raise ValueError('MSSWindows: GetDIBits() failed.')

return pixels.raw
/!\ Dans le code, à la ligne de définition de buffer_len, il y a un &amp; qui traine, il faut le remplacer par &.

Spécialité de Microsoft oblige, le début de l'image est en bas à gauche et les pixels sont dans l'ordre BGR... Ne cherchez pas une logique là-dedans o_Ô.


Boucle générale


Parce que je n'ai pas encore terminé 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 &amp; qui traine, il faut le remplacer par &.

Dans la fonction Image.frombuffer(), c'est le dernier argument qui permet de spécifier que le début de l'image est en bas à gauche (-1).

Voici le script complet, et la capture d'écran c'est cadeau :)


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 ☺


Sources diverses