screenshot – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Explication de code : python-mss http://sametmax.com/explication-de-code-python-mss/ http://sametmax.com/explication-de-code-python-mss/#comments Sat, 22 Feb 2014 14:19:02 +0000 http://sametmax.com/?p=9219 envoyez nous les codes que vous ne pigez pas". Celle-ci est un peu particulière.]]> 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 .
    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:

    

    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' '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

]]>
http://sametmax.com/explication-de-code-python-mss/feed/ 9 9219