Générer à la volée des miniatures
avec mise en cache

La miniature (ou thumbnail en Anglais) est une version d'une image dont les dimensions sont réduites par rapport à l'originale. En principe on clique sur la miniature pour présenter la version originale. On peut utiliser les miniatures pour créer une galerie d'images, afin d'en visualiser un plus grand nombre à la fois. Les miniatures étant souvent de poids plus faibles, elles sont plus rapides à télécharger, améliorant ainsi la navigation.

Les miniatures peuvent être créées manuellement, éventuellement par lots, en utilisant un logiciel graphique, ou bien générées à la volée, auquel cas elles peuvent être mises en cache. La génération à la volée me semble intéressante pour les raisons suivantes :

  • Moins de travail pour l'utilisateur : de nombreuses variations peuvent être créées sans son intervention.
  • La mise à jour est beaucoup plus simple puisqu'il suffit de modifier le fichier original pour que les versions soient mises à jour.
  • Puisque les miniatures sont générées à la volée on peut réduire l'espace disque qu'elles occupent.

L'article que je vous propose décrit un système de génération à la volée de miniatures avec mise en cache. Nous en verrons les concepts et l'implémentation. Le code présenté se base sur des fonctionnalités de la classe WdImage qui ont été introduites dans les articles Une classe et sept méthodes pour créer des miniatures et Dessiner un damier à la Photoshop.

Une démonstration de ces fonctionnalités, reprises et améliorées par le module « thumbnailer » est disponible. Je vous invite à consulter l'article « Des miniatures à la demande grâce au module « thumbnailer » qui y est associé.

Vous pouvez obtenir la dernière version de la classe en téléchargeant le framework WdCore.

Déroulement

L'obtention d'une miniature se fait par l'entremise d'un script PHP qui prendra soin de rediriger la requête HTTP vers le fichier de la miniature, créant le fichier au passage s'il n'existe pas.

Les paramètres pour créer la miniature sont passés par la chaine de requête de l'URL. Ces mêmes paramètres sont associés aux informations du fichier d'origine afin de créer une clé unique qui servira à identifier la miniature. Cette clé est utilisée pour nommer le fichier de la miniature lors de la mise en cache.

Composition de l'URL

L'URL se compose en fonction de l'emplacement du script sur l'hébergement, de l'emplacement de l'image originale sur l'hébergement, des dimensions de la miniatures et des paramètres optionnels tels que la méthode de redimensionnement, la couleur de remplissage, ou les couleurs de remplissage si l'on souhaite obtenir une grille (bien connue des utilisateurs de Photoshop).

Le chemin de l'image d'origine est relatif à la racine de notre hébergement. Ainsi, si notre script se trouve à la racine de notre hébergement on pourrait imaginer les URL suivantes :

  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60
  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60&format=jpeg&quality=70
  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60&method=scale-max
  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60&method=scale-max&background=red
  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60&method=scale-max&background=#F00,yellow
  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60&method=scale-max&background=#F00,#00FFFF,medium
  • /thumbnailer.php?f=/path_to_original/original.jpeg&w=60&h=60&method=scale-max&background=#F00,#00FFFF,16

Description des paramètres

  • f : il s'agit de l'URL de l'image originale, relative au dossier racine de notre hébergement.
  • w et h : sont la largeur et la hauteur de la miniature.
  • format : le format d'image de la miniature. Vous avez le choix entre jpeg, png et gif.
  • quality : permet de définir la qualité de compression pour le format JPEG, allant de 0 à 100 pour la qualité maximale.
  • method : permet de définir la méthode de redimensionnement à utiliser. Il s'agit d'une des sept méthodes décrites dans l'article Une classe et sept méthodes pour créer des miniatures.
  • background : permet de définir la couleur de remplissage ou, dans sa version étendue, les couleurs et la dimension de la grille à utiliser avec la méthode WdImage::drawGrid() que j'avais présenté dans l'article Dessiner un damier à la Photoshop.

Les paramètres par défaut sont : format=jpeg, quality=70, method=scale-min et background=white.

Conseil sur la construction des URL

Attention à la construction des URL ! Le développeur peu regardant écrira l'URL à la main comme un sauvage, utilisera la couleur #F00 et s'étonnera que « ça ne marche pas ». Effectivement, si le caractère # n'est pas échappé il sera utilisé comme une ancre ! Le développeur consciencieux (parce qu'il n'aime pas perdre son temps à chercher « pourquoi ça ne marche pas ») utilisera la fonction http_build_query() pour créer son URL :

/thumbnailer.php?f=%2Fpath_to_original%2Foriginal.jpeg&w=60&h=60&background=%23F00

Le script thumbnailer.php

Le script thumbnailer.php utilise la classe WdThumbnailer dérivée d'un des modules de mon CMS WdPublisher. Il vous suffira de quelques lignes pour créer le script de génération :

<?php

#
# thumbnailer.php
#

define('WDCORE_ROOT'$_SERVER['DOCUMENT_ROOT'] . '/path_to_wdcore/');

WdThumbnailer::operation_get($_GET$_GET['f']$_GET['w']$_GET['h']);

La classe WdThumbnailer utilise la classe WdImage du framework WdCore. Vous devez définir la constante WDCORE_ROOT pour permettre le chargement de la classe WdImage.

La classe WdThumbnailer

<?php

class WdThumbnailer
{
    const METHOD = 'method';
    const QUALITY = 'quality';
    const FORMAT = 'format';
    const BACKGROUND = 'background';

    const REPOSITORY_URL = '/repository/thumbnailer/';

    static $background;

    static public function get($file$w$h, array $options=array())
    {
        #
        # Is the repository ready ?
        #

        if (!is_dir($_SERVER['DOCUMENT_ROOT'] . self::REPOSITORY_URL))
        {
            WdDebug::trigger('The repository %url does not exists !'array('%url' => self::REPOSITORY_URL));

            return false;
        }

        #
        # We check if the file exists
        #

        $path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $file;

        if (!is_file($path))
        {
            return false;
        }

        #
        # We merge the filtered user options with the default ones
        #

        require_once WDCORE_ROOT . 'wdimage.php';

        $options = self::filterOptions($options) + array
        (
            self::BACKGROUND => 'white',
            self::FORMAT => 'jpeg',
            self::METHOD => WdImage::RESIZE_SCALE_MIN,
            self::QUALITY => 70 
        );
        
        ksort($options);

        #
        # We create a unique key for the thumbnail, using the image information
        # and the options used to create the thumbnail.
        #

        $stats = stat($path);

        $key = implode('|'array($file$w . 'x' . $h$stats['mtime']$stats['size'])) . json_encode($options);
        $key = sha1($key);

        #
        # If the file doesn't exists we create it
        #
        
        $format = $options['format'];
        
        $file = self::REPOSITORY_URL . $key . '.' . $format;

        $destination = $_SERVER['DOCUMENT_ROOT'] . $file;
        
        if (!is_file($destination))
        {
            self::$background = self::decodeBackground($options[self::BACKGROUND]);
                
            $callback = array(__CLASS__, 'fill_callback');

            $image = WdImage::load($path$info);
            $image = WdImage::resize($image$w$h$options['method']$callback);
            
            #
            # choose thumbnail file format
            #

            static $functions = array
            (
                'gif' => 'imagegif',
                'jpeg' => 'imagejpeg',
                'png' => 'imagepng'
            );

            $function = $functions[$format];
            $args = array($image$destination);

            if ($format == 'jpeg')
            {
                #
                # add quality option for the 'jpeg' format
                #

                $args[] = $options['quality'];
            }

            $rc = call_user_func_array($function$args);

            imageDestroy($image);

            if (!$rc)
            {
                return false;
            }
        }

        return $file;
    }

    static public function operation_get($params$file$w$h)
    {
        $location = self::get($file$w$h$params);

        if ($location)
        {
            header('Location: ' . $locationtrue307);
        }
        else
        {
            header('HTTP/1.0 404 Not Found');
        }

        exit;
    }

    static private function filterOptions($options)
    {
        $options = array_intersect_key
        (
            $options, array
            (
                self::FORMAT => true,
                self::METHOD => true,
                self::QUALITY => true,
                self::BACKGROUND => true
            )
        );

        return $options;
    }
    
    static private function decodeBackground($background)
    {
        $parts = explode(','$background);

        $parts[0] = WdImage::decodeColor($parts[0]);
        
        if (count($parts) == 1)
        {
            return array($parts[0], null, 0);
        }
        
        $parts[1] = WdImage::decodeColor($parts[1]);
        
        return $parts;
    }

    static public function fill_callback($image$w$h)
    {
        #
        # We create WdImage::drawGrid() arguments from the dimensions of the image
        # and the values passed using the BACKGROUND parameter.
        #
        
        $args = (array) self::$background;
        
        array_unshift($args$image00$w - 1$h - 1);
        
        call_user_func_array(array('WdImage''drawGrid')$args);
    }
}

N'oubliez pas de modifier la constante REPOSITORY_URL de la classe pour l'adapter à vos besoins. Il convient de fournir l'URL d'un dossier dont les droits d'écritures sont publics, sinon les fichiers des miniatures ne pourront être générés.

Conclusion

Comme vous pouvez le voir, l'implémentation est assez simple, la gestion du cache également. De solides bases pour de futures améliorations comme la prise en charge de filtres ou de superpositions de couches ou de logos…

Je vous présenterai dans un prochain billet une fonction permettant de maintenir la taille du cache dans des proportions raisonnables.

En attendant amusez-vous bien avec ce générateur, j'espère qu'il vous permettra de faire des merveilles.

Laisser un commentaire

8 commentaires

Savageman
Savageman

Sympa ! Vraiment sympa. J'y vois un seul inconvénient : redimensionner des images, ça consomme du CPU. ll devient possible de « surcharger » le serveur avec peu de requêtes. Y aurait-t-il une suggestion pour parer à ce problème ?

Lucas
Lucas

Oui, en écrivant une valeur temporaire dans un fichier lors de la génération, et consulter ce fichier à chaque exécution pour savoir si une exécution est en cours ?

fabrice
fabrice

Salut, j'ai un petite difficulté à comprendre cette ligne dans ton code :

<?php

if (is_file($destination))
{
    unlink($destination);
}

unlink ne détruit-il pas le fichier miniature s'il existe au lieu de renvoyer directement $file ? Je te remercie par avance d'éclairer un peu mon cerveau.

Olivier
Olivier

Merci Fabrice, j'avais oublié de supprimer ces lignes de débogage. Pour me faire pardonner, je mettrai en ligne pour le weekend une démonstration utilisant le module « Thumbnailer » de mon framework. Tu verras que la gestion du cache et le transfert de fichier y sont bien plus aboutis.

Alors à bientôt !

fabrice
fabrice

Merci pour ta réponse. J'ai hâte de lire cette démonstration ! J'ai fait un petit test avec le module webdevelopper sur mozilla pour tester la redirection http renvoyée par operation_get(); et j'ai constaté que pour 1 photo, 2 à 3 requetes http sont nécessaires au navigateur pour loader l'image. J'ai lu quelque part que c'est un comportement fréquent des redirections. Je ne sais pas si tu as constaté ça également ? Du coup, j'ai préféré utiliser directement ta méthode get() de cette manière : img src="' . WdThumbnailer::get($image, $w, $h) . '" /> Si tu peux me dire ce que tu en penses …

fabrice
fabrice

Quitte à être un peu envahissant j'en rajoute une couche ;) : j'ai été confronté au pb de l'utilisation de la superglobale $SERVER['DOCUMENT_ROOT'] dans le cas d'un vhost sous apache pointant vers un répertoire utilisateur (mod_userdir) car le document root avec mod_userdir n'est pas celui attendu (ou alors j'ai loupé un truc dans la config mais bon..). Du coup j'utilise une constante du genre MON_DOCUMENTROOT = dirname(__ FILE__ ); que j'initialise à la racine du site, en attendant de passer à php 5.3 qui introduit __DIR__ . Ça me semble moins contraignant.

Olivier
Olivier

Salut Fabrice,

Comme promis, j'ai crée une petite démonstration du module « thumbnailer ». Tu peux jouer avec sur mon site, ou la télécharger pour t'amuser chez toi et surtout, parce que je pense que c'est ce qui t'intéresse, décortiquer le code source.

Le module se trouve dans le dossier « /protected/modules/ ».

Tu verras que les méthodes de mise en cache sont bien plus sophistiquées que celles présentées dans cet article. Tu me diras ce que tu en pense.

En ce qui concerne les DOCUMENT_ROOT qui pointent pas au bon endroit, en général j'appelle l'hébergeur pour qu'il corrige ça, sinon je change sa valeur moi-même dans le tableau $_SERVER, comme une brute, mais bon, c'est quand même une erreur de configuration, alors il vaut mieux qu'elle soit corrigée que contournée.

Fabrice
Fabrice

Salut Olivier, Merci pour ta publication du Week-End, je lis ça dès que j'ai un peu de temps. Merci de partager tout ça. A bientôt.