Vous cherchez peut être les archives ?

Présentation de Dotconf

Dotconf est un parseur de fichier de configuration avancé pour Python, et accessoirement aussi, un projet personnel sur lequel je travaille depuis quelques semaines.

Il existe un certain nombre de parseur de fichier configuration en Python, ConfigParser, qui est un package intégré à la bibliothèque standard du langage, ConfigObj, qui reprend la syntaxe de ConfigParser (celle des fichiers .ini en fait) et qui apporte les sections imbriquées et la validation à partir d'un schéma, YAML, JSON, ou XML qui sont plus des formats de sérialisation que des formats de configuration. Aucun de ces formats ne me convient vraiment, certains sont trop simples (ConfigParser), ont des syntaxes bizarres (ConfigObj pour l'imbrication), d'autres ont une syntaxe trop restrictive pour de la configuration (JSON), les autres ne me semblent pas assez adaptés à la lecture ou l'écriture par les humains.

Mon format idéal devait rassembler à ceci :

  • Une syntaxe simple, claire, et peu ambigüe
  • Un typage au sein même de la syntaxe ("42" != 42) et un nombre correct de types primitifs (nombre, booléen, chaine de caractères, liste)
  • Des sections imbriquables
  • Un système de validation du fichier de configuration

Ne trouvant pas mon bonheur, je me suis attelé à son développement. Ainsi est né Dotconf.

Pour commencer, voici un exemple de configuration possible avec Dotconf pour un serveur web imaginaire :

daemon = yes
pidfile = '/var/run/myapp.pid'
interface = '10.0.10.0:80', '127.0.0.1:8080'
interface_ssl = '0.0.0.0:443'
http_buffer = 8Ki

host 'example.org' {
    path '/' {
        rate_limit = 30
    }
}

host 'protected.example.org' {
    enable_ssl = yes

    path '/files' {
        enable_auth = yes
        user 'foo' {
            password = 'bar'
        }
        user 'bar' {
           password = 'foo'
        }
    }
}

include "/etc/myserver.d/*.conf"

On remarque que :

  • Les chaînes de caractères utilisent les quotes (ou les double quotes, au choix) comme délimiteur, il est donc possible de différencier une chaine contenant un nombre du nombre en lui-même.
  • Les booléens utilisent les mots clé yes et no.
  • Il est possible d'utiliser des unités pour les nombres (8Ki == 8192)
  • Les listes sont définies par plusieurs éléments séparés par des virgules. Les listes peuvent contenir des nombres, chaines, et booléens (les trois types scalaires primitifs), il n'est par contre pas possible d'imbriquer des listes (mon avis est que les listes de listes sont des structures trop complexes pour de la configuration, il est possible de les émuler de manière plus claire avec des sections, par exemple ici avec les users).
  • Une section est un simple nom suivi d'un bloc délimité par des accolades.
  • Une section peut recevoir une liste de valeurs spéciales que j'ai appelé l'argument, cet argument est défini entre le nom de la section et le début du bloc.
  • Il est très simple d'inclure d'autres fichiers de configuration à l'aide de la directive "include".

La fonctionnalité qui reste la plus intéressante selon moi est la validation d'un fichier de configuration selon un schéma. Celle-ci permet de définir simplement les clés, leur contenu, et l'arborescence de sections que doit contenir votre fichier de configuration.

Voici un petit exemple :

from dotconf.schema import many, once
from dotconf.schema.containers import Section, Value
from dotconf.schema.types import Boolean, Integer, Float, String

# Schema definition:

class UserSection(Section):
    password = Value(String())
    _meta = {'repeat': many, 'unique': True}

class PathSection(Section):
    rate_limit = Value(Float(), default=0)
    enable_auth = Value(Boolean(), default=False)
    user = UserSection()

class VirtualHostSection(Section):
    base_path = Value(String())
    enable_ssl = Value(Boolean(), default=False)
    path = PathSection()
    _meta = {'repeat': many, 'unique': True}

class MyWebserverConfiguration(Section):
    daemon = Value(Boolean()default=False)
    pidfile = Value(String(), default=None)
    interface = Value(String(), default='127.0.0.1:80')
    interface_ssl = Value(String(), default='127.0.0.1:443')
    host = VirtualHostSection()
    http_buffer = Value(Float(), default=1024)

Quelques informations :

  • Chaque schéma de section est définie dans une classe, le fichier de configuration étant lui-même une section.
  • Chaque clé possible est un attribut statique de cette classe.
  • Il y a une distinction entre le container et le type de la valeur. Là où le conteneur peut être une valeur (scalaire), une liste, ou une section, le type sera un entier, une chaine ou un booléen.
  • Ça n'est pas forcément visible dans l'exemple, mais il existe des types plus complexes que les Integer, String ou Booleans, par exemple les Url, IPAddress ou Regex.
  • Les sous-sections sont définies dans des classes puis utilisées dans les sections parentes comme n'importe quel autre container.
  • Des méta-informations permettent de définir plusieurs contraintes sur les containers section, comme par exemple si elles doivent être répétées, le schéma de l'argument ou encore si les clés inconnues doivent être gardées.

Il est ensuite possible de parser et valider votre configuration de cette manière :

>>> from dotconf import Dotconf
>>> from myconfschema import MyWebserverConfiguration
>>> parsed_conf = Dotconf(conf, schema=MyWebserverConfiguration)
>>> print 'daemon:', parsed_conf.get('daemon')
daemon: True
>>> for vhost in parsed_conf.subsections('host'):
>>>     print vhost.args[0]
>>>     if vhost.get('enable_ssl'):
>>>         print '  SSL enabled'
>>>     for path in vhost.subsections('path'):
>>>         print '  ' + path.args[0]
>>>         if path.get('enable_auth'):
>>>             print '    Following users can access to this directory:'
>>>             for user in path.subsections('user'):
>>>                 print '     - ' + user.args[0]
>>>
example.org
  /
protected.example.org
  SSL enabled
  /files
    Following users can access to this directory:
      - foo
      - bar

Le tout est disponible sous licence MIT, sur le pipy, il est donc possible de l'installer avec pip :

pip install dotconf

Un paquet est aussi disponible pour Archlinux sur AUR (merci à Sébastien), et le paquet Debian sera disponible dans la prochaine version.

Il est évidemment possible de contribuer, le code source est disponible via ces moyens, et les patches et remarques sont les bienvenues :

DocBucket, version 0.1

Je ne m'attendais pas à un tel engouement suite à l'annonce dans mon précédant billet, DocBucket, où la gestion facile de son joyeux bordel. Je dois dire que je suis assez content que ce développement puisse intéresser d'autres personnes que moi.

Je me suis donc attelé au packaging de l'application afin de rendre sa mise en place un peu moins acrobatique. Pour le moment, je ne me suis occupé que des packages Python, mais des packages pour au moins Debian devraient arriver assez vite. Au passage, si un mainteneur ou développeur Debian passe par ici est souhaite me donner un coup de main pour rendre disponible le package sur les dépôts officiels, ce serait avec plaisir :-).

Une documentation, localisation en français et l'ajout de fonctionnalités arriveront eux aussi très bientôt. En attendant, voici un petit tutoriel d'installation sur Debian Squeeze, les utilisateurs des autres distributions pourront, je pense, aisément l'adapter pour leurs distributions.

Installation des dépendances

Les dépendances sont assez nombreuses, heureusement, elles sont pratiquement toutes packagées par Debian :

# aptitude install python-django python-imaging python-whoosh python-pymongo cuneiform ghostscript exactimage

Malheureusement, mongoengine n'est pas disponible sur Debian, il faut donc l'installer depuis le package python, vous pouvez utiliser easy_install (package Debian python-setuptools) ou pip (package python-pip) pour l'installer :

Il vous faudra aussi un serveur faisant tourner mongodb, ça peut être la même machine ou une autre :

# aptitude install mongodb

Installation de DocBucket

Vous avez normalement déjà installé easy_install ou pip pour l'étape d'installation de mongoengine, il vous suffit donc de les utiliser pour installer DocBucket à son tour :

# easy_install docbucket

ou

# pip install docbucket

Création de la base de données

Commençons par créer une base de données et un utilisateur sur le mongodb que nous venons d'installer.

# mongo
MongoDB shell version: 1.6.3
connecting to: test
> use docbucket
switched to db docbucket
> db.addUser('docbucket', 'secret')
{
	"user" : "docbucket",
	"readOnly" : false,
	"pwd" : "2aed2908ebee85906effb373b061d354"
}

C'est tout !

Création du projet Django

Placez vous dans le répertoire que vous voulez utiliser pour stocker la configuration de votre instance de DocBucket. Vous pouvez par exemple créer un nouvel utilisateur et la mettre dans sa homedir.

Exécutez la commande suivante

$ django-admin startproject mydocbucket

(il est important que le nom du projet ne soit pas “docbucket”, l'application Django portant déjà ce nom)

Le script va vous créer un répertoire mydocbucket contenant l'arborescence standard des projets Django :

.
|-- __init__.py 
|-- manage.py
|-- settings.py
`-- urls.py

Ce qui nous intéresse ici, ce sont les fichiers settings.py et urls.py. Le premier contient la configuration du projet Django, le second, le mapping d'urls. Si vous n'êtes pas familier avec Django, copiez simplement les exemples contenus dans le répertoire example de l'archive source.

Les options importantes à éditer dans le settings.py sont :

  • MONGO_* : configuration de la connexion à MongoDB.
  • MEDIA_ROOT : chemin absolu vers le répertoire contenant les médias. Celui-ci dépend de votre installation, le fichier d'exemple contient le chemin vers le répertoire media lors de mon installation pour vous aider à le trouver.
  • INCOMING_DIRECTORY : le chemin absolu vers le dossier où stocker les .tiff en sortie de votre scanner.
  • WHOOSH_INDEX : chemin absolu vers le dossier stockant les indexes de la base de recherche, vous pouvez le mettre dans le dossier courant par exemple.

Le reste ne devrait pas avoir à être changé.

Exécutez ensuite la commande python manage.py create_index pour créer l'index de la base de recherche.

À ce stade, DocBucket est installé et vous pouvez le tester en utilisant la commande python manage.py runserver avant d'ouvrir http://127.0.0.1:8000 dans votre navigateur. L'utilisation du serveur web embarqué à Django n'est pas recommandé pour la production, toutefois, si vous êtes le seul utilisateur de l'application, ça ne devrait pas poser de problème. Si vous souhaitez continuer par une installation en passant par un serveur web, je vous redirige vers les multiples documentations à ce sujet.

Pour terminer, les contributions sont évidemment ouvertes : la licence finalement choisie est la GPL3, j'ai aussi mis en place une forge pour ce projet, vous pouvez donc voir les sources, les modifier, m'envoyer des patchs ou même écrire sur le dépôt Git (moyennant un petit mail), mais aussi utiliser le bugtracker pour soumettre bugs et autres idées.

DocBucket, où la gestion facile de son joyeux bordel

Fiouf, ça faisait plus d'un an que je n'avais pas écrit ici. C'est pourtant pas les idées de billet qui me manquaient, mais vous savez, la flemme... Bref, je vais aujourd'hui présenter un petit outil perso : DocBucket. Ce nom cache en fait un outil de GED orienté paperasse personnelle.

le logo de docbucket

Je sais pas comment vous vous en sortez, mais moi, la paperasse, ça me gonfle pas mal et la trier, c'est encore pire. Ma méthode actuelle consiste à faire un tas qui grandit doucement sur mon bureau et de le fourrer dans une boite quand il m'emmerde, puis de recommencer avec un nouveau tas. C'est une méthode assez efficace en écriture, mais en lecture et particulièrement en recherche, c'est juste l'horreur.

Me mettre au tri n'était pas une solution, d'autant plus que la recherche n'aurait pas vraiment été plus facile. Je me suis alors mis à imaginer une solution informatique de gestion de mes papiers. Dématérialiser sa paperasse apporte vraiment beaucoup davantage, la recherche rapide et par mot clés, sans avoir à fouiller dans des gros cartons, le fait de pouvoir y accéder de partout, mais surtout, la sauvegarde facile de ses documents.

Tout le problème est de faire en sorte que la tâche ne soit pas trop pénible, j'ai donc simplifié au maximum le "workflow" d'ingestion d'un document :

  1. La première phase consiste à scanner le document, pour ça, rien d'excentrique, j'utilise SANE et scanbuttond qui me permettent de scanner et post-processer un document en appuyant juste sur un bouton de mon scanner. Le document ainsi scanné est déposé au format .tiff dans un dossier d'importation.
  2. La seconde étape consiste à ingérer le document dans la base de données. Pour ça, je passe par une interface web (la même que pour la consultation des documents en fait) et je choisis un titre pour le document, une catégorie et les fichiers .tiff qui feront partie du document. A ce moment, il est possible de re-trier l'ordre des pages du document facilement par drag'n drop.
  3. À la validation, le document est traité par l'OCR, les mots clés sont enregistrés en base de recherche, les .tiff sont agrégés dans un PDF et au passage, le texte est rendu "selectionnable". Enfin, le PDF est enregistré par le serveur.
Création d'un nouveau document

L'ingestion d'un document passe donc par assez peu de manipulations.

Au niveau technique, j'utilise Python comme langage de programmation, Django pour le framework web, JQuery comme framework JavaScript, une base de donnée MongoDB pour stocker les documents (et leurs métadonnées), Woosh pour la base de recherche, PIL pour le traitement des images, Cuneiform pour l'OCR, hocr2pdf pour créer le PDF selectionnable, et GhostScript pour assembler les différentes pages du PDF.

Voici quelques captures supplémentaires :

Listing des documents

Recherche d'un mot clé

N'ayant pas prévu à la base de le publier, j'ai naturellement suivi la méthode de la rache pour développer ce projet. Il n'y a donc pas de Git, pas de version, ou de doc d'installation. Voici l'archive du DocBucket tel qu'installé sur mon serveur, modulo les trucs sensibles. Si je vois que ce projet intéresse vraiment quelques personnes, il est probable que je prenne un peu de temps pour le rendre plus facilement utilisable.

Marvin, de A à Z

Vous avez été plusieurs à me poser plein de questions un peu technique sur Marvin, et ça m'a donné envie de l'écrire ce billet technique. Je vais tenter d'expliquer l'ensemble de son fonctionnement et la démarche pour arriver au résultat final, de A à Z.

Commençons par le chassie. Il est en bois, rien d'original. Le bois est très simple à usiner avec des outils standards et peu cher : perceuse, scie sauteuse, et papier de verre. J'ai utilisé deux types de bois différents. Du contreplaqué de 8mm comme support, et un autre, plus fin, qui sert habituellement aux fonds de meubles, pour relier les deux planchettes de contreplaqué. Coupé en petite dimensions, ce bois est plutôt rigide.

La structure du robot est en fait organisée en deux étages, chaque étage possède deux « surfaces » pour fixer des composants. La surface inférieure, celle qui se trouve face au sol, supporte les quatre moteurs et les accus du robot (huit accus AA). La face opposée supporte elle une protoboard et la carte Arduino (je reviendrais la dessus plus bas). L'autre étage supporte lui sur sa surface inférieur l'AlixBoard (qui prend toute la place), ainsi que sur sa surface supérieur les capteurs comme le radar, la caméra, le capteur de luminosité etc. Bien sur, les surfaces verticales sont aussi utilisées, pour fixer l'antenne, des boutons, un écran, les capteurs d'obstacle etc.

L'alimentation électrique utilise huit accus AA de 1.2V ce qui me donne donc une tension de 9.5V. L'AlixBoard doit être alimentée entre 7 et 20V, donc pas de problèmes avec elle, l'Arduino (et tous les capteurs qui passent par son alimentation) est alimentée par l'USB connecté à l'AlixBoard, donc pas non plus de problème à ce niveau. Les moteurs eux, sont alimentés en 6V, j'ai donc du utiliser un régulateur 7806 pour obtenir cette tension de 6V. Le 7806 supporte une intensité de 1.5A seulement, heureusement, mes moteurs sont bien en dessous de ce seuil avec une consommation de 200mA environ par moteur.

Les batteries

Marvin utilise un micro-contrôleur ATmega1280 de la carte Arduino Mega. Ce micro-contrôleur sert pour toutes les opérations bas niveau comme interrogation des capteurs, la gestion des moteurs, et elle possède peu de logique. En fait, la seule opération de décision qu'elle doit faire c'est de stopper les moteurs en cas d'obstacle. Elle gère cette opération elle même car le temps entre la détection de l'obstacle et l'arrêt des moteurs doit être très court.

Schéma général

Sur ce micro-contrôleur est connecté tout une tripotée de capteurs et autre gadgets électroniques : capteur d'obstacle, radar, sonde température, sonde luminosité, écran LCD etc. Ils servent majoritairement à la prise de décision du robot (comme je l'ai dit plus haut). Des codeurs sont aussi installés sur les roues, ils observent les mouvement de la roue pour coder un mouvement. Ces capteurs servent à mesurer la distance parcourue par les moteurs et a les arrêter une fois l'objectif atteint. Tout ceci est géré directement par le micro-contrôleur ainsi les messages à lui envoyer pour faire avancer les moteurs sont du type « avance de 100 unités à la vitesse maximum ».

Les batteries

Le micro-contrôleur contrôle aussi trois servomoteurs. Pour ceux qui ne connaissent pas, un servomoteur est un moteur qui permet d'assurer un mouvement très précis. Il comporte un moteur (logique), un capteur de position du moteur et un jeu d'engrenages pour augmenter le couple du moteur (au détriment de la vitesse donc). Il suffit de donner un ordre au moteur (une position) pour qu'il s'y rende et la maintienne. Si on force, il forcera dans le sens inverse en retour. Ce genre de moteur est très utilisé en modélisme, pour la direction par exemple. Dans mon cas, j'en utilise un pour contrôler l'angle du radar, et deux autres pour la « visée » de la caméra (un pour le pan, l'autre pour le tilt).

Le micro-contrôleur est directement interfacé avec l'AlixBoard par USB, et plus précisément du Serial over USB. Le composant intégré à la carte Arduino simule un simple port série. Du coté de l'AlixBoard, qui tourne sous Linux, on a l'impression d'avoir affaire à un simple port série. J'utilise d'ailleurs la bibliothèque pySerial pour communiquer avec le port.

J'ai bien sûr dû définir un protocole pour la communication des deux cartes. L'USB étant un protocole fiable, mon protocole ne s'occupe pas du tout de la détection et de la correction d'erreurs. Il est donc extrêmement simple et ne possède que deux (en plus des données) champs : le type de la trame envoyée, et la longueur de ses données. Et éventuellement, un champ data de la taille de la valeur du champ précédant (qui peut être de zéro).

Un petit schéma pour mieux se représenter la chose :

Type
(1 octet)
Taille données
(1 octets )
Données
(0+ octets)

Chaque trame est analysée et correspond à un morceau de code en fonction du type. Les données transmissent peuvent être n'importe quoi. Ce morceau de code leur donnent ensuite un sens.

Concernant les moteurs, ils sont drivés par une carte a part. Celle-ci communique avec l'Arduino par port Série aussi (le protocole est lui propre à la carte). J'ai choisi d'utiliser un driver tout fait pour éviter les prises de tête « électroniques », ce domaine n'étant pas vraiment ce qu'on pourrait appeler une spécialité pour moi (surtout l'électronique analogique). Il gère donc l'énergie envoyée aux moteurs et permet de moduler leur vitesse.

Les batteries

L'AlixBoard s'occupe de la partie logique du robot. Bon, pour l'instant la logique concise simplement à suivre les ordres de l'utilisateur, mais j'envisage plus tard de faire de la vision par ordinateur. L'AlixBoard gère aussi les communications WiFi qui servent à commander le robot à distance, ainsi que la webcam qui permettra de faire du pilotage en immersion. Elle tourne sous Linux Debian Lenny stocké sur une simple carte compact flash de un giga. On peut accéder au système par SSH pour la configuration et autres tâches de compilation du firmware de l'Arduino, mais toute la partie robotique est gérée par un daemon en Python sobrement appelé « marvind ».

Les batteries

Le daemon fait le pont entre les ordres de l'utilisateur (via WiFi) et la carte Arduino (via l'USB). Il peut aussi faire de la prise de décision.

La communication avec le daemon du point de vue de l'utilisateur se fait via un protocole de RPC maison. En fait, j'ai d'abord commencé par définir un protocole de communication très simple (encore une fois) à base de données JSON encapsulées dans du TCP. Une trame de ce protocole ressemble à ceci :

Taille données
(4 octets )
Données JSON
(0+ octets)

Je pouvais difficilement faire plus simple :-).

Cela me permit de converser facilement en communiquant des structures typées complexes. La bibliothèque qui implémente ce protocole s'appelle « zaphod ». Je la releaserais peut être un jour...

Au dessus de ça, j'ai créé mon protocole de RPC, « zaphodrpc ». Il permet au serveur de publier des fonctions distantes (je parle bien de fonction, on récupère une valeur de sortie), mais aussi une communication bidirectionnelle d'envoi de notification. Le serveur peut donc « pusher » des informations aux clients connectés (genre, attention, j'ai faillis me manger un mur, mais je me suis arrêté).

Je ne détaille pas plus sur ce RPC car même si il est assez simple comparé aux autres, il y a quand même beaucoup à dire la dessus.

Reste ensuite le client, je n'ai pas encore décidé ce à quoi il aurait l'air pour le moment.

Le projet à pas mal avancé depuis le dernier billet. L'AlixBoard est intégrée, et je suis en train de programmer l'API du robot au travers le RPC. Il manque encore la caméra et beaucoup de code à terminer avant de rendre le robot utilisable, mais ça ne serais tarder... Cela dit, il est déjà capable de fonctionner en autonomie (c-à-d sur batterie) et d'être contrôlé à distance via le WiFi, donc on peut dire que le plus gros est fait :-).

Marvin #1

Quand j'étais petit, je voulais construire un robot. En fait, j'en avais même fait un, construit avec une vieille télécommande et un jouet radio commandé. Le problème c'est qu'il ne fonctionnait pas vraiment, étant donné que j'avais juste scotché la télécommande au jouet. J'étais bien déçu.

Depuis un moment, l'idée me trottait à nouveau dans la tête. Cette fois je savais un peu plus ce que j'avais à faire. J'ai donc commencé par réfléchir au problème. Quatre roues motrices avec une direction différentielle (pour la simplicité mécanique), une carte Arduino pour toutes les opérations « bas niveau » (contrôle des moteurs, récupération des informations provenant des capteurs...), et une carte Alix (géode 500Mhz, afin de faire tourner toute la « logique », utiliser la connectivité WiFi, et y brancher une webcam). Au fur et à mesure, le projet se construisait dans ma tête.

J'ai ensuite mis environ deux mois à me décider à me lancer : tout ça représente en effet une certaine somme d'argent, d'autant plus que je voulais éviter la récup' pour être sûr d'avoir quelque chose qui fonctionne correctement au final. Sans parler de l'intérêt assez faible de l'objet en lui-même...

Finalement, me restant quelques euros après mon voyage de cet été, j'ai décidé de me lancer.

Je voulais quand même éviter d'acheter du matériel pour rien si je n'arrivais pas à terminer le projet. Je l'ai donc divisé en trois étapes :

  • Construction du chassie et ajout des éléments de bas niveaux (moteurs, carte Arduino, capteurs).
  • Ajout de l'Alix Board pour les opérations de haut niveau (WiFi...).
  • Ajout de la vision au robot (En gros Webcam, mais aussi servo moteurs pour la diriger).

J'en suis pour l'instant à la première étape, elle n'est pas terminée, mais j'ai déjà construit le chassie, ajouté les moteurs, la carte Arduino, et tout ce petit monde roule correctement. Et il peut s'arrêter s'il détecte un obstacle. J'ai même terminé de spécifier le protocole de communication entre l'Alix Board (qui est pour l'instant mon PC) et l'Arduino. Les prochains travaux seront la mise en place des autres capteurs (Radar à ultrasons, capteur de luminosité, température...).

Pour terminer, voici quelques photos et vidéos de l'engin :

Vue d'ensemble Fais le beau ! Électronique Vue de devant

Un autre billet plus technique suivra (peut être) au sujet de Marvin.

Comment fonctionne le format d'images PNG ?

Je ne détaillerais pas le cheminement qui m'a amené à devoir lire la RFC 2083 qui décrit le format d'image PNG, mais maintenant que c'est fait, je trouvais intéressant d'écrire un petit article la dessus pour ceux que ça intéresse.

Je vais donc, sous forme de didactitiel pas à pas, tenter d'expliquer très globalement comment fonctionne le format, et comment créer une image avec.

Tout au long de l'article, j'utilise un interpréteur interactif Python pour automatiser certaines tâches très légèrement fastidieuses. Même si le but n'est pas de programmer un encodeur d'image, quelques rudiments de programmation seront tout de même utiles pour tout comprendre.

La matière première

L'image utilisée dans cet article est affichée ci-dessous (grossie 100x). J'ai pris une image relativement petite de 16x16px afin de pouvoir la manipuler plus facilement.

Mouton grossi 100x

Nous n'allons pas partir totalement de rien. J'ai préalablement créé une structure Python très simple pour décrire mon image. Comme tout le monde le sait, une image est composée de pixels. Un pixel, c'est l'élément le plus petit d'une image. Selon le type d'image (noir et blanc, couleur, avec transparence ou non), un pixel peut être décrit par plusieurs valeurs différentes, on parle de canaux.

Dans notre cas, l'image est en noir et blanc, mais nous allons coder les couleurs en RVB, et utiliser la transparence (RVBA donc, A pour Alpha, ce qui correspond au canal de la transparence). Nous avons donc 4 valeurs qui définissent notre pixel : le rouge, le vert, le bleu, et la transparence qui mélangés donnent une couleur, avec sa transparence. Toutes ces valeurs sont codées sur un octet, soit 8 bits. Cela permet 256 valeurs différentes, de 0 à 255. Le format PNG permet aussi de coder les canaux sur 2 octets pour obtenir de plus grandes nuances de couleurs, mais nous n'utiliserons pas cette fonctionnalité. Le tableau ci-dessous montre comment le mélange de couleurs permet de former le pixel, et comment l'assemblage de tous les pixels permet de former l'image.

RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA

Pour représenter ça en Python, j'ai donc pris chacune des quatres valeurs qui définissent un pixel pour les regrouper dans un tuple. Chacun des pixels d'une ligne est regroupé dans un tuple, et chaque tuple de ligne est regroupé dans un autre qui contient toutes les lignes de l'image.

Petit exemple, pour cette image de 2x2 pixels :

Image de 2x2 pixels

La représentation en Python sera :

(
	( (0, 0, 0, 255), (255, 255, 255, 127) ),
	( (255, 255, 255, 127), (0, 0, 0, 255) )
)

Vous pouvez télécharger le module Python qui contient la structure représentant mon image ou fabriquer votre propre image représentée sous cette forme.

Parlons PNG

La première chose que nous allons faire est de représenter notre image sous une autre forme plus "PNGesque". En fait, nous allons simplifier notre structures actuelle pour représenter une ligne sous forme de chaine d'octets. Chaque octet étant une des valeurs d'un pixel (R, V, B ou A) directement. La fonction Python chr() permet de transformer un entier en octet (ou caractère, ou encore byte).

Le code pour faire cette transformation est celui-ci :

>>> from data import image_brute
>>> image = [''.join([''.join([chr(v) for v in px]) for px in ligne]) for ligne in image_brute]

Vous pouvez afficher image, ce qui vous donnera une liste de charabia incompréhensible correspondant à chaque ligne de notre image.

Le filtrage

Avant de compresser notre belle image avec algorithme bien connu de compression (inflate), nous devons l'optimiser un peu afin de maximiser la compression : on appelle cela le filtrage.

Il existe actuellement une seule méthode de filtrage qui répertorie cinq types de filtres différents. Une méthode de filtre est un genre de "pack" de filtres que l'on peut ensuite, au choix, utiliser sur chacune des lignes. Le choix d'un filtre pour une ligne peut se faire de différentes manières. La RFC n'impose pas la manière de faire cela (elle donne juste quelques indications), c'est donc à ceux qui implémentent un encodeur de déterminer eux même un algorithme pour cette tâche. Une des manières proposée par la RFC est de filtrer la ligne avec chacun des filtres successivement, puis de garder celui qui compresse le mieux. J'imagine que c'est l'algorithme utilisé la plupart du temps dans les encodeurs.

Vu que le but n'est pas de coder un encodeur, ni de rendre notre image la plus réduite possible, nous allons utiliser le filtre 0, celui qui consiste à ne rien faire. Il faut toutefois, au début de chaque ligne, ajouter le numéro du filtre utilisé :

>>> image = [chr(0)+ligne for ligne in image]

Compression !

Enfin, nous allons compresser notre image. L'algorithme de compression utilisé par le format PNG est inflate. Sans entrer dans les détails, inflate est un algorithme de compression sans perte qui combine l'algorithme LZ77 et le codage de Huffman. Ceux qui voudront plus de détails sur cette partie trouveront abondamment de la documentation sur leur moteur de recherche préféré.

Pour effectuer cette tâche nous n'allons donc pas implémenter l'algorithme, mais utiliser la fameuse zlib qui s'en chargera très bien. La zlib est utilisable en Python au travers du module zlib (logique) fournit en standard.

Nous allons dans un premier temps mettre tous les éléments de notre liste bout à bout puisque nous n'avons plus besoin d'effectuer des opérations par ligne de l'image :

>>> image = ''.join(image)

La compression en elle même est très simple avec la zlib :

>>> import zlib
>>> image_compressee = zlib.compress(image)

Nous pouvons aussi constater que inflate compresse très bien notre image :

>>> print 'Compression à %.1f%%' % (100-len(image_compressee)*100.0/len(image))

La structure d'un fichier PNG

Nous avons maintenant notre image compressée avec l'algorithme qui va bien, mais le PNG ce n'est pas que ça, c'est aussi l'enrobage. Cet enrobage donne tout plein d'informations sur l'image, certaines sont indispensables comme la méthode de filtrage utilisée, le type de codage de la couleur etc. D'autres informations sont optionnelles, comme par exemple les commentaires que l'on pourrait définir sur une image.

Ces informations sont regroupées sous forme de bloc dans le jargon PNGien. Un bloc possède la forme suivante :

Longueur
(4 octets)
Type
(4 octets)
Données
(0+ octets)
CRC
(4 octets)

Le champ longueur indique tout simplement la longueur du champ donnée qui est lui même variable. Le champ type est un peu comme le nom du bloc. Il utilise des valeurs de la table ASCII, on trouve alors des blocs de type IDAT ou IHDR que nous détailleront plus tard. Les données dépendent du type de bloc (il peut ne pas y en avoir). Enfin le champ CRC est un controle d'intégrité du bloc et se calcule à partir des champs type et données.

Il existe naturellement plusieurs types de blocs PNG. Certains sont obligatoires comme les blocs IHDR (image header, il indique des informations caractéristiques de l'image comme ses dimensions), PLTE (palette, qui contient la palette de couleurs de l'image si l'image utilise le mode par couleur indéxées), IDAT (image data, qui contient l'image en elle même, compressée tel que nous l'avont fait plus haut), et IEND (marqueur de fin de l'image). L'ordonnancement de ces blocs est libre, à l'exception de IHDR qui doit se trouver au début de l'image, et de IEND qui doit se trouver à la fin de l'image. Cependant, certains blocs auxiliaires peuvent necessiter un ordonnancement particulier.

D'autres blocs, appelés blocs auxiliaires sont utilisables. La tableau ci-dessous récapitule tous les blocs auxiliaires définis par la spécification PNG.

Nom de type Description
bKGD Spécifie une couleur de fond par défaut pour l'affichage de l'image.
cHRM Spécifie les coordonnées x, y du modèle chromatique CIE 1931 pour les couleurs rouge, vert, et bleu primaires, ainsi que la référence du point blanc.
gAMA Spécifie le gamma de la caméra (réelle ou virtuelle) qui a produit l'image.
hIST Donne une fréquence approximative de l'utilisation de chaque couleur de la palette.
pHYs Spécifie la taille du pixel et son rapport dimensionnel.
sBIT Spécifie le nombre de bits significatifs d'origine.
tEXt Informations textuelles attachées à l'image.
tIME Date et heure de la dernière modification de l'image.
tRNS Spécifie que l'image utilise la transparence simple.
zTXt Informations textuelles compressées attachées à l'image.

Pour notre image, nous n'allons utiliser que les blocs absolument obligatoires :  IHDR, IDAT et IEND. PLTE étant inutile puisque nous n'utilisont pas le mode par couleurs indéxées.

Dernière chose, un fichier PNG possède une signature, une suite d'octets qui sert à la détection du format d'un fichier par les programmes. Cette signature doit positionner au tout début du fichier, juste avant le bloc IHDR. Elle est la suivante (en décimal) :

137 80 78 71 13 10 26 10

Le bloc IHDR

Commençont par le bloc qui sera le premier de notre fichier. Ce bloc donne donc les caractéristiques de l'image : sa largeur, sa hauteur, sont échantillonage, le type de couleurs utilisées, la méthode de compression, la méthode de filtrage, et enfin, la méthode d'entrelacement. Voici la forme de la partie données du bloc :

Largeur
(4 octets)
Hauteur
(4 octets)
Echantillonage
(1 octets)
Couleur
(1 octets)
Compression
(1 octets)
Filtrage
(1 octets)
Entrelacement
(1 octets)

Définissons chacune de ces valeurs avant de les coder.

Les dimensions de notre images sont de 16x16 pixels. Pour l'échantillonage, nous l'avons dit plus haut, nous utilisons des canaux de couleurs codés sur un octet, soit 8 bits. Le modèle de couleur à utiliser pour une image qui utilise le RVBA est le numéro 6. Je n'invente rien à ce niveau, tout est spécifié dans la RFC.

Pour la compression ainsi que le filtrage, à ce jour, seul une seule méthode existe, nous renseigneront donc 0. Nous n'entrelaçons pas, nous utiliseront donc aussi la valeur 0 pour ce champ.

Pour coder ces données en Python, nous utilisont le module struct qui sert justement à ça :

>>> import struct
>>> IHDR = ['', '', '', ''] # Les 4 éléments d'un bloc
>>> IHDR[1] = u'IHDR'.encode('ascii')
>>> IHDR[2] = struct.pack('>IIBBBBB', 16, 16, 8, 6, 0, 0, 0)
>>> IHDR[0] = struct.pack('>I', len(IHDR[2]))

N'oublions pas le CRC, nous allons à nouveau utiliser ce cher module zlib pour ça, puisqu'il contient une fonction tout prete cette tâche :

>>> IHDR[3] = struct.pack('>i', zlib.crc32(''.join(IHDR[1:3])))

Le bloc IDAT

La partie donnée du bloc contient simplement l'image sous la forme compressée que nous avont créé plus haut, la création de se bloc sera donc assez simple :

>>> IDAT = ['', '', '', '']
>>> IDAT[1] = u'IDAT'.encode('ascii')
>>> IDAT[2] = image_compressee
>>> IDAT[0] = struct.pack('>I', len(IDAT[2]))
>>> IDAT[3] = struct.pack('>i', zlib.crc32(''.join(IDAT[1:3])))

Le bloc IEND

Encore plus simple que le bloc IDAT, le bloc IEND qui ne contient aucune donnée puisqu'il n'est qu'un marqueur de fin de fichier. Sa création se fait ainsi :

>>> IEND = ['', '', '', '']
>>> IEND[1] = u'IEND'.encode('ascii')
>>> IEND[0] = struct.pack('>I', len(IEND[2]))
>>> IEND[3] = struct.pack('>i', zlib.crc32(''.join(IEND[1:3])))

Signature du fichier

Comme expliqué plus haut, la signature du fichier sert à la détection du format par les programmes, sa création est des plus simples :

>>> signature = chr(137) + chr(80) + chr(78) + chr(71) + chr(13) + chr(10) + chr(26) + chr(10)

Et on colle tout ça :

Nous allons maintenant concaténer tous ces éléments dans l'ordre approprié, c'est à dire la signature en premier, suivie du bloc IHDR, suivi du bloc IDAT, et pour terminer du bloc IEND :

>>> png = signature + ''.join(IHDR) + ''.join(IDAT) + ''.join(IEND)

Finalement, nous enregistrons tout ceci dans un beau fichier :

>>> f = open('mon_beau.png', 'w')
>>> f.write(png)
>>> f.close()

Vous pouvez alors ouvrir le fichier avec votre lecteur préféré, il devrait normalement s'ouvrir sans encombre :-).

Pycon 2009

Cette année encore je me suis rendu à la Pycon, un weekend de conférences sur le langage Python, ou plutôt à un morceau de Pycon, puisque je n'y ai été que le samedi, et pas à toutes les sessions.

Pycon
(Oui la photo est moche, mais j'avais la flemme et le 50mm était trop long pour la taille de la salle)

Cette année j'ai donc assisté à une présentation du fonctionnement du développement de CPython, l'implémentation principale (en C) de Python, par Victor Stinner. Il a passé en revue les différents outils utilisés par les développeurs de Python pour communiquer, et donné des recommandations à ceux qui voudraient soumettre des bugs.

La seconde conférences à laquelle j'ai assisté concernait PyQuery, présenté par Olivier Lauzanne. PyQuery est une bibliothèque qui reprend l'API de JQuery pour l'interrogation de documents xml ou html. Ça ressemble à ça :

>>> from pyquery import PyQuery as pq
>>> journaux = pq(url='http://inaps.org/journal/')
>>> print [j.text_content() for j in journaux('h2')]
['Vacances...', 'Programmation avec Curses en Python', "Escaline2 : la marmotte s'est remise au travail", "L'algorithme de diksta... dijstrak... DIJKSTRA !", 'Mon premier module sur le CheeseShop', "OMG, une nouvelle version d'I, NaPs ?!", "Qui n'en veuuuuxx ? :)", 'Et c\'est qui qui dit "fail" ?', 'PyCon fr 08', u'Mais o\xc3\xb9 en est Bearnaise ?']

Personnellement, je trouve la syntaxe assez sympa et carrément plus pratique que la manipulation directe d'un arbre dom. Je pense que ça va potentiellement être très utile pour mon travail si un partenaire nous demande à nouveau de nous démerder avec son site au lieu de filer un flux xml de ses données. Merci donc au conférencier (qui semble aussi être l'auteur de la bibliothèque) de m'avoir fait connaître ça.

Dans la conférence suivant, Gael Pasgrimaud nous parlait de Deliverance qui est, si j'ai bien compris, un proxy, qui se place devant le serveur web et qui permet de modifier les pages servies par celui-ci à la volée. Le conférencier à évoqué plusieurs utilisations, j'ai surtout retenu la possibilité de faire des templates que le proxy traitera à la volée. J'ai globalement pas trop suivi cette conf, étant plutôt occupé à tenter de monter mon vpn sur le réseau tout filtré de la cité des sciences (ouh lala).

Benoît Chesneau nous a ensuite présenté une série de bases de données non sql et l'implémentation de leur bibliothèque d'accès en Python : bdd à clé/valeurs comme Memcache, Redis ou encore Tokyo cabinet. Bdd orientées documents comme CouchDB ou MongoDB. Les bdd orientées colonne (WTF ??!) comme Hbase. Ou encore les bdd objet, comme Zope et sa ZoDB. Une présentation assez intéressante qui nous montre qu'il n'y a pas que MySQL dans la vie. Pour ma part, je m'intéressais déjà à CouchDB depuis quelque temps, j'attendais d'ailleurs pas mal de la conférence dédiée à CouchDBKit (bibliothèque CouchDB en Python) de l'après midi dont je parle juste en dessous.

Après un petit passage au Quick, retour à la cyber-base.

Les sessions de l'après midi commencent par une grosse conférence d'1h sur Python en usage scientifique par Gael Varoquaux. Globalement intéressant, ça parlait d'IPython, le shell Python avancé, et des modules matplotlib et scipy. Je n'ai par contre pas tout suivis, quand ça commençait à partir sur des algorithmes pointus appliqués à des images. Cependant selon l'auteur, ces modules Python lui permettent d'implémenter en quelques lignes des algorithmes très complexes, là ou d'autres s'arrachent les cheveux avec du C.

La conférence suivant était une présentation de l'OLPC (de l'ordinateur en lui même et du projet) par Jean-François Cauche, et plus particulièrement de son interface graphique, Sugar, réalisée en Python. Une conférence assez intéressante qui donne envie d'acquérir un nouveau jouet (ou préciiieuuux, comme dises certains).

Entre deux, un petit Lightning Talk de 10mn de Michael Scherer sur Python dans les environnement mobiles. Michael à évoqué les plates-formes existantes et les possibilités d'y coder en Python dessus. En résumé : Androïd bof, iPhone bof, Windows CE moyen mais api windows relou, Symbian pas mal, mais le mieux était selon lui les plates-formes complètement ouvertes comme open-moko.

S'en est suivit d'un retour d'expérience de Marie-Josee Cros, chercheuse à l'INRA (Institut National de la Recherche Agronomique) de Toulouse. Son projet est la mise en place d'une plate-forme RNA-Space d'annotation génomique d'ARN non-codant-protéine. Leur plate-forme sert en fait à collecter auprès des chercheurs des données à traiter, puis à retourner les résultats à ceux-ci. Elle est programmée en Python et utilise le framework web CherryPy. Marie-Josee Cros à donc expliqué les choix de son équipé et commenté les problèmes auxquels ils se sont confrontés lors du développement de leur programme.

Enfin, la dernière conférence auquel j'ai assisté portait sur CouchDBKit, une bibliothèque qui permet de se connecter à une base de donnée CouchDB. Présenté par son auteur, Benoît Chesneau, le module est en fait formé d'une série d'helpers qui permettent de grandement simplifier l'accès à la base. L'auteur nous a présenté son module en développant en live une petite application web de ToDo list.

Pour terminer, un grand merci aux organisateurs de l'AFPY de proposer chaque années des conférences intéressantes (et gratuites) sur Python.

Vacances...

Billet de train

Programmation avec Curses en Python

J'ai déplacé le contenu de ce billet dans cet article.

Escaline2 : la marmotte s'est remise au travail

Bon, voilà, je me suis enfin décidé de me sortir les doigts du derrière pour bosser un peu sur la nouvelle mouture d'Escaline. Pour ceux qui ne suivent pas d'assez près mes fabuleuses, bien que peu nombreuses, créations (ainsi que celles de Sunny en l'occurrence), Escaline est un shell pour le web. Il permet d'enregistrer des commandes qui seront des alias vers (par exemple) différents moteurs de recherche. Il est ensuite possible de l'installer sur son navigateur, au travers d'une configuration pour les navigateurs Gecko (Firefox, Epiphany) ou d'un plugin opensearch.

La version 2 d'Escaline est une refonte complète du site. On passe du framework web.py sur Python 2.4 au framework Django sur Python 2.5 (ou Python 2.6, il faut que je voie si Lenny sera à jour sur ce point). Mais ce n'est pas la seule nouveauté, car j'en ai profité pour implémenter tout un tas de fonctions cool-es que tout le monde attend (ou pas).

Tout d'abord, j'ai implémenté les commandes multi-arguments (fonction qui a réellement été demandée par "beaucoup de monde"). Il sera possible d'écrire des commandes beaucoup plus complexes que ce qui se fait maintenant. Par exemple, il sera possible d'écrire une URI qui permet de gérer une commande de ce style :

g lang:lr%3Dlang_fr type:image Escaline is good

L'URI en question pourrait être :

http://www.google.fr/{type:search}?q={0}[&meta={lang}]

Comme vous pouvez le deviner, le nouveau parser gère les arguments nommés ("lang", "type"), possédant une valeur par défaut (définie après les deux points, a droite du nom de l'argument), ou avec des crochets qui permettent de supprimer du texte autour de l'argument si celui-ci n'est pas renseigné.

Mais cette commande n'est pas très belle. Sérieusement, qui penserait à mettre "lr%3Dlang_fr" à coté de lang pour faire une recherche en français ? Et puis je suis bien bloqué avec mon type, car je n'ai finalement le choix qu'entre des images et du web... Le nouveau parser est certes mieux, mais je le trouve un peu limite pour certains sites un peu tordus. J'ai donc développé un nouveau système de commandes : les commandes "avancées", ou "programmées". Le principe est de passer par un langage de programmation pour définir une commande. Le parser permet toujours de parser les arguments qui entrent en ligne de commande (ce n'est pas obligatoire), mais son résultat au lieu d'être passé à un autre parser qui génère une uri est passé à un programme. Le langage choisit pour cela est le Lua, parce qu'il est hyper léger (l'interpreteur doit faire une centaine de Ko), parce qu'il est hyper facile à utiliser (sa syntaxe s'apprend en à peine 30mn, et encore) et parce qu'il est facilement embarquable et sandboxable (il est fait pour ça). Il sera donc possible d'écrire avec ça des vraies commandes de fous, avec tout plein d'arguments, ou des commandes qui ne retournent pas une URI vers qui se rediriger, mais qui retourne du texte à l'utilisateur (exemple, calc 14+3 retournerait "14+3 = 42").

Voici un exemple de ce que pourrait être le code de la commande passée plus haut :

#!lua
-- Recherche google

search_type = kwargv.type
language = kwargv.lang

search_type_mapping = { web = 'search', image = 'images' }
language_mapping = { fr = 'lr%3Dlang_fr', en = ''}

-- Valeurs par défaut
if search_type == nil then
	search_type = 'web'
end
if language == nil then
	language = 'fr'
end

-- Création de l'uri
uri = string.format('http://google.fr/%s?q=lala&meta=%s',
		search_type_mapping[search_type], language_mapping[language])

-- Retour du résultat
escaline.return_uri(uri)

La troisième nouveauté est plus mineure mais non pas moins utile (à par sur les EEEpc et autres écrans taillés pour les mouches) : il sera possible de chainer des commandes. Ceci est une fonctionnalité du nouveau parser, mais c'est un peu à part du reste. En clair, si vous tapez g Escaline && y Escaline, Escaline ouvrira les deux pages des deux moteurs de recherche, chacune dans une frame. Ceci permettra très facilement de comparer les résultats de deux (ou plus) moteurs.

Escaline en commandes multiples

La quatrième nouveauté est un système de ticket utilisateur. Ces tickets remplaceront un éventuel système de compte utilisateur (qui n'existe pas actuellement), mais de manière anonyme. Le principe c'est de se créer un ticket avec une commande. Le site vous retourne ensuite un numéro et enregistre un cookie sur le navigateur. Grâce au numéro, vous pouvez installer le cookie sur d'autres navigateurs (avec une autre commande). Une fois le cookie en place, vous pouvez toucher à un certain nombre d'options (avec une troisième commande) qui permettront par exemple de personnaliser le logo, de changer le moteur de recherche par défaut etc.

Enfin, la dernière nouveauté (qui sera révélée aujourd'hui) est la disponibilité des sources et ce dès maintenant. Vous pouvez suivre, récupérer et tester l'avancement du site qui est disponible sous forme de dépot git sur idevelop.org. Les sources seront bien entendus libres (releasées sous licence GPL3). Cependant, le principe d'Escaline étant d'être centralisé, on espère que les gens ne s'amuseront pas à créer 150 clones publiques. Pour terminer là-dessus, notez qu'en plus de suivre, récupérer ou tester Escaline, vous pouvez aussi y participer, surtout si vous avez quelques compétences en design, intégration ou Javascript. N'hésitez pas à me contacter si ça vous intéresse.

J'utilise Escaline 
!