Des images au cordeau pour vos applications Symfony
Mathieu
Santostefano
Développeur web
PHP, JS, Docker, Elasticsearch, ...
Images responsives
⏲
🗜
😀
🌿
Photo by Michael Maasen on Unsplash
Device Pixel Ratio
Rapport entre pixels physiques (écran)
et pixels logiques (CSS)
1 par défaut
2 pour les écrans de type iPhone 6s
3 pour les écrans de type iPhone 6s +
4 pour les écrans de type Samsung Galaxy S8
DPR = 1 : 1 pixel CSS = 1 pixel d'écran
DPR = 2 : 1 pixel CSS = 2 pixels d'écran
DPR = 3 : 1 pixel CSS = 3 pixels d'écran
…
Device Pixel Ratio
Photo by Hal Gatewood on Unsplash
🤨
Appareils de test
Smartphone (iPhone 6,7,8) | 750x1334 | DPR 2 |
Smartphone (Samsung Galaxy S6, S7) | 1440x2560 | DPR 4 |
Desktop (Laptop 13 pouces) | 1920x1080 | DPR 1 |
Desktop (MacBook Pro Retina) | 1440x900 | DPR 2 |
Photo by chuttersnap on Unsplash
Photo by Wolfgang Hasselmann on Unsplash
1920px
1276px
<img src="elephant_1920.jpg"
alt="Éléphant"
width="1920"
height="1276">
img {
max-width: 100%;
height: auto;
}
Image fluide 🥳
mais lourde
😓
Chargement des images
- Le navigateur charge le DOM
- Il le parse
- Il lance les requêtes pour charger les images, les CSS et les scripts JS, ...
Chargement des images
Au parsing du DOM, le navigateur connaît :
- le DPR de l'appareil
- les dimensions du viewport
- la qualité du réseau
mais, le CSS n'étant pas encore chargé, il ne connaît pas :
- les dimensions des zones de rendu des balises <img>
Responsive
=
Breakpoints
Maquette
50vw
Maquette
100vw
Utilitaire CLI pour générer des breakpoints d'image à partir de données analytics
Source : https://caniuse.com/#search=srcset
L'attribut sizes indique au navigateur quelle largeur (en vw ou en pixels CSS) fera la balise <img> selon la largeur du viewport (en pixels physiques)
On utilise ici la syntaxe des media-queries CSS.
💡
<img src="elephant_1920.jpg" alt="Éléphant"
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="
elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w"
>
L'attribut srcset indique au navigateur quelles sources d'image sont disponibles, ainsi que leurs largeurs, en pixels physiques (descripteur w).
Ainsi le navigateur peut choisir l'image la plus adaptée.
<img src="elephant_1920.jpg" alt="Éléphant"
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="
elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w"
>
Pas besoin de spécifier le DPR ici, le navigateur le connait, et il saura choisir la bonne image :
Viewport == 1440px <img> = 50vw
DPR = 1 elephant_1166.jpg
DPR = 2 elephant_1585.jpg
(50% de 1440) = 720
(50% de (1440 x 2)) = 1440
<img src="elephant_1920.jpg" alt="Éléphant"
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="
elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w"
>
L'attribut srcset peut aussi être utilisé pour gérer les DPR. Ici, un avatar d'une largeur fixe en pixels CSS, sur tous les écrans.
On indique au navigateur les différentes sources disponibles selon le DPR actuel.
<img src="avatar.jpg" alt=""
width="50" height="50"
srcset="avatar.jpg 1x,
avatar_2x.jpg 2x,
avatar_3x.jpg 3x
avatar_4x.jpg 4x">
srcset et sizes ne sont que des indications pour le navigateur. Il n'est pas obligé de les suivre.
Sur un smartphone avec un DPR de 4, avec peu de réseau, le navigateur peut forcer le chargement de l'image la plus légère
💡
L'ordre des sources dans srcset n'a aucune importance
L'ordre des media-queries dans sizes a une importance
Source : https://caniuse.com/#search=picture
L'attribut srcset sert également à servir différents formats d'image tout en gardant une compatibilité pour les navigateurs ne supportant pas les formats comme webp.
<picture>
<source srcset="elephant.webp"
type="image/webp">
<img src="elephant.jpg"
alt="Éléphant">
</picture>
<picture>
<source media="(max-width: 767px)"
sizes="(max-width: 1534px) 100vw, 1534px"
srcset="elephant_750.jpg 750w,
elephant_1131.jpg 1131w,
elephant_1415.jpg 1415w,
elephant_1534.jpg 1534w">
<source media="(min-width: 768px) and (max-width: 1199px)"
sizes="(max-width: 2400px) 50vw, 1200px"
srcset="elephant_384.jpg 384w,
elephant_785.jpg 785w,
elephant_1026.jpg 1026w,
elephant_1200.jpg 1200w">
<img src="elephant_1920.jpg" alt=""
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w">
</picture>
<picture>
<source media="(max-width: 767px)"
sizes="(max-width: 1534px) 100vw, 1534px"
srcset="elephant_750.jpg 750w,
elephant_1131.jpg 1131w,
elephant_1415.jpg 1415w,
elephant_1534.jpg 1534w">
<source media="(min-width: 768px) and (max-width: 1199px)"
sizes="(max-width: 2400px) 50vw, 1200px"
srcset="elephant_384.jpg 384w,
elephant_785.jpg 785w,
elephant_1026.jpg 1026w,
elephant_1200.jpg 1200w">
<img src="elephant_1920.jpg" alt=""
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w">
</picture>
<picture>
<source media="(max-width: 767px)"
sizes="(max-width: 1534px) 100vw, 1534px"
srcset="elephant_750.jpg 750w,
elephant_1131.jpg 1131w,
elephant_1415.jpg 1415w,
elephant_1534.jpg 1534w">
<source media="(min-width: 768px) and (max-width: 1199px)"
sizes="(max-width: 2400px) 50vw, 1200px"
srcset="elephant_384.jpg 384w,
elephant_785.jpg 785w,
elephant_1026.jpg 1026w,
elephant_1200.jpg 1200w">
<img src="elephant_1920.jpg" alt=""
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w">
</picture>
<picture>
<source media="(max-width: 767px)"
sizes="(max-width: 1534px) 100vw, 1534px"
srcset="elephant_750.jpg 750w,
elephant_1131.jpg 1131w,
elephant_1415.jpg 1415w,
elephant_1534.jpg 1534w">
<source media="(min-width: 768px) and (max-width: 1199px)"
sizes="(max-width: 2400px) 50vw, 1200px"
srcset="elephant_384.jpg 384w,
elephant_785.jpg 785w,
elephant_1026.jpg 1026w,
elephant_1200.jpg 1200w">
<img src="elephant_1920.jpg" alt=""
sizes="(max-width: 3840px) 50vw, 1920px"
srcset="elephant_600.jpg 600w,
elephant_1166.jpg 1166w,
elephant_1585.jpg 1585w,
elephant_1920.jpg 1920w">
</picture>
💡
L'ordre des <source> a une importance, le premier attribut media vérifié est utilisé
<img srcset="..." sizes="...">
!=
<picture> & <source>
srcset est une indication, le navigateur peut ne pas la respecter.
<picture> <source media="..."> la media-query est toujours testée, et respectée par le navigateur si elle est valide.
Générer différentes versions d'une
même image
Glide by The PHP League
Glide by The PHP League
Bibliothèque PHP de manipulation d'images, basée sur Intervention Image et Flysystem
<?php
$server = League\Glide\ServerFactory::create([
'source' => 'path/to/source/folder',
'cache' => 'path/to/cache/folder',
]);
$response = $server->getImageResponse('users/1.jpg', [
'w' => 600,
'h' => 400
]);
Glide by The PHP League
- Support d'ImageMagick et de gd
- Intégration de Flysystem
- Gestion des watermarks
- Mise en cache des images générées
- API complète de gestion d'image
- crop
- resize
- orientation
- format
- dpr (device pixel ratio)
- qualité
- ...
Glide by The PHP League
Sécurité par signature des URLs avec une clé privée
Prévient les attaques de type "mass image-resize"
<?php
use League\Glide\Urls\UrlBuilderFactory;
$signkey = 'v-LK4WCdhcfcc%jt*VC2cj%nVpu+xQKvLUA%H86kRVk_4bgG8&CWM#k';
$urlBuilder = UrlBuilderFactory::create('/img/', $signkey);
$url = $urlBuilder->getUrl('cat.jpg', ['w' => 500]);
echo '<img src="'.$url.'">';
// <img src="/img/cat.jpg?w=500&s=af3dc18fc6bfb2afb521e587c348b904">
Glide-symfony
Ajoute le support de
HttpFoundation\StreamedResponse
Définition de presets
parameters:
media_filters:
article_384: { w: 384, h: 255 }
article_600: { w: 600, h: 399 }
article_750: { w: 750, h: 498 }
article_785: { w: 785, h: 522 }
article_1026: { w: 1026, h: 682 }
article_1131: { w: 1131, h: 751 }
article_1166: { w: 1166, h: 775 }
article_1200: { w: 1200, h: 798 }
article_1415: { w: 1415, h: 940 }
article_1534: { w: 1534, h: 1019 }
article_1585: { w: 1585, h: 1053 }
article_1920: { w: 1920, h: 1276 }
services:
App\Service\Glide:
class: App\Service\Glide
arguments: ['%glide_config%', '%glide_media_filters%']
Glide-symfony
Glide-symfony
class Glide
{
protected $server;
protected $filters;
public function __construct(array $serverConfig, array $filters)
{
$this->server = ServerFactory::create([
'response' => new SymfonyResponseFactory(),
'source' => $serverConfig['source_path'],
'cache' => $serverConfig['cache_path'],
]);
$this->filters = $filters;
}
public function getServer(): Server
{
return $this->server;
}
public function getFilters(): array
{
return $this->filters;
}
}
Utilisation dans un controller
Glide-symfony
/**
* @Route("/glide/{filterName}/{imageName}", name="glide")
*/
public function index(Glide $glide, string $filterName, string $imageName)
{
$glideServer = $glide->getServer();
$filter = $glide->getFilters()[$filterName] ?? [];
return $glideServer->getImageResponse($imageName, $filter);
}
{% macro picture(image) %}
<picture class="media-object">
<source media="(max-width: 767px)"
sizes="(max-width: 1534px) 100vw, 1534px"
srcset="
{{ path('glide', { filterName: 'article_750', imageName: image }) }} 750w,
{{ path('glide', { filterName: 'article_1131', imageName: image }) }} 1131w,
{{ path('glide', { filterName: 'article_1415', imageName: image }) }} 1415w,
{{ path('glide', { filterName: 'article_1534', imageName: image }) }} 1534w">
<source media="(min-width: 768px) and (max-width: 1199px)"
sizes="(max-width: 2400px) 50vw, 1200px"
srcset="
{{ path('glide', { filterName: 'article_384', imageName: image }) }} 384w,
{{ path('glide', { filterName: 'article_785', imageName: image }) }} 785w,
{{ path('glide', { filterName: 'article_1026', imageName: image }) }} 1026w,
{{ path('glide', { filterName: 'article_1200', imageName: image }) }} 1200w">
<img id="test" sizes="(max-width: 3840px) 50vw, 1920px"
srcset="
{{ path('glide', { filterName: 'article_600', imageName: image }) }} 600w,
{{ path('glide', { filterName: 'article_1166', imageName: image }) }} 1166w,
{{ path('glide', { filterName: 'article_1585', imageName: image }) }} 1585w,
{{ path('glide', { filterName: 'article_1920', imageName: image }) }} 1920w"
src="{{ path('glide', { filterName: 'article_1920', imageName: image }) }}"
alt="">
</picture>
{% endmacro %}
{% import _self as macros %}
{{ macros.picture('elephant.jpg') }}
LiipImagineBundle
LiipImagineBundle
Bundle Symfony de manipulation d'images
exposé via une API HTTP.
Basé sur la bibliothèque PHP Imagine
Mêmes fonctionnalités que Glide, mais plus fortement lié à Symfony (routing pré-défini, service déjà déclaré, ...)
Principal inconvénient : pas de gestion native de la sécurité
LiipImagineBundle
Liip édite Rokka.io
SaaS de stockage et d'optimisation d'images
Rokka.io
- Client PHP
- Bundle Symfony
- Intégration à LiipImagineBundle comme driver
# config/packages/imagine.yaml
liip_imagine:
driver: rokka
cache: rokka
Permet de se décharger du stockage et du traitement des images
En utilisant un service externe
(auto-hébergé ou en SaaS)
Solution open-source de gestion d'images écrite en Python
- API complète de gestion d'image
- Gestion de la sécurité (signature des urls)
- Mise en cache des images générées
- Intégration à Symfony via jbouzekri/PhumborBundle
- Service de générations d'URL
- Configuration des transformations d'images
- Fonction Twig
- Support de différents drivers de stockage (local, S3, ...)
Fonctionnalité intéressante : smart-cropping
Attention, c'est pratique mais pas fiable à 100%
Souvent, la direction artistique voudra avoir la main sur le cropping
docker run -p 8000:8000 \
-e DETECTORS="[ \
'thumbor.detectors.glasses_detector', \
'thumbor.detectors.face_detector', \
'thumbor.detectors.feature_detector', \
'thumbor.detectors.profile_detector' \
]" apsl/thumbor
ALLOWED_SOURCES = ["https://jolicode.com"]
Sécurité
SECURITY_KEY = 'MY_SECURE_KEY'
http://localhost:8000/[HASH]/300x200/media/image.jpg
jbouzekri/PhumborBundle
jb_phumbor:
server:
url: http://your-thumbor-instance
secret: yourThumborSecretKey
transformations:
article_750:
fit_in: { width: 750, height: 498 }
{{ thumbor(asset('images/test.jpg'), 'article_750') }}
Quelle solution choisir ?
Images servies par Symfony ou par un service tiers ?
Quantité d'images à traiter ?
Quantité d'appareils différents à prendre en charge ?
Contraintes de la Direction Artistique ?
Budget mensuel pour un SaaS ?
...
Merci !
Photo by Jonathan Pielmayer on Unsplash
Questions ?
Des images au cordeau pour vos applications Symfony
By Mathieu Santostefano
Des images au cordeau pour vos applications Symfony
Presentation for Symfony Live Paris 2019
- 4,414