Présentation de la librairie Imagine pour la manipulation des images en PHP
Presentation
Imagine est une librairie PHP 5 orienté objet permettant de manipuler les images.
Elle nécéssite d'être au minimum en version 5.3 de PHP car elle utilise les namespaces. Développée à la base (je crois mais j'ai un petit doute) pour symfony 2, la librairie peut également s'utiliser avec symfony 1, voir sans framework (il faut seulement gérer l'autoload manuellement).
Son grand intérêt est de simplifier la manipulation des images, et surtout de s'affranchir des différents moteurs (GD, Imagick ou Gmagick).
Par contre, comme elle est compatible avec ces 3 moteurs, seules les manipulations communes aux 3 moteurs sont implémentées, ce qui est le plus gros défault d'Imagine. Mais pour l'utilisation courante cela est bien suffisant.
Je vous conseille de regarder cette présentation qui résume très bien l'esprit de la librairie: introduction-toimagine
Comme cette librairie est full objet, il est beaucoup plus facile de l'implémenter dans un projet objet ou ou dans un framework, de se créer des transformations d'images types et de les réutiliser.
Attention, la librairie utilise un système de levée d'exceptions en cas de problème. Détail à bien prendre en compte quand on l'utilise.
La documentation officiel est assez complète également, vous y trouverez aussi les différentes manières de récupérer la librairie: documentation
Utilisation basique
Lorsque l'on manipule des images en PHP, les étapes sont toujours sensiblement les mêmes:
- on instancie l'image (soit en la créant de toute pièce, soit le plus souvent en partant d'une image de base)
- on lui fait subir différents traitements (redimmensionnement ou crop, ajout d'un watermark...)
- on enregistre l'image modifiée au format souhaité (jpg, png...)
Et bien avec Imagine c'est pareil!
On commence par instancier un objet Imagine en sélectionnant le moteur souhaité (ici Gd)
$Imagine = new Imagine\Gd\Imagine();
Ensuite on crée un objet image (ici on ouvre une image existante):
$Image = $Imagine->open($path);
Pour les différents traitements sur l'image, Imagine considère qu'on applique des filtres, un esemble de filtres étant nommé une transformation.
(je détaillerai des exemples de filtre plus tard).
La récuperation de l'image modifiée peut se faire en écrivant l'image dans un fichier ou en retournant la source de l'image pour un affichage dans le navigateur.
#sauvegarde dans un fichier $Image->save($new_path); #obtenir la source de l'image echo $Image->get($format);
Lors de l'écriture de l'image dans un fichier, Imagine se base sur l'extension pour déterminer le format d'image (jpg, png, gif). Pour obtenir la source, il faut passer en paramètre à la méthode get() le format souhaité.
Création de classe de filtre
Comme la présentation l'indique, les classes de filtres d'Imagine doivent implémenter l'interface FilterInterface que voici:
interface FilterInterface { /** * Applies scheduled transformation to ImageInterface instance * Returns processed ImageInterface instance * * @param Imagine\Image\ImageInterface $image * * @return Imagine\Image\ImageInterface */ function apply(ImageInterface $image); }
Elle ne contient qu'une méthode apply() qui prend en paramètre un objet de type ImageInterface qu'il doit renvoyer (afin de pouvoir chainer les filtres).
L'exemple ReflectionFilter de la documentation indique que le constructeur d'une class de filtre prend en paramètre un objet Imagine.
On peut donc créer une classe de base de filtre de ce type:
abstract class BaseFilter implements Imagine\Filter\FilterInterface { protected $imagine; protected $config; public function __construct(Imagine\ImagineInterface $imagine, $conf) { $this->imagine = $imagine; $this->config = $conf; } }
J'ai volontairement ajouter un attribut à la classe par rapport à l'exemple, un tableau de configuration pour le filtre, afin de rendre les classes de filtres plus souples.
Voici donc un exemple de filtre pour ajouter un watermark en bas à gauche d'une image.
Le tableau de configuration contient le chemin vers l'image servant de watermark (en index 'source') et la distance en pixel en x et y du watermark par rapport au coin en bas à gauche de l'image:
class AddWatermarkFilter extends BaseFilter implements Imagine\Filter\FilterInterface { public function apply(Imagine\Image\ImageInterface $image) { //on essaye de créer une image Imagine à partir de la source fournie en config, sinon on renvoit l'image de départ try { $watermark = $this->imagine->open(sfConfig::get('sf_web_dir').$this->config['source']); } catch (Exception $e) { return $image; } //calcul des point x et y du watermark à partir de la taille de l'image de départ, de celle du watermark et de la configuration $pointX = $image->getSize()->getWidth() - $watermark->getSize()->getWidth() - $this->config['pointX']; $pointY = $image->getSize()->getHeight() - $watermark->getSize()->getHeight() - $this->config['pointY']; //on ajoute le watermark sur l'image de départ $image = $image->paste($watermark, new Imagine\Image\Point($pointX, $pointY)); return $image; } }
Bien sûr, cette classe peut être améliorée, par exemple en prévoyant un sytème dans le cas ou les paramètres de configuration sont absents.
Ainsi, on peut ajouter dans la class de base ces 2 méthodes:
/** * @param string $key * @param mixed $default */ protected function getConfig($key, $default = null) { return $this->hasConfig($key) ? $this->config[$key] : $default; } /** * @param string $key */ protected function hasConfig($key) { return isset($this->config[$key]); }
La méthode apply() devient:
public function apply($image) { //on essaye de créer une image Imagine à partir de la source fournie en config, sinon on renvoit l'image de départ try { $watermark = $this->imagine->open(sfConfig::get('sf_web_dir').$this->getConfig('source')); } catch (Exception $e) { return $image; } //calcul des point x et y du watermark à partir de la taille de l'image de départ, de celle du watermark et de la configuration $pointX = $image->getSize()->getWidth() - $watermark->getSize()->getWidth() - $this->getConfig('pointX', 5); $pointY = $image->getSize()->getHeight() - $watermark->getSize()->getHeight() - $this->getConfig('pointY', 5); //on ajoute le watermark sur l'image de départ $image = $image->paste($watermark, new Imagine\Image\Point($pointX, $pointY)); return $image; }
Exemple de transformation
Dans cet exemple, à partir d'une image fournie, je veux créer 2 images:
- une petite pour une galerie
- une grande avec un watermark
(remarque: j'avais utilisé Symfony 1.4 pour faire ce code, certaines parties, comme la récupération du fichier de configuration sont spécifique au framework)
J'ai un fichier de configuration en yml qui ressemble à ceci:
images: formats: mini: folder: mini filters: resize: largeSide: 80 smallSide: 80 big: folder: big filters: resize: largeSide: 600 smallSide: 400 addWatermark: source: /images/imagine/watermark.png pointX: 10 pointY: 10
Et voici le code qui, à partir de l'image source, crée 2 images et les enregistre. J'ai volontairement simplifié le code et je l'ai présenté de façon un peu procédurale pour l'article:
//Initialisation Imagine $Imagine = new Imagine\Gd\Imagine(); //Instanciation image de départ $Image = $this->oImagine->open($path); //récuparation de la config sous forme d'un tableau // array('mini" => array('folder' => 'mini', 'filters' => array()...) $conf = sfConfig::get('app_images_formats'); //pour chaque format foreach ($conf as $format) { $new_image = $Image->copy(); foreach ($format['filters'] as $filter => $config) { $filterClass = ucfirst($filter).'Filter'; $Filter = new $filterClass($Imagine, $config); $new_image = $oFilter->apply($new_image); } $new_image->save(sfConfig::get('app_upload_dir').'/'.$format['folder'].'/'.$name); }
Nous avons donc 2 images, ayant le même nom mais enregistrées dans des dossiers différents:
- celle enregistré dans le dossier mini a une taille de 80x80
- celle enregistré dans le dossier big a une taille de 600x400 et a un watermark à 10 pixels du coin gauche de l'image.
Pour être complet, voici la class de resize:
class ResizeFilter extends BaseFilter implements Imagine\Filter\FilterInterface { public function apply($image) { //config largeur et hauteur souhaitées if ($image->getSize()->getWidth() > $image->getSize()->getHeight()) { //paysage $width = $this->getConfig('largeSide', 100); $heigth = $this->getConfig('smallSide', 100); } else { //portrait $width = $this->getConfig('smallSide', 100); $heigth = $this->getConfig('largeSide', 100); } //crop si modif de rapport $oldRatio = $image->getSize()->getWidth()/$image->getSize()->getHeight(); $newRatio = $width/$heigth; if ($oldRatio != $newRatio) { $diffWidth = $image->getSize()->getWidth() - ($image->getSize()->getHeight()*$newRatio); if ($diffWidth > 0) { $image= $image->crop(new Imagine\Image\Point((int)($diffWidth / 2), 0), new Imagine\Image\Box($image->getSize()->getHeight()*$newRatio, $image->getSize()->getHeight())); } else { $ratio = $heigth/$width; $diffHeight = $poImage->getSize()->getHeight() - ($image->getSize()->getWidth()*$ratio); $image= $image->crop(new Imagine\Image\Point(0, $diffHeight / 2), new Imagine\Image\Box($image->getSize()->getWidth(), $image->getSize()->getWidth()*$ratio)); } } //resize $image= $image->resize(new Imagine\Image\Box($width, $heigth)); return $image; } }
Changer de moteur d'image
Il est assez simple de changer le moteur d'image utilisé avec Imagine en fonction de la configuration du serveur. Il suffit de modifier l'instaciation de la librairie, l'utilisation du design pattern factory ou Injection de dépendance rendrait cette tache très simple.
#utilisation de GD $Imagine = new Imagine\Gd\Imagine(); #utilisiation de Imagick $Imagine = new Imagine\Imagick\Imagine();
Cette permutation ne pose aucun problème car Imagine se limite aux fonctions communes des librairies qu'elle utilise.
Conclusion
J'espère que cet article vous a permis de découvrir cette librairie très pratique et vous a donné envie de l'utiliser. En tout cas, moi, elle m'a réconcilié avec la manipulation d'images qui est vraiment très fastidieux avec GD.
Réponse de Ulrich le 30 sept. 2012