Sam & Max » mac http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Explication de code : python-mss 9 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 <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
]]>
http://sametmax.com/explication-de-code-python-mss/feed/ 9
Ouvrir un fichier avec le bon programme en Python 16 http://sametmax.com/ouvrir-un-fichier-avec-le-bon-programme-en-python/ http://sametmax.com/ouvrir-un-fichier-avec-le-bon-programme-en-python/#comments Thu, 17 Oct 2013 10:09:59 +0000 http://sametmax.com/?p=7469 Votre logiciel doit permettre d’ouvrir un fichier avec un programme externe. Oui mais lequel ?

Les OS ont des réglages par défaut pour chaque type de fichier, et on peut demander “ouvrir le prog pour ce type de fichier par défaut”. Par exemple, moi, si je demande d’ouvrir un fichier vidéo, je m’attend à ce que VLC soit lancé.

Voilà comment faire ça en Python :

import subprocess
import sys
import os
 
def run_file(path):
 
    # Pas de EAFP cette fois puisqu'on est dans un process externe,
    # on ne peut pas gérer l'exception aussi facilement, donc on fait
    # des checks essentiels avant.
 
    # Vérifier que le fichier existe
    if not os.path.exists(path):
        raise IOError('No such file: %s' % path)
 
    # On a accès en lecture ?
    if hasattr(os, 'access') and not os.access(path, os.R_OK):
        raise IOError('Cannot access file: %s' % path)
 
    # Lancer le bon programme pour le bon OS :
 
    if hasattr(os, 'startfile'): # Windows
        # Startfile est très limité sous Windows, on ne pourra pas savoir
        # si il y a eu une erreu
        proc = os.startfile(path)
 
    elif sys.platform.startswith('linux'): # Linux:
        proc = subprocess.Popen(['xdg-open', path], 
                                 # on capture stdin et out pour rendre le 
                                 # tout non bloquant
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
    elif sys.platform == 'darwin': # Mac:
        proc = subprocess.Popen(['open', '--', path], 
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
    else:
        raise NotImplementedError(
            "Your `%s` isn't a supported operatin system`." % sys.platform)
 
    # Proc sera toujours None sous Windows. Sous les autres OS, il permet de
    # récupérer le status code du programme, and lire / ecrire sur stdin et out
    return proc

C’était le petit snippet sympas du jour !

P.S : si quelqu’un utilise BDSM BSD ou Solaris, je veux bien qu’il complète le snippet.

]]>
http://sametmax.com/ouvrir-un-fichier-avec-le-bon-programme-en-python/feed/ 16
Tiens, je suis toujours sur OSX 81 http://sametmax.com/tiens-je-suis-toujours-sur-osx/ http://sametmax.com/tiens-je-suis-toujours-sur-osx/#comments Wed, 19 Jun 2013 11:23:42 +0000 http://sametmax.com/?p=6419 Ceci est un post invité de coyote posté sous licence creative common 3.0 unported.

Attention: ce qui suit est un billet d’humeur personnel. Prenez le comme tel…

Je suis un linuxien. Pas un activiste, mais tout de même un fervent défenseur du logiciel libre et de Linux (oui pour moi écrire GNU/Linux partout c’est pédant et inutile – et ça te fait passer pour un gros nazi.). J’ai même créé une association de défense du libre, un LUG et fait switcher de très nombreuses personnes.

Mais alors, bordel, que fais-je sur1 OSX ?

1 Si quelqu’un a une règle pour décrire rapidement que l’on utilise un OS, en lieu et place de “sur Ubuntu”, “sous Windows’”, etc ; qu’il la balance car c’est juste insupportable.

Tout à commencé sans doute quand j’ai découvert Sublime Text (merci Sam!). J’ai alors rompu mon workflow entièrement libre pour y intégrer un outil proprio, mais fantastique et dont je me suis précipité pour payer la licence afin de remercier le développeur pour cette bouffée d’oxygène.

J’utilise régulièrement des Mac comme machine depuis plusieurs années (2003?) et ce pour plusieurs raisons:

  • C’est propre. Pas de sticker dégueulasse, de look plastique ou autre.
  • C’est une grande marque avec peu de modèles: l’assurance d’avoir un support pour Linux relativement rapidement.
  • C’est de bonne qualité (oui il y a toujours des exceptions), ça dure longtemps et ça se revend pas trop mal.
  • Le connecteur d’alim aimanté ; tous ceux qui ont déjà cassé un laptop à cause de ça comprendront…
  • Je fais un peu de dev multiplateforme (libre!) et il faut toujours tester/ajuster sur OSX.

Me voilà donc avec une machine Mac dont je suis satisfait, faisant tourner un Ubuntu que j’adore avec un dual boot que j’utilise dans les aéroports ou autre car la conso batterie est incomparable entre Ubuntu et OSX.

Linux a toujours eu des petits problèmes, la batterie dont je parlais, le WiFi, le suspend/hibernation, etc. Rien d’insurmontable pour quelqu’un de convaincu.

L’aigle noir

Soudain, je sais plus exactement vers quelle version, je crois que c’était la 10.10, Ubuntu a commencé à devenir de plus en plus pénible et les nouvelles versions n’ont fait qu’accroitre la frustration.

Le passage à Unity a été difficile, et le fait de tester d’autres alternatives (XFCE, Xmonad) permet de se rendre compte à quel point Ubuntu est intégré: beaucoup de composants ne sont plus vraiment interchangeable ou alors au prix de gros efforts de configurations voir de hacks. En bref, Ubuntu, c’est bien si tu y touches pas trop.

Au fil des versions, des nouveaux problèmes, des fonctionnalités perdues, toute tentative montrant à quel point Ubuntu s’écarte du Linux modulaire, j’ai décidé de franchir le Rubicon et de tester d’autres distros.

Pour faire (très) court :

  • Toutes les non-debian ne m’ont pas plu: la gestion des paquets est contre-productive pour moi qui ait des années d’apt dans les dents. Surtout, c’est lent comme la mort. Incroyablement lent en 2013 ; WTF?
  • Les Debian-Ubuntu-based: la plupart sont des customisations d’Ubuntu avec des paquets différents, des thèmes et des outils. J’ai pas vu d’avantages vraiment et toutes ont des petits désagréments supplémentaires, sans compter que les problèmes spécifiques sont dur à résoudre car ils ont peu d’utilisateurs.
  • Debian ; j’ai beaucoup aimé car c’est très simple, la modularité est là, la vitesse aussi ; c’est bien intégré. Malheureusement, la gestion non souple des paquets (soit tout est vieux, soit tout est jeune) couplé avec l’absence de PPA (les PPA, c’est génial!) font que j’ai renoncé.

Archlinux

Je suis donc resté sur Archlinux. J’avais utilisé un peu par le passé et ça correspondait bien à ce que je voulais actuellement: quelque chose de propre, documenté où je savais qu’en cas de problème, je pourrais l’identifier facilement.

Archlinux m’a pris du temps pour le configurer. C’est sans doute ce qui m’a le plus dérangé car j’utilise mon laptop pour travailler, pas pour geeker. Une journée de perdue à configurer la machine, c’est une journée de travail en moins avec des conséquences en sousous.

Mais j’étais satisfait d’Archlinux, c’était effectivement beaucoup plus simple et clair qu’Ubuntu. J’ai pu faire fonctionner des petites choses qui marchaient pas avec Ubuntu ; j’avais un Gnome à jour, etc.

Mais, car il y a toujours un mais ; Archlinux a aussi ses problèmes: il veut que vous soyez à jour, et vous avez pas intérêt à le contrarier ; mais en même temps, il faut vérifier ce qu’il fait donc re-perte de temps.

Au fil du temps, les problèmes des logiciels récents se faisaient plus frustrants ; une version qui marche ; une qui marche pas, etc.

Quand je me suis rendu compte que je devais redémarrer ma machine plus d’une fois par semaine, et que je perdais vraiment du temps avec des bêtises, je me suis dit “oh et puis merde, je vais mettre OSX!” (oui je reste relativement poli dans ma tête).

Le trou noir

J’ai formatté ma machine sur un coup de tête, un week-end, pour voir si vraiment Linux devenait de la fiente d’âne ou si c’était partout pareil. Je me suis dis, ce sera l’occasion de voir à quoi ressemble la concurrence, de voir comment se comporte cette machine dans son environnement naturel (genre voir ce que ça fait d’utiliser un SSD qui a couté la peau du Q). Je voulais tester ça une semaine, deux tout au plus.

C’était il y a des mois déjà. Je sais plus trop quand. Janvier ? Février ? Je ne m’en souviens pas car je ne pense plus à ma machine. Je l’oublie complètement.

Bien sûr, au début, je raillais ce système et ses perversions (installer GCC nécessite de passer par un Store à la con), et puis, passé les deux premiers jours de setup, je l’ai oublié. Le matin, j’arrive au boulot, la machine est là, allumée, prête, rapide, disponible. Je travaille, et je lui cloue le bec en partant, chose qui ne marchait pas avant.

Je peux me promener avec sans craindre pour la batterie, je peux utiliser le Wifi à tout moment (pas besoin de patcher le kernel dans un hôtel), je peux faire de la visio-conf sans jongler avec 2 outils PulseAudio, le trackpad marche à merveille, le tout démarre en moins de 10s, j’ai mon Sublime Text, mon terminal, mon ipython et toute sa clique, bref, c’est le bonheur.

Je ne sais pas quand je vais retourner sous Linux ; la simple idée de me retaper le setup à l’envers alors que je suis si productif maintenant me donne la nausée. Je n’ai même pas été tenté à la sortie du 13.04. Tout juste ais-je pris le temps de lancer le Live-CD dans VirtualBox.

Bien sûr, tout n’est pas rose, je connais bien les arcanes de Linux, mais pas celles d’OSX ; dès qu’il y a une dépendances bizarre dans un projet (genre opencv), je prends peur car tout n’est pas aussi bien packagé, ou aussi facilement compilable, etc mais finalement, ça n’arrive pas très souvent.

Vous m’avez lu jusqu’ici, bravo, je n’ai pas écrit tout ça pour vous convaincre de quoi que ce soit ou pour me justifier. Je suis toujours autant dérangé par Apple, je ne me sens pas à l’aise en utilisant OSX, mais je peux enfin utiliser ma machine comme l’outil de dev qu’il devrait être et rien d’autre ; c’est un soulagement très grand.

J’espère retrouver dans les commentaires de ce billet vos frustrations concernant Linux et pourquoi pas ce que vous faites pour y remédier (Punching Ball?).

]]>
http://sametmax.com/tiens-je-suis-toujours-sur-osx/feed/ 81
Testez vos Webapp sur iPhone / iPad avec le Simulator iOS – [Mac] 2 http://sametmax.com/testez-vos-webapp-sur-iphone-ipad-avec-le-simulator-ios-mac/ http://sametmax.com/testez-vos-webapp-sur-iphone-ipad-avec-le-simulator-ios-mac/#comments Mon, 14 Jan 2013 07:19:51 +0000 http://sametmax.com/?p=4120 La plupart d’entre vous le savent certainement mais moi je viens de le découvrir :)

Au début j’utilisais Ripple mais ce n’est pas de l’émulation, juste de l’encapsulage donc pas top.

Hier un pote me parle d’iOs Simulator , un émulateur iPhone / iPad livré avec Xcode. J’ai été surpris par la qualité de l’outil, un petit executable de 5 Mo qui émule très bien l’iPhone, l’iphone Retina ainsi que l’iPad.

Pour se le procurer c’est un peu galère, il faut télécharger Xcode (4GB!) dans le dev center, avoir un compte dev donc. ça se passe ici . C’est pas la mort mais y a plus simple…

Pour le trouver une fois installé..CHERCHEZ ! De tous les sites que j’ai parcouru aucun n’avait le même emplacement que moi, les dev de chez apple adorent faire des blagues. ça va donc dépendre de votre version de Xcode, de votre OS, du temps qu’il fait.
Un indice quand même c’est dans un des sous répertoires /Developer/ .
Moi il est dans /Developer/Platforms/iPhoneSimulator.platform/Developer/Applications et il se nomme “Simulateur iOS”

Pratique pour tester ses webapps sur iPhone / iPad

L’utilisation est nickel et j’ai pu corriger quelques bugs que je n’avais pas sur Ripple sous chrome ou même sous Safari. Ceci dit d’après un autre pote qui lui bosse en tant que dev sur les machines apple, rien ne vaut un appareil original, même avec l’émulateur il peut y avoir des problèmes.

Android a lui aussi un émulateur dans son SDK téléchargeable ici

Pour le lancer c’est un peu plus compliqué long car il faut configurer un appareil.

Une fois le SDK téléchargé, allez dans le répertoire adt-bundle-mac-x86_64/sdk/tools et lancez la commande ./android avd qui va avoir pour but de lancer le manager d’appareils, depuis ce manager vous allez pouvoir créer un nouvel appareil si il n’y en a pas déjà et le lancer (bouton “start”).

]]>
http://sametmax.com/testez-vos-webapp-sur-iphone-ipad-avec-le-simulator-ios-mac/feed/ 2
Effacer le cache DNS sous Mac Os http://sametmax.com/effacer-le-cache-dns-sous-mac-os/ http://sametmax.com/effacer-le-cache-dns-sous-mac-os/#comments Thu, 22 Nov 2012 21:17:30 +0000 http://sametmax.com/?p=3257 Si vous avez encore l’ancienne IP lorsque vous faites un ping sur un site c’est que votre cache DNS l’a mémorisée. Il faut parfois le réinitialiser (si vous changez votre site de serveur ou que vous ne pouvez plus accéder à un site en particulier).

Ouvrez un Terminal:
Dans Applications > Utilitaires > Terminal

et tapez:

lookupd -flushcache

Si ça ne marche pas essayez (pour les Mac plus récents):

dscacheutil -flushcache

Rappel:
la commande ping est toujours utile, c’est un outil qui sert à tester la conneciton entre 2 IPs.

ping yahoo.com
PING yahoo.com (72.30.38.140): 56 data bytes
64 bytes from 72.30.38.140: icmp_seq=0 ttl=42 time=275.592 ms
64 bytes from 72.30.38.140: icmp_seq=1 ttl=42 time=212.991 ms
64 bytes from 72.30.38.140: icmp_seq=2 ttl=42 time=255.605 ms
]]>
http://sametmax.com/effacer-le-cache-dns-sous-mac-os/feed/ 0
Internet Explorer 6, 7, 8, 9 Sous Mac/Linux Facilement avec VMWare Fusion 15 http://sametmax.com/internet-explorer-6-7-8-9-sous-maclinux-facilement/ http://sametmax.com/internet-explorer-6-7-8-9-sous-maclinux-facilement/#comments Sun, 18 Nov 2012 16:26:36 +0000 http://sametmax.com/?p=3155 Quand on developpe des applications web on doit s’assurer de la compatibilité de ces dernières avec les différents navigateurs du marché. Jusqu’à présent sous MAC/Linux c’était pas évident de tester son application sur Internet Explorer.

J’ai essayé cross-over sous MAC qui ne vaut pas le coup à mes yeux (trop lent, plantage, pas compatible pour certaines versions d’IE, etc.), Wine sous Linux dont cross-over est un dérivé il me semble, mêmes galères.
Il y a aussi IETester qui a l’air pas mal pour avoir toutes les versions d’IE sur un seul navigateur mais il ne marche que sous Windows.

Les choses ont changées et Microsoft propose désormais gratuitement des Images de son Os Windows avec la version IE qui va bien.
La seule contrainte est qu’il faut posséder VMWare (ou VirtualBox GRATUIT), je conseille la version 5 pour 50 euros qui a un mode magique, le mode “Unity” qui fusionne votre Os émulé avec votre Os hôte, j’ai dans mon dock Mac une icône IE8, c’est trop mignon, fluide et pratique.
Une chose non négligeable c’est le copier/coller qui marche entre les deux OS.

Pratique pour voir si son site fonctionne sous IE depuis son Mac


Installation:

Rendez-vous chez Microsoft et téléchargez la version qui vous convient

Une fois les archives téléchargées vous les décompressées (avec The unarchiver sous Mac)

Dans VMWare vous sélectionnez “Ajouter > Importer” et vous importer l’image téléchargée, c’est celle qui comporte l’extension VHD (ex: Win7_IE8.vhd). Il va vous demander de convertir l’image etc, faites mouliner.

Attention:

Le mot de passe des OS est Password1 à taper en clavier QWERTY, si vous n’y arrivez pas, cliquez à gauche à l’écran d’accueil Windows sur l’espèce de petite roue et sélectionnez “Show keyboard”, un clavier virtuel va s’afficher et vous pourrez cliquer sur les touches.

Passez l’activation de windows il n’y en a pas besoin.

Pour éviter que sa copie arrive à expiration le mieux est de faire un Snapshot (sauvegarde de l’état de la Machine virtuelle à un instant T) dès la première installation de votre machine virtuelle.

Je dois dire que c’est plutôt sympa, car je peux coder sur mon Mac tout en testant le résultat sous IE comme si c’était une appli Mac.

NB: Si vous lancez un serveur web sous votre Mac et n’arrivez pas à y acceder depuis le navigateur IE, lancez le serveur sur le port 80 et l’ip de votre machine (192.168…..)

]]>
http://sametmax.com/internet-explorer-6-7-8-9-sous-maclinux-facilement/feed/ 15