🖼️ Tutoriel Script PHP

Traitement d'Image à la Volée

← Retour à l'accueil

📖 Introduction

Ce tutoriel présente un script PHP complet pour le traitement d'images à la volée. Le script permet d'optimiser, redimensionner, recadrer et convertir des images dynamiquement via des paramètres URL.

💡 Fonctionnalités principales :
  • Optimisation automatique (réduction du poids sans perte visible de qualité)
  • Redimensionnement intelligent avec conservation du ratio
  • Recadrage (crop) avec positionnement configurable
  • Conversion de format (WebP, JPEG, PNG)
  • Mise en cache pour de meilleures performances
  • Gestion des filtres (flou, netteté, niveaux de gris...)
  • Rotation et retournement

⚙️ Installation et Prérequis

Prérequis système

  • PHP 7.4 ou supérieur
  • Extension GD (recommandée) ou Imagick
  • Support WebP compilé dans GD/Imagick

Vérifier les extensions

php -m | grep -E "gd|imagick"

Structure de fichiers

project/
├── image.php           # Script principal de traitement
├── .htaccess          # Configuration Apache (optionnel)
├── cache/             # Dossier de cache (créé automatiquement)
└── uploads/           # Dossier des images sources

📝 Le Script Principal (image.php)

Code complet

<?php
/**
 * Script de traitement d'image à la volée
 *
 * Paramètres URL :
 * - src      : Chemin de l'image source (obligatoire)
 * - w        : Largeur en pixels
 * - h        : Hauteur en pixels
 * - fit      : Mode de redimensionnement (contain, cover, fill, scale-down)
 * - crop     : Recadrage (top, center, bottom, left, right)
 * - quality  : Qualité JPEG/WebP (0-100, défaut: 85)
 * - format   : Format de sortie (webp, jpg, png, auto)
 * - optimize : Optimisation automatique (1 ou 0, défaut: 1)
 * - blur     : Flou gaussien (0-100)
 * - grayscale: Niveaux de gris (1 ou 0)
 * - rotate   : Rotation en degrés (0-360)
 * - flip     : Retournement (h, v, both)
 * - sharpen  : Netteté (1 ou 0)
 * - bg       : Couleur de fond (hex sans #, ex: ffffff)
 */

class ImageProcessor {
    private $sourcePath;
    private $sourceImage;
    private $sourceWidth;
    private $sourceHeight;
    private $sourceMime;
    private $cacheDir = 'cache';
    private $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];

    // Configuration par défaut
    private $config = [
        'quality' => 85,
        'optimize' => true,
        'format' => 'auto',
        'fit' => 'contain',
        'crop' => 'center',
        'maxWidth' => 3000,
        'maxHeight' => 3000,
        'cacheEnabled' => true,
        'sharpen' => false,
        'background' => 'ffffff'
    ];

    public function __construct($sourcePath, $options = []) {
        $this->sourcePath = $sourcePath;
        $this->config = array_merge($this->config, $options);

        // Créer le dossier de cache
        if ($this->config['cacheEnabled'] && !is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
    }

    /**
     * Traite l'image et retourne le résultat
     */
    public function process() {
        // Vérifier si l'image existe
        if (!file_exists($this->sourcePath)) {
            $this->error(404, "Image non trouvée");
        }

        // Vérifier le cache
        $cacheFile = $this->getCacheFilename();
        if ($this->config['cacheEnabled'] && file_exists($cacheFile)) {
            $this->outputImage($cacheFile);
            return;
        }

        // Charger l'image source
        $this->loadSourceImage();

        // Créer l'image de destination
        $destImage = $this->createDestinationImage();

        // Appliquer les filtres
        $this->applyFilters($destImage);

        // Sauvegarder et afficher
        $this->saveAndOutput($destImage, $cacheFile);

        // Libérer la mémoire
        imagedestroy($this->sourceImage);
        imagedestroy($destImage);
    }

    /**
     * Charge l'image source
     */
    private function loadSourceImage() {
        $imageInfo = getimagesize($this->sourcePath);
        if (!$imageInfo) {
            $this->error(400, "Fichier image invalide");
        }

        $this->sourceMime = $imageInfo['mime'];
        $this->sourceWidth = $imageInfo[0];
        $this->sourceHeight = $imageInfo[1];

        if (!in_array($this->sourceMime, $this->allowedMimes)) {
            $this->error(400, "Type de fichier non supporté");
        }

        // Créer la ressource image selon le type
        switch ($this->sourceMime) {
            case 'image/jpeg':
                $this->sourceImage = imagecreatefromjpeg($this->sourcePath);
                break;
            case 'image/png':
                $this->sourceImage = imagecreatefrompng($this->sourcePath);
                break;
            case 'image/gif':
                $this->sourceImage = imagecreatefromgif($this->sourcePath);
                break;
            case 'image/webp':
                $this->sourceImage = imagecreatefromwebp($this->sourcePath);
                break;
        }

        if (!$this->sourceImage) {
            $this->error(500, "Impossible de charger l'image");
        }
    }

    /**
     * Crée l'image de destination avec les dimensions appropriées
     */
    private function createDestinationImage() {
        $dimensions = $this->calculateDimensions();

        // Créer l'image de destination
        $destImage = imagecreatetruecolor($dimensions['destWidth'], $dimensions['destHeight']);

        // Gérer la transparence pour PNG et GIF
        $this->handleTransparency($destImage);

        // Copier et redimensionner l'image
        imagecopyresampled(
            $destImage,
            $this->sourceImage,
            $dimensions['destX'],
            $dimensions['destY'],
            $dimensions['srcX'],
            $dimensions['srcY'],
            $dimensions['copyWidth'],
            $dimensions['copyHeight'],
            $dimensions['srcWidth'],
            $dimensions['srcHeight']
        );

        return $destImage;
    }

    /**
     * Calcule les dimensions de redimensionnement
     */
    private function calculateDimensions() {
        $srcWidth = $this->sourceWidth;
        $srcHeight = $this->sourceHeight;

        // Récupérer les dimensions demandées
        $targetWidth = isset($this->config['width']) ? min($this->config['width'], $this->config['maxWidth']) : null;
        $targetHeight = isset($this->config['height']) ? min($this->config['height'], $this->config['maxHeight']) : null;

        // Si aucune dimension n'est spécifiée, garder l'originale
        if (!$targetWidth && !$targetHeight) {
            $targetWidth = $srcWidth;
            $targetHeight = $srcHeight;
        }

        // Calculer le ratio
        $srcRatio = $srcWidth / $srcHeight;

        // Déterminer les dimensions selon le mode
        switch ($this->config['fit']) {
            case 'cover':
                // L'image remplit complètement la zone (peut être croppée)
                $dimensions = $this->calculateCoverDimensions($srcWidth, $srcHeight, $targetWidth, $targetHeight, $srcRatio);
                break;

            case 'fill':
                // L'image est étirée pour remplir exactement la zone
                $dimensions = [
                    'destWidth' => $targetWidth ?: $srcWidth,
                    'destHeight' => $targetHeight ?: $srcHeight,
                    'destX' => 0,
                    'destY' => 0,
                    'srcX' => 0,
                    'srcY' => 0,
                    'srcWidth' => $srcWidth,
                    'srcHeight' => $srcHeight,
                    'copyWidth' => $targetWidth ?: $srcWidth,
                    'copyHeight' => $targetHeight ?: $srcHeight
                ];
                break;

            case 'scale-down':
                // Comme contain mais ne pas agrandir
                $contain = $this->calculateContainDimensions($srcWidth, $srcHeight, $targetWidth, $targetHeight, $srcRatio);
                if ($contain['copyWidth'] > $srcWidth || $contain['copyHeight'] > $srcHeight) {
                    $contain['destWidth'] = $srcWidth;
                    $contain['destHeight'] = $srcHeight;
                    $contain['copyWidth'] = $srcWidth;
                    $contain['copyHeight'] = $srcHeight;
                }
                $dimensions = $contain;
                break;

            case 'contain':
            default:
                // L'image entière est visible (peut avoir des marges)
                $dimensions = $this->calculateContainDimensions($srcWidth, $srcHeight, $targetWidth, $targetHeight, $srcRatio);
                break;
        }

        return $dimensions;
    }

    /**
     * Calcule les dimensions pour le mode 'contain'
     */
    private function calculateContainDimensions($srcWidth, $srcHeight, $targetWidth, $targetHeight, $srcRatio) {
        if ($targetWidth && $targetHeight) {
            $targetRatio = $targetWidth / $targetHeight;

            if ($srcRatio > $targetRatio) {
                // Image plus large que la cible
                $destWidth = $targetWidth;
                $destHeight = round($targetWidth / $srcRatio);
            } else {
                // Image plus haute que la cible
                $destWidth = round($targetHeight * $srcRatio);
                $destHeight = $targetHeight;
            }
        } elseif ($targetWidth) {
            $destWidth = $targetWidth;
            $destHeight = round($targetWidth / $srcRatio);
        } else {
            $destHeight = $targetHeight;
            $destWidth = round($targetHeight * $srcRatio);
        }

        return [
            'destWidth' => $destWidth,
            'destHeight' => $destHeight,
            'destX' => 0,
            'destY' => 0,
            'srcX' => 0,
            'srcY' => 0,
            'srcWidth' => $srcWidth,
            'srcHeight' => $srcHeight,
            'copyWidth' => $destWidth,
            'copyHeight' => $destHeight
        ];
    }

    /**
     * Calcule les dimensions pour le mode 'cover'
     */
    private function calculateCoverDimensions($srcWidth, $srcHeight, $targetWidth, $targetHeight, $srcRatio) {
        if (!$targetWidth) $targetWidth = $targetHeight * $srcRatio;
        if (!$targetHeight) $targetHeight = $targetWidth / $srcRatio;

        $targetRatio = $targetWidth / $targetHeight;

        if ($srcRatio > $targetRatio) {
            // Image source plus large, on crop les côtés
            $srcCropWidth = round($srcHeight * $targetRatio);
            $srcCropHeight = $srcHeight;
        } else {
            // Image source plus haute, on crop le haut/bas
            $srcCropWidth = $srcWidth;
            $srcCropHeight = round($srcWidth / $targetRatio);
        }

        // Calculer la position du crop
        $cropPosition = $this->calculateCropPosition($srcWidth, $srcHeight, $srcCropWidth, $srcCropHeight);

        return [
            'destWidth' => $targetWidth,
            'destHeight' => $targetHeight,
            'destX' => 0,
            'destY' => 0,
            'srcX' => $cropPosition['x'],
            'srcY' => $cropPosition['y'],
            'srcWidth' => $srcCropWidth,
            'srcHeight' => $srcCropHeight,
            'copyWidth' => $targetWidth,
            'copyHeight' => $targetHeight
        ];
    }

    /**
     * Calcule la position de recadrage
     */
    private function calculateCropPosition($srcWidth, $srcHeight, $cropWidth, $cropHeight) {
        $x = 0;
        $y = 0;

        switch ($this->config['crop']) {
            case 'top':
                $x = ($srcWidth - $cropWidth) / 2;
                $y = 0;
                break;
            case 'bottom':
                $x = ($srcWidth - $cropWidth) / 2;
                $y = $srcHeight - $cropHeight;
                break;
            case 'left':
                $x = 0;
                $y = ($srcHeight - $cropHeight) / 2;
                break;
            case 'right':
                $x = $srcWidth - $cropWidth;
                $y = ($srcHeight - $cropHeight) / 2;
                break;
            case 'center':
            default:
                $x = ($srcWidth - $cropWidth) / 2;
                $y = ($srcHeight - $cropHeight) / 2;
                break;
        }

        return ['x' => round($x), 'y' => round($y)];
    }

    /**
     * Gère la transparence pour PNG et GIF
     */
    private function handleTransparency($image) {
        $format = $this->getOutputFormat();

        if ($format === 'png' || ($format === 'auto' && $this->sourceMime === 'image/png')) {
            imagealphablending($image, false);
            imagesavealpha($image, true);
            $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127);
            imagefilledrectangle($image, 0, 0, imagesx($image), imagesy($image), $transparent);
        } else {
            // Pour JPEG et WebP, utiliser un fond de couleur
            $bgColor = $this->hexToRgb($this->config['background']);
            $background = imagecolorallocate($image, $bgColor[0], $bgColor[1], $bgColor[2]);
            imagefilledrectangle($image, 0, 0, imagesx($image), imagesy($image), $background);
        }
    }

    /**
     * Applique les filtres à l'image
     */
    private function applyFilters($image) {
        // Rotation
        if (isset($this->config['rotate']) && $this->config['rotate'] > 0) {
            $image = imagerotate($image, -$this->config['rotate'], 0);
        }

        // Retournement
        if (isset($this->config['flip'])) {
            switch ($this->config['flip']) {
                case 'h':
                    imageflip($image, IMG_FLIP_HORIZONTAL);
                    break;
                case 'v':
                    imageflip($image, IMG_FLIP_VERTICAL);
                    break;
                case 'both':
                    imageflip($image, IMG_FLIP_BOTH);
                    break;
            }
        }

        // Niveaux de gris
        if (isset($this->config['grayscale']) && $this->config['grayscale']) {
            imagefilter($image, IMG_FILTER_GRAYSCALE);
        }

        // Flou
        if (isset($this->config['blur']) && $this->config['blur'] > 0) {
            $blur = min($this->config['blur'], 100);
            for ($i = 0; $i < $blur; $i++) {
                imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR);
            }
        }

        // Netteté
        if ($this->config['sharpen']) {
            $sharpenMatrix = [
                [-1, -1, -1],
                [-1, 16, -1],
                [-1, -1, -1]
            ];
            imageconvolution($image, $sharpenMatrix, 8, 0);
        }
    }

    /**
     * Sauvegarde et affiche l'image
     */
    private function saveAndOutput($image, $cacheFile) {
        $format = $this->getOutputFormat();
        $quality = $this->getQuality();

        // Déterminer le type MIME
        switch ($format) {
            case 'webp':
                $mimeType = 'image/webp';
                break;
            case 'png':
                $mimeType = 'image/png';
                break;
            case 'jpg':
            default:
                $mimeType = 'image/jpeg';
                break;
        }

        // Headers
        header('Content-Type: ' . $mimeType);
        header('Cache-Control: max-age=31536000, public');

        // Sauvegarder dans le cache et afficher
        ob_start();

        switch ($format) {
            case 'webp':
                imagewebp($image, null, $quality);
                break;
            case 'png':
                // PNG qualité est inversée (0 = meilleur, 9 = pire)
                $pngQuality = round((100 - $quality) / 11.111);
                imagepng($image, null, $pngQuality);
                break;
            case 'jpg':
            default:
                imagejpeg($image, null, $quality);
                break;
        }

        $imageData = ob_get_contents();
        ob_end_clean();

        // Sauvegarder dans le cache
        if ($this->config['cacheEnabled']) {
            file_put_contents($cacheFile, $imageData);
        }

        // Afficher
        echo $imageData;
    }

    /**
     * Détermine le format de sortie
     */
    private function getOutputFormat() {
        if ($this->config['format'] !== 'auto') {
            return $this->config['format'];
        }

        // Auto : utiliser WebP si supporté, sinon le format source
        if (function_exists('imagewebp')) {
            return 'webp';
        }

        switch ($this->sourceMime) {
            case 'image/png':
                return 'png';
            case 'image/jpeg':
            default:
                return 'jpg';
        }
    }

    /**
     * Obtient la qualité d'optimisation
     */
    private function getQuality() {
        $quality = $this->config['quality'];

        // Optimisation automatique
        if ($this->config['optimize']) {
            $format = $this->getOutputFormat();

            if ($format === 'webp') {
                // WebP peut utiliser une qualité plus basse avec de meilleurs résultats
                $quality = min($quality, 85);
            } elseif ($format === 'jpg') {
                // JPEG optimisé
                $quality = min($quality, 90);
            }
        }

        return max(0, min(100, $quality));
    }

    /**
     * Génère le nom du fichier de cache
     */
    private function getCacheFilename() {
        $params = [
            $this->sourcePath,
            filemtime($this->sourcePath),
            $this->config
        ];

        $hash = md5(serialize($params));
        $format = $this->getOutputFormat();
        $extension = $format === 'jpg' ? 'jpg' : $format;

        return $this->cacheDir . '/' . $hash . '.' . $extension;
    }

    /**
     * Affiche le cache
     */
    private function outputImage($file) {
        $mimeType = mime_content_type($file);
        header('Content-Type: ' . $mimeType);
        header('Cache-Control: max-age=31536000, public');
        header('X-Cache: HIT');
        readfile($file);
        exit;
    }

    /**
     * Convertit hex en RGB
     */
    private function hexToRgb($hex) {
        $hex = ltrim($hex, '#');
        return [
            hexdec(substr($hex, 0, 2)),
            hexdec(substr($hex, 2, 2)),
            hexdec(substr($hex, 4, 2))
        ];
    }

    /**
     * Affiche une erreur
     */
    private function error($code, $message) {
        http_response_code($code);
        header('Content-Type: application/json');
        echo json_encode(['error' => $message]);
        exit;
    }
}

// Script principal
if (!isset($_GET['src']) || empty($_GET['src'])) {
    http_response_code(400);
    die(json_encode(['error' => 'Paramètre src manquant']));
}

// Récupérer les options
$options = [
    'width' => isset($_GET['w']) ? (int)$_GET['w'] : null,
    'height' => isset($_GET['h']) ? (int)$_GET['h'] : null,
    'quality' => isset($_GET['quality']) ? (int)$_GET['quality'] : 85,
    'format' => isset($_GET['format']) ? $_GET['format'] : 'auto',
    'fit' => isset($_GET['fit']) ? $_GET['fit'] : 'contain',
    'crop' => isset($_GET['crop']) ? $_GET['crop'] : 'center',
    'optimize' => !isset($_GET['optimize']) || $_GET['optimize'] !== '0',
    'blur' => isset($_GET['blur']) ? (int)$_GET['blur'] : 0,
    'grayscale' => isset($_GET['grayscale']) && $_GET['grayscale'] === '1',
    'rotate' => isset($_GET['rotate']) ? (int)$_GET['rotate'] : 0,
    'flip' => isset($_GET['flip']) ? $_GET['flip'] : null,
    'sharpen' => isset($_GET['sharpen']) && $_GET['sharpen'] === '1',
    'background' => isset($_GET['bg']) ? $_GET['bg'] : 'ffffff',
    'cacheEnabled' => !isset($_GET['nocache'])
];

// Nettoyer le chemin source
$sourcePath = str_replace('..', '', $_GET['src']);

// Traiter l'image
$processor = new ImageProcessor($sourcePath, $options);
$processor->process();
?>
⚠️ Sécurité : Ce script inclut des protections de base, mais pour un environnement de production, ajoutez :
  • Validation stricte des chemins (whitelist de dossiers)
  • Limitation du taux de requêtes
  • Authentification si nécessaire
  • Validation des dimensions maximales

🚀 Utilisation

Exemples d'URL

1. Redimensionnement simple

<!-- Largeur 800px, hauteur automatique -->
<img src="image.php?src=uploads/photo.jpg&w=800">

<!-- Hauteur 600px, largeur automatique -->
<img src="image.php?src=uploads/photo.jpg&h=600">

<!-- Dimensions fixes avec mode contain (défaut) -->
<img src="image.php?src=uploads/photo.jpg&w=800&h=600">

2. Mode Cover avec Crop

<!-- Remplit 800x600 en croppant au centre -->
<img src="image.php?src=uploads/photo.jpg&w=800&h=600&fit=cover">

<!-- Crop en haut -->
<img src="image.php?src=uploads/photo.jpg&w=800&h=600&fit=cover&crop=top">

<!-- Crop en bas -->
<img src="image.php?src=uploads/photo.jpg&w=800&h=600&fit=cover&crop=bottom">

3. Conversion de format

<!-- Convertir en WebP -->
<img src="image.php?src=uploads/photo.jpg&format=webp">

<!-- Convertir en JPEG avec qualité spécifique -->
<img src="image.php?src=uploads/photo.png&format=jpg&quality=90">

<!-- Auto (WebP si supporté) -->
<img src="image.php?src=uploads/photo.jpg&format=auto">

4. Optimisation

<!-- Optimisation automatique activée (défaut) -->
<img src="image.php?src=uploads/photo.jpg&w=800">

<!-- Désactiver l'optimisation -->
<img src="image.php?src=uploads/photo.jpg&w=800&optimize=0">

<!-- Optimisation + qualité personnalisée -->
<img src="image.php?src=uploads/photo.jpg&w=800&quality=75">

5. Filtres

<!-- Niveaux de gris -->
<img src="image.php?src=uploads/photo.jpg&grayscale=1">

<!-- Flou -->
<img src="image.php?src=uploads/photo.jpg&blur=10">

<!-- Netteté -->
<img src="image.php?src=uploads/photo.jpg&sharpen=1">

<!-- Rotation -->
<img src="image.php?src=uploads/photo.jpg&rotate=90">

<!-- Retournement horizontal -->
<img src="image.php?src=uploads/photo.jpg&flip=h">

6. Combinaisons complexes

<!-- Redimensionner + convertir WebP + optimiser -->
<img src="image.php?src=uploads/photo.jpg&w=1200&h=800&fit=cover&crop=center&format=webp&quality=80">

<!-- Thumbnail carré en niveaux de gris -->
<img src="image.php?src=uploads/photo.jpg&w=200&h=200&fit=cover&grayscale=1">

<!-- Background personnalisé pour PNG vers JPEG -->
<img src="image.php?src=uploads/logo.png&format=jpg&bg=ff0000">

📋 Référence des Paramètres

Paramètre Type Valeurs Description
src string chemin Obligatoire. Chemin vers l'image source
w int 1-3000 Largeur en pixels
h int 1-3000 Hauteur en pixels
fit string contain, cover, fill, scale-down Mode de redimensionnement
crop string center, top, bottom, left, right Position du recadrage (avec fit=cover)
quality int 0-100 Qualité JPEG/WebP (défaut: 85)
format string auto, webp, jpg, png Format de sortie (défaut: auto)
optimize bool 0, 1 Optimisation automatique (défaut: 1)
blur int 0-100 Intensité du flou gaussien
grayscale bool 0, 1 Conversion en niveaux de gris
sharpen bool 0, 1 Appliquer un filtre de netteté
rotate int 0-360 Rotation en degrés (sens horaire)
flip string h, v, both Retournement (h=horizontal, v=vertical)
bg string hex (sans #) Couleur de fond (ex: ffffff, ff0000)
nocache bool présent ou non Désactiver le cache pour cette requête

Modes de redimensionnement (fit)

contain (défaut)

L'image entière est visible, peut avoir des marges. Conserve le ratio.

image.php?src=photo.jpg&w=800&h=600&fit=contain

cover

L'image remplit complètement la zone, peut être croppée. Conserve le ratio.

image.php?src=photo.jpg&w=800&h=600&fit=cover

fill

L'image est étirée pour remplir exactement la zone. Ne conserve pas le ratio.

image.php?src=photo.jpg&w=800&h=600&fit=fill

scale-down

Comme contain mais l'image n'est jamais agrandie.

image.php?src=photo.jpg&w=800&h=600&fit=scale-down

🔧 Configuration Apache (.htaccess)

Pour des URLs plus propres, vous pouvez configurer un .htaccess :

# .htaccess
<IfModule mod_rewrite.c>
    RewriteEngine On

    # Réécriture pour les images
    # /img/uploads/photo.jpg/800x600.webp
    RewriteRule ^img/(.+)/(\d+)x(\d+)\.(webp|jpg|png)$ image.php?src=$1&w=$2&h=$3&format=$4 [L,QSA]

    # /img/uploads/photo.jpg/800.webp
    RewriteRule ^img/(.+)/(\d+)\.(webp|jpg|png)$ image.php?src=$1&w=$2&format=$3 [L,QSA]

    # Cache des images générées
    <FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
        Header set Cache-Control "max-age=31536000, public"
    </FilesMatch>
</IfModule>

# Compression
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>

Utilisation avec .htaccess :

<!-- URL classique -->
<img src="image.php?src=uploads/photo.jpg&w=800&h=600&format=webp">

<!-- URL réécrite -->
<img src="img/uploads/photo.jpg/800x600.webp">

<!-- Largeur seulement -->
<img src="img/uploads/photo.jpg/800.webp">

⚡ Optimisation des Performances

1. Système de cache

Le script intègre un système de cache automatique :

  • Les images traitées sont sauvegardées dans le dossier cache/
  • Le nom du cache est un hash MD5 des paramètres + timestamp du fichier source
  • Si l'image source change, le cache est invalidé automatiquement

2. Nettoyage du cache

# Supprimer les fichiers de cache de plus de 30 jours
find cache/ -type f -mtime +30 -delete

# Vider tout le cache
rm -rf cache/*

3. Script de nettoyage automatique (cron)

<?php
// cleanup-cache.php
$cacheDir = __DIR__ . '/cache';
$maxAge = 30 * 24 * 60 * 60; // 30 jours

$files = glob($cacheDir . '/*');
$now = time();
$deleted = 0;

foreach ($files as $file) {
    if (is_file($file)) {
        if ($now - filemtime($file) >= $maxAge) {
            unlink($file);
            $deleted++;
        }
    }
}

echo "Nettoyage terminé : $deleted fichiers supprimés\n";
?>
# Ajouter au crontab (exécuter tous les jours à 3h du matin)
0 3 * * * /usr/bin/php /path/to/cleanup-cache.php

4. Optimisation de la qualité

💡 Recommandations de qualité :
  • WebP : 75-85 pour un bon équilibre qualité/poids
  • JPEG : 80-90 pour les photos
  • PNG : Utilisé pour les images avec transparence

5. Picture element pour responsive

<picture>
    <!-- WebP pour les navigateurs modernes -->
    <source
        srcset="image.php?src=uploads/photo.jpg&w=320&format=webp 320w,
                image.php?src=uploads/photo.jpg&w=640&format=webp 640w,
                image.php?src=uploads/photo.jpg&w=1024&format=webp 1024w"
        sizes="(max-width: 640px) 100vw, 640px"
        type="image/webp">

    <!-- Fallback JPEG -->
    <img
        src="image.php?src=uploads/photo.jpg&w=640&format=jpg"
        srcset="image.php?src=uploads/photo.jpg&w=320&format=jpg 320w,
                image.php?src=uploads/photo.jpg&w=640&format=jpg 640w,
                image.php?src=uploads/photo.jpg&w=1024&format=jpg 1024w"
        sizes="(max-width: 640px) 100vw, 640px"
        alt="Description">
</picture>

💼 Cas d'Usage Pratiques

1. Galerie d'images responsive

<div class="gallery">
    <?php
    $images = glob('uploads/*.{jpg,jpeg,png}', GLOB_BRACE);
    foreach ($images as $image):
    ?>
    <a href="image.php?src=<?= $image ?>&w=1920&fit=contain"
       data-lightbox="gallery">
        <img src="image.php?src=<?= $image ?>&w=400&h=300&fit=cover&format=webp"
             alt="Photo"
             loading="lazy">
    </a>
    <?php endforeach; ?>
</div>

2. Avatar utilisateur

<!-- Petit avatar carré optimisé -->
<img src="image.php?src=<?= $user['avatar'] ?>&w=80&h=80&fit=cover&crop=center&format=webp&quality=75"
     alt="<?= $user['name'] ?>"
     class="avatar">

3. Image de couverture

<!-- Header avec image de fond optimisée -->
<header style="background-image: url('image.php?src=uploads/cover.jpg&w=1920&h=600&fit=cover&crop=center&format=webp&quality=80')">
    <h1>Mon Site</h1>
</header>

4. Placeholder avec flou

<!-- Technique du placeholder flou (LQIP) -->
<div class="image-container">
    <!-- Image très petite et floutée, chargée immédiatement -->
    <img src="image.php?src=uploads/photo.jpg&w=50&blur=20&quality=50"
         class="placeholder"
         style="filter: blur(20px); transform: scale(1.1);">

    <!-- Image finale, chargée en lazy -->
    <img src="image.php?src=uploads/photo.jpg&w=1200&format=webp"
         class="final-image"
         loading="lazy"
         onload="this.style.opacity=1">
</div>

<style>
.image-container {
    position: relative;
    overflow: hidden;
}
.placeholder {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.final-image {
    position: relative;
    opacity: 0;
    transition: opacity 0.3s;
}
</style>

5. Open Graph / Social Media

<!-- Meta tags pour les réseaux sociaux -->
<meta property="og:image" content="https://example.com/image.php?src=uploads/og-image.jpg&w=1200&h=630&fit=cover&format=jpg&quality=90">
<meta name="twitter:image" content="https://example.com/image.php?src=uploads/og-image.jpg&w=1200&h=600&fit=cover&format=jpg&quality=90">

🔌 Extensions Possibles

1. Watermark (filigrane)

// Ajouter dans la classe ImageProcessor

private function applyWatermark($image) {
    if (!isset($this->config['watermark'])) {
        return;
    }

    $watermarkPath = $this->config['watermark'];
    if (!file_exists($watermarkPath)) {
        return;
    }

    $watermark = imagecreatefrompng($watermarkPath);
    $wmWidth = imagesx($watermark);
    $wmHeight = imagesy($watermark);

    // Position (coin inférieur droit avec marge)
    $destX = imagesx($image) - $wmWidth - 10;
    $destY = imagesy($image) - $wmHeight - 10;

    // Appliquer avec transparence
    imagecopy($image, $watermark, $destX, $destY, 0, 0, $wmWidth, $wmHeight);
    imagedestroy($watermark);
}

// Utilisation :
// image.php?src=photo.jpg&watermark=watermark.png

2. Détection de visages (Face Detection)

// Nécessite l'extension php-facedetect
// Permet de centrer automatiquement sur les visages lors du crop

private function detectFaces() {
    $faces = face_detect($this->sourcePath);

    if (!empty($faces)) {
        // Calculer le centre des visages
        $centerX = 0;
        $centerY = 0;
        foreach ($faces as $face) {
            $centerX += $face['x'] + $face['w'] / 2;
            $centerY += $face['y'] + $face['h'] / 2;
        }
        $centerX /= count($faces);
        $centerY /= count($faces);

        return ['x' => $centerX, 'y' => $centerY];
    }

    return null;
}

3. Support AVIF

// AVIF est un format encore plus performant que WebP
// Nécessite PHP 8.1+ et l'extension GD compilée avec AVIF

private function getOutputFormat() {
    if ($this->config['format'] !== 'auto') {
        return $this->config['format'];
    }

    // Vérifier le support AVIF
    if (function_exists('imageavif')) {
        return 'avif';
    }

    if (function_exists('imagewebp')) {
        return 'webp';
    }

    // Fallback...
}

// Dans saveAndOutput :
case 'avif':
    imageavif($image, null, $quality);
    break;

4. Génération de sprites

// Combiner plusieurs images en un sprite CSS

class SpriteGenerator {
    public function generateSprite($images, $spacing = 0) {
        // Calculer les dimensions totales
        $totalWidth = 0;
        $maxHeight = 0;

        $imageResources = [];
        foreach ($images as $image) {
            $img = imagecreatefromjpeg($image);
            $imageResources[] = [
                'resource' => $img,
                'width' => imagesx($img),
                'height' => imagesy($img)
            ];
            $totalWidth += imagesx($img) + $spacing;
            $maxHeight = max($maxHeight, imagesy($img));
        }

        // Créer le sprite
        $sprite = imagecreatetruecolor($totalWidth, $maxHeight);

        $x = 0;
        foreach ($imageResources as $img) {
            imagecopy($sprite, $img['resource'], $x, 0, 0, 0, $img['width'], $img['height']);
            $x += $img['width'] + $spacing;
        }

        return $sprite;
    }
}

🔒 Sécurité Avancée

1. Validation stricte des chemins

// Ajouter dans le constructeur
private $allowedPaths = ['uploads/', 'images/', 'content/'];

private function validatePath($path) {
    // Nettoyer le chemin
    $path = realpath($path);

    if (!$path) {
        $this->error(404, "Fichier non trouvé");
    }

    // Vérifier que le chemin est dans un dossier autorisé
    $allowed = false;
    foreach ($this->allowedPaths as $allowedPath) {
        $fullAllowedPath = realpath($allowedPath);
        if (strpos($path, $fullAllowedPath) === 0) {
            $allowed = true;
            break;
        }
    }

    if (!$allowed) {
        $this->error(403, "Accès interdit");
    }

    return $path;
}

2. Rate Limiting

// Limiter le nombre de requêtes par IP

class RateLimiter {
    private $storage = 'cache/ratelimit/';
    private $limit = 100; // requêtes
    private $period = 3600; // par heure

    public function check($ip) {
        $file = $this->storage . md5($ip);

        if (!is_dir($this->storage)) {
            mkdir($this->storage, 0755, true);
        }

        $data = [];
        if (file_exists($file)) {
            $data = json_decode(file_get_contents($file), true);
        }

        // Nettoyer les anciennes entrées
        $now = time();
        $data = array_filter($data, function($timestamp) use ($now) {
            return $timestamp > $now - $this->period;
        });

        if (count($data) >= $this->limit) {
            return false;
        }

        $data[] = $now;
        file_put_contents($file, json_encode($data));

        return true;
    }
}

// Utilisation :
$rateLimiter = new RateLimiter();
if (!$rateLimiter->check($_SERVER['REMOTE_ADDR'])) {
    http_response_code(429);
    die(json_encode(['error' => 'Trop de requêtes']));
}

3. Signature d'URL

// Générer des URLs signées pour éviter les abus

class URLSigner {
    private $secret = 'YOUR_SECRET_KEY';

    public function sign($url, $expiration = 3600) {
        $expires = time() + $expiration;
        $signature = hash_hmac('sha256', $url . $expires, $this->secret);

        return $url . '&expires=' . $expires . '&signature=' . $signature;
    }

    public function verify($url, $signature, $expires) {
        if (time() > $expires) {
            return false;
        }

        $expected = hash_hmac('sha256', $url . $expires, $this->secret);
        return hash_equals($expected, $signature);
    }
}

// Génération :
$signer = new URLSigner();
$signedUrl = $signer->sign('image.php?src=uploads/photo.jpg&w=800');

// Vérification (dans image.php) :
if (isset($_GET['signature']) && isset($_GET['expires'])) {
    $url = $_SERVER['REQUEST_URI'];
    $url = preg_replace('/&(expires|signature)=[^&]+/', '', $url);

    if (!$signer->verify($url, $_GET['signature'], $_GET['expires'])) {
        http_response_code(403);
        die(json_encode(['error' => 'Signature invalide']));
    }
}

📊 Monitoring et Statistiques

// stats.php - Dashboard de statistiques

<?php
$cacheDir = 'cache/';
$files = glob($cacheDir . '*');

$totalSize = 0;
$totalFiles = 0;
$formatStats = [];

foreach ($files as $file) {
    if (is_file($file)) {
        $totalFiles++;
        $size = filesize($file);
        $totalSize += $size;

        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if (!isset($formatStats[$ext])) {
            $formatStats[$ext] = ['count' => 0, 'size' => 0];
        }
        $formatStats[$ext]['count']++;
        $formatStats[$ext]['size'] += $size;
    }
}

function formatBytes($bytes) {
    $units = ['B', 'KB', 'MB', 'GB'];
    $i = 0;
    while ($bytes >= 1024 && $i < 3) {
        $bytes /= 1024;
        $i++;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>Statistiques Cache Images</title>
    <style>
        body { font-family: Arial; padding: 20px; }
        .stat { background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 5px; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
    </style>
</head>
<body>
    <h1>📊 Statistiques du Cache d'Images</h1>

    <div class="stat">
        <strong>Total fichiers :</strong> <?= $totalFiles ?>
    </div>

    <div class="stat">
        <strong>Taille totale :</strong> <?= formatBytes($totalSize) ?>
    </div>

    <h2>Par format</h2>
    <table>
        <tr>
            <th>Format</th>
            <th>Nombre</th>
            <th>Taille</th>
            <th>% Total</th>
        </tr>
        <?php foreach ($formatStats as $format => $stats): ?>
        <tr>
            <td><?= strtoupper($format) ?></td>
            <td><?= $stats['count'] ?></td>
            <td><?= formatBytes($stats['size']) ?></td>
            <td><?= round($stats['size'] / $totalSize * 100, 2) ?>%</td>
        </tr>
        <?php endforeach; ?>
    </table>
</body>
</html>

✅ Conclusion

🎉 Vous disposez maintenant d'un système complet de traitement d'image !

Ce script PHP vous permet de :

  • ✨ Optimiser automatiquement vos images pour le web
  • 🎯 Redimensionner et recadrer à la volée
  • 🚀 Convertir en formats modernes (WebP, AVIF)
  • 🎨 Appliquer des filtres et transformations
  • ⚡ Bénéficier d'un cache performant
  • 🔒 Contrôler la sécurité et les accès

Ressources complémentaires

Prochaines étapes

  1. Installer le script sur votre serveur
  2. Configurer les chemins autorisés
  3. Tester les différents paramètres
  4. Configurer le cache et le nettoyage automatique
  5. Implémenter la sécurité (rate limiting, signatures)
  6. Monitorer les performances avec les statistiques