Php est très connu pour ses capacités à générer des pages html. Tout le monde sait aussi qu'il est possible d'effectuer quelques opérations sur des images, comme les redimensionner ou y écrire du texte, en utilisant l'extension gd2. Je vais vous donner dans cet article quelques exemples d'utilisations plus poussées du couple php/gd.
Commençons directement par un exemple qui nous permettra de voir la majorité des fonctions utiles. Les images carrées sont trop strictes. C'est pourquoi, sur les forums, les gens cherchent à savoir comment les arrondir automatiquement. Si on est un utilisateur habitué des logiciels de retouche d'image tels que GIMP, on pense immédiatement aux masques de calque. Voyons comment appliquer celà avec php et gd.
Il nous faut une image originale, que l'on acceptera au choix, au format gif, png ou jpg.
Ces trois types sont pris en charge par gd, mais il nous faut connaître le format à l'avance. Heureusement, Php a pensé à nous et est livré avec une fonction magique. Ne la cherchez toutefois pas dans la librairie gd, elle n'en fait pas partie.
array getimagesize ( string filename [, array &imageinfo] )
Je ne traiterai pas du paramètre optionnel imageinfo
pour ne m'intéresser qu'au tableau renvoyé. Ce dernier contient notamment 3 informations indispensables, la largeur de l'image, sa longueur et son type. Le type renvoyé est numérique. Les valeurs qui nous intéressent sont 1 pour le format gif, 2 pour le jpeg et 3 pour le png. A partir de php 4.3.0, vous aurez directement accès au type MIME de l'image ouverte, ce qui permet d'envoyer directement l'entête adéquat. On ne se servira toutefois pas de cette fonctionnalité ici.
Il me reste à vous présenter l'image qui va nous accompagner tout au long de cet article, déclinée dans trois formats ici pour vous montrer les informations complètes données par getimagesize
.
Voici un code test et sa sortie :
<?php print_r (getimagesize('tux.gif')); print_r (getimagesize('tux.jpg')); print_r (getimagesize('tux.png')); ?>
Array ( [0] => 337 [1] => 400 [2] => 1 [3] => width="337" height="400" [bits] => 8 [channels] => 3 [mime] => image/gif ) Array ( [0] => 337 [1] => 400 [2] => 2 [3] => width="337" height="400" [bits] => 8 [channels] => 3 [mime] => image/jpeg ) Array ( [0] => 337 [1] => 400 [2] => 3 [3] => width="337" height="400" [bits] => 8 [mime] => image/png )
On note la possibilité d'utiliser l'index 3 pour remplir une balise img
avec les largeur et longueur correctes. On ignorera les variables correspondant aux index bits
et channels
, elles représentent le nombre de bits utilisés pour coder chaque couleur (sans fondement pour les gifs ou les png indexés, puisqu'on utilise une palette). Pour les jpeg, la variable d'index channels
vaut 3 pour un jpeg rvb et 4 pour un jpeg cmjn.
Le type de l'image étant maintenant connu, on peut la charger, selon qu'elle est de type gif, jpg ou png, on utilisera une des fonctions suivantes, respectivement :
resource imagecreatefromgif ( string filename ) resource imagecreatefromjpeg ( string filename ) resource imagecreatefrompng ( string filename )
Ces fonctions prennent en paramètre le chemin absolu ou relatif de l'image à charger et retournent une "image gd", à garder pour le reste de notre script. En cas de problème, elles renvoient FALSE
.
On stockera les masques de transparence dans des fichiers png, dans lesquels on pourra stocker la couche alpha sur 8 bits. On pourrait également utiliser un des canaux d'une image RVB ou faire correspondre les index d'une palette à des niveaux de transparence.
De même qu'on peut lire divers types d'images, on peut également les regénérer en sortie, indépendamment de celui qu'on utilise en entrée.
Pour envoyer une image dans un navigateur, on envoie l'en-tête contenant le type mime adéquat, grâce à la fonction header
puis le contenu de l'image à l'aide d'une des fonctions ci-dessous :
bool imagegif ( resource image [, string filename] ) bool imagejpeg ( resource image [, string filename [, int quality]] ) bool imagepng ( resource image [, string filename] )
Ces fonctions prennent en paramètre une "image gd" créée, par exemple, à l'aide d'une des fonctions "imagecreate*
" vues précédemment. Par défaut, l'image est envoyée sur la sortie standard, c'est à dire au navigateur dans la plupart des cas. On peut modifier ce comportement et créer un fichier image, en spécifiant le chemin dans le paramètre optionnel filename
. Dans le cas des jpeg, on peut spécifier la qualité désirée, grâce à l'argument facultatif quality
.
Un script php transformant notre tux.jpg
en png s'écrirait donc très simplement ainsi :
<?php $img = imagecreatefromjpeg ('tux.jpg'); if ($img) { header ("Content-Type: image/png"); imagepng ($img); } ?>
Si vous n'obtenez pas une image dans votre navigateur, vérifiez que vous n'avez pas oublié la ligne contenant l'instruction header
. Si vous obtenez une image, vous pouvez l'enregistrer avec le nom tux.png
et constater que la conversion a bien eu lieu dans votre afficheur/analyseur d'image préféré (ou avec file
).
Pour écrire le résultat de la transformation dans un fichier tux.png
dans un sous répertoire (accesible en écriture), on modifie le script de la façon suivante. Vous devez vous assurer d'avoir le droit d'écrire dans le répertoire writable (vous ou l'utilisateur exécutant le serveur web, selon la configuration retenue par votre hébergeur).
<?php $img = imagecreatefromjpeg ('tux.jpg'); if ($img) { imagepng ($img, 'writable/tux.png'); } ?>
On doit pouvoir appliquer le masque qu'il fasse ou non la même taille que notre image source. On ne peut pas directement redimensionner un image en php/gd. On doit créer une image de la bonne taille et y copier une version redimensionnée de l'image originale.
Pour cette opération, on a le choix entre deux fonctions qui permettent de copier et redimensionner une portion d'image dans une autre.
bool imagecopyresized ( resource dst_image, resource src_image, int dst_x, int dst_y, int src_x, int src_y, int dst_w, int dst_h, int src_w, int src_h ) bool imagecopyresampled ( resource dst_image, resource src_image, int dst_x, int dst_y, int src_x, int src_y, int dst_w, int dst_h, int src_w, int src_h )
Les paramètres sont les mêmes pour les deux fonctions. Tout d'abord, on trouve les images destination et source, suivent les coordonnées du coin supérieur gauche de la zone dans laquelle on va copier dans l'image destination puis celles du coin supérieur gauche de la zone prise dans l'image source. Enfin, on spécifie les largeur et hauteur de la zone destination puis celles de la zone de source.
On préfèrera la première fonction quand la vitesse prime sur la qualité de l'image résultante et la seconde pour l'inverse. Faites des tests avec ces deux fonctions pour vous rendre compte des subtiles différences existant entre elles.
On a vu comment charger l'image source mais il nous faut d'abord créer l'image de destination. Le couple php/gd nous permet de créer des images truecolor, on choisit donc naturellement ce format et on utilise la fonction suivante :
resource imagecreatetruecolor ( int x_size, int y_size )
imagecreatetruecolor
prend en arguments la largeur et la hauteur de l'image à générer, laquelle est renvoyée par l'appel.
Que l'on utilise une image palette ou truecolor, on utilise les couleurs par l'intermédiaire d'index. En conséquence, avant d'utiliser une couleur, on doit l'allouer à l'aide d'une des fonctions suivantes :
int imagecolorallocate ( resource image, int red, int green, int blue ) int imagecolorallocatealpha ( resource image, int red, int green, int blue, int alpha )
Ces deux fonctions prennent pour paramètre une image et les composantes rouge, verte et bleue de la couleur désirée (avec l'habituelle plage 0-255 pour chaque composante).imagecolorallocatealpha
accepte un paramètre supplémentaire qui spécifie le taux de transparence de la couleur. Cette valeur doit être comprise entre 0 (opaque) et 127 (transparente).
bool imagesetpixel ( resource image, int x, int y, int color )
On utilisera cet index dans toutes les fonctions nécessitant un paramètre couleur
, comme par exemple imagesetpixel
, qui permet d'affecter une couleur color
au pixel ayant pour coordonnées x
et y
dans l'image image
.
Plus tard, pour connaître la couleur prise par un pixel en particulier, on récupèrera l'index de cette dernière, puis la valeur de ses composantes, grâce aux fonctions suivantes :
int imagecolorat ( resource image, int x, int y ) array imagecolorsforindex ( resource image, int index )
Le premier paramètre de ces fonctions est une image gd. imagecolorat
prend ensuite les coordonnées du pixel dont on cherche la couleur (toujours avec l'origine en haut à gauche de l'image) et renvoie l'index de la couleur utilisée, que l'on passe en second paramètre à imagecolorsforindex
, laquelle nous retourne un tableau associatif faisant correspondre aux clés red
, green
, blue
et alpha
, la valeur de chacune des composantes de la couleur utilisée pour le pixel.
A présent que les bases sont acquises, on va pouvoir attaquer le premier exemple proprement dit.
Il nous reste deux fonctions à aborder pour comprendre ce filtre :
bool imagealphablending ( resource image, bool blendmode ) bool imagesavealpha ( resource image, bool saveflag )
Ces deux fonctions prennent une image gd comme premier paramètre, et un booléen en second. imagealphablending
avec le flag à true
fera en sorte de toujours générer des pixels opaques, l'apha servant au "mélange" (blending) des couleurs, avec le flag à false
, la valeur de l'alpha est conservée. Le paramètre est pris en compte dans toutes les fonctions qui modifient des pixels (imagesetpixel, imagecopy, ...).
imagesavealpha
permet de sauver le canal alpha dans l'image envoyée au navigateur ou écrite dans un fichier si le flag saveflag
vaut true
, sinon, on ne peut spécifier qu'une couleur transparente et retrouver une gestion de la transparence, "à la gif". Cette fonction n'a de sens qu'avec une sortie au format png.
Avant d'attaquer le code, je vais vous donner une méthode simple pour générer un masque permettant un effet semblable à celui obtenu grâce au filtre alien glow du GIMP mais avec une lueur blanche :
Voyons le script php
<?php $src['file'] = 'tux.jpg'; $src['maskfile'] = 'tux.mask.png'; $src['infos'] = getimagesize ($src['file']); $src['origmask_info'] = getimagesize ($src['maskfile']); $src['origmask'] = imagecreatefrompng ($src['maskfile']); $src['mask'] = imagecreatetruecolor ($src['infos'][0], $src['infos'][1]); imagealphablending ($src['mask'], FALSE); imagecopyresized ( $src['mask'], $src['origmask'], 0, 0, 0, 0, $src['infos'][0], $src['infos'][1], $src['origmask_info'][0], $src['origmask_info'][1]); imagedestroy ($src['origmask']); $src['img'] = imagecreatefromjpeg ($src['file']); imagealphablending ($src['img'], FALSE); for ($i=0; $i < $src['infos'][0]; ++$i) { for ($j=0; $j < $src['infos'][1]; ++$j) { $pxl_alpha = imagecolorsforindex ( $src['mask'], imagecolorat ($src['mask'], $i, $j)); $pxl_color = imagecolorsforindex ( $src['img'], imagecolorat ($src['img'], $i, $j)); $color = imagecolorallocatealpha ( $src['img'], $pxl_color['red'], $pxl_color['green'], $pxl_color['blue'], $pxl_alpha['alpha']); imagesetpixel ($src['img'], $i, $j, $color); } } imagedestroy ($src['mask']); imagesavealpha ($src['img'], TRUE); header ('Content-Type: image/png'); imagepng ($src['img']); imagedestroy ($src['img']); ?>
J'ai choisi de regrouper toutes les informations dans un même tableau que j'appelle $src
.
On commence par y stocker le chemin et les infos des image et masque.
On charge ensuite le masque et on le "redimensionne" grâce à la technique vue précédemment pour lui donner les mêmes dimensions que notre image. Cette phase n'est pas nécessaire si vous avez suivi la méthode donnée pour obtenir un masque mais, l'avantage est que vous pourrez réutiliser le même masque même si vous redimensionnez l'image source. Il ne faut pas oublier de désactiver l'alpha blending pour conserver les informations de transparence du masque.
Notez qu'une fois que le masque ne sert plus, on peut "décharger" l'image en utilisant la fonction suivante :
bool imagedestroy ( resource image )
La fonction imagedestroy
prend en paramètre une image gd "dont on n'a plus besoin". Les ressources associées sont alors libérées. Les ressources affectées à un script php sont limitées, et même si elles sont libérées à la fin de l'exécution du script, c'est une bonne habitude à prendre de faire le ménage soi-même.
On poursuit en chargeant enfin l'image à traiter. Ici, on sait qu'elle est au format jpeg, on utilise donc la fonction adéquate.
On cherche à obtenir une image transparente, on doit donc ici aussi passer l'alpha blending à FALSE
.
Vient ensuite la partie du code à lire avec attention. On parcourt l'image, pixel par pixel. Pour chaque pixel, on récupère la couleur dans le masque (pxl_alpha
) et dans l'image originale (pxl_color
). On alloue ensuite une couleur ayant les composantes rouge, verte et bleue de l'image originale mais la valeur de transparence du masque. On affecte ensuite cette couleur au pixel courant, directement dans l'image d'origine.
On peut ensuite détruire l'image gd utilisée comme masque.
On spécifie ensuite, que l'on souhaite enregistrer les informations de transparence dans l'image résultante, grâce à la fonction imagesavealpha
. On peut ensuite envoyer l'image au navigateur (imagepng
), en prenant auparavant soin de lui envoyer le bon type MIME ("Content-Type"), via la fonction header
.
On utilisera la page suivante pour tester la transparence :
<html> <body bgcolor="red"> <img alt="test alphamask" src="alphamask.php" /> </body> </html>
Il y a plusieurs limitations au script ci-dessus :
On va maintenant voir des pistes de résolution à ces problèmes, dans cet ordre.
Ce morceau là est simple. On va passer en "query-string" l'image et le masque, ce qui nous donnerait, pour l'exemple précédent la balise img suivante :
<img alt="test alphamask" src="alphamask.php?img=tux.jpg&mask=tux.mask.png" />
On passe donc l'image dans un paramètre img, que l'on retrouve dans le script comme $_GET['img']
et le masque de transparence dans un paramètre mask, que l'on récupère dans $_GET['mask']
.
On doit ajouter un petit peu de code en lieu et place de nos deux lignes d'initialisation précédentes, et remplacer :
$src['file'] = 'tux.jpg'; $src['maskfile'] = 'tux.mask.png';par
if (!isset ($_GET['img'])) die ('missing img parameter'); if (!isset ($_GET['mask'])) die ('missing mask parameter'); if (strstr ('http://', $_GET['img'])) die('Only for internal use'); if (strstr ('http://', $_GET['mask'])) die ('Only for internal use'); $src['file'] = $_GET['img']; $src['maskfile'] = $_GET['mask']; if (!file_exists ($src['file'])) die ('Image does not exist'); if (!file_exists ($src['maskfile'])) die ('Image does not exist');
On commence par vérifier l'existence de nos deux paramètres dans la "query-string".
Ensuite, on vérifie qu'on ne tente pas de nous faire faire le traitement sur des images extérieures au site. On n'est pas un gimp online en libre service ...
On affecte alors nos chemins d'images à des variables.
On vérifie que les images existent.
On doit vérifier le type d'image à charger, afin d'utiliser la bonne fonction pour le faire. Voici le code qu'on utilise pour ça :
// On charge l'image de départ en fonction de son type switch ($src['infos']['mime']) { case 'image/jpeg': $src['img'] = imagecreatefromjpeg ($src['file']); break; case 'image/gif': $tmp = imagecreatefromgif ($src['file']); $src['img'] = imagecreatetruecolor ($src['infos'][0], $src['infos'][1]); imagecopy ($src['img'], $tmp, 0, 0, 0, 0, $src['infos'][0], $src['infos'][1]); imagedestroy ($tmp); break; case 'image/png': $src['img'] = imagecreatefrompng ($src['file']); break; default: die ('Mauvais format'); }
J'ai utilisé ici l'index mime
mais on aurait pu se baser sur l'index 2
si on souhaite une compatibilité avec les versions de php antérieures à 4.3.
Notez en outre, que pour convertir une image gif en image truecolor, on utilise une image temporaire, que l'on copie dans une image truecolor créée vide.
Ce brave Microsoft Internet Explorer, en plus de ne pas connaître les onglets (je parle des versions "stables" à l'heure actuelle, donc pre-7), ne gère pas la couche alpha dans les png ... Ce qui donne un résultat pour le moins déplorable ...
On doit donc adapter la sortie de notre script au navigateur employé ... Cela se fait sans peine en testant si la variable $_SERVER['HTTP_USER_AGENT']
contient la chaîne 'MSIE'. On n'a plus ensuite qu'à adapter le contenu envoyé.
La première solution à laquelle on pense est de remplacer un png avec une vraie couche alpha par un png avec une couleur transparente, comme au temps du gif ... Cette solution a l'avantage d'être simple mais on a des problèmes à choisir la couleur transparente.
Un autre solution consiste à partir du principe que le fond est uni et à passer en paramètre la couleur de ce fond. On peut ensuite simuler l'alpha résultant en mélangeant les couleurs avec cette couleur de fond. On génère alors une image jpeg plutôt que png. Cette technique donne de très bons résultats et est utilisable également dans les autres navigateurs, dans lesquels elle permet d'économiser du poids sur les images envoyées. Son seul défaut est que le fond est uni et qu'on ne peut donc pas appliquer cette technique sur un fond complexe.
On utilise une variable globale $isIE
pour stocker le fait qu'on est visités par MSIE ou non :
if (strstr ($_SERVER['HTTP_USER_AGENT'], 'MSIE')) { $isIE = true; } else { $isIE = false; }
On prévoit de récupérer la valeur de fond par 3 variables bg_r
, bg_g
et bg_b
, passées en query-string, définissant les composantes rouge, verte et bleue de la couleur de fond désirée.
if (isset ($_GET['bg_r']) && isset ($_GET['bg_g']) && isset ($_GET['bg_b'])) { $src['img_bgcolor']['r'] = intval ($_GET['bg_r']); $src['img_bgcolor']['g'] = intval ($_GET['bg_g']); $src['img_bgcolor']['b'] = intval ($_GET['bg_b']); }
Si on spécifie une couleur de fond, notre image finale sera opaque mais on devra mélanger notre image originale avec la couleur de fond, selon l'alpha du masque. On doit donc mettre l'alphablending en marche si la couleur de fond a été spécifiée.
imagealphablending ($src['img'], isset ($src['img_bgcolor']));
Si on n'a pas de couleur de fond, on passe l'image en mode indexé grâce à la fonction :
bool imagetruecolortopalette ( resource image, bool dither, int ncolors )
Cette fonction prend en paramètre l'image gd à convertir, un flag dither
, et le nombre de couleurs que doit comporter la palette résultante. Le flag dither
, s'il est à true
, permet de spécifier que l'on souhaite utiliser un algorithme opérant un "tramage" des couleurs. Faites des tests pour déterminer la valeur qui vous convient.
On décide arbitrairement que la couleur du pixel en haut à gauche est la couleur transparente de l'image, on utilise à cette fin la fonction imagecolortransparent
, qui prend en paramètre l'image gd à laquelle appliquer ce réglage et la couleur qui doit devenir la couleur trasparente.
if ($isIE && !isset ($src['img_bgcolor'])) { imagetruecolortopalette ($src['img'], FALSE, 256); $trans_color = imagecolorat ($src['img'], 0, 0); imagecolortransparent ($src['img'], $trans_color); }
Le traitemet des pixels diffère de celui par défaut dans le sens où on doit gérer le cas des images indéxées et le cas des images pour lesquelles un fond a été spécifié.
for ($i=0; $i < $src['infos'][0]; ++$i) { for ($j=0; $j < $src['infos'][1]; ++$j) { if ($isIE && !isset ($src['img_bgcolor'])) { $pxl = imagecolorsforindex ( $src['mask'], imagecolorat ($src['mask'], $i, $j)); $pxl_alpha = $pxl['alpha']; if ($pxl_alpha > =120) imagesetpixel ($src['img'], $i, $j, $trans_color); } else { $pxl_alpha = imagecolorsforindex ( $src['mask'], imagecolorat ($src['mask'], $i, $j)); $pxl_color = imagecolorsforindex ( $src['img'], imagecolorat ($src['img'], $i, $j)); if (!isset ($src['img_bgcolor'])) $color = imagecolorallocatealpha ( $src['img'], $pxl_color['red'], $pxl_color['green'], $pxl_color['blue'], $pxl_alpha['alpha']); else $color = imagecolorallocatealpha ( $src['img'], $src['img_bgcolor']['r'], $src['img_bgcolor']['g'], $src['img_bgcolor']['b'], 127-$pxl_alpha['alpha']); imagesetpixel ($src['img'], $i, $j, $color); } } }
Si on se trouve dans le cas d'une image indexée, on garde la couleur originale si la valeur de l'alpha dans le masque ne dépasse pas un certain seuil (ici 120), sinon, on la remplace par la couleur transparente.
Dans le cas d'une image non indexée, soit la couleur de fond n'est pas spécifiée et on retrouve le même traitement que tout à l'heure, soit la couleur de fond était spécifiée et on doit faire un mélange de couleur ... Plutôt que d'appliquer l'alpha du masque à notre couleur, on y ajoute la couleur de fond. La valeur d'alpha spécifiée pour la couleur originale ajoutée à l'alpha de la couleur de fond doivent être égales à 127, afin d'obtenir un mélange correct, d'où le 127-$pxl_alpha['alpha']
.
Avant d'envoyer l'image au client, on désactive pour IE la gestion de l'alpha dans l'image envoyée :
if (!$isIE) { imagesavealpha ($src['img'], TRUE); }
Ci dessous, on voit à gauche que MSIE ne gère décidément pas le png avec un canal alpha. A droite, la technique de la couleur montre ses limites si on ne retouche pas tux pour lui mettre une couleur unie totalement différente à la place du blanc qui l'entoure. On voit souvent du rose ou du vert dans ce rôle de future couleur transparente. L'image reste malgré tout très pixellisée, la transparence étant gérée de façon booléenne, en tout ou rien.
Ci dessous, on voit que le rendu passant par du jpeg donne une qualité comparable au png sous firefox. Mais ceci n'est valable que pour un fond uni.
Ce calcul, réalisé pixel par pixel, est très consommateur de temps processeur. Le résultat pour une même image source, un même masque, un même fond éventuel ou un même navigateur étant toujours le même, on est tenté de stocker chaque résultat du calcul en "cache", de façon à ne pas le recalculer inutilement. Je vous propose donc une implémentation de mini-système de cache, qui ne recalcule les images qu'en cas de modification de l'image, du masque, de la couleur de fond (si elle est spécifiée) ou du navigateur.
On commence par créer un nom unique pour le fichier en cache, basé sur la somme md5 du chemin vers l'image et le masque, à laquelle on ajoute éventuellement les composantes de la couleur de fond et IE si le navigateur est MSIE.
On prévoit de stocker les fichiers de cache dans un sous répertoire "./cache"
, il faudra s'assurer d'avoir les droits en écriture sur ce répertoire.
$src['cache_file'] = "./cache/".md5 ($src['file'].$src['maskfile']); if (isset ($src['img_bgcolor'])) { $src['cache_file'] .= "r".$src['img_bgcolor']['r']; $src['cache_file'] .= "g".$src['img_bgcolor']['g']; $src['cache_file'] .= "b".$src['img_bgcolor']['b']; } else { $src['cache_file'] .= (($isIE)?"IE":""); }
On doit vérifier si le fichier en cache existe et est valide. Si c'est le cas, on pourra l'envoyer directement, plutôt que de recalculer l'image.
if (file_exists ($src['cache_file']) && (filemtime ($src['file']) < filemtime ($src['cache_file'])) && (filemtime ($src['maskfile']) < filemtime ($src['cache_file']))) { if (!isset ($src['img_bgcolor'])) { header ("Content-Type: image/png"); } else { header ("Content-Type: image/jpeg"); } readfile ($src['cache_file']); exit(); }
Pour vérifier l'existence du fichier cache, on utilise la fonction file_exists
.
On vérifie ensuite que le fichier cache a été créé après les dernières modifications de l'image et du masque, grâce à la fonction filemtime
Dans le cas ou on considère valide le fichier en cache, on l'envoie au client, via la fonction readfile
qui envoie sur la sortie standard ce qu'elle lit dans le fichier passé en argument. On spécifie le type mime correct grâce à la fonction header
en se basant sur la définition ou non d'une couleur d'arrière plan.
Dans la première version de ce script, je créais le fichier cache, grâce à la fonction touch, juste près les tests, s'il n'existait pas. Celà evite d'avoir 2 scripts qui calculent l'image en même temps quand elle doit être mise à jour, en contrepartie, si le script ne se termine pas correctement, vous servirez un fichier vide à vos visiteurs. On peut tester que le fichier n'est pas vide mais on retombe sur le cas où on ne le crée pas. Une solution à base de lock sur le fichier pourrait être envisagée mais la simple fonction flock n'est pas garantie de fonctionner à 100%. La solution que j'utilise est celle présentée ici et elle fait qu'on se retrouve toujours avec un fichier valide. Ce système de cache est néanmoins perfectible et j'attends vos améliorations avec impatience.
Le reste du traitement se déroule comme précédemment jusqu'à l'envoi au client.
if (!isset ($src['img_bgcolor'])) { imagepng ($src['img'], $src['cache_file']); } else { imagejpeg ($src['img'], $src['cache_file']); }
Le fichier est créé en cache, au format jpeg si on a une couleur de fond ou png sinon.
On peut ensuite faire le ménage dans les images gd :
imagedestroy ($src['img']); imagedestroy ($src['mask']);
Arrivé à la fin du script, on a une image valide dans le fichier cache, donc on l'envoie, précédé du bon type mime, comme vu plus haut.
if (!isset ($src['img_bgcolor'])) { header ("Content-Type: image/png"); } else { header ("Content-Type: image/jpeg"); } readfile ($src['cache_file']);
On a vu au travers du premier exemple les techniques php/gd pour manipuler une image et envoyer le résultat à nos clients. Je vous propose donc de voir une série de scripts démontrant quelques traitements d'image courants.
Les techniques de cache restent valable dans tous les cas. On veillera en revanche à préfixer le nom du fichier en cache par le nom du script qui le génère (avant ou après appliquer la somme md5), afin de s'assurer que le nom du fichier en cache est unique.
Convertir une image en niveaux de gris est très simple. Il suffit, pour chaque pixel, d'affecter à chaque composante, rouge, verte et bleue, la moyenne de ces dernières.
<?php $src['file'] = 'tux.jpg'; $src['infos'] = getimagesize ($src['file']); $src['img'] = imagecreatefromjpeg ($src['file']); for ($i=0; $i < $src['infos'][0]; ++$i) { for ($j=0; $j < $src['infos'][1]; ++$j) { $pxl_color = imagecolorsforindex ($src['img'], imagecolorat ($src['img'], $i, $j)); $gray = intval (($pxl_color['blue'] + $pxl_color['green'] + $pxl_color['blue'])/3); $color = imagecolorallocatealpha ($src['img'], $gray, $gray, $gray, $pxl_color['alpha']); imagesetpixel ($src['img'], $i, $j, $color); } } header ('Content-Type: image/png'); imagepng ($src['img']); imagedestroy ($src['img']); ?>
On réutilise notre image tux.jpg et on en lit la taille d'une façon semblable à celle vue précédemment avant de l'ouvrir.
On utilise ensuite deux boucles imbriquées, comme pour le filtre précédent, afin de faire la moyenne de nos composantes et de l'affecter à chacune d'entre elles.
L'image est envoyée au serveur à l'issue du traitement dans ces boucles.
On utilise en entrée une image convertie en niveaux de gris. On retrouve donc la valeur de gris dans n'importe laquelle des composantes rouge vert ou bleue du pixel.
On peut appliquer un principe simple consistant à dire que toute valeur de gris supérieure à 127 sera blanche et les autres noires. Le résultat obtenu est toutefois trop brut.
Pour obtenir une image noir et blanc propre, on doit utiliser du tramage. On utilise bien la technique citée ci-dessus mais on calcule l'erreur générée par le passage du gris au blanc ou noir. On redistribue cette erreur sur les pixels voisins. Cet algorithme est connu sous le nom de Floyd-Steinberg Dithering par les habitués des logiciels de retouche d'image.
On parcourt l'image ligne par ligne, de gauche à droite, et on répartit l'erreur du pixel courant sur tous les pixels voisins n'ayant pas encore été traités (au nombre de quatre, pour un total de 8). La répartition de l'erreur faite sur un pixel X est représentée sur l'image ci-dessous. Les pixels marqués T sont ceux ayant déjà été convertis en noir et blanc.
Un variante prévoit de parcourir les lignes de pixels alternativement de droite à gauche puis de gauche à droite afin d'obtenir une meilleure répartition de l'erreur. Je n'ai toutefois jamais constaté de gain de qualité visible à l'oeil nu.
<?php $src['file'] = 'tux.jpg'; $src['infos'] = getimagesize ($src['file']); $src['img'] = imagecreatefromjpeg ($src['file']); for ($i=0; $i < $src['infos'][0]; ++$i) { for ($j=0; $j < $src['infos'][1]; ++$j) { $pxl_color = imagecolorsforindex ($src['img'], imagecolorat ($src['img'], $i, $j)); $gray = intval (($pxl_color['blue'] + $pxl_color['green'] + $pxl_color['blue'])/3); $color = imagecolorallocate ($src['img'], $gray, $gray, $gray); imagesetpixel ($src['img'], $i, $j, $color); } } for ($j=0; $j < $src['infos'][1]; ++$j) { for ($i=0; $i < $src['infos'][0]; ++$i) { $gray = imagecolorat ($src['img'], $i, $j) & 0xFF; $nb = ($gray>127) ? 255 : 0; $color = imagecolorallocate ($src['img'], $nb, $nb, $nb); imagesetpixel ($src['img'], $i, $j, $color); $err = ($gray-$nb)/16; if ($j+1 < $src['infos'][1]) { if ($i > 0) { $gray = imagecolorat ($src['img'], $i-1, $j+1) & 0xFF; $gray += 3*$err; $gray = intval ($gray); if ($gray > 255) { $gray = 255; } else if ($gray < 0) { $gray = 0; } $color = imagecolorallocate ($src['img'], $gray, $gray, $gray); imagesetpixel ($src['img'], $i-1, $j+1, $color); } $gray = imagecolorat ($src['img'], $i, $j+1) & 0xFF; $gray += 5*$err; $gray = intval ($gray); if ($gray > 255) { $gray = 255; } else if ($gray < 0) { $gray = 0; } $color = imagecolorallocate ($src['img'], $gray, $gray, $gray); imagesetpixel ($src['img'], $i, $j+1, $color); if ($i+1 < $src['infos'][0]) { $gray = imagecolorat ($src['img'], $i+1, $j+1) & 0xFF; $gray += $err; $gray = intval ($gray); if ($gray > 255) { $gray = 255; } else if ($gray < 0) { $gray = 0; } $color = imagecolorallocate ($src['img'], $gray, $gray, $gray); imagesetpixel ($src['img'], $i+1, $j+1, $color); } } if ($i+1 < $src['infos'][0]) { $gray = imagecolorat ($src['img'], $i+1, $j) & 0xFF; $gray += 7*$err; $gray = intval ($gray); if ($gray > 255) { $gray = 255; } else if ($gray < 0) { $gray = 0; } $color = imagecolorallocate ($src['img'], $gray, $gray, $gray); imagesetpixel ($src['img'], $i+1, $j, $color); } } } header ('Content-Type: image/png'); imagepng ($src['img']); imagedestroy ($src['img']); ?>
On reconnaît ci-dessus le passage en niveaux de gris de l'image selon l'algorithme présenté juste avant.
Vient ensuite le parcours de l'image comme expliqué dans l'introduction de cette partie. On répartit sur les pixels non traités l'erreur du pixel courant en veillant à passer un nombre entier, sous peine d'avoir d'incompréhensibles bugs d'affichage.
Ce script (dont on prendra soin de mettre les résultats en cache), permet d'obtenir des images pouvant être imprimées sur des imprimantes noir et blanc, avec une qualité très correcte. Selon la résolution de ces dernières, on arrive à simuler une image en niveaux de gris (à condition tout de même de ne pas y regarder de trop près). Posez donc votre magazine et reculez vous. Rapidement, vous ne verrez plus les points dans l'image ci dessous (mais vous aurez l'air bizarre).
Une grande partie des filtres appliqués aux images repose sur la convolution de matrice. Derrière ce terme aimé des mathématiciens se cache une réalité très simple.
Un filtre utilisant cette technique définit simplement comment calculer la couleur d'un pixel à partir de celle des pixels voisins.
On utilise souvent des matrices 3x3 ou 5x5. Naturellement, plus grande est la matrice, plus long est le calcul.
Dans la matrice ci-dessus, chaque composante de chaque pixel, notée Z(x,y) est définie par la formule suivante :
(a+b+c+d+e+f+g+h+i)*Z(x,y) = a*Z(x-1,y-1)+b*Z(x,y-1)+c*Z(x-1,y-1)+d*Z(x-1,y)+e*Z(x,y)+f*Z(x+1,y)+g*Z(x-1,y+1)+h*Z(x,y+1)+i*Z(x+1,y+1)
Il y a toutefois une exception, dans le cas où la somme des cases de la matrice (a+b+c+d+e+f+g+h+i) est nulle, on la remplace par 1.
Ceux qui ont suivi noteront ici qu'on ne peut pas traiter les pixels au bord de l'image avec cette formule puisqu'on devrait alors utiliser des pixels qui n'existent pas. On utilise dans ce cas une des trois techniques suivantes, au choix :
Dans les cas que l'on abordera dans la suite de l'article, on utilisera la première solution.
Intéressons nous au filtre de détection de bords (edge detection), qui a le mérite de pouvoir se faire avec une matrice très simple, donnée ci-dessous. Comme dans la plupart des filtres, ce n'est pas la seule que l'on peut utiliser pour obtenir le résultat attendu.
Ce qui donne la formule suivante:
Z(x,y)=-5*Z(x-1,y-1)+5*Z(x+1,y+1)
Cette matrice accentue surtout les diagonales, on peut également lui préférer cette autre matrice, produisant un effet moins violent.
Ce qui donne la formule suivante:
Z(x,y)=-Z(x-1,y-1)-Z(x,y-1)-Z(x+1,y-1)+Z(x-1,y+1)+Z(x,y+1)+Z(x+1,y+1)
Voici la matrice utilisée pour "bosseler" une image, technique que l'on retrouve habituellement pour calculer les coefficients pour le filtre de "Bosselage" (Bump mapping), qui permet de donner un effet 3D dans les images.
Soit:
Z(x,y)=2*Z(x-1,y-1)-Z(x,y)-Z(x+1,y+1)
La detection de bords et le bosselage fonctionnent mieux sur une image en niveaux de gris. On utilisera donc le même type de script pour les deux. Ci dessous, une implémentation du filtre de détection de bords.
<?php $src['file'] = 'tux.jpg'; $src['infos'] = getimagesize ($src['file']); $src['img'] = imagecreatefromjpeg ($src['file']); $filter = array ( array (-1,-1,-1), array ( 0, 0, 0), array ( 1, 1, 1) ); $filter_sum = 0; foreach ($filter as $ligne) { foreach ($ligne as $val) { $filter_sum += $val; } } $filter_w = count ($filter[0]); $filter_h = count ($filter); for ($i=0; $i < $src['infos'][0]; ++$i) { for ($j=0; $j < $src['infos'][1]; ++$j) { $pxl_color = imagecolorsforindex ($src['img'], imagecolorat ($src['img'], $i, $j)); $gray = ($pxl_color['blue'] + $pxl_color['green'] + $pxl_color['blue'])/3; $color = imagecolorallocate ($src['img'], $gray, $gray, $gray); imagesetpixel ($src['img'], $i, $j, $color); } } $temp = imagecreatetruecolor ($src['infos'][0], $src['infos'][1]); imagecopy ($temp, $src['img'], 0, 0, 0, 0, $src['infos'][0], $src['infos'][1]); for ($i=1; $i < $src['infos'][0]-1; ++$i) { for ($j=1; $j < $src['infos'][1]-1; ++$j) { $sumc=0; for ($k=0; $k < $filter_w; ++$k) { for ($l=0; $l < $filter_h; ++$l) { $gray = imagecolorat ($src['img'], $i-(($filter_w-1)>>1)+$k, $j-(($filter_h-1)>>1)+$l) & 0xFF; $sumc += $gray * $filter[$k][$l]; } } if ($filter_sum) $sumc /= $filter_sum; else $sumc += 128; $sumc = intval ($sumc); if ($sumc > 255) $sumc = 255; elseif ($sumc < 0) $sumc = 0; $color = imagecolorallocate ($temp, $sumc, $sumc, $sumc ); imagesetpixel ($temp, $i, $j, $color); } } imagedestroy ($src['img']); header ('Content-Type: image/png'); imagepng ($temp); imagedestroy ($temp); ?>
Après avoir fourni l'image de départ et l'avoir chargée, on définit notre matrice, un tableau de tableaux. On calcule ensuite dans une double boucle foreach
la somme des nombres la constituant ($filter_sum
). On stocke enfin la largeur ($filter_w
) et la hauteur ($filter_h
) de la matrice pour l'utiliser utlérieurement.
L'image est convertie en niveaux de gris puis copiée dans une image, laquelle contiendra le résultat à la fin du traitement. On doit passer par une image intermédiaire puisqu'on a besoin d'accéder aux valeurs de couleurs de pixels qui ont déjà été "transformés" par le filtre. Dans les faits, on n'a pas besoin de l'image entière (2 lignes ou colonnes suffiraient) mais c'est plus simple à gérer et nos images étant petites on a de la place en mémoire (400x400 pixels représentent un total de 160 000 pixels. A raison de 8 bits par composante, une image occuppe 520 ko de mémoire environ et par défaut, un script php a droit à 8 Mo). Il ne faudrait pas prendre de tels raccourcis en pratique, sauf si l'on connaît son environnement sur le bout des doigts.
On poursuit le script par les deux boucles for
imbriquées qui parcourent les pixels de l'image.
Pour chaque pixel, on applique la formule vue précédemment. On fait donc la somme des valeurs de gris multipliées par les coefficients de la matrice. Cette partie est prise en charge par les deux boucles for
suivantes.
Cette somme est alors divisée par la somme des coefficients de la matrice. Si cette somme est nulle, on ne fait pas la division mais on ajoute 128, pour remonter la moyenne des valeurs dans la tranche 0-255. On veille enfin à ne pas avoir une valeur hors de cette zone. On affecte ensuite la couleur au pixel en cours.
La suite de script libère les ressources et envoie l'image au navigateur.
Ce script ne détecte pas les bords, mais les fait ressortir. On remarque que les bords sont très noirs ou très blancs. On pense alors à un filtre (seuillage) que l'on "bricole" rapidement en remplaçant :
if ($sumc > 255) $sumc = 255; elseif ($sumc < 0) $sumc = 0;
par
if ($sumc >= 200) $sumc = 0; elseif ($sumc <= 55) $sumc = 0; else $sumc = 255;
Ou
if ($sumc >= 155) $sumc = 0; elseif ($sumc <= 100) $sumc = 0; else $sumc = 255;
En adaptant les seuils pour finalement extraire les "bords" de notre mascotte avec la granulité qui convient à chacun.
Pour appliquer le filtre de "bosselage", il nous suffit de remplacer la déclaration du filtre précédent par:
$filter = array ( array ( 2, 0, 0), array ( 0,-1, 0), array ( 0, 0,-1) );
Vous aurez naturellement pris soin de désactiver le bricolage nous ayant précédemment permis d'obtenir les bords de Tux.
Intéressons nous maintenant au filtres permettant d'améliorer la netteté d'une image ou, au contraire, de la rendre floue.
On ne fait plus la conversion en niveaux de gris, on doit donc modifier les scripts précédents pour qu'ils appliquent les filtres sur les trois composantes rouge, verte et blueue. Le seul changement notable s'opère dans les boucles for
imbriquées. Les modifications sont ci-dessous :
for ($i=1; $i < $src['infos'][0]-1; ++$i) { for ($j=1; $j < $src['infos'][1]-1; ++$j) { $sumr=0; $sumg=0; $sumb=0; for ($k=0; $k < $filter_w; ++$k) { for ($l=0; $l < $filter_h; ++$l) { $pxl_color = imagecolorsforindex ( $src['img'], imagecolorat ($src['img'], $i-(($filter_w-1)>>1)+$k, $j-(($filter_h-1)>>1)+$l) ); $sumr += $pxl_color['red'] * $filter[$k][$l]; $sumg += $pxl_color['green'] * $filter[$k][$l]; $sumb += $pxl_color['blue'] * $filter[$k][$l]; } } if ($filter_sum) { $sumr /= $filter_sum; $sumg /= $filter_sum; $sumb /= $filter_sum; } else { $sumr += 128; $sumg += 128; $sumb += 128; } if ($sumr > 255) $sumr = 255; elseif ($sumr < 0) $sumr = 0; if ($sumg > 255) $sumg = 255; elseif ($sumg < 0) $sumg = 0; if ($sumb > 255) $sumb = 255; elseif ($sumb < 0) $sumb = 0; $color = imagecolorallocate ($temp, intval($sumr), intval($sumg), intval($sumb) ); imagesetpixel($temp, $i, $j, $color); } }
L'initialisation concerne à présent sumr
, sumg
et sumb
qui au lieu de l'unique sumc
.
On récupère à nouveau les composantes des pixels à l'aide de la combinaison des fonctions imagecolorsforindex
et imagecolorat
et on applique le filtre à chacune.
Enfin, toujours pour les trois composantes, on divise par la somme des valeurs de la matrice et on veille à ce que les valeurs soient dans la bonne fourchette avant de créer la couleur résultante et de l'affecter au pixel en cours.
Il nous reste à voir quelles matrices on utilise pour améliorer la netteté ou rendre floue une image.
On utilise pour cet effet la matrice suivante, qui permet d'augmenter les différences existantes entre un pixel et ses voisins.
De façon plus générale, on utilise une matrice dépendant d'un paramètre $f
.
On fait varier $f
pour adapter la force de l'effet. La somme des coefficients vaut toujours 1.
Il existe des exemples de matrices permettant de flouter une image. Voici celle que j'ai utilisée :
Le détail qui saute alors aux yeux du mathématicien est qu'on peut décomposer cette matrice comme le produit de deux vecteurs (1,2,1), ce qui permet d'optimiser grandement notre code. Je ne traiterai toutefois pas de cette optimisation ici. Vous trouverez un script en tenant compte dans les liens en fin d'article.
On peut (et on doit) pour chacun de ces scripts passer les valeurs variables en paramètres et utiliser un mécanisme de cache. Pour peu que l'on respecte ces prérequis, ces manipulations d'images peuvent être intégrées à un site et fonctionner sur de petites ou moyennes images ou permettre des traitements sur des images plus conséquentes dans des scripts utilitaires exécutés par l'interpréteur php dans sa version ligne de commande (CLI), via la crontab par exemple.
imagefilter
, fonction introduite dans php 5 permet d'appliquer des filtres prédéfinis.
Une autre fonction au nom évocateur a fait son apparition dans la version 5.1 de php, imageconvolution
. Cette fonction est limitée à des matrices 3x3 mais on a vu au long de cet article qu'elles étaient suffisantes.
Pourquoi utiliser ces fonctions, plutôt que les scripts vu ci-dessus ? Si vous avez la version de php adéquate et que vous utilisez la librairie gd interne à php (c'est à dire php compilé avec le flag --with-gd
et non --with-gd=/path
) utilisez sans hésiter les fonctions intégrées. Elles sont beaucoup plus rapides car il y a énormément moins d'échanges entre php et votre script.
bool imagefilter ( resource src_im, int filtertype [, int arg1 [, int arg2 [, int arg3]]] )
Les filtres disponibles sont assez nombreux, consultez la page d'aide pour les détails.
A titre d'exemple, l'application d'un filtre de flou gaussien est donné ci-dessous :
<?php $src['file'] = 'tux.jpg'; $src['infos'] = getimagesize ($src['file']); $src['img'] = imagecreatefromjpeg ($src['file']); imagefilter ( $src['img'], IMG_FILTER_GAUSSIAN_BLUR); header ('Content-Type: image/png'); imagepng ($src['img']); imagedestroy ($src['img']); ?>
bool imageconvolution ( resource image, array matrix3x3, float div, float offset )
Pour l'utiliser, il suffit dans vos scripts de remplacer les 4 boucles for
par un appel à cette fonction.
On doit lui passer l'image gd en premier paramètre, la matrice du filtre (la variable $filter
), la somme des coefficients (la variable $filter_sum
).
<?php $src['file'] = 'tux.jpg'; $src['infos'] = getimagesize ($src['file']); $src['img'] = imagecreatefromjpeg ($src['file']); $filter = array ( array (1,2,1), array (2,4,2), array (1,2,1) ); $filter_sum = 0; foreach ($filter as $ligne) { foreach ($ligne as $val) { $filter_sum += $val; } } imageconvolution ( $src['img'], $filter, $filter_sum, 0 ); header ('Content-Type: image/png'); imagepng ($src['img']); imagedestroy ($src['img']); ?>
J'ai effectué quelques tests successifs avec les trois scripts pour avoir une idée du gain de temps, sans gestion de cache, pour ne s'intéresser qu'aux scripts.
Les performances ne sont bien sûr qu'un aspect. imagefilter
ne permet pas quasimment pas de modifier le comportement des filtres (pas du tout dans le cas du filtre de flou gaussien), imageconvolution
ne permet pas d'utiliser des matrices non 3x3. De plus, ces fonctions ne sont pas disponibles chez beaucoup d'hébergeurs. Toutefois, si leur traitement vous convient et que vous en avez la possibilité, utilisez les sans hésiter.
Deux requêtes successives sur chaque script rapportent les temps suivants :
imagefilter.php
: 311msec, 361msecimageconvolution.php
: 351msec, 340msecblur.php
: 7691msec, 7862msecIl n'y a pas de surprise, on retrouve les mêmes écarts de temps de traitement en comparant des traitements similaires en tcl/tk pur et en utilisant une extension réalisant les traitements en C.
a+