Compresser les images dans le navigateur
Uploader une image de profil, vous avez sûrement déjà vécu cette situation. Cette petite image qui ne fera au mieux que 250px * 250px. Mettez vous dans la peau de mamie du cantal, est-ce que vous croyez qu’elle va redimensionner son image avant upload ? Notre utilisateur qui balance une image de 8000px x 6000px va devoir patienter un bon moment le temps d’envoyer tout ça avant que, enfin, le serveur optimise cette dernière.
Mais pourquoi ne pas faire tout ça directement dans le navigateur ?! On y gagne en tous points. L’utilisateur ne perd pas son temps à uploader xMb pour rien, vous économisez de la puissance et de la bande passante serveur et vous œuvrez pour la planète ! 5 bonnes raisons de faire ça dans le browser. On y va ?
C’est en programmant l’interface des préférences de Buzeo que je me suis fait la réflexion. La petite image de profil fait 60px * 60px, autant la mettre directement au bon format.

Comment ça marche ?
Tout navigateur un tant soit peu moderne a tout ce qu’il faut. Voici comment on va procéder :
- on récupère une image directement depuis un
<input type="file">
, - on convertit l’image en
HTMLImageElement
afin de pouvoir l’utiliser aveccanvas
, - on “affiche” l’image dans le canvas aux dimensions voulues (le canvas ne sera même pas inséré dans le DOM, donc invisible),
- on retourne l’image en base64 qui pourra être directement fournie en tant que
src
d’une baliseimg
et envoyée au serveur.
Rien d’extrêmement complexe. J’ai simplement omis dans la liste ci-dessus un petit détail technique. Redimensionner une image implique de conserver son ratio si on ne veut pas qu’elle se déforme. On va donc passer par une étape intermédiaire afin d’obtenir les dimensions réelles de l’image pour en calculer le ratio.
Let’s code!
Pour plus de clarté, nous allons organiser le code en différentes fonctions. Cela nous permet aussi de découper le processus en étapes logiques.
Convertir l’image en base64
Commençons par créer la fonction permettant de convertir l’image en base64.
function convertFileToB64(file, callback) {
// on créé un object fileReader.
// Cela permet de lire le contenu des fichiers
const reader = new FileReader();
// dès que le fichier est chargé, on envoie le résultat
reader.addEventListener('load', () => {
callback(reader.result);
}, false);
// on précise que l'on veut se fichier sous la forme d'une dataURL (base64)
reader.readAsDataURL(file);
}
Cette fonction prend en entrée un fichier de type file, tel que récupéré depuis un champ de sélection de fichier. Comme dans l’exemple ci-dessous.
// on imagine que notre champ a l'id="upload-image"
const inputFile = document.getElementById('upload-image');
// il faut écouter les changements pour savoir quand un fichier est sélectionné
inputFile.onchange = function(event) {
const img = inputFile.files[0];
}
Obtenir un objet image exploitable
Dans un second temps, on va créer la fonction permettant d’obtenir la résolution de l’image en pixels. Par ailleurs, canvas
ne comprend pas le base64, on va donc le nourrir d’HTMLImageElement
.
function getImage(b64img, callback) {
// créé un objet HTMLImageElement vide
const imgObj = new Image();
// dès que l'image est chargée, on appelle le callback
imgObj.onload = () => {
callback(imgObj);
};
// on lui donne l'image source
imgObj.src = b64img;
}
Cette fonction prend en entrée une image encodée en base64, ça tombe bien, nous avons déjà créé la fonction qui nous permet de faire ça ! Elle renvoie dans le callback un objet de type HTMLImageElement
depuis lequel nous pourrons accéder aux propriétés de l’image.
Redimensionnement
Nous avons maintenant ce qu’il nous faut pour attaquer le redimensionnement, on passe aux choses sérieuses ! Histoire d’avoir une fonction un peu souple, on proposera deux formats de sortie : le jpeg et le png. On donnera également la possibilité de forcer les dimensions, c’est à dire que le ratio d’origine sera ignoré et que l’image sera redimensionnée précisément aux valeurs données en paramètres.
Notre fonction acceptera les paramètres suivants :
img
: c’est un objet image tel que récupéré depuis le champ<input type="file">
options
:- {string} outputFormat – (jpe?g ou png)
- {string} targetWidth – largeur désirée
- {string} targetHeight – hauteur désirée
- {bool} forceRatio – s’il est à true, on force les dimensions même si cela déforme l’image
callback
function resize(img, options, callback) {
// on s'assure que l'image est bien en jpeg ou png
// on se sert ici d'une REGEX pour plus de concision
if (!/(jpe?g|png)$/i.test(img.type)) {
callback(new TypeError('image must be either jpeg or png'));
return;
}
// on vérifie que le format de sortie est supporté (jpeg, jpg ou png)
if (options.outputFormat !== 'jpg' && options.outputFormat !== 'jpeg' && options.outputFormat !== 'png') {
callback(new Error('outputFormat must be either jpe?g or png'));
return;
}
// on définit le format
const output = (options.outputFormat === 'png') ? 'png' : 'jpeg';
// on appelle nos petites fonctions bien pratiques ;)
convertFileToB64(img, (b64img) => {
getImage(b64img, (imgObj) => {
// prépare le canvas et on récupère les dimensions de l'image
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const imgWidth = imgObj.width;
const imgHeight = imgObj.height;
let width;
let height;
// calcul du ratio
// on part du principe que l'image ne doit pas dépasser les dimensions fournies
// donc si la longueur et la hauteur ne correspondent pas au ratio
// on ajuste l'image pour conserver le ratio tout en ne dépassant par la largeur ou hauteur
if (!options.forceRatio) {
if (imgWidth > imgHeight) {
width = options.targetWidth;
height = Math.round((imgHeight / imgWidth) * width);
}
else {
height = options.targetHeight;
width = Math.round((imgWidth / imgHeight) * height);
}
}
// pas de calcul du ratio si l'option forceRatio est à true
else {
width = options.targetWidth;
height = options.targetHeight;
}
// on donne ses dimensions au canvas
canvas.width = width;
canvas.height = height;
// on dessine l'image
context.drawImage(imgObj, 0, 0, width, height);
// on exporte l'image au format souhaité en base64
// directement dans le callback
callback(canvas.toDataURL(`image/${output}`));
});
});
}
Et voilà, vous voyez qu’une tâche en apparence assez complexe s’accomplit finalement en trois fonctions et une petite centaine de lignes de code. Je crois que la puissance des API js des navigateurs n’est plus à démontrer.
Pour faire les choses proprement, le mieux est d’encapsuler ce code dans un module pour ne pas polluer l’espace global avec des fonctions inutiles (elles ne servent que dans le cadre de resize
).
Vous trouverez un code un peu plus complet sur GitHub, lequel utilise les modules ES6. Vous pouvez l’installer avec npm -i smart-img-resize
et l’utiliser avec des outils de build moderne tels que Rollup, Webpack ou Browserify.
De plus, dans la version GitHub, j’ai ajouté la possibilité de recadrer de manière intelligente. Par exemple, si le format de destination est carré mais que la photo source est au format portrait (ou paysage), l’image sera automatiquement croppée sur le visage !
Vos visiteurs se sentent déjà mieux et vous remercient !
Notez que si vous devez supportez des navigateurs plus anciens, il faudrait mettre en place quelques tests pour s’assurer que canvas
, Image()
et Reader()
sont supportés. Notez également que ce code est écrit en ES6, tous les navigateurs ne le comprennent pas. Pensez donc à transpiler le code en ES5 au besoin. Si vous n’avez rien en place pour automatiser ce processus, vous pouvez le faire manuellement en ligne.
Commentaires
Rejoignez la discussion !