☠ 2018-05-06 : cette page n'est plus vraiment d'actualité. L'article est en cours de réécriture, en attendant, reportez-vous au dépôt officiel.

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 utilisant seulement du python natif, à l'aide du module Quartz. Je ferai certainement des tests avec le module ctypes plus tard, mais ça ne sera pas mieux que ce qui est présenté dans cet article (plus long et fastidieux, ceci dit c'est intéressant pour le côté technique). Il existe déjà des articles qui détaillent le processus pour GNU/Linux et Windows.

Les tests ont été effectués sous Mac OS X Lion sur Virtualbox et Mac OS X Mountain Lion (merci à Eownis). D'après la documentation officielle, le script présenté ici devrait être fonctionnel sur quasiment toutes les déclinaisons de Mac OS X, voire même iOS (je m'avance peut-être, n'étant pas un fervent adepte d'Apple, je manque cruellement de matériel).


Imports

Voici la liste des imports nécessaires :

from Quartz import *

# Nécessaire pour l'enregistrement de la capture dans un fichier.
# Ici, on utilise le PNG, mais d'autres formats existent [UTType]
from LaunchServices import kUTTypePNG

[UTType] Dans le document UTType Reference, on peut trouver, entre autres, le type JPEG (kUTTypeJPEG) ou encore GIF (kUTTypeGIF).


Informations des écrans

Parce qu'il faut bien commencer quelque part, récupérons les dimensions et coordonnées des écrans disponibles. Le nom des fonctions est assez explicite :

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:
        rect = CGRectInfinite
        results.append({
            b'left'    : int(rect.origin.x),
            b'top'     : int(rect.origin.y),
            b'width'   : int(rect.size.width),
            b'height'  : int(rect.size.height)
        })
    else:
        max_displays = 32  # Peut-être augmenté, si besoin...
        rotations = {0.0: 'normal', 90.0: 'right', -90.0: 'left'}
        res, ids, count = CGGetActiveDisplayList(max_displays, None, None)
        for display in ids:
            rect = CGRectStandardize(CGDisplayBounds(display))
            left, top = rect.origin.x, rect.origin.y
            width, height = rect.size.width, rect.size.height
            
            # La fonction suivante retourne un double pour exprimer la
            # rotation de l'écran : 0.0, 90.0, -90.0, ... On le traduit
            # par normal, left ou right, plus compréhensible.
            rot = CGDisplayRotation(display)
            rotation = rotations[rot]
            if rotation in ['left', 'right']:
                width, height = height, width
            results.append({
                b'left'    : int(left),
                b'top'     : int(top),
                b'width'   : int(width),
                b'height'  : int(height),
                b'rotation': rotation
            })
    return results

Si onshot=True, nous utilisons CGRectInfinite qui renvoie des données assez... monstrueuses ! Je vous laisse tester pour voir ce que ça donne, ça n'est pas utilisable en l'état. Mais une option dans la fonction suivante permettra de rectifier le tir.

Exemples de retour :

# Un seul écran :
[{'width': 1280, 'top': 0, 'height': 800, 'left': 0}]

# Deux écrans :
[
	{'width': 1280, 'top': 0, 'rotation': 'normal', 'height': 800, 'left': 0},
	{'width': 1360, 'top': 32, 'rotation': 'normal', 'height': 768, 'left': -1360}
]

# Deux écrans, oneshot=True :
[{'width': 179769..., 'top': -89884..., 'height': 179769..., 'left': -89884...}]

Récupération des pixels et enregistrement

Le module Quartz est vraiment cool à utiliser, voyez comme il est facile de récupérer les données de l'écran et les enregistrer dans un fichier PNG :

def get_pixels_and_save(monitor, filename):
    # Récupérer les pixels d'un écran, puis enregistrement.

    width, height = monitor[b'width'], monitor[b'height']
    left, top = monitor[b'left'], monitor[b'top']
    dpi = 72

    # Récupération des données brutes
    rect = CGRect((left, top), (width, height))
    image = CGWindowListCreateImage(
                rect, kCGWindowListOptionOnScreenOnly,
                kCGNullWindowID, kCGWindowImageDefault)

    # Enregistrement
    url = NSURL.fileURLWithPath_(filename)
    dest = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, None)
    properties = {
        kCGImagePropertyDPIWidth: dpi,
        kCGImagePropertyDPIHeight: dpi,
    }
    CGImageDestinationAddImage(dest, image, properties)
    if CGImageDestinationFinalize(dest):
        print('Fichier {0} créé.'.format(filename))

L'option kCGWindowListOptionOnScreenOnly permet de ne créer une image que de ce qui est affiché à l'écran. Elle permet de ne pas avoir une image improbable lorsqu'on récupère les dimensions via CGRectInfinite.


Boucle générale

Plus court, tu meurs :

if __name__ == '__main__':
    # Une capture par écran
    i = 1
    for monitor in enum_display_monitors():
        filename = 'mss-capture-{0}.png'.format(i)
        get_pixels_and_save(monitor, filename)
        i += 1

    # Capture complète
    monitor = enum_display_monitors(oneshot=True)[0]
    filename = 'mss-capture-complet.png'
    get_pixels_and_save(monitor, filename)

Voici le script complet, et la capture d'écran (merci à Eownis).


Sources :