Fonctionnement général du module d'images

Le but du module d'images est de permettre, autant que possible, de fonctionner en utilisant les bibliothèques de gestion d'images actuellement disponibles. Si PIL n'est pas disponible mais qu'on peut obtenir le même résultat en utilisant cairo, par exemple, on veut que ce soit fait de manière automatique sans que le programme appelant n'ait besoin de s'en soucier (bien sûr, la disponibilité des types de sortie dépendra des modules installés, on ne pourra générer de PIL.Image que si PIL est présent, mais les types de base array et stream sont normalement toujours dispos).

Pour cela, chaque action disponible est référencée en fonction des types que peuvent prendre en entrée et en sortie les fonctions correspondantes. La fonction principale de chargement d'image, core.perform (non-accessible depuis l'extérieur mais appelée automatiquement par les fonctions d'interface), va donc découper la chaîne décrivant l'image, passer en revue les différentes actions, et vérifier à chaque fois quels formats peuvent être utilisés, pour déterminer la combinaison de fonctions de moindre coût.

Toutefois, la fonction actuelle, assez naïve (elle passe simplement toutes les possibilités en revue dans l'ordre dans lequel elles viennent), peut assez vite rencontrer une explosion combinatoire quand une même action est disponible pour beaucoup de combinaisons de types (il y a par exemple une vingtaine de combinaisons disponibles pour faire un overlay, sachant qu'il reste ensuite pour chacune d'elles à regarder comment charger les deux images à combiner), ce qui peut représenter assez vite un traitement pour déterminer la combinaison la plus économe qui est plus gourmand en temps et en CPU que ce traitement lui-même.

Pour éviter ce problème, en attendant un algorithme plus efficace, la méthode détermine un seuil d'acceptabilité (actuellement déterminé arbitrairement par le nombre d'actions dans la chaîne initiale multiplié par le nombre de formats disponibles et divisé par deux), et la première combinaison de coût inférieur à ce seuil est utilisée sans se poser plus de question.

Entre chaque étape, des conversions implicites d'un format vers un autre peuvent être automatiquement ajoutés pour garantir leur continuité (leur coût est pris en compte dans le calcul global). S'il n'existe pas de manière directe de convertir un format vers un autre, la combinaison de conversions la plus efficace sera calculée et mise en cache, ce cache étant vidé chaque fois qu'on référence une nouvelle fonction de conversion.

Référencements des différentes fonction

Le module principal fournit, en plus des méthodes d'accès aux images, un certain nombre de décorateurs permettant de référencer de nouvelles fonction et ainsi d'enrichir les actions disponibles.

Le décorateur transform permet de référencer une action de transformation d'une image en une autre. Il prend en paramètre un format d'origine (qui peut être « string » s'il s'agit de créer une image à partir de rien) et un format de destination. S'il s'agit d'une méthode combinant plusieurs images, un format d'origine secondaire peut également être précisé (dans ce cas, le premier format d'origine ne peut pas être « string »). Un coût peut être précisé (il vaut 1 par défaut), et, comme pour pas mal d'autres de mes décorateurs, un nom peut être précisé, faute de quoi on utilisera celui de la fonction.

Le décorateur convert sert à référencer les fonctions de conversion d'un format vers un autre qui peuvent être utilisées de manière anonyme entre deux étapes du traitement. Les format d'origine et de destination (qui ne peuvent ici pas être « string ») sont à préciser en paramètre, ainsi qu'un coût éventuel (1 par défaut).

Le décorateur prepare sert à référencer une fonction pour préparer les arguments pour les transformations. En effet, par défaut, une fonction de transformation reçoit l'image sur laquelle elle doit travailler, un éventuel paramètre qui lui est transmis (ou une image secondaire à combiner), et un dictionnaire contenant diverses variables de contexte. Une fonction spécifique (qui sera partagée par toutes les transformations de même nom) peut servir à changer cette liste d'arguments, par exemple en décidant de valeurs par défaut si le contexte ne contient pas les informations attendues, ce qui permet ensuite à la fonction de traitement d'image de ne contenir que le code correspondant à sa tâche.

Le décorateur alias permet de référencer des transformations particulières, qui ne modifient pas l'image, mais les variables de contexte ou le chemin décrivant l'image. Elles peuvent être mises en concurrence avec les transformations classiques : par exemple, l'action greyshade (ou grayshade) qui transforme une image en nuances de gris peut être effectuée directement sur certains formats, mais correspond également au résultat de l'action tint(black), qui est disponible avec davantage de formats. L'algorithme de sélection déterminera donc le meilleur choix parmi ces différentes possibilités.

Enfin, le décorateur load sert à référencer une fonction qui va charger une image d'un certain type mime, attendu qu'il faudra souvent lire des fichiers images en entrée. Il faut donc indiquer le format de fichier reconnu, le format d'images utilisé en sortie, un booléen indiquant si on attend un objet bytes (par défaut) ou s'il faut tenter de le convertir en str, et, une fois encore, un coût, qui vaut 1 par défaut. Si le code utilisé nécessite un chemin d'accès plutôt qu'un contenu de fichier, passer None au paramètre bytes. Les fonctions référencées comme pouvant charger le type mime image/png et dont le paramètre bytes vaut vrai sont automatiquement référencées également comme pouvant convertir le type d'images interne stream, celui-ci n'étant rien d'autres que le contenu de fichiers PNG.

Organisation générale du package

Le module core contient les fonctions de base de l'algorithme de génération d'images, ainsi que les fonctions de transformations les plus basiques (chargement d'une image depuis un fichier, une sélection X et un presse-papier). Les modules colours et fonts servent à définir les types correspondants, utilisés en interne du module (mais également disponibles pour l'extérieur).

La plupart des autres modules sont dédiés à un format en particulier : params contient les fonctions de préparation des arguments, pathes contient les différentes fonctions de modification du contexte et de modifications des chemins, pillows contient les fonctions utilisant la bibliothèque PIL, surfaces celles utilisant la bibliothèque cairo, et pixbufs les fonctions de base liées à GDK.

Les modules arrays et streams sont cependant un peu particulier : en effet, le premier n'est pas vraiment un format d'images, et les fonctions qu'il contient codent directement les algorithmes concernés plutôt que de faire appel à des bibliothèques. De ce fait, certains traitement peuvent être appliqués directement lors de la conversion depuis d'autres formats d'images, plutôt que de devoir d'abord convertir, puis transformer (c'est par exemple la raison pour laquelle il existe autant de possibilités différentes de réaliser un overlay).

Le second pourrait contenir des appels un peu plus variés. Par exemple, il contient actuellement la fonction de chargement d'un fichier SVG par la bibliothèque cairosvg (qui ne permet hélas que d'obtenir le PNG résultant, et pas la Surface probablement utilisée en interne). Ça pourrait aussi être fun d'y placer quelques appels à ImageMagick pour effectuer diverses opérations quand ce n'est pas faisable en interne (avec un coût plus élevé que les fonctions restant dans le même programme Python, mais ça reste en TODO pour le moment).

Enfin, le package resources contient les images de base utilisées pour les icônes recolorées, ainsi qu'une collection de caractères Unicode associés à des noms d'icônes pour simuler un thème particulier (voir le détail de « mode »). Les images de base à recolorer sont fournies sous la forme d'objets bytes qui peuvent être directement utilisés pour initialiser des array, puisque c'est probablement dans ce format qu'ils seront ensuite transformés.