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 sa petite image avant upload ? Redimensio-quoi ? Ok, notre bon utilisateur qui balance une image de 8000px * 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 serveur, vous ne frustrez pas celui qui sait (mais qui a la flemme d’ouvrir son Photoshop), et puis 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.

préférences buzeo

Panneau des préférences de Buzeo

Comment ça marche ?

Tout navigateur un tant soit peu moderne a tout ce qu’il faut. Voici comment on va procéder :

  1. on récupère une image directement depuis un <input type="file">,
  2. on convertit l’image en HTMLImageElement afin de pouvoir l’utiliser avec canvas
  3. on « affiche » l’image dans le canvas aux dimensions voulues (le canvas ne sera même pas inséré dans le DOM, donc invisible),
  4. on retourne l’image en base64 qui pourra être directement fournie en tant que src d’une balise img 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 afin d’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

Bous 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(null, 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 le code complet sur Github avec toutes les informations pour l’installer. Avec bower, c’est même aussi simple que :

bower install resizeimage --save

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.

Il n'y a pas encore de commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *