📖 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
- Installer le script sur votre serveur
- Configurer les chemins autorisés
- Tester les différents paramètres
- Configurer le cache et le nettoyage automatique
- Implémenter la sécurité (rate limiting, signatures)
- Monitorer les performances avec les statistiques