Développeur web
PHP, JS, Docker, Elasticsearch, ...
Photo by Michael Maasen on Unsplash
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
…
Photo by Hal Gatewood on Unsplash
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;
}
Au parsing du DOM, le navigateur connaît :
mais, le CSS n'étant pas encore chargé, il ne connaît pas :
50vw
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
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>
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.
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
]);
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">
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%']
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
/**
* @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') }}
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é
SaaS de stockage et d'optimisation d'images
# config/packages/imagine.yaml
liip_imagine:
driver: rokka
cache: rokka
Permet de se décharger du stockage et du traitement des images
Solution open-source de gestion d'images écrite en Python
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"]
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') }}
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 ?
...
Photo by Jonathan Pielmayer on Unsplash