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.

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))
        {
            unlink($destination);
        }

        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

 
 Souhaitez-vous être informé par E-Mail de la réponse à votre message ?