Le problème ici ne vas pas être de brouiller l'image comme on en a l'habitude, car au contraire le texte de celle-ci sera tout a fait lisible. Cependant nous gardons le principe d'un texte à lire contenu dans une image(#1) que nous allons découper par la moitié(#2) et inverser les morceaux(#3) dans l'image finale(#4).

Puis grâce au CSS nous allons faire en sorte que les morceaux de l'image apparaissent dans l'ordre grâce au décalage et la répétition(#5) afin de permettre au lecteur de lire le texte. De cette manière la majorité des bots actuels[1] possédants un OCR auront beau récupérer l'image ils seront incapables de fait de comprendre le texte vu que celui-ci sera coupé en plusieurs morceaux dans l'image.

L'illustration suivante explique le principe en image :

Je ferais volontairement l'impasse sur certains point de sécurité ou détail divers, mais un peu de bon sens, quelques lignes de code en plus, et votre anti-bot sera pleinement opérationnel et efficace. Je me concentre sur l'essentiel pour mettre en pratique l'idée de l'anti-bot en CSS, une autre fois je pourrais éventuellement corriger certaines choses, en optimiser d'autres, et fignoler la totalité... si quelqu'un s'en occupe avant qu'il le propose !

Le code que je vous donne marche aussi bien sous PHP4 que sous PHP5.

Bon j'ai fait plusieurs tests de mise en application pour bien comprendre ce qu'une telle solution implique. et le premier constat est que si on veut couper l'image en PHP puis la recoller en CSS et bien il faut que PHP et CSS connaissent tout deux les dimensions de l'image ainsi que les offsets de découpe... Or ceci n'est pas si évident que ça car d'une part nous n'avons qu'un texte seul au départ (le code à afficher) et on ne peut pas se permettre de diffuser les données nécessaires dans les urls ou quelque chose du genre car ce serait un moyen très facile pour reconstituer l'image avec un bot.

La solution qui à mon avis sera la plus pratique et aussi la plus simple avec une sécurité suffisante serais de créer un tableau contenant le texte et les dimensions de l'image associée ainsi que l'offset de découpe. Tout ceci pourrait être généré aléatoirement, et il suffirait alors de donner un peu partout (pages HTML, url, CSS...) seulement l'indice du tableau et le reste serais imbriqué dans du PHP grâce à cet indice.

Ca donnerais quelque chose du genre (les chiffres ne représentent rien, je marque n'importe quoi pour l'exemple) :

<?php
$grand_tableau = array(
     array('texte'=>'azerty','w'=>200,'h'=>40,'w_o'=>48,'h_o'=>27),
     array('texte'=>'antibot','w'=>200,'h'=>50,'w_o'=>79,'h_o'=>34),
     array('texte'=>'natsimhan','w'=>300,'h'=>32,'w_o'=>39,'h_o'=>12),
     array('texte'=>'phpcss','w'=>400,'h'=>60,'w_o'=>123,'h_o'=>53),
);
?>

Mais pour aujourd'hui, et aussi pour ne pas tout faire à votre place et vous laisser un peu de travail ;-) nous allons nous contenter de créer une fonction qui donnera tous les paramètres nécessaire en fonction d'une chaine de texte. Notez cependant que je suis gentil car je vous fournit du coup les outils pour générer rapidement le tableau avec les bons paramètres... en transformant quelque peu cette fonction ça sera rapidement mis en place ;-)

Cette fonction est assez simple. On se contente d'utiliser imagettfbbox qui permet de récupérer les coordonnées de l'image qui en résulterait... ensuite il faut faire quelques "savants" calcul que je vous donne de but en blanc. je tiens à signaler que les quelques lignes permettant de récupérer la largeur et la hauteur ont été récupérées sur le site officiel de PHP, dans la page parlant de la fonction en question. Je remercie les auteur proposant librement des réponses à certains problèmes parfois épineux, nous faisant alors gagner du temps à ne pas réinventer la roue.

Voici le code de ma fonction de calcul, je ne vous commente rien de plus que ce qui est dedans, je pense que c'est suffisant :-)

<?php
# fonction permettant de faire tous les calcul en fontion d'une simple chaine
# retourne un tableau contenant toutes les valeurs calculées
function getTabCaptcha($texte="CAPTCHA"){
  $captcha = array(
                    'font'=>array(),
                    'img'=>array(),
                    'txt'=>$texte
                  );
  # Définition des paramètres de la police
  $captcha['font']['file'] = 'BorisBlackBloxx.ttf';
  $captcha['font']['size'] = 26;
  # on récupère les dimensions de l'image du code avec un angle de 0 bien sûr
  $captcha['font']['bbox'] = imagettfbbox($captcha['font']['size'], 0, $captcha['font']['file'], $captcha['txt']);
  $captcha['font']['w'] =  max($captcha['font']['bbox'][0],
                               $captcha['font']['bbox'][2],
                               $captcha['font']['bbox'][4],
                               $captcha['font']['bbox'][6])
                        -  min($captcha['font']['bbox'][0],
                               $captcha['font']['bbox'][2],
                               $captcha['font']['bbox'][4],
                               $captcha['font']['bbox'][6]);
  $captcha['font']['h'] =  max($captcha['font']['bbox'][1],
                               $captcha['font']['bbox'][3],
                               $captcha['font']['bbox'][5],
                               $captcha['font']['bbox'][7])
                         - min($captcha['font']['bbox'][1],
                               $captcha['font']['bbox'][3],
                               $captcha['font']['bbox'][5],
                               $captcha['font']['bbox'][7]);
  # on calcul les dimensions de l'image qui contiendra le texte
  $captcha['img']['marge'] = 5;
  $captcha['img']['w'] = $captcha['font']['w'] + 2 * $captcha['img']['marge'];
  $captcha['img']['h'] = $captcha['font']['h'] + 2 * $captcha['img']['marge'];
  # on prépare dans des variable les dimensions de coupe : "offset"
  $captcha['img']['x_offset'] = intval($captcha['img']['w']);
  $captcha['img']['y_offset'] = intval($captcha['img']['h']);
  # on retourne le tableau contenant tous nos calculs
  return $captcha;
}
?>

On peut observer tout de même qu'une marge de 5px est proposée tout autour du texte dans l'image, ceci permettra de ne pas avoir un texte collé au bords de l'image.

N'oubliez pas que dans le cas d'une génération d'un tableau contenant plein de chaines de caractères aléatoires pour votre captcha final, il est important de ne pas se contenter de diviser par deux les dimensions pour l'offset (variables 'x_offset' et 'y_offset')... mais bien faire des valeurs aléatoires. Si je ne le fais pas ici, c'est que dans le but de l'exemple il est important de faire une division identique pour chaque test afin de pouvoir comparer visuellement les différences et l'intérêt de l'exercice.

Vous devriez donc avoir quelque chose du genre dans le cas de la génération du tableau (je vous mâche vraiment tout le travail...) :

<?php
  # on prépare dans des variable les dimensions de coupe : "offset"
  $captcha['img']['x_offset'] = rand(0,$captcha['img']['w']-1);
  $captcha['img']['y_offset'] = rand(0,$captcha['img']['h']-1);
?>

Bon maintenant on va commencer notre réel captcha. Je vous rappel que dans le but de l'exercice on commence par appelée la fonction donnée ci-dessous, alors que dans une mise en application il faudrait appeler un tableau et récupérer les valeurs suivant un indice donné dans l'url...

Alors la première ligne est facile :

<?php
# on réalise tous les calculs en fonction du texte à placer dans l'image
$captcha = getTabCaptcha("ANTIBOTCSS");
?>

Ensuite on vérifie si c'est l'image qui est demandée, car le même fichier est utilisé pour générer l'image et fournir le code html de la page. Donc si ce n'est pas l'image qui est demandée on affiche la page html qui elle appelle les images, vous suivez ? :-)

Voici donc le canevas qu'on va ensuite remplir en deux temps, génération PHP puis page HTML :

<?php
# on test si c'est l'image qui est demandée
if(isset($_GET['image'])){
  # GENERATION DE L'IMAGE
}
# comme ce n'est pas l'image qui est demandée, on affiche la page html
else{
  # CONTENU DE LA PAGE EN HTML 
}
?>

Comme pour toute génération d'image il faut appeler la fonction imagecreatetruecolor afin d'avoir une instance avec laquelle travailler sous PHP. Puis on déclare la couleur de fond et on remplit l'image. On définit ensuite la couleur du texte.

<?php
  # on cré l'image qui contiendra le texte
  $img = imagecreatetruecolor($captcha['img']['w'], $captcha['img']['h']);
  # on définit la couleur de fond et on colorie le fond avec
  $white = imagecolorallocate($img, 0xFF, 0x99, 0x00);
  imagefill($img,0,0,$white);
  # on définit la couleur du texte
  $color_text = imagecolorallocate($img, 0x00,0x00,0x00);
?>

On dessine enfin le texte dans l'image en utilisant la marge pour qu'il ne soit pas coller au bord. Seulement il faut faire très attention, car une chose est "pénible" avec la fonction imagettftext c'est que la coordonnée donnée en y correspond ni au bord haut ni au bord bas, mais à la ligne de base de la font... et si vous ne la connaissez pas pour la taille choisie et bien c'est énervant...

... et on s'arrache les cheveux qui nous reste à avoir une apparence correcte à force d'ajustement au pixel en y allant par tâtonnement comme on sait si bien faire de façon anti-universitaire/productive[2]... :-)

Alors l'astuce que je vais utiliser et vous proposer, c'est d'utiliser tout les caractères en majuscule, avec une police qui ne fait pas descendre de lettre sous la ligne de base pour les majuscules :-)

Du coup on utilise la marge plus la hauteur calculé précédemment pour définir la position en y (je vous rappel que les ordonnées [y] vont de haut [0] en bas [hauteur de l'image])

<?php
  # on dessine le caractère dans l'image
  imagettftext($img, $captcha['font']['size'], 0, $captcha['img']['marge'], 
    $captcha['img']['marge']+$captcha['font']['h'], $color_text, 
    $captcha['font']['file'], $captcha['txt']);
?>

Puis pour faire jolie on peut rajouter un petit filet de un pixel tout autour de l'image. Sauf que je tiens à relativiser cette beauté qui pourrait donner un indice à un bot comme l'embêter, car si on divise en 4 l'image, et bien on va avoir une croix dans l'image, et ça c'est quelque chose qui peut donner un repère car c'est facilement reconnaissable... donc vous le garderez... ou pas :-)

<?php
  # Création d'un filet noir autour de l'image
  imagerectangle($img, 0, 0, $captcha['img']['w']-1, $captcha['img']['h']-1, imagecolorallocate($img, 0x00,0x00,0x00));
?>

Maintenant vient un moment important, celui du découpage de l'image. Nous allons le faire en plusieurs étapes, d'abord horizontalement, puis verticalement. Sachant qu'à chaque fois il faut passer par une ressource d'image temporaire afin d'y copier un bout de l'image, déplacer dans l'image originale le bout restant, puis recopier le premier bout contenu dans l'image temporaire à la place du bout que l'on vient de déplacer... vous suivez toujours ? vous êtes courageux :-) merci ;-)

Ce qui donne pour l'horizontal en commençant par la création de l'image temporaire :

<?php
    # on cré une image temporaire pour faire le découpage
    $im_tmp = imagecreatetruecolor($captcha['img']['w'], $captcha['img']['h']);
    # on fait les copiés-collés horizontaux
    imagecopy($im_tmp,$img,0,0,0,0,$captcha['img']['w'],$captcha['img']['y_offset']);
    imagecopy($img,$img,0,0,0,$captcha['img']['y_offset'],$captcha['img']['w'],$captcha['img']['h']-$captcha['img']['y_offset']);
    imagecopy($img,$im_tmp,0,$captcha['img']['h']-$captcha['img']['y_offset'],0,0,$captcha['img']['w'],$captcha['img']['y_offset']);
?>

Et pour le vertical avec le même principe, en oubliant pas de supprimer la ressource temporaire afin de travailler proprement[3] :

<?php
    imagecopy($im_tmp,$img,0,0,0,0,$captcha['img']['x_offset'],$captcha['img']['h']);
    imagecopy($img,$img,0,0,$captcha['img']['x_offset'],0,$captcha['img']['w']-$captcha['img']['x_offset'],$captcha['img']['h']);
    imagecopy($img,$im_tmp,$captcha['img']['w']-$captcha['img']['x_offset'],0,0,0,$captcha['img']['x_offset'],$captcha['img']['h']);
    # on supprime la ressource de l'image temporaire
    imagedestroy($im_tmp);
?>

On termine la génération de l'image en l'affichant grâce à l'envoi des bons en-têtes ainsi que de la fonction imagepng qui nous génère notre image en PNG. Sans oublier à la fin de supprimer la ressource, autant être cohérent :-)

<?php
  # On affiche l'image à l'écran
  header('Content-Type: image/png');
  header('Cache-control: no-cache, no-store');
  imagepng($img);
  # on supprime la ressource afin de quitter proprement <img src="/dotclear/themes/Natsimhan/smilies/wink.png" alt=";-)" class="smiley" />
  imagedestroy($img);
?>

Bon maintenant il reste à faire une page HTML basique comparant les différents résultat du générateur et surtout ce qui nous intéresse, l'astuce en CSS toute simple permettant d'afficher correctement l'image qui elle est mélangée...

On applique le squelette suivant, les éléments irons donc dans le body :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Un anti-bot en PHP et CSS pas à pas</title>
</head>
<body>
PLACE DU CODE AFFICHANT LES IMAGES
</body>
</html>

Le code pour afficher une image en HTML, vous le connaissez, c'est tout simple comme ceci :

<img src="index.php?image" alt="Image générée en PHP" />

Ensuite si on veut pouvoir la recomposer lorsqu'elle est coupée il faut passer par CSS et afficher l'image en arrière plan en effectuant un décalage et en plus la répéter afin de palier à la disparition d'un bout de l'image décalée. Le décalage donné va correspondre à la différence entre une dimension et son offset. Par exemple LARGEUR-X_OFFSET donne un décalage horizontal.

Donc pour un découpage horizontal et vertical on devrais avoir en CSS ceci (avec des valeurs sans importance pour l'exemple) :

<div style="display:box;width:300px;height:50px;background:#fff url(index.php?image&amp;couper=2) repeat -200px -20px;"></div>

En appliquant à notre anti-bot ça donne ce que vous voyez ci-dessous, on remplace simplement les valeurs par les variables...<br> Je suis désolé pour le côté lourd par le choix que j'ai fait ici d'utiliser echo pour chaque variable en mélangeant PHP et HTML, mais je pense que ça sera plus clair et plus facilement compréhensible à condition de pas être effrayé par le pâté de texte :-)

<div style="display:box;width:<?php echo $captcha['img']['w']; ?>px;
  height:<?php echo $captcha['img']['h']; ?>px;
  background:#fff url(index.php?image&amp;couper=2) repeat 
  -<?php echo $captcha['img']['w']-$captcha['img']['x_offset']; ?>px 
  -<?php echo $captcha['img']['h']-$captcha['img']['y_offset']; ?>px;">
</div>

Et bien cet article touche à sa fin, il fut très long et je félicite tous ceux qui ont eu le courage de le lire jusqu'au bout, et si en plus vous avez tout compris :-) N'hésitez pas à tester cet anti-bot voire même l'utiliser en l'améliorant un peu, et faites part à tous de votre participation ;-)

Je vous donne ci-dessous un lien vers l'archive où vous trouverez le fichier PHP ainsi qu'une police de caractère gratuite, il suffit d'installer ça sur un serveur Apache avec PHP et d'aller dans le répertoire, et vous verrez l'anti-bot en PHP et CSS en action.

L'archive : ANTIBOT-PHP-CSS.zip

Notes

[1] Sauf bien sûr des bots programmés pour contrer ceci, à supposer que d'autres y ont déjà pensé et mis en application au cas où...

[2] Ceux qui ont eu des profs comme les miens se reconnaitront :-)

[3] Autant prendre toujours de bonnes habitudes même si pour l'exemple ça n'a pas grande incidence...