Explication de code : python-mss


Ca faisait longtemps qu’on avait pas eu une petite explication de code dans le cadre de notre politique “envoyez nous les codes que vous ne pigez pas“.

Celle-ci est un peu particulière.

D’abord parce qu’elle traine dans la boîte mail depuis un siècle ou deux. Je pense que l’auteur de la demande n’en a plus besoin…

Ensuite parce que le code est assez complexe, notamment à cause de l’utilisateur d’API C. Donc si vous n’avez pas des notions de C, vous n’allez rien piger. En effet je ne vais pas expliquer les bases de C ou de Python, ce n’est pas un tuto, donc il me faut des prérequis. Je vous invite quand même à vous rafraichir la mémoire en utilisant notre introduction au module ctypes car il est massivement utilisé dans ce code.

Exceptionnellement, je ne vais pas pondre de version alternative car :

  • le code est très très long, et j’ai déjà passé mon samedi matin à écrire cet article.
  • je n’ai pas de mac pour tester cette partie.

Comme d’habitude, je vais faire des remarques parfois critiques sur le code, mais mon intention n’est bien entendu pas de faire du tord à l’auteur. C’est pédagogique pour les lecteurs. D’ailleurs ce bout de code est assez impressionnant et a du demander des heures et des heures de travail tant au niveau du code que de la recherche d’information. J’insiste donc sur mon respect pour l’auteur. On est pas sur bashfr.

Je tiens tout de même à prévenir que la lib ne fonctionne pas sur ma machine, plante sur certains appels, ou produits des screenshots illisibles. Je ne doute néanmoins pas de la compétence de l’auteur, ce qu’il essaye de faire est vraiment compliqué, et ne pense que Python n’est pas son premier langage.

On m’a signalé dans le twitcoutuer qu’il manque de la zik :

Normalement le but de la lib est de permettre de faire des screenshots en pure Python sous Windows, Linux et Mac. Exemple de code sous Linux :

>>> from mss import MSSLinux
>>> mss = MSSLinux()
>>> screnshots = mss.save(output='/tmp/screenshots', oneshot=True)
>>> list(screnshots)
[u'/tmp/screenshots-full.png']

Et voici le code commenté. C’est un gros morceau, et bien complexes, avec des notions parfois que je ne maitrise pas. J’ai pu faire des erreurs et dire des bêtises. Si l’auteur passe par là, il a bien entendu un droit de réponse.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
''' A cross-platform multi-screen shot module in pure python using ctypes.
 
    This module is maintained by Mickaël Schoentgen <contact@tiger-222.fr>.
    If you find problems, please submit bug reports/patches via the
    GitHub issue tracker (https://github.com/BoboTiG/python-mss/issues).
 
    Note: please keep this module compatible to Python 2.6.
 
    Still needed:
    * support for additional systems
 
    Many thanks to all those who helped (in no particular order):
 
      Oros, Eownis
 
    History:
 
    <see Git checkin messages for history>
 
    0.0.1 - first release
    0.0.2 - add support for python 3 on Windows and GNU/Linux
    0.0.3 - MSSImage: remove PNG filters
          - MSSImage: remove 'ext' argument, using only PNG
          - MSSImage: do not overwrite existing image files
          - MSSImage: few optimizations into png()
          - MSSLinux: few optimizations into get_pixels()
    0.0.4 - MSSLinux: use of memoization => huge time/operations gains
    0.0.5 - MSSWindows: few optimizations into _arrange()
          - MSSImage: code simplified
 
    You can always get the latest version of this module at:
 
            https://raw.github.com/BoboTiG/python-mss/master/mss.py
 
    If that URL should fail, try contacting the author.
'''
 
from __future__ import (unicode_literals, absolute_import,
                        division, print_function)
 
__version__ = '0.0.5'
__author__ = "Mickaël 'Tiger-222' Schoentgen"
__copyright__ = '''
    Copyright (c) 2013, Mickaël 'Tiger-222' Schoentgen
 
    Permission to use, copy, modify, and distribute this software and its
    documentation for any purpose and without fee or royalty is hereby
    granted, provided that the above copyright notice appear in all copies
    and that both that copyright notice and this permission notice appear
    in supporting documentation or portions thereof, including
    modifications, that you make.
'''
 
# Bon je vais pas vous expliquer ce que les lignes du dessus font hein...hein
 
 
# __all__ liste les objets importables si on fait from mss import *
# ce qui limite la pollution du namespace avec tout un tas de choses inutiles
# comme pack, isfile, system, etc. qui sont quelques lignes plus bas.
__all__ = ['MSSImage', 'MSSLinux', 'MSSMac', 'MSSWindows']
 
# Permet de trouve une bibliothèqe via son nom sur le système en cherchant
# divers chemins standard à l'OS
from ctypes.util import find_library
 
# Permet une forme de sérialisation des types simples qui est compatible
# entre Python et C
from struct import pack
 
# Permet de vérifier si un fichier existe et n'est pas un dossier
from os.path import isfile
 
# Permet de récupérer le nom de l'OS
from platform import system
 
# Divers opérations sur le système
import sys
 
# Compression zip
import zlib
 
# On importe conditionnellement des packages selon le système sur lequel on
# est. La raison de cela est que certains packages n'existe pas (ou ne sont pas
# utiles) sur certains OS.
 
# Darwin, c'est l'OS open source qui sert de base au Mac. C'est
# un mélange de NeXTSTEP et de FreeBSD.
if system() == 'Darwin':
    # Quartz est la techno derrière l'affichage des Mac incluant notament
    # le compositeur graphique et moteur de rendu 2D. Il a un binding Python
    # intégré puisque les Mac utilisent Python un peu partout. A ce demander
    # pourquoi ces neuneus nous ont collé objectifs C pour le dev.
    from Quartz import *
    # On importe juste l'équivalent du mimetype sous Mac pour le PNG. C'est
    # une "constante"
    from LaunchServices import kUTTypePNG
 
 
elif system() == 'Linux':
 
    # Accès aux variables d'environnement
    from os import environ
    # Fonction pour transformer ~ dans les chemin d'accès en chemin vers
    # le dossier utilisateur
    from os.path import expanduser
    # Parseur de xml
    import xml.etree.ElementTree as ET
 
    # 'byref' permet d'obtenir un pointer sur une fonction c,
    # 'cast' est similaire à l'opérateur cast en c et permet le type casting
    # d'un objet c, 'cdll' permet de charger des shared lib C
    from ctypes import byref, cast, cdll
 
    # je ne sais pas pourquoi ça n'a pas été fait une seule ligne...
    # Tout ça représente les types c éponymes, mais en plus Structure est une
    # classe abstraite dont on peut hériter faire une classe qui peut être
    # passée en paramètre à une fonction C qui attend un struc.
    from ctypes import (
        c_char_p, c_int, c_int32, c_uint, c_uint32,
        c_ulong, c_void_p, POINTER, Structure
    )
 
    # On hérite de Structure ce qui nous fait pour le moment une structure
    # vide
    class Display(Structure):
        pass
 
    # Une structure représentant les attributs d'une fenêtre pour le serveur
    # d'affichage sous Linux.
    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)
        ]
 
    # structure définissant une image telle qu'elle existe dans la mémoire
    # d'un client du serveur d'affichage
    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)
        ]
 
    # Apparement la fonction pack avec ce format va être appelée souvent
    # doc l'auteur se fait un raccourci. Le format en question est 'B', donc
    # du unsigned char, et '<', donc du little endian. Et là vous comprenez
    # le bonheur de travailler dans un langage de haut niveau comme Python.
    def b(x):
        return pack(b'<B', x)
 
elif system() == 'Windows':
 
 
    from ctypes import (
 
        # 'memset' fait pareil que memset en C : on rempli un bloc de mémoire
        # avec les bytes passés en param mais il n'est pas utilisé dans le code,
        # donc je suppose que c'est un oubli. 'pointer' va créer une instance de
        # la classe qui permet de manipuler un pointer sur un type c en Python.
        # 'sizeof', même chose que l'opérateur c, ça retourne la taille en bytes
        # d'un type
        # c. 'windll', comme 'cdll' plus haut, mais l'appel des fonctions suit
        # la convention stdcall typique de la win32api et pas la convention c
        # standard.
        byref, memset, pointer, sizeof, windll,
        # Représentation du type void en c, aliasé sous deux noms. Je n'ai pas
        # vraiment compris l'interêt de le faire, excepté pour explicitement
        # montrer quand on l'utilise dans un appel qui attend LPRECT (un pointer
        # sur une struc qui représente un rectangle ou un pointer sur void)
        c_void_p as LPRECT,
        c_void_p as LPVOID,
        # créer un array de char en c
        create_string_buffer,
        # ça on a vu
        Structure,
        POINTER,
        # Ceci est une fonction. Pourquoi c'est en majuscule, je n'en sais rien.
        # Une incohérence dans l'API. En tout cas ça permet de retourner
        # un prototype de fonction qui respecte la convention stdcall.
        WINFUNCTYPE,
    )
    # Des "constantes" qui représentent des types spécifiques à l'API widows
    # Notez que le PEP8 n'est pas respecté, mais bon, vu la taille du code,
    # il y a forcément des coquilles, c'est normal. Certains comme SHORT
    # et HANDLE ne sont pas utilisés.
    from ctypes.wintypes import (
        BOOL, DOUBLE, DWORD, HANDLE, HBITMAP, HDC, HGDIOBJ,
        HWND, INT, LPARAM, LONG,RECT,SHORT, UINT, WORD
    )
 
    # Structure contenant les information sur les dimentions et la couleur
    # d'un BMP (on appelle ça aussi un bitmap ou DIB, donc dans les docs
    # vous verrez l'une ou l'autre de ces appellations)
    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)
        ]
 
    # Un wrapper sur la structure précédente, je suppose nécessaire pour
    # être compatible avec une des bizarreries de l'API windows.
    class BITMAPINFO(Structure):
        _fields_ = [
            ('bmiHeader', BITMAPINFOHEADER),
            ('bmiColors', DWORD * 3)
        ]
 
    # Là j’avoue je ne comprends pas trop le principe. Il tente de vérifier
    # si le système est du Python 2, si oui il ne caste pas la string en bytes
    # car le type de base des strings est déjà de type bytes (ce qui n'est
    # pas le cas en Python 3 : c'est un type unicode). Donc il fabrique la
    # fonction raccourcie selon la version. Sauf qu'il ne le fait pas
    # dans la condition pour linux. Du coup je pige pas trop comment il
    # s'en sort. Il est compatible que 2.7 sous Linux ?
    # Dans tous les cas ces fonctions pourraient être en dehors de la clause
    # if, tout comme certains imports pour des raisons de DRY.
    if sys.version < '3':
        def b(x):
            return x
    else:
        def b(x):
            return pack(b'<B', x)
 
 
# Moi aussi j'adore mettre des commentaires comme ça dans mon code.mon
# Que ceux qui n'aiment pas aillent se faire foutre.
 
 
# ----------------------------------------------------------------------
# --- [ C'est parti mon kiki ! ] ---------------------------------------
# ----------------------------------------------------------------------
 
# Comme précisé dans la docstring, c'est une classe de base. Ca implemente
# essentiellement le logging du mode debug et la logique d'enregistrement
# des fichiers.
class MSS(object):
    ''' This class will be overloaded by a system specific one.
        It checkes if there is a class available for the current system.
        Raise an exception if no one found.
    '''
 
 
    # De l'initialisation, pas super intéressant...
    DEBUG = False
 
    def __init__(self, debug=False):
        ''' Global vars and class overload. '''
 
        self.DEBUG = debug
        self.monitors = []
        self.oneshot = False
 
        # L'init présuppose quand même qu'une classe fille a une fonction
        #  init. Je vois pas trop l'interêt car ça peut se faire par héritage
        # ou alors quitte à faire ça, autant faire en utilisant un système
        # de call back sur des events genre "oninit", c'est plus propre, plus
        # découplé et ça tombe pas en marche si la fonction init() n'existe pas
        # dans l'enfant.
        self.init()
 
    # Une fonction print un peut élaborée qui permet d'afficher rapidement
    # le contenu qu'on lui passe en paramètre de manière formatté pour le debug.
    # Utiliser le module logging aurait été préférable, mais c'est tellement
    # chiant à setup que je comprends qu'il ait eu la flemme.
    def debug(self, method='', scalar=None, value=None):
        ''' Simple debug output. '''
 
        if self.DEBUG:
            if scalar is None:
                print(':: ' + method + '()')
            else:
                print(method + '()', scalar, type(value).__name__, value)
 
    # La fonction "kifétou". Elle demande la création des screenshots, les
    # sauvegarde dans des fichiers png dans un dossier. Il aurait été sympa
    # de découper cette fonction en sous fonctions : une prendre le screen
    # d'un moniteur, une pour recupérer le screenshot tout forme d'objet image,
    # etc. Là la seule option c'est "tu prends tout, tu sauves tout". C'est pas
    # très souple, et dur à tester.
    def save(self, output='mss', oneshot=False):
        ''' For each monitor, grab a screen shot and save it to a file.
 
            Parameters:
             - output - string - the output filename without extension
             - oneshot - boolean - grab only one screen shot of all monitors
 
            This is a generator which returns created files:
                'output-1.png',
                'output-2.png',
                ...,
                'output-NN.png'
                or
                'output-full.png'
        '''
 
        self.debug('save')
 
        # Là on voit un antipattern très courrant en POO : mettre des paramètres
        # pour un appel de méthode directement en attribut pour toute la classe.
        # Cela veut dire qu'un appel à cette fonction "lock" la classe dans
        # un état dépendant de cet appel. En plus de réduire à néant les possibilités
        # de multithreading et rendre les tests compliqués, ça veut dire aussi
        # que tout débuggage doit tenir compte de l'état de la classe. Et bien
        # entendu, ça veut dire qu'on ne peut pas avoir plusieurs états en
        # parallèles, à moins d'instancier plusieurs classes. Il vaudrait mieux
        # que tout ça soit passé en paramètres, quitte à faire un autre objet
        # nommé "config" qui contienne tout ça.
        #
        # En tout cas, ça stoque le param "oneshot" et la liste des moniteurs
        # retournés par le code fourni par la classe fille puisque
        # enum_display_monitors n'est pas défini dans cette classe.
        self.oneshot = oneshot
        self.monitors = self.enum_display_monitors() or []
 
        self.debug('save', 'oneshot', self.oneshot)
 
        # On lève une exception si il est impossible de trouver des écrans dont
        # on peut faire la capture, ce qui est une bonne chose. Mais une bonne
        # pratique serait de faire :
        # class ScreenshotError(Exception):
        #     pass
        # Afin d'avoir quelque chose de plus significatif que ValueError.
        # De plus, ce code est typique d'un programmeur d'un autre langage
        # qui apprend encore les idiomes Python car cette condition pourrait
        # se réduire à :
        # if not self.monitors: <- pas besoin de len
        #     raise ScreenshotError('MSS: no monitor found.'))
        if len(self.monitors) < 1:
            raise ValueError('MSS: no monitor found.')
 
        # Monitors screen shots!
        # On itère sur les écrans, et on fait un screen par écran. Même remarque
        # sur le fait que l'auteur cherche encore ses marques en Python (notez
        # bien que c'est naturel, personne n'a la science infuse et mon style
        # est horrible dans les autres langages), car ici le compteur i est
        # aventageusement remplacé par :
        # for i, monitor in enumerate(self.monitors)
        i = 1
        for monitor in self.monitors:
            self.debug('save', 'monitor', monitor)
 
            # Si on ne fait qu'un gros screenshot, on l'appelle name-full.png
            # sinon name-numero_du_moniteur.png
            if self.oneshot:
                filename = output + '-full'
            else:
                filename = output + '-' + str(i)
                i += 1
            filename += '.png'
 
            # Si le fichier n'existe pas, on l'écrit. Ceci est encore une
            # erreur car une API métier ne doit pas faire ce genre de
            # vérification. Il devrait écraser les fichiers. Il est en revanche
            # possible de mettre un système de callback pour réagir si le
            # fichier existe. Mais le comportement par défaut devrait être
            # d'écraser le fichiers, sinon une personne qui ne lit pas le code
            # source va croire que le code ne marche pas. De plus, il est plus
            # rare de ne pas vouloir écraser les fichiers d'origine que
            # l'inverse. Enfin, puisqu'on ne controle pas le nommage des fichiers
            # depuis l'extérieur de la classe, il n'y a pas de moyen de réagir
            # proprement pour dire "je veux garder les anciens fichiers et
            # avoir les nouveaux quand même" à part utilise un dossier différent
            # à chaque fois.
            if not isfile(filename):
 
                # On récupère l'array de pixels en utilisant le code de la
                # classe fille une fois de plus. C'est nécessaire puisque ça
                # dépend de l'OS.
                # Ce code ne fait pas du tout ce que vous croyez. get_pixels
                # va sauvegarder l'image sans self.image, et retourner 1.
                # C'est donc une fonction qui marche via ses effets de bords,
                # ce qui est une très mauvaise pratique.
                pixels = self.get_pixels(monitor)
 
                # Même remarque que sur l'autre exception. De plus l'exception
                # devrait être levée au niveau de get_pixels, pas au niveau
                # de save(). Il y a un mélange de base niveau et de haut
                # niveau dans cette classe qui rend l'API assez maladroite.
                # D'ailleurs get_pixels ne retourne jamais None...
                if pixels is None:
                    raise ValueError('MSS: no data to process.')
 
                # Bon, ça c'est super crade. En gros il délègue le save à une
                # méthode si elle existe, et sinon le fait à la main.
                # L'héritage est fait pour ça, et il aurait été préférable
                # de faire une méthode save(pixels, size, filename) générique
                # et ensuite l'écraser dans les enfants. Peut être que c'est un
                # dev C et qu'il n'est pas encore très à l'aise avec le POO.
                # Il est possible que l'auteur ait rencontré des problèmes du
                # fait d'une inconsistance d'api et n'ai pas su le gérer en
                # mettant un adapter par dessus. Quand je vous dis que les
                # design pattern c'est parfois utile :)
                if hasattr(self, 'save_'):
                    img_out = self.save_(output=filename)
                else:
                    img = MSSImage(pixels, monitor[b'width'], monitor[b'height'])
                    img_out = img.dump(filename)
                self.debug('save', 'img_out', img_out)
                if img_out:
                    # Etant donné que la génération du screenshot prend du
                    # temps et qu'il y en a plusieurs, l'auteur a intelligemment
                    # choisit de faire un générateur, ce qui permet de ne pas
                    # attendre que tous les screenshots soient terminé avant
                    # de d'avoir un premier résultat.
                    yield img_out
            else:
                yield filename + ' (already exists)'
 
    # Les fameuses méthodes délégues aux classes filles. Un petit
    # NotImplemented serait mieux que pass car c'est plus explicite, mais
    # rien de grave.
    def enum_display_monitors(self):
        ''' Get positions of all monitors.
 
            If self.oneshot is True, this function has to return a dict
            with dimensions of all monitors at the same time.
            If the monitor has rotation, you have to deal with inside this method.
 
            Must returns a dict with a minima:
            {
                'left':   the x-coordinate of the upper-left corner,
                'top':    the y-coordinate of the upper-left corner,
                'width':  the width,
                'height': the height
            }
        '''
        pass
 
    def get_pixels(self, monitor_infos):
        ''' Retrieve screen pixels for a given monitor.
 
            monitor_infos should contain at least:
            {
                'left':   the x-coordinate of the upper-left corner,
                'top':    the y-coordinate of the upper-left corner,
                'width':  the width,
                'heigth': the height
            }
 
            Returns a dict with pixels.
        '''
        pass
 
# Maintenant on par dans les détails de l'implémentation de l'algo qui récupère
# les screenshots qui est spécifique à chaque OS.
class MSSMac(MSS):
    ''' Mutli-screen shot implementation for Mac OSX.
        It uses intensively the Quartz.
    '''
 
    # Bon, du coup plutôt que de faire une fonction init, il aurait mieux
    # valut faire une fonction __init__ qui appelle super().
    def init(self):
        ''' Mac OSX initialisations '''
        self.debug('init')
        pass
 
    # Là c'est le gros du boulot, et c'est là que je vous dis que l'auteur
    # c'est vraiment super cassé le cul pour trouver toutes ces infos sur
    # les APIS propres à chaque système, et en plus en C. Kudos.
    def enum_display_monitors(self):
        ''' Get positions of one or more monitors.
            Returns a dict with minimal requirements (see MSS class).
        '''
 
        self.debug('enum_display_monitors')
 
        results = []
        # Si il y a un seul screen à prend, on se fait pas chier et on prend
        # un rectangle de taille infinie pour les limites du screen. Je
        # suppose que l'OS est capable de se démerder avec ça et borner
        # à la taille du matos automatiquement. C'est un truc de faignasse, et
        # c'est plutôt malin.
        if self.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:
            # Par contre là il faut se taper le calcul des coordonnées de
            # rectangle à la main
 
            # Cette variable est requise par CGGetActiveDisplayList mais
            # comme le suggère le commentaire de l'auteur ci-dessous : LOL.
            max_displays = 32  # Peut-être augmenté, si besoin...
 
            # Apparemment il est possible que l'affichage ait subit une
            # rotation. Je suppose que MacOS anticipait déjà l’existence des
            # tablettes à l'époque.
            rotations = {0.0: 'normal', 90.0: 'right', -90.0: 'left'}
 
            # On demande les affichages qui sont effectivement en court
            # d'utilisation, et on récupère un identifiant unique pour chacun
            # d'entre deux.
            res, ids, count = CGGetActiveDisplayList(max_displays, None, None)
            for display in ids:
                # On interroge l'API Mac pour avoir ses coordonnées, et on
                # les extrait.
                rect = CGRectStandardize(CGDisplayBounds(display))
                left, top = rect.origin.x, rect.origin.y
                width, height = rect.size.width, rect.size.height
                # Correction des coordonnées en cas de rotation.
                rot = CGDisplayRotation(display)
                rotation = rotations[rot]
                if rotation in ['left', 'right']:
                    width, height = height, width
 
                # Et on insère le résultat dans la liste des monitors. On
                # aurait pu yielder aussi comme dans save(), mais bon, c'est
                # parce que je yield partout que je dis ça.
                results.append({
                    b'left'    : int(left),
                    b'top'     : int(top),
                    b'width'   : int(width),
                    b'height'  : int(height),
                    b'rotation': rotation
                })
        return results
 
    def get_pixels(self, monitor):
        ''' Retreive all pixels from a monitor. Pixels have to be RGB.
        '''
 
        self.debug('get_pixels')
 
        # On récupère les coordonnées retournées par enum_display_monitors
        # et on en fait un CGRect qui est nécessaire pour CGWindowListCreateImage.
        width, height = monitor[b'width'], monitor[b'height']
        left, top = monitor[b'left'], monitor[b'top']
        rect = CGRect((left, top), (width, height))
        # CGWindowListCreateImage retourne l'image à ces coordonnées.
        # Les 3 derniers paramètres sont là grace à "from Quartz import *"
        # (c'est pour ça que je vous dis de ne pas le faire, car c'est pas
        # facile à retracer). kCGWindowListOptionOnScreenOnly force l'inclusion
        # uniquement des fenêtre affichées et non celles hors écran.
        # kCGNullWindowID est l'équivalent de None, dans ce contexte. C'est
        # la fenêtre de référence, et on en veut aucune.
        # kCGWindowImageDefault demande de capturer le fenêtre ET sa décoration
        # (comme les ombres portées).
        # Ces deux derniers params sont les valeurs par défaut donc je ne sais
        # pas pourquoi il les passent.
        self.image = CGWindowListCreateImage(
                    rect, kCGWindowListOptionOnScreenOnly,
                    kCGNullWindowID, kCGWindowImageDefault)
 
        # Comme je le disais dans save(), la fonction ne retourne PAS l'objet
        # image mais 1. L'image est stockée dans self.image et ce sont les
        # autres méthodes qui tapent dans cet attribut. C'est mal.
        return 1
 
    # la fameurse méthode save_ utilisée par save()...
    def save_(self, output):
        ''' Special method to not use MSSImage class. '''
 
        self.debug('save_')
 
        # résolution du png
        dpi = 72
        # chemin du png. Tout est URL dans Mac OS. Un chemin de fichier
        # est une URL file://
        url = NSURL.fileURLWithPath_(output)
        # On fabrique l'objet fichier
        dest = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, None)
        properties = {
            kCGImagePropertyDPIWidth: dpi,
            kCGImagePropertyDPIHeight: dpi,
        }
        # On met notre image dans l'objet fichier. Là il faut se souvenir
        # que save() appelle get_pixel() qui remplit self.image puis appelle
        # save_() derrière. Je vous l'avais dis que les effets de bord, ça
        # craint).
        CGImageDestinationAddImage(dest, self.image, properties)
        # On écrit notre objet fichier sur le disk.
        if not CGImageDestinationFinalize(dest):
            output = None
 
        # L'auteur vide ouput si CGImageDestinationFinalize à échoué. Il
        # vaudrait mieux lever une exception.
        return output
 
# On passe à l'implémentation Linux.
class MSSLinux(MSS):
    ''' Mutli-screen shot implementation for GNU/Linux.
        It uses intensively the Xlib.
    '''
 
    # Fait un travail de nettoyage quand l'objet est garbage collecté, en
    # l'occurence ferme la connection avec le serveur x. Je rappelle que
    # l'appel à __del__ n'est pas garanti donc mieux faut faire une méthode
    # close() officielle et un context manager avec un finally.
    def __del__(self):
        ''' Disconnect from X server '''
 
        self.debug('__del__')
 
        if self.display:
            self.XCloseDisplay(self.display)
 
 
    def init(self):
        ''' GNU/Linux initialisations '''
 
        self.debug('init')
 
        # on charge la shared lib du serveur x
        x11 = find_library('X11')
        if x11 is None:
            raise OSError('MSSLinux: no X11 library found.')
        else:
            xlib = cdll.LoadLibrary(x11)
 
        self.debug('init', 'xlib', xlib)
 
        # Aliasing des méthode de xlib. Pourquoi ? Aucune idée.
        # passer l'objet xlib le parait plus logique. Peut être qu'il y a
        # une raison technique liée au mapping ctypes, mais je ne la connais
        # pas.
        self.XOpenDisplay = xlib.XOpenDisplay
        self.XDefaultScreen = xlib.XDefaultScreen
        self.XDefaultRootWindow = xlib.XDefaultRootWindow
        self.XGetWindowAttributes = xlib.XGetWindowAttributes
        self.XAllPlanes = xlib.XAllPlanes
        self.XGetImage = xlib.XGetImage
        self.XGetPixel = xlib.XGetPixel
        self.XFree = xlib.XFree
        self.XCloseDisplay = xlib.XCloseDisplay
 
        # ctypes permet de spécifier pour chaque fonction des types attendus
        # en paramètre et en valeur de retour. Cela permet l'autocasting
        # de types compatibles et une exception explicite en cas de passage
        # de mauvais paramètres plutôt qu'un core dump.
        # L'auteur ici à mis ce code un peu lourd dans des méthodes dédiées et
        # préfixées de _ puisqu'à usage interne.
        self._set_argtypes()
        self._set_restypes()
 
        # On récupère l'affichage en cours en tapant dans les variables
        # d'environnement. Le jobby-jobba et du au fait qu'il veut garder
        # la compatibilité Python 3 et 2.7 et qu'il faut accomoder
        # les types str/unicode/bytes.
        # C'est une vérification pour éviter un segfault si il n'y pas de
        # serveur x qui tourne.
        display = None
        self.display = None
        try:
            if sys.version > '3':
                display = bytes(environ['DISPLAY'], 'utf-8')
            else:
                display = environ['DISPLAY']
        except KeyError:
            err = 'MSSLinux: $DISPLAY not set. Stopping to prevent segfault.'
            raise ValueError(err)
        self.debug('init', '$DISPLAY', display)
 
        # On récupère l'écran par défaut et la fenêtre racine sur l'affichage
        # en cours.
 
        # At this point, if there is no running server, it could end on
        # a segmentation fault. And we cannot catch it.
        self.display = self.XOpenDisplay(display)
        self.debug('init', 'display', self.display)
        self.screen = self.XDefaultScreen(self.display)
        self.debug('init', 'screen', self.screen)
        self.root = self.XDefaultRootWindow(self.display, self.screen)
        self.debug('init', 'root', self.root)
 
    # les deux méthodes qui servent à manuellement définir les types
    # des autres méthodes dont j'ai parlé plus haut.
 
    def _set_argtypes(self):
        ''' Functions arguments '''
 
        self.debug('_set_argtypes')
 
        self.XOpenDisplay.argtypes = [c_char_p]
        self.XDefaultScreen.argtypes = [POINTER(Display)]
        self.XDefaultRootWindow.argtypes = [POINTER(Display), c_int]
        self.XGetWindowAttributes.argtypes = [POINTER(Display),
            POINTER(XWindowAttributes), POINTER(XWindowAttributes)]
        self.XAllPlanes.argtypes = []
        self.XGetImage.argtypes = [POINTER(Display), POINTER(Display),
            c_int, c_int, c_uint, c_uint, c_ulong, c_int]
        self.XGetPixel.argtypes = [POINTER(XImage), c_int, c_int]
        self.XFree.argtypes = [POINTER(XImage)]
        self.XCloseDisplay.argtypes = [POINTER(Display)]
 
    def _set_restypes(self):
        ''' Functions return type '''
 
        self.debug('_set_restypes')
 
        self.XOpenDisplay.restype = POINTER(Display)
        self.XDefaultScreen.restype = c_int
        self.XDefaultRootWindow.restype = POINTER(XWindowAttributes)
        self.XGetWindowAttributes.restype = c_int
        self.XAllPlanes.restype = c_ulong
        self.XGetImage.restype = POINTER(XImage)
        self.XGetPixel.restype = c_ulong
        self.XFree.restype = c_void_p
        self.XCloseDisplay.restype = c_void_p
 
 
    def enum_display_monitors(self):
        ''' Get positions of one or more monitors.
            Returns a dict with minimal requirements (see MSS class).
        '''
 
        self.debug('enum_display_monitors')
 
        results = []
        if self.oneshot:
            # Dans le cas d'un seul screenshot pour tout, on récupère juste
            # les coordonnées de la fenêtre racine.
            gwa = XWindowAttributes()
            self.XGetWindowAttributes(self.display, self.root, byref(gwa))
            results.append({
                b'left'  : int(gwa.x),
                b'top'   : int(gwa.y),
                b'width' : int(gwa.width),
                b'height': int(gwa.height)
            })
        else:
 
            # Sinon on parse le fichier XML de config pour essayer de
            # trouver les coordonnées. Je ne suis pas certain que ce soit
            # une bonne stratégie puisque le fichier monitors.xml contient
            # tous les moniteurs jamais branché sur la machine, y compris ceux
            # qu'on a pas branché depuis 1000 ans...
 
            # It is a little more complicated, we have to guess all stuff
            # from ~/.config/monitors.xml, if present.
            monitors = expanduser('~/.config/monitors.xml')
            if not isfile(monitors):
                # Ici, lever une exception serait pas mal.
                # A la place, l'auteur choisit de mettre oneshot et de relancer
                # la capture. Donc au lieu d'avoir ce qu'on demande ou une
                # erreur, on a ce qu'on demande ou ce qu'on ne demande pas.
                # Encore une fois, j'en profite pour souligner qu'il faut
                # éviter ce genre de config à base de site effects. Devoir
                # setter un attribut pour avoir un résultat différent à cette
                # méthode n'est pas très propre.
                self.debug('ERROR', 'MSSLinux: enum_display_monitors() failed (no monitors.xml).')
                self.oneshot = True
                return self.enum_display_monitors()
 
            # Le XML est une collection de noeuds 'configuration' qui représentent
            # chacun un moniteur. On récupère ici le premier noeud 'configurations'
            tree = ET.parse(monitors)
            root = tree.getroot()
            config = root.findall('configuration')[-1]
            conf = []
            # chaque noeud "configurations" à une série de noeuds ouput qui
            # représentent chaque format de sortie (VGA, HDMI, etc). On
            # boucle dessus.
            for output in config.findall('output'):
                name = output.get('name')
                if name != 'default':
                    # On récupère les coordonnées, la rotation, on extrait,
                    # on corrige, on ajoute à la liste... Bref, même topo
                    # qu'avec MacOsX.
                    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 in ['left', '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
 
    def get_pixels(self, monitor):
        ''' Retreive all pixels from a monitor. Pixels have to be RGB.
        '''
 
        self.debug('get_pixels')
 
        # On récupère les coordonnées des monitors revoyées par enum_display_monitors
        width, height = monitor[b'width'], monitor[b'height']
        left, top = monitor[b'left'], monitor[b'top']
        ZPixmap = 2
 
        # On récupère un masque de pixels pour l'ensemble de l'affichage
        allplanes = self.XAllPlanes()
        self.debug('get_pixels', 'allplanes', allplanes)
 
        # Visiblement un fix. C'est ce qu'on appelle un commentaire utile.
        # Fix for XGetImage: expected LP_Display instance instead of LP_XWindowAttributes
        root = cast(self.root, POINTER(Display))
 
        # On récupère un dump des pixels pour les coordonnées en cours, de
        # l'affichage en cours, on lui applique le masque de pixel mais
        # je ne sais pas pourquoi on doit le faire. x11 est une bestiole
        # très tarabiscotée, et mes recherches n'ont rien donné. L'auteur
        # a du bien s'amuser à trouver comment faire ce genre de chose.
        image = self.XGetImage(self.display, root, left, top, width,
            height, allplanes, ZPixmap)
        if image is None:
            raise ValueError('MSSLinux: XGetImage() failed.')
 
        # Les pixels doivent être récupérés en RGB. L'auteur fait donc une
        # fonction de conversion (c'est une fonction inline, donc jetable)
        # puis l'applique à la liste des pixels qu'il récupère dans l'image.
        # Une simple boucle for aurait fait l'affaire mais l'auteur a mis
        # en place une stratégie de mémoisation (mise en cache) dans la fonction.
        # Vu que la fonction est inline, un simple dico aurait aussi fait
        # l'affaire. Mais une il est très possible qu'on lui ait donné
        # le truc et comme il n'est pas habitué à Python il a juste copié/collé.
        # Je le fais souvent en Java / C donc je vais pas lui jeter la pierre.
        def pix(pixel, _resultats={}):
            ''' Apply shifts to a pixel to get the RGB values.
                This method uses of memoization.
            '''
            # La mise en cache se fait à ce niveau.
            if not pixel in _resultats:
                # Là c'est du byte shifting, mais quelle logique exacte est
                # implémentée, aucune idée. Il faudrait regarder les algos
                # de conversion pixels vers RGB et trouver celui qui est
                # appliqué. C'est le genre de truc que je copie/colle car
                # je suis trop feignant et tester que ça marche est plus rapide
                # que comprendre.
                _resultats[pixel] = b((pixel & 16711680) >> 16) + b((pixel & 65280) >> 8) + b(pixel & 255)
            return _resultats[pixel]
 
        # Aliasing de la fonction pour gagner en vitesse en évitant un lookup
        # d'attribut dans une boucle.
        get_pix = self.XGetPixel
        # Boucle de conversion via une liste en intention.
        pixels = [pix(get_pix(image, x, y)) for y in range(height) for x in range(width)]
 
        # Ici get_pixels retourne l'objet image plutôt que de la sauver
        # dans un attribut self.image. Ouch.
        self.XFree(image)
        return b''.join(pixels)
 
# On passe maintenant à l'implémentation pour Windows
 
class MSSWindows(MSS):
    ''' Mutli-screen shot implementation for Microsoft Windows. '''
 
    # Même topo que pour la version Linux. Même principe que son init.
    def init(self):
        ''' Windows initialisations '''
 
        self.debug('init')
 
        self.GetSystemMetrics = windll.user32.GetSystemMetrics
        self.EnumDisplayMonitors = windll.user32.EnumDisplayMonitors
        self.GetWindowDC = windll.user32.GetWindowDC
        self.CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
        self.CreateCompatibleBitmap = windll.gdi32.CreateCompatibleBitmap
        self.SelectObject = windll.gdi32.SelectObject
        self.BitBlt = windll.gdi32.BitBlt
        self.GetDIBits = windll.gdi32.GetDIBits
        self.DeleteObject = windll.gdi32.DeleteObject
 
        self._set_argtypes()
        self._set_restypes()
 
    def _set_argtypes(self):
        ''' Functions arguments '''
 
        self.debug('_set_argtypes')
 
        self.MONITORENUMPROC = WINFUNCTYPE(INT, DWORD, DWORD,
            POINTER(RECT), DOUBLE)
        self.GetSystemMetrics.argtypes = [INT]
        self.EnumDisplayMonitors.argtypes = [HDC, LPRECT,
            self.MONITORENUMPROC, LPARAM]
        self.GetWindowDC.argtypes = [HWND]
        self.CreateCompatibleDC.argtypes = [HDC]
        self.CreateCompatibleBitmap.argtypes = [HDC, INT, INT]
        self.SelectObject.argtypes = [HDC, HGDIOBJ]
        self.BitBlt.argtypes = [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD]
        self.DeleteObject.argtypes = [HGDIOBJ]
        self.GetDIBits.argtypes = [HDC, HBITMAP, UINT, UINT, LPVOID,
            POINTER(BITMAPINFO), UINT]
 
    def _set_restypes(self):
        ''' Functions return type '''
 
        self.debug('_set_restypes')
 
        self.GetSystemMetrics.restypes = INT
        self.EnumDisplayMonitors.restypes = BOOL
        self.GetWindowDC.restypes = HDC
        self.CreateCompatibleDC.restypes = HDC
        self.CreateCompatibleBitmap.restypes = HBITMAP
        self.SelectObject.restypes = HGDIOBJ
        self.BitBlt.restypes =  BOOL
        self.GetDIBits.restypes = INT
        self.DeleteObject.restypes = BOOL
 
    def enum_display_monitors(self):
        ''' Get positions of one or more monitors.
            Returns a dict with minimal requirements (see MSS class).
        '''
 
        self.debug('enum_display_monitors')
 
        # le code qui permet de récupérer les moniteurs est visiblement
        # asynchrone, donc on fabrique un callback qui va remplir le tableau
        # des résultats.
 
        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 = []
        # si c'est juste un seul screenshot, c'est un appel synchrone
        if self.oneshot:
            # ce sont des constantes qui déterminent quelle info ont veut que
            # GetSystemMetrics renvoit. Ici on demande les coordonnées de tout
            # l'écran, une par une.
            SM_XVIRTUALSCREEN = 76
            SM_YVIRTUALSCREEN = 77
            SM_CXVIRTUALSCREEN = 78
            SM_CYVIRTUALSCREEN = 79
            left = self.GetSystemMetrics(SM_XVIRTUALSCREEN)
            right = self.GetSystemMetrics(SM_CXVIRTUALSCREEN)
            top = self.GetSystemMetrics(SM_YVIRTUALSCREEN)
            bottom = self.GetSystemMetrics(SM_CYVIRTUALSCREEN)
            results.append({
                b'left'  : int(left),
                b'top'   : int(top),
                b'width' : int(right - left),
                b'height': int(bottom - top)
            })
        else:
            # On enrobe le callback Python dans un proxy qui le rend
            # utilisable par le code C
            callback = self.MONITORENUMPROC(_callback)
            # On demande à windows de nous lister les moniteurs, et comme
            # cet appel est asynchrone, on lui passe un callback pour qu'il
            # replisse la liste au fur et à mesure
            self.EnumDisplayMonitors(0, 0, callback, 0)
 
        # On retourne la liste. A ce stade là, on ne sait pas si la liste
        # est vide, à moitié remplie ou complètement remplie. Utiliser du
        # code asynchrone au milieu d'une lib synchrone, c'est assez dangereux
        # donc je me demande comment il retombe sur ses pieds.
        return results
 
    # A ce stade là il est 13h, j'ai commencé à 9h et j'ai la dalle. J'en ai
    # marre de commenter ce code. 'envoyez-vous les codes que vous pigez pas'
    # est une idée à la con. Je vous hais tous. Mon coloc va me ramener des
    # frites.
 
    def get_pixels(self, monitor):
        ''' Retreive all pixels from a monitor. Pixels have to be RGB. '''
 
        self.debug('get_pixels')
 
        # Encore une fois on récupère les coordonnées des moniteurs filées
        # par enum_display_monitors
        width, height = monitor[b'width'], monitor[b'height']
        left, top = monitor[b'left'], monitor[b'top']
 
        # Reajustement de la taille de la largeur. Je suppose que c'est
        # empirique, mais je peux me tromper.
        good_width = (width * 3 + 3) & -4
        # Valeur de paramètre qui dit de copier directement dans le rectangle
        # des destination.
        SRCCOPY = 0xCC0020
        DIB_RGB_COLORS = 0
 
        # Récupère le Device Context (title bar, menus, and scroll bars, etc)
        srcdc = self.GetWindowDC(0)
        # On fabrique une copie en mémoire.
        memdc = self.CreateCompatibleDC(srcdc)
        # On fabrique un bitmap basé sur ce DC
        bmp = self.CreateCompatibleBitmap(srcdc, width, height)
        # On injecte un bitmap dans le DC en mémoire.
        self.SelectObject(memdc, bmp)
        # On transfert les bits d'un DC à l'autre.
        self.BitBlt(memdc, 0, 0, width, height, srcdc, left, top, SRCCOPY)
        # On fabrique le header du BMP
        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biWidth = width
        bmi.bmiHeader.biHeight = height
        bmi.bmiHeader.biBitCount = 24
        bmi.bmiHeader.biPlanes = 1
        buffer_len = height * good_width
        # On fabrique un array de char
        pixels = create_string_buffer(buffer_len)
        # On récupère les bits du bitmap issu du DC
        bits = self.GetDIBits(memdc, bmp, 0, height, byref(pixels),
            pointer(bmi), DIB_RGB_COLORS)
 
        self.debug('get_pixels', 'srcdc', srcdc)
        self.debug('get_pixels', 'memdc', memdc)
        self.debug('get_pixels', 'bmp', bmp)
        self.debug('get_pixels', 'buffer_len', buffer_len)
        self.debug('get_pixels', 'bits', bits)
        self.debug('get_pixels', 'len(pixels.raw)', len(pixels.raw))
 
        # Nettoyage des objets.
        # Clean up
        self.DeleteObject(srcdc)
        self.DeleteObject(memdc)
        self.DeleteObject(bmp)
 
        # Apparemment la récupération peut échouer et on peut vérifier
        # cet échec en checkant la longueur. Après la cause de l'échec est
        # un mystère
        if bits != height or len(pixels.raw) != buffer_len:
            raise ValueError('MSSWindows: GetDIBits() failed.')
 
        # Réorganise les bits dans le bonne ordre et converti le format
        # de couleur de BGR vers RGB
 
        # Note that the origin of the returned image is in the
        # bottom-left corner, 32-bit aligned. And it is BGR.
        # Need to "arrange" that.
        return self._arrange(pixels.raw, good_width, height)
 
 
    def _arrange(self, data, width, height):
        ''' Reorganises data when the origin of the image is in the
            bottom-left corner and converts BGR triple to RGB. '''
 
        self.debug('_arrange')
 
        # On crée une nouvelle liste pleine de zéro, et on la rempli
        # avec la nouvelle position des pixels.
        total = width * height
        scanlines = [b'0'] * total
        for y in range(height):
            off = width * (y + 1)
            offset = total - off
            for x in range(0, width - 2, 3):
                # On inverse aussi la position du bleu et du rouge
                scanlines[off+x:off+x+3] = b(data[offset+x+2]), b(data[offset+x+1]), b(data[offset+x])
        return b''.join(scanlines)
 
# Un wrapper pour dumper un array de pixels dans un fichier
# Typiquement un truc qui aurait pu tenir dans une fonction au lieu d'une classe.
 
class MSSImage(object):
    ''' This is a class to save data (raw pixels) to a picture file.
    '''
 
    def __init__(self, data=None, width=1, height=1):
        self.data = data
        self.width = int(width)
        self.height = int(height)
 
        if self.data is None:
            raise ValueError('MSSImage: no data to process.')
        elif self.width < 1 or self.height < 1:
            raise ValueError('MSSImage: width or height must be positive.')
 
 
    # Tout le boulot se passe ici.
 
    def dump(self, output):
        ''' Dump data to the image file.
            Pure python PNG implementation.
            Image represented as RGB tuples, no interlacing.
            http://inaps.org/journal/comment-fonctionne-le-png
        '''
 
        # On ouvre le fichier en mode écriture binaire.
 
        with open(output, 'wb') as fileh:
            # Pour cette partie il faut connaitre les subtilités du format PNG
            # pour comprendre, ce qui n'est pas mon cas. Donc je vais faire
            # de la déduction au gros doigt mouillé.
 
            # Ca prend les données en pixel, ça en fait des morceaux de la bonne taille,
            # organisés en rangés de pixels puisqu'il y a range(height) rangés.
            to_take = (self.width * 3 + 3) & -4
            padding = 0 if to_take % 8 == 0 else (to_take % 8) // 2
            height, data = self.height, self.data
            scanlines = [b''.join([b'0', data[to_take*y:to_take*y+to_take-padding]]) for y in range(height)]
 
            # les "magic bytes", le marqueur du début de fichier qui indique
            # que c'est un fichier png
            magic = pack(b'>8B', 137, 80, 78, 71, 13, 10, 26, 10)
 
            # Les metadata du fichiers : taille de l'image, une somme de
            # controle, la profondeur de couleur, méthode de compression,
            # le mode d'entrelacement, etc.
            # Header: size, marker, data, CRC32
            ihdr = [b'', b'IHDR', b'', b'']
            ihdr[2] = pack(b'>2I5B', self.width, self.height, 8, 2, 0, 0, 0)
            ihdr[3] = pack(b'>I', zlib.crc32(b''.join(ihdr[1:3])) & 0xffffffff)
            ihdr[0] = pack(b'>I', len(ihdr[2]))
 
            # l'image en elle même avec un marker de départ, les pixels
            # et une somme de controle
            # Data: size, marker, data, CRC32
            idat = [b'', b'IDAT', b'', b'']
            idat[2] = zlib.compress(b''.join(scanlines), 9)
            idat[3] = pack(b'>I', zlib.crc32(b''.join(idat[1:3])) & 0xffffffff)
            idat[0] = pack(b'>I', len(idat[2]))
 
            # Les metadata à la fin du fichier qui sont vides.
            # Footer: size, marker, None, CRC32
            iend = [b'', b'IEND', b'', b'']
            iend[3] = pack(b'>I', zlib.crc32(iend[1]) & 0xffffffff)
            iend[0] = pack(b'>I', len(iend[2]))
 
            # On écrit le fichier, et on retourne son nom
            fileh.write(magic + b''.join(ihdr) + b''.join(idat) + b''.join(iend))
            return output
        return None
 
# Un exemple d'usage avec l'habituel if __name__ pour éviter de
# le lancer à l'import
if __name__ == '__main__':
 
    systems = {
        'Darwin' : MSSMac,
        'Linux'  : MSSLinux,
        'Windows': MSSWindows
    }
    try:
        MSS = systems[system()]
    except KeyError:
        err = 'System "{0}" not implemented.'.format(system())
        raise NotImplementedError(err)
 
    try:
        mss = MSS(debug=False)
 
        # One screen shot per monitor
        for filename in mss.save():
            print('File "{0}" created.'.format(filename))
 
        # A shot to grab them all :)
        for filename in mss.save(oneshot=True):
            print('File "{0}" created.'.format(filename))
    except Exception as ex:
        print(ex)
        raise

9 thoughts on “Explication de code : python-mss

  • Roro

    Je dirais même plus :

    Avant de m’éplucher cette grosse pomme, bien tranquillement;
    je ne dirai qu’un mot au sujet du commentage explicationnel : GENIAL ! Heu…C’est peut-être un peu trop fort non ?
    Allez…Alors: Parfait !
    Et, à: La ouaaaque barre !

  • Stéphane

    objectifs C -> objectiveC
    Ce n’est pas parce que le langage est pourri qu’il faut écorcher son nom ! ;-)

    Sinon, c’est une belle analyse de code.

  • Roro

    Qu’est-ce que c’est que ce souk ? Les messages font des petits tous seuls maintenant ?

  • blop

    Le lien “introduction au module ctypes” ne pointe pas vers la bonne page.

  • Tiger-222

    Wooooo ! ^^
    Merci Sam, ça fait plaisir que tu puisses passer un peu de temps dessus. C’est vrai que par endroit ça fait novice, en plus je connais ces “techniques”, mais je me suis tellement concentré sur les mécanismes propres à chaque OS que je suis passé à côté… En tout cas, ça me fera de quoi améliorer la bête :)
    Notamment, je ne voyais pas un moyen efficace et pas trop crade pour le coup des init() et des classes en général. J’avais dans l’idée de ne créer qu’une classe MSS() et de la déclarer 3 fois suivant l’OS. Si tu as une meilleure suggestion, je suis preneur !

    Sur quel OS tournes-tu ? Je suis curieux de savoir où ça plante.

    Encore merci beaucoup :D

  • Karn

    Hi

    I have been trying this but I get an image that is skewed. It could be that the png saving logic is not working?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.