Manipulation d'images avec php

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.

Exemple 1 : Masque de transparence

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.

Informations sur l'image

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.

Tux the penguin

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.

Charger les images

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.

Envoyer les images au navigateur

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');
}
?>

Redimensionnement d'image

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.

Création d'images

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.

Création de couleurs

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.

Appliquer le masque de transparence

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 :

  1. Ouvrez tux.jpg dans GIMP
  2. Commençons par ajouter un canal alpha (Clic-droit > Layer > Transparency > Add alpha channel)
  3. Sélectionnez l'outil baguette magique et cliquez dans le blanc autour de tux.
  4. Faites Ctrl+K. On se retrouve avec un damier autour de tux
  5. Faites alors Ctrl+I pour inverser la selection
  6. On va agrandir un peu la sélection pour ne pas avoir de parasite (Clic droit > Select > Grow). On choisit une valeur de 5 pixels
  7. Faites Ctrl+K à nouveau.
  8. On doit redonner à la sélection sa taille d'origine. (Clic droit > Select > Shrink) et on réduit de 5 pixels.
  9. On va adoucir un peu la sélection. (Clic droit > Select > Feather) et on utilise la valeur 20 pixels.
  10. On choisit ensuite l'outil remplissage, on passe le blanc (la couleur importe peu, c'est l'apha résultant qui nous intéresse) et on clique deux fois dans la sélection.
  11. On enregistre le masque résultant comme tux.mask.png dans le même répertoire que tux.jpg.

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>
screenshot alphamask

Vie réelle

Il y a plusieurs limitations au script ci-dessus :

On va maintenant voir des pistes de résolution à ces problèmes, dans cet ordre.

Passage de l'image en paramètre

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.

Détection du type d'image

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.

La cas de MSIE

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é.

Alternatives ?

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.

Implémentation

On utilise une variable globale $isIEpour 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.

couleur transparente

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.

jpeg et bgcolor

Gestion de cache

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']);

Autres Exemples

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.

Exemple 2 : Conversion en niveaux de gris

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.

N et B Tux

Exemple 3 : Noir et blanc

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.

repartition d'erreur fs

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).

N et B Tux

Une poignée de matrices ...

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.

Matrix 1

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.

Détection de bords / Bosselage

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.

Matrix 2

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.

Matrix 3

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.

Matrix 4

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.

bords de Tux

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.

bords de Tux (2) bords de Tux (3)

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.

Bump Tux

Netteté / Floutage

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.

Amélioration de netteté (Sharpen)

On utilise pour cet effet la matrice suivante, qui permet d'augmenter les différences existantes entre un pixel et ses voisins.

Matrix 5
Tux est net

De façon plus générale, on utilise une matrice dépendant d'un paramètre $f.

Matrix 6

On fait varier $f pour adapter la force de l'effet. La somme des coefficients vaut toujours 1.

Rendre flou (Gaussian Blur)

Il existe des exemples de matrices permettant de flouter une image. Voici celle que j'ai utilisée :

Matrix 7
Tux est flou

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.

Performances

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.

Quoi de neuf dans php 5 ?

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.

Exemples

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 :

Il 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.

Liens & Références :

a+

Auteur : Xavier GARREAU
Modifié le 31.05.2006