mots-clefs : programmation, C, Perl, CGI, Web, HTML, HTTP, GIF, JPEG, TIFF, tiffutil, ImageMagick, FBM, GIMP
Traitements par lot et opérations NON interactives |
Vous ne connaissez par Elfège ?
Non, non ! Ce n'est pas le nom d'un nouveau programme, c'est le prénom d'une personne à qui l'on demandait récemment d'archiver des centaines de documents par semaine en s'aidant d'un scanner et du logiciel appropriés pour des numérisations en nombre.
Sauf que, une fois les documents saisis dans un format fixé à l'avance pour l'ensemble des scans, il restait à leur enlever les marges inutiles, opération que ne prenait pas en charge le programme spécialisé.
Alors, consciencieuse, elle lançait un autre programme nommé Imagenation, chargeait la première image, appelait la fonction en question, sauvait l'image et passait à la suivante, et ce, jusqu'à 500 fois par jour. Une véritable partie de plaisir comme vous pouvez l'imaginer.
Alors, je vois déjà les petits malins me dire : « Pas futée la gamine ! Un petit script dans Photoshop et le tour est joué. »
Et d'une, le « petit » script en question n'est pas si trivial à réaliser. Ensuite, aussi intéressant soit-il, Photoshop n'est pas le produit le plus économique qui soit pour résoudre ce genre de problème. Enfin, et surtout, même si la version 5 sait désormais lire les fichiers TIFF compressés dans le mode CCITT Group IV, il ne permet en aucun cas d'enregistrer dans ce mode. Ce qui le rendait inutilisable dans le cas présent.
Quelques heures auparavant, ayant procédé à une de mes premières installations de Linux Red Hat 5.1, je passais en revue les différents outils disponibles dans cette distribution et tombais justement sur ImageMagick et ses différents utilitaires. C'était un peu comme une révélation, surtout quand j'ai survolé le 'man'uel de la commande convert. Ce programme a effectivement quelque chose de 'Magick'.
Suit le premier script que je lui écrivais (presque du Cyrano).
#!/bin/bash for fic in *.tif do echo -n Cropping $fic ... /usr/X11R6/bin/convert -crop 0x0 $fic cropped_$fic echo OK done
AVANT |
APRÈS |
Au cœur d'icelui, la fameuse commande convert avec pour principal paramètre l'action à effectuer, ici, la suppression des bordures ne contenant que la couleur de fond sur la plus grande largeur à chaque fois. Autrement dit, on enlève le cadre blanc qui n'offre aucun intérêt.
Qui plus est, convert est un programme pourvu de bon sens (de toute évidence, ça n'a pas été développé par des employés de chez MicroOnAPasInventéLaPoudreSoft) puisque, sauvant par défaut dans le même format que celui de l'image source, il opte automatiquement pour la méthode de compression la plus appropriée.
Dans mon premier test sur ses possibilités, l'image fournie était en noir et blanc et compressée en LZW, le résultat était pour sa part compressé en CCITT Group IV, assurément plus économique.
Bien entendu, le mieux étant l'ennemi du bien, comme la gestion de cet algorithme de compression n'est pas très répandu dans les programmes manipulant le format TIFF, son emploi peut desservir l'utilisateur. MAIS il existe une parade qui se limite malheureusement à interdire purement et simplement toute compression avec l'option +compress qu'il suffirait de placer avant l'option -crop.
Incidemment, et comme si cela ne suffisait pas, convert nous réglait aussi un dernier problème introduit par l'utilisation du programme Imagenation cité plus haut. Ce dernier a la mauvaise idée de coder le paramètre 'RowsPerStrip' des images sauvées au format TIFF avec la valeur 'Infinite', or, si cela est autorisé, il existe au moins un programme du commerce spécialisé dans la consultation d'archives au format TIFF qui n'aime pas du tout cette valeur. Vous l'aurez deviné, le client pour qui sont réalisées ces numérisations (un très grand compte) emploie justement CE programme. La bonne nouvelle est que convert rectifie automatiquement le tir en donnant à ce paramètre la valeur souhaitée.
Les traitements de groupes de fichiers peuvent aussi être réalisés à l'aide de la commande mogrify comme dans l'exemple suivant :
mogrify -crop 0x0 image_{1,2}.tiff
La différence majeure consiste dans le remplacement du contenu des fichiers d'origine par le résultat du traitement dont ils font l'objet.
Le principal avantage avec cette commande réside dans l'inutilité de scripts comme celui présenté plus haut, ce qui implique une prise en main plus simple et plus rapide pour tous ceux qui ne souhaitent pas se plonger dans les méandres des shells.
Toutefois il subsiste quand même certains petites (ou grandes) prérogatives aux scripts, surtout avec des programmes aussi peu loquaces (ou au contraire trop verbeux). Même si c'est peu de chose, l'une d'entre elles est de pouvoir afficher la progression de la tâche demandée, surtout si elle est longue. Il est toujours rassurant de savoir qu'un blocage n'est pas survenu lorsque l'on est informé, fichier après fichier, que le traitement continue.
Le premier exemple plus haut avec mogrify est intéressant car il va nous permettre de bien comprendre la différence de comportement qu'offre ce programme par rapport à convert.
Une commande comme
convert -crop 0x0 image_{1,2}.tiff
est traduite par le shell en
convert -crop 0x0 image_1.tiff image_2.tiff
ce qui signifie dans la syntaxe comprise par convert « opère le traitement demandé sur le contenu du fichier image_1.tiff et enregistre le résultat dans le fichier image_2.tiff ».
À l'opposé, les mêmes paramètres fournis à mogrify dans la commande
mogrify -crop 0x0 image_{1,2}.tiff
sont, évidemment traduits de la même manière par le shell en
mogrify -crop 0x0 image_1.tiff image_2.tiff
MAIS sont interprétés par ce programme en « opère un traitement sur le contenu du fichier image_1.tiff et enregistre le résultat dans le fichier image_1.tiff, PUIS, opère le traitement demande sur le contenu du fichier image_2.tiff et enregistre le résultat dans ce fichier image_2.tiff ».
Ce qui, vous en conviendrez, ne revient pas du tout au même.
Remarque en passant, à la suite de tentatives avortées, des fichiers temporaires générés par convert ou mogrify peuvent polluer votre répertoire /tmp, portant des noms préfixés avec 'magicka', et se terminant par un numéro qui correspond de toute évidence au PID auquel était associé le programme en cours d'exécution avant d'être sauvagement interrompu. En d'autres termes, si ces fichiers se multiplient et que la fréquence de nettoyage de ce dossier n'est pas assez importante, vous risquez rapidement de ne plus avoir de place sur votre filesystem.
Quelques jours plus tard (authentique), une autre charmante personne, Florance, me demande quel type d'interaction offrir à un internaute pour la consultation d'une carte de grand format. Elle se doute parfaitement qu'on ne peut imposer le téléchargement d'un énorme fichier, aussi fortement compressé soit-il, à l'utilisateur et ne peut se résoudre à lui présenter une image réduite qui contiendrait des informations tellement petites qu'elles seraient parfaitement illisibles et par conséquent inexploitables.
D'autant que, à l'image de
Là encore, mettant à profit ma découverte récente, je lui fais part de la possibilité d'utiliser ImageMagick pour répondre à son attente et propose de lui préparer une maquette. Celle-ci va permette d'illustrer le sujet de cet article.
Quelques uns des avantages déterminants que peut offrir un programme comme convert dans la réalisation de ce type d'application sont la possibilité de s'exécuter en tâche de fond sans requérir aucune interface graphique, ce qui implique une belle économie de ressources en terme de charge CPU et mémoire et, d'un autre côté, la disponibilité de traitements directement exploitables sans nécessiter de programmation et encore moins de connaissance intrinsèques des formats graphiques ou de leur manipulation. En conséquence de quoi un prototypage aisé, et pourquoi pas, une exploitation en conditions réelles si les temps de réponse s'avèrent satisfaisants.
Étape 1 : convertir l'image originale pour obtenir un fichier au format RGB.
En admettant que l'image originale porte le nom 'carte.tif', la commande idoine sera
convert carte.tif rgb:carte.rgb
Cette étape est indispensable car convert ne permet le découpage de portion d'image que si le fichier source est codé dans le format RGB. À noter tout de même qu'une image RGB non compressée de 65536 pixels de côté occupe 24 Go (et pas seulement 12 car CHAQUE composante rouge, verte et bleue de CHAQUE point est codée sur 2 octets).
Étape 2 : faire une vignette (une représentation réduite) qui servira de point de départ à l'internaute.
Prenez connaissance des dimensions de votre image d'origine (à l'aide de tiffinfo par exemple si elle est au format TIFF) et déterminez, uniquement sur la base de votre bon sens, les dimensions que devra avoir la vignette. Lancez ensuite une commande du genre
convert -geometry 30%x30% carte.tif gif:carte.gif
ou, si vous préférez spécifier les valeurs en pixels (mon modèle fait 685 par 925 pixels)
convert -geometry 205x277 carte.tif gif:carte.gif
Bien entendu, les deux premières phases peuvent parfaitement être réalisées avec n'importe quel programme, interactif ou non, permettant des manipulations équivalentes.
Au contraire, on peut vouloir calculer ces valeurs si l'on souhaite traiter automatiquement de la sorte plusieurs cartes de dimensions différentes. Si il est relativement aisé de le faire à l'aide de langages tels que Perl ou TCL en filtrant les informations rapportées par la commande tiffinfo, il est plus délicat de le faire avec un shell traditionnel, mais assurément pas impossible.
Récupération des largeur et hauteur :
[prompt_shell]% largeur=`tiffinfo carte.tif |grep 'Image Width' |awk '{ print $3 }'`
[prompt_shell]% hauteur=`tiffinfo carte.tif |grep 'Image Width' |awk '{ print $6 }'`
Trois remarques. La première concerne la syntaxe utilisée ci-dessus car l'assignation de valeurs aux variables d'environnement est un des aspects qui distingue les différents shells. La présente syntaxe fonctionne en particulier pour le shell Bash, pour d'autres il faudra peut être adapter.
En second, il est primordial de faire très attention aux différents types d'apostrophes mis en jeu dans les lignes ci-dessus (la typographie, une fois l'article imprimé, peut réserver quelques désagréables surprises). Dans le cas présent, retenez que les apostrophes extérieures sont toutes deux des apostrophes 'inverses' (accessibles sur la touche du chiffre 7 du pavé alphanumérique, EN CONJONCTION avec la touche 'Alt Gr'). Toutes les apostrophes intérieures (celles qui délimitent l'argument passé à grep AINSI que celles entourant le script passé à awk) sont ici des apostrophes 'classiques' (accessibles sur la touche du chiffre 4 du pavé alphanumérique).
Enfin, les références au format TIFF pour l'image originale étant omniprésentes dans ce qui précède, il va sans dire qu'il peut être préférable de convertir dès le départ son image dans ce format à l'aide de la commande
convert carte.EXTENSION_FORMAT_QUELCONQUE tiff:carte.tif
Ensuite, on choisit les largeur et hauteur que devront avoir chaque vignette. En toute logique, on choisira une largeur constante, ce qui semble relativement approprié dans le cas d'une exploitation avec une interface Web. Prenons donc 250 pixels de large. Le calcul de la hauteur correspondante ressemblera alors à
[prompt_shell]% hauteur_apres_reduction=`expr 250000 / $largeur \* $hauteur / 1000`
Avec pas moins de quatre remarques cette fois. S'il peut sembler ridicule d'utiliser la valeur 250 000 pour, en finale, diviser le tout par 1 000, il faut bien garder à l'esprit que la commande expr travaille essentiellement sur des valeurs entières et que les calculs avec des nombres arrondis donnent des résultats par trop approximatifs. Qui plus est, il serait assez hasardeux d'essayer d'introduire des parenthèses dans une expression soumise à expr car si cette dernière ne bronche pas, les résultats sont pour le moins déroutants.
Le second point porte sur la présence d'une barre de fraction inverse juste devant l'astérisque, ceci dans le but d'empêcher le remplacement par le shell de cette astérisque par la liste des fichiers contenus dans le répertoire courant. (Eh oui! Faudrait voir à pas oublier le comportement du shell dans cette histoire.)
En troisième lieu, prenez soin d'insérer au moins un espace après chaque opérande et opérateur car la syntaxe de expr l'exige. Enfin, les apostrophes sont ici de type 'inverse'.
En conclusion, après ces différentes commandes, la création de la vignette ressemblera à
convert -geometry 250x$hauteur_apres_reduction carte.tif gif:carte.gif
Il est possible que la vignette fasse un pixel de moins en largeur que la taille demandée car les résultats fournis par expr sont systématiquement arrondis par défaut et que, par défaut, convert conserve les proportions de l'image.
Étape 3 : rédaction du script qui affichera une portion découpée depuis l'image originale au format RBG, en fonction des coordonnées du point sélectionné par l'utilisateur sur la représentation réduite (la vignette) comme s'il s'agissait d'un zoom.
Le fichier HTML qui va présenter la vignette à l'utilisateur devra comporter les éléments suivants :
<FORM ACTION="/cgi-bin/zoom" METHOD="GET"> <INPUT TYPE="image" SRC="carte.gif" NAME="point_selectionne" BORDER="0"> </FORM>
Ces trois directives sont pleinement
suffisantes pour expérimenter notre petite application. Il faut copier le fichier carte.gif dans le même répertoire que le fichier HTML ci-dessus, en l'occurrence le dossier /home/httpd/html/carto, créé pour l'occasion. |
Ci-après, le contenu du fichier nommé 'zoom' dans le répertoire des scripts CGI (tel que défini dans le fichier de configuration de votre serveur Web, /home/httpd/cgi-bin par défaut pour apache 1.2.6 sur Linux Red Hat 5.1). Remarque probablement inutile mais n'oubliez pas de rendre le script exécutable par un
chmod a+x zoom
PREMIÈRE VERSION :
1 #!/bin/bash 2 3 largeur_vignette='250' 4 5 largeur=`/usr/bin/tiffinfo /home/httpd/html/carto/carte.tif |grep 'Image Width' |awk '{ print $3 }'` 6 hauteur=`/usr/bin/tiffinfo /home/httpd/html/carto/carte.tif |grep 'Image Width' |awk '{ print $6 }'` 7 8 facteur=`expr $largeur \* 1000 / $largeur_vignette` 9 10 x=`echo $QUERY_STRING | sed -e 's/point_selectionne\.x=//;s/&point_selectionne\.y=[0-9]*//'` 11 y=`echo $QUERY_STRING | sed -e 's/point_selectionne\.x=[0-9]*&point_selectionne\.y=//'` 12 13 csg_x=`expr $x \* $facteur / 1000 - 100` 14 csg_y=`expr $y \* $facteur / 1000 - 100` 15 16 /usr/X11R6/bin/convert -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] gif:/home/httpd/html/carto/tmp/region_$$.gif 17 18 19 echo Content-type: text/html 20 echo 21 22 echo "x = $x ; y = $y<P>" 23 echo "csg_x = $csg_x ; csg_y = $csg_y<P>" 24 25 echo "PID = $$<P>" 26 27 echo "largeur = $largeur ; hauteur = $hauteur<P>" 28 29 echo "/usr/X11R6/bin/convert -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] gif:/home/httpd/html/carto/tmp/region_$$.gif<P>" 30 31 echo '<IMG SRC="/carto/tmp/region_'$$'.gif">'
Note : les numéros en regard de chaque ligne sont uniquement présents pour des raisons de commodité et ne doivent pas apparaître dans votre script.
Ligne 3, la largeur de la vignette étant totalement arbitraire, il nous faut bien la définir explicitement. Lignes 5 et 6, c'est du déjà vu. Ligne 8, le point que va sélectionner l'internaute sera exprimé par rapport aux dimensions de la vignette, or, il nous faut des valeurs exprimées par rapport aux dimensions de l'image originale pour pouvoir découper une région dedans. La variable 'facteur' va nous permettre de les calculer. Lignes 10 et 11, à l'aide de sed, on extrait de la variable d'environnement QUERY_STRING, créée par le serveur Web pour le script CGI, les valeurs x et y du point sélectionné (en fonction de la directive INPUT de notre document HTML, la variable QUERY_STRING est une chaîne ressemblera à : point_selectionne.x=123&point_selectionne.y=456). Lignes 13 et 14, on calcule les coordonnées du coin supérieur gauche de la région que l'on va découper en choisissant arbitrairement une portion carrée de 200 pixels de côté.
Ligne 16, on procède au découpage à proprement parlé. Trois choses sont à noter. En premier lieu, l'utilisation du chemin d'accès complet pour chercher le programme convert. Ceci afin de pallier aux aléas de la valeur de la variable d'environnement PATH communiquée au script (remarquez au passage le manque cruel de rigueur puisque les commandes sed et expr ne bénéficient pas du même traitement, quant à echo, c'est une commande interne du shell qui ne peut donc pas se voir attribuée de chemin d'accès). Petit rappel, la commande 'which' peut vous permettre de connaître le chemin d'accès d'un programme, simplement en lui fournissant comme paramètre le nom de ce dernier comme dans 'which convert'. En second, l'utilisation de la variable spéciale $$. C'est un vieux truc assez simple et suffisamment efficace lorsque l'on veut créer des fichiers temporaires sur un système multi-tâches/multi-utilisateurs. De fait, on ne peut se permettre de choisir un nom de fichier figé car le même programme (ce script en l'occurrence) peut être exécuté par plusieurs personnes presque simultanément avec pour conséquence un fichier temporaire valide uniquement pour le dernier des utilisateurs à l'avoir sollicité. Pour prévenir cela, on utilise un petit subterfuge qui consiste à introduire un élément variable dans le nom du fichier temporaire généré. Oui mais voilà, sur quel élément variable peut-on compter dans un script pour être sur qu'il ne sera pas employé par les sollicitations successives de ce même script ?
Certainement PAS sur un nombre aléatoire fourni la variable spéciale $RANDOM de bash car, étant justement aléatoire, on peut rapidement retomber sur une valeur déjà tirée.
L'idée serait que cet élément soit une des caractéristique qui identifie de manière unique le script en cours d'exécution et auquel on puisse avoir accès, cet élément, c'est le PID (Process IDentification), qui est remis à 0 seulement lors du reboot. L'accès à cette valeur dans un shell se fait par le biais de la variable $$, d'où l'utilisation de cette dernière dans le cas présent.
Enfin, le troisième point important est l'usage des accolades comme délimiteurs du nom de variable 'largeur'. Quelle en est la raison et pourquoi est-elle la seule dans cette ligne à jouir de ce privilège ? La réponse tient dans la nature du caractère qui suit immédiatement le nom de cette variable. C'est une lettre (x dans le cas présent) et si une personne perspicace et observatrice est en mesure de séparer le x de ce nom de variable en lui accordant une signification bien particulière, le shell, pour sa part, ne voit qu'une variable dont la dernière lettre est 'x'. Pour contourner cette interprétation par défaut (et puisque la syntaxe imposée par convert n'autorise pas l'emploi d'espace avant ce fameux 'x'), probablement la seule solution à notre disposition est l'emploi de ces accolades qui permettent d'indiquer clairement au shell où prend fin le nom de la variable. Comme vous l'aurez compris les autres variables peuvent s'en passer, n'étant pas en contact direct avec les lettres (chiffres ou souligné) qui les suivent (plus, point et crochet ne sont pas considérés comme des caractères autorisés dans la composition d'un nom de variable).
Afin de ne pas tout mélanger, les fichiers temporaires sont entreposés dans un sous-répertoire créé spécialement nommé 'tmp' dans notre dossier 'carto'. Il ne faut pas oublier de donner le droit d'écriture à tout le monde pour le répertoire 'tmp' car, par défaut, l'utilisateur associé à l'exécution d'un script CGI est le pseudo-utilisateur 'nobody' et, s'il n'a pas le droit d'écrire dans 'tmp', aucune image temporaire ne pourra être créée.
chmod a+wx /home/httpd/html/carto/tmp
Lignes 19 et 20, c'est l'envoi au navigateur Web de l'entête du document HTML que nous sommes en train de générer dynamiquement. Le deuxième echo sans paramètre est extrêmement important, il provoque l'envoi d'un second retour chariot, qui est interprété par le navigateur Web comme le séparateur entre la zone des entêtes qui est arrivée en premier et la zone des données qui va suivre. Lignes 22 à 29, affichage de toutes les variables nécessaires à ce script. Utile uniquement pendant la phase d'élaboration, je les ai laissées pour bien montrer que c'est encore la méthode la plus simple et la plus pratique de vérifier la cohérence des données tout au long de la conception, allant même jusqu'à afficher la façon dont le shell interprète les paramètres communiqués à une commande pour mettre en évidence des pièges comme celui qui impose de faire usage d'accolades. Ligne 31, la directive HTML pour l'affichage de la portion d'image découpée avec seulement deux remarques. D'une part, bien faire attention aux chemins d'accès mis en œuvre car nous sommes contraints de mélanger des références propres au système de fichiers du système d'exploitation et d'autres propres au classement interne du serveur Web (de plus, prendre garde à l'emploi d'un chemin d'accès relatif, le répertoire ou s'exécute le script n'ayant rien à voir avec celui où se trouve le fichier HTML qui y fait référence). D'autre part, l'emploi qui est fait des apostrophes comme délimiteurs. La difficulté ici est d'éviter l'interprétation des signes supérieur et inférieur, on pourrait utiliser des guillemets ou des apostrophes MAIS puisque l'on veut aussi incorporer des guillemets dans la directive HTML, on utilise alors les apostrophes comme délimiteurs, MAIS, comme les apostrophes, au contraires des guillemets, annulent l'évaluation des variables introduites par le caractères $, on place ces dernières en DEHORS des guillemets. En résume, la règle à retenir est que les apostrophes évitent l'interprétation de caractères jugés spéciaux par le shell SAUF pour les variables avec comme préfixe $. Les apostrophes vont plus loin en inhibant aussi l'interprétation de ce caractère $. Pour les programmeurs Perl, c'est d'autant plus facile à retenir que ces règles y sont identiques.
Les deux principaux défauts
de cette première version sont l'absence de traitement approprié
pour les découpages qui débordent de l'image originale et
l'utilisation de fichiers temporaires. En effet, si le morceau à découper déborde de l'images source, le résultat n'est pas automatiquement tronqué. Un débordement en largeur crée une bande latérale sur la nouvelle générée. En hauteur, le programme convert ne rend tout simplement pas la main. |
Étape 3b : amélioration du script pour recentrer l'image découpée en fonction des limites de l'image originale de sorte à éviter des bordures latérales disgracieuses et des blocages pour les découpes susceptibles de dépasser en haut ou en bas. Par conséquent, introduction de calculs supplémentaires.
Et ces calculs étant tout de même plus simples à exprimer dans un langage comme Perl plutôt qu'avec des commandes comme expr (ou même avec des commandes echo redirigées par un tube de communication sur le programme bc), vous trouverez ci-dessous la traduction en Perl avec une légère amélioration consistant donc à recadrer la portion découpée, lignes 21 et 22, afin qu'elle soit toujours dans les strictes limites de l'image originale, et aussi pour être certain de ne pas provoquer de blocage.
1 #!/usr/bin/perl 2 3 $largeur_vignette = 205; 4 5 open (TIFFINFO, '/usr/bin/tiffinfo /home/httpd/html/carto/carte.tif |'); 6 while (<TIFFINFO>) 7 { 8 if (/Image Width: (\d+) Image Length: (\d+)/) 9 { 10 $largeur = $1; 11 $hauteur = $2; 12 last; 13 } 14 } 15 close (TIFFINFO); 16 17 $facteur = $largeur / $largeur_vignette; 18 19 ($x, $y) = $ENV{'QUERY_STRING'} =~ /point_selectionne\.x=(\d+)&point_selectionne\.y=(\d+)/; 20 21 $csg_x = min (max (0, int ($x * $facteur - 100)), $largeur - 200); 22 $csg_y = min (max (0, int ($y * $facteur - 100)), $hauteur - 200);; 23 24 system ("/usr/X11R6/bin/convert -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] gif:/home/httpd/html/carto/tmp/region_$$.gif"); 25 26 print <<"FIN_HTML"; 27 Content-type: text/html 28 29 x = $x ; y = $y<P> 30 csg_x = $csg_x ; csg_y = $csg_y<P> 31 32 PID = $$<P> 33 34 largeur = $largeur ; hauteur = $hauteur<P> 35 36 /usr/X11R6/bin/convert -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] gif:/home/httpd/html/carto/tmp/region_$$.gif<P> 37 38 <IMG SRC="/carto/tmp/region_$$.gif"> 39 FIN_HTML 40 41 sub min 42 { 43 local ($v1, $v2) = @_; 44 45 return ($v1 < $v2) ? $v1 : $v2; 46 } 47 48 sub max 49 { 50 local ($v1, $v2) = @_; 51 52 return ($v1 > $v2) ? $v1 : $v2; 53 }
Étape 3c : nouvelle amélioration du script en remplaçant les fichiers temporaires par une utilisation judicieuse de la sortie standard avec convert, car les fichiers temporaires sont particulièrement gênants dans le cas du Web, étant dans l'incapacité de déterminer avec exactitude quelle durée de vie leur accorder.
DEUX SOLUTIONS : soit l'on affiche EXCLUSIVEMENT la portion découpée, et dans ce cas, il s'agit d'un simple script qui envoie l'entête approprié et conclut son traitement par l'exécution de convert qui envoie, à son tour, son résultat sur la sortie standard.
Soit on veut incorporer la portion choisie dans un document HTML et il faut alors scinder la tâche en deux scripts qui vont travailler en alternance, un premier pour générer dynamiquement le document HTML avec une directive d'insertion d'image faisant référence au second, ce dernier ayant pour rôle le découpage de la portion d'image et son envoi sur la sortie standard avec l'entête approprié.
Passage à la pratique. Premier cas :
1 #!/usr/bin/perl 2 3 $largeur_vignette = 205; 4 5 open (TIFFINFO, '/usr/bin/tiffinfo /home/httpd/html/carto/carte.tif |'); 6 while (<TIFFINFO>) 7 { 8 if (/Image Width: (\d+) Image Length: (\d+)/) 9 { 10 $largeur = $1; 11 $hauteur = $2; 12 last; 13 } 14 } 15 close (TIFFINFO); 16 17 $facteur = $largeur / $largeur_vignette; 18 19 ($x, $y) = $ENV{'QUERY_STRING'} =~ /point_selectionne\.x=(\d+)&point_selectionne\.y=(\d+)/; 20 21 $csg_x = min (max (0, int ($x * $facteur - 100)), $largeur - 200); 22 $csg_y = min (max (0, int ($y * $facteur - 100)), $hauteur - 200);; 23 24 $| = 1; 25 26 print "Content-type: image/gif\n\n"; 27 28 system ("/usr/X11R6/bin/convert -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] gif:-"); 29 30 sub min 31 { 32 local ($v1, $v2) = @_; 33 34 return ($v1 < $v2) ? $v1 : $v2; 35 } 36 37 sub max 38 { 39 local ($v1, $v2) = @_; 40 41 return ($v1 > $v2) ? $v1 : $v2; 42 }
Vous remarquerez les deux principales innovations de ce script, le type du document généré 'image/gif' qui remplace 'text/html', et le nom du fichier destination dans les paramètres de convert qui laisse place au tiret, caractère qui symbolise pour bon nombre de programmes UNIX, et convert n'échappe pas à cette règle, l'entrée ou la sortie standard. L'image ainsi créée ne transite plus sur le disque mais est directement expédiée au navigateur Web de l'internaute.
Petite variante : si votre image d'origine n'est pas une carte utilisant peu de couleurs mais une photo, le format GIF n'est pas forcément le plus approprié. Au contraire, le JPEG est recommandé. Et cela tombe à point car non seulement vous retrouverez toutes vos couleurs mais, cerise sur le gâteau, l'image générée nécessitera un nombre d'octets bien moins important que son équivalent GIF (et c'est la note de téléphone de l'internaute qui vous remerciera), raisonnablement trois fois moins en conservant une bonne qualité. Enfin, et cela est aussi intéressant pour une image peu colorée, dans le contexte présent convert demandera environ cinq fois MOINS de temps à créer une représentation au format JPEG plutôt qu'au format GIF.
Pour arriver à ce résultat, il suffit d'apporter trois modifications mineures aux lignes 26 et 28 pour obtenir ce qui suit.
26 print "Content-type: image/jpeg\n\n"; 27 28 system ("/usr/X11R6/bin/convert -quality 90 -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] jpeg:-");
Ligne 26, on remplace 'gif' par 'jpeg'. Ligne 28, on fait de même et on ajoute le niveau de qualité de l'image à produire (75 par défaut si cette option est absente).
Second cas :
Un premier document au format HTML est retourné au navigateur, qui fait référence à une image qui N'EXISTE PAS encore puisque l'on se contente juste dans cette référence d'invoquer le script qui aura pour tâche de la générer. Le premier script est donc remanié pour donner ce qui suit.
1 #!/usr/bin/perl 2 3 $largeur_vignette = 205; 4 5 open (TIFFINFO, '/usr/bin/tiffinfo /home/httpd/html/carto/carte.tif |'); 6 while (<TIFFINFO>) 7 { 8 if (/Image Width: (\d+) Image Length: (\d+)/) 9 { 10 $largeur = $1; 11 $hauteur = $2; 12 last; 13 } 14 } 15 close (TIFFINFO); 16 17 $facteur = $largeur / $largeur_vignette; 18 19 ($x, $y) = $ENV{'QUERY_STRING'} =~ /point_selectionne\.x=(\d+)&point_selectionne\.y=(\d+)/; 20 21 $csg_x = min (max (0, int ($x * $facteur - 100)), $largeur - 200); 22 $csg_y = min (max (0, int ($y * $facteur - 100)), $hauteur - 200);; 23 24 25 print <<"FIN_HTML"; 26 Content-type: text/html 27 28 Document HTML généré dynamiquement faisant référence à une 29 image qui va être à son tour générée dynamiquement<P> 30 31 <IMG SRC="/cgi-bin/image_seule.pl?$csg_x.$csg_y.$largeur.$hauteur"><P> 32 33 SANS laisser de trace sur le disque du serveur. 34 35 FIN_HTML 36 37 sub min 38 { 39 local ($v1, $v2) = @_; 40 41 return ($v1 < $v2) ? $v1 : $v2; 42 } 43 44 sub max 45 { 46 local ($v1, $v2) = @_; 47 48 return ($v1 > $v2) ? $v1 : $v2; 49 }
On y retrouve une structure générale présentée précédemment avec quelques retouches. L'appel à convert a disparu, et la directive 'IMG' fait désormais référence à un second script plutôt qu'a une image statique. (Monsieur Tim Berners-Lee, de tout mon cœur, vous êtes génial, on ne le dira jamais assez.) |
Ce fameux script, 'image_seule.pl', vous en avez le contenu ci-dessous.
1 #!/usr/bin/perl 2 3 ($csg_x, $csg_y, $largeur, $hauteur) = split (/\./, $ENV{'QUERY_STRING'}); 4 5 $| = 1; 6 7 print "Content-type: image/jpeg\n\n"; 8 9 system ("/usr/X11R6/bin/convert -quality 90 -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb[200x200+$csg_x+$csg_y] jpeg:-");
Point très important à respecter : la présence de la ligne '$| = 1;', ici, et dans les scripts précédents. La variable spéciale $| en Perl gère le comportement des entrées/sorties quant à l'utilisation des buffers. Par défaut, les entrées/sorties sont 'bufferisées' (anglicisme barbare mais faute de trouver mieux), essentiellement pour des questions de performances. Dans le cas présent, l'affichage de l'entête 'Content-type…' ne suffit pas à lui seul à remplir le buffer, le contenu de ce dernier n'est donc pas 'transmis' (de plus, la fin du script n'étant pas atteinte, ce buffer n'a pas non plus de raison d'être vidé - 'flush' en anglais - ) et l'on passe alors sur la commande system qui invoque convert. Cette dernière envoie l'image, le script récupère la main, se termine et ses buffers sont vidés, l'entête est alors, seulement à ce moment, envoyé… Un peu trop tard !
Étape 3d : utilisation d'une version compressée de l'image RGB.
Si l'image au format RGB est vraiment trop volumineuse, il est possible de la compresser, au détriment, on s'en doute, des performance. En effet, convert reconnaît et traite automatiquement les fichiers compressés à l'aide de compress ou de gzip, juste par la présence des suffixes caractéristiques que ces commandes ajoutent aux noms des fichiers.
Deux minuscules manipulations suffisent. La première est bien sûr de compresser l'image RGB.
gzip -9 carte.rgb
La seconde est de remplacer dans le script CGI le nom 'carte.rgb' par 'carte.rgb.gz'. C'est tout !
Étape 3e : ajout d'une image superposée à celle découpée qui peut contenir, par exemple, une légende, une règle des distances pour avoir une idée de l'échelle, le logo de la société qui offre le service (en guise de copyright) ou une sorte d'étiquette avec en gros le mot 'évaluation' pour montrer à l'internaute qu'il n'accède qu'à une version de démonstration.
La légende à superposer
Dans cette optique, je reprends
system ("/usr/X11R6/bin/convert -draw 'image 90,170 /home/httpd/html/carto/legende.tif' -quality 90 -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb.gz[200x200+$csg_x+$csg_y] jpeg:-");
Vous remarquerez la présence
effective de l'extension '.gz' derrière le nom de l'image RGB et la
syntaxe permettant d'incruster notre légende sur la portion de carte présentée
à l'internaute, placée dans son coin inférieur droit, à
10 pixels de distance des bords. Seul petit regret, à moins d'avoir omis une possibilité dans mes tests, l'absence de gestion de la transparence de l'image collée, si elle existe. |
Étape 3f : gestion libre de la correction gamma.
Poussons le bouchon un peu plus loin pour épater notre internaute en lui offrant la possibilité de modifier à sa guise la correction gamma de la portion d'image qui lui sera présentée. Il faut pour cela modifier nos trois fichiers.
Dans le document HTML de présentation de la vignette, on ajoute quelques directives pour arriver au résultat suivant
<FORM ACTION="/cgi-bin/doc_html.pl" METHOD="GET"> <INPUT TYPE="image" SRC="carte.gif" NAME="point_selectionne" BORDER="0"> <P>Correction gamma <SELECT NAME="gamma"> <OPTION>0.8<OPTION SELECTED>1.0<OPTION>1.2<OPTION>1.4 <OPTION>1.6<OPTION>1.8<OPTION>2.0<OPTION>2.2</SELECT> </FORM>
En fait, on se contente de proposer un menu déroulant avec quelques valeurs prédéfinies dans l'intervalle autorisé par l'option -gamma de convert. |
Dans le script principal, les lignes 19, 20 et 31 sont remplacées par
19 ($gamma, $x, $y) = $ENV{'QUERY_STRING'} =~ /gamma=(\d\.\d)&point_selectionne\.x=(\d+)&point_selectionne\.y=(\d+)/; 20 $gamma *= 10; : 31 <IMG SRC="/cgi-bin/image_seule.pl?$csg_x.$csg_y.$largeur.$hauteur.$gamma"><P>
Enfin, le second et dernier script (image_seule.pl) ressemblera à
#!/usr/bin/perl ($csg_x, $csg_y, $largeur, $hauteur, $gamma) = split (/\./, $ENV{'QUERY_STRING'}); $gamma /= 10; $| = 1; print "Content-type: image/jpeg\n\n"; system ("/usr/X11R6/bin/convert -draw 'image 90,170 /home/httpd/html/carto/legende.tif' -quality 90 -gamma $gamma -size ${largeur}x$hauteur /home/httpd/html/carto/carte.rgb.gz[200x200+$csg_x+$csg_y] jpeg:-");
Ceci clos ce survol de quelques unes des très nombreuses possibilités de convert.
Passons maintenant aux réflexions de comptoir. Bien évidemment, il est possible d'améliorer encore les temps de réponse, en commençant par éviter d'invoquer systématiquement le programme tiffinfo alors que l'on peut stocker les informations nécessaires dans un fichier à part pouvant s'appeler carte.rgb.size par exemple. Il est maintenant de votre ressort d'optimiser ces quelques exemples.
On pourrait aussi, afin d'éviter la multiplication des scripts qui concourent à un même but, les regrouper en un seul avec une détermination automatique du traitement à accomplir par une simple analyse du contenu de la variable d'environnement QUERY_STRING. Un unique script pourrait alors, tour à tour, générer le document HTML ou envoyer l'image invoquée par ce document suivant la nature des paramètres détectés dans QUERY_STRING.
Conseil, avant de vous lancer avec d'imposants fichiers, faites des tests avec des documents de tailles très raisonnables de façon à ne pas être surpris ensuite par les durées des différents traitements et éviter ainsi de vous ronger les ongles devant un ordinateur en apparence inerte.
Pour conclure, le rôle de convert ne se cantonne pas aux nettoyages d'images ou aux découpages, il inclut aussi la création d'animations, au format GIF par exemple, ou la réalisation de planches contact en utilisant plusieurs images comme sources. Il est aussi capable d'extraire une image isolée ou une séquence au sein d'une animation.
Le principal reproche que l'on pourrait faire à ces outils (convert ou mogrify), si ce n'est qu'ils ne font toujours pas le café, c'est de faire preuve d'un mutisme que bien des espions soumis à la torture leurs envieraient.
Si il est des programmes qui vous racontent leur vie pour un oui ou pour un non, le moins qu'on puisse dire pour convert et mogrify, c'est qu'ils ne permettent pas de comprendre leur fonctionnement et la syntaxe qu'ils attendent par une suite d'essais et d'échecs.
La réalité est un peu plus nuancée car chacun de ces programmes dispose d'une option verbose. Mais, d'une part, elle n'est d'aucune aide en cas de syntaxe erronée et surtout, elle image parfaitement le proverbe « le mieux est l'ennemi du bien » car elle affiche les caractéristiques détaillées de chaque document, à l'image de ce que peut afficher la commande tiffinfo lorsqu'il s'agit d'un fichier au format TIFF.
Qui plus est, l'option verbose provoque l'affichage d'informations non pas sur la sortie standard mais sur celle des erreurs, ce qui ne facilite pas le filtrage des messages dans l'optique de se focaliser aux seuls représentatifs de la progression des traitements demandés. (Par contre, c'est de première si l'on doit tourner un écran informatique pour les besoins d'un film car cela fait vraiment sérieux toutes ces lignes cabalistiques qui défilent sur un terminal.)
Remarque : une petite erreur de mise en page s'est glissée dans le 'man'uel de la commande mogrify (tout au moins dans la version disponible avec Linux Red Hat 5.1) puisque l'explication de l'option 'label' se trouve mélangée à celle de l'option 'interlace', la ligne débutant le paragraphe étant victime d'un malencontreux retrait.
Et n'hésitez pas à nous faire part de vos propres emplois originaux de cette famille d'utilitaires, nous essayerons de nous en faire l'écho afin que le plus grand nombre en profite, toujours dans l'esprit qui fait le succès de ces programmes ;-)
Vous pourrez trouver ImageMagick à l'adresse ftp://ftp.wizards.dupont.com/pub/ImageMagick/ImageMagick.X.tar.gz (où X correspond à la numérotation de la dernière version disponible).
Jusqu'ici nous avons utilisé convert (du lot d'utilitaires ImageMagick) pour extraire une petite portion d'une image volumineuse. Nous allons ici améliorer encore ce processus, prétexte pour présenter d'autres commandes intéressantes.
Mais surtout, nous nous affranchirons d'un petit problème que l'on rencontre avec les commandes ImageMagick, à savoir le chargement intégral des images avant leur exploitation. Inconvénient partagé avec bon nombre d'utilitaires graphiques.
Autrement dit, pourquoi charger une image pouvant faire plusieurs milliards d'octets alors que seule l'extraction d'une infime partie nous intéresse. Là encore, cette contrainte sera le prétexte à la mise en œuvre d'une bibliothèque bien sympathique.
On commence en donnant une alternative à l'emploi de convert pour ce qui est de l'extraction, avec la commande fbext tirée du lot de commandes FBM, Fuzzy Pixmap Manipulation (ftp://nl.cs.cmu.edu/usr/mlm/ftp/fbm1.2.tar.Z).
Le bénéfice ici est de pouvoir utiliser en entrée des formats graphiques compressés, ce que n'autorise pas convert dans le cadre d'une extraction si ce n'est par l'artifice consistant à compresser le fichier RGB avec la commande gzip, ce qui est loin d'être idéal.
Une note en passant pour recommander la modification du Makefile de FBM afin que les variables BIN et MAN pointent respectivement sur /usr/local/bin et /usr/local/man (les créer au besoin ainsi que /usr/local/man/manl).
Les utilitaires fournis sont susceptibles de reconnaître les formats les plus usités, à condition toutefois de disposer des bibliothèques correspondantes et d'en fournir les chemins d'accès via le fichier Makefile (pour les TIFF et JPEG en particulier).
Juste pour montrer à quoi pourrait ressembler un appel à fbext dans notre script (image_seule.pl), je me contente du format GIF pour le codage de l'image source. Par contre, fbext ne connaissant que la version 87a du format GIF, il faut au préalable convertir notre image dans ce format comme ci-dessous :
convert /home/httpd/html/carto/carte.tif gif87:/home/httpd/html/carto/carte.gif87
Ensuite dans le script image_seule.pl, il suffit de modifier la ligne principale pour qu'elle ressemble à ce qui suit :
system ("/usr/local/bin/fbext -G $csg_x $csg_y 200 200 </home/httpd/html/carto/carte.gif87 | /usr/X11R6/bin/convert -draw 'image 90,170 /home/httpd/html/carto/legende.tif' -quality 90 -gamma $gamma gif87:- jpeg:-");
Les remarques maintenant. L'option -G non documentée dans le 'man'uel de fbext correspond à l'indication de format GIF en sortie. Cette option ainsi que celles des autres formats connus sont visibles par 'man fbm'. Le format en entrée est déterminé automatiquement et peut donc être différent du résultat.
Il est évident que pour des images de plus de 16 000 pixels de côté, il faudra recourir au format TIFF en entrée et donc interfacer les commandes FBM avec la libtiff. Enfin, comme on peut le constater l'intérêt global demeure faible car il faut tout de même faire appel à convert pour l'incrustation d'une légende sous forme de vignette graphique. Cela permet toutefois de présenter FBM et la syntaxe pour demander à convert de lire sur l'entrée standard dans un format donné.
Cette petite transition n'était là que pour présenter FBM, un des nombreux outils de manipulation des graphiques bitmap. Il n'en reste pas moins qu'il n'est pas forcément très adapté à des extractions au sein de très gros fichiers si lui aussi charge l'intégralité de ces fichiers avant que d'aller piocher dedans.
Aussi, à défaut de trouver la perle rare (qui pourtant existe, je n'en doute pas), on peut la développer nous même. Le cahier des charges est relativement simple : support de grandes images (jusqu'à 4 Go en l'occurrence), pas vraiment de limitation en nombre de pixels en largeur et hauteur, maximum de 256 couleurs (puisque prévu à priori pour des cartes) et surtout, deux points importants, le stockage d'un index pour connaître l'adresse de chaque début de ligne dans le fichier car, et c'est là le deuxième point et l'aspect le plus intéressant, nous allons compacter chaque ligne à l'aide d'une fonction de la librairie zlib.
En clair, notre document pourra faire jusqu'à 4 Go, MAIS 4 Go compressés.
Vous retrouvez les sources auto-documentés des deux programmes, conversion vers, ET depuis, notre format propriétaire. Et même si ce n'est pas encore la panacée, vous percevez rapidement que nous ne nous préoccupons que des lignes dont une partie des points vont se retrouver dans la portion extraite, grâce à ce fameux index.
De retour dans notre script image_seule.pl, la commande principale devient désormais :
system ("/usr/local/bin/cpii2rgb /home/httpd/html/carto/carte.cpii $csg_x $csg_y 200 200 | /usr/X11R6/bin/convert -draw 'image 90,170 /home/httpd/html/carto/legende.tif' -quality 90 -gamma $gamma -size 200x200 rgb:- jpeg:-");
Vous remarquerez que l'on s'appuie toujours sur convert pour générer le document dans son format définitif. Cela nous évite de développer les routines d'écriture en GIF, JPEG ou autre PNG. On se contente d'envoyer du RGB, ce qui est simpliste, et convert se charge du travail délicat. Il permet aussi et surtout de traiter l'incrustation de notre vignette graphique et la correction gamma.
Au préalable, il aura bien évidemment fallu convertir notre image originale dans notre nouveau format (le CPII) à l'aide de la commande suivante :
/usr/local/bin/rgb2cpii /home/httpd/html/carto/carte.rgb 1000 1000 /home/httpd/html/carto/carte.cpii
Concernant les sources, vous passerez rapidement sur la piètre qualité de leur rédaction pour ne vous focaliser que sur certains détails spécifiques à l'emploi des fonctions compress2 et uncompress de zlib.
Le point probablement le plus important à prendre en considération est la double fonction de la variable contenant la taille du buffer de destination. En effet, si après l'appel aux fonctions compress2 ou uncompress elle contient la longueur résultant du traitement, elle sert aussi à fournir à ces mêmes fonctions en entrée la largeur du buffer dans lequel elles peuvent évoluer. Il est donc indispensable de toujours y placer la valeur appropriée AVANT chaque appel.
La grande diversité de librairies ou d'API graphiques spécialisées (libtiff, API de GIMP, librairies d'ImageMagick, libpng, libjpeg, libungif, etc.) fournies en standard ou téléchargeables depuis Internet offre beaucoup de perspectives pour n'importe quel développement spécifique.
Loin de les mettre en opposition comme pourrait le laisser croire le titre de cette note, je voudrais juste souligner une différence de taille qui peut inciter certains utilisateurs de Photoshop à se tourner vers son outsider. Elle concerne la limitation arbitraire qu'impose ce programme, avec des image dont les dimensions ne peuvent excéder très exactement 30 000 pixels de côté. Et cela reste vrai dans la version 5 qui vient de sortir.
Cela peut paraître démesurément grand et hors de portée mais quand (comme ma copine Elfège) vous êtes amenés à utiliser des scanners à tambour format A0 avec une finesse pouvant atteindre les 1200 dpi, vous mesurez rapidement que les limites évoquées n'ont plus rien du fantasme.
GIMP, et vous pouvez faire le test, accepte parfaitement de créer des images supérieures à 65000 pixels. Ce qui n'a rien de délirant, puisque des formats comme le TIFF acceptent, depuis la nuit des temps, de telles valeurs.
Petite information pour ceux qui souhaitent faire évoluer la version de GIMP (0.99) livrée avec Linux Red Hat 5.1. Après avoir téléchargé les différentes archives au format RPM sur le site GIMP, vous allez probablement être confrontés à un message d'avertissement concernant la version de libjpeg disponible sur votre système, déclarée trop « ancienne » par l'archive principale qui contient un plug-in pour la gestion du format JPEG (message « /usr/lib/gimp/1.0/plug-ins/jpeg: error in load no shared libraries libjpeg.so.62 » au lancement manuel de gimp par le biais du shell). Qu'à cela ne tienne, vous pouvez trouver une version à jour sur le CD-ROM du numéro 53 de Dream, dans le répertoire /Linux/gnome0.20/i386, portant le nom libjpeg-6b-4.i386.rpm.
Bien entendu rpm va « râler » à propos de soi-disant problèmes entre le fichier /usr/lib/libjpeg.so.6 et tout un tas d'utilitaires qui en dépendent, mais sans réelle raison apparente car ce lien ainsi que la librairie sur lequel il pointe ne sont pas affectés par l'installation de cette nouvelle version.
Tout cela est, je vous l'accorde, un peu alambiqué et moi-même ai pour l'heure du mal à bien interpréter les avertissements présentés par rpm (je doute même qu'ils soient tous justifiés).
Toujours est-il que n'écoutant que mon inconscience, je lui rétorque un imparable :
rpm -i --nodeps --force libjpeg-6b-4.i386.rpm
C'est un peu cavalier comme procédé mais très efficace, au risque de créer des incompatibilités si l'on ne prend pas garde.