Retrouvez cet article dans : Linux Magazine 85
Freevo est avant tout un logiciel multimédia permettant aisément de visualiser des films, des photos ou d’écouter de la musique. Mais on peut lui adjoindre toutes sortes de greffons pour divers usages. Dans cette optique, on peut programmer ses propres greffons, afin d’étendre ses capacités à d’autres domaines.
reevo est programmé en Python, ses greffons le sont également. Pour aborder cet article, il faut avoir des connaissances, au moins basiques, en Python. Elles sont considérées comme acquises.
Nous verrons comment programmer un greffon simple, en utilisant les fonctionnalités intégrées dans la version par défaut de Freevo. Nous prendrons pour exemple l’implémentation d’une fonctionnalité de photocopie, au moyen d’un scanner et d’une imprimante. Il nous faudra afficher un aperçu de l’image à photocopier, afin de vérifier qu’elle est conforme à nos désirs, pour choisir ensuite le type de photocopie à exécuter : couleur, niveaux de gris ou noir et blanc.

Fig. 1 : Principe de fonctionnement d’un système de gestion de version
Où placer le greffon ?
Étant donné que Freevo est un module du langage Python, ses fichiers sources peuvent être trouvés dans le répertoire Site Packages de Python (sous Debian GNU/Linux, dans le répertoire /usr/lib/python2.3/site-packages/freevo/). C’est là que se trouve le répertoire plugins/, contenant tous les greffons disponibles pour Freevo.
Le fonctionnement de Freevo nous oblige à placer le greffon dans ce répertoire. Cependant, celui-ci appartenant à l’utilisateur root, nous pourrions être tentés de programmer directement en super-utilisateur ; comme chacun le sait, c’est une très mauvaise habitude (bien que, dans ce cas, seule l’écriture du fichier se ferait en root ; son exécution se faisant toujours avec l’utilisateur qui lance Freevo).
Pour contourner cette limitation, nous allons créer le greffon dans un répertoire utilisateur, pour ensuite créer un lien symbolique dans le répertoire de Freevo. Nous créons donc le fichier ~/photocopie.py, avec un lien de /usr/lib/python2.3/site-packages/freevo/photocopie.py vers ce fichier.
Ajouter une entrée au menu principal
Pour accéder à notre photocopieuse sous Linux, il faut d’abord ajouter une entrée dans le menu principal de Freevo. Profitons-en pour créer un squelette de greffon.
# -!- coding: utf-8 -!- import plugin from menu import Menu class PluginInterface(plugin.MainMenuPlugin):    def __init__(self):        plugin.MainMenuPlugin.__init__(self)    def items(self, parent):        return [ SaneMainMenu(parent) ] class SaneMainMenu(Item):    def __init__(self, parent):        Item.__init__(self, parent, skin_type=’image’)        self.name  = _(‘Photocopier un document’)        self.menus = [ ]    def actions(self):        items = [ ( self.create_mainmenu , _(‘Photocopier un document’) ) ]        return items    def create_mainmenu(self, arg=None, menuw=None):        sane_menu = Menu(_(‘Photocopieuse’), self.menus)        menuw.pushmenu(sane_menu)        menuw.refresh()
La classe SaneMainMenu est la pièce maîtresse de notre greffon. Elle hérite de Item pour être représentée comme une entrée du menu principal. Pour qu’elle fonctionne, il faut que la fonction actions() existe et retourne une liste d’actions. En ce qui nous concerne, cette liste ne contient qu’une action, qui affiche le menu principal de notre greffon.
Ce menu principal est une instance de Menu, classe de base dans Freevo. Cette classe prend en argument un titre et une liste d’éléments qui constituent le menu. Pour l’instant, cette liste (self.menus) est vide.
Lorsque l’entrée du menu principal Photocopier un document est sélectionnée, la première fonction, dans la liste retournée par actions(), est exécutée. Dans notre cas, c’est l’affichage du menu principal (create_mainmenu()).
Le menu de notre photocopieuse
La liste self.menus doit contenir des instances de la classe MenuItem. En l’occurrence, voici le code dont nous avons besoin :
import plugin from item import Item from menu import Menu, MenuItem [...] class SaneMainMenu(Item):    def __init__(self, parent):        Item.__init__(self, parent, skin_type=’image’)        self.name  = _(‘Photocopier un document’)        self.menus = [            MenuItem(_(‘Apercu’), self.preview, skin_type=’image’),            MenuItem(_(‘Photocopier (couleur)’), self.print_color),            MenuItem(_(‘Photocopier (niveaux de gris)’), self.print_gray),            MenuItem(_(‘Photocopier (noir et blanc)’), self.print_bw)        ]    [...]    def preview(self, arg=None, menuw=None):        pass    def print_color(self, arg=None, menuw=None):        pass    def print_gray(self, arg=None, menuw=None):        pass    def print_bw(self, arg=None, menuw=None):        pass
Chacune des instances de MenuItem permet d’accéder à une fonction différente de la classe SaneMainMenu. Lorsqu’une entrée est sélectionnée dans le menu, la fonction correspondante est exécutée, avec d’éventuels arguments (non utilisés ici), ainsi que la variable menuw, correspondant au menu à partir duquel la fonction est appelée.
Initialisation
Nous avons besoin d’un répertoire temporaire dans lequel stocker des informations ; il faut donc l’initialiser. Par ailleurs, nous allons mettre en place une image par défaut lorsque aucun aperçu n’est disponible.
[...]
import base64, os, tempfile
[...]
class SaneMainMenu(Item):
def __init__(self, parent):
Item.__init__(self, parent, skin_type=’image’)
self.name = _(‘Photocopier un document’)
self.menus = [
MenuItem(_(‘Apercu’), self.preview, skin_type=’image’),
MenuItem(_(‘Photocopier (couleur)’), self.print_color),
MenuItem(_(‘Photocopier (niveaux de gris)’), self.print_gray),
MenuItem(_(‘Photocopier (noir et blanc)’), self.print_bw),
]
self.tmp_dir = None
self.reinit()
def reinit(self):
if self.tmp_dir:
os.system(‘rm -fr %s’ % self.tmp_dir)
self.tmp_dir = tempfile.mkdtemp(‘’, ‘tmp-freevo-sane-’)
self.set_preview(None, menuw=menuw)
def set_preview(self, file_=’preview’, menuw=None):
"""
file_ = None if you want to remove the preview
"""
if not file_:
phold = file(os.path.join(self.tmp_dir, ‘placeholder.png’), ‘w’)
phold.write(base64.decodestring(_placeholder_data))
phold.close()
file_ = ‘placeholder’
image = os.path.join(self.tmp_dir, ‘%s.png’ % file_)
for menu in self.menus:
menu.image = image
if menuw:
menuw.refresh()
[...]
_placeholder_data = \
"""[...]
[...]"""
La fonction d’initialisation appelle reinit(), qui crée le répertoire temporaire (en effaçant l’ancien répertoire, s’il existe), et qui appelle set_preview() pour afficher l’image par défaut.
Pour simplifier la distribution de ce greffon, nous choisissons d’embarquer cette image dans le code Python : il suffit d’encoder l’image voulue en base64 avec la fonction base64.encodestring(file(‘<image>.png’).read()) et de placer le résultat dans _placeholder_data.
Lorsque la fonction set_preview() est exécutée, elle remplace l’image du menu de notre photocopieuse. Si la variable file_ est None, l’image par défaut est affichée.
Dans les menus habituels de Freevo, cette image est différente pour chacune des entrées ; c’est pratique, par exemple, pour une liste de musiques, où chacune a sa pochette. Dans notre cas, nous attribuons la même image à toutes les entrées du menu.
En changeant la valeur de la variable image de chacune des entrées du menu (for menu in self.menus), nous nous assurons que la nouvelle image sera chargée.
Si nous nous étions contentés d’écrire le contenu de l’image dans un fichier existant, l’affichage ne se mettrait pas à jour : les images sont en cache, les modifications sur disque ne sont donc pas prises en compte.
Une fois que l’image est modifiée, le menu est rafraîchi avec la fonction refresh().
Traitement des images
SANE est l’infrastructure de numérisation sous GNU/Linux. Il offre, entre autres, la commande scanimage, qui permet de numériser un document en ligne de commande.
C’est ce programme qui sera appelé par l’intermédiaire de la fonction os.system(). Pour le traitement des images, la commande convert de la suite logicielle ImageMagick sera utilisée.
Les commandes suivantes nous seront utiles :
Scanimage ne représente qu’un élément de la gamme d’outils SANE, des interfaces graphiques et greffons pour The Gimp sont également disponibles.
scanimage --resolution 75 preview.pnmpuisconvert preview.pnm preview.png: acquisition d’un aperçu (75 DPI) ;scanimage --resolution --mode Gray 300 final.pnmpuisconvert final.pnm -density 300 -units PixelsPerInch final.ps: acquisition du document en niveaux de gris et conversion en PostScript ;lp: impression du document PostScript.
Aperçu
Maintenant que nous avons réalisé un menu, les squelettes des fonctions et de quoi afficher une image, nous pouvons nous intéresser à l’acquisition d’un aperçu...
Nous allons donc compléter la fonction preview() :
[...]
import config
from gui import PopupBox
[...]
def preview(self, arg=None, menuw=None):
self.reinit(menuw=menuw)
pnmfile = os.path.join(self.tmp_dir, ‘preview.pnm’)
pngfile = os.path.join(self.tmp_dir, ‘preview.png’)
popup = PopupBox(text=_(‘Acquisition de l apercu’))
popup.show()
os.system(‘%s --resolution %d > %s’ % (config.SANE_SCANIMAGE_PATH, config.SANE_PREVIEW_RESOLUTION, pnmfile))
os.system(‘%s %s %s’ % (config.SANE_CONVERT_PATH, pnmfile, pngfile))
self.set_preview(menuw=menuw)
popup.destroy()
Tout d’abord, avant d’acquérir l’aperçu, le répertoire temporaire et l’affichage sont réinitialisés (self.reinit()). Un message surgissant (popup) est présenté, gelant ainsi l’interface et nous informant que l’aperçu est en cours d’acquisition ; c’est le rôle de la fonction show().
La fonction os.system() est ensuite utilisée pour appeler scanimage (l’aperçu est tout simplement une image en basse résolution). Une fois que ce dernier est enregistré, il est converti au format PNG, puis affiché dans le cadre prévu à cet effet, avec la fonction set_preview(). Le message d’attente est ensuite supprimé, l’utilisateur a de nouveau le contrôle sur son système.
Dans cette fonction, nous accédons à des variables de la configuration. Ces variables sont spécifiées dans le fichier local_conf.py. Nous verrons cela en détail un peu plus loin.
Photocopie
La fonction print_(), dont le but est de photocopier le document qui est dans le scanner, est créée. Les trois fonctions que nous avons définies plus haut appellent celle-ci, avec des arguments différents.
def print_color(self, arg=None, menuw=None):
self.print_(mode=’Color’)
def print_gray(self, arg=None, menuw=None):
self.print_(mode=’Gray’)
def print_bw(self, arg=None, menuw=None):
self.print_(mode=’Lineart’)
def print_(self, mode=’Color’):
pnmfile = os.path.join(self.tmp_dir, ‘final.pnm’)
psfile = os.path.join(self.tmp_dir, ‘final.ps’)
popup = PopupBox(text=_(‘[1/3] Acquisition de l image’))
popup.show()
os.system(‘%s --resolution %d --mode %s > %s’ % (config.SANE_SCANIMAGE_PATH, config.SANE_ACQUIRE_RESOLUTION, mode, pnmfile))
popup.destroy()
popup = PopupBox(text=_(‘[2/3] Conversion’))
popup.show()
os.system(‘%s %s -density %d -units PixelsPerInch %s’ % (config.SANE_CONVERT_PATH, pnmfile, config.SANE_ACQUIRE_RESOLUTION, psfile))
popup.destroy()
popup = PopupBox(text=_(‘[3/3] Impression’))
popup.show()
os.system(‘%s %s’ % (config.SANE_PRINT_COMMAND, psfile))
popup.destroy()
La procédure est la même qu’avec l’acquisition de l’aperçu : affichage de popups, exécution de commandes...
Configuration
Freevo, lors de son démarrage, lit le fichier local_conf.py. Toutes les variables de ce fichier sont intégrées dans le module config. Par conséquent, toute configuration effectuée dans ce fichier se trouve dans ce module. Les éléments que nous utilisons ici sont :
SANE_SCANIMAGE_PATH: chemin vers la commande scanimage ;SANE_CONVERT_PATH: chemin vers la commandeconvertÂ;SANE_PREVIEW_RESOLUTION: résolution pour l’aperçu ;SANE_ACQUIRE_RESOLUTION: résolution pour la photocopie ;SANE_PRINT_COMMAND: commande d’impression.
Finalement...
Finalement, le code complet de ce petit greffon est le suivant :
# -!- coding: utf-8 -!- import base64, os, tempfile import config, plugin from gui import PopupBox from item import Item from menu import Menu, MenuItem class PluginInterface(plugin.MainMenuPlugin):    def __init__(self):        plugin.MainMenuPlugin.__init__(self)    def items(self, parent):        return [ SaneMainMenu(parent) ] class SaneMainMenu(Item):    def __init__(self, parent):        Item.__init__(self, parent, skin_type=’image’)        self.name  = _(‘Photocopier un document’)        self.menus = [            MenuItem(_(‘Apercu’), self.preview, skin_type=’image’),            MenuItem(_(‘Photocopier (couleur)’), self.print_color),            MenuItem(_(‘Photocopier (niveaux de gris)’), self.print_gray),            MenuItem(_(‘Photocopier (noir et blanc)’), self.print_bw)        ]        self.tmp_dir = None        self.reinit()    def actions(self):        items = [ ( self.create_mainmenu , _(‘Photocopier un document’) ) ]        return items    def set_preview(self, file_=’preview’, menuw=None):        """        file_ = None if you want to remove the preview        """        if not file_:            phold = file(os.path.join(self.tmp_dir, ‘placeholder.png’), ‘w’)            phold.write(base64.decodestring(_placeholder_data))            phold.close()            file_ = ‘placeholder’        image = os.path.join(self.tmp_dir, ‘%s.png’ % file_)        for menu in self.menus:            menu.image = image        if menuw:            menuw.refresh()    def create_mainmenu(self, arg=None, menuw=None):        sane_menu = Menu(_(‘Photocopieuse’), self.menus)        menuw.pushmenu(sane_menu)        menuw.refresh()    def preview(self, arg=None, menuw=None):        self.reinit(menuw=menuw)        pnmfile = os.path.join(self.tmp_dir, ‘preview.pnm’)        pngfile = os.path.join(self.tmp_dir, ‘preview.png’)        popup = PopupBox(text=_(‘Acquisition de l apercu’))        popup.show()        os.system(‘%s --resolution %d > %s’ % (config.SANE_SCANIMAGE_PATH, config.SANE_PREVIEW_RESOLUTION, pnmfile))        os.system(‘%s %s %s’ % (config.SANE_CONVERT_PATH, pnmfile, pngfile))        self.set_preview(menuw=menuw)        popup.destroy()    def print_color(self, arg=None, menuw=None):        self.print_(mode=’Color’)    def print_gray(self, arg=None, menuw=None):        self.print_(mode=’Gray’)    def print_bw(self, arg=None, menuw=None):        self.print_(mode=’Lineart’)    def print_(self, mode=’Color’):        pnmfile = os.path.join(self.tmp_dir, ‘final.pnm’)        psfile = os.path.join(self.tmp_dir, ‘final.ps’)        popup = PopupBox(text=_(‘[1/3] Acquisition de l image’))        popup.show()        os.system(‘%s --resolution %d --mode %s > %s’ % (config.SANE_SCANIMAGE_PATH, config.SANE_ACQUIRE_RESOLUTION, mode, pnmfile))        popup.destroy()        popup = PopupBox(text=_(‘[2/3] Conversion’))        popup.show()        os.system(‘%s %s -density %d -units PixelsPerInch %s’ % (config.SANE_CONVERT_PATH, pnmfile, config.SANE_ACQUIRE_RESOLUTION, psfile))        popup.destroy()        popup = PopupBox(text=_(‘[3/3] Impression’))        popup.show()        os.system(‘%s %s’ % (config.SANE_PRINT_COMMAND, psfile))        popup.destroy()    def reinit(self, arg=None, menuw=None):        if self.tmp_dir:            os.system(‘rm -fr %s’ % self.tmp_dir)        self.tmp_dir = tempfile.mkdtemp(‘’, ‘tmp-freevo-sane-’)        self.set_preview(None, menuw=menuw) _placeholder_data = \ """[...] [...]"""
Pour activer ce greffon, les lignes suivantes sont ajoutées à local_conf.py :
plugin.activate(‘photocopie’, level = 60) SANE_SCANIMAGE_PATH=’/usr/bin/scanimage’ SANE_CONVERT_PATH=’/usr/bin/convert’ SANE_PREVIEW_RESOLUTION=75 # résolution minimale de mon scanner SANE_ACQUIRE_RESOLUTION=300 # résolution d’impression SANE_PRINT_COMMAND=’lp -d hl2030’ # impression sur une imprimante precise
Améliorations
A partir de cette base, beaucoup d’améliorations sont envisageables :
- Il est possible de s’affranchir des menus habituels pour composer quelque chose de spécifique à la photocopie (cf. le greffon weather).
- Différentes fonctionnalités pourraient être ajoutées, telles que le réglage de la luminosité et du contraste, le nombre de copies, etc.
- On peut également envisager une interface pour " découper " l’image à numériser et n’en imprimer qu’une partie, en l’agrandissant par exemple.
Nous constatons donc que Freevo ne pose pas de limite à l’imagination. Il est assez flexible pour s’adapter à nos besoins et non le contraire.
Retrouvez cet article dans : Linux Magazine 85

